@rightkit/release 0.3.0 → 0.4.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/README.md +74 -10
- package/addon-command.mjs +3 -4
- package/adoption-evidence.mjs +650 -0
- package/android-play-upload.mjs +2 -1
- package/apple-profile-create.mjs +113 -10
- package/apple-profile-renew.mjs +11 -3
- package/artifact-storage.mjs +35 -2
- package/bin/darwin/rightkit-swap +0 -0
- package/binary-license-notices.mjs +161 -0
- package/build-release.mjs +138 -47
- package/cargo-license-notices.mjs +111 -0
- package/cli/right-release.mjs +21 -2
- package/codesign-environment.mjs +2 -2
- package/content-digest.mjs +209 -0
- package/development.mjs +17 -6
- package/direct-bootstrap.mjs +5 -6
- package/disk-command.mjs +1 -0
- package/disk-policy.json +6 -2
- package/disk-stable-root.mjs +13 -9
- package/github-updater-release.mjs +70 -7
- package/in-place-install.mjs +17 -14
- package/install-app-windows.mjs +24 -7
- package/install-app.mjs +21 -40
- package/ios-asc-credentials.mjs +163 -0
- package/ios-build-context.mjs +129 -0
- package/ios-build-upload.mjs +282 -0
- package/ios-credentials-command.mjs +70 -0
- package/ios-dev.mjs +248 -0
- package/ios-host.mjs +76 -0
- package/ios-lock.mjs +75 -0
- package/ios-package-recipe.mjs +707 -0
- package/ios-testflight.mjs +266 -0
- package/ios-upload.mjs +313 -0
- package/local-publish.mjs +222 -53
- package/managed-cargo.mjs +8 -0
- package/mirror-root-artifact.mjs +5 -1
- package/native-cargo-layout.mjs +47 -4
- package/ownership-command.mjs +2 -1
- package/ownership-dependency-allowlist.json +3 -1
- package/ownership-evidence.mjs +30 -18
- package/ownership-key-command.mjs +69 -2
- package/ownership-mandatory-set.json +117 -67
- package/ownership-mandatory-set.mjs +141 -21
- package/ownership-repositories.json +1 -1
- package/ownership-scope.mjs +94 -0
- package/ownership.mjs +49 -7
- package/package-recipe-targets.mjs +41 -1
- package/package.json +1 -1
- package/pipeline-admission.mjs +375 -0
- package/pipeline-config.mjs +100 -0
- package/pipeline-generators.json +4 -0
- package/pipeline-generators.mjs +137 -0
- package/pipeline-scan.mjs +285 -0
- package/pipeline-tools.mjs +171 -0
- package/preflight.mjs +14 -7
- package/production-mode.mjs +42 -0
- package/project-type.mjs +3 -1
- package/protected-credentials.mjs +28 -1
- package/publication-artifacts.mjs +30 -5
- package/publication-gate.mjs +94 -32
- package/publish-cargo.mjs +117 -25
- package/publish-swift.mjs +12 -3
- package/publish-update.mjs +15 -2
- package/qa-build-features.mjs +16 -13
- package/qa-contract.mjs +30 -2
- package/qa-control-scan.mjs +8 -0
- package/release-config.mjs +330 -0
- package/release-invocation.mjs +118 -8
- package/release-switch-registry.json +126 -27
- package/release-switch-scan.mjs +88 -21
- package/release.mjs +82 -20
- package/rightkit-ownership.json +1 -1
- package/rightkit-sync.mjs +8 -0
- package/rightkit-versions.json +428 -194
- package/runtime-budget-crates.mjs +14 -0
- package/runtime-budgets.mjs +248 -47
- package/sign-updater.mjs +56 -59
- package/source-gate.mjs +8 -1
- package/stage-sidecar.mjs +207 -40
- package/swap-helper-bundle.mjs +7 -3
- package/target-distribution.mjs +5 -0
- package/upload-release.mjs +7 -2
- package/bin/darwin/rightkit-swap.sign.json +0 -6
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// The continuous owners that must carry a runtime budget (docs/RUNTIME-BUDGETS.md). Kept free of
|
|
2
|
+
// dependencies so the secretless publish plan job can check the runtime proof table against it.
|
|
3
|
+
export const RUNTIME_BUDGET_CRATES = Object.freeze([
|
|
4
|
+
"rightkit-fsindex",
|
|
5
|
+
"rightkit-file-watch",
|
|
6
|
+
"rightkit-job-queue",
|
|
7
|
+
"rightkit-daemon",
|
|
8
|
+
"rightkit-cache",
|
|
9
|
+
"rightkit-sync",
|
|
10
|
+
"rightkit-logs",
|
|
11
|
+
"rightkit-tauri-shell",
|
|
12
|
+
"rightkit-qa",
|
|
13
|
+
"rightkit-send",
|
|
14
|
+
]);
|
package/runtime-budgets.mjs
CHANGED
|
@@ -18,6 +18,8 @@
|
|
|
18
18
|
// "hostPolicyReason": "no Windows backend",
|
|
19
19
|
// "ceilings": { "residentMemoryBytes": N, "idleCpuPercent": N, "wakeupsPerSecond": N, "diskOpsPerSecond": N }, // null = undeclared, refused
|
|
20
20
|
// "ceilingSources": { "residentMemoryBytes": "path:line" },
|
|
21
|
+
// "ceilingBasis": { "wakeupsPerSecond": "delta-over-bare" }, // optional; see "Delta basis" below
|
|
22
|
+
// "deltaCeilings": { "wakeupsPerSecond": N }, // required for, and only for, a figure with a delta basis
|
|
21
23
|
// "requiredScale": { "unit": "entries", "minCount": 1000000, "source": "path:line" }, // null = undeclared, refused
|
|
22
24
|
// "status": { "api": "Index::status()", "file": "src/index.rs", "exposes": ["residentMemoryBytes", ...] },
|
|
23
25
|
// "measurements": [ {
|
|
@@ -32,8 +34,20 @@
|
|
|
32
34
|
// Measurements are written by `node runtime-budgets.mjs record`, never typed: the raw bench output is
|
|
33
35
|
// stored under runtime-budget.evidence/, and the gate re-parses it and compares every value.
|
|
34
36
|
//
|
|
37
|
+
// Delta basis. A whole-process wake-up rate on a desktop host is dominated by host and window-server
|
|
38
|
+
// activity the crate does not own. A crate may declare `ceilingBasis.wakeupsPerSecond = "delta-over-bare"`:
|
|
39
|
+
// its synthetic bench then measures the crate's workload and a bare runtime loop back to back (separate
|
|
40
|
+
// processes), and the gate compares (wake-ups - bare wake-ups), computed here from the two raw figures,
|
|
41
|
+
// to `deltaCeilings.wakeupsPerSecond`. A synthetic measurement of that crate must carry `baseline` and
|
|
42
|
+
// `delta` (`{ wakeupsPerSecond }`); one without a baseline is refused, and a reported delta that differs
|
|
43
|
+
// from raw minus bare is refused (the gate never trusts it). `ceilings.wakeupsPerSecond` stays the
|
|
44
|
+
// whole-process ceiling and keeps governing every other scope (real, installed) and app adoption, so an
|
|
45
|
+
// installed app is never compared to a delta ceiling and nothing weakens for apps. Measurements of
|
|
46
|
+
// other versions are history and are not re-gated, so they need no baseline.
|
|
47
|
+
//
|
|
35
48
|
// Bench output the `record` command accepts (exactly one such line in the file):
|
|
36
49
|
// RESULT budget crate=<name> version=<semver> resident_bytes=<int> idle_cpu_percent=<n> wakeups_per_s=<n> disk_ops_per_s=<n> scale_unit=<word> scale_count=<int>
|
|
50
|
+
// optionally followed by `bare_wakeups_per_s=<n> wakeups_delta_per_s=<n>` (both or neither; delta may be negative).
|
|
37
51
|
//
|
|
38
52
|
// Check objects use the preflight shape ({ name, status, detail, fix }) so formatPreflight() in
|
|
39
53
|
// preflight.mjs prints them as `[FAIL] runtime-budget: ...`.
|
|
@@ -45,7 +59,8 @@ import path from "node:path";
|
|
|
45
59
|
import { fileURLToPath } from "node:url";
|
|
46
60
|
import { spawnSync } from "./background-process.mjs";
|
|
47
61
|
import { parse } from "smol-toml";
|
|
48
|
-
import {
|
|
62
|
+
import { RUNTIME_BUDGET_CRATES } from "./runtime-budget-crates.mjs";
|
|
63
|
+
import { singleHostOfPolicy } from "./publication-gate.mjs";
|
|
49
64
|
import { findWorkspaceRoot, lockClosure, pathDependencyDirs, readCargoPackage } from "./crate-graph.mjs";
|
|
50
65
|
|
|
51
66
|
export const RUNTIME_BUDGET_CHECK = "runtime-budget";
|
|
@@ -54,18 +69,7 @@ export const RUNTIME_BUDGET_FILE = "runtime-budget.json";
|
|
|
54
69
|
export const RUNTIME_BUDGET_EVIDENCE_DIR = "runtime-budget.evidence";
|
|
55
70
|
export const RUNTIME_BUDGET_SCHEMA = 1;
|
|
56
71
|
|
|
57
|
-
export
|
|
58
|
-
"rightkit-fsindex",
|
|
59
|
-
"rightkit-file-watch",
|
|
60
|
-
"rightkit-job-queue",
|
|
61
|
-
"rightkit-daemon",
|
|
62
|
-
"rightkit-cache",
|
|
63
|
-
"rightkit-sync",
|
|
64
|
-
"rightkit-logs",
|
|
65
|
-
"rightkit-tauri-shell",
|
|
66
|
-
"rightkit-qa",
|
|
67
|
-
"rightkit-send",
|
|
68
|
-
]);
|
|
72
|
+
export { RUNTIME_BUDGET_CRATES };
|
|
69
73
|
|
|
70
74
|
export const BUDGET_FIGURES = Object.freeze(["residentMemoryBytes", "idleCpuPercent", "wakeupsPerSecond", "diskOpsPerSecond"]);
|
|
71
75
|
const FIGURE_LABEL = Object.freeze({
|
|
@@ -83,6 +87,14 @@ const HOSTS = Object.freeze({ darwin: "macOS", win32: "Windows" });
|
|
|
83
87
|
const SHA = /^[0-9a-f]{40}$/;
|
|
84
88
|
const SHA256 = /^[0-9a-f]{64}$/;
|
|
85
89
|
const SCOPES = Object.freeze(["synthetic", "real", "installed"]);
|
|
90
|
+
// Figures that may be gated as a delta over a bare-runtime baseline, and the only scope that carries one.
|
|
91
|
+
export const CEILING_BASIS_DELTA = "delta-over-bare";
|
|
92
|
+
export const DELTA_FIGURES = Object.freeze(["wakeupsPerSecond"]);
|
|
93
|
+
const DELTA_SCOPE = "synthetic";
|
|
94
|
+
// Each rate is printed to 3 decimals, so raw - bare and the printed delta may differ by rounding only.
|
|
95
|
+
const DELTA_TOLERANCE = 0.002;
|
|
96
|
+
// A baseline more than this far above its own watchdog run is a mismatched or corrupt pair, not a quiet crate.
|
|
97
|
+
const DELTA_FLOOR = -2;
|
|
86
98
|
|
|
87
99
|
// Long-lived work a crate must declare. Each id must be named in `continuous_reason` for a `continuous = false`
|
|
88
100
|
// crate; for a held-resource crate every file that matches must be listed in `resources`.
|
|
@@ -132,7 +144,8 @@ export function isRuntimeBudgetCrate(crate, manifestText) {
|
|
|
132
144
|
|
|
133
145
|
/** Hosts the budget must be measured on. `evidence` (release evidence) is the policy authority when given. */
|
|
134
146
|
export function requiredBudgetHosts(budget, evidence) {
|
|
135
|
-
|
|
147
|
+
const single = singleHostOfPolicy(evidence ?? budget);
|
|
148
|
+
return single ? [single] : ["darwin", "win32"];
|
|
136
149
|
}
|
|
137
150
|
|
|
138
151
|
// ---------------------------------------------------------------- README view (generated from the JSON)
|
|
@@ -183,7 +196,10 @@ export function renderRuntimeBudgetBody(budget) {
|
|
|
183
196
|
out.push("");
|
|
184
197
|
out.push(`- Resident memory ceiling: ${figure("residentMemoryBytes", budget.ceilings?.residentMemoryBytes)}.`);
|
|
185
198
|
out.push(`- CPU ceiling (idle, steady state): ${figure("idleCpuPercent", budget.ceilings?.idleCpuPercent)}.`);
|
|
186
|
-
|
|
199
|
+
if (budget.ceilingBasis?.wakeupsPerSecond === CEILING_BASIS_DELTA) {
|
|
200
|
+
out.push(`- Wake-ups ceiling (whole process; installed and real measurements): ${figure("wakeupsPerSecond", budget.ceilings?.wakeupsPerSecond)}.`);
|
|
201
|
+
out.push(`- Wake-ups gate (synthetic bench): at most ${figure("wakeupsPerSecond", budget.deltaCeilings?.wakeupsPerSecond)} over a bare runtime loop measured in the same bench run.`);
|
|
202
|
+
} else out.push(`- Wake-ups ceiling: ${figure("wakeupsPerSecond", budget.ceilings?.wakeupsPerSecond)}.`);
|
|
187
203
|
out.push(`- Disk operations ceiling: ${figure("diskOpsPerSecond", budget.ceilings?.diskOpsPerSecond)}.`);
|
|
188
204
|
const rs = budget.requiredScale;
|
|
189
205
|
out.push(rs && rs.unit && Number.isFinite(rs.minCount) ? `- Required scale: ${count(rs.minCount)} ${rs.unit} or more.` : "- Required scale: undeclared.");
|
|
@@ -200,7 +216,9 @@ export function renderRuntimeBudgetBody(budget) {
|
|
|
200
216
|
const scale = `${count(m.dataScale?.count)} ${m.dataScale?.unit} (${m.dataScale?.description})`;
|
|
201
217
|
const app = m.scope === "installed" && m.app ? `, app ${m.app.name} ${m.app.version}` : "";
|
|
202
218
|
const observed = BUDGET_FIGURES.map((k) => `${FIGURE_LABEL[k]} ${m.observed?.[k] === undefined || m.observed?.[k] === null ? "n/a (not measurable on this host)" : FORMAT[k](m.observed[k])}`).join(", ");
|
|
203
|
-
|
|
219
|
+
const bare = isNumber(m.baseline?.wakeupsPerSecond) && isNumber(m.delta?.wakeupsPerSecond)
|
|
220
|
+
? `; bare baseline wake-ups ${FORMAT.wakeupsPerSecond(m.baseline.wakeupsPerSecond)}, delta ${FORMAT.wakeupsPerSecond(m.delta.wakeupsPerSecond)}` : "";
|
|
221
|
+
out.push(`- Measured: ${host}, ${budget.crate} ${m.crateVersion} at ${String(m.commit).slice(0, 12)}, ${scale}, ${m.scope}${app}, \`${m.command}\`, ${m.date}, evidence ${String(m.evidence?.sha256 ?? "none").slice(0, 12)}: ${observed}${bare}.`);
|
|
204
222
|
}
|
|
205
223
|
return out;
|
|
206
224
|
}
|
|
@@ -232,17 +250,86 @@ const posix = (p) => p.split(path.sep).join("/");
|
|
|
232
250
|
/**
|
|
233
251
|
* The git pathspecs (relative to the workspace root) whose change makes a measurement stale: the
|
|
234
252
|
* crate's src, benches, Cargo.toml (features) and build.rs, plus src, Cargo.toml and build.rs of every
|
|
235
|
-
* workspace crate it reaches through `path` dependencies (transitively). Returns { root, specs }.
|
|
253
|
+
* workspace crate it reaches through `path` dependencies (transitively). Returns { root, specs, problems }.
|
|
254
|
+
* Callers must refuse incomplete metadata rather than using a partial dependency scope.
|
|
236
255
|
*/
|
|
237
256
|
export function measuredPathspecs(crateDir, deps = {}) {
|
|
238
|
-
const
|
|
257
|
+
const { exists = existsSync, readFile = readFileSync } = deps;
|
|
258
|
+
const problems = [];
|
|
259
|
+
// Workspace discovery deliberately tolerates bad ancestor manifests elsewhere. Freshness cannot:
|
|
260
|
+
// a swallowed read/parse failure could hide inherited dependencies and shrink the measured graph.
|
|
261
|
+
const checkedRead = (file, encoding) => {
|
|
262
|
+
try {
|
|
263
|
+
const contents = readFile(file, encoding);
|
|
264
|
+
if (path.basename(file) === "Cargo.toml") parse(contents);
|
|
265
|
+
return contents;
|
|
266
|
+
} catch (error) {
|
|
267
|
+
problems.push(`cannot read dependency metadata ${file}: ${error.message}`);
|
|
268
|
+
throw error;
|
|
269
|
+
}
|
|
270
|
+
};
|
|
271
|
+
const checked = { exists, readFile: checkedRead };
|
|
272
|
+
let root = path.resolve(crateDir);
|
|
239
273
|
const specs = [];
|
|
240
274
|
const add = (dir, names) => { for (const name of names) specs.push(posix(path.relative(root, path.join(dir, name)))); };
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
275
|
+
try {
|
|
276
|
+
root = findWorkspaceRoot(crateDir, checked) ?? root;
|
|
277
|
+
add(path.resolve(crateDir), CRATE_PATHS);
|
|
278
|
+
const dependencies = pathDependencyDirs(crateDir, checked);
|
|
279
|
+
const workspace = exists(path.join(root, "Cargo.toml")) ? parse(checkedRead(path.join(root, "Cargo.toml"), "utf8")).workspace : undefined;
|
|
280
|
+
for (const dir of [path.resolve(crateDir), ...dependencies]) {
|
|
281
|
+
const manifestPath = path.join(dir, "Cargo.toml");
|
|
282
|
+
if (!exists(manifestPath)) {
|
|
283
|
+
// A manifest-free standalone path has no Cargo graph. A declared path dependency must exist.
|
|
284
|
+
if (dir !== path.resolve(crateDir)) problems.push(`dependency manifest is missing: ${manifestPath}`);
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
const manifest = parse(checkedRead(manifestPath, "utf8"));
|
|
288
|
+
if (!nonEmpty(manifest.package?.name)) problems.push(`dependency manifest has no package.name: ${manifestPath}`);
|
|
289
|
+
const holders = [manifest, ...Object.values(manifest.target ?? {})];
|
|
290
|
+
for (const holder of holders) {
|
|
291
|
+
for (const key of ["dependencies", "build-dependencies", ...(dir === path.resolve(crateDir) ? ["dev-dependencies"] : [])]) {
|
|
292
|
+
for (const [name, spec] of Object.entries(holder?.[key] ?? {})) {
|
|
293
|
+
if (spec && typeof spec === "object" && spec.workspace === true && workspace?.dependencies?.[name] === undefined) {
|
|
294
|
+
problems.push(`workspace dependency ${name} cannot be resolved for ${manifestPath}`);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
for (const dir of dependencies) add(dir, DEPENDENCY_PATHS);
|
|
301
|
+
} catch (error) {
|
|
302
|
+
if (!problems.length) problems.push(`cannot resolve measured dependency paths for ${crateDir}: ${error.message}`);
|
|
303
|
+
}
|
|
304
|
+
return { root, specs: [...new Set(specs)], problems: [...new Set(problems)] };
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// lockClosure compares reachable entries, but its permissive parser can return an empty/partial graph.
|
|
308
|
+
// Require the root and every reachable edge before allowing equality to establish freshness.
|
|
309
|
+
function completeLockClosure(contents, name) {
|
|
310
|
+
const packages = parse(contents).package;
|
|
311
|
+
if (!Array.isArray(packages)) throw new Error("missing [[package]] entries");
|
|
312
|
+
const roots = packages.filter((entry) => entry.name === name);
|
|
313
|
+
if (!roots.length) throw new Error(`missing package ${name}`);
|
|
314
|
+
const seen = new Set();
|
|
315
|
+
const pending = [...roots];
|
|
316
|
+
while (pending.length) {
|
|
317
|
+
const entry = pending.pop();
|
|
318
|
+
if (seen.has(entry)) continue;
|
|
319
|
+
seen.add(entry);
|
|
320
|
+
if (!nonEmpty(entry.name) || !nonEmpty(entry.version)) throw new Error(`incomplete package identity for ${entry.name ?? name}`);
|
|
321
|
+
if (entry.dependencies !== undefined && !Array.isArray(entry.dependencies)) throw new Error(`invalid dependencies for ${entry.name}`);
|
|
322
|
+
for (const edge of entry.dependencies ?? []) {
|
|
323
|
+
if (!nonEmpty(edge)) throw new Error(`invalid dependency edge for ${entry.name}`);
|
|
324
|
+
const match = /^(\S+)(?: (\S+))?(?: \(([^)]+)\))?$/.exec(edge);
|
|
325
|
+
if (!match) throw new Error(`invalid dependency ${edge} of ${entry.name}`);
|
|
326
|
+
const candidates = packages.filter((candidate) => candidate.name === match[1]
|
|
327
|
+
&& (!match[2] || candidate.version === match[2]) && (!match[3] || candidate.source === match[3]));
|
|
328
|
+
if (!candidates.length) throw new Error(`unresolved dependency ${edge} of ${entry.name}`);
|
|
329
|
+
pending.push(...candidates);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
return lockClosure(contents, name);
|
|
246
333
|
}
|
|
247
334
|
|
|
248
335
|
/**
|
|
@@ -261,7 +348,8 @@ export function measurementStaleness(commit, crateDir, git = defaultGit, deps =
|
|
|
261
348
|
const known = git(["cat-file", "-t", commit], crateDir);
|
|
262
349
|
if (known.status !== 0 || known.stdout !== "commit") return `commit ${commit.slice(0, 12)} is not a commit in this repository (shallow clone?)`;
|
|
263
350
|
if (git(["merge-base", "--is-ancestor", commit, "HEAD"], crateDir).status !== 0) return `commit ${commit.slice(0, 12)} is not an ancestor of HEAD`;
|
|
264
|
-
const { root, specs } = measuredPathspecs(crateDir, { exists, readFile });
|
|
351
|
+
const { root, specs, problems } = measuredPathspecs(crateDir, { exists, readFile });
|
|
352
|
+
if (problems.length) return `cannot resolve measured dependency graph: ${problems.join("; ")}`;
|
|
265
353
|
// `git diff <commit>` (no second revision) compares the working tree to the commit.
|
|
266
354
|
const changed = git(["diff", "--name-only", commit, "--", ...specs], root);
|
|
267
355
|
if (changed.status !== 0) return "cannot diff the crate against the measured commit";
|
|
@@ -270,17 +358,29 @@ export function measurementStaleness(commit, crateDir, git = defaultGit, deps =
|
|
|
270
358
|
if (untracked.status !== 0) return "cannot list untracked crate files";
|
|
271
359
|
if (untracked.stdout !== "") return `untracked files since the measurement: ${untracked.stdout.split("\n").slice(0, 5).join(", ")}`;
|
|
272
360
|
let name = null;
|
|
273
|
-
try {
|
|
361
|
+
try {
|
|
362
|
+
if (exists(path.join(crateDir, "Cargo.toml"))) {
|
|
363
|
+
name = readCargoPackage(crateDir, { exists, readFile }).name;
|
|
364
|
+
if (!nonEmpty(name)) return `cannot resolve Cargo.lock: ${path.join(crateDir, "Cargo.toml")} has no package.name`;
|
|
365
|
+
}
|
|
366
|
+
} catch (error) { return `cannot resolve Cargo.lock package identity: ${error.message}`; }
|
|
274
367
|
if (name) {
|
|
275
368
|
const lockPath = path.join(root, "Cargo.lock");
|
|
276
|
-
|
|
369
|
+
let now;
|
|
370
|
+
try { now = exists(lockPath) ? readFile(lockPath, "utf8") : null; }
|
|
371
|
+
catch (error) { return `cannot read Cargo.lock ${lockPath}: ${error.message}`; }
|
|
277
372
|
const then = git(["show", `${commit}:./Cargo.lock`], root);
|
|
278
373
|
const had = then.status === 0;
|
|
279
374
|
if (now === null && had) return "Cargo.lock was removed since the measurement";
|
|
280
375
|
if (now !== null && !had) return "Cargo.lock did not exist at the measured commit";
|
|
376
|
+
if (now === null && !had) {
|
|
377
|
+
// A failed show alone cannot distinguish an absent lock from an unreadable Git object.
|
|
378
|
+
const listed = git(["ls-tree", commit, "--", "Cargo.lock"], root);
|
|
379
|
+
if (listed.status !== 0 || listed.stdout !== "") return "cannot establish that Cargo.lock was absent at the measured commit";
|
|
380
|
+
}
|
|
281
381
|
if (now !== null) {
|
|
282
382
|
try {
|
|
283
|
-
if (JSON.stringify(
|
|
383
|
+
if (JSON.stringify(completeLockClosure(now, name)) !== JSON.stringify(completeLockClosure(then.stdout, name))) {
|
|
284
384
|
return `Cargo.lock changed in the dependency graph of ${name} since commit ${commit.slice(0, 12)}`;
|
|
285
385
|
}
|
|
286
386
|
} catch (error) { return `cannot compare Cargo.lock: ${error.message}`; }
|
|
@@ -297,6 +397,8 @@ export const HOST_UNAVAILABLE_FIGURES = Object.freeze({ darwin: Object.freeze(["
|
|
|
297
397
|
export const figureUnavailableOn = (os, key) => (HOST_UNAVAILABLE_FIGURES[os] ?? []).includes(key);
|
|
298
398
|
const RESULT_NUMBERS = Object.freeze({ resident_bytes: "residentMemoryBytes", idle_cpu_percent: "idleCpuPercent", wakeups_per_s: "wakeupsPerSecond", disk_ops_per_s: "diskOpsPerSecond", scale_count: "scaleCount" });
|
|
299
399
|
const RESULT_WORDS = Object.freeze(["crate", "version", "scale_unit"]);
|
|
400
|
+
// Optional pair emitted by a delta-basis bench: the bare-runtime wake-up rate and the bench's own reported delta.
|
|
401
|
+
const RESULT_DELTA = Object.freeze({ bare_wakeups_per_s: "bareWakeupsPerSecond", wakeups_delta_per_s: "wakeupsDeltaPerSecond" });
|
|
300
402
|
|
|
301
403
|
/** Parses the one `RESULT budget ...` line of a bench's raw output. */
|
|
302
404
|
export function parseBudgetResult(text) {
|
|
@@ -308,7 +410,7 @@ export function parseBudgetResult(text) {
|
|
|
308
410
|
const eq = token.indexOf("=");
|
|
309
411
|
const key = eq > 0 ? token.slice(0, eq) : token;
|
|
310
412
|
const value = eq > 0 ? token.slice(eq + 1) : "";
|
|
311
|
-
if (!(key in RESULT_NUMBERS) && !RESULT_WORDS.includes(key)) problems.push(`unknown field ${key}`);
|
|
413
|
+
if (!(key in RESULT_NUMBERS) && !(key in RESULT_DELTA) && !RESULT_WORDS.includes(key)) problems.push(`unknown field ${key}`);
|
|
312
414
|
else if (key in fields) problems.push(`duplicate field ${key}`);
|
|
313
415
|
else fields[key] = value;
|
|
314
416
|
}
|
|
@@ -320,6 +422,14 @@ export function parseBudgetResult(text) {
|
|
|
320
422
|
else if (fields[key] === undefined || fields[key] === "" || !Number.isFinite(n) || n < 0) problems.push(`field ${key} must be a non-negative number`);
|
|
321
423
|
else value[name] = n;
|
|
322
424
|
}
|
|
425
|
+
const present = Object.keys(RESULT_DELTA).filter((key) => key in fields);
|
|
426
|
+
if (present.length === 1) problems.push(`fields ${Object.keys(RESULT_DELTA).join(" and ")} must appear together`);
|
|
427
|
+
for (const key of present) {
|
|
428
|
+
const n = Number(fields[key]);
|
|
429
|
+
// The delta is a difference and may be negative; the bare rate is a rate and may not.
|
|
430
|
+
if (fields[key] === "" || !Number.isFinite(n) || (key === "bare_wakeups_per_s" && n < 0)) problems.push(`field ${key} must be a ${key === "bare_wakeups_per_s" ? "non-negative " : ""}number`);
|
|
431
|
+
else value[RESULT_DELTA[key]] = n;
|
|
432
|
+
}
|
|
323
433
|
return problems.length ? { ok: false, problems } : { ok: true, problems: [], value };
|
|
324
434
|
}
|
|
325
435
|
|
|
@@ -343,6 +453,14 @@ function checkEvidence(m, index, budget, crateDir, { exists, readFile }) {
|
|
|
343
453
|
if (v.version !== m.crateVersion) problems.push(`evidence version ${v.version} != recorded ${m.crateVersion}`);
|
|
344
454
|
for (const key of BUDGET_FIGURES) if (v[key] !== m.observed[key]) problems.push(`evidence ${key} ${v[key]} != recorded ${m.observed[key]}`);
|
|
345
455
|
if (v.scale_unit !== m.dataScale.unit || v.scaleCount !== m.dataScale.count) problems.push("evidence data scale differs from the record");
|
|
456
|
+
// The baseline and delta belong to the record exactly when the raw output carries them.
|
|
457
|
+
const recordedBare = m.baseline?.wakeupsPerSecond;
|
|
458
|
+
const recordedDelta = m.delta?.wakeupsPerSecond;
|
|
459
|
+
if (v.bareWakeupsPerSecond !== recordedBare) problems.push(`evidence bare_wakeups_per_s ${v.bareWakeupsPerSecond} != recorded baseline ${recordedBare}`);
|
|
460
|
+
if (v.wakeupsDeltaPerSecond !== recordedDelta) problems.push(`evidence wakeups_delta_per_s ${v.wakeupsDeltaPerSecond} != recorded delta ${recordedDelta}`);
|
|
461
|
+
if (v.bareWakeupsPerSecond !== undefined && !(isNumber(v.wakeupsPerSecond) && Math.abs(v.wakeupsPerSecond - v.bareWakeupsPerSecond - v.wakeupsDeltaPerSecond) <= DELTA_TOLERANCE)) {
|
|
462
|
+
problems.push(`evidence wakeups_delta_per_s ${v.wakeupsDeltaPerSecond} disagrees with wakeups_per_s ${v.wakeupsPerSecond} minus bare_wakeups_per_s ${v.bareWakeupsPerSecond}`);
|
|
463
|
+
}
|
|
346
464
|
return problems.map((p) => `${at} ${p}`);
|
|
347
465
|
}
|
|
348
466
|
|
|
@@ -423,9 +541,19 @@ function validateMeasurement(m, index) {
|
|
|
423
541
|
if (isNumber(m.observed?.residentMemoryBytes) && m.observed.residentMemoryBytes === 0) {
|
|
424
542
|
problems.push(`${at} observed.residentMemoryBytes of 0 is implausible for a running process`);
|
|
425
543
|
}
|
|
544
|
+
// Optional bare-runtime baseline and reported delta: shape only here; the gate recomputes the delta.
|
|
545
|
+
for (const [field, signed] of [["baseline", false], ["delta", true]]) {
|
|
546
|
+
if (m[field] === undefined) continue;
|
|
547
|
+
const ok = m[field] && typeof m[field] === "object" && Object.keys(m[field]).every((key) => DELTA_FIGURES.includes(key) && isNumber(m[field][key]) && (signed || m[field][key] >= 0));
|
|
548
|
+
if (!ok) problems.push(`${at} ${field} must map ${DELTA_FIGURES.join(", ")} to ${signed ? "numbers" : "non-negative numbers"}`);
|
|
549
|
+
}
|
|
550
|
+
if ((m.baseline === undefined) !== (m.delta === undefined)) problems.push(`${at} baseline and delta must be recorded together`);
|
|
426
551
|
return problems;
|
|
427
552
|
}
|
|
428
553
|
|
|
554
|
+
/** Figures of `budget` gated as a delta over the bare baseline (validated separately; unknown bases never match). */
|
|
555
|
+
const deltaFiguresOf = (budget) => DELTA_FIGURES.filter((key) => budget.ceilingBasis?.[key] === CEILING_BASIS_DELTA);
|
|
556
|
+
|
|
429
557
|
const escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
430
558
|
|
|
431
559
|
/** True when `api` (`Type::method()` or a bare type) is a `pub fn` / `pub struct|enum|trait` item in `source`, not in a comment. */
|
|
@@ -444,7 +572,14 @@ export function statusApiDeclared(api, source) {
|
|
|
444
572
|
* `evidence` is the release gate evidence for it (the host-policy authority). `requireInstalled`
|
|
445
573
|
* (adoption) additionally needs a fresh in-ceiling installed-app measurement, optionally for `app`.
|
|
446
574
|
*/
|
|
447
|
-
export function evaluateRuntimeBudget(
|
|
575
|
+
export function evaluateRuntimeBudget(options, deps = {}) {
|
|
576
|
+
return evaluateRuntimeBudgetOwned(options, deps);
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// Private owner mode: no caller-supplied boolean can weaken publication or adoption.
|
|
580
|
+
const STRUCTURE_ONLY = Symbol("runtime-budget-structure-only");
|
|
581
|
+
function evaluateRuntimeBudgetOwned({ crate, crateDir, version, evidence, requireInstalled = false, app, heldResource = false }, { readFile = readFileSync, exists = existsSync, git = defaultGit } = {}, mode) {
|
|
582
|
+
const structural = mode === STRUCTURE_ONLY;
|
|
448
583
|
const file = path.join(crateDir, RUNTIME_BUDGET_FILE);
|
|
449
584
|
if (!exists(file)) return { ok: false, problems: [`${RUNTIME_BUDGET_FILE} is missing (docs/RUNTIME-BUDGETS.md)`] };
|
|
450
585
|
let budget;
|
|
@@ -456,14 +591,15 @@ export function evaluateRuntimeBudget({ crate, crateDir, version, evidence, requ
|
|
|
456
591
|
if (budget.schema !== RUNTIME_BUDGET_SCHEMA) problems.push(`${RUNTIME_BUDGET_FILE} schema must be ${RUNTIME_BUDGET_SCHEMA}`);
|
|
457
592
|
if (budget.crate !== crate) problems.push(`${RUNTIME_BUDGET_FILE} is for ${budget.crate}, expected ${crate}`);
|
|
458
593
|
if (!nonEmpty(version)) problems.push("publishing version is unknown, cannot bind the measurements to it");
|
|
459
|
-
if (budget.hostPolicy !== undefined && budget.hostPolicy !== "darwin-only") problems.push('hostPolicy must be "darwin-only" when present');
|
|
460
|
-
if (budget.hostPolicy === "darwin-only" && !nonEmpty(budget.hostPolicyReason)) problems.push(
|
|
594
|
+
if (budget.hostPolicy !== undefined && budget.hostPolicy !== "darwin-only" && budget.hostPolicy !== "win32-only") problems.push('hostPolicy must be "darwin-only" or "win32-only" when present');
|
|
595
|
+
if ((budget.hostPolicy === "darwin-only" || budget.hostPolicy === "win32-only") && !nonEmpty(budget.hostPolicyReason)) problems.push(`hostPolicy ${budget.hostPolicy} needs a hostPolicyReason`);
|
|
461
596
|
// One source of truth for the host policy: the release evidence. The budget file may repeat it, never differ.
|
|
462
597
|
if (evidence) {
|
|
463
|
-
const
|
|
464
|
-
const
|
|
465
|
-
|
|
466
|
-
|
|
598
|
+
const policyLabel = (hosts) => (hosts.length === 1 ? `${hosts[0]}-only` : "macOS and Windows");
|
|
599
|
+
const evidenceHosts = requiredBudgetHosts(null, evidence);
|
|
600
|
+
const budgetHosts = requiredBudgetHosts(budget);
|
|
601
|
+
if (evidenceHosts.join() !== budgetHosts.join()) {
|
|
602
|
+
problems.push(`hostPolicy disagrees with the release evidence (evidence: ${policyLabel(evidenceHosts)}; ${RUNTIME_BUDGET_FILE}: ${policyLabel(budgetHosts)})`);
|
|
467
603
|
}
|
|
468
604
|
}
|
|
469
605
|
|
|
@@ -476,6 +612,24 @@ export function evaluateRuntimeBudget({ crate, crateDir, version, evidence, requ
|
|
|
476
612
|
else ceilings[key] = value;
|
|
477
613
|
}
|
|
478
614
|
|
|
615
|
+
// Ceiling basis: an explicit, closed declaration. A delta figure needs its own positive delta ceiling,
|
|
616
|
+
// and a delta ceiling without the basis would be dead text, so both directions are refused.
|
|
617
|
+
const basis = budget.ceilingBasis;
|
|
618
|
+
if (basis !== undefined && (!basis || typeof basis !== "object" || Array.isArray(basis))) problems.push("ceilingBasis must be an object");
|
|
619
|
+
else for (const [key, value] of Object.entries(basis ?? {})) {
|
|
620
|
+
if (!DELTA_FIGURES.includes(key)) problems.push(`ceilingBasis.${key} is not a figure that can use a delta basis (${DELTA_FIGURES.join(", ")})`);
|
|
621
|
+
else if (value !== CEILING_BASIS_DELTA) problems.push(`ceilingBasis.${key} must be "${CEILING_BASIS_DELTA}"`);
|
|
622
|
+
}
|
|
623
|
+
const deltaFigures = deltaFiguresOf(budget);
|
|
624
|
+
const deltaCeilings = {};
|
|
625
|
+
for (const key of DELTA_FIGURES) {
|
|
626
|
+
const value = budget.deltaCeilings?.[key];
|
|
627
|
+
if (deltaFigures.includes(key)) {
|
|
628
|
+
if (!isNumber(value) || value <= 0) problems.push(`deltaCeilings.${key} must be a positive number (ceilingBasis.${key} is ${CEILING_BASIS_DELTA})`);
|
|
629
|
+
else deltaCeilings[key] = value;
|
|
630
|
+
} else if (value !== undefined) problems.push(`deltaCeilings.${key} is declared but ceilingBasis.${key} is not ${CEILING_BASIS_DELTA}`);
|
|
631
|
+
}
|
|
632
|
+
|
|
479
633
|
// Required scale: the unit and minimum the crate's measurement must reach.
|
|
480
634
|
const scale = budget.requiredScale;
|
|
481
635
|
let requiredScale = null;
|
|
@@ -499,7 +653,7 @@ export function evaluateRuntimeBudget({ crate, crateDir, version, evidence, requ
|
|
|
499
653
|
|
|
500
654
|
// Measurements.
|
|
501
655
|
const records = Array.isArray(budget.measurements) ? budget.measurements : [];
|
|
502
|
-
if (records.length === 0) problems.push("no measurements recorded (a declared ceiling is not evidence)");
|
|
656
|
+
if (!structural && records.length === 0) problems.push("no measurements recorded (a declared ceiling is not evidence)");
|
|
503
657
|
const valid = [];
|
|
504
658
|
records.forEach((m, index) => {
|
|
505
659
|
const bad = validateMeasurement(m, index);
|
|
@@ -508,18 +662,47 @@ export function evaluateRuntimeBudget({ crate, crateDir, version, evidence, requ
|
|
|
508
662
|
if (evidenceProblems.length) problems.push(...evidenceProblems); else valid.push({ m, index });
|
|
509
663
|
});
|
|
510
664
|
const counted = valid.filter(({ m }) => m.crateVersion === version);
|
|
511
|
-
if (valid.length && nonEmpty(version) && counted.length === 0) {
|
|
665
|
+
if (!structural && valid.length && nonEmpty(version) && counted.length === 0) {
|
|
512
666
|
problems.push(`no measurement for version ${version} (recorded: ${[...new Set(valid.map(({ m }) => m.crateVersion))].join(", ")})`);
|
|
513
667
|
}
|
|
514
|
-
|
|
668
|
+
// Why a measurement fails one figure, or null. A delta-basis figure of a synthetic measurement is gated on
|
|
669
|
+
// observed - baseline, recomputed here; the reported delta must agree and the baseline must exist. Every
|
|
670
|
+
// other scope (real, installed) keeps the whole-process ceiling, so an app is never held to a delta.
|
|
671
|
+
const usesDelta = (m, key) => deltaFigures.includes(key) && m.scope === DELTA_SCOPE;
|
|
672
|
+
const figureFailure = (m, key) => {
|
|
673
|
+
if (usesDelta(m, key)) {
|
|
674
|
+
const raw = m.observed[key];
|
|
675
|
+
const bare = m.baseline?.[key];
|
|
676
|
+
const reported = m.delta?.[key];
|
|
677
|
+
if (deltaCeilings[key] === undefined) return null; // the declaration problem is already reported
|
|
678
|
+
if (!isNumber(raw)) return `${FIGURE_LABEL[key]} is not measurable here but the ${FIGURE_LABEL[key]} ceiling is a delta over a bare baseline`;
|
|
679
|
+
if (!isNumber(bare) || !isNumber(reported)) return `has no bare baseline for ${FIGURE_LABEL[key]} (ceilingBasis.${key} is ${CEILING_BASIS_DELTA}; re-run the bench and record it again)`;
|
|
680
|
+
const delta = Number((raw - bare).toFixed(3)); // the bench prints 3 decimals; compare at that precision
|
|
681
|
+
if (Math.abs(delta - reported) > DELTA_TOLERANCE) return `reported ${FIGURE_LABEL[key]} delta ${reported} disagrees with observed ${raw} minus baseline ${bare}`;
|
|
682
|
+
if (delta < DELTA_FLOOR) return `bare baseline ${bare} is more than ${-DELTA_FLOOR} ${FIGURE_LABEL[key]}/s above observed ${raw}: a mismatched pair; re-run the bench`;
|
|
683
|
+
return delta > deltaCeilings[key] ? `exceeds the ${FIGURE_LABEL[key]} delta ceiling over the bare baseline: ${raw} - ${bare} = ${delta} > ${deltaCeilings[key]}` : null;
|
|
684
|
+
}
|
|
685
|
+
if (ceilings[key] !== undefined && m.observed[key] !== null && m.observed[key] > ceilings[key]) {
|
|
686
|
+
return `exceeds the ${FIGURE_LABEL[key]} ceiling: observed ${m.observed[key]} > ${ceilings[key]}`;
|
|
687
|
+
}
|
|
688
|
+
return null;
|
|
689
|
+
};
|
|
690
|
+
for (const { m, index } of (structural ? valid : counted)) {
|
|
515
691
|
for (const key of BUDGET_FIGURES) {
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
}
|
|
692
|
+
const failure = figureFailure(m, key);
|
|
693
|
+
if (failure) problems.push(`measurement[${index}] on ${m.host.os}/${m.host.arch} ${failure}`);
|
|
519
694
|
}
|
|
520
695
|
}
|
|
521
|
-
const withinCeilings = (m) => BUDGET_FIGURES.every((key) => ceilings[key] !== undefined && (m.observed[key] === null ? figureUnavailableOn(m.host?.os, key) : m.observed[key] <= ceilings[key]));
|
|
696
|
+
const withinCeilings = (m) => BUDGET_FIGURES.every((key) => ceilings[key] !== undefined && (usesDelta(m, key) ? deltaCeilings[key] !== undefined && figureFailure(m, key) === null : m.observed[key] === null ? figureUnavailableOn(m.host?.os, key) : m.observed[key] <= ceilings[key]));
|
|
522
697
|
const atScale = (m) => requiredScale !== null && m.dataScale.unit === requiredScale.unit && m.dataScale.count >= requiredScale.minCount;
|
|
698
|
+
if (structural) {
|
|
699
|
+
if (!Array.isArray(budget.measurements)) problems.push("measurements must be an array");
|
|
700
|
+
const scope = measuredPathspecs(crateDir, { exists, readFile });
|
|
701
|
+
problems.push(...scope.problems);
|
|
702
|
+
for (const key of deltaFigures) if (!nonEmpty(budget.deltaSources?.[key])) problems.push(`deltaSources.${key} must name its ceiling source`);
|
|
703
|
+
if (budget.deltaSources !== undefined && (!budget.deltaSources || typeof budget.deltaSources !== "object" || Array.isArray(budget.deltaSources) || Object.keys(budget.deltaSources).some((key) => !deltaFigures.includes(key)))) problems.push("deltaSources must name only declared delta figures");
|
|
704
|
+
for (const { m, index } of valid) if (!atScale(m)) problems.push(`measurement[${index}] is below required scale`);
|
|
705
|
+
}
|
|
523
706
|
const staleness = new Map();
|
|
524
707
|
const stale = (m) => {
|
|
525
708
|
if (!staleness.has(m.commit)) staleness.set(m.commit, measurementStaleness(m.commit, crateDir, git, { exists, readFile }));
|
|
@@ -528,10 +711,10 @@ export function evaluateRuntimeBudget({ crate, crateDir, version, evidence, requ
|
|
|
528
711
|
|
|
529
712
|
// Required hosts: one fresh, in-ceiling, at-scale measurement of this version on each.
|
|
530
713
|
// A held-resource crate defers measurement to adoption: no per-host publish measurement is required.
|
|
531
|
-
const required = heldResource ? [] : requiredBudgetHosts(budget, evidence);
|
|
714
|
+
const required = heldResource || structural ? [] : requiredBudgetHosts(budget, evidence);
|
|
532
715
|
for (const host of required) {
|
|
533
716
|
const onHost = counted.filter(({ m }) => m.host.os === host);
|
|
534
|
-
if (onHost.length === 0) { problems.push(`no ${HOSTS[host]} measurement for version ${version} (required hosts: ${required.map((h) => HOSTS[h]).join(" and ")}${required.length > 1 ? "; apps ship on both and the release gate requires both unless a darwin-only policy with a reason is recorded" : `;
|
|
717
|
+
if (onHost.length === 0) { problems.push(`no ${HOSTS[host]} measurement for version ${version} (required hosts: ${required.map((h) => HOSTS[h]).join(" and ")}${required.length > 1 ? "; apps ship on both and the release gate requires both unless a darwin-only policy with a reason is recorded" : `; ${required[0]}-only policy: ${String((evidence ?? budget).hostPolicyReason).trim()}`})`); continue; }
|
|
535
718
|
const fresh = onHost.filter(({ m }) => stale(m) === null);
|
|
536
719
|
if (fresh.length === 0) { problems.push(`${HOSTS[host]} measurement is stale: ${onHost.map(({ m }) => stale(m)).join("; ")}`); continue; }
|
|
537
720
|
const within = fresh.filter(({ m }) => withinCeilings(m));
|
|
@@ -632,7 +815,17 @@ export function heldResourceProblems({ crate, crateDir, meta }, { readFile = rea
|
|
|
632
815
|
* naming every matched pattern), "held-resource" (resources table, adoption-time proof) or always-on
|
|
633
816
|
* (runtime-budget.json, fully checked). `readFile`, `exists` and `git` are injectable.
|
|
634
817
|
*/
|
|
635
|
-
export function runtimeBudgetCheck(
|
|
818
|
+
export function runtimeBudgetCheck(options, deps = {}) {
|
|
819
|
+
return runtimeBudgetCheckOwned(options, deps);
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
/** Source structure only; grants no runtime qualification or semantic admission. */
|
|
823
|
+
export function runtimeBudgetStructureCheck(options, deps = {}) {
|
|
824
|
+
const result = runtimeBudgetCheckOwned({ ...options, requireInstalled: false }, deps, STRUCTURE_ONLY);
|
|
825
|
+
return { ...result, name: "runtime-budget-structure" };
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
function runtimeBudgetCheckOwned({ crate, crateDir, version, evidence, requireInstalled = false, app }, deps = {}, mode) {
|
|
636
829
|
const { readFile = readFileSync, exists = existsSync } = deps;
|
|
637
830
|
let pkg;
|
|
638
831
|
try { pkg = readCargoPackage(crateDir, { exists, readFile }); } catch (error) {
|
|
@@ -679,9 +872,9 @@ export function runtimeBudgetCheck({ crate, crateDir, version, evidence, require
|
|
|
679
872
|
return fail(crate, crateDir, [`undeclared: none of ${RUNTIME_BUDGET_FILE}, continuous = false with a continuous_reason, or continuous = "held-resource" with resources`],
|
|
680
873
|
`declare the crate (docs/RUNTIME-BUDGETS.md)`);
|
|
681
874
|
}
|
|
682
|
-
const result =
|
|
875
|
+
const result = evaluateRuntimeBudgetOwned({ crate, crateDir, version: version ?? pkg.version, evidence, requireInstalled, app }, deps, mode);
|
|
683
876
|
if (result.ok) {
|
|
684
|
-
return { name: RUNTIME_BUDGET_CHECK, status: "ok", detail: `${crate} ${version ?? pkg.version} has measured runtime budgets on every required host` };
|
|
877
|
+
return { name: RUNTIME_BUDGET_CHECK, status: "ok", detail: mode === STRUCTURE_ONLY ? `${crate} ${version ?? pkg.version} runtime budget structure is valid; qualification is separate` : `${crate} ${version ?? pkg.version} has measured runtime budgets on every required host` };
|
|
685
878
|
}
|
|
686
879
|
return fail(crate, crateDir, result.problems);
|
|
687
880
|
}
|
|
@@ -839,6 +1032,13 @@ export function recordMeasurement({ crateDir, resultFile, scope, command, descri
|
|
|
839
1032
|
if (stale) throw new Error(`refusing to record: ${stale} (commit the code under test, then re-run the bench)`);
|
|
840
1033
|
const file = path.join(crateDir, RUNTIME_BUDGET_FILE);
|
|
841
1034
|
const budget = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
1035
|
+
const hasBaseline = v.bareWakeupsPerSecond !== undefined;
|
|
1036
|
+
if (scope === DELTA_SCOPE && !hasBaseline && deltaFiguresOf(budget).length) {
|
|
1037
|
+
throw new Error(`bench output rejected: ${RUNTIME_BUDGET_FILE} gates ${deltaFiguresOf(budget).join(", ")} as ${CEILING_BASIS_DELTA}, so the RESULT line needs bare_wakeups_per_s and wakeups_delta_per_s`);
|
|
1038
|
+
}
|
|
1039
|
+
if (hasBaseline && !(isNumber(v.wakeupsPerSecond) && Math.abs(v.wakeupsPerSecond - v.bareWakeupsPerSecond - v.wakeupsDeltaPerSecond) <= DELTA_TOLERANCE)) {
|
|
1040
|
+
throw new Error("bench output rejected: wakeups_delta_per_s disagrees with wakeups_per_s minus bare_wakeups_per_s");
|
|
1041
|
+
}
|
|
842
1042
|
const evidenceName = `${host.os}-${host.arch}-${pkg.version}-${scope}${scope === "installed" ? `-${app.name}` : ""}.txt`.replace(/[^\w.-]/g, "_");
|
|
843
1043
|
const evidenceRel = `${RUNTIME_BUDGET_EVIDENCE_DIR}/${evidenceName}`;
|
|
844
1044
|
fs.mkdirSync(path.join(crateDir, RUNTIME_BUDGET_EVIDENCE_DIR), { recursive: true });
|
|
@@ -849,6 +1049,7 @@ export function recordMeasurement({ crateDir, resultFile, scope, command, descri
|
|
|
849
1049
|
dataScale: { description, unit: v.scale_unit, count: v.scaleCount },
|
|
850
1050
|
command,
|
|
851
1051
|
observed: Object.fromEntries(BUDGET_FIGURES.map((key) => [key, v[key]])),
|
|
1052
|
+
...(hasBaseline ? { baseline: { wakeupsPerSecond: v.bareWakeupsPerSecond }, delta: { wakeupsPerSecond: v.wakeupsDeltaPerSecond } } : {}),
|
|
852
1053
|
evidence: { file: evidenceRel, sha256: sha256Of(raw) },
|
|
853
1054
|
};
|
|
854
1055
|
const same = (m) => m.host?.os === host.os && m.host?.arch === host.arch && m.crateVersion === pkg.version && m.scope === scope && (m.app?.name ?? null) === (app?.name ?? null);
|