create-cmp-cli 0.23.0 → 0.25.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/bin/create-cmp.mjs +33 -3
- package/package.json +10 -4
- package/packages/harness/package.json +18 -8
- package/packages/harness/src/approve.mjs +46 -18
- package/packages/harness/src/console/console-data.mjs +117 -0
- package/packages/harness/src/console/console-evidence.mjs +122 -0
- package/packages/harness/src/console/console-overview.mjs +642 -0
- package/packages/harness/src/console/console-shell.mjs +1139 -0
- package/packages/harness/src/console/console-tabs.mjs +2828 -0
- package/packages/harness/src/console/contrast.mjs +74 -0
- package/packages/harness/src/console/preview-service.mjs +1384 -0
- package/packages/harness/src/framework-check.mjs +747 -0
- package/packages/harness/src/lib/a11y.mjs +1 -1
- package/packages/harness/src/lib/affected-tests.mjs +99 -44
- package/packages/harness/src/lib/agent-hold.mjs +234 -0
- package/packages/harness/src/lib/approvals.mjs +177 -350
- package/packages/harness/src/lib/audit-cadence.mjs +1 -1
- package/packages/harness/src/lib/comments.mjs +18 -4
- package/packages/harness/src/lib/determinism.mjs +275 -14
- package/packages/harness/src/lib/evidence-badge.mjs +38 -6
- package/packages/harness/src/lib/evidence-ladder.mjs +202 -0
- package/packages/harness/src/lib/evidence-level.mjs +144 -59
- package/packages/harness/src/lib/feature-brief.mjs +53 -16
- package/packages/harness/src/lib/flight-recorder.mjs +72 -8
- package/packages/harness/src/lib/framework-check.mjs +778 -0
- package/packages/harness/src/lib/harness-lock.mjs +87 -19
- package/packages/harness/src/lib/harness-manifest.mjs +208 -0
- package/packages/harness/src/lib/harness-region.mjs +106 -2
- package/packages/harness/src/lib/harness-source.mjs +142 -0
- package/packages/harness/src/lib/inputs-hash.mjs +180 -22
- package/packages/harness/src/lib/lane-markers.mjs +78 -0
- package/packages/harness/src/lib/lane-narrator.mjs +4 -2
- package/packages/harness/src/lib/lane-runner.mjs +72 -6
- package/packages/harness/src/lib/plan.mjs +44 -11
- package/packages/harness/src/lib/plant-calibration.mjs +135 -0
- package/packages/harness/src/lib/profile-loader.mjs +323 -0
- package/packages/harness/src/lib/profiles/cmp/affected.mjs +70 -0
- package/packages/harness/src/lib/profiles/cmp/android-checks.mjs +56 -0
- package/packages/harness/src/lib/profiles/cmp/artifacts.mjs +381 -0
- package/packages/harness/src/lib/{component-stories.mjs → profiles/cmp/component-stories.mjs} +5 -2
- package/packages/harness/src/lib/profiles/cmp/console-copy.mjs +29 -0
- package/packages/harness/src/lib/profiles/cmp/declarations.mjs +181 -0
- package/{template/qa/lib → packages/harness/src/lib/profiles/cmp}/e2e-coverage.mjs +13 -3
- package/packages/harness/src/lib/profiles/cmp/index.mjs +47 -0
- package/packages/harness/src/lib/profiles/cmp/ladder.mjs +67 -0
- package/packages/harness/src/lib/profiles/cmp/maestro.mjs +113 -0
- package/packages/harness/src/lib/profiles/cmp/plants.mjs +95 -0
- package/{template/qa/lib → packages/harness/src/lib/profiles/cmp}/reachability.mjs +1 -1
- package/packages/harness/src/lib/{steps-cmp.mjs → profiles/cmp/steps-cmp.mjs} +230 -28
- package/packages/harness/src/lib/receipt-validate.mjs +84 -16
- package/packages/harness/src/lib/spec-coverage.mjs +179 -92
- package/packages/harness/src/lib/spec-model.mjs +262 -0
- package/packages/harness/src/lib/step-cache.mjs +11 -2
- package/packages/harness/src/lib/step-outcomes.mjs +84 -151
- package/packages/harness/src/lib/walk.mjs +1 -1
- package/packages/harness/src/plan.mjs +30 -1
- package/packages/harness/src/preview-gallery.mjs +1 -1
- package/packages/harness/src/receipt-check.mjs +95 -26
- package/packages/harness/src/record-audit.mjs +1 -1
- package/packages/harness/src/scaffold-feature.mjs +10 -2
- package/packages/harness/src/verify.mjs +222 -142
- package/packages/harness/src/watch.mjs +146 -33
- package/packages/receipts/package.json +1 -1
- package/packages/receipts/src/index.mjs +2 -2
- package/packages/receipts/src/inputs-hash.mjs +180 -22
- package/packages/receipts/src/receipt-validate.mjs +84 -16
- package/src/commands/attach.mjs +128 -4
- package/src/commands/harden.mjs +7 -1
- package/src/commands/upgrade.mjs +7 -0
- package/src/lib/minimal.mjs +23 -1
- package/src/scaffold.mjs +8 -0
- package/template/.claude/settings.json +2 -2
- package/template/.claude/skills/add-feature/SKILL.md +7 -3
- package/template/.claude/skills/add-repository/SKILL.md +2 -2
- package/template/.claude/skills/add-screen/SKILL.md +8 -4
- package/template/.githooks/pre-push +9 -0
- package/template/CLAUDE.md +55 -186
- package/template/docs/TESTING.md +1 -1
- package/template/gitignore +4 -0
- package/template/qa/approve.mjs +46 -18
- package/template/qa/comments.json +1 -1
- package/template/qa/evidence/schema.json +258 -34
- package/template/qa/framework-check.mjs +747 -0
- package/template/qa/harness-manifest.json +10 -0
- package/template/qa/lib/a11y.mjs +1 -1
- package/template/qa/lib/affected-tests.mjs +99 -44
- package/template/qa/lib/agent-hold.mjs +234 -0
- package/template/qa/lib/approvals.mjs +177 -350
- package/template/qa/lib/audit-cadence.mjs +1 -1
- package/template/qa/lib/comments.mjs +18 -4
- package/template/qa/lib/determinism.mjs +275 -14
- package/template/qa/lib/evidence-badge.mjs +38 -6
- package/template/qa/lib/evidence-ladder.mjs +202 -0
- package/template/qa/lib/evidence-level.mjs +144 -59
- package/template/qa/lib/feature-brief.mjs +53 -16
- package/template/qa/lib/flight-recorder.mjs +72 -8
- package/template/qa/lib/framework-check.mjs +778 -0
- package/template/qa/lib/harness-lock.mjs +87 -19
- package/template/qa/lib/harness-manifest.mjs +208 -0
- package/template/qa/lib/harness-region.mjs +106 -2
- package/template/qa/lib/harness-source.mjs +142 -0
- package/template/qa/lib/inputs-hash.mjs +180 -22
- package/template/qa/lib/lane-markers.mjs +78 -0
- package/template/qa/lib/lane-narrator.mjs +4 -2
- package/template/qa/lib/lane-runner.mjs +72 -6
- package/template/qa/lib/plan.mjs +44 -11
- package/template/qa/lib/plant-calibration.mjs +135 -0
- package/template/qa/lib/profile-loader.mjs +323 -0
- package/template/qa/lib/profiles/cmp/affected.mjs +70 -0
- package/template/qa/lib/profiles/cmp/android-checks.mjs +56 -0
- package/template/qa/lib/profiles/cmp/artifacts.mjs +381 -0
- package/template/qa/lib/{component-stories.mjs → profiles/cmp/component-stories.mjs} +5 -2
- package/template/qa/lib/profiles/cmp/console-copy.mjs +29 -0
- package/template/qa/lib/profiles/cmp/declarations.mjs +181 -0
- package/{packages/harness/src/lib → template/qa/lib/profiles/cmp}/e2e-coverage.mjs +13 -3
- package/template/qa/lib/profiles/cmp/index.mjs +47 -0
- package/template/qa/lib/profiles/cmp/ladder.mjs +67 -0
- package/template/qa/lib/profiles/cmp/maestro.mjs +113 -0
- package/template/qa/lib/profiles/cmp/plants.mjs +95 -0
- package/{packages/harness/src/lib → template/qa/lib/profiles/cmp}/reachability.mjs +1 -1
- package/template/qa/lib/{steps-cmp.mjs → profiles/cmp/steps-cmp.mjs} +230 -28
- package/template/qa/lib/receipt-validate.mjs +84 -16
- package/template/qa/lib/spec-coverage.mjs +179 -92
- package/template/qa/lib/spec-model.mjs +262 -0
- package/template/qa/lib/step-cache.mjs +11 -2
- package/template/qa/lib/step-outcomes.mjs +84 -151
- package/template/qa/lib/walk.mjs +1 -1
- package/template/qa/plan.mjs +30 -1
- package/template/qa/preview-gallery.mjs +1 -1
- package/template/qa/receipt-check.mjs +95 -26
- package/template/qa/record-audit.mjs +1 -1
- package/template/qa/scaffold-feature.mjs +10 -2
- package/template/qa/verified-surface.json +17 -0
- package/template/qa/verify.mjs +222 -142
- package/template/qa/watch.mjs +146 -33
- /package/packages/harness/src/lib/{device-lease.mjs → profiles/cmp/device-lease.mjs} +0 -0
- /package/packages/harness/src/lib/{device-provider.mjs → profiles/cmp/device-provider.mjs} +0 -0
- /package/packages/harness/src/lib/{render.mjs → profiles/cmp/render.mjs} +0 -0
- /package/packages/harness/src/lib/{token-drift.mjs → profiles/cmp/token-drift.mjs} +0 -0
- /package/packages/harness/src/lib/{tree.mjs → profiles/cmp/tree.mjs} +0 -0
- /package/template/qa/lib/{device-lease.mjs → profiles/cmp/device-lease.mjs} +0 -0
- /package/template/qa/lib/{device-provider.mjs → profiles/cmp/device-provider.mjs} +0 -0
- /package/template/qa/lib/{render.mjs → profiles/cmp/render.mjs} +0 -0
- /package/template/qa/lib/{token-drift.mjs → profiles/cmp/token-drift.mjs} +0 -0
- /package/template/qa/lib/{tree.mjs → profiles/cmp/tree.mjs} +0 -0
|
@@ -20,35 +20,47 @@
|
|
|
20
20
|
import fs from "node:fs";
|
|
21
21
|
import path from "node:path";
|
|
22
22
|
import { compareTokenDrift } from "./token-drift.mjs";
|
|
23
|
-
import { evaluateApprovalsGate } from "
|
|
24
|
-
import {
|
|
23
|
+
import { evaluateApprovalsGate } from "../../approvals.mjs";
|
|
24
|
+
import { clauseTierCoverage, listFlowFiles, scanCitations, scanSpecClauses, walkFiles } from "../../spec-coverage.mjs";
|
|
25
|
+
import { specModelFrom } from "../../spec-model.mjs";
|
|
26
|
+
import { layout as cmpLayout, tiers as cmpTiers, reports, grammar as cmpGrammar } from "./declarations.mjs";
|
|
27
|
+
import { RENDER_MARKER_FRESH_MS, RENDER_MARKER_NAME } from "../../lane-markers.mjs";
|
|
28
|
+
|
|
29
|
+
// The scanner's model, built from this profile's own declarations — the pack
|
|
30
|
+
// hands its facts to the core rather than round-tripping through the manifest.
|
|
31
|
+
const SPEC_MODEL = (() => {
|
|
32
|
+
const r = specModelFrom({ id: "cmp", layout: cmpLayout, tiers: cmpTiers, grammar: cmpGrammar });
|
|
33
|
+
if (!r.ok) throw new Error(r.reason);
|
|
34
|
+
return r.model;
|
|
35
|
+
})();
|
|
36
|
+
/** Where the flows the lane runs live. */
|
|
37
|
+
const E2E_FLOW_DIR = cmpLayout.flows.dir;
|
|
25
38
|
import { evaluateComponentStoryParity } from "./component-stories.mjs";
|
|
26
39
|
import { evaluateReachability } from "./reachability.mjs";
|
|
27
40
|
import { evaluateE2eCoverage } from "./e2e-coverage.mjs";
|
|
28
|
-
import { memoizeStep } from "
|
|
29
|
-
import { changedWorkingTreePaths, deriveAffectedFilter } from "
|
|
41
|
+
import { memoizeStep } from "../../step-cache.mjs";
|
|
42
|
+
import { changedWorkingTreePaths, deriveAffectedFilter } from "../../affected-tests.mjs";
|
|
43
|
+
import { affected as cmpAffected } from "./affected.mjs";
|
|
30
44
|
import { acquireDeviceLease, releaseDeviceLease, formatHolder } from "./device-lease.mjs";
|
|
31
|
-
import { ARCH_DOC_REL_PATH, SECTION_IDS, regenerateArchDoc } from "
|
|
32
|
-
import { DETERMINISM_TIMEZONES, compareOutcomes,
|
|
33
|
-
import { evaluateAuditCadence } from "
|
|
34
|
-
import { androidChecksOutcome
|
|
45
|
+
import { ARCH_DOC_REL_PATH, SECTION_IDS, regenerateArchDoc } from "../../arch-doc.mjs";
|
|
46
|
+
import { DETERMINISM_TIMEZONES, compareOutcomes, parseReportOutcomes, reportFormatProblem } from "../../determinism.mjs";
|
|
47
|
+
import { evaluateAuditCadence } from "../../audit-cadence.mjs";
|
|
48
|
+
import { androidChecksOutcome } from "./android-checks.mjs";
|
|
49
|
+
import { deviceLogIncidents, maestroOutcome, parseMaestroJunit } from "./maestro.mjs";
|
|
35
50
|
import { ensureDevice, releaseDevice } from "./device-provider.mjs";
|
|
36
|
-
import { checkHarnessIntegrity, describeIntegrity, LOCK_PATH } from "
|
|
37
|
-
import { stepDisplayName } from "
|
|
38
|
-
import { CMP_LADDER } from "./
|
|
51
|
+
import { checkHarnessIntegrity, describeIntegrity, LOCK_PATH } from "../../harness-lock.mjs";
|
|
52
|
+
import { stepDisplayName } from "../../lane-runner.mjs";
|
|
53
|
+
import { CMP_LADDER } from "./ladder.mjs";
|
|
39
54
|
|
|
40
55
|
/**
|
|
41
56
|
* @param {object} ctx
|
|
42
57
|
* @param {string} ctx.ROOT project root
|
|
43
58
|
* @param {string} ctx.HERE the qa/ directory
|
|
44
|
-
* @param {string} ctx.GRADLEW the wrapper invocation
|
|
45
|
-
* @param {string} ctx.RERUN " --rerun" in full mode, "" in fast
|
|
46
59
|
* @param {boolean} ctx.fast
|
|
47
60
|
* @param {boolean} ctx.determinism
|
|
48
61
|
* @param {string} ctx.profile
|
|
49
62
|
* @param {"full"|"fast"} ctx.mode
|
|
50
63
|
* @param {Function} ctx.sh subprocess helper (throws StepTimeout past the step's deadline)
|
|
51
|
-
* @param {Function} ctx.shGradle sh + the KSP-collision self-heal
|
|
52
64
|
* @param {Function} ctx.tryGit
|
|
53
65
|
* @param {Function} ctx.tryGitLines
|
|
54
66
|
* @param {string[]} ctx.DEGRADED_PATHS degraded-path activations the receipt reports
|
|
@@ -56,8 +68,112 @@ import { CMP_LADDER } from "./evidence-level.mjs";
|
|
|
56
68
|
* FAST_EXCLUDED_NAMES: string[], STEP_FN_BY_NAME: Record<string, Function>,
|
|
57
69
|
* stepDeterminism: Function, releaseLease: () => void}}
|
|
58
70
|
*/
|
|
71
|
+
/**
|
|
72
|
+
* Is the inspector server in this app at all? `--no-inspector` deletes the
|
|
73
|
+
* androidDebug inspector sources (the feature manifest's paths), so the file
|
|
74
|
+
* that answers `/inspect/health` is simply not there. Asked BEFORE the lane
|
|
75
|
+
* touches a device: the 2026-09-08 context audit found this step booting a
|
|
76
|
+
* headless emulator first and only then discovering the endpoint it wanted to
|
|
77
|
+
* read was never shipped — a device paid for by every `--no-inspector` app,
|
|
78
|
+
* for a step that then SKIPped. Structural, so the receipt stays done-evidence.
|
|
79
|
+
*/
|
|
80
|
+
export function inspectorShipped(root) {
|
|
81
|
+
const base = path.join(root, "composeApp", "src", "androidDebug");
|
|
82
|
+
if (!fs.existsSync(base)) return false;
|
|
83
|
+
const stack = [base];
|
|
84
|
+
while (stack.length) {
|
|
85
|
+
const dir = stack.pop();
|
|
86
|
+
for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
87
|
+
if (e.isDirectory()) stack.push(path.join(dir, e.name));
|
|
88
|
+
else if (e.name === "InspectorHttpServer.kt") return true;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
|
|
59
95
|
export function createCmpSteps(ctx) {
|
|
60
|
-
const { ROOT, HERE,
|
|
96
|
+
const { ROOT, HERE, fast, determinism, profile, mode, sh, tryGit, tryGitLines, DEGRADED_PATHS } = ctx;
|
|
97
|
+
|
|
98
|
+
// ── Gradle is THIS profile's build tool ───────────────────────────────────
|
|
99
|
+
// The wrapper invocation, the integrity flag and the coexistence dance used
|
|
100
|
+
// to live in qa/verify.mjs and arrive through ctx — which meant the runner,
|
|
101
|
+
// the spine every stack shares, knew what Gradle is, where composeApp/build
|
|
102
|
+
// sits, and what a KSP cache collision looks like. Stage 0 PR 6a moved them
|
|
103
|
+
// here: the runner hands the pack a plain subprocess helper, and the pack
|
|
104
|
+
// wraps it with whatever its own toolchain needs.
|
|
105
|
+
const GRADLEW = process.platform === "win32" ? "gradlew.bat" : "./gradlew";
|
|
106
|
+
|
|
107
|
+
// `--rerun` exists for evidence integrity: it stops Gradle's build cache
|
|
108
|
+
// replaying a PASS recorded against a different tree into a receipt that
|
|
109
|
+
// claims tests executed. That belongs to runs producing integrity-bearing
|
|
110
|
+
// artifacts — and a --fast run does not: its receipt already declares itself
|
|
111
|
+
// non-evidence (mode "fast", no evidence rung, refused by
|
|
112
|
+
// qa/receipt-check.mjs), so forcing execution there paid an integrity tax to
|
|
113
|
+
// protect an artifact with nothing to protect.
|
|
114
|
+
const RERUN = fast ? "" : " --rerun";
|
|
115
|
+
|
|
116
|
+
// ── Preview-daemon coexistence ───────────────────────────────────────────
|
|
117
|
+
// The preview daemon (the eyes) and this lane both spawn Gradle against this
|
|
118
|
+
// project and share <buildDir>/kspCaches, whose KSP incremental storage is
|
|
119
|
+
// single-owner — two concurrent builds throw "Storage for [...] is already
|
|
120
|
+
// registered" and one side dies. Three defenses, all automatic:
|
|
121
|
+
// 1. COORDINATE (this lane -> the daemon): the lane stamps its marker for
|
|
122
|
+
// its duration (qa/.lane-in-progress, core state); the preview service
|
|
123
|
+
// defers renders while it exists (mtime-bounded, so a crashed lane
|
|
124
|
+
// never wedges the eyes for long).
|
|
125
|
+
// 2. COORDINATE (the daemon -> this lane), the symmetric half: the daemon
|
|
126
|
+
// stamps its OWN marker for the duration of a render's Gradle build;
|
|
127
|
+
// shGradle waits for it to clear (or go stale) before launching this
|
|
128
|
+
// lane's own Gradle command — same mtime-bounded shape, so a crashed
|
|
129
|
+
// daemon never wedges the lane for long either.
|
|
130
|
+
// 3. SELF-HEAL: a Gradle step that still hits the collision clears
|
|
131
|
+
// kspCaches and retries once — the manual recovery that always worked,
|
|
132
|
+
// automated.
|
|
133
|
+
// The render marker is the PROVIDER's, so it lives under this profile's own
|
|
134
|
+
// buildDir; qa/lib/lane-markers.mjs names the file and the freshness bound
|
|
135
|
+
// so the daemon and the lane can never disagree about either.
|
|
136
|
+
const KSP_COLLISION_RE = /Storage for \[[^\]]*\] is already registered/;
|
|
137
|
+
const BUILD_DIR = cmpLayout.buildDir;
|
|
138
|
+
const RENDER_MARKER = path.join(ROOT, ...BUILD_DIR.split("/"), RENDER_MARKER_NAME);
|
|
139
|
+
const RENDER_WAIT_TIMEOUT_MS = 3 * 60 * 1000; // give up waiting after this long regardless
|
|
140
|
+
const RENDER_WAIT_POLL_MS = 2000;
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Defer this lane's next Gradle command while the preview daemon's render
|
|
144
|
+
* marker is present AND fresh (mtime younger than RENDER_MARKER_FRESH_MS).
|
|
145
|
+
* Polls every RENDER_WAIT_POLL_MS; gives up and proceeds anyway after
|
|
146
|
+
* RENDER_WAIT_TIMEOUT_MS, or the moment the marker disappears or goes stale —
|
|
147
|
+
* whichever comes first. A missing/unreadable marker returns immediately:
|
|
148
|
+
* this is a coexistence courtesy, never a hard dependency on the daemon.
|
|
149
|
+
*/
|
|
150
|
+
function waitForRenderMarker() {
|
|
151
|
+
const deadline = Date.now() + RENDER_WAIT_TIMEOUT_MS;
|
|
152
|
+
for (;;) {
|
|
153
|
+
let stat;
|
|
154
|
+
try {
|
|
155
|
+
stat = fs.statSync(RENDER_MARKER);
|
|
156
|
+
} catch {
|
|
157
|
+
return; // no render in flight
|
|
158
|
+
}
|
|
159
|
+
if (Date.now() - stat.mtimeMs >= RENDER_MARKER_FRESH_MS) return; // gone stale
|
|
160
|
+
if (Date.now() >= deadline) return; // waited long enough — proceed regardless
|
|
161
|
+
sh(`sleep ${RENDER_WAIT_POLL_MS / 1000}`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function shGradle(cmd, opts = {}) {
|
|
166
|
+
waitForRenderMarker();
|
|
167
|
+
const first = sh(cmd, opts);
|
|
168
|
+
if (first.ok || !KSP_COLLISION_RE.test(first.out)) return first;
|
|
169
|
+
console.error("· KSP cache collision (concurrent Gradle — the preview daemon?) — clearing kspCaches, retrying once");
|
|
170
|
+
fs.rmSync(path.join(ROOT, ...BUILD_DIR.split("/"), "kspCaches"), { recursive: true, force: true });
|
|
171
|
+
const retry = sh(cmd, opts);
|
|
172
|
+
retry.durationMs += first.durationMs;
|
|
173
|
+
retry.selfHealed = "ksp-cache-collision";
|
|
174
|
+
DEGRADED_PATHS.push("ksp-cache-collision: cleared kspCaches and retried the Gradle step");
|
|
175
|
+
return retry;
|
|
176
|
+
}
|
|
61
177
|
|
|
62
178
|
// Recursive: desktopTest writes TEST-*.xml flat, but connected (instrumented) results
|
|
63
179
|
// land one directory level down per device (build/outputs/androidTest-results/connected/
|
|
@@ -252,6 +368,29 @@ function stepHarnessIntegrity() {
|
|
|
252
368
|
const r = checkHarnessIntegrity(ROOT);
|
|
253
369
|
const base = { name: "harnessIntegrity", durationMs: Date.now() - started, harness: r };
|
|
254
370
|
|
|
371
|
+
// ADR-0010 — THE FLOOR, BEFORE THE STATUS. A region with no engine module
|
|
372
|
+
// reads `intact` and is telling the truth: it is unmodified since it was
|
|
373
|
+
// locked. It is simply not a lane, and a lane that is not there cannot vouch
|
|
374
|
+
// for anything.
|
|
375
|
+
//
|
|
376
|
+
// On an ordinary tree this cannot fire — a region with no qa/verify.mjs has
|
|
377
|
+
// nothing to run this check. It fires in the one mode ADR-0008 rejects: a
|
|
378
|
+
// lane running from OUTSIDE the region, over a project whose qa/ holds only
|
|
379
|
+
// declarations. There the lane runs, this step executes, and until now it
|
|
380
|
+
// returned PASS over a project carrying none of the code that produced the
|
|
381
|
+
// verdict — the residue that ADR predicted, in the row that exists to
|
|
382
|
+
// prevent exactly it.
|
|
383
|
+
if (r.vacuous) {
|
|
384
|
+
return {
|
|
385
|
+
...base,
|
|
386
|
+
verdict: "FAIL",
|
|
387
|
+
reason:
|
|
388
|
+
`the harness region holds ${r.fileCount} file(s) and NONE of them is engine code — this is not a lane, so it cannot vouch for one. ` +
|
|
389
|
+
`A lane running from outside the region (node_modules) over a project whose qa/ carries only declarations produces exactly this: ` +
|
|
390
|
+
`install the lane into the tree (\`prooflane init\`, or \`prooflane upgrade\` to re-vendor it) so the code that issues the verdict is the code the receipt binds`,
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
|
|
255
394
|
if (r.status === "intact") {
|
|
256
395
|
return { ...base, verdict: "PASS", note: describeIntegrity(r) };
|
|
257
396
|
}
|
|
@@ -290,16 +429,16 @@ function stepSpecCoverage() {
|
|
|
290
429
|
return { name: "specCoverage", verdict: "SKIP", reason: "no specs/ directory in this project", durationMs: Date.now() - started };
|
|
291
430
|
}
|
|
292
431
|
|
|
293
|
-
const clauses = scanSpecClauses(ROOT);
|
|
294
|
-
const tags = scanCitations(ROOT);
|
|
295
|
-
const searchDirs =
|
|
296
|
-
const files = searchDirs.flatMap((d) => walkFiles(d, [
|
|
432
|
+
const clauses = scanSpecClauses(ROOT, SPEC_MODEL);
|
|
433
|
+
const tags = scanCitations(ROOT, SPEC_MODEL);
|
|
434
|
+
const searchDirs = SPEC_MODEL.citationRoots.map((rel) => path.join(ROOT, ...rel.split("/")));
|
|
435
|
+
const files = searchDirs.flatMap((d) => walkFiles(d, [...SPEC_MODEL.citationExts, ...(SPEC_MODEL.flows ? SPEC_MODEL.flows.exts : [])]));
|
|
297
436
|
|
|
298
437
|
const citedIds = new Set(tags.map((t) => t.id));
|
|
299
438
|
const orphanClauses = [...clauses.entries()].filter(([, c]) => !c.withdrawn).filter(([id]) => !citedIds.has(id));
|
|
300
439
|
const orphanTags = tags.filter((t) => !clauses.has(t.id) || clauses.get(t.id).withdrawn);
|
|
301
440
|
|
|
302
|
-
const tiers = clauseTierCoverage(clauses, tags);
|
|
441
|
+
const tiers = clauseTierCoverage(clauses, tags, SPEC_MODEL);
|
|
303
442
|
|
|
304
443
|
if (orphanClauses.length === 0 && orphanTags.length === 0 && tiers.unmetTier.length === 0) {
|
|
305
444
|
// Tier visibility, still not a gate for UNDECLARED clauses (industry rule:
|
|
@@ -330,11 +469,18 @@ function stepSpecCoverage() {
|
|
|
330
469
|
// failure than a missing one, and its message has to say WHY the citation it
|
|
331
470
|
// can see does not count.
|
|
332
471
|
for (const u of tiers.unmetTier) {
|
|
472
|
+
if (u.unknown) {
|
|
473
|
+
lines.push(
|
|
474
|
+
` [${u.id}] ${u.file} — declares [tier: ${u.requiredTier}], a requirement this profile does not define. ` +
|
|
475
|
+
`Declared requirements: ${Object.keys(SPEC_MODEL.tiers.satisfying).join(", ")}. Fix the tag or declare the tier in the profile.`,
|
|
476
|
+
);
|
|
477
|
+
continue;
|
|
478
|
+
}
|
|
333
479
|
const has = u.tiers.length ? `cited only from ${u.tiers.join(", ")}` : "cited by nothing";
|
|
334
480
|
lines.push(
|
|
335
481
|
` [${u.id}] ${u.file} — declares [tier: ${u.requiredTier}] but is ${has}. ` +
|
|
336
482
|
`A test on those tiers cannot observe this promise (no process lifecycle, no OS facts, no real device). ` +
|
|
337
|
-
`Add a citing test in ${
|
|
483
|
+
`Add a citing test in ${SPEC_MODEL.tiers.satisfying[u.requiredTier].join(" or ")} — and note that tier SKIPPING for want of a device ` +
|
|
338
484
|
`leaves this clause unproven, which is the point: "I could not check this" is a failure, not a quieter rung.`,
|
|
339
485
|
);
|
|
340
486
|
}
|
|
@@ -627,7 +773,7 @@ function stepUnitTests() {
|
|
|
627
773
|
if (changed === null) {
|
|
628
774
|
note = "full suite — git unavailable, cannot derive the change (fail open)";
|
|
629
775
|
} else {
|
|
630
|
-
const filter = deriveAffectedFilter(changed);
|
|
776
|
+
const filter = deriveAffectedFilter(changed, cmpAffected);
|
|
631
777
|
if (filter.mode === "filtered") {
|
|
632
778
|
testsArgs = filter.patterns.map((p) => ` --tests "${p}"`).join("");
|
|
633
779
|
note = `affected: ${filter.patterns.join(", ")} — ${filter.sourcePaths.length} changed source file(s)`;
|
|
@@ -720,12 +866,17 @@ function stepDeterminism() {
|
|
|
720
866
|
};
|
|
721
867
|
}
|
|
722
868
|
|
|
723
|
-
const
|
|
869
|
+
const reportProblem = reportFormatProblem(reports);
|
|
870
|
+
if (reportProblem) return { name: "determinism", verdict: "ERROR", reason: reportProblem, durationMs: elapsed() };
|
|
871
|
+
const resultsDir = path.join(ROOT, reports.dir, "desktopTest");
|
|
724
872
|
const legs = [];
|
|
725
873
|
for (const { tz, label } of DETERMINISM_TIMEZONES) {
|
|
726
874
|
const res = shGradle(`${GRADLEW} :composeApp:desktopTest${RERUN} --console=plain`, { env: { ...process.env, TZ: tz } });
|
|
727
875
|
// Parsed NOW, before the next leg overwrites the same results directory.
|
|
728
|
-
|
|
876
|
+
// The DISPATCHER, not one format's parser. A pack that named the JUnit
|
|
877
|
+
// reader directly would silently ignore its own `reports.format` the day a
|
|
878
|
+
// profile declared tap or ctrf — reading zero tests and calling it green.
|
|
879
|
+
const outcomes = parseReportOutcomes(resultsDir, { format: reports.format });
|
|
729
880
|
legs.push({ tz, label, ok: res.ok, outcomes, tail: res.out.split("\n").slice(-8).join("\n") });
|
|
730
881
|
}
|
|
731
882
|
const [a, b] = legs;
|
|
@@ -757,7 +908,17 @@ function stepDeterminism() {
|
|
|
757
908
|
};
|
|
758
909
|
}
|
|
759
910
|
|
|
760
|
-
|
|
911
|
+
// WHICH STEP owns a test class is this stack's knowledge: Kotlin class-name
|
|
912
|
+
// conventions on the left, this pack's step names on the right. It lived in
|
|
913
|
+
// qa/lib/determinism.mjs until Stage 0 A3, where the core knowing four step
|
|
914
|
+
// names was stamping them onto other stacks' determinism diffs.
|
|
915
|
+
const attribute = (classname) => {
|
|
916
|
+
if (/GoldenTreeTest$/.test(classname)) return "goldenTrees";
|
|
917
|
+
if (/ArchitectureConformanceTest$/.test(classname)) return "conformance";
|
|
918
|
+
if (/A11yConformanceTest$/.test(classname)) return "a11y";
|
|
919
|
+
return "unitTests";
|
|
920
|
+
};
|
|
921
|
+
const diffs = compareOutcomes(a.outcomes, b.outcomes, labelA, labelB, attribute);
|
|
761
922
|
if (diffs.length > 0) {
|
|
762
923
|
const lines = [
|
|
763
924
|
`Nondeterminism under timezone shift — the same tree produced different outcomes under ${labelA} vs ${labelB}. Something reads ambient time or zone past the ARCH-13 net (a library default, an uninjected clock, a golden that captures "today"):`,
|
|
@@ -830,6 +991,10 @@ function stepTokenDrift() {
|
|
|
830
991
|
const started = Date.now();
|
|
831
992
|
const elapsed = () => Date.now() - started;
|
|
832
993
|
|
|
994
|
+
if (!inspectorShipped(ROOT)) {
|
|
995
|
+
return { name: "tokenDrift", verdict: "SKIP", skipKind: "structure", reason: "inspector not shipped in this project (--no-inspector) — no endpoint to read, no device needed", durationMs: elapsed() };
|
|
996
|
+
}
|
|
997
|
+
|
|
833
998
|
const device = ensureLaneDevice("tokenDrift");
|
|
834
999
|
if (device) return { ...device, durationMs: elapsed() };
|
|
835
1000
|
|
|
@@ -908,7 +1073,7 @@ function maestroGuards(name) {
|
|
|
908
1073
|
if (!fs.existsSync(path.join(ROOT, E2E_FLOW_DIR))) {
|
|
909
1074
|
return { name, verdict: "SKIP", skipKind: "structure", reason: "e2e harness not included in this project (--no-e2e)", durationMs: 0 };
|
|
910
1075
|
}
|
|
911
|
-
if (listFlowFiles(ROOT).length === 0) {
|
|
1076
|
+
if (listFlowFiles(ROOT, SPEC_MODEL).length === 0) {
|
|
912
1077
|
return { name, verdict: "SKIP", skipKind: "structure", reason: `${E2E_FLOW_DIR}/ holds no flows (*.yaml) — nothing to drive`, durationMs: 0 };
|
|
913
1078
|
}
|
|
914
1079
|
if (!maestroAvailable()) {
|
|
@@ -944,7 +1109,7 @@ function maestroGuards(name) {
|
|
|
944
1109
|
const E2E_RUN_BOUND_MS = (flowCount) => 120_000 + 180_000 * Math.max(1, flowCount);
|
|
945
1110
|
|
|
946
1111
|
function runMaestroSmoke(name, priorDurationMs) {
|
|
947
|
-
const flows = listFlowFiles(ROOT);
|
|
1112
|
+
const flows = listFlowFiles(ROOT, SPEC_MODEL);
|
|
948
1113
|
const report = path.join(ROOT, "qa-artifacts", `maestro-${name}.xml`);
|
|
949
1114
|
fs.mkdirSync(path.dirname(report), { recursive: true });
|
|
950
1115
|
fs.rmSync(report, { force: true });
|
|
@@ -1244,7 +1409,7 @@ const stepsForProfile = {
|
|
|
1244
1409
|
// end-to-end lane — every pure-Node step through the REAL runner, marker,
|
|
1245
1410
|
// receipt and journal, and NO Gradle, no device, no network. Its job is to
|
|
1246
1411
|
// prove the framework RETURNS, fast, in both directions, before any real
|
|
1247
|
-
// work is pointed at it.
|
|
1412
|
+
// work is pointed at it. qa/framework-check.mjs drives it: PASS on a
|
|
1248
1413
|
// fresh scaffold, then FAIL BY NAME on one planted spec edit, each bounded
|
|
1249
1414
|
// in seconds. Its receipt is refused as done-evidence (qa/receipt-check.mjs)
|
|
1250
1415
|
// exactly like --fast: it proves the instrument, never the change.
|
|
@@ -1347,6 +1512,14 @@ const STEP_FN_BY_NAME = {
|
|
|
1347
1512
|
releaseSmoke: stepReleaseSmoke,
|
|
1348
1513
|
releaseBuild: stepReleaseBuild,
|
|
1349
1514
|
};
|
|
1515
|
+
// Where to look when one of THIS pack's steps blows its deadline. The spine
|
|
1516
|
+
// prints it verbatim and knows nothing about Gradle or adb (Stage 0 PR 6b.2);
|
|
1517
|
+
// marked on the step function, like `fn.layer`.
|
|
1518
|
+
const GRADLE_TIMEOUT_HINT = "A wedged Gradle daemon or a device that stopped answering are the usual causes; check `./gradlew --status` and `adb devices`.";
|
|
1519
|
+
for (const fn of [stepBuild, stepReleaseBuild, stepUnitTests, stepConformance, stepGoldenTrees, stepA11y, ...Object.values(STEP_FN_BY_NAME)]) {
|
|
1520
|
+
if (typeof fn === "function") fn.timeoutHint = GRADLE_TIMEOUT_HINT;
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1350
1523
|
for (const name of FAST_EXCLUDED_NAMES) {
|
|
1351
1524
|
if (!STEP_FN_BY_NAME[name]) {
|
|
1352
1525
|
// Drift guard: a new device-tier step must be mapped here or --fast would silently run it.
|
|
@@ -1356,11 +1529,40 @@ for (const name of FAST_EXCLUDED_NAMES) {
|
|
|
1356
1529
|
}
|
|
1357
1530
|
|
|
1358
1531
|
return {
|
|
1532
|
+
// WHICH PACK this is. The receipt names it (verify.mjs → receipt.pack) so a
|
|
1533
|
+
// cmp L2 and some other pack's L2 can never be mistaken for the same claim
|
|
1534
|
+
// on the wire. The pack declares its own identity — the spine writes what it
|
|
1535
|
+
// is told, never a name it assumed (docs/proposals/AGNOSTIC-HARNESS-
|
|
1536
|
+
// ARCHITECTURE.md §8.1, Stage 0 PR 1). Versioned with the harness until the
|
|
1537
|
+
// profile loader lands, so no `version` here yet: the spine pairs `id` with
|
|
1538
|
+
// the lock's version.
|
|
1539
|
+
id: "cmp",
|
|
1359
1540
|
stepsForProfile,
|
|
1360
1541
|
DEVICE_STEPS,
|
|
1542
|
+
// The verdict line's strength phrase. The core prints what it is handed and
|
|
1543
|
+
// nothing when handed nothing: "desktop-only" is THIS stack's word for the
|
|
1544
|
+
// absence of a device, not the harness's word for the absence of anything.
|
|
1545
|
+
// It used to be composed in verify.mjs, so a backend service with neither a
|
|
1546
|
+
// desktop nor a device read "(desktop-only)" on every verdict it ever printed.
|
|
1547
|
+
strengthLabel: (onDeviceSteps) => (onDeviceSteps.length ? `on-device: ${onDeviceSteps.join("+")}` : "desktop-only"),
|
|
1361
1548
|
FAST_EXCLUDED_NAMES,
|
|
1362
1549
|
STEP_FN_BY_NAME,
|
|
1363
1550
|
stepDeterminism,
|
|
1551
|
+
// The step whose failure makes every verdict behind it meaningless — for
|
|
1552
|
+
// this stack, `./gradlew :composeApp:assembleDebug`. The spine stops the
|
|
1553
|
+
// lane there (qa/lib/lane-runner.mjs's compileShortCircuit) and it used to
|
|
1554
|
+
// find this step by the literal name "build", which is Gradle's word for
|
|
1555
|
+
// the task and Gradle's word for the output directory, and nobody else's.
|
|
1556
|
+
// Declared here so the spine reads a name instead of assuming one; the
|
|
1557
|
+
// value is unchanged, so this lane stops exactly where it always did.
|
|
1558
|
+
compileStepName: "build",
|
|
1559
|
+
// The bounds on a step's derived deadline. These are the spine's historical
|
|
1560
|
+
// fallback to the millisecond, so this lane does not move — but they are a
|
|
1561
|
+
// judgement about how long a cold Gradle daemon plus an Android build can
|
|
1562
|
+
// honestly take, and that judgement belongs to whoever owns the toolchain.
|
|
1563
|
+
// Declared so the numbers have an owner rather than being inherited by
|
|
1564
|
+
// every stack that never chose them.
|
|
1565
|
+
stepDeadlines: { floorMs: 5 * 60_000, ceilingMs: 30 * 60_000 },
|
|
1364
1566
|
// The ladder this pack's steps can earn (evidence-level.mjs). A pack that
|
|
1365
1567
|
// returns none earns no rung — the spine never grades a pack by another
|
|
1366
1568
|
// pack's step names.
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// tarball rather than the working tree.
|
|
7
7
|
//
|
|
8
8
|
// SINGLE SOURCE OF TRUTH: packages/receipts/src/receipt-validate.mjs in the
|
|
9
|
-
// create-cmp repo (the
|
|
9
|
+
// create-cmp repo (the `prooflane-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
12
|
// `node scripts/sync-harness.mjs`.
|
|
@@ -74,20 +74,31 @@ export function checkLaneVouching(receipt) {
|
|
|
74
74
|
detail: `the receipt's verdict is PASS but ${failed.length} step(s) did not pass: ${names} — the row is the more specific truth`,
|
|
75
75
|
};
|
|
76
76
|
}
|
|
77
|
-
|
|
77
|
+
// THE ROW THAT VOUCHES IS THE ROW CARRYING THE VOUCHING DATA, not the row with
|
|
78
|
+
// a particular name. This found a step named exactly `harnessIntegrity` — a
|
|
79
|
+
// name the cmp pack chose, that `REQUIRED_EXPORTS` never mentions, and that a
|
|
80
|
+
// profile author has no way to discover. A green lane whose self-vouching step
|
|
81
|
+
// was spelled `harness_integrity` minted receipts that were invalid FOREVER,
|
|
82
|
+
// in every reader, and the refusal accused the lane of not vouching for
|
|
83
|
+
// itself. The `harness` object on a step row is what the schema already
|
|
84
|
+
// documents as the integrity check's own findings, so it is the honest key.
|
|
85
|
+
// The name is kept as a fallback for receipts written before rows carried it.
|
|
86
|
+
const integrity = steps.find((s) => s && s.harness && typeof s.harness === "object") ?? steps.find((s) => s && s.name === "harnessIntegrity");
|
|
78
87
|
if (!integrity) {
|
|
79
88
|
return {
|
|
80
89
|
ok: false,
|
|
81
|
-
detail:
|
|
90
|
+
detail:
|
|
91
|
+
"no step on this receipt vouches for the lane — no row carries a `harness` object and none is named harnessIntegrity, " +
|
|
92
|
+
"so nothing attests that the lane's own code is the code that ran",
|
|
82
93
|
};
|
|
83
94
|
}
|
|
84
95
|
if (integrity.verdict !== "PASS") {
|
|
85
96
|
return {
|
|
86
97
|
ok: false,
|
|
87
|
-
detail:
|
|
98
|
+
detail: `${integrity.name ?? "the integrity step"} is ${integrity.verdict}, not PASS — the lane did not vouch for itself, so its PASS over the tree cannot be trusted`,
|
|
88
99
|
};
|
|
89
100
|
}
|
|
90
|
-
return { ok: true, detail:
|
|
101
|
+
return { ok: true, detail: `lane vouched for itself (${integrity.name ?? "integrity step"} PASS, no failing rows)` };
|
|
91
102
|
}
|
|
92
103
|
|
|
93
104
|
export function evaluateReceipt(receipt, recompute) {
|
|
@@ -101,6 +112,33 @@ export function evaluateReceipt(receipt, recompute) {
|
|
|
101
112
|
};
|
|
102
113
|
}
|
|
103
114
|
|
|
115
|
+
// WHICH PACK GRADED THIS — ADR-0011. §8.9's comparability rule rests entirely
|
|
116
|
+
// on this field ("a `cmp` L2 and any other pack's L2 are different claims"),
|
|
117
|
+
// and until now no predicate read it: remove `pack` and a receipt kept its
|
|
118
|
+
// rung while losing the only thing that says what the rung is a rung OF.
|
|
119
|
+
//
|
|
120
|
+
// REFUSED, not flagged, and the cost is known. A receipt written before the
|
|
121
|
+
// field existed (2026-09-04) is refused too, because nothing in a receipt can
|
|
122
|
+
// tell "never had one" from "had one, and it was removed" — and of those two
|
|
123
|
+
// errors, accepting tampering is the one a predicate exists to prevent. The
|
|
124
|
+
// remedy is the one the binding check above already offers for the same class
|
|
125
|
+
// of staleness, and costs the same: re-run the lane.
|
|
126
|
+
//
|
|
127
|
+
// This is deliberately NOT the ADR-0007 case. There a label moved and no
|
|
128
|
+
// assertion changed, so invalidating old receipts would have been pure loss.
|
|
129
|
+
// Here the receipt is genuinely missing the field that makes its rung mean
|
|
130
|
+
// something — it is not being punished for a name, it is being asked for a
|
|
131
|
+
// claim it never made.
|
|
132
|
+
if (!receipt.pack || typeof receipt.pack.id !== "string" || receipt.pack.id.length === 0) {
|
|
133
|
+
return {
|
|
134
|
+
valid: false,
|
|
135
|
+
reason:
|
|
136
|
+
`receipt names no step pack — re-run the lane (attesting profile: ${profile ?? "unknown"}). ` +
|
|
137
|
+
`A rung is comparable only within its pack, so a receipt that does not name one cannot be compared to any other`,
|
|
138
|
+
profile,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
104
142
|
if (receipt.verdict === "FAIL") {
|
|
105
143
|
return {
|
|
106
144
|
valid: false,
|
|
@@ -146,12 +184,33 @@ export const DEFAULT_POLICY = {
|
|
|
146
184
|
/** A receipt older than this no longer counts as fresh (hosted check only). */
|
|
147
185
|
maxAgeMs: 30 * 24 * 60 * 60 * 1000, // 30 days
|
|
148
186
|
/**
|
|
149
|
-
*
|
|
150
|
-
*
|
|
151
|
-
*
|
|
152
|
-
*
|
|
187
|
+
* An absolute wall-time floor for executed (non-SKIP) gates. `null` — OFF by
|
|
188
|
+
* default, and that is a decision rather than an omission.
|
|
189
|
+
*
|
|
190
|
+
* This was 5000, with the reasoning that a PASS receipt summing to less
|
|
191
|
+
* cannot attest a real lane run: the tell for a replayed/cached green or a
|
|
192
|
+
* hand-written verdict. That reasoning holds for a Gradle lane and is FALSE
|
|
193
|
+
* for a Go service, a Rust crate, a Python package or a TypeScript library,
|
|
194
|
+
* whose lanes honestly finish in hundreds of milliseconds. Those adopters
|
|
195
|
+
* were told their evidence was fabricated — the one accusation this product
|
|
196
|
+
* cannot afford to make wrongly.
|
|
197
|
+
*
|
|
198
|
+
* The number was not the defect. ONE receipt carries nothing that could
|
|
199
|
+
* justify any number: no start time, no top-level duration, no baseline —
|
|
200
|
+
* `generatedAt` is a timestamp, not an interval — so nothing on it can be
|
|
201
|
+
* cross-checked against anything else on it. A floor is therefore a fact
|
|
202
|
+
* about the STACK, and this module does not know the stack. It is the
|
|
203
|
+
* notary's to set, from data the notary has and the receipt does not: a
|
|
204
|
+
* lane that has taken thirty seconds every day for a month and today claims
|
|
205
|
+
* forty-two milliseconds is a real finding, and it is a finding about a
|
|
206
|
+
* HISTORY, not about a receipt.
|
|
207
|
+
*
|
|
208
|
+
* What is lost, said plainly: a hand-written receipt claiming small
|
|
209
|
+
* durations is no longer refused here. It was never much of a defence — a
|
|
210
|
+
* forger types a larger number — and every stack-independent check that
|
|
211
|
+
* does catch fabrication is untouched below.
|
|
153
212
|
*/
|
|
154
|
-
minExecutedMs:
|
|
213
|
+
minExecutedMs: null,
|
|
155
214
|
};
|
|
156
215
|
|
|
157
216
|
/**
|
|
@@ -176,9 +235,15 @@ export function checkFreshness(receipt, { now = Date.now(), maxAgeMs = DEFAULT_P
|
|
|
176
235
|
}
|
|
177
236
|
|
|
178
237
|
/**
|
|
179
|
-
* Execution plausibility:
|
|
180
|
-
* real
|
|
181
|
-
*
|
|
238
|
+
* Execution plausibility: did this lane execute anything, and are its numbers
|
|
239
|
+
* real numbers?
|
|
240
|
+
*
|
|
241
|
+
* Three refusals, all stack-independent and all about the SHAPE of the
|
|
242
|
+
* evidence rather than its size: a receipt with no steps, a receipt whose every
|
|
243
|
+
* step is a SKIP or an ERROR (neither measured anything), and a step whose
|
|
244
|
+
* duration is not a finite non-negative number. An absolute wall-time floor is
|
|
245
|
+
* a fourth check and is OFF unless a caller sets `minExecutedMs` — see
|
|
246
|
+
* DEFAULT_POLICY for why a default one is a claim about the stack.
|
|
182
247
|
* @returns {{ok: boolean, detail: string, executedMs?: number, executedSteps?: number}}
|
|
183
248
|
*/
|
|
184
249
|
export function checkExecutionPlausibility(receipt, { minExecutedMs = DEFAULT_POLICY.minExecutedMs } = {}) {
|
|
@@ -200,10 +265,13 @@ export function checkExecutionPlausibility(receipt, { minExecutedMs = DEFAULT_PO
|
|
|
200
265
|
}
|
|
201
266
|
total += step.durationMs;
|
|
202
267
|
}
|
|
203
|
-
|
|
268
|
+
// Applied only when a caller supplies one. The message attributes the floor
|
|
269
|
+
// to whoever set it and states the measurement, rather than asserting that a
|
|
270
|
+
// fast receipt cannot be real — which this module has no way to know.
|
|
271
|
+
if (typeof minExecutedMs === "number" && minExecutedMs > 0 && total < minExecutedMs) {
|
|
204
272
|
return {
|
|
205
273
|
ok: false,
|
|
206
|
-
detail: `
|
|
274
|
+
detail: `executed gates report ${total}ms total, below this validator's configured ${minExecutedMs}ms floor — for a fast stack that may be honest, so treat it as a finding to explain rather than proof of fabrication`,
|
|
207
275
|
executedMs: total,
|
|
208
276
|
executedSteps: executed.length,
|
|
209
277
|
};
|
|
@@ -247,7 +315,7 @@ export function validateReceiptForTree({ root, now = Date.now(), policy = {} } =
|
|
|
247
315
|
if (receipt === null) {
|
|
248
316
|
return {
|
|
249
317
|
status: "missing",
|
|
250
|
-
reason: `no receipt at ${RECEIPT_REL_PATH} — this repo does not carry the
|
|
318
|
+
reason: `no receipt at ${RECEIPT_REL_PATH} — this repo does not carry the prooflane evidence harness (that is not a failure)`,
|
|
251
319
|
checks: [{ id: "receipt-present", ok: false, detail: `no parsable receipt at ${RECEIPT_REL_PATH}` }],
|
|
252
320
|
skips: [],
|
|
253
321
|
};
|