npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@rightkit/release

v0.6.8

Published

Portable release CLI/SDK with fail-closed public GitHub Releases and private R2 + RightApps distribution.

Readme

@rightkit/release

Portable release CLI and SDK for Right Suite products: builds, signs, and publishes to public GitHub Releases or to private R2 with RightApps distribution. It also runs the ownership catalog and lockfile checks and the release gates.

| Item | Value | |---|---| | Where it runs | Build and release tooling. Native signing runs on the build host. | | Rust pair | None. The Rust build broker files in this package (managed-cargo.mjs, managed-roots.mjs) are moving to the separate RustRight product. Do not extend them here. | | Version | see INDEX.md | | License | MIT OR Apache-2.0 |

Signing and publication rules are in the workspace file docs/rules/release-signing.md. Public releases use RightKit CI. Private builds run on the native host.

No test-automation surface ships

Release and update builds (right-release build, publish-update) must not contain any test-automation or debugging surface; the development lane may. Layers, in the order they run:

  1. Source (before anything is built; right-release build --dry-run only loads the config and does not reach this layer) - qa-build-features.mjs refuses a build command, a package.json script it runs, or an app Cargo.toml that enables devtools, qa-native, qa-control, dev-native or dev-inspect (also --all-features, and devtools on the tauri dependency, qa-control on a non-optional rightkit-qa-driver (or control on a non-optional rightkit-qa; rightkit-qa/control is also refused as a feature item), dev-inspect on a non-optional rightkit-tauri-shell, a non-optional tauri-plugin-wdio-webdriver, or any of them reachable from the default features).

  2. Pre-pack payload scan - qa-control-scan.mjs over the raw EXE (Windows NSIS and portable), the macOS signing set and notarize.app (the .app going into the DMG; dmg build also scans its staging directory), the MSIX stageDir before packing, and target.qaControlScan. Mach-O, PE and ELF files are matched against five rules: qa-control-server (marker RIGHTKIT-QA-CONTROL-SERVER-PRESENT), shell-dev-inspect (marker RIGHTKIT-SHELL-DEV-INSPECT-COMPILED, written by rightkit-tauri-shell's dev-inspect feature; no allow-list lifts it), wdio-webdriver (2 of 3 of tauri-plugin-wdio-webdriver, tauri_plugin_wdio_webdriver, wdio-webdriver:default), remote-debugging (--remote-debugging-port, WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS) and tauri-devtools (plugin:webview|internal_toggle_devtools, toggleDevtoolsHotkey); each signature also as UTF-16LE. A scan that finds no native binary fails.

    Web-bundle mock scan: release also runs assertNoWebBundleMocks over the frontend dist (target.webBundleScan, else build.frontendDist of src-tauri/tauri.conf.json) before packaging when it already exists (also under --dry-run) and after the package command, where a missing or empty dist fails. right-release bundle-scan (Node/Python runtime roots) and identity-scan (needs the app's declared cdhash/team/identifier) are not automatic: both need app-specific inputs, so each app runs them from its own checks.

  3. Packed MSIX - the finished .msix is read through its ZIP central directory (stored and deflate entries, nested ZIPs to depth 3, 1 GiB per entry, 8 GiB inflated per container; encrypted, unsupported-method or oversized entries are findings).

  4. Hardening targets (final artifacts) and every sealed file (verifySealedRelease, used by every seal and upload path) run the same rules, including ZIP containers.

Not coverable after packing: an NSIS exe (LZMA payload) and a compressed DMG. They are covered only by layer 2, so the release fails when no pre-pack scan saw a native binary, and the log names each unreadable installer. Not coverable at all: assets embedded compressed in the app, resources an NSIS installer packs that were never in the scanned set (list them in target.qaControlScan), and any surface without a rule in QA_RULES. The wdio-webdriver rule is weaker than the marker rule (signatures chosen without the plugin source). Full list: crates/rightkit-qa-driver/README.md, "QA control never ships".

DevTools policy (owner's fixed design)

Every Right app has absolutely NO access to DevTools; the single exception is ViewRight, which may inspect the HTML content it opens, never its own UI or shell. The rules above therefore refuse devtools (and rightkit-tauri-shell's content-devtools) for every app, with one narrow allowance (devtools-policy.mjs): an app whose right-release config declares devtools: "content-only", and that is listed in devtools-policy.json (contentOnlyApps, today only viewright), and whose Cargo manifest depends on rightkit-tauri-shell with content-devtools, and whose build.rs declares an application ACL manifest. Then the source layer accepts devtools / content-devtools, and the binary layer accepts the tauri-devtools signatures only in a binary that also carries RIGHTKIT-SHELL-DEVTOOLS-CONTENT-ONLY-COMPILED (written by rightkit-tauri-shell when content-devtools is compiled). --all-features, qa-native, qa-control, dev-native, dev-inspect and every other rule stay refused. The allowance is recorded in the sealed manifest (devtools) and re-checked against the allow-list when a sealed file is verified. Any other app declaring it is refused (devtools-policy.test.mjs).

Shell contract rules (shell-contract.mjs, preflight SC-R1..SC-R9)

How an app wires rightkit-tauri-shell is checked from committed files alone, before any build. shell-contract.mjs exports SHELL_CONTRACT_RULES ({ id, gap, run({ root, files }) }, findings { file, subject, problem, fix } like the G13-G21 rules); scripts/preflight.mjs runs them on any repository with a tauri.conf.json and ratchets their findings in ci/app-check-baseline.json (the nine rules are the SC-R rows of the table in "The app check") (identity rule|file|subject, no line numbers). Run one with node scripts/preflight.mjs --repo <app dir> --only shell-csp. Each app is a tracked tauri.conf.json; a tauri.<platform>.conf.json is merged into it by JSON Merge Patch (arrays replace) and judged as its own target only when its patch restates the checked key.

The module imports node builtins only (preflight runs with no node_modules), reads Rust and TSX with a comment- and string-aware scanner instead of a parser (an expression it cannot resolve is skipped, never guessed), and reports a rule that fails on one app as a rule-error finding instead of throwing. Its content-devtools Cargo test is a copy of selectedContentDevtoolsCrate in devtools-policy.mjs; shell-contract.test.mjs holds the two together.

Release switch scan (M21/M23, owner decision D4, ratified)

release-switch-scan.mjs proves that release code cannot use an environment variable, argument, settings key or build flag to select a test mode, data root, executable, library, model or identity. The runtime half is rightkit_daemon::mode: a build without the test-mode cargo feature reads no override selector at all. Registry: release-switch-registry.json (forbidden features, the RIGHTKIT-TEST-MODE-COMPILED marker, deny names with their class). Apps add their own deny names under releaseSwitches.forbid. Selector classes: S1 test activation, S2 authority or identity override, S3 executable/library/model, S4 internal state or data root (S1-S4: forbidden, no allow-list), S5 user-content location (allowed, user gesture), S6 diagnostics/tuning (allowed, bounded value).

  1. Feature closure (authoritative) - release.mjs runs cargo tree -e features,no-dev --locked --target <triple> in the app repository (resolveFeatureClosure; forwards the build command's --features/--no-default-features; manifest resolves targets.<os>.cargoManifest -> releaseSwitches.manifest -> src-tauri/Cargo.toml -> Cargo.toml and must exist; features are also read from package.json scripts the hooks run (up to three levels) plus releaseSwitches.features; no manifest fails with release-switch-manifest-missing unless the app sets releaseSwitches.rust: false (the only skip); a cargo failure fails a real release, a dry run reports it) and passes it to featureClosureFindings(...) and commandSwitchFindings(argv) refuse test-mode, qa-native, dev-native, qa-control, dev-inspect anywhere in the resolved graph (finding id forbidden-feature <name>). The binary marker below is secondary evidence only. qa-build-features.mjs also lists test-mode as a forbidden feature, including on a non-optional rightkit-daemon dependency. Two daemon host features are declarations rather than bans: rightkit-daemon remote is refused (remote-feature-undeclared) unless the app sets releaseSwitches.remoteHost: true, and the loopback feature remote-loopback (CodeRight's loopback HTTP + bearer host) is refused (remote-loopback-undeclared) unless the app sets releaseSwitches.remoteLoopback: true. Neither declaration covers the other; a remote reached only through remote-loopback in the cargo tree indentation is judged by the loopback rule, any other path to remote (a direct request, testing) still needs remoteHost, and a tree without indentation counts every remote as direct. Both values must be booleans.
  2. Inventory completeness - sourceSwitchFindings walks app source (Rust, TS/JS, Swift, C; not tests, fixtures, node_modules, target) and rejects an environment read whose name is not in switches.json (unregistered-read), a non-literal read in a file not listed under dynamicReads (dynamic-read), an S1-S4 entry without compiledOut (not-compiled-out), and a malformed inventory (inventory-invalid). Argument flags are enforced for deny-listed flags only.
  3. Binary - scanSwitchPaths / bufferSwitchFindings over Mach-O, PE and ELF files: the marker (test-mode-capability) and any deny name from S1-S4 (release-switch-names), each as Latin-1 and UTF-16LE. The receipt holds the sha256 and size of every scanned file; the caller adds source commit, target triple and resolved features.

assertNoReleaseSwitches({ treeText, commands, sourceRoot, inventoryText, binaryPaths, appForbid }) runs every layer it is given input for. Limits: packed NSIS/DMG and compressed MSIX members are covered only by the pre-pack scan of the staged payload, symlinks are skipped, and browser JS is checked at the source layer only. release.mjs runs the source layer once before packaging (inventory at releaseSwitches.inventory or switches.json; a missing inventory fails a non-dry-run release) and the binary layer wherever it runs the QA-control binary scan; a finding stops the release. Trust model: a release build reads no override switch at all; the test capability is compiled out (test-mode cargo feature, refused by qa-build-features.mjs), never hidden behind a flag.

switches.json (one per app)

{
  "schema": 1,
  "app": "viewright",
  "switches": [
    { "name": "HEARDRIGHT_ENGINE_TEST_MODE", "class": "S1", "channel": "env",
      "anchor": "src/test_mode.rs:97", "compiledOut": "feature:test-mode", "note": "" },
    { "name": "LIGHTCRAFT_LOG", "class": "S6", "channel": "env", "valueType": "enum" },
    { "name": "--library", "class": "S5", "channel": "argv", "userGesture": true }
  ],
  "dynamicReads": [ { "file": "src/x.rs", "reason": "why a literal name is impossible" } ]
}
  • name (required, unique): environment variable, argument flag, settings key or VITE_* name.
  • class (required): S1..S6. channel (required): env, argv, settings, preference or frontend.
  • S1-S4 need compiledOut: feature:<name>, cfg:test, cfg:debug_assertions or, for Swift, swift:DEBUG (the read sits inside #if DEBUG; the read exists only there). The source walk skips test directories, including Swift bundles named Tests, UITests, <App>Tests and <App>UITests. S5 needs userGesture: true. S6 needs valueType of bool, int or enum.
  • anchor and note are informational. Fixtures: fixtures/release-switches/ (app/ is a clean example).

Non-shipped tooling: releaseSwitches.nonShipped

On desktop (non-iOS) targets the source walk covers the whole app root, so app tooling that never ships (a bakeoff harness) can produce unregistered-read findings that cannot truthfully be declared in switches.json. Declare such directories in the release config:

"releaseSwitches": { "nonShipped": [ { "dir": "bakeoff", "reason": "bakeoff harness; not compiled or bundled" } ] }
  • It is an exclusion list: any directory not listed is still scanned, so an omission fails safe. dir is relative to the app root; reason is required and non-empty. iOS is unchanged (it already walks only the Xcode project and the local crates in its cargo tree).
  • Every entry is validated before the walk and a bad one fails the release with inventory-invalid. Refused: a dir that is missing, not a real directory (a symlink), absolute, escaping the root, or the root itself; an empty reason; a dir that contains or sits inside a local crate of the resolved release cargo tree (a crate at the app root protects src/), src-tauri, or the Tauri frontendDist bundle; a dir that contains a package.json or sits inside a directory below the root that has one; when the root has a package.json, src/, public/ and every existing directory named by the root index.html (src/href), a quoted string in vite.config.* (root, publicDir, build input) or include/files of tsconfig.json, plus anything inside or containing them; and any dir holding a path named by src-tauri/tauri.conf.json beforeBuildCommand/beforeDevCommand/beforeBundleCommand a root package.json build* script, or the release recipe's own commands (targets.<os>.prePackage, package, postSign, packageHook, the finalizer; followed through pnpm|npm|yarn run <script>), because those run during the release build.
  • Not protected: the release code does not otherwise know the frontend source layout. A frontend input that none of the rules above names (for example one referenced only from a nested config, or built by a script not reachable from those commands) is not refused, so list only directories you know never ship.

Commands

right-release maps these subcommands to the modules in this package:

| Group | Subcommands | |---|---| | Build | release, build, package, build --web / web-build (web frontend only) | | Build (SwiftPM apps) | swift-package (windows-stage, msix, mac-app) (config schema 1: windows.{exe,out,searchDirs,include,dependencies,publisher,assets}, mac.{executable,bundleId,out,frameworks,resources,entitlements}) | | Install | install-app | | Publish | publish, upload, publish-update, github, mirror-root-artifact | | Signing | sign-windows, sign-updater, signing-agent, create-mac-updater, sign-dev-artifact, sign-candidate, sign-qa-bundle | | Engine locator | engine-locate --engine cutright\|genright [--sha256 <hex>] [--json]: the verified route to the installed CutRight or GenRight engine, also importable as locateEngine from engine-locator.mjs. The path is fixed (<LocalAppData or ~/Library/Application Support>/Orthic Labs/<App>/current/bin/<engine>), must be a regular file (no symlink or junction), and must pass signature and identity checks (macOS: strict codesign, Developer ID Team ID, fixed Identifier, hardened runtime, not ad hoc; Windows: valid Authenticode with the Orthic Labs signer). An optional sha256 pins the exact build. There is no environment variable, path option or PATH search, so release builds never trust a caller-supplied engine path. Exit 1 with a typed ENGINE_* code on any failure | | Candidate signing | sign-candidate --platform win\|mac --artifact <dir> --candidate-receipt <receipt.json> --out <binding.json> [--manifest <sign.json>] [--publisher <subject>] [--dry-run]: signs the exact unsigned candidate bytes in place instead of rebuilding from source. The candidate receipt ({ schema: 1, files: [{ path, sha256 }] }) is checked first and any mismatch refuses; Windows signs through Azure Artifact Signing (--publisher pins the MSIX signer subject), macOS through Developer ID with fixed identifiers from --manifest. Writes a binding receipt (rebuilt: false, candidate sha -> signed sha per file). Never builds, notarizes or publishes | | Dev artifact signing | sign-dev-artifact --artifact <dir> --manifest <sign.json> [--metadata <payload.json>] [--evidence <path>] [--dry-run]: Developer ID signs an existing CI dev artifact per file with fixed identifiers (macOS). It verifies the recorded hashes in the payload metadata (default payload.json, { schema: 1, files: [{ path, sha256 }] }) first, signs each sign-manifest entry (file relative to the artifact, identifier required, nested code before its bundle) through the dev-signing identity (missing Developer ID: DEV_MAC_SIGNING_DEVELOPER_ID_REQUIRED, no ad hoc fallback), verifies with codesign --verify --strict, reads back Identifier and TeamIdentifier, refreshes the metadata hashes, and writes evidence JSON. Never builds, notarizes or publishes. The payload metadata must sit outside every signed entry (SIGN_DEV_ARTIFACT_METADATA_IN_SIGNED_ENTRY). An optional declarative refresh in the sign manifest recomputes digests that other JSON files record about signed bytes: { "refreshRoot": "<dir inside the artifact>", "refresh": [{ "file", "pointer" (JSON pointer to an existing string), "compute": "sha256-file" \| "sha256-tree-v1" \| "verify-sha256-file", "of", "format"?, "currentMatches"? }] } (file and of relative to refreshRoot; sha256-tree-v1 = sha256 over the sorted relative paths, each as path, NUL, 8-byte big-endian length, bytes; verify-sha256-file writes nothing and refuses a digest that changed; format embeds the digest in a locator string through one {value}; currentMatches refuses to overwrite a value of another kind, such as a signed attestation locator). Order is enforced: operations run after the entries they read are signed and before the first entry that contains a refreshed file, so that bundle (the wrapper) signs last over the final bytes; an operation may not read what a later one writes. JSON is rewritten in place with key order kept, and a file the writer would have to reformat is refused. Evidence gains a refresh list | | Checks | deps, hardening, harden, hardeningscan, cargo-target | | Update | update check\|apply\|gate\|land\|discard\|run\|schedule [--repo <path>]: runs the right-update bin of the @rightkit/outdated this package pins (an exact dependency), because pnpm links only an app's direct dependencies' bins and no app has right-update on its path. It reads .rightkit/update.json; pnpm exec right-release update run is what a stale-pin refusal tells an app to run. A missing updater exits 2 | | Ownership | ownership, ownership-approval, ownership-report, ownership-mandatory, adoption-evidence | | Test | test (the app's declared suites) | | Legal | license-notices npm, license-notices cargo, license-notices binaries | | Other | dev, doctor, suite-doctor, qa, dmg, cache, disk (see "Disk hygiene on the agent volume"), apple, inventory, model, addon, hooks, lsclean, generate-dmg-background, publish cargo, publish npm, publish swift |

The app check: deterministic first, no flag skips it

right-release check [--repo <dir>] [--config <file>] [--commit <rev> | --worktree] [--only <rule,...>] [--list] [--print-baseline] [--baseline <file>] is the offline app check, the same as rightkit-gate check: it reads files only (the committed tree of --commit, default HEAD, or with --worktree the tree on disk, which is what a compile would see), runs every rule and prints every finding in one run. Exit 0 passes, 1 has findings, 2 could not run (a repository with no commit yet says the repository has no commit yet; commit first). The repository is the git top-level of --config's folder, else of --app-root (recipe-build), else of the working directory.

The commands that compile, package, sign, ship or inspect a release run it first, in the same process, and start nothing when it is red or cannot run (outside a git repository it is red). There is no flag that skips it: --help, --dry-run, --check and --list only change what a command does after the check.

| Mode | Commands | |---|---| | commit (HEAD: what is built, packaged, signed and uploaded) | release, build (also build --web and the direct-flag form right-release --platform ...), package, web-build, swift-package, recipe-build, qa, upload, publish, publish-update, github, sign-windows, sign-updater, create-mac-updater, sign-candidate, addon build | | worktree (the tree on disk, as a compile sees it) | dev, doctor, test, stage-sidecar, sign-dev-artifact |

Not guarded: the report forms of guarded commands (publish cargo|npm|swift, qa audit, doctor --all) and every other command. Each of those is listed with its reason in UNCHECKED_COMMANDS (app-check.mjs), and app-check.test.mjs fails a command that is in neither list. dmg takes --app/--out paths, compiles nothing and has no repository; addon is checked only as addon build (it runs the add-on's own build command), its doctor, upload and adopt forms compile nothing.

The rules (right-release check --list prints their ids and gaps; scripts/preflight.mjs adds RightKit's own):

| Rule | Gap | Finds | |---|---|---| | eol-policy | G04 | a committed blob with CRLF where .gitattributes says eol=lf | | file-url | G12 | the .pathname of a URL built from import.meta.url used as a path (wrong on Windows; use fileURLToPath) | | retired-alias | P11 | an invocation of rightkit, the retired name of the RustRight client, outside history | | qa-platform | G13 | a Rust "platform:<x>" requirement that is not darwin or windows (the test skips forever) | | test-harness | G14 | a Cargo [[test]] whose file defines fn main( but lacks harness = false (libtest runs 0 tests) | | scenario-skip | G15 | a .scenario( test that prints SKIP and returns (a skip that reports green) | | release-config | G16 | right-release.config.json, read as data (or a legacy right-release.config.mjs): a provider publish-update.mjs refuses, an updater keyFile, an undefined Cargo profile; JSON that right-release cannot load is a finding | | skip-checks | G17 | a package.json script that runs right-release with --skip-checks | | qa-dev-server | G18 | a package.json qa* script that runs tauri dev (QA would test the dev server, not the app) | | ffmpeg-release-escape | G19 | allow_in_release_builds in non-test Rust outside crates/rightkit-ffmpeg | | switches-inventory | G20 | a directory with a right-release.config.json, read as data (or a legacy right-release.config.mjs), and no switches.json (or its releaseSwitches.inventory) | | speech-link | G21 | import Speech under swift/RightKitSwift/Sources or Tests | | e2e-under-cargo-test | G22 | cargo test, cargo run or cargo nextest (plain, rustright cargo, or the retired rightkit) that runs an end-to-end crate in the broker: it holds the compile slot for the whole run | | suite-key-literal | C3-04 | the Right Suite licence public key pasted as a byte array, 64 hex digits, base64 or \x escapes, or the updater key text outside a tauri.conf.json, in a non-test file; take it from the contract's generated constants | | suite-updater-key | C3-14 | a tauri.conf.json plugins.updater.pubkey that is not a copy of a key in contracts/right-suite-api.v1.json (missing, a placeholder, another key or an unparseable file); a platform override may leave it to the base config | | engine-record-store | C3-05 | a call to from_record_store outside crates/rightkit-license: the owner binary hands the engine a snapshot through rightkit_credentials::handoff instead | | shell-window-hidden | SC-R1 | a window created at startup (desktop targets) without visible: false; a platform patch that restates app.windows must restate it | | shell-plugin-order | SC-R2 | the shell plugin installed from setup, or behind a non-QA plugin (the shell's single_instance_plugin and QA plugins may come first; a shell built with single_instance(false) may follow the plugin that arbitrates) | | shell-handler | SC-R3 | an rk_shell_* command the renderer invokes (a literal, the body of an @rightkit/tauri-shell import, or the members it reaches on its createShell(): shell.window.close() is rk_shell_window) that is neither listed in generate_handler! nor covered by handler! (the command map of @rightkit/tauri-shell ships in the package as shell-contract-bridge.json, generated by scripts/generate-shell-contract-bridge.mjs); a shell it cannot follow needs at least one listed | | shell-csp | SC-R4 | no CSP, or one without default-src 'self', frame-ancestors 'none' or connect-src, or with unsafe-inline / unsafe-eval in script-src (default-src when there is none); style-src 'unsafe-inline' and ipc: are fine | | shell-grants | SC-R5 | a core:window:allow-* grant that rk_shell_window mirrors (minimize, maximize, unmaximize, toggle-maximize, set-fullscreen, close, hide, show) while that command is registered; start-dragging and verbs the shell has no command for stay allowed | | qa-window-label | SC-R6 | QA Control bound to a label other than the shell's main_label (a bare Control binds the launcher's [ui] window_label, which defaults to main, so an app whose window is not main names it with .window(..)), or a main label no config or code window carries | | devtools-declaration | SC-R7 | the five sites (release config devtools: "content-only", devtools-policy.json, content-devtools feature, build.rs ACL manifest, .devtools_policy(ContentOnly)) disagree once any of the declaration, the feature or the policy is present | | shell-lifecycle-external | SC-R8 | lifecycle_only / lifecycle_plugin beside a WebviewUrl::External window; host_windows() keeps hardening and the navigation policy | | legal-receipt-native | SC-R9 | <Onboarding> (legalMode not none/notice) with no legalReceipt or a hand-written one, or a LegalGate that stamps the acceptance with the webview clock while the app has no createNativeLegalReceiptPort | | release-config-identity | CA-01 | a right-release.config.mjs whose default-exported object declares schema but whose top-level app or version is missing, undefined, null, empty or not a string (read as text, never run); a value it cannot prove from a string literal or a top-level const string in the same file is an UNSUPPORTED finding, not a pass; a JSON config needs strings, or a $fromJson/$fromToml node whose selected value (resolved from the committed file as release-config.mjs does) is a non-empty string | | pnpm-node-floor | CA-05 | a workflow job that runs pnpm and sets up a Node below what the pinned pnpm needs (pnpm 11: Node 22.13, consumer-contracts.json), or a .node-version/.nvmrc below it | | rust-dependency-msrv | CA-05 | a Cargo.lock crates.io entry whose rust_version (the pinned crates.io index snapshot in consumer-contracts.json) is above the concrete channel of the nearest rust-toolchain(.toml); stable and versions the snapshot lacks are not judged | | release-age | CA-06 | a pnpm-lock.yaml package published (pinned npm snapshot in consumer-contracts.json) after the check's clock less minimumReleaseAge (explicit, or the pnpm 11 default of 1440 minutes) and not in minimumReleaseAgeExclude | | notice-lock-digest | CA-14 | a notice or licence file whose <lock> sha256=<hex> (right-release license-notices, @rightkit/legal npm appendix) or <lock> sha256 <hex> (legal-snapshot notices) differs from the committed lock (a legal-snapshot notices.hashes entry, else the nearest lock of that name); a lock digest in another form, or for a lock the tree does not hold, is UNSUPPORTED; history folders (evidence, fixtures, plans, archive) are not judged | | doc-comment-bare-cr | CA-15 | a carriage return that ends no line inside a Rust ///, //!, /** */ or /*! */ doc comment (rustc: bare CR not allowed in doc-comment); CRLF line ends and plain comments are fine | | checkout-step-input | CA-07 | a workflow step that reads a tracked path (a script, a source file, a working-directory) its job's cone-mode sparse checkout does not materialize; git pathspecs, globs and comments are not inputs, and a full checkout reads everything | | source-relation | CA-09 | a value in one committed file that names a row another committed file does not have: the relations RightKit knows (SOURCE_RELATIONS: SwiftRight's hand-entries.json statusDocBlock to a docs/sr1-status.md table row) plus an app's own .rightkit/source-relations.json ({ "schema": 1, "relations": [{ id, source, select, target, adapter: "markdown-table-first-cell" }] }) | | checkout-longpaths | CA-11 | a job on a GitHub-hosted Windows runner whose checkout holds tracked paths over the Win32 limit under D:\a\<repo>\<repo>\ (<repo> from the GitHub origin remote, never the checker's directory; file 259, directory 247 characters) with no step before it that provably runs git config --system\|--global core.longpaths true as its whole script (or GIT_CONFIG_COUNT covering a KEY_n/VALUE_n pair); a conditional, echoed, custom-shell or action form, or an unknown repository name, is an UNSUPPORTED: finding, never a pass; long paths outside the sparse cone, off Windows or on a self-hosted runner are fine | | workflow-action-pair | CA-04 | one job of a workflow runs github/codeql-action init, autobuild, analyze or resolve-environment from different refs (a floating tag beside a commit counts); separate jobs, upload-sarif and other repositories keep their own refs | | referenced-input | CA-07 | a package.json script or workflow run: that runs node/sh/bash/python <file> absent from the commit or outside the repository, a pnpm --dir/-C/npm --prefix directory, a workflow working-directory or actions/setup-* cache-dependency-path the commit lacks; ignored (.gitignore) and build-output paths, ${...}/globs, directories an unconditional actions/checkout or actions/download-artifact step earlier in the same job writes (path:), and fixtures/ trees are not judged; an input only a later step, another job, an if:/continue-on-error step or an actions/cache restore could produce is reported UNSUPPORTED | | caddy-global-options | CA-08 | a Caddyfile whose first block is the global options block (no address) holding a site directive (encode, reverse_proxy, ...), or whose first address is only {$VAR} placeholders with no default (an empty variable turns it into the global options block); an explicit global block first or {$VAR:default} passes, unknown names (plugin options) are not judged | | legion-schema-projection | LG-02 | in a tree with Legion's schema generator, a schema it byte-compares that the checked commit's own .gitattributes leaves to line-ending conversion (info/attributes, global and system attributes do not count) (Windows checks it out as CRLF and --check reports SCHEMA DRIFT), or an enum that is not the code-owned Rust &[&str] list it projects | | legion-legacy-tokens | LG-03 | in a tree with Legion's naming ledger, a legacy token (seer, nemesis, forge, sentinel, sorcerer) in a path or file that src/config/naming-legacy-allowlist.json does not classify, or classifies with another exact count; immutable qualification evidence is exempt as Legion's producer exempts it | | legion-retirement-ledger | LG-04 | in a tree with Legion's retirement ledger, a skill, script, hook, doctrine, agent-rule or native command file deleted after c1d80c9d (bound to the full id c1d80c9d190e0f45dfd630d4d65e8bed748381ce) with no docs/provenance/retirements.md row; a shallow clone fails as unverified | | legion-authority-body | LG-05 | in a tree with Legion's roster, an agent card without its doctrine body verbatim (a variant also its two roster bodies), a card naming doctrine/ or src/roster/ outside its method, or a council-seat body that differs from doctrine | | retired-token | CA-13 | a token the app retired (declared in .rightkit/consumer-policy.json retiredTokens: tokens, scoped globs, extensions) in a scoped file, outside a whole-file (allowFiles) or exact-fragment (allowFragments: text, identifier or string) exception, each with a reason | | route-deny-policy | CA-13 | the app's declared deny set (routeDenyPolicies: a policy JSON of denied, exceptions and irreversible segments and fragments) disagrees with its committed route table or OpenAPI document: a denied or excepted route that is not a route, a DELETE not denied, an irreversible-looking mutation neither denied nor excepted, a route both denied and excepted, or a denied route the irreversible marks do not cover | | layer-reach | CA-13 | a value import chain (read lexically, never run) from a declared from file reaches a forbid file of the same layerBoundaries entry; a stop file is a reviewed API, accepted and not walked; a load with a non-literal specifier is a finding | | spec-route-parity | CA-13 | the committed API spec and the committed route table disagree (specRouteParity): a documented route the spec lacks, a spec operation that is not a route, or a declared undocumented route the spec documents | | source-closure | SF-01 | in any committed *.json, a sealed record { sourceClosure: { sources: { "<path>": "sha256:<hex>" }, digest } } whose sources are missing from the tree or changed since sealing, or whose digest is not sha256 over the canonical JSON of sources; a digest over a projection is declared in .rightkit/source-closures.json by naming a RightKit-approved transform identity (APPROVED_TRANSFORMS: source path, families, dropped keys and input schema are RightKit's), applied to the current bytes and compared; the repository never supplies pointers. Membership a producer would now add is not visible (it is computed by repository code) | | mandatory-foundation | OW-05 | an app (root rightkit-mandatory.json, or a right-release.config.(mjs\|json) at the root or beside a src-tauri/tauri.conf.json) without its declaration; or whose profile requires a foundation owner of ownership-mandatory-set.json that it does not declare as a dependency (missing or dev-only), or that selects an unknown or unratified profile; or a notApplicable row that is not a ruling ratified for the declared app in appAmendments, reason verbatim (the row then still applies). Owner-pending rows never fail | | ownership-exceptions | OW-06 | a .rightkit/ownership-exceptions.json with an unknown key, or an entry without capability, package, reason, owner, approvedBy, a real date, a real expires and an issue naming a RightKit issue; an entry past its expires date fails with no identity, so no baseline silences it; an entry no ratified ruling (appAmendments.<app>.exceptions: capability, package or crate, path when named, RightKit issue, expiry no later than the ruling) covers |

The CA-13 rules check contracts an app declares about itself in .rightkit/consumer-policy.json (schema 1; the shape is in the header of consumer-policy.mjs). An app without the file has nothing to check. They read JSON and source text only: no repository script, generator or config runs, so a contract that can only be judged by running the app's code (an OpenAPI body generated at runtime, for one) stays with the app's own test.

e2e-under-cargo-test (G22): the broker compiles; it does not run end-to-end tests. An end-to-end crate is a Cargo package that depends on rightkit-qa not optionally (the optional dependency is an app's in-app control server) and has a .rs file outside examples/ that names a driver module of it (harness, control, suite, scenario, engine, in_app, interactive, page, browser, android); a name like *-e2e is not evidence. A command selects it by -p/--package, --manifest-path, --workspace/--all (the members of the workspace in the working directory, less --exclude), --test, or for cargo run --bin; and by naming nothing at all in a virtual workspace (a [workspace] with no [package]): cargo test and cargo test --manifest-path Cargo.toml test every member, or the default-members when it declares them, less --exclude (a root that holds a package of its own tests that package only, so a bare run selects nothing there). It looks in package.json scripts, shell, PowerShell, batch, Node and Python scripts, Makefile and justfile recipes, workflow run: steps, git hooks, and code in README.md, AGENTS.md, CLAUDE.md and docs/ (history documents excluded). --no-run, rightkit-qa run-tests, rightkit-qa journeys run and --test qa_surface_guard are not findings. A code span in a runbook's running text that is only the command (`cargo test`, `rustright cargo test`, no argument) is a command line only where an unnegated imperative (run, use, execute, invoke, call, type) stands right before it in its clause ("Run cargo test before you push"); "cargo test unit tests", "cargo test would hold the compile slot", "only CI runs cargo test", a list label "cargo test: Rust unit tests" and "do not run cargo test" name the command and are not findings. With an argument, in a fenced block, a script, a package.json script, a recipe, a workflow step or a hook, the same command is judged whatever stands around it; a PowerShell <# ... #> block comment is a comment. The identity is e2e-under-cargo-test|<file>|<script or crate>. Fix: compile with --no-run, then rightkit-qa run-tests -p <crate> [--test <target>] -- <libtest args>; a whole-workspace run must also leave the crate out (--workspace --exclude <crate>), since --no-run on the workspace compiles it all and runs none. cargo run has no --no-run: the finding says to build the program in the broker (rustright cargo build -p <crate> --bin <bin>, with --manifest-path for a crate outside the workspace in the working directory) and run the built binary outside it, or rightkit-qa journeys run.

Known findings are tolerated only through a baseline: --baseline <file>, else the checked tree's .rightkit/app-check-baseline.json, else (from a RightKit checkout only) ci/app-check-baseline.json. The app's own file is always { "schema": 1, "known": [{ "id", "issue", "added", "sunset" }] }; the central { "known": { "<repo>": [...] } } shape in an app's file is refused (exit 2). Every app entry needs an issue, an added date (YYYY-MM-DD, UTC, not later than today) and a sunset that is not before added and at most 90 days after it. Past its sunset an entry stops tolerating its finding (the finding fails with its baseline entry expired) and is reported as EXPIRED; a listed identity that no finding has any more is STALE and fails, so the list only shrinks. A rule the build refuses whatever a baseline says (switches-inventory) fails even when listed.

The pre-push gate on a host without the broker: the Rust gate is deferred, the HOST RECEIPT gates the merge

The gate chooses its executor itself (gate-executor.mjs resolveExecutor), never the caller: the RustRight broker where the machine has it (rustright on PATH, or its managed cargo shim), a refusal on a broker-managed machine whose broker cannot be reached, and direct cargo on any other host (a cloud session, a CI runner). Every host runs the same gates in the same order (app rules, class A, then the compile scans); an unmanaged host is never sent to a broker and nothing is deferred to one. Every pushed ref is judged in any position (git's stdin is split on single ASCII spaces, so a Unicode space in a ref name cannot hide one), and a stdin line that is not four fields refuses the push on every host and in every repository, because git sends the local field as typed and a rev expression such as HEAD^{/one thing} holds spaces: such a line is never dropped, so the pushed commit is always compared with HEAD.

scriptBrokerCall reads package.json scripts only: it follows pnpm <script>, pnpm run, npm run, yarn and the wrapper words, not node -e, a node script, sh -c, or a run in another package (pnpm --dir, --filter, -r). A unit whose test reaches the broker that way says so in the app's own ci/gate.config.json, in the npm entry the unit's quietTests already uses (the key is the unit name: the package name, or the directory with / as - for a unit of a pnpm root below the repository root):

{ "npm": { "heardright": { "requiresBroker": { "reason": "the root test runs tauri-app-next/scripts/verify-local.mjs, which spawns rustright cargo test" } },
           "tauri-app-next": { "requiresBroker": { "reason": "test runs scripts/run-contract-tests.mjs, which spawns rustright cargo test" } } } }

The reason is required and is printed. The file is read from the gated commit, so the declaration is a reviewed change in the diff that adds it. Where the Rust gate is deferred, the unit's test:npm:<unit> scan is deferred with it and named declared in ci/gate.config.json npm.<unit>.requiresBroker: <reason>, never reported as passed; on a host with the broker, in CI, on a protected ref and with the opt-out it runs as it did. A declaration of any other shape (true, a string, an empty or blank reason, an array) is ignored with a note and the test runs where it can: a malformed entry never skips a test. false or null is no declaration, and a call the scripts do show is reported from the scripts. The gate does not read node scripts for a literal rustright or cargo: a literal cannot tell a spawn from a command table, a mention or a test fixture, so the declaration is the way.

The pre-push hook finds the gate in a repository whose root installs nothing

rightkit-gate install-hook writes .githooks/pre-push (and sets core.hooksPath). The hook runs the repository's own .git/hooks/pre-push, then rightkit-gate check --commit HEAD, then the gate, and finds the gate binary in node_modules/.bin/rightkit-gate of the repository root, else of any workspace package up to three levels down, else through pnpm exec, else with node at a path written into the hook. That path is always relative to the repository, the same for every version and a plain name, never absolute, never ../ out of the repository, never a node_modules/.pnpm/@[email protected]/ store path, and it is decided by what the tracked package.json files declare, never by what is installed: the same repository gets the same hook on a fresh clone, from an exports checkout, after pnpm install and after a version bump, so install-hook as a prepare script never meets a hook it wrote earlier as one that "already exists and differs". When the root package.json declares @rightkit/release (alone, or together with packages: Pulse declares it at the root and in hub/), the path is the root's node_modules/@rightkit/release/bin/rightkit-gate.mjs and the hook is the single-root form. When only packages declare it (CutRight's apps/cutright, HeardRight's tauri-app-next), the path is the same link in the first of them in sorted order, whichever is installed, the hook also tries each declaring package's node_modules/.bin/rightkit-gate directly (deeper than the glob reaches) and it runs the pnpm fallback as pnpm --dir <package> exec rightkit-gate, because pnpm exec at a root that does not declare the package has no rightkit-gate to run. When no package.json declares it, the gate's own path inside the repository if it runs from there and not from a node_modules (a checkout of the package itself), else the root's link. A declaring package whose directory is not made of letters, digits and _ - . @ + is named in a warning and left out. A root-install repository gets, byte for byte, the hook that 0.6.4 and 0.6.5 wrote (fixtures/published-gate-hooks.json); a multi-root repository whose committed hook still has a baked path regenerates it once with rightkit-gate install-hook --force, and the result no longer changes with the version or the install.

The rustfmt scan reads each file's edition from its crate

The class A rustfmt:<edition> scans group the changed .rs files by the edition of the crate they belong to: the nearest Cargo.toml at or above the file decides. A literal edition = "NNNN" in its [package] table is that edition. edition.workspace = true (or edition = { workspace = true }) takes the [workspace.package] edition of the owning workspace root, the nearest Cargo.toml at or above the crate that declares [workspace] (the crate's own manifest, for a root package that is also the workspace). When the inheritance cannot be followed (no workspace root, a root with no literal [workspace.package] edition, a manifest in the chain that cannot be read, or an edition entry in a spelling the scan does not recognise such as edition = 2021) the gate fails with a path-policy:* finding that names the manifest and the reason, once per manifest, and those files are not checked under a guessed edition. Fix the manifest (declare [workspace.package] edition, or write the edition in the crate). A manifest that names no edition at all, and a .rs file below no manifest, are checked as 2021.

The repository secret scan: reviewed records, not guesses

The class A secret-scan scan reads every file of each changed crate and package. Crate packaging, the package archive and the local-publish batch scan are strict: every .env* file, key file (*.pem, *.p8, *.p12, *.key) and env template is refused by name, and content that matches the credential shapes (SECRET_PATTERNS: a PEM private key header, AKIA..., gh[pousr]_..., npm_...) is refused. The repository gate (scanCrateForSecrets(dir, { envTemplates: true, repoRoot })) is the only mode that reads the repository's records, .rightkit/secret-scan-records.json of the gated commit:

{ "schema": 1, "records": [
  { "path": ".env.example", "sha256": "<hex of the bytes, CRLF read as LF>", "kind": "env-template", "note": "local development defaults only, no credential" },
  { "path": "engine/src/redaction.rs", "sha256": "<hex>", "kind": "fixture", "note": "redaction test vectors, fake values" }
] }

| Finding | Admitted by | Never admitted | |---|---|---| | An env template (.env.example, .env.<name>.example, .env.sample, .env.template) | a record of kind env-template whose sha256 is the file's | a template that is not UTF-8 (UTF-16 byte-order mark, NUL bytes, invalid sequence); one that holds a PEM private key header, github_pat_..., sk_live_..., xox[abprs]-..., AIza..., a Slack webhook URL or a SECRET_PATTERNS shape; one inside a publishable crate (its [package] does not say publish = false) or package (package.json not "private": true) | | Any other file whose content matches SECRET_PATTERNS (a redaction or screening test vector) | a record of kind fixture whose sha256 is the file's | | | .env, .env.local, any other .env.* that is not a template, *.pem, *.p8, *.p12, *.key | nothing: refused by name, and a record naming one is itself a violation | any |

A violation reads <path>[:<line>]: <reason>. For a template or a fixture it carries the file's sha256 and the exact record to add, with a placeholder note: review the file, write the note (what it holds and why it is safe; the placeholder is refused), and commit the record. Any edit to a recorded file changes its sha256, so the file fails again until its record is replaced, and the change of the record is the review in the pull request diff. A violation names the line and the kind of shape a hit matched, never the matched text. A malformed records file is a violation: bad JSON, a schema other than 1, an unknown key or kind, a missing, empty or placeholder note, a sha256 that is not 64 hex characters, a path that is not a normalised repository-relative path, a duplicate path, a kind that does not fit the path (env-template for a file that is not a template, fixture for a template). A record with a problem admits nothing, and the file must be a regular file inside the repository (a symbolic link is refused). Records are keyed by the repository path, so a moved file needs a new record, and the records file is scanned like any other file, so a note must not quote a credential.

The npm scans the gate plans

The gate takes the npm layout from the tracked files. Every pnpm root is discovered: the repository root, and any other directory outside node_modules (at most 6 levels deep, at most 64 roots) that holds a pnpm-lock.yaml or a pnpm-workspace.yaml beside a package.json. CodeRight has two roots, apps/coderight-tauri and apps/coderight-web, and no package at the repository root. Each root gets pnpm-lockfile and pnpm-audit run in its own directory (pnpm-lockfile:<root> and pnpm-audit:<root> below the repository root), planned when a unit of the root is in scope or its own package.json, lock or workspace file changed; each unit gets tsc:<unit>, a linter and test:npm:<unit>. A unit of the repository root keeps its package name in those ids; a unit of any other root is named from its directory (apps-coderight-web), so its ids carry the root path. A repository with one root plans what it always did. A unit is linted by the tool it configures: eslint:<unit> where an ESLint config (eslint.config.*, .eslintrc*) exists in its directory or any directory above it, biome:<unit> (pnpm exec biome lint .) where a biome.json or biome.jsonc does, both where both do and neither where neither does.

The scans that read the live checkout (cargo, pnpm and the tests) run only when the checkout equals HEAD where they read it: any change or untracked file there blocks the gate (BLOCKED: the checkout differs from HEAD where broker scans read it). The paths checked are the Rust units and workspaces and their shared build inputs, every npm unit's directory, each pnpm root's package.json, lock, workspace file, .npmrc and linter configs, and, for each npm unit, every ESLint and Biome config name in every directory between the repository root and the unit (cleanCheckoutPaths), because those are the files eslint:<unit> and biome:<unit> are planned from: an uncommitted apps/eslint.config.js above apps/cutright blocks the gate like one inside it.

The class B pnpm scans (tsc:<unit>, eslint:<unit>, biome:<unit> and test:npm:<unit>) run with pnpm_config_verify_deps_before_run=false. pnpm 11 verifies before every pnpm run and pnpm exec that node_modules matches pnpm-lock.yaml and, if not, runs pnpm install, which wants to purge the modules directory and aborts without a TTY ([ERR_PNPM_ABORTED_REMOVE_MODULES_DIR_NO_TTY]) before the tool or the test starts. The gate never installs into the checkout it reads; it verifies the lockfile itself (pnpm-lockfile in the clean export) and runs the scans against the tree as installed, so keep the tree installed (pnpm install) before a push: a tree that lacks a dependency fails in the tool or the test with its own error. The setting is part of each scan's environment (shown by --plan, in the cache key), and a failed scan's detail leads with the first [ERR_PNPM_...] line pnpm printed.

Declared test suites, web build, npm notices and artifact mirroring

These replace per-app wrapper scripts. Each is declared in right-release.config.mjs and runs without a shell from the app root (the config file's directory). A step is { cmd | node, args?, cwd?, env?, nodeFlags?, nodeOptions? }: node is an app-relative script run by the Node that runs right-release, cmd: "node" means the same Node, nodeFlags go before the script, nodeOptions are merged once into NODE_OPTIONS. cwd and node must stay inside the app; unknown keys are refused.

right-release test

tests: {
  eventPrefix: "e2e",                       // optional, default "test": events are <prefix>_suite_started / _completed
  runners: {                                // optional named bases
    vitest: { node: "node_modules/vitest/vitest.mjs", nodeFlags: ["--no-experimental-webstorage"], nodeOptions: ["--no-experimental-webstorage"] },
  },
  suites: [
    { name: "tooling", runner: "vitest", args: ["run", "scripts/tooling.test.mjs", "--no-file-parallelism", "--maxWorkers=1"] },
    { name: "runtime-config", node: "scripts/runtime-config.test.mjs", nodeFlags: ["--test"] },
    // Expands to journeys-1, journeys-2, ...: the names in the JSON list run `chunk` at a time, one process per chunk.
    { name: "journeys", node: "scripts/qa-browser-functional.mjs", args: ["--keep-going"],
      journeys: { file: "scripts/qa-e2e-journeys.json", key: "journeys", chunk: 12, flag: "--journey" } },
  ],
}

right-release test runs every suite in order and stops at the first failure, exiting with that suite's code; each suite prints a JSON line before and after ({"event":"e2e_suite_completed","suite":"tooling","exitCode":0,"durationMs":1234}). --suite a,b (repeatable) selects suites; a group name such as journeys selects all its chunks. --runner vitest -- run --project unit runs a runner once with the arguments after -- (watch mode, one project); -- <args> after --suite is allowed when exactly one suite is selected. --list [--json] prints the resolved commands and runs nothing. Wrap it as the repository needs, for example rustright storage run --app <app> -- right-release test.

right-release build --web (or web-build)

web: { build: [
  { node: "node_modules/typescript/bin/tsc", nodeFlags: ["--disable-warning=DEP0205"] },
  { node: "node_modules/vite/bin/vite.js", args: ["build"], nodeFlags: ["--disable-warning=DEP0205"] },
] }

Runs only the web.build steps: no Tauri, no signing, no packaging. --dry-run prints the commands. It is guarded by the same GitHub-Actions-only execution policy as the other build commands.

right-release license-notices npm

legal: { npmNotices: { output: "legal/THIRD_PARTY_NPM_LICENSES.html", productName: "ViewRight", exclude: ["@rightkit/"] } }

Writes the third-party npm license appendix for the production closure of a pnpm app (pnpm licenses list --prod --json), with each package's own LICENSE / COPYING / NOTICE text. A package with no license file gets the standard published text for its declared MIT or ISC license, labelled as a fallback; any other license without a file fails the run. The page records the app version, the pnpm-lock.yaml sha256 and generated-by=right-release license-notices npm, so equal inputs give equal bytes. --check writes nothing and exits 1 when the file differs. --output and --product-name override the config.

right-release license-notices cargo

legal: { cargoNotices: {
  output: "legal/THIRD_PARTY_CARGO_LICENSES.html",
  template: "legal/about.hbs",
  config: "legal/about.toml",                 // optional cargo-about config
  manifestPath: "src-tauri/Cargo.toml",       // optional Cargo manifest
  features: ["desktop"],                    // or allFeatures: true
} }

Runs cargo about generate --locked --fail through managed Cargo, with declared config, manifest, features & template. Inputs & output stay inside app root. Missing inputs, unresolved licenses, command failure or empty output fail generation. --output overrides config; --check writes nothing & exits 1 when generated bytes differ. rightkit-legal rust-licenses delegates to this route.

license-notices binaries generates notices for declared native binaries; --receipt checks staged non-catalog programs against declared coverage. Both routes accept --config, --output & --check.

Mirroring an artifact (mirror-root-artifact)

targets: { win: {
  mirror: [{ file: "src-tauri/target/x86_64-pc-windows-msvc/release/bundle/nsis/App_1.0.0_x64-setup.exe", pruneSuffix: "-setup.exe" }],
  postSign: { cmd: "right-release", args: ["mirror-root-artifact", "--config", "right-release.config.mjs", "--platform", "win"] },
} }

Copies each targets.<platform>.mirror entry (an app-relative path, or { file, name?, pruneSuffix? }) atomically into RIGHT_RELEASE_ARTIFACT_DIR. The directory must be outside the repository and the command refuses to run without it, so it never writes into the repository root. pruneSuffix then removes the other files with that suffix from that directory, so it holds only the current installer. The one-file form (--file <path> --package-root <dir> [--name <name>]) is unchanged.

Broker-managed Cargo target directory (cargoTargetRoot)

With cargoTargetRoot: true (or an app-relative Cargo.toml path) in winPackage / macPackage, declared artifact paths under <cargo dir>/target/ follow cargo metadata's target directory. On a RustRight-managed host that directory is owned by the broker and lies outside the app (for example D:\.rightkit-managed\rightkit-build-control\ws\<hash>\g\<n>\t); release.mjs needs app-relative paths, so the recipe keeps the declared paths at their default location under the app's ignored src-tauri/target/ and plans verified copies (managed-target-stage.mjs, the stage section of the recipe-build plan):

  • after tauri build, the raw EXE (win) or the built .app (mac) is copied from the real target directory to the app-relative path; every later step (sign, package, seal) sees app-relative paths unchanged;
  • win: tauri bundle embeds the EXE it finds in the real target directory, so the signed app-relative EXE is copied back there first, and the installer is copied out afterwards (its source is deleted before the bundle step, so a leftover can never be copied);
  • every copy deletes a stale destination first, writes a temp file beside the destination, verifies sha256 (a directory: a digest of every entry, symlinks kept) against the source before and after the copy, then renames; a source the build did not produce fails the step with STAGE_SOURCE_MISSING and no stale copy is left to package.

Safeguards on the managed Windows package step (a temporary bridge until the broker exposes artifacts into the app; a RustRight issue tracks that): the real raw EXE must equal the signed in-app EXE by sha256 right after the copy-back and again after tauri bundle returns (STAGE_REAL_CHANGED: another cargo job in the same workspace may have relinked it), and the installer is copied out only after both checks. A copy whose destination is in the broker tree never creates its parent (STAGE_PARENT_MISSING: the generation was rotated or evicted). The recipe re-resolves the Cargo target directory through cargo metadata before and after the step and refuses when it differs from the directory planned at expansion (STAGE_TARGET_DIR_CHANGED; docs/RIGHTKIT-THIN-BUILD-CONTROL.md §5 and §15).

Only a target directory under a managed root (resolveManagedRoots: RIGHTKIT_DATA_ROOT, RIGHTKIT_MANAGED_ROOT, RIGHTKIT_EXTERNAL_ROOT, RIGHTKIT_CONTROL_ROOT, the legacy D:\.rightkit-managed / /Volumes/D/.rightkit-managed) is staged. Any other directory outside the app still fails closed: the Cargo target directory <dir> is outside the app, so <path> cannot be addressed by an app-relative path; point CARGO_TARGET_DIR inside the app or declare the artifact paths yourself.

macOS updater tarball from macPackage

targets.mac.macPackage.updater: true (or an app-relative .tar.gz path) adds a create-mac-updater command at the front of postSign: it packs the signed, stapled .app and signs the tarball. The default output is <App>.app.tar.gz, named after the app in notarize.app. Declare the file in updater.artifacts as before. See docs/MAC-PACKAGE-RECIPE.md.

QA receipts and admission (warn by default, refuse per app)

right-release qa --stage source|installed --platform mac|win runs rightkit-qa and writes a MAC'd receipt. Admission is a per-app policy in qa-admission-policy.json:

{ "schema": 1,
  "apps": {
    "postright": { "mode": "refuse", "since": "2026-11-02", "set_by": "adrian" },
    "genright": { "mode": "warn", "since": "2026-10-24", "set_by": "adrian",
                  "stages": { "source": { "mode": "refuse", "since": "2026-11-09", "set_by": "adrian" } } } },
  "approvedRunners": [ { "sha256": "<64 hex>", "version": "0.9.0", "platform": "mac", "set_by": "adrian", "since": "2026-10-24" } ] }

Per-stage modes. An app entry is one mode for both stages, or carries stages: { source?, installed? } entries that override it for that stage (source is the build; installed is upload, publish-update and github). The installed stage cannot pass until the installed lane exists, so move an app to refuse for source first: qa policy set --app <app> --stage source --mode refuse --set-by <name>. Without --stage one mode governs both stages and any stages object is dropped. A stage that was refuse and is no longer (removed, or warn) needs set_by: "adrian", checked per stage on write and on load.

  • warn (default; an app with no entry). A missing or invalid receipt does not block. build, upload, publish-update and github print [right-release] QA warning: ... to stderr, continue, and record the outcome (below).
  • refuse. The same refusal throws QA admission refused for <app> (policy refuse since ... by ...); nothing is sealed, uploaded or published. Switch an app on with right-release qa policy set --app <app> --mode refuse --set-by <name> [--stage source|installed] (or node release-qa-policy.mjs set ...). Moving an app back to warn (or deleting its entry) is refused unless set_by is adrian; a hand edit that does so is caught on every load by comparing with the copy committed at RIGHT_RELEASE_QA_POLICY_BASELINE_REF (default HEAD; CI should use origin/main). set_by is a recorded claim, not an authenticated identity: the file must be owner-protected in the repository (CODEOWNERS and branch protection), and a downgrade that was already committed becomes the new baseline. An unreadable or malformed policy file is a hard error, never a fall back to warn. When a baseline ref is requested (RIGHT_RELEASE_QA_POLICY_BASELINE_REF or the baselineRef argument) and git cannot show the policy at that ref, the load fails closed instead of running without a baseline; with nothing requested a checkout without that history (an app's node_modules) still loads with no baseline.

Approving a runner. right-release qa policy approve-runner --runner <file> --version <v> --platform mac|win --set-by adrian [--note <text>] digests the file, runs <file> --version (the output must be exactly rightkit-qa <v>), requires <v> to be the published rightkit-qa in rightkit-versions.json (registry.cargo), re-digests the file to catch a swap while it ran, and appends { sha256, version, platform, set_by, since, note? }. Only adrian may approve (a runner entry is new admission authority; a hand-added entry by anyone else is refused on load against the baseline). An approval is per platform because a runner's digest differs per host and OS; an entry with no platform (older files) approves the digest on either. Running the approved binary is the caller's job: set qa.runner in the release config to the managed path, since a bare which rightkit-qa can resolve to a different build.

Hardening, applied in both modes (a failure is a warning in warn and a refusal in refuse):

  1. Platform and release id. The sealed receipt must be for the platform and the release id being sealed or published.
  2. Approved runner. The rightkit-qa executable's sha256 recorded in the receipt must be listed in approvedRunners for the platform being sealed or published (an entry without platform approves either). An empty list approves nothing. The binary is digested before the run and again after it; a swap during the run is refused.
  3. Every release-critical journey. With qa.journeys in the release config ({ manifest?: "journeys.toml", receipt?: "<path>", args?: { source?, installed? } }) right-release qa runs rightkit-qa journeys run (or reads receipt), validates the rightkit.journeys.receipt/1 through rightkit-qa journeys validate-receipt --manifest .. --tree .. (exit code 0 and a final ACCEPTED line), and seals a journeys summary. release-qa-floor.json maps the journey floor id to "all release-critical journeys passed": at admission the required set is recomputed from the journeys.toml commi