create-cmp-cli 0.13.0 → 0.14.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.
- 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 +1723 -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 +115 -1
- package/src/lib/harness-upgrade.mjs +193 -5
- 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 +110 -2
- package/template/.gradle/8.11.1/checksums/checksums.lock +0 -0
- package/template/.gradle/8.11.1/fileChanges/last-build.bin +0 -0
- package/template/.gradle/8.11.1/fileHashes/fileHashes.lock +0 -0
- package/template/.gradle/8.11.1/gc.properties +0 -0
- package/template/.gradle/buildOutputCleanup/buildOutputCleanup.lock +0 -0
- package/template/.gradle/buildOutputCleanup/cache.properties +0 -2
- package/template/.gradle/vcs-1/gc.properties +0 -0
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
// The harness region — which files in a stamped app are MACHINE-OWNED.
|
|
2
|
+
//
|
|
3
|
+
// A create-cmp app carries two kinds of file. App-owned files are the app: its
|
|
4
|
+
// screens, specs, goldens, approvals, e2e flows. Machine-owned files are the
|
|
5
|
+
// verify lane itself — executable harness code that is identical in every app
|
|
6
|
+
// ever stamped, carrying no app content whatsoever.
|
|
7
|
+
//
|
|
8
|
+
// Treating the second kind like the first is what made upgrades expensive: a
|
|
9
|
+
// three-way merge over 10k lines of engine code produced ~1,000 conflicted
|
|
10
|
+
// lines per app with ZERO app-specific tokens in them. The right operation for
|
|
11
|
+
// a derived artifact is replace, not merge. This module draws that line.
|
|
12
|
+
//
|
|
13
|
+
// The rule is deliberately mechanical, with no per-file list to keep in sync:
|
|
14
|
+
//
|
|
15
|
+
// machine-owned == the .mjs files directly under qa/ and qa/lib/
|
|
16
|
+
//
|
|
17
|
+
// Everything else under qa/ is app state (approvals.json, comments.json,
|
|
18
|
+
// evidence/, golden/) or app content (e2e/*.yaml — seeded once at stamp time,
|
|
19
|
+
// app-owned forever after, because apps edit their smoke flow as tabs change).
|
|
20
|
+
//
|
|
21
|
+
// Three consequences, each load-bearing:
|
|
22
|
+
//
|
|
23
|
+
// 1. NEVER STAMPED. The region is copied byte-identical from the engine —
|
|
24
|
+
// token replacement must not touch it. It used to: qa/lib/approvals.mjs
|
|
25
|
+
// carries a comment warning that a literal "__PACKAGE__" in lane source
|
|
26
|
+
// gets silently rewritten at stamp time, and qa/scaffold-feature.mjs
|
|
27
|
+
// shipped an error message that meant to name the unresolved token and
|
|
28
|
+
// instead named the app's real package. Anything app-specific the lane
|
|
29
|
+
// needs is read at RUNTIME from create-cmp.json.
|
|
30
|
+
//
|
|
31
|
+
// 2. VERIFIABLE. Because the copy is byte-identical to a known version, an
|
|
32
|
+
// app can prove offline that its lane is the real one. Without this a
|
|
33
|
+
// receipt is unfalsifiable: edit qa/verify.mjs to force every step green
|
|
34
|
+
// and the receipt still validates, since the edited file is simply part
|
|
35
|
+
// of the hashed surface.
|
|
36
|
+
//
|
|
37
|
+
// 3. REPLACEABLE. `create-cmp upgrade --harness` overwrites the region
|
|
38
|
+
// wholesale instead of merging it.
|
|
39
|
+
//
|
|
40
|
+
// SINGLE SOURCE OF TRUTH: packages/harness/src/lib/harness-region.mjs in the
|
|
41
|
+
// create-cmp repo. The copy in a generated project's qa/lib/ is vendored
|
|
42
|
+
// byte-identical at scaffold time — edit the package source, then run
|
|
43
|
+
// `node scripts/sync-harness.mjs`.
|
|
44
|
+
|
|
45
|
+
import { createHash } from "node:crypto";
|
|
46
|
+
import fs from "node:fs";
|
|
47
|
+
import path from "node:path";
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Directories whose direct `.mjs` children are machine-owned, relative to the
|
|
51
|
+
* project root. Direct children only — a nested directory added later is not
|
|
52
|
+
* silently swept into the region without someone editing this list.
|
|
53
|
+
*/
|
|
54
|
+
export const HARNESS_DIRS = ["qa", "qa/lib"];
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Is this project-relative path part of the machine-owned harness region?
|
|
58
|
+
* @param {string} relPath project-relative path, "/"-separated
|
|
59
|
+
* @returns {boolean}
|
|
60
|
+
*/
|
|
61
|
+
export function isHarnessFile(relPath) {
|
|
62
|
+
if (typeof relPath !== "string" || !relPath.endsWith(".mjs")) return false;
|
|
63
|
+
const dir = relPath.includes("/") ? relPath.slice(0, relPath.lastIndexOf("/")) : "";
|
|
64
|
+
return HARNESS_DIRS.includes(dir);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Every machine-owned file present under `root`, as project-relative posix
|
|
69
|
+
* paths, sorted — so the list (and any hash over it) is deterministic.
|
|
70
|
+
* @param {string} root project root
|
|
71
|
+
* @returns {string[]}
|
|
72
|
+
*/
|
|
73
|
+
export function listHarnessFiles(root) {
|
|
74
|
+
const found = [];
|
|
75
|
+
for (const dir of HARNESS_DIRS) {
|
|
76
|
+
let names;
|
|
77
|
+
try {
|
|
78
|
+
names = fs.readdirSync(path.join(root, dir), { withFileTypes: true });
|
|
79
|
+
} catch {
|
|
80
|
+
continue; // a project without qa/lib yet is not an error here
|
|
81
|
+
}
|
|
82
|
+
for (const ent of names) {
|
|
83
|
+
if (!ent.isFile()) continue;
|
|
84
|
+
const rel = `${dir}/${ent.name}`;
|
|
85
|
+
if (isHarnessFile(rel)) found.push(rel);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return found.sort();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** sha256 of one file's bytes, hex. */
|
|
92
|
+
function fileHash(abs) {
|
|
93
|
+
return createHash("sha256").update(fs.readFileSync(abs)).digest("hex");
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Content hash of the whole region, plus the per-file hashes it was built from.
|
|
98
|
+
*
|
|
99
|
+
* The digest covers PATHS as well as content, so moving a file between the two
|
|
100
|
+
* harness directories changes the hash even if no byte of any file changed.
|
|
101
|
+
* NUL separators keep the encoding unambiguous — no filename can forge a
|
|
102
|
+
* boundary.
|
|
103
|
+
*
|
|
104
|
+
* @param {string} root project root
|
|
105
|
+
* @returns {{sha256: string, fileCount: number, files: Record<string,string>}}
|
|
106
|
+
*/
|
|
107
|
+
export function hashHarnessRegion(root) {
|
|
108
|
+
const rels = listHarnessFiles(root);
|
|
109
|
+
const files = {};
|
|
110
|
+
const digest = createHash("sha256");
|
|
111
|
+
for (const rel of rels) {
|
|
112
|
+
const h = fileHash(path.join(root, rel));
|
|
113
|
+
files[rel] = h;
|
|
114
|
+
digest.update(rel, "utf8").update("\0").update(h, "utf8").update("\n");
|
|
115
|
+
}
|
|
116
|
+
return { sha256: digest.digest("hex"), fileCount: rels.length, files };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Compare a tree's region against a recorded manifest of per-file hashes.
|
|
121
|
+
* Reports WHICH files differ, not just that something did — an app that
|
|
122
|
+
* patched its lane needs to see the list, and an upgrade needs it to decide
|
|
123
|
+
* what to preserve.
|
|
124
|
+
*
|
|
125
|
+
* @param {string} root project root
|
|
126
|
+
* @param {{sha256?: string, files?: Record<string,string>}} recorded
|
|
127
|
+
* @returns {{intact: boolean, sha256: string, modified: string[],
|
|
128
|
+
* missing: string[], extra: string[]}}
|
|
129
|
+
* modified present in both, different content
|
|
130
|
+
* missing recorded but absent from the tree
|
|
131
|
+
* extra present in the tree but not recorded
|
|
132
|
+
*/
|
|
133
|
+
export function compareHarnessRegion(root, recorded) {
|
|
134
|
+
const actual = hashHarnessRegion(root);
|
|
135
|
+
// `typeof null === "object"`, and an array would enumerate as index keys —
|
|
136
|
+
// a manifest that is absent or malformed must read as NOT intact, never crash
|
|
137
|
+
// the lane step that calls this.
|
|
138
|
+
const f = recorded?.files;
|
|
139
|
+
const expected = f && typeof f === "object" && !Array.isArray(f) ? f : {};
|
|
140
|
+
const modified = [];
|
|
141
|
+
const missing = [];
|
|
142
|
+
const extra = [];
|
|
143
|
+
|
|
144
|
+
for (const [rel, hash] of Object.entries(expected)) {
|
|
145
|
+
if (!(rel in actual.files)) missing.push(rel);
|
|
146
|
+
else if (actual.files[rel] !== hash) modified.push(rel);
|
|
147
|
+
}
|
|
148
|
+
for (const rel of Object.keys(actual.files)) {
|
|
149
|
+
if (!(rel in expected)) extra.push(rel);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return {
|
|
153
|
+
intact: modified.length === 0 && missing.length === 0 && extra.length === 0,
|
|
154
|
+
sha256: actual.sha256,
|
|
155
|
+
modified: modified.sort(),
|
|
156
|
+
missing: missing.sort(),
|
|
157
|
+
extra: extra.sort(),
|
|
158
|
+
};
|
|
159
|
+
}
|
|
@@ -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");
|
|
@@ -92,6 +93,14 @@ Flags:
|
|
|
92
93
|
with --profile ci (or release) it runs
|
|
93
94
|
inside the lane and lands on the receipt.
|
|
94
95
|
Never combinable with --fast
|
|
96
|
+
--no-journal skip the qa/flight-recorder.jsonl append.
|
|
97
|
+
qa/watch.mjs passes this on every
|
|
98
|
+
save-triggered run: journalling each save
|
|
99
|
+
would add hundreds of committed lines a day
|
|
100
|
+
and leave a permanently dirty tree inside
|
|
101
|
+
the loop the recorder exists to observe. The
|
|
102
|
+
gap is disclosed in the retrospective's own
|
|
103
|
+
output
|
|
95
104
|
--json print the receipt as JSON instead of the
|
|
96
105
|
human-readable step-by-step log
|
|
97
106
|
--help, -h print this usage and exit 0 without
|
|
@@ -115,7 +124,13 @@ if (rawArgs.includes("--help") || rawArgs.includes("-h")) {
|
|
|
115
124
|
process.exit(0);
|
|
116
125
|
}
|
|
117
126
|
|
|
118
|
-
|
|
127
|
+
// Every flag this file CONSUMES must be listed here, or the strict check below
|
|
128
|
+
// rejects it. `--no-journal` was consumed but unlisted in 0.13.0, which meant
|
|
129
|
+
// qa/watch.mjs — whose spawn passes exactly these flags — exited 2 on every
|
|
130
|
+
// save without ever running the lane. test/verify-flags.test.mjs now pins
|
|
131
|
+
// consumed ⊆ recognized and watch's spawn ⊆ recognized so the class cannot
|
|
132
|
+
// recur.
|
|
133
|
+
const RECOGNIZED_FLAGS = new Set(["--profile", "--json", "--fast", "--determinism", "--no-journal"]);
|
|
119
134
|
for (let i = 0; i < rawArgs.length; i += 1) {
|
|
120
135
|
const arg = rawArgs[i];
|
|
121
136
|
if (arg === "--profile") {
|
|
@@ -416,6 +431,62 @@ function settleAdb() {
|
|
|
416
431
|
// itself lives in qa/lib/spec-coverage.mjs — the SAME scan feature-brief.mjs
|
|
417
432
|
// derives doneness from, so this gate and the Features view can never disagree
|
|
418
433
|
// about a clause. This step owns only the orphan decision + bookkeeping.
|
|
434
|
+
// The first question any verdict depends on: is the lane that is about to
|
|
435
|
+
// issue it the lane this app was given?
|
|
436
|
+
//
|
|
437
|
+
// Without this the receipt is unfalsifiable in one specific way — edit
|
|
438
|
+
// qa/verify.mjs to force every step PASS and the receipt still validates,
|
|
439
|
+
// because the edited file is simply part of the hashed input surface. Hashing
|
|
440
|
+
// the machine-owned region against qa/harness.lock.json closes that: the lane
|
|
441
|
+
// cannot vouch for itself while modified.
|
|
442
|
+
//
|
|
443
|
+
// DELIBERATELY NOT MEMOIZED. Every other pure-Node step can serve a cached
|
|
444
|
+
// PASS when its inputs are unchanged; a cached PASS on an integrity check is
|
|
445
|
+
// precisely the failure it exists to prevent, and 34 file reads are too cheap
|
|
446
|
+
// to be worth the risk.
|
|
447
|
+
//
|
|
448
|
+
// Three states, three verdicts:
|
|
449
|
+
// intact PASS
|
|
450
|
+
// modified FAIL — named files, with the command that restores them
|
|
451
|
+
// unlocked SKIP — an app stamped before locks existed. Nothing is known to
|
|
452
|
+
// be wrong, but nothing is proven either; recording the gap keeps
|
|
453
|
+
// the pipeline honest instead of quietly passing.
|
|
454
|
+
function stepHarnessIntegrity() {
|
|
455
|
+
const started = Date.now();
|
|
456
|
+
const r = checkHarnessIntegrity(ROOT);
|
|
457
|
+
const base = { name: "harnessIntegrity", durationMs: Date.now() - started, harness: r };
|
|
458
|
+
|
|
459
|
+
if (r.status === "intact") {
|
|
460
|
+
return { ...base, verdict: "PASS", note: describeIntegrity(r) };
|
|
461
|
+
}
|
|
462
|
+
if (r.status === "unlocked") {
|
|
463
|
+
return {
|
|
464
|
+
...base,
|
|
465
|
+
verdict: "SKIP",
|
|
466
|
+
reason: `no ${LOCK_PATH} — this app was stamped before harness locks existed. ` +
|
|
467
|
+
"`npx create-cmp-cli upgrade --harness` records one.",
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
const named = [
|
|
472
|
+
...r.modified.map((f) => `modified ${f}`),
|
|
473
|
+
...r.missing.map((f) => `missing ${f}`),
|
|
474
|
+
...r.extra.map((f) => `unrecorded ${f}`),
|
|
475
|
+
];
|
|
476
|
+
return {
|
|
477
|
+
...base,
|
|
478
|
+
verdict: "FAIL",
|
|
479
|
+
reason:
|
|
480
|
+
`the verify lane has been modified since it was installed — ${describeIntegrity(r)}. ` +
|
|
481
|
+
"Lane code is machine-owned: it is byte-identical in every create-cmp app and carries " +
|
|
482
|
+
"no app content, so a local edit is either an accident, a half-applied upgrade, or an " +
|
|
483
|
+
"attempt to make this receipt say something the lane would not. Restore it with " +
|
|
484
|
+
"`npx create-cmp-cli upgrade --harness`, which also reports any genuine local patch " +
|
|
485
|
+
"instead of discarding it.",
|
|
486
|
+
files: named,
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
|
|
419
490
|
function stepSpecCoverage() {
|
|
420
491
|
const started = Date.now();
|
|
421
492
|
const specsDir = path.join(ROOT, "specs");
|
|
@@ -1307,8 +1378,11 @@ const stepArchDocMemo = memoized("archDoc", stepArchDoc);
|
|
|
1307
1378
|
const stepsForProfile = {
|
|
1308
1379
|
// scaffold: what `create-cmp --verify` proves at stamp time — specCoverage,
|
|
1309
1380
|
// the full JVM tier (unit + conformance + golden + UI tests) plus the Android build.
|
|
1310
|
-
scaffold: [stepSpecCoverageMemo, stepApprovalsMemo, stepComponentStoriesMemo, stepReachabilityMemo, stepArchDocMemo, stepSchemaHistory, stepBuild, stepUnitTests],
|
|
1381
|
+
scaffold: [stepHarnessIntegrity, stepSpecCoverageMemo, stepApprovalsMemo, stepComponentStoriesMemo, stepReachabilityMemo, stepArchDocMemo, stepSchemaHistory, stepBuild, stepUnitTests],
|
|
1311
1382
|
local: [
|
|
1383
|
+
// First, always: every verdict below is only worth what the lane issuing
|
|
1384
|
+
// it is worth.
|
|
1385
|
+
stepHarnessIntegrity,
|
|
1312
1386
|
stepSpecCoverageMemo,
|
|
1313
1387
|
stepApprovalsMemo,
|
|
1314
1388
|
stepComponentStoriesMemo,
|
|
@@ -1502,6 +1576,30 @@ if (fs.existsSync(ARTIFACTS_DIR)) {
|
|
|
1502
1576
|
// Bind the receipt to the content of the verified surface (ADR-0005), NOT the
|
|
1503
1577
|
// parent SHA (rebase/merge-fragile). Must be computed before latest.json is
|
|
1504
1578
|
// written — the receipt is an output and must never hash itself.
|
|
1579
|
+
/**
|
|
1580
|
+
* The receipt's harness summary — compact by design. The per-file detail lives
|
|
1581
|
+
* on the harnessIntegrity step; this is the part a receipt-holder needs to
|
|
1582
|
+
* identify the lane, plus the names of any modified files (an auditor told
|
|
1583
|
+
* "not intact" and not told which files has been given a rumour, not a fact).
|
|
1584
|
+
*/
|
|
1585
|
+
function harnessForReceipt() {
|
|
1586
|
+
const row = steps.find((st) => st.name === "harnessIntegrity");
|
|
1587
|
+
const r = row?.harness ?? checkHarnessIntegrity(ROOT);
|
|
1588
|
+
const summary = {
|
|
1589
|
+
name: r.name,
|
|
1590
|
+
version: r.version,
|
|
1591
|
+
sha256: r.sha256,
|
|
1592
|
+
status: r.status,
|
|
1593
|
+
intact: r.status === "intact",
|
|
1594
|
+
};
|
|
1595
|
+
if (r.status === "modified") {
|
|
1596
|
+
summary.modified = r.modified;
|
|
1597
|
+
summary.missing = r.missing;
|
|
1598
|
+
summary.extra = r.extra;
|
|
1599
|
+
}
|
|
1600
|
+
return summary;
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1505
1603
|
const inputs = computeInputsHash(ROOT);
|
|
1506
1604
|
|
|
1507
1605
|
// The receipt. Deterministic key order; ONE volatile timestamp field.
|
|
@@ -1524,6 +1622,16 @@ const receipt = {
|
|
|
1524
1622
|
fileCount: inputs.fileCount,
|
|
1525
1623
|
},
|
|
1526
1624
|
steps,
|
|
1625
|
+
// WHICH LANE issued this verdict. A receipt that cannot name its own harness
|
|
1626
|
+
// can only be checked against the tree it came from; naming the version and
|
|
1627
|
+
// the region digest lets a third party who holds the receipt ask the harder
|
|
1628
|
+
// question — was this the real published lane? — without the tree at all.
|
|
1629
|
+
//
|
|
1630
|
+
// `intact` is the LOCAL claim only: unmodified since installed. It is a
|
|
1631
|
+
// checksum, not a signature, and someone who edits the lane can edit this
|
|
1632
|
+
// too. What they cannot edit is what the registry published under that
|
|
1633
|
+
// version, which is why `version` + `sha256` travel together.
|
|
1634
|
+
harness: harnessForReceipt(),
|
|
1527
1635
|
strength: { onDeviceSteps },
|
|
1528
1636
|
evidenceLevel: level,
|
|
1529
1637
|
artifacts,
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
File without changes
|
|
Binary file
|
|
File without changes
|