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
|
@@ -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-
|
|
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-
|
|
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, "&").replace(/</g, "<").replac
|
|
|
57
72
|
|
|
58
73
|
const html = `<!doctype html>
|
|
59
74
|
<meta charset="utf-8">
|
|
60
|
-
<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
|
|
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>
|
package/template/qa/verify.mjs
CHANGED
|
@@ -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,
|