@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.
Files changed (83) hide show
  1. package/README.md +74 -10
  2. package/addon-command.mjs +3 -4
  3. package/adoption-evidence.mjs +650 -0
  4. package/android-play-upload.mjs +2 -1
  5. package/apple-profile-create.mjs +113 -10
  6. package/apple-profile-renew.mjs +11 -3
  7. package/artifact-storage.mjs +35 -2
  8. package/bin/darwin/rightkit-swap +0 -0
  9. package/binary-license-notices.mjs +161 -0
  10. package/build-release.mjs +138 -47
  11. package/cargo-license-notices.mjs +111 -0
  12. package/cli/right-release.mjs +21 -2
  13. package/codesign-environment.mjs +2 -2
  14. package/content-digest.mjs +209 -0
  15. package/development.mjs +17 -6
  16. package/direct-bootstrap.mjs +5 -6
  17. package/disk-command.mjs +1 -0
  18. package/disk-policy.json +6 -2
  19. package/disk-stable-root.mjs +13 -9
  20. package/github-updater-release.mjs +70 -7
  21. package/in-place-install.mjs +17 -14
  22. package/install-app-windows.mjs +24 -7
  23. package/install-app.mjs +21 -40
  24. package/ios-asc-credentials.mjs +163 -0
  25. package/ios-build-context.mjs +129 -0
  26. package/ios-build-upload.mjs +282 -0
  27. package/ios-credentials-command.mjs +70 -0
  28. package/ios-dev.mjs +248 -0
  29. package/ios-host.mjs +76 -0
  30. package/ios-lock.mjs +75 -0
  31. package/ios-package-recipe.mjs +707 -0
  32. package/ios-testflight.mjs +266 -0
  33. package/ios-upload.mjs +313 -0
  34. package/local-publish.mjs +222 -53
  35. package/managed-cargo.mjs +8 -0
  36. package/mirror-root-artifact.mjs +5 -1
  37. package/native-cargo-layout.mjs +47 -4
  38. package/ownership-command.mjs +2 -1
  39. package/ownership-dependency-allowlist.json +3 -1
  40. package/ownership-evidence.mjs +30 -18
  41. package/ownership-key-command.mjs +69 -2
  42. package/ownership-mandatory-set.json +117 -67
  43. package/ownership-mandatory-set.mjs +141 -21
  44. package/ownership-repositories.json +1 -1
  45. package/ownership-scope.mjs +94 -0
  46. package/ownership.mjs +49 -7
  47. package/package-recipe-targets.mjs +41 -1
  48. package/package.json +1 -1
  49. package/pipeline-admission.mjs +375 -0
  50. package/pipeline-config.mjs +100 -0
  51. package/pipeline-generators.json +4 -0
  52. package/pipeline-generators.mjs +137 -0
  53. package/pipeline-scan.mjs +285 -0
  54. package/pipeline-tools.mjs +171 -0
  55. package/preflight.mjs +14 -7
  56. package/production-mode.mjs +42 -0
  57. package/project-type.mjs +3 -1
  58. package/protected-credentials.mjs +28 -1
  59. package/publication-artifacts.mjs +30 -5
  60. package/publication-gate.mjs +94 -32
  61. package/publish-cargo.mjs +117 -25
  62. package/publish-swift.mjs +12 -3
  63. package/publish-update.mjs +15 -2
  64. package/qa-build-features.mjs +16 -13
  65. package/qa-contract.mjs +30 -2
  66. package/qa-control-scan.mjs +8 -0
  67. package/release-config.mjs +330 -0
  68. package/release-invocation.mjs +118 -8
  69. package/release-switch-registry.json +126 -27
  70. package/release-switch-scan.mjs +88 -21
  71. package/release.mjs +82 -20
  72. package/rightkit-ownership.json +1 -1
  73. package/rightkit-sync.mjs +8 -0
  74. package/rightkit-versions.json +428 -194
  75. package/runtime-budget-crates.mjs +14 -0
  76. package/runtime-budgets.mjs +248 -47
  77. package/sign-updater.mjs +56 -59
  78. package/source-gate.mjs +8 -1
  79. package/stage-sidecar.mjs +207 -40
  80. package/swap-helper-bundle.mjs +7 -3
  81. package/target-distribution.mjs +5 -0
  82. package/upload-release.mjs +7 -2
  83. 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
+ ]);
@@ -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 { isDarwinOnlyPolicy } from "./publication-gate.mjs";
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 const RUNTIME_BUDGET_CRATES = Object.freeze([
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
- return isDarwinOnlyPolicy(evidence ?? budget) ? ["darwin"] : ["darwin", "win32"];
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
- out.push(`- Wake-ups ceiling: ${figure("wakeupsPerSecond", budget.ceilings?.wakeupsPerSecond)}.`);
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
- 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}.`);
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 root = findWorkspaceRoot(crateDir, deps) ?? path.resolve(crateDir);
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
- add(path.resolve(crateDir), CRATE_PATHS);
242
- let dependencies = [];
243
- try { dependencies = pathDependencyDirs(crateDir, deps); } catch { /* unreadable manifest: the crate paths alone */ }
244
- for (const dir of dependencies) add(dir, DEPENDENCY_PATHS);
245
- return { root, specs: [...new Set(specs)] };
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 { name = readCargoPackage(crateDir, { exists, readFile }).name; } catch { /* no manifest: nothing to resolve in the lock */ }
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
- const now = exists(lockPath) ? readFile(lockPath, "utf8") : null;
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(lockClosure(now, name)) !== JSON.stringify(lockClosure(then.stdout, name))) {
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({ crate, crateDir, version, evidence, requireInstalled = false, app, heldResource = false }, { readFile = readFileSync, exists = existsSync, git = defaultGit } = {}) {
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("hostPolicy darwin-only needs a hostPolicyReason");
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 evidenceDarwinOnly = requiredBudgetHosts(null, evidence).length === 1;
464
- const budgetDarwinOnly = requiredBudgetHosts(budget).length === 1;
465
- if (evidenceDarwinOnly !== budgetDarwinOnly) {
466
- problems.push(`hostPolicy disagrees with the release evidence (evidence: ${evidenceDarwinOnly ? "darwin-only" : "macOS and Windows"}; ${RUNTIME_BUDGET_FILE}: ${budgetDarwinOnly ? "darwin-only" : "macOS and Windows"})`);
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
- for (const { m, index } of counted) {
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
- if (ceilings[key] !== undefined && m.observed[key] !== null && m.observed[key] > ceilings[key]) {
517
- problems.push(`measurement[${index}] on ${m.host.os}/${m.host.arch} exceeds the ${FIGURE_LABEL[key]} ceiling: observed ${m.observed[key]} > ${ceilings[key]}`);
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" : `; darwin-only policy: ${String((evidence ?? budget).hostPolicyReason).trim()}`})`); continue; }
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({ crate, crateDir, version, evidence, requireInstalled = false, app }, deps = {}) {
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 = evaluateRuntimeBudget({ crate, crateDir, version: version ?? pkg.version, evidence, requireInstalled, app }, deps);
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);