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.
Files changed (54) hide show
  1. package/package.json +6 -2
  2. package/packages/harness/package.json +38 -0
  3. package/packages/harness/src/approve.mjs +247 -0
  4. package/packages/harness/src/arch-doc.mjs +69 -0
  5. package/packages/harness/src/comment.mjs +76 -0
  6. package/packages/harness/src/lib/a11y.mjs +113 -0
  7. package/packages/harness/src/lib/affected-tests.mjs +147 -0
  8. package/packages/harness/src/lib/approvals.mjs +1403 -0
  9. package/packages/harness/src/lib/arch-doc.mjs +451 -0
  10. package/packages/harness/src/lib/audit-cadence.mjs +290 -0
  11. package/packages/harness/src/lib/comments.mjs +252 -0
  12. package/packages/harness/src/lib/component-stories.mjs +183 -0
  13. package/packages/harness/src/lib/determinism.mjs +179 -0
  14. package/packages/harness/src/lib/device-lease.mjs +249 -0
  15. package/packages/harness/src/lib/evidence-badge.mjs +158 -0
  16. package/packages/harness/src/lib/evidence-level.mjs +117 -0
  17. package/packages/harness/src/lib/feature-brief.mjs +324 -0
  18. package/packages/harness/src/lib/flight-recorder.mjs +332 -0
  19. package/packages/harness/src/lib/harness-lock.mjs +147 -0
  20. package/packages/harness/src/lib/harness-region.mjs +159 -0
  21. package/packages/harness/src/lib/inputs-hash.mjs +194 -0
  22. package/packages/harness/src/lib/reachability.mjs +211 -0
  23. package/packages/harness/src/lib/receipt-validate.mjs +234 -0
  24. package/packages/harness/src/lib/render.mjs +254 -0
  25. package/packages/harness/src/lib/spec-coverage.mjs +131 -0
  26. package/packages/harness/src/lib/step-cache.mjs +221 -0
  27. package/packages/harness/src/lib/token-drift.mjs +94 -0
  28. package/packages/harness/src/lib/tree.mjs +108 -0
  29. package/packages/harness/src/preview-gallery.mjs +122 -0
  30. package/packages/harness/src/receipt-check.mjs +96 -0
  31. package/packages/harness/src/record-audit.mjs +83 -0
  32. package/packages/harness/src/refusal-demo.mjs +498 -0
  33. package/packages/harness/src/retrospective.mjs +51 -0
  34. package/packages/harness/src/scaffold-feature.mjs +723 -0
  35. package/packages/harness/src/setup-hooks.mjs +33 -0
  36. package/packages/harness/src/verify.mjs +1709 -0
  37. package/packages/harness/src/walkthrough.mjs +499 -0
  38. package/packages/harness/src/watch.mjs +622 -0
  39. package/packages/receipts/package.json +36 -0
  40. package/packages/receipts/src/index.mjs +16 -0
  41. package/packages/receipts/src/inputs-hash.mjs +194 -0
  42. package/packages/receipts/src/receipt-validate.mjs +234 -0
  43. package/src/commands/upgrade.mjs +96 -0
  44. package/src/lib/harness-upgrade.mjs +159 -2
  45. package/src/scaffold.mjs +60 -1
  46. package/template/AGENTS.md +5 -0
  47. package/template/CLAUDE.md +30 -0
  48. package/template/gitignore +8 -0
  49. package/template/qa/lib/harness-lock.mjs +147 -0
  50. package/template/qa/lib/harness-region.mjs +159 -0
  51. package/template/qa/lib/inputs-hash.mjs +1 -1
  52. package/template/qa/lib/receipt-validate.mjs +1 -1
  53. package/template/qa/preview-gallery.mjs +17 -2
  54. package/template/qa/verify.mjs +95 -1
@@ -8,7 +8,7 @@
8
8
  // create-cmp repo (the `cmp-receipts` package). The copy in a generated
9
9
  // project's qa/lib/ is vendored byte-identical at scaffold time and pinned by
10
10
  // test/receipts-parity.test.mjs — edit the package source, then run
11
- // `node scripts/sync-receipts.mjs`.
11
+ // `node scripts/sync-harness.mjs`.
12
12
  //
13
13
  // See docs/adr/0005-evidence-binding-by-inputs-hash.md for the why.
14
14
 
@@ -9,7 +9,7 @@
9
9
  // create-cmp repo (the `cmp-receipts` package). The copy in a generated
10
10
  // project's qa/lib/ is vendored byte-identical at scaffold time and pinned by
11
11
  // test/receipts-parity.test.mjs — edit the package source, then run
12
- // `node scripts/sync-receipts.mjs`.
12
+ // `node scripts/sync-harness.mjs`.
13
13
  //
14
14
  // See docs/adr/0005-evidence-binding-by-inputs-hash.md for the why.
15
15
 
@@ -23,6 +23,21 @@ const previewsDir = resolve(
23
23
  const { renderTreeSvg } = await import(new URL("./lib/render.mjs", import.meta.url));
24
24
  const { auditA11y } = await import(new URL("./lib/a11y.mjs", import.meta.url));
25
25
 
26
+ // The app's display name is read at RUNTIME from create-cmp.json, never stamped in.
27
+ // Every .mjs under qa/ is machine-owned harness code copied byte-identical from the
28
+ // engine — token substitution must not touch it (see qa/lib/harness-region.mjs), so
29
+ // anything app-specific is looked up from the record that already holds that truth.
30
+ function appName() {
31
+ try {
32
+ const rec = JSON.parse(readFileSync(join(HERE, "..", "create-cmp.json"), "utf8"));
33
+ if (typeof rec.name === "string" && rec.name.trim()) return rec.name;
34
+ } catch {
35
+ // Not stamped, or an unreadable record — the gallery is a report, not a gate.
36
+ }
37
+ return "App";
38
+ }
39
+ const APP_NAME = appName();
40
+
26
41
  const manifest = JSON.parse(readFileSync(join(previewsDir, "manifest.json"), "utf8"));
27
42
  const { width, height, pngScale } = manifest.viewport;
28
43
 
@@ -57,7 +72,7 @@ const esc = (s) => String(s).replace(/&/g, "&amp;").replace(/</g, "&lt;").replac
57
72
 
58
73
  const html = `<!doctype html>
59
74
  <meta charset="utf-8">
60
- <title>__APP_NAME__ — screen previews (headless)</title>
75
+ <title>${esc(APP_NAME)} — screen previews (headless)</title>
61
76
  <style>
62
77
  :root { color-scheme: light; }
63
78
  body { font-family: -apple-system, system-ui, sans-serif; margin: 0; background: #F7F9FC; color: #1A1A1A; }
@@ -77,7 +92,7 @@ const html = `<!doctype html>
77
92
  .lbl { font-size: 10px; letter-spacing: .06em; text-transform: uppercase; color: #9CA3AF; margin: 0 0 4px; }
78
93
  </style>
79
94
  <header>
80
- <h1>__APP_NAME__ — screen previews</h1>
95
+ <h1>${esc(APP_NAME)} — screen previews</h1>
81
96
  <p>Rendered headlessly (no device/emulator) by <code>:composeApp:renderScreens</code> —
82
97
  ${width}×${height}dp, PNG @${pngScale}x · pixels for humans, wireframe+tree for the AI ·
83
98
  regenerate: <code>./gradlew :composeApp:renderScreens && node qa/preview-gallery.mjs</code></p>
@@ -45,6 +45,7 @@ import { ARCH_DOC_REL_PATH, SECTION_IDS, regenerateArchDoc } from "./lib/arch-do
45
45
  import { DETERMINISM_TIMEZONES, compareOutcomes, parseJUnitOutcomes } from "./lib/determinism.mjs";
46
46
  import { evaluateAuditCadence } from "./lib/audit-cadence.mjs";
47
47
  import { appendFlightRecord, buildFlightEntry } from "./lib/flight-recorder.mjs";
48
+ import { checkHarnessIntegrity, describeIntegrity, LOCK_PATH } from "./lib/harness-lock.mjs";
48
49
 
49
50
  const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
50
51
  const EVIDENCE_DIR = path.join(ROOT, "qa", "evidence");
@@ -416,6 +417,62 @@ function settleAdb() {
416
417
  // itself lives in qa/lib/spec-coverage.mjs — the SAME scan feature-brief.mjs
417
418
  // derives doneness from, so this gate and the Features view can never disagree
418
419
  // about a clause. This step owns only the orphan decision + bookkeeping.
420
+ // The first question any verdict depends on: is the lane that is about to
421
+ // issue it the lane this app was given?
422
+ //
423
+ // Without this the receipt is unfalsifiable in one specific way — edit
424
+ // qa/verify.mjs to force every step PASS and the receipt still validates,
425
+ // because the edited file is simply part of the hashed input surface. Hashing
426
+ // the machine-owned region against qa/harness.lock.json closes that: the lane
427
+ // cannot vouch for itself while modified.
428
+ //
429
+ // DELIBERATELY NOT MEMOIZED. Every other pure-Node step can serve a cached
430
+ // PASS when its inputs are unchanged; a cached PASS on an integrity check is
431
+ // precisely the failure it exists to prevent, and 34 file reads are too cheap
432
+ // to be worth the risk.
433
+ //
434
+ // Three states, three verdicts:
435
+ // intact PASS
436
+ // modified FAIL — named files, with the command that restores them
437
+ // unlocked SKIP — an app stamped before locks existed. Nothing is known to
438
+ // be wrong, but nothing is proven either; recording the gap keeps
439
+ // the pipeline honest instead of quietly passing.
440
+ function stepHarnessIntegrity() {
441
+ const started = Date.now();
442
+ const r = checkHarnessIntegrity(ROOT);
443
+ const base = { name: "harnessIntegrity", durationMs: Date.now() - started, harness: r };
444
+
445
+ if (r.status === "intact") {
446
+ return { ...base, verdict: "PASS", note: describeIntegrity(r) };
447
+ }
448
+ if (r.status === "unlocked") {
449
+ return {
450
+ ...base,
451
+ verdict: "SKIP",
452
+ reason: `no ${LOCK_PATH} — this app was stamped before harness locks existed. ` +
453
+ "`npx create-cmp-cli upgrade --harness` records one.",
454
+ };
455
+ }
456
+
457
+ const named = [
458
+ ...r.modified.map((f) => `modified ${f}`),
459
+ ...r.missing.map((f) => `missing ${f}`),
460
+ ...r.extra.map((f) => `unrecorded ${f}`),
461
+ ];
462
+ return {
463
+ ...base,
464
+ verdict: "FAIL",
465
+ reason:
466
+ `the verify lane has been modified since it was installed — ${describeIntegrity(r)}. ` +
467
+ "Lane code is machine-owned: it is byte-identical in every create-cmp app and carries " +
468
+ "no app content, so a local edit is either an accident, a half-applied upgrade, or an " +
469
+ "attempt to make this receipt say something the lane would not. Restore it with " +
470
+ "`npx create-cmp-cli upgrade --harness`, which also reports any genuine local patch " +
471
+ "instead of discarding it.",
472
+ files: named,
473
+ };
474
+ }
475
+
419
476
  function stepSpecCoverage() {
420
477
  const started = Date.now();
421
478
  const specsDir = path.join(ROOT, "specs");
@@ -1307,8 +1364,11 @@ const stepArchDocMemo = memoized("archDoc", stepArchDoc);
1307
1364
  const stepsForProfile = {
1308
1365
  // scaffold: what `create-cmp --verify` proves at stamp time — specCoverage,
1309
1366
  // the full JVM tier (unit + conformance + golden + UI tests) plus the Android build.
1310
- scaffold: [stepSpecCoverageMemo, stepApprovalsMemo, stepComponentStoriesMemo, stepReachabilityMemo, stepArchDocMemo, stepSchemaHistory, stepBuild, stepUnitTests],
1367
+ scaffold: [stepHarnessIntegrity, stepSpecCoverageMemo, stepApprovalsMemo, stepComponentStoriesMemo, stepReachabilityMemo, stepArchDocMemo, stepSchemaHistory, stepBuild, stepUnitTests],
1311
1368
  local: [
1369
+ // First, always: every verdict below is only worth what the lane issuing
1370
+ // it is worth.
1371
+ stepHarnessIntegrity,
1312
1372
  stepSpecCoverageMemo,
1313
1373
  stepApprovalsMemo,
1314
1374
  stepComponentStoriesMemo,
@@ -1502,6 +1562,30 @@ if (fs.existsSync(ARTIFACTS_DIR)) {
1502
1562
  // Bind the receipt to the content of the verified surface (ADR-0005), NOT the
1503
1563
  // parent SHA (rebase/merge-fragile). Must be computed before latest.json is
1504
1564
  // written — the receipt is an output and must never hash itself.
1565
+ /**
1566
+ * The receipt's harness summary — compact by design. The per-file detail lives
1567
+ * on the harnessIntegrity step; this is the part a receipt-holder needs to
1568
+ * identify the lane, plus the names of any modified files (an auditor told
1569
+ * "not intact" and not told which files has been given a rumour, not a fact).
1570
+ */
1571
+ function harnessForReceipt() {
1572
+ const row = steps.find((st) => st.name === "harnessIntegrity");
1573
+ const r = row?.harness ?? checkHarnessIntegrity(ROOT);
1574
+ const summary = {
1575
+ name: r.name,
1576
+ version: r.version,
1577
+ sha256: r.sha256,
1578
+ status: r.status,
1579
+ intact: r.status === "intact",
1580
+ };
1581
+ if (r.status === "modified") {
1582
+ summary.modified = r.modified;
1583
+ summary.missing = r.missing;
1584
+ summary.extra = r.extra;
1585
+ }
1586
+ return summary;
1587
+ }
1588
+
1505
1589
  const inputs = computeInputsHash(ROOT);
1506
1590
 
1507
1591
  // The receipt. Deterministic key order; ONE volatile timestamp field.
@@ -1524,6 +1608,16 @@ const receipt = {
1524
1608
  fileCount: inputs.fileCount,
1525
1609
  },
1526
1610
  steps,
1611
+ // WHICH LANE issued this verdict. A receipt that cannot name its own harness
1612
+ // can only be checked against the tree it came from; naming the version and
1613
+ // the region digest lets a third party who holds the receipt ask the harder
1614
+ // question — was this the real published lane? — without the tree at all.
1615
+ //
1616
+ // `intact` is the LOCAL claim only: unmodified since installed. It is a
1617
+ // checksum, not a signature, and someone who edits the lane can edit this
1618
+ // too. What they cannot edit is what the registry published under that
1619
+ // version, which is why `version` + `sha256` travel together.
1620
+ harness: harnessForReceipt(),
1527
1621
  strength: { onDeviceSteps },
1528
1622
  evidenceLevel: level,
1529
1623
  artifacts,