@rightkit/release 0.2.174 → 0.4.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.
Files changed (82) hide show
  1. package/README.md +200 -2
  2. package/addon-command.mjs +3 -4
  3. package/android-play-upload.mjs +2 -1
  4. package/apple-profile-renew.mjs +11 -3
  5. package/artifact-storage.mjs +35 -2
  6. package/bin/darwin/rightkit-swap +0 -0
  7. package/bin/darwin/rightkit-swap.sign.json +1 -1
  8. package/build-release.mjs +138 -47
  9. package/cli/right-release.mjs +40 -5
  10. package/codesign-environment.mjs +2 -2
  11. package/command-spec.mjs +80 -0
  12. package/content-digest.mjs +209 -0
  13. package/deps-cargo.mjs +55 -0
  14. package/deps.mjs +7 -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/dist-scan.mjs +102 -0
  21. package/dmg-build.mjs +53 -4
  22. package/github-updater-release.mjs +70 -7
  23. package/icon-pack.mjs +117 -0
  24. package/in-place-install.mjs +17 -14
  25. package/init-command.mjs +116 -0
  26. package/install-app-windows.mjs +24 -7
  27. package/install-app.mjs +56 -42
  28. package/ios-asc-credentials.mjs +163 -0
  29. package/ios-build-context.mjs +129 -0
  30. package/ios-build-upload.mjs +282 -0
  31. package/ios-credentials-command.mjs +70 -0
  32. package/ios-dev.mjs +248 -0
  33. package/ios-host.mjs +76 -0
  34. package/ios-lock.mjs +75 -0
  35. package/ios-package-recipe.mjs +707 -0
  36. package/ios-testflight.mjs +266 -0
  37. package/ios-upload.mjs +313 -0
  38. package/local-publish.mjs +390 -47
  39. package/macho-approval-hash.mjs +181 -0
  40. package/managed-cargo.mjs +57 -1
  41. package/media-sidecar.mjs +20 -4
  42. package/mirror-root-artifact.mjs +74 -22
  43. package/native-cargo-layout.mjs +47 -4
  44. package/npm-license-notices.mjs +159 -0
  45. package/ownership-command.mjs +2 -1
  46. package/ownership-dependency-allowlist.json +2 -1
  47. package/ownership-evidence.mjs +30 -18
  48. package/ownership-key-command.mjs +69 -2
  49. package/ownership-mandatory-set.mjs +66 -16
  50. package/ownership-repositories.json +1 -1
  51. package/ownership-scope.mjs +94 -0
  52. package/ownership.mjs +86 -11
  53. package/package-recipe-targets.mjs +56 -2
  54. package/package.json +1 -1
  55. package/pe-content.mjs +133 -0
  56. package/preflight.mjs +14 -7
  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 +83 -5
  61. package/publish-cargo.mjs +124 -18
  62. package/publish-update.mjs +15 -2
  63. package/qa-build-features.mjs +16 -13
  64. package/qa-contract.mjs +30 -2
  65. package/qa-control-scan.mjs +4 -0
  66. package/release-dependencies-command.mjs +45 -0
  67. package/release-dependencies.mjs +60 -0
  68. package/release-invocation.mjs +118 -8
  69. package/release-switch-registry.json +126 -27
  70. package/release-switch-scan.mjs +62 -24
  71. package/release.mjs +144 -21
  72. package/rightkit-ownership.json +46 -2
  73. package/rightkit-versions.json +380 -189
  74. package/runtime-budgets.mjs +134 -23
  75. package/sign-updater.mjs +56 -59
  76. package/source-gate.mjs +8 -1
  77. package/stage-sidecar.mjs +634 -69
  78. package/target-distribution.mjs +5 -0
  79. package/test-command.mjs +190 -0
  80. package/toolchain-swift.mjs +239 -0
  81. package/upload-release.mjs +7 -2
  82. package/web-build.mjs +67 -0
@@ -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,7 @@ 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 { singleHostOfPolicy } from "./publication-gate.mjs";
49
63
  import { findWorkspaceRoot, lockClosure, pathDependencyDirs, readCargoPackage } from "./crate-graph.mjs";
50
64
 
51
65
  export const RUNTIME_BUDGET_CHECK = "runtime-budget";
@@ -83,6 +97,14 @@ const HOSTS = Object.freeze({ darwin: "macOS", win32: "Windows" });
83
97
  const SHA = /^[0-9a-f]{40}$/;
84
98
  const SHA256 = /^[0-9a-f]{64}$/;
85
99
  const SCOPES = Object.freeze(["synthetic", "real", "installed"]);
100
+ // Figures that may be gated as a delta over a bare-runtime baseline, and the only scope that carries one.
101
+ export const CEILING_BASIS_DELTA = "delta-over-bare";
102
+ export const DELTA_FIGURES = Object.freeze(["wakeupsPerSecond"]);
103
+ const DELTA_SCOPE = "synthetic";
104
+ // Each rate is printed to 3 decimals, so raw - bare and the printed delta may differ by rounding only.
105
+ const DELTA_TOLERANCE = 0.002;
106
+ // A baseline more than this far above its own watchdog run is a mismatched or corrupt pair, not a quiet crate.
107
+ const DELTA_FLOOR = -2;
86
108
 
87
109
  // Long-lived work a crate must declare. Each id must be named in `continuous_reason` for a `continuous = false`
88
110
  // crate; for a held-resource crate every file that matches must be listed in `resources`.
@@ -132,7 +154,8 @@ export function isRuntimeBudgetCrate(crate, manifestText) {
132
154
 
133
155
  /** Hosts the budget must be measured on. `evidence` (release evidence) is the policy authority when given. */
134
156
  export function requiredBudgetHosts(budget, evidence) {
135
- return isDarwinOnlyPolicy(evidence ?? budget) ? ["darwin"] : ["darwin", "win32"];
157
+ const single = singleHostOfPolicy(evidence ?? budget);
158
+ return single ? [single] : ["darwin", "win32"];
136
159
  }
137
160
 
138
161
  // ---------------------------------------------------------------- README view (generated from the JSON)
@@ -183,7 +206,10 @@ export function renderRuntimeBudgetBody(budget) {
183
206
  out.push("");
184
207
  out.push(`- Resident memory ceiling: ${figure("residentMemoryBytes", budget.ceilings?.residentMemoryBytes)}.`);
185
208
  out.push(`- CPU ceiling (idle, steady state): ${figure("idleCpuPercent", budget.ceilings?.idleCpuPercent)}.`);
186
- out.push(`- Wake-ups ceiling: ${figure("wakeupsPerSecond", budget.ceilings?.wakeupsPerSecond)}.`);
209
+ if (budget.ceilingBasis?.wakeupsPerSecond === CEILING_BASIS_DELTA) {
210
+ out.push(`- Wake-ups ceiling (whole process; installed and real measurements): ${figure("wakeupsPerSecond", budget.ceilings?.wakeupsPerSecond)}.`);
211
+ 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.`);
212
+ } else out.push(`- Wake-ups ceiling: ${figure("wakeupsPerSecond", budget.ceilings?.wakeupsPerSecond)}.`);
187
213
  out.push(`- Disk operations ceiling: ${figure("diskOpsPerSecond", budget.ceilings?.diskOpsPerSecond)}.`);
188
214
  const rs = budget.requiredScale;
189
215
  out.push(rs && rs.unit && Number.isFinite(rs.minCount) ? `- Required scale: ${count(rs.minCount)} ${rs.unit} or more.` : "- Required scale: undeclared.");
@@ -200,7 +226,9 @@ export function renderRuntimeBudgetBody(budget) {
200
226
  const scale = `${count(m.dataScale?.count)} ${m.dataScale?.unit} (${m.dataScale?.description})`;
201
227
  const app = m.scope === "installed" && m.app ? `, app ${m.app.name} ${m.app.version}` : "";
202
228
  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}.`);
229
+ const bare = isNumber(m.baseline?.wakeupsPerSecond) && isNumber(m.delta?.wakeupsPerSecond)
230
+ ? `; bare baseline wake-ups ${FORMAT.wakeupsPerSecond(m.baseline.wakeupsPerSecond)}, delta ${FORMAT.wakeupsPerSecond(m.delta.wakeupsPerSecond)}` : "";
231
+ 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
232
  }
205
233
  return out;
206
234
  }
@@ -297,6 +325,8 @@ export const HOST_UNAVAILABLE_FIGURES = Object.freeze({ darwin: Object.freeze(["
297
325
  export const figureUnavailableOn = (os, key) => (HOST_UNAVAILABLE_FIGURES[os] ?? []).includes(key);
298
326
  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
327
  const RESULT_WORDS = Object.freeze(["crate", "version", "scale_unit"]);
328
+ // Optional pair emitted by a delta-basis bench: the bare-runtime wake-up rate and the bench's own reported delta.
329
+ const RESULT_DELTA = Object.freeze({ bare_wakeups_per_s: "bareWakeupsPerSecond", wakeups_delta_per_s: "wakeupsDeltaPerSecond" });
300
330
 
301
331
  /** Parses the one `RESULT budget ...` line of a bench's raw output. */
302
332
  export function parseBudgetResult(text) {
@@ -308,7 +338,7 @@ export function parseBudgetResult(text) {
308
338
  const eq = token.indexOf("=");
309
339
  const key = eq > 0 ? token.slice(0, eq) : token;
310
340
  const value = eq > 0 ? token.slice(eq + 1) : "";
311
- if (!(key in RESULT_NUMBERS) && !RESULT_WORDS.includes(key)) problems.push(`unknown field ${key}`);
341
+ if (!(key in RESULT_NUMBERS) && !(key in RESULT_DELTA) && !RESULT_WORDS.includes(key)) problems.push(`unknown field ${key}`);
312
342
  else if (key in fields) problems.push(`duplicate field ${key}`);
313
343
  else fields[key] = value;
314
344
  }
@@ -320,6 +350,14 @@ export function parseBudgetResult(text) {
320
350
  else if (fields[key] === undefined || fields[key] === "" || !Number.isFinite(n) || n < 0) problems.push(`field ${key} must be a non-negative number`);
321
351
  else value[name] = n;
322
352
  }
353
+ const present = Object.keys(RESULT_DELTA).filter((key) => key in fields);
354
+ if (present.length === 1) problems.push(`fields ${Object.keys(RESULT_DELTA).join(" and ")} must appear together`);
355
+ for (const key of present) {
356
+ const n = Number(fields[key]);
357
+ // The delta is a difference and may be negative; the bare rate is a rate and may not.
358
+ 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`);
359
+ else value[RESULT_DELTA[key]] = n;
360
+ }
323
361
  return problems.length ? { ok: false, problems } : { ok: true, problems: [], value };
324
362
  }
325
363
 
@@ -343,6 +381,14 @@ function checkEvidence(m, index, budget, crateDir, { exists, readFile }) {
343
381
  if (v.version !== m.crateVersion) problems.push(`evidence version ${v.version} != recorded ${m.crateVersion}`);
344
382
  for (const key of BUDGET_FIGURES) if (v[key] !== m.observed[key]) problems.push(`evidence ${key} ${v[key]} != recorded ${m.observed[key]}`);
345
383
  if (v.scale_unit !== m.dataScale.unit || v.scaleCount !== m.dataScale.count) problems.push("evidence data scale differs from the record");
384
+ // The baseline and delta belong to the record exactly when the raw output carries them.
385
+ const recordedBare = m.baseline?.wakeupsPerSecond;
386
+ const recordedDelta = m.delta?.wakeupsPerSecond;
387
+ if (v.bareWakeupsPerSecond !== recordedBare) problems.push(`evidence bare_wakeups_per_s ${v.bareWakeupsPerSecond} != recorded baseline ${recordedBare}`);
388
+ if (v.wakeupsDeltaPerSecond !== recordedDelta) problems.push(`evidence wakeups_delta_per_s ${v.wakeupsDeltaPerSecond} != recorded delta ${recordedDelta}`);
389
+ if (v.bareWakeupsPerSecond !== undefined && !(isNumber(v.wakeupsPerSecond) && Math.abs(v.wakeupsPerSecond - v.bareWakeupsPerSecond - v.wakeupsDeltaPerSecond) <= DELTA_TOLERANCE)) {
390
+ problems.push(`evidence wakeups_delta_per_s ${v.wakeupsDeltaPerSecond} disagrees with wakeups_per_s ${v.wakeupsPerSecond} minus bare_wakeups_per_s ${v.bareWakeupsPerSecond}`);
391
+ }
346
392
  return problems.map((p) => `${at} ${p}`);
347
393
  }
348
394
 
@@ -423,9 +469,19 @@ function validateMeasurement(m, index) {
423
469
  if (isNumber(m.observed?.residentMemoryBytes) && m.observed.residentMemoryBytes === 0) {
424
470
  problems.push(`${at} observed.residentMemoryBytes of 0 is implausible for a running process`);
425
471
  }
472
+ // Optional bare-runtime baseline and reported delta: shape only here; the gate recomputes the delta.
473
+ for (const [field, signed] of [["baseline", false], ["delta", true]]) {
474
+ if (m[field] === undefined) continue;
475
+ 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));
476
+ if (!ok) problems.push(`${at} ${field} must map ${DELTA_FIGURES.join(", ")} to ${signed ? "numbers" : "non-negative numbers"}`);
477
+ }
478
+ if ((m.baseline === undefined) !== (m.delta === undefined)) problems.push(`${at} baseline and delta must be recorded together`);
426
479
  return problems;
427
480
  }
428
481
 
482
+ /** Figures of `budget` gated as a delta over the bare baseline (validated separately; unknown bases never match). */
483
+ const deltaFiguresOf = (budget) => DELTA_FIGURES.filter((key) => budget.ceilingBasis?.[key] === CEILING_BASIS_DELTA);
484
+
429
485
  const escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
430
486
 
431
487
  /** True when `api` (`Type::method()` or a bare type) is a `pub fn` / `pub struct|enum|trait` item in `source`, not in a comment. */
@@ -456,14 +512,15 @@ export function evaluateRuntimeBudget({ crate, crateDir, version, evidence, requ
456
512
  if (budget.schema !== RUNTIME_BUDGET_SCHEMA) problems.push(`${RUNTIME_BUDGET_FILE} schema must be ${RUNTIME_BUDGET_SCHEMA}`);
457
513
  if (budget.crate !== crate) problems.push(`${RUNTIME_BUDGET_FILE} is for ${budget.crate}, expected ${crate}`);
458
514
  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");
515
+ if (budget.hostPolicy !== undefined && budget.hostPolicy !== "darwin-only" && budget.hostPolicy !== "win32-only") problems.push('hostPolicy must be "darwin-only" or "win32-only" when present');
516
+ if ((budget.hostPolicy === "darwin-only" || budget.hostPolicy === "win32-only") && !nonEmpty(budget.hostPolicyReason)) problems.push(`hostPolicy ${budget.hostPolicy} needs a hostPolicyReason`);
461
517
  // One source of truth for the host policy: the release evidence. The budget file may repeat it, never differ.
462
518
  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"})`);
519
+ const policyLabel = (hosts) => (hosts.length === 1 ? `${hosts[0]}-only` : "macOS and Windows");
520
+ const evidenceHosts = requiredBudgetHosts(null, evidence);
521
+ const budgetHosts = requiredBudgetHosts(budget);
522
+ if (evidenceHosts.join() !== budgetHosts.join()) {
523
+ problems.push(`hostPolicy disagrees with the release evidence (evidence: ${policyLabel(evidenceHosts)}; ${RUNTIME_BUDGET_FILE}: ${policyLabel(budgetHosts)})`);
467
524
  }
468
525
  }
469
526
 
@@ -476,6 +533,24 @@ export function evaluateRuntimeBudget({ crate, crateDir, version, evidence, requ
476
533
  else ceilings[key] = value;
477
534
  }
478
535
 
536
+ // Ceiling basis: an explicit, closed declaration. A delta figure needs its own positive delta ceiling,
537
+ // and a delta ceiling without the basis would be dead text, so both directions are refused.
538
+ const basis = budget.ceilingBasis;
539
+ if (basis !== undefined && (!basis || typeof basis !== "object" || Array.isArray(basis))) problems.push("ceilingBasis must be an object");
540
+ else for (const [key, value] of Object.entries(basis ?? {})) {
541
+ if (!DELTA_FIGURES.includes(key)) problems.push(`ceilingBasis.${key} is not a figure that can use a delta basis (${DELTA_FIGURES.join(", ")})`);
542
+ else if (value !== CEILING_BASIS_DELTA) problems.push(`ceilingBasis.${key} must be "${CEILING_BASIS_DELTA}"`);
543
+ }
544
+ const deltaFigures = deltaFiguresOf(budget);
545
+ const deltaCeilings = {};
546
+ for (const key of DELTA_FIGURES) {
547
+ const value = budget.deltaCeilings?.[key];
548
+ if (deltaFigures.includes(key)) {
549
+ if (!isNumber(value) || value <= 0) problems.push(`deltaCeilings.${key} must be a positive number (ceilingBasis.${key} is ${CEILING_BASIS_DELTA})`);
550
+ else deltaCeilings[key] = value;
551
+ } else if (value !== undefined) problems.push(`deltaCeilings.${key} is declared but ceilingBasis.${key} is not ${CEILING_BASIS_DELTA}`);
552
+ }
553
+
479
554
  // Required scale: the unit and minimum the crate's measurement must reach.
480
555
  const scale = budget.requiredScale;
481
556
  let requiredScale = null;
@@ -511,14 +586,35 @@ export function evaluateRuntimeBudget({ crate, crateDir, version, evidence, requ
511
586
  if (valid.length && nonEmpty(version) && counted.length === 0) {
512
587
  problems.push(`no measurement for version ${version} (recorded: ${[...new Set(valid.map(({ m }) => m.crateVersion))].join(", ")})`);
513
588
  }
589
+ // Why a measurement fails one figure, or null. A delta-basis figure of a synthetic measurement is gated on
590
+ // observed - baseline, recomputed here; the reported delta must agree and the baseline must exist. Every
591
+ // other scope (real, installed) keeps the whole-process ceiling, so an app is never held to a delta.
592
+ const usesDelta = (m, key) => deltaFigures.includes(key) && m.scope === DELTA_SCOPE;
593
+ const figureFailure = (m, key) => {
594
+ if (usesDelta(m, key)) {
595
+ const raw = m.observed[key];
596
+ const bare = m.baseline?.[key];
597
+ const reported = m.delta?.[key];
598
+ if (deltaCeilings[key] === undefined) return null; // the declaration problem is already reported
599
+ if (!isNumber(raw)) return `${FIGURE_LABEL[key]} is not measurable here but the ${FIGURE_LABEL[key]} ceiling is a delta over a bare baseline`;
600
+ 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)`;
601
+ const delta = Number((raw - bare).toFixed(3)); // the bench prints 3 decimals; compare at that precision
602
+ if (Math.abs(delta - reported) > DELTA_TOLERANCE) return `reported ${FIGURE_LABEL[key]} delta ${reported} disagrees with observed ${raw} minus baseline ${bare}`;
603
+ 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`;
604
+ return delta > deltaCeilings[key] ? `exceeds the ${FIGURE_LABEL[key]} delta ceiling over the bare baseline: ${raw} - ${bare} = ${delta} > ${deltaCeilings[key]}` : null;
605
+ }
606
+ if (ceilings[key] !== undefined && m.observed[key] !== null && m.observed[key] > ceilings[key]) {
607
+ return `exceeds the ${FIGURE_LABEL[key]} ceiling: observed ${m.observed[key]} > ${ceilings[key]}`;
608
+ }
609
+ return null;
610
+ };
514
611
  for (const { m, index } of counted) {
515
612
  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
- }
613
+ const failure = figureFailure(m, key);
614
+ if (failure) problems.push(`measurement[${index}] on ${m.host.os}/${m.host.arch} ${failure}`);
519
615
  }
520
616
  }
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]));
617
+ 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
618
  const atScale = (m) => requiredScale !== null && m.dataScale.unit === requiredScale.unit && m.dataScale.count >= requiredScale.minCount;
523
619
  const staleness = new Map();
524
620
  const stale = (m) => {
@@ -531,7 +627,7 @@ export function evaluateRuntimeBudget({ crate, crateDir, version, evidence, requ
531
627
  const required = heldResource ? [] : requiredBudgetHosts(budget, evidence);
532
628
  for (const host of required) {
533
629
  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; }
630
+ 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
631
  const fresh = onHost.filter(({ m }) => stale(m) === null);
536
632
  if (fresh.length === 0) { problems.push(`${HOSTS[host]} measurement is stale: ${onHost.map(({ m }) => stale(m)).join("; ")}`); continue; }
537
633
  const within = fresh.filter(({ m }) => withinCeilings(m));
@@ -755,16 +851,18 @@ export function appRuntimeBudgetAdoptionCheck({ appRoot, app, version, cargoLock
755
851
  const used = Object.keys(classes).filter((name) => locked.has(name));
756
852
  if (used.length === 0) return null;
757
853
  const problems = [];
854
+ // A crate whose ceilings or scale RightKit has not declared yet cannot be measured against anything, so it
855
+ // never blocks an app: it is reported as pending (warn) until the crate declares its budget.
856
+ const pending = used.filter((name) => !classes[name].ceilings || !classes[name].requiredScale);
857
+ const measurable = used.filter((name) => !pending.includes(name));
758
858
  const adoptionPath = path.join(appRoot, APP_ADOPTION_FILE);
759
859
  let records = [];
760
- if (!exists(adoptionPath)) problems.push(`${APP_ADOPTION_FILE} is missing`);
860
+ if (!exists(adoptionPath)) { if (measurable.length) problems.push(`${APP_ADOPTION_FILE} is missing`); }
761
861
  else {
762
862
  try { records = JSON.parse(readFile(adoptionPath, "utf8")).measurements ?? []; } catch (error) { problems.push(`${APP_ADOPTION_FILE} is not valid JSON: ${error.message}`); }
763
863
  }
764
- for (const crate of used) {
864
+ for (const crate of measurable) {
765
865
  const { ceilings, requiredScale } = classes[crate];
766
- if (!ceilings) { problems.push(`${crate}: ceilings undeclared (declare them in its runtime-budget.json and regenerate ${CLASSES_FILE})`); continue; }
767
- if (!requiredScale) { problems.push(`${crate}: requiredScale undeclared`); continue; }
768
866
  const mine = records.map((m, index) => ({ m, index })).filter(({ m }) => m?.crate === crate && m.scope === "installed" && locked.get(crate).has(m.crateVersion));
769
867
  if (mine.length === 0) { problems.push(`${crate} ${[...locked.get(crate)].join("/")}: no installed-scope measurement of ${app}`); continue; }
770
868
  const good = mine.filter(({ m, index }) => {
@@ -781,9 +879,14 @@ export function appRuntimeBudgetAdoptionCheck({ appRoot, app, version, cargoLock
781
879
  });
782
880
  if (good.length === 0 && !problems.some((p) => p.startsWith(`${crate}`))) problems.push(`${crate}: no qualifying measurement`);
783
881
  }
784
- return problems.length
785
- ? { name: RUNTIME_BUDGET_CHECK, status: "fail", detail: `${app} uses held-resource crate(s) ${used.join(", ")}: ${problems.join("; ")}`, fix: `record an installed measurement of ${app} for each crate in ${APP_ADOPTION_FILE} (docs/RUNTIME-BUDGETS.md)` }
786
- : { name: RUNTIME_BUDGET_CHECK, status: "ok", detail: `${app} has installed measurements for ${used.join(", ")}` };
882
+ const pendingNote = pending.length ? `; pending RightKit budget declaration (not blocking): ${pending.join(", ")}` : "";
883
+ if (problems.length) {
884
+ return { name: RUNTIME_BUDGET_CHECK, status: "fail", detail: `${app} uses held-resource crate(s) ${measurable.join(", ")}: ${problems.join("; ")}${pendingNote}`, fix: `record an installed measurement of ${app} for each crate in ${APP_ADOPTION_FILE} (docs/RUNTIME-BUDGETS.md)` };
885
+ }
886
+ if (pending.length) {
887
+ return { name: RUNTIME_BUDGET_CHECK, status: "warn", detail: `${app} uses held-resource crate(s) whose RightKit ceilings are not declared yet: ${pending.join(", ")}${measurable.length ? `; measured: ${measurable.join(", ")}` : ""}`, fix: "RightKit declares the crate ceilings; then record installed measurements (docs/RUNTIME-BUDGETS.md)" };
888
+ }
889
+ return { name: RUNTIME_BUDGET_CHECK, status: "ok", detail: `${app} has installed measurements for ${used.join(", ")}` };
787
890
  }
788
891
 
789
892
  // ---------------------------------------------------------------- record (measurement provenance)
@@ -832,6 +935,13 @@ export function recordMeasurement({ crateDir, resultFile, scope, command, descri
832
935
  if (stale) throw new Error(`refusing to record: ${stale} (commit the code under test, then re-run the bench)`);
833
936
  const file = path.join(crateDir, RUNTIME_BUDGET_FILE);
834
937
  const budget = JSON.parse(fs.readFileSync(file, "utf8"));
938
+ const hasBaseline = v.bareWakeupsPerSecond !== undefined;
939
+ if (scope === DELTA_SCOPE && !hasBaseline && deltaFiguresOf(budget).length) {
940
+ 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`);
941
+ }
942
+ if (hasBaseline && !(isNumber(v.wakeupsPerSecond) && Math.abs(v.wakeupsPerSecond - v.bareWakeupsPerSecond - v.wakeupsDeltaPerSecond) <= DELTA_TOLERANCE)) {
943
+ throw new Error("bench output rejected: wakeups_delta_per_s disagrees with wakeups_per_s minus bare_wakeups_per_s");
944
+ }
835
945
  const evidenceName = `${host.os}-${host.arch}-${pkg.version}-${scope}${scope === "installed" ? `-${app.name}` : ""}.txt`.replace(/[^\w.-]/g, "_");
836
946
  const evidenceRel = `${RUNTIME_BUDGET_EVIDENCE_DIR}/${evidenceName}`;
837
947
  fs.mkdirSync(path.join(crateDir, RUNTIME_BUDGET_EVIDENCE_DIR), { recursive: true });
@@ -842,6 +952,7 @@ export function recordMeasurement({ crateDir, resultFile, scope, command, descri
842
952
  dataScale: { description, unit: v.scale_unit, count: v.scaleCount },
843
953
  command,
844
954
  observed: Object.fromEntries(BUDGET_FIGURES.map((key) => [key, v[key]])),
955
+ ...(hasBaseline ? { baseline: { wakeupsPerSecond: v.bareWakeupsPerSecond }, delta: { wakeupsPerSecond: v.wakeupsDeltaPerSecond } } : {}),
845
956
  evidence: { file: evidenceRel, sha256: sha256Of(raw) },
846
957
  };
847
958
  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);
package/sign-updater.mjs CHANGED
@@ -1,68 +1,65 @@
1
1
  #!/usr/bin/env node
2
- import { spawnSync } from "./background-process.mjs";
3
- import { existsSync, readFileSync, rmSync } from "node:fs";
2
+ import { existsSync, rmSync } from "node:fs";
4
3
  import path from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
5
  import { signMacUpdaterFiles } from "./macos-signing-session.mjs";
6
- import { refuseManagedEnvSecrets, MANAGED_HOST_ENV_SECRET_REFUSED } from "./managed-host-secrets.mjs";
6
+ import { refuseManagedEnvSecrets } from "./managed-host-secrets.mjs";
7
+ import { execWithUpdaterCredential } from "./protected-credentials.mjs";
8
+ import { resolvePnpmCli } from "./development.mjs";
7
9
 
8
- const args = process.argv.slice(2);
9
- const files = [];
10
- let dryRun = false;
11
- let keyFile = process.env.RIGHT_UPDATER_KEY_FILE || "";
12
- for (let i = 0; i < args.length; i++) {
13
- if (args[i] === "--file") files.push(path.resolve(args[++i]));
14
- else if (args[i] === "--key-file") keyFile = args[++i] ?? "";
15
- else if (args[i] === "--dry-run") dryRun = true;
16
- else fail(`unknown argument: ${args[i]}`);
17
- }
18
- if (!files.length) fail("at least one --file is required");
10
+ export const UPDATER_SIGNING_SECRET = "updater-signing-key";
19
11
 
20
- const env = { ...process.env };
21
- if (!dryRun) {
22
- try { refuseManagedEnvSecrets(process.env, ["TAURI_SIGNING_PRIVATE_KEY", "TAURI_SIGNING_PRIVATE_KEY_PASSWORD"]); }
23
- catch (error) { fail(`[${MANAGED_HOST_ENV_SECRET_REFUSED}] ${error.message}`); }
24
- }
25
- if (!dryRun && !env.TAURI_SIGNING_PRIVATE_KEY) {
26
- if (process.platform === "win32") {
27
- if (!keyFile) fail("updater key path required on win32: pass --key-file or set RIGHT_UPDATER_KEY_FILE (release config: targets.win.updater.keyFile), or set TAURI_SIGNING_PRIVATE_KEY");
28
- const key = path.resolve(keyFile.replace(/%([^%]+)%/g, (m, name) => process.env[name] ?? m));
29
- if (!existsSync(key)) fail(`updater key not found: ${key}`);
30
- env.TAURI_SIGNING_PRIVATE_KEY = readFileSync(key, "utf8");
31
- } else if (process.platform !== "darwin") {
32
- fail("TAURI_SIGNING_PRIVATE_KEY is required on this platform");
12
+ export function runSignUpdater(args, {
13
+ env = process.env, platform = process.platform, cwd = process.cwd(),
14
+ execCredential = execWithUpdaterCredential, resolvePnpm = resolvePnpmCli,
15
+ signMac = signMacUpdaterFiles, exists = existsSync, remove = rmSync, log = console.log,
16
+ } = {}) {
17
+ const files = [];
18
+ let dryRun = false;
19
+ for (let i = 0; i < args.length; i++) {
20
+ if (args[i] === "--file") {
21
+ if (!args[i + 1] || args[i + 1].startsWith("--")) throw new Error("--file requires an artifact path");
22
+ files.push(path.resolve(cwd, args[++i]));
23
+ } else if (args[i] === "--key-file") throw new Error("--key-file is removed: updater keys come from the OS credential provider");
24
+ else if (args[i] === "--dry-run") dryRun = true;
25
+ else throw new Error(`unknown argument: ${args[i]}`);
33
26
  }
34
- }
35
- env.TAURI_SIGNING_PRIVATE_KEY_PASSWORD ??= "";
36
-
37
- for (const file of files) {
38
- if (!dryRun && !existsSync(file)) fail(`updater artifact not found: ${file}`);
39
- rmSync(`${file}.sig`, { force: true });
40
- console.log(`${dryRun ? "[dry-run] " : ""}pnpm exec tauri signer sign ${file}`);
41
- if (dryRun) continue;
42
- if (process.platform === "darwin") continue;
43
- const result = process.platform === "win32"
44
- ? spawnSync(
45
- process.env.ComSpec || "cmd.exe",
46
- ["/d", "/s", "/c", `pnpm exec tauri signer sign ${file}`],
47
- {
48
- cwd: process.cwd(),
49
- env,
50
- stdio: "inherit",
51
- },
52
- )
53
- : spawnSync("pnpm", ["exec", "tauri", "signer", "sign", file], {
54
- cwd: process.cwd(),
55
- env,
56
- stdio: "inherit",
57
- });
58
- if (result.status !== 0) process.exit(result.status ?? 1);
59
- }
60
- if (!dryRun && process.platform === "darwin") {
61
- try { signMacUpdaterFiles(files, { env }); }
62
- catch (error) { fail(`${error.code ? `[${error.code}] ` : ""}${error.message}`); }
27
+ if (!files.length) throw new Error("at least one --file is required");
28
+ if (!dryRun) {
29
+ refuseManagedEnvSecrets(env, ["TAURI_SIGNING_PRIVATE_KEY", "TAURI_SIGNING_PRIVATE_KEY_PASSWORD"]);
30
+ for (const key of Object.keys(env)) {
31
+ if (["TAURI_SIGNING_PRIVATE_KEY", "TAURI_SIGNING_PRIVATE_KEY_PASSWORD"].includes(key.toUpperCase()) && env[key]) {
32
+ throw new Error("updater signing key/password must come from the OS credential provider");
33
+ }
34
+ }
35
+ for (const file of files) if (!exists(file)) throw new Error(`updater artifact not found: ${file}`);
36
+ }
37
+ const signingEnv = { ...env };
38
+ for (const key of Object.keys(signingEnv)) {
39
+ if (["TAURI_SIGNING_PRIVATE_KEY", "TAURI_SIGNING_PRIVATE_KEY_PASSWORD"].includes(key.toUpperCase())) delete signingEnv[key];
40
+ }
41
+ for (const file of files) {
42
+ log(`${dryRun ? "[dry-run] " : ""}pnpm exec tauri signer sign ${file}`);
43
+ if (dryRun) continue;
44
+ remove(`${file}.sig`, { force: true });
45
+ if (platform === "darwin") continue;
46
+ const result = execCredential(process.execPath,
47
+ [resolvePnpm({ root: cwd, env: signingEnv }), "exec", "tauri", "signer", "sign", file],
48
+ { cwd, env: signingEnv });
49
+ if (result.error || result.status !== 0) {
50
+ const error = new Error(`updater credential provider/signing failed (exit ${result.status ?? "unavailable"})`);
51
+ error.exitCode = result.status ?? 1;
52
+ throw error;
53
+ }
54
+ }
55
+ if (!dryRun && platform === "darwin") signMac(files, { env: signingEnv, cwd });
56
+ return { files, dryRun };
63
57
  }
64
58
 
65
- function fail(message) {
66
- console.error(`sign-updater: ${message}`);
67
- process.exit(1);
59
+ if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
60
+ try { runSignUpdater(process.argv.slice(2)); }
61
+ catch (error) {
62
+ console.error(`sign-updater: ${error.code ? `[${error.code}] ` : ""}${error.message}`);
63
+ process.exitCode = error.exitCode ?? 1;
64
+ }
68
65
  }
package/source-gate.mjs CHANGED
@@ -30,11 +30,18 @@ export function stripOwnLockRecords(status) {
30
30
  return kept.length ? `${kept.join("\0")}\0` : "";
31
31
  }
32
32
 
33
+ /** Path of one porcelain v1 record ("XY path"). Tolerates a record whose leading status column was
34
+ * trimmed away by a caller ("M path"), so the first path never loses its first character (#782). */
35
+ function porcelainPath(entry) {
36
+ const m = /^([ MADRCU?!]{2} |[MADRCU?!] )/.exec(entry);
37
+ return m ? entry.slice(m[0].length) : entry;
38
+ }
39
+
33
40
  export function assertCleanSource({ status, commandId = "release" }) {
34
41
  if (typeof status !== "string") throw new Error("release source status is required");
35
42
  status = stripOwnLockRecords(status);
36
43
  if (status.length !== 0) {
37
- const files = status.split("\0").filter(Boolean).map((entry) => entry.slice(3)).slice(0, 10);
44
+ const files = status.split("\0").filter(Boolean).map(porcelainPath).slice(0, 10);
38
45
  throw new Error(`${commandId} requires a clean working tree; commit or stash first:\n${files.map((file) => `- ${file}`).join("\n")}`);
39
46
  }
40
47
  }