@rightkit/release 0.2.21 → 0.2.22

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.
@@ -0,0 +1,68 @@
1
+ import { isDeepStrictEqual } from "node:util";
2
+
3
+ import { verifyRuntimeArtifactEnvelope } from "./runtime-artifact-manifest.mjs";
4
+
5
+ const ASR_AUTHORITY = "heardright";
6
+ const ASR_CONSUMER = "scraperight";
7
+ const ASR_KIND = "asr-model";
8
+
9
+ export function assertAsrArtifactAdoption({ authorityEnvelope, consumerEnvelope, publicKey, trustedKeyId }) {
10
+ const authority = verifyTrusted(authorityEnvelope, publicKey, trustedKeyId);
11
+ const consumer = verifyTrusted(consumerEnvelope, publicKey, trustedKeyId);
12
+
13
+ assertManifestRole(authority, ASR_AUTHORITY, "authority");
14
+ assertManifestRole(consumer, ASR_CONSUMER, "consumer");
15
+ for (const manifest of [authority, consumer]) {
16
+ if (manifest.artifactKind !== ASR_KIND) throw new Error("ASR adoption manifest must be an asr-model");
17
+ if (manifest.promotion.authorityAppKey !== ASR_AUTHORITY) throw new Error("ASR promotion authority must be heardright");
18
+ }
19
+
20
+ assertSame(authority.target, consumer.target, "target");
21
+ assertSame(authority.object.filename, consumer.object.filename, "object.filename");
22
+ assertSame(authority.object.sha256, consumer.object.sha256, "object.sha256");
23
+ assertSame(authority.object.sizeBytes, consumer.object.sizeBytes, "object.sizeBytes");
24
+ for (const field of ["runtime", "model", "tokenizer", "preprocessing", "license", "provenance"]) {
25
+ assertSame(authority.versions[field], consumer.versions[field], `versions.${field}`);
26
+ }
27
+ assertSame(authority.provenance, consumer.provenance, "provenance");
28
+ assertSame(authority.promotion, consumer.promotion, "promotion");
29
+ return {
30
+ sha256: authority.object.sha256,
31
+ promotionId: authority.promotion.promotionId,
32
+ target: authority.target,
33
+ };
34
+ }
35
+
36
+ export function assertAsrAdapterPair(authority, consumer) {
37
+ assertAdapter(authority, ASR_AUTHORITY, "authority");
38
+ assertAdapter(consumer, ASR_CONSUMER, "consumer");
39
+ for (const [field, expected] of [
40
+ ["authorityAppKey", ASR_AUTHORITY],
41
+ ["artifactKind", ASR_KIND],
42
+ ["delivery", "private-r2"],
43
+ ["entitlementTier", "pro"],
44
+ ]) {
45
+ assertSame(authority[field], expected, `authority adapter ${field}`);
46
+ assertSame(consumer[field], expected, `consumer adapter ${field}`);
47
+ }
48
+ return true;
49
+ }
50
+
51
+ function verifyTrusted(envelope, publicKey, trustedKeyId) {
52
+ if (envelope?.signature?.keyId !== trustedKeyId) throw new Error("untrusted runtime artifact key id");
53
+ return verifyRuntimeArtifactEnvelope(envelope, publicKey);
54
+ }
55
+
56
+ function assertManifestRole(manifest, appKey, role) {
57
+ if (manifest.entitlement.appKey !== appKey) throw new Error(`${role} manifest must belong to ${appKey}`);
58
+ }
59
+
60
+ function assertAdapter(value, appKey, role) {
61
+ if (!value || value.schema !== 1 || value.appKey !== appKey || value.role !== role) {
62
+ throw new Error(`invalid ${role} ASR adapter`);
63
+ }
64
+ }
65
+
66
+ function assertSame(left, right, field) {
67
+ if (!isDeepStrictEqual(left, right)) throw new Error(`ASR drift: ${field}`);
68
+ }
@@ -0,0 +1,122 @@
1
+ import assert from "node:assert/strict";
2
+ import { generateKeyPairSync } from "node:crypto";
3
+ import test from "node:test";
4
+
5
+ import { assertAsrAdapterPair, assertAsrArtifactAdoption } from "./asr-artifact-adoption.mjs";
6
+ import { buildRuntimeArtifactManifest, signRuntimeArtifactManifest } from "./runtime-artifact-manifest.mjs";
7
+
8
+ const digest = "a".repeat(64);
9
+ const notice = "b".repeat(64);
10
+ const evidence = "c".repeat(64);
11
+ const { privateKey, publicKey } = generateKeyPairSync("ed25519");
12
+
13
+ function envelope(appKey, overrides = {}) {
14
+ const manifest = buildRuntimeArtifactManifest({
15
+ artifactKind: "asr-model",
16
+ entitlement: { appKey, tier: "pro" },
17
+ distribution: { delivery: "private-r2", bucket: "rightapps-updates" },
18
+ target: { os: "windows", arch: "x86_64" },
19
+ filename: "parakeet-unified-en-0.6b.tar.zst",
20
+ sha256: digest,
21
+ sizeBytes: 1234,
22
+ versions: {
23
+ runtime: "rightkit-asr@0.1.0",
24
+ model: "parakeet-unified-en-0.6b",
25
+ tokenizer: "sha256:tokenizer-v1",
26
+ preprocessing: "heardright-frontend-v1",
27
+ license: "CC-BY-4.0",
28
+ provenance: "nvidia/parakeet-tdt-0.6b-v3",
29
+ },
30
+ provenance: {
31
+ source: "https://huggingface.co/nvidia/parakeet-tdt-0.6b-v3",
32
+ sourceRevision: "revision-1",
33
+ licenseId: "CC-BY-4.0",
34
+ noticeSha256: notice,
35
+ },
36
+ promotion: {
37
+ authorityAppKey: "heardright",
38
+ promotionId: "hr-asr-2026-07-14",
39
+ promotedAt: "2026-07-14T00:00:00.000Z",
40
+ evidenceSha256: evidence,
41
+ },
42
+ ...overrides,
43
+ });
44
+ return signRuntimeArtifactManifest(manifest, privateKey, "rightkit-runtime-artifacts-2026-07");
45
+ }
46
+
47
+ test("accepts app-scoped pointers with one exact HeardRight-certified ASR identity", () => {
48
+ const result = assertAsrArtifactAdoption({
49
+ authorityEnvelope: envelope("heardright"),
50
+ consumerEnvelope: envelope("scraperight"),
51
+ publicKey,
52
+ trustedKeyId: "rightkit-runtime-artifacts-2026-07",
53
+ });
54
+
55
+ assert.equal(result.sha256, digest);
56
+ assert.equal(result.promotionId, "hr-asr-2026-07-14");
57
+ });
58
+
59
+ test("rejects a validly signed ScrapeRight manifest with ASR drift", () => {
60
+ const drifted = envelope("scraperight", {
61
+ versions: {
62
+ runtime: "rightkit-asr@0.1.1",
63
+ model: "parakeet-unified-en-0.6b",
64
+ tokenizer: "sha256:tokenizer-v1",
65
+ preprocessing: "heardright-frontend-v1",
66
+ license: "CC-BY-4.0",
67
+ provenance: "nvidia/parakeet-tdt-0.6b-v3",
68
+ },
69
+ });
70
+
71
+ assert.throws(() => assertAsrArtifactAdoption({
72
+ authorityEnvelope: envelope("heardright"),
73
+ consumerEnvelope: drifted,
74
+ publicKey,
75
+ trustedKeyId: "rightkit-runtime-artifacts-2026-07",
76
+ }), /ASR drift: versions\.runtime/);
77
+ });
78
+
79
+ test("rejects wrong roles, authority, target, signature key, and artifact kind", () => {
80
+ const base = {
81
+ authorityEnvelope: envelope("heardright"),
82
+ consumerEnvelope: envelope("scraperight"),
83
+ publicKey,
84
+ trustedKeyId: "rightkit-runtime-artifacts-2026-07",
85
+ };
86
+ assert.throws(() => assertAsrArtifactAdoption({ ...base, authorityEnvelope: envelope("scraperight") }), /authority manifest must belong to heardright/);
87
+ assert.throws(() => assertAsrArtifactAdoption({ ...base, consumerEnvelope: envelope("heardright") }), /consumer manifest must belong to scraperight/);
88
+ assert.throws(() => assertAsrArtifactAdoption({ ...base, trustedKeyId: "rotated-without-app-update" }), /untrusted runtime artifact key id/);
89
+ assert.throws(() => assertAsrArtifactAdoption({
90
+ ...base,
91
+ consumerEnvelope: envelope("scraperight", { target: { os: "darwin", arch: "aarch64" } }),
92
+ }), /ASR drift: target/);
93
+ assert.throws(() => assertAsrArtifactAdoption({
94
+ ...base,
95
+ consumerEnvelope: envelope("scraperight", { artifactKind: "ocr-model" }),
96
+ }), /must be an asr-model/);
97
+ });
98
+
99
+ test("rejects a consumer signed by a different key", () => {
100
+ const other = generateKeyPairSync("ed25519");
101
+ const consumer = signRuntimeArtifactManifest(
102
+ envelope("scraperight").manifest,
103
+ other.privateKey,
104
+ "rightkit-runtime-artifacts-2026-07",
105
+ );
106
+ assert.throws(() => assertAsrArtifactAdoption({
107
+ authorityEnvelope: envelope("heardright"),
108
+ consumerEnvelope: consumer,
109
+ publicKey,
110
+ trustedKeyId: "rightkit-runtime-artifacts-2026-07",
111
+ }), /signature verification failed/);
112
+ });
113
+
114
+ test("locks the thin app adapters to HeardRight authority and private Pro delivery", () => {
115
+ const authority = {
116
+ schema: 1, appKey: "heardright", role: "authority", authorityAppKey: "heardright",
117
+ artifactKind: "asr-model", delivery: "private-r2", entitlementTier: "pro",
118
+ };
119
+ const consumer = { ...authority, appKey: "scraperight", role: "consumer" };
120
+ assert.equal(assertAsrAdapterPair(authority, consumer), true);
121
+ assert.throws(() => assertAsrAdapterPair(authority, { ...consumer, authorityAppKey: "scraperight" }), /ASR drift/);
122
+ });
@@ -101,11 +101,34 @@ function assertNoRightKitOverrides(parsed, label, source) {
101
101
  }
102
102
  }
103
103
 
104
+ // `tomllib` is Python 3.11+. Windows pins `py -3.11`, but a bare `python3` on macOS is the system
105
+ // 3.9, which has no tomllib — so every app's release:doctor died on a stock Mac, and reported the
106
+ // manifest as invalid TOML when the real problem was the interpreter. Probe for one that can.
107
+ let tomlRunner;
108
+ function resolveTomlRunner() {
109
+ if (tomlRunner) return tomlRunner;
110
+ const probe = "import sys,tomllib";
111
+ const candidates =
112
+ process.platform === "win32"
113
+ ? [["py", ["-3.11"]], ["py", ["-3"]], ["python", []]]
114
+ : [["python3.13", []], ["python3.12", []], ["python3.11", []], ["python3", []]];
115
+ for (const [command, prefix] of candidates) {
116
+ const check = spawnSync(command, [...prefix, "-c", probe], { encoding: "utf8", windowsHide: true });
117
+ if (check.status === 0) {
118
+ tomlRunner = { command, prefix };
119
+ return tomlRunner;
120
+ }
121
+ }
122
+ throw new Error(
123
+ "no Python with tomllib found (needs 3.11+). Install one, or put it on PATH: the release " +
124
+ "contract reads Cargo manifests through it.",
125
+ );
126
+ }
127
+
104
128
  function readToml(file, label) {
105
129
  const script = "import json,pathlib,sys,tomllib; json.dump(tomllib.loads(pathlib.Path(sys.argv[1]).read_text(encoding='utf-8')),sys.stdout,default=str)";
106
- const command = process.platform === "win32" ? "py" : "python3";
107
- const args = process.platform === "win32" ? ["-3.11", "-c", script, file] : ["-c", script, file];
108
- const result = spawnSync(command, args, { encoding: "utf8", windowsHide: true });
130
+ const { command, prefix } = resolveTomlRunner();
131
+ const result = spawnSync(command, [...prefix, "-c", script, file], { encoding: "utf8", windowsHide: true });
109
132
  if (result.status !== 0) throw new Error(`${label} is not valid TOML: ${String(result.stderr ?? "").trim()}`);
110
133
  return JSON.parse(result.stdout);
111
134
  }
@@ -44,6 +44,8 @@ if (first === "--version" || first === "-v") {
44
44
  const rest = args.slice(1);
45
45
  if (rest[0] === "cargo") {
46
46
  run("publish-cargo.mjs", rest.slice(1));
47
+ } else if (rest[0] === "swift") {
48
+ run("publish-swift.mjs", rest.slice(1));
47
49
  } else {
48
50
  run("release.mjs", rest.includes("--upload") || rest.includes("--publish") ? rest : [...rest, "--upload"]);
49
51
  }
@@ -120,6 +122,7 @@ Commands:
120
122
  release [--platform mac|win] --tier patch|update Build/package through the signed release lane
121
123
  publish [--platform mac|win] --tier patch|update Release plus R2 upload + RightApps registration
122
124
  publish cargo --crate <name> [--dry-run] Test, inspect, scan, and publish one crates.io package
125
+ publish swift [--scope <s>] [--url <u>] [--dry-run] Test, archive, scan, and publish RightKitSwift to a Swift registry
123
126
  model promote --authority heardright --config <file> [--dry-run]
124
127
  Sign and promote one runtime/model artifact
125
128
  doctor [--platform mac|win] Inspect one app's release config
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rightkit/release",
3
- "version": "0.2.21",
3
+ "version": "0.2.22",
4
4
  "description": "Portable Right Suite release CLI/SDK: signed installers, updater artifacts, patch/update routing, hardening, R2 publish, and RightApps registration.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,122 @@
1
+ #!/usr/bin/env node
2
+ // Publish RightKitSwift to a Swift package registry, gated exactly like `publish cargo`:
3
+ // secret scan -> swift test -> release build -> deterministic source archive + hash ->
4
+ // registry dry-run -> publish. Canonical source stays under tools/rightkit/swift; the
5
+ // registry package is an output only. No Git distribution repo, no path/Git dependency.
6
+ //
7
+ // swift-tools is macOS/Linux only, so the plan builder, arg parser, and version check are
8
+ // pure and unit-tested on any OS (publish-swift.test.mjs); the swift steps run where swift exists.
9
+ import fs from "node:fs";
10
+ import path from "node:path";
11
+ import { createHash } from "node:crypto";
12
+ import { spawnSync } from "node:child_process";
13
+ import { scanCrateForSecrets } from "./publish-cargo.mjs";
14
+
15
+ const IDENTIFIER = /^[a-z0-9]+(?:[-_][a-z0-9]+)*$/i;
16
+
17
+ export function parseSwiftPublishArgs(argv, env = process.env) {
18
+ const allowed = new Set(["--scope", "--url", "--dry-run", "--allow-dirty"]);
19
+ let scope = env.RIGHTKIT_SWIFT_REGISTRY_SCOPE ?? "rightsuite";
20
+ let url = env.RIGHTKIT_SWIFT_REGISTRY_URL ?? "";
21
+ let dryRun = false;
22
+ let allowDirty = false;
23
+ for (let index = 0; index < argv.length; index += 1) {
24
+ const arg = argv[index];
25
+ if (!allowed.has(arg)) throw new Error(`unexpected argument: ${arg}`);
26
+ if (arg === "--dry-run") dryRun = true;
27
+ else if (arg === "--allow-dirty") allowDirty = true;
28
+ else {
29
+ const value = argv[(index += 1)];
30
+ if (value === undefined) throw new Error(`${arg} needs a value`);
31
+ if (arg === "--scope") scope = value;
32
+ if (arg === "--url") url = value;
33
+ }
34
+ }
35
+ if (!IDENTIFIER.test(scope)) throw new Error(`invalid registry scope: ${scope}`);
36
+ if (allowDirty && !dryRun) throw new Error("--allow-dirty is only valid with --dry-run");
37
+ if (!dryRun && !url) throw new Error("registry publish needs --url or RIGHTKIT_SWIFT_REGISTRY_URL");
38
+ return { scope, url, dryRun, allowDirty };
39
+ }
40
+
41
+ // The VERSION file is the single source of truth; Package.swift must name the package and the
42
+ // version must be a clean semver so the registry archive is reproducible.
43
+ export function readPackageIdentity(packageDir) {
44
+ const manifest = path.join(packageDir, "Package.swift");
45
+ if (!fs.existsSync(manifest)) throw new Error(`Package.swift not found: ${manifest}`);
46
+ const versionFile = path.join(packageDir, "VERSION");
47
+ if (!fs.existsSync(versionFile)) throw new Error(`VERSION not found: ${versionFile}`);
48
+ const version = fs.readFileSync(versionFile, "utf8").trim();
49
+ if (!/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(version)) {
50
+ throw new Error(`VERSION is not clean semver: "${version}"`);
51
+ }
52
+ const nameMatch = fs.readFileSync(manifest, "utf8").match(/name:\s*"([^"]+)"/);
53
+ if (!nameMatch) throw new Error("could not read package name from Package.swift");
54
+ return { name: nameMatch[1], version };
55
+ }
56
+
57
+ export function buildSwiftPublishPlan({ packageDir, scope, name, version, url, dryRun, allowDirty = false }) {
58
+ const packageId = `${scope}.${name}`;
59
+ const archive = path.join(packageDir, ".build", `${name}-${version}.zip`);
60
+ const plan = [
61
+ { label: "secret scan", internal: "secret-scan" },
62
+ { label: "tests", command: "swift", args: ["test", "--package-path", packageDir] },
63
+ { label: "release build", command: "swift", args: ["build", "-c", "release", "--package-path", packageDir] },
64
+ // Deterministic source archive; its bytes are what the registry stores, so hash it for the log.
65
+ { label: "source archive", command: "swift", args: ["package", "--package-path", packageDir, "archive-source", "-o", archive] },
66
+ { label: "archive hash", internal: "archive-hash", archive },
67
+ ];
68
+ if (dryRun) {
69
+ plan.push({ label: "registry dry-run (validate only)", internal: "dry-run-note", packageId, version });
70
+ return plan;
71
+ }
72
+ plan.push({
73
+ label: "registry publish",
74
+ command: "swift",
75
+ args: ["package-registry", "publish", packageId, version, "--url", url, "--package-path", packageDir],
76
+ });
77
+ return plan;
78
+ }
79
+
80
+ export function runSwiftPublish(options, execute = spawnSync) {
81
+ const packageDir = options.packageDir;
82
+ const { name, version } = readPackageIdentity(packageDir);
83
+ const plan = buildSwiftPublishPlan({ ...options, packageDir, name, version });
84
+
85
+ for (const step of plan) {
86
+ process.stdout.write(`[right-release] swift ${step.label}\n`);
87
+ if (step.internal === "secret-scan") {
88
+ const violations = scanCrateForSecrets(packageDir);
89
+ if (violations.length) throw new Error(`secret scan rejected: ${violations.join(", ")}`);
90
+ continue;
91
+ }
92
+ if (step.internal === "dry-run-note") {
93
+ process.stdout.write(`[right-release] dry-run OK: ${step.packageId} ${step.version} (not published)\n`);
94
+ continue;
95
+ }
96
+ if (step.internal === "archive-hash") {
97
+ const bytes = fs.readFileSync(step.archive);
98
+ process.stdout.write(`sha256 ${createHash("sha256").update(bytes).digest("hex")} ${path.basename(step.archive)}\n`);
99
+ continue;
100
+ }
101
+ const result = execute(step.command, step.args, { cwd: packageDir, encoding: "utf8", windowsHide: true, stdio: "inherit" });
102
+ if (result.error) {
103
+ if (result.error.code === "ENOENT") {
104
+ throw new Error(`${step.command} not found — run publish swift on macOS/Linux with the Swift toolchain installed`);
105
+ }
106
+ throw result.error;
107
+ }
108
+ if (result.status !== 0) throw new Error(`${step.label} failed with exit code ${result.status}`);
109
+ }
110
+ return { name, version, published: !options.dryRun };
111
+ }
112
+
113
+ if (process.argv[1]?.endsWith("publish-swift.mjs")) {
114
+ try {
115
+ const options = parseSwiftPublishArgs(process.argv.slice(2));
116
+ const packageDir = path.resolve(path.dirname(new URL(import.meta.url).pathname.replace(/^\/(\w:)/, "$1")), "../../swift/RightKitSwift");
117
+ runSwiftPublish({ ...options, packageDir });
118
+ } catch (error) {
119
+ process.stderr.write(`${error.message}\n`);
120
+ process.exit(1);
121
+ }
122
+ }
@@ -0,0 +1,44 @@
1
+ import assert from "node:assert/strict";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import test from "node:test";
5
+ import { parseSwiftPublishArgs, buildSwiftPublishPlan, readPackageIdentity } from "./publish-swift.mjs";
6
+
7
+ const here = path.dirname(fileURLToPath(import.meta.url));
8
+ const packageDir = path.resolve(here, "../../swift/RightKitSwift");
9
+
10
+ test("dry-run needs no url; publish requires one", () => {
11
+ assert.deepEqual(parseSwiftPublishArgs(["--dry-run"], {}), {
12
+ scope: "rightsuite", url: "", dryRun: true, allowDirty: false,
13
+ });
14
+ assert.throws(() => parseSwiftPublishArgs([], {}), /needs --url/);
15
+ assert.equal(parseSwiftPublishArgs(["--url", "https://reg.local"], {}).url, "https://reg.local");
16
+ });
17
+
18
+ test("env supplies scope and url; --allow-dirty is dry-run only", () => {
19
+ const parsed = parseSwiftPublishArgs([], { RIGHTKIT_SWIFT_REGISTRY_URL: "https://e", RIGHTKIT_SWIFT_REGISTRY_SCOPE: "acme" });
20
+ assert.deepEqual(parsed, { scope: "acme", url: "https://e", dryRun: false, allowDirty: false });
21
+ assert.throws(() => parseSwiftPublishArgs(["--allow-dirty"], { RIGHTKIT_SWIFT_REGISTRY_URL: "https://e" }), /only valid with --dry-run/);
22
+ assert.throws(() => parseSwiftPublishArgs(["--bogus"], {}), /unexpected argument/);
23
+ });
24
+
25
+ test("reads the real package identity from VERSION + Package.swift", () => {
26
+ const { name, version } = readPackageIdentity(packageDir);
27
+ assert.equal(name, "RightKitSwift");
28
+ assert.match(version, /^\d+\.\d+\.\d+/);
29
+ });
30
+
31
+ test("dry-run plan stops before publish; real plan ends with publish", () => {
32
+ const base = { packageDir, scope: "rightsuite", name: "RightKitSwift", version: "0.1.0", url: "https://reg.local" };
33
+ const dry = buildSwiftPublishPlan({ ...base, dryRun: true });
34
+ assert.equal(dry[0].internal, "secret-scan");
35
+ assert.ok(dry.some((s) => s.label === "tests" && s.command === "swift"));
36
+ assert.ok(!dry.some((s) => s.label === "registry publish"), "dry-run must not publish");
37
+
38
+ const real = buildSwiftPublishPlan({ ...base, dryRun: false });
39
+ const publish = real.at(-1);
40
+ assert.equal(publish.label, "registry publish");
41
+ assert.deepEqual(publish.args, [
42
+ "package-registry", "publish", "rightsuite.RightKitSwift", "0.1.0", "--url", "https://reg.local", "--package-path", packageDir,
43
+ ]);
44
+ });
@@ -0,0 +1,61 @@
1
+ import { existsSync, readFileSync, readdirSync } from "node:fs";
2
+ import path from "node:path";
3
+
4
+ export function validateQaBackdoorContract(root, packageScripts = {}) {
5
+ if (!existsSync(path.join(root, "right-qa.config.mjs"))) return [];
6
+
7
+ const issues = [];
8
+ const cargoPath = path.join(root, "src-tauri", "Cargo.toml");
9
+ if (!existsSync(cargoPath)) return ["right-qa.config.mjs requires src-tauri/Cargo.toml"];
10
+
11
+ const cargoToml = readFileSync(cargoPath, "utf8");
12
+ const dependency = cargoToml.match(/tauri-plugin-wdio-webdriver\s*=\s*\{([^}]*)\}/s)?.[1] ?? "";
13
+ if (!dependency) issues.push("Cargo.toml must declare tauri-plugin-wdio-webdriver");
14
+ else if (!/optional\s*=\s*true/.test(dependency)) issues.push("tauri-plugin-wdio-webdriver must be optional");
15
+
16
+ const qaFeature = cargoToml.match(/qa-native\s*=\s*\[([^\]]*)\]/s)?.[1] ?? "";
17
+ if (!/dep:tauri-plugin-wdio-webdriver/.test(qaFeature)) {
18
+ issues.push("qa-native must enable dep:tauri-plugin-wdio-webdriver");
19
+ }
20
+
21
+ const rustSource = readRustSources(path.join(root, "src-tauri", "src")).replace(/\s+/g, " ");
22
+ if (!/cfg\s*\(\s*all\s*\(\s*not\s*\(\s*debug_assertions\s*\)\s*,\s*feature\s*=\s*"qa-native"\s*\)\s*\)/.test(rustSource)
23
+ || !/compile_error!/.test(rustSource)) {
24
+ issues.push("Rust must compile_error when qa-native is enabled outside debug builds");
25
+ }
26
+ if (!/cfg\s*\(\s*all\s*\(\s*debug_assertions\s*,\s*feature\s*=\s*"qa-native"\s*\)\s*\)/.test(rustSource)
27
+ || !/tauri_plugin_wdio_webdriver::init/.test(rustSource)) {
28
+ issues.push("WebDriver registration must require debug_assertions and qa-native");
29
+ }
30
+ if (!/RIGHTKIT_QA_NATIVE/.test(rustSource)) {
31
+ issues.push("WebDriver registration must also require RIGHTKIT_QA_NATIVE");
32
+ }
33
+
34
+ for (const [name, command] of Object.entries(packageScripts)) {
35
+ if (!/qa-native|RIGHTKIT_QA_NATIVE/.test(command)) continue;
36
+ if (/^(qa|test|dev)(:|$)/.test(name)) continue;
37
+ if (/(release|publish|build|dmg|installer|package)/i.test(name) || /\btauri\s+build\b/.test(command)) {
38
+ issues.push(`package script ${name} must not enable native QA`);
39
+ }
40
+ }
41
+ return issues;
42
+ }
43
+
44
+ export function assertQaBackdoorContract(root, packageScripts = {}) {
45
+ const issues = validateQaBackdoorContract(root, packageScripts);
46
+ if (issues.length) throw new Error(`production QA backdoor contract failed:\n- ${issues.join("\n- ")}`);
47
+ }
48
+
49
+ function readRustSources(root) {
50
+ if (!existsSync(root)) return "";
51
+ const files = [];
52
+ const visit = (dir) => {
53
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
54
+ const absolute = path.join(dir, entry.name);
55
+ if (entry.isDirectory()) visit(absolute);
56
+ else if (entry.isFile() && entry.name.endsWith(".rs")) files.push(absolute);
57
+ }
58
+ };
59
+ visit(root);
60
+ return files.sort().map((file) => readFileSync(file, "utf8")).join("\n");
61
+ }
@@ -0,0 +1,47 @@
1
+ import assert from "node:assert/strict";
2
+ import { mkdirSync, mkdtempSync, writeFileSync } from "node:fs";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import test from "node:test";
6
+ import { validateQaBackdoorContract } from "./qa-contract.mjs";
7
+
8
+ function fixture({ optional = true, releaseScript = "right-release release --tier patch" } = {}) {
9
+ const root = mkdtempSync(path.join(os.tmpdir(), "right-release-qa-"));
10
+ mkdirSync(path.join(root, "src-tauri", "src"), { recursive: true });
11
+ writeFileSync(path.join(root, "right-qa.config.mjs"), "export default {};\n");
12
+ writeFileSync(path.join(root, "src-tauri", "Cargo.toml"), `
13
+ [features]
14
+ qa-native = ["dep:tauri-plugin-wdio-webdriver"]
15
+ [dependencies]
16
+ tauri-plugin-wdio-webdriver = { version = "1.2.0", optional = ${optional} }
17
+ `);
18
+ writeFileSync(path.join(root, "src-tauri", "src", "lib.rs"), `
19
+ #[cfg(all(not(debug_assertions), feature = "qa-native"))]
20
+ compile_error!("qa-native must never be enabled in release builds");
21
+ #[cfg(all(debug_assertions, feature = "qa-native"))]
22
+ fn qa_enabled() -> bool { std::env::var("RIGHTKIT_QA_NATIVE").as_deref() == Ok("1") }
23
+ #[cfg(all(debug_assertions, feature = "qa-native"))]
24
+ fn add_plugin() { tauri_plugin_wdio_webdriver::init(); }
25
+ `);
26
+ return { root, scripts: { "qa:native": "tauri dev --features qa-native", "release:patch:win": releaseScript } };
27
+ }
28
+
29
+ test("accepts a debug-only, runtime-gated native QA surface", () => {
30
+ const value = fixture();
31
+ assert.deepEqual(validateQaBackdoorContract(value.root, value.scripts), []);
32
+ });
33
+
34
+ test("rejects a non-optional WebDriver plugin", () => {
35
+ const value = fixture({ optional: false });
36
+ assert.match(validateQaBackdoorContract(value.root, value.scripts).join("\n"), /must be optional/);
37
+ });
38
+
39
+ test("rejects release scripts that enable native QA", () => {
40
+ const value = fixture({ releaseScript: "tauri build --features qa-native" });
41
+ assert.match(validateQaBackdoorContract(value.root, value.scripts).join("\n"), /release:patch:win/);
42
+ });
43
+
44
+ test("does not impose native QA on an app before its config lands", () => {
45
+ const root = mkdtempSync(path.join(os.tmpdir(), "right-release-no-qa-"));
46
+ assert.deepEqual(validateQaBackdoorContract(root, {}), []);
47
+ });
package/release.mjs CHANGED
@@ -5,6 +5,7 @@ import path from "node:path";
5
5
  import { fileURLToPath, pathToFileURL } from "node:url";
6
6
  import { spawn, spawnSync } from "node:child_process";
7
7
  import { validateRightKitCargoContract } from "./cargo-contract.mjs";
8
+ import { assertQaBackdoorContract } from "./qa-contract.mjs";
8
9
 
9
10
  const TOOL_ROOT = path.dirname(fileURLToPath(import.meta.url));
10
11
  const HARDENING_SCAN = path.resolve(TOOL_ROOT, "hardeningscan.mjs");
@@ -141,6 +142,8 @@ if (opts.upload && !target.publish) {
141
142
  }
142
143
  }
143
144
 
145
+ sweepStaleRustArtifacts();
146
+
144
147
  console.log(`right-release: done (${Date.now() - started}ms)`);
145
148
  releaseLock.release();
146
149
 
@@ -160,6 +163,32 @@ async function mustExist(file, message) {
160
163
  await access(file).catch(() => fail(message));
161
164
  }
162
165
 
166
+ // Post-release target/ hygiene: `cargo sweep --installed` deletes artifacts left
167
+ // by toolchains rustup no longer has (each update orphans a multi-GB pile per
168
+ // app) and never touches anything the current toolchains produced. Runs after
169
+ // publish so it cannot race an artifact, and is best-effort: a missing
170
+ // cargo-sweep or a sweep failure must never fail a release.
171
+ function sweepStaleRustArtifacts() {
172
+ const candidates = [...new Set([root, workdir].flatMap((base) => [base, path.join(base, "src-tauri")]))];
173
+ const projects = candidates.filter((dir) => existsSync(path.join(dir, "target")));
174
+ for (const dir of projects) {
175
+ if (opts.dryRun) {
176
+ console.log(`dry-run: cargo sweep --installed ${dir}`);
177
+ continue;
178
+ }
179
+ try {
180
+ const result = spawnSync("cargo", ["sweep", "--installed", dir], { stdio: "inherit" });
181
+ if (result.status !== 0) {
182
+ console.log("right-release: stale-artifact sweep skipped (install with: cargo install cargo-sweep)");
183
+ return;
184
+ }
185
+ } catch {
186
+ console.log("right-release: stale-artifact sweep skipped (install with: cargo install cargo-sweep)");
187
+ return;
188
+ }
189
+ }
190
+ }
191
+
163
192
  async function runInstall(pm, cwd) {
164
193
  if (pm === "pnpm") await run("pnpm", ["install", "--frozen-lockfile"], cwd);
165
194
  else if (pm === "npm") await run("npm", ["ci"], cwd);
@@ -180,6 +209,7 @@ async function validateRightKitPackageContract(root, appName) {
180
209
  const packageJsonPath = path.join(root, "package.json");
181
210
  const pkg = JSON.parse(await readFile(packageJsonPath, "utf8"));
182
211
  const scripts = pkg.scripts ?? {};
212
+ assertQaBackdoorContract(root, scripts);
183
213
  const workflowDir = path.join(root, ".github", "workflows");
184
214
  if (existsSync(workflowDir)) {
185
215
  const workflows = await readdir(workflowDir);
package/release.test.mjs CHANGED
@@ -264,6 +264,21 @@ test("Windows update re-signs the updater artifact after Azure code signing", ()
264
264
  assert.ok(publishAt > updaterAt, result.stdout);
265
265
  });
266
266
 
267
+ test("release sweeps stale rust artifacts when a target dir exists", () => {
268
+ const config = fixture();
269
+ const dir = path.dirname(config);
270
+ mkdirSync(path.join(dir, "src-tauri", "target"), { recursive: true });
271
+ const result = run(config, "--tier=patch");
272
+ assert.equal(result.status, 0, result.stderr);
273
+ assert.match(result.stdout, /dry-run: cargo sweep --installed .*src-tauri/);
274
+ });
275
+
276
+ test("release skips the sweep when no target dir exists", () => {
277
+ const result = run(fixture(), "--tier=patch");
278
+ assert.equal(result.status, 0, result.stderr);
279
+ assert.doesNotMatch(result.stdout, /cargo sweep/);
280
+ });
281
+
267
282
  test("release lock is single-flight and is cleaned after success", () => {
268
283
  const { dir, config } = lockFixture();
269
284
  const lockPath = path.join(dir, ".cache", "right-release", "mac.lock.json");
@@ -10,8 +10,13 @@ import {
10
10
  assertPublishedRightKitCargoDependencies,
11
11
  validateRightKitCargoContract,
12
12
  } from "./cargo-contract.mjs";
13
+ import { assertAsrAdapterPair } from "./asr-artifact-adoption.mjs";
13
14
 
14
15
  const workspace = path.resolve(new URL("../../../..", import.meta.url).pathname.replace(/^\/(\w:)/, "$1"));
16
+ // The Right Suite web layer's repo + filesystem path is `rightsites` post the 2026-07-15 naming lock;
17
+ // a not-yet-renamed checkout may still have `rightapps`. Resolve whichever exists so the contract
18
+ // passes on both. (The runtime service namespace stays `rightapps` — that is deliberately untouched.)
19
+ const siteRepoDir = existsSync(path.join(workspace, "rightsites")) ? "rightsites" : "rightapps";
15
20
  const versionsPath = path.join(path.dirname(new URL(import.meta.url).pathname.replace(/^\/(\w:)/, "$1")), "rightkit-versions.json");
16
21
  const versions = JSON.parse(readFileSync(versionsPath, "utf8"));
17
22
  const isolatedCargoHome = mkdtempSync(path.join(tmpdir(), "rightkit-cargo-home-"));
@@ -21,7 +26,9 @@ const apps = [
21
26
  { key: "viewright", root: "viewright", tauri: "src-tauri/tauri.conf.json", releaseFiles: ["scripts/build-mac-notarized.sh"] },
22
27
  { key: "scraperight", root: "scraperight", tauri: "src-tauri/tauri.conf.json", releaseFiles: ["package.sh"] },
23
28
  { key: "heardright", root: "heardright/tauri-app-next", repoRoot: "heardright", tauri: "src-tauri/tauri.conf.json", releaseFiles: ["scripts/mac-dmg.mjs", "scripts/publish-release.mjs"] },
24
- { key: "mailright", root: "mailright", tauri: "src-tauri/tauri.conf.json", releaseFiles: ["scripts/build-mac.sh"] },
29
+ // mac-dmg.mjs is the release file, as in HeardRight/CodeRight. build-mac.sh only delegates to it,
30
+ // so asserting the mirror call against the wrapper failed a pipeline that does mirror.
31
+ { key: "mailright", root: "mailright", tauri: "src-tauri/tauri.conf.json", releaseFiles: ["scripts/mac-dmg.mjs"] },
25
32
  { key: "coderight", root: "coderight/apps/coderight-tauri", tauri: "src-tauri/tauri.conf.json", releaseFiles: ["scripts/mac-dmg.mjs"] },
26
33
  ];
27
34
 
@@ -310,7 +317,7 @@ test("Cargo version contract rejects staged versions that mismatch canonical man
310
317
  test("RightKit exposes one current version manifest", () => {
311
318
  assert.equal(existsSync(versionsPath), true, "rightkit-versions.json must be the single current-version source");
312
319
  assert.match(versions.packageManager, /^pnpm@\d+\.\d+\.\d+$/);
313
- assert.equal(versions.npm["@rightkit/release"], "0.2.21");
320
+ assert.equal(versions.npm["@rightkit/release"], "0.2.22");
314
321
  assert.equal(versions.npm["@rightkit/license"], "0.1.5");
315
322
  assert.equal(versions.npm["@rightkit/logs"], "0.1.3");
316
323
  assert.equal(versions.npm["@rightkit/tauri"], "0.1.0");
@@ -329,7 +336,7 @@ test("license v2 public vector is identical at every portable consumer boundary"
329
336
  );
330
337
  for (const relativePath of [
331
338
  "tools/rightkit/packages/license/test-vectors/license-v2.json",
332
- "rightapps/packages/api/src/licensing/test-vectors/license-v2.json",
339
+ `${siteRepoDir}/packages/api/src/licensing/test-vectors/license-v2.json`,
333
340
  "scraperight/tests/fixtures/license-v2.json",
334
341
  ]) {
335
342
  assert.equal(
@@ -450,6 +457,12 @@ for (const app of apps) {
450
457
  });
451
458
  }
452
459
 
460
+ test("HeardRight and ScrapeRight expose one locked ASR promotion adapter contract", async () => {
461
+ const heard = (await import(`${pathToFileURL(path.join(workspace, "heardright/tauri-app-next/right-release.config.mjs"))}?asr=${Date.now()}`)).default;
462
+ const scrape = (await import(`${pathToFileURL(path.join(workspace, "scraperight/right-release.config.mjs"))}?asr=${Date.now()}`)).default;
463
+ assert.equal(assertAsrAdapterPair(heard.runtimeArtifacts?.asr, scrape.runtimeArtifacts?.asr), true);
464
+ });
465
+
453
466
  function findFiles(root, filename) {
454
467
  const found = [];
455
468
  const visit = (dir) => {
@@ -488,7 +501,7 @@ test("Right Suite has no hosted workflow files", () => {
488
501
 
489
502
  test("RightApps brand hosts expose app-keyed update manifest proxies", () => {
490
503
  for (const app of apps) {
491
- const siteRoot = path.join(workspace, "rightapps", app.key);
504
+ const siteRoot = path.join(workspace, siteRepoDir, app.key);
492
505
  for (const route of ["patches", "latest"]) {
493
506
  const routeFile = path.join(siteRoot, "src", "routes", "releases", `${route}.json`, "index.ts");
494
507
  const source = readFileSync(routeFile, "utf8");
@@ -4,14 +4,23 @@
4
4
  "npm": {
5
5
  "@rightkit/license": "0.1.5",
6
6
  "@rightkit/logs": "0.1.3",
7
- "@rightkit/release": "0.2.21",
7
+ "@rightkit/platform-ui": "0.1.0",
8
+ "@rightkit/qa": "0.1.0",
9
+ "@rightkit/release": "0.2.22",
8
10
  "@rightkit/tauri": "0.1.0",
9
11
  "@rightkit/updates": "0.2.3"
10
12
  },
11
13
  "cargo": {
12
14
  "rightkit-license": "0.1.2",
13
15
  "rightkit-logs": "0.1.0",
16
+ "rightkit-process": "0.1.0",
14
17
  "rightkit-tauri": "0.1.0"
15
18
  },
16
- "stagedCargo": {}
19
+ "stagedCargo": {},
20
+ "swift": {
21
+ "rightkit-swift": {
22
+ "url": "https://github.com/bogusyogi/rightkit-swift.git",
23
+ "version": "0.1.0"
24
+ }
25
+ }
17
26
  }