@rightkit/release 0.6.6 → 0.6.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +122 -9
- package/admission-files.mjs +3 -2
- package/advisory-db.json +4 -0
- package/advisory-db.mjs +124 -0
- package/advisory-exceptions.mjs +13 -10
- package/app-check.mjs +257 -38
- package/bin/darwin/rightkit-swap +0 -0
- package/bin/rightkit-agent-hook.mjs +9 -0
- package/bin/rightkit-gate.mjs +495 -344
- package/build-release.mjs +13 -6
- package/cli/right-release.mjs +29 -6
- package/compile-inputs.mjs +66 -0
- package/consumer-contracts.json +1044 -0
- package/consumer-contracts.mjs +627 -0
- package/consumer-input-contracts.mjs +422 -0
- package/consumer-policy.mjs +588 -0
- package/consumer-source-contracts.mjs +469 -0
- package/deny-default.toml +42 -0
- package/deterministic-scans.mjs +240 -29
- package/disk-command.mjs +270 -30
- package/disk-policy.json +50 -10
- package/disk-policy.mjs +482 -35
- package/disk-report.mjs +129 -0
- package/door-policy.mjs +228 -0
- package/gate-executor.mjs +30 -0
- package/gate-stages.mjs +110 -0
- package/github-updater-release.mjs +48 -8
- package/init-command.mjs +41 -0
- package/legion-semantic-contracts.mjs +428 -0
- package/ownership-class-rules.mjs +277 -0
- package/ownership-key-authority.mjs +3 -1
- package/ownership-mandatory-set.mjs +3 -2
- package/package.json +10 -3
- package/publish-cargo.mjs +5 -2
- package/publish-update.mjs +14 -11
- package/release-config.mjs +51 -6
- package/rightkit-versions.json +172 -167
- package/shell-contract-bridge.json +251 -0
- package/shell-contract.mjs +1069 -0
- package/sign-updater.mjs +24 -4
- package/source-closure-rules.mjs +204 -0
- package/stage-sidecar-command.mjs +2 -1
- package/stage-sidecar.mjs +437 -114
- package/suite-api.generated.mjs +96 -6
- package/suite-trust-roots.mjs +198 -0
package/app-check.mjs
CHANGED
|
@@ -20,13 +20,62 @@
|
|
|
20
20
|
// undefined Cargo profile, JSON right-release cannot load
|
|
21
21
|
// skip-checks G17 a package.json script that runs right-release with --skip-checks
|
|
22
22
|
// qa-dev-server G18 a package.json qa* script that runs `tauri dev` (QA would test the dev server, not the app)
|
|
23
|
-
// ffmpeg-release-escape G19 `allow_in_release_builds` in non-test Rust
|
|
23
|
+
// ffmpeg-release-escape G19 `allow_in_release_builds` or a use of `DevFfmpeg::unverified` (call or path) in non-test Rust
|
|
24
|
+
// outside crates/rightkit-ffmpeg (an item or file gated on feature "dev-overrides" is excused)
|
|
24
25
|
// switches-inventory G20 a right-release.config.json (or .mjs) directory without a switches.json (or its releaseSwitches.inventory)
|
|
25
26
|
// speech-link G21 `import Speech` anywhere under swift/RightKitSwift/Sources or Tests (Apple Speech is removed, rightkit#978;
|
|
26
27
|
// the LinkProbes control fixture is the one sanctioned import)
|
|
27
28
|
// e2e-under-cargo-test G22 `cargo test|run|nextest` (also as `rustright cargo`, or the retired `rightkit`) that runs an end-to-end
|
|
28
29
|
// crate (rightkit-qa driver modules) in the broker without --no-run: it holds the compile slot for the whole run
|
|
29
30
|
//
|
|
31
|
+
// Suite trust roots (lane C3; logic in suite-trust-roots.mjs). The licence and updater public keys have one owner,
|
|
32
|
+
// contracts/right-suite-api.v1.json (generated into suite-api.generated.mjs):
|
|
33
|
+
// suite-key-literal C3-04 the licence public key pasted as a byte array, 64 hex digits or base64, or the updater key text outside
|
|
34
|
+
// a tauri.conf.json, in a non-test file
|
|
35
|
+
// suite-updater-key C3-14 a tauri*.conf.json plugins.updater.pubkey that is not a copy of a contract key (missing, a placeholder,
|
|
36
|
+
// another key, or an unparseable file)
|
|
37
|
+
// engine-record-store C3-05 a call to from_record_store outside crates/rightkit-license: the owner binary hands the engine a
|
|
38
|
+
// snapshot through rightkit_credentials::handoff instead
|
|
39
|
+
// The shell contract (shell-contract.mjs, SHELL_CONTRACT_RULES, appended after e2e-under-cargo-test) reads an app's Tauri config, capabilities,
|
|
40
|
+
// Rust and renderer sources:
|
|
41
|
+
// shell-window-hidden SC-R1 a startup window of any platform config that is not `visible: false`
|
|
42
|
+
// shell-plugin-order SC-R2 the shell plugin installed from setup, or after a non-QA plugin
|
|
43
|
+
// shell-handler SC-R3 an invoked rk_shell_* command that no handler registers
|
|
44
|
+
// shell-csp SC-R4 a CSP without default-src 'self', frame-ancestors 'none', connect-src, or with script unsafe-inline/eval
|
|
45
|
+
// shell-grants SC-R5 a core:window:allow-* grant that the shell's own window command mirrors
|
|
46
|
+
// qa-window-label SC-R6 the shell's main_label, the QA Control's window and the created windows disagree
|
|
47
|
+
// devtools-declaration SC-R7 the five DevTools declaration sites (config, allow-list, Cargo feature, ACL manifest, runtime policy) disagree
|
|
48
|
+
// shell-lifecycle-external SC-R8 lifecycle_only() in an app that also opens an external webview (hardening and navigation policy are off)
|
|
49
|
+
// legal-receipt-native SC-R9 an Onboarding legal gate whose receipt is not the native service
|
|
50
|
+
// The consumer input contracts (consumer-input-contracts.mjs, CONSUMER_INPUT_RULES, appended after the shell contract) read workflow YAML,
|
|
51
|
+
// JSON and Markdown as data for inputs a committed tree declares but a later step cannot reach:
|
|
52
|
+
// checkout-step-input CA-07 a workflow step reads a tracked path its job's cone-mode sparse checkout does not materialize
|
|
53
|
+
// source-relation CA-09 a value in one committed file names a row another committed file does not have (SOURCE_RELATIONS
|
|
54
|
+
// plus an app's .rightkit/source-relations.json)
|
|
55
|
+
// checkout-longpaths CA-11 a GitHub-hosted Windows job checks out tracked paths over the Win32 limit without Git long paths
|
|
56
|
+
// Legion's semantic catalog and provenance contracts (legion-semantic-contracts.mjs, LEGION_SEMANTIC_RULES, after the consumer source contracts; each
|
|
57
|
+
// applies only to a tree holding its Legion producer or data file):
|
|
58
|
+
// legion-schema-projection LG-02 a schema Legion's generator byte-compares that .gitattributes leaves to eol conversion, or whose
|
|
59
|
+
// enum is not the code-owned Rust list it projects
|
|
60
|
+
// legion-legacy-tokens LG-03 a legacy token in a path or file that src/config/naming-legacy-allowlist.json does not classify
|
|
61
|
+
// with its exact occurrence count
|
|
62
|
+
// legion-retirement-ledger LG-04 a ledger-surface file deleted after c1d80c9d with no docs/provenance/retirements.md row
|
|
63
|
+
// legion-authority-body LG-05 an agent card without its doctrine (and, for a variant, roster) body verbatim
|
|
64
|
+
// Consumer semantic policy (consumer-policy.mjs, CONSUMER_POLICY_RULES, after Legion's semantic contracts) reads an app's own declared contracts from
|
|
65
|
+
// .rightkit/consumer-policy.json, as data: retired-token, route-deny-policy, layer-reach and spec-route-parity (CA-13).
|
|
66
|
+
// Sealed source closures (source-closure-rules.mjs, SOURCE_CLOSURE_RULES, after the shell contract):
|
|
67
|
+
// source-closure SF-01 a committed JSON record { sourceClosure: { sources: { path: sha256 }, digest } } whose sources are
|
|
68
|
+
// missing or changed since sealing, or whose digest does not cover them (normalised digests are
|
|
69
|
+
// declared in .rightkit/source-closures.json)
|
|
70
|
+
// The ownership class rules (ownership-class-rules.mjs, OWNERSHIP_CLASS_RULES, appended last; code is placed by class, never by
|
|
71
|
+
// consumer count):
|
|
72
|
+
// mandatory-foundation OW-05 an app (root rightkit-mandatory.json, or a release config that ships a desktop app) whose profile
|
|
73
|
+
// requires a foundation owner (ownership-mandatory-set.json) it does not use, that selects an unknown
|
|
74
|
+
// or unratified profile, that has no declaration, or that excuses a row without a ratified ruling
|
|
75
|
+
// ownership-exceptions OW-06 a .rightkit/ownership-exceptions.json entry without capability, package, reason, owner, approvedBy,
|
|
76
|
+
// date, expires and a RightKit issue, past its expires date (never baselineable), or that no
|
|
77
|
+
// ratified ruling (appAmendments.<app>.exceptions) covers
|
|
78
|
+
//
|
|
30
79
|
// Ratchet: findings that exist today may be listed so they print as KNOWN and do not fail; an unlisted finding fails; a listed
|
|
31
80
|
// identity (`${rule}|${file}|${subject}`, no line numbers) that is gone fails as STALE, so the list only shrinks. The baseline is
|
|
32
81
|
// 1. --baseline <file>, which must exist; else
|
|
@@ -46,6 +95,15 @@ import os from "node:os";
|
|
|
46
95
|
import path from "node:path";
|
|
47
96
|
import { fileURLToPath } from "node:url";
|
|
48
97
|
import { spawnSync } from "./background-process.mjs";
|
|
98
|
+
import { LEGION_SEMANTIC_RULES } from "./legion-semantic-contracts.mjs";
|
|
99
|
+
import { CONSUMER_INPUT_RULES } from "./consumer-input-contracts.mjs";
|
|
100
|
+
import { CONSUMER_CONTRACT_RULES } from "./consumer-contracts.mjs";
|
|
101
|
+
import { OWNERSHIP_CLASS_RULES } from "./ownership-class-rules.mjs";
|
|
102
|
+
import { SOURCE_CLOSURE_RULES } from "./source-closure-rules.mjs";
|
|
103
|
+
import { SHELL_CONTRACT_RULES } from "./shell-contract.mjs";
|
|
104
|
+
import { CONSUMER_SOURCE_RULES } from "./consumer-source-contracts.mjs";
|
|
105
|
+
import { CONSUMER_POLICY_RULES } from "./consumer-policy.mjs";
|
|
106
|
+
import { engineRecordStore, suiteKeyLiteral, suiteUpdaterKey } from "./suite-trust-roots.mjs";
|
|
49
107
|
|
|
50
108
|
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
51
109
|
export const readText = (root, rel) => { try { return fs.readFileSync(path.join(root, rel), "utf8"); } catch { return null; } };
|
|
@@ -379,17 +437,82 @@ function qaDevServer({ root, files }) {
|
|
|
379
437
|
return out;
|
|
380
438
|
}
|
|
381
439
|
|
|
382
|
-
// G19: ffmpeg's release gate is the point of crates/rightkit-ffmpeg; allow_in_release_builds
|
|
383
|
-
// path that ships a build the gate would have refused.
|
|
440
|
+
// G19: ffmpeg's release gate is the point of crates/rightkit-ffmpeg; allow_in_release_builds and a use of DevFfmpeg::unverified (a call,
|
|
441
|
+
// or the path itself, as in .map(DevFfmpeg::unverified)) outside it and its tests open a path that ships a build the gate would have refused. An app pinned to an older rightkit-ffmpeg still
|
|
442
|
+
// compiles both, so the names stay refused after the crate stops exporting them. UnverifiedRequest is not named here: it is the
|
|
443
|
+
// argument of resolve_ffmpeg_handle(_with), which refuses in release builds, so refusing it would refuse the sanctioned fix.
|
|
444
|
+
// DevFfmpeg::try_unverified is the mode-checked constructor and never matches (nor does any other name that merely starts with "unverified").
|
|
445
|
+
const DEV_UNVERIFIED = /\bDevFfmpeg\s*::\s*unverified\b/g;
|
|
446
|
+
// cfg(feature = "dev-overrides") and cfg(any(test, feature = "dev-overrides")): what they gate is not compiled without the feature.
|
|
447
|
+
const DEV_OVERRIDES_PREDICATE = String.raw`(?:feature\s*=\s*"dev-overrides"|any\s*\(\s*(?:test\s*,\s*feature\s*=\s*"dev-overrides"|feature\s*=\s*"dev-overrides"\s*,\s*test)\s*\))`;
|
|
448
|
+
const DEV_OVERRIDES_GATE = new RegExp(String.raw`#\s*\[\s*cfg\s*\(\s*${DEV_OVERRIDES_PREDICATE}\s*\)\s*\]`, "g");
|
|
449
|
+
// The same gate as an inner attribute that opens the file (only other inner attributes may come before it): the whole file is gated.
|
|
450
|
+
const DEV_OVERRIDES_FILE_GATE = new RegExp(String.raw`^(?:\s*#\s*!\s*\[[^\]]*\])*\s*#\s*!\s*\[\s*cfg\s*\(\s*${DEV_OVERRIDES_PREDICATE}\s*\)\s*\]`);
|
|
451
|
+
// What starts a gated item or statement (the item rule: ends at its ';' or its closing '}'); anything else is a match arm, a struct field or an expression, which a depth-0 ',' ends too.
|
|
452
|
+
const ITEM_START = /^(?:(?:pub|fn|const|static|mod|impl|struct|enum|union|trait|type|use|unsafe|async|extern|let|macro_rules)\b|\{)/;
|
|
453
|
+
// A comment or a string or char literal: its text is prose or a message, and its braces are not code.
|
|
454
|
+
const NON_CODE = /\/\/[^\n]*|\/\*[\s\S]*?\*\/|b?r(#*)"[\s\S]*?"\1|"(?:\\[\s\S]|[^"\\])*"|'(?:\\(?:u\{[0-9a-fA-F_]+\}|x[0-9a-fA-F]{2}|[^\n])|[^\\'\n])'/g;
|
|
455
|
+
/** Rust `text` with every comment and string or char literal blanked, offsets and line numbers kept. */
|
|
456
|
+
const blankNonCode = (text) => text.replace(NON_CODE, (m) => m.replace(/[^\n]/g, " "));
|
|
457
|
+
|
|
458
|
+
/** [from, to) spans of `text` that a dev-overrides gate covers (`prose` is its blanked copy, same offsets). The gated item is what
|
|
459
|
+
* follows the attribute, stacked attributes included, up to the first `;` outside brackets or the `}` that closes the first `{`:
|
|
460
|
+
* an fn, mod, impl or block, or a `let` or const that ends at its `;`. A gated match arm or struct field (it starts with no item
|
|
461
|
+
* keyword and no `{`) ends at its `,` outside brackets as well, so an arm with no block never excuses the arms after it.
|
|
462
|
+
* Limits: a gate on `mod x;` covers that line only (x.rs is checked on its own, so a call there is still refused); any other
|
|
463
|
+
* predicate (`all(...)`, `not(...)`, another feature) is no gate; an inner `#![cfg(...)]` is not a span (ffmpegReleaseEscape excuses the whole file for it);
|
|
464
|
+
* a `let` whose value goes on after a block (`if a { b } else { c }`) is covered to that block's end; a gated arm or field
|
|
465
|
+
* whose value has a `,` outside brackets (generics such as `f::<A, B>(...)`) is covered to that comma; a tail whose braces
|
|
466
|
+
* never balance is covered to the end of the file. A hit it misses is refused: gate the call itself. */
|
|
467
|
+
function devOverrideItems(text, prose) {
|
|
468
|
+
const spans = [];
|
|
469
|
+
for (const gate of text.matchAll(DEV_OVERRIDES_GATE)) {
|
|
470
|
+
if (prose[gate.index] !== "#") continue; // the attribute is itself inside a comment or a string
|
|
471
|
+
let brackets = 0;
|
|
472
|
+
let braces = 0;
|
|
473
|
+
let end = prose.length;
|
|
474
|
+
let first = gate.index + gate[0].length;
|
|
475
|
+
for (let attribute = /^\s*#\s*\[/.exec(prose.slice(first)); attribute; attribute = /^\s*#\s*\[/.exec(prose.slice(first))) {
|
|
476
|
+
let depth = 1;
|
|
477
|
+
first += attribute[0].length;
|
|
478
|
+
for (; first < prose.length && depth > 0; first++) depth += prose[first] === "[" ? 1 : prose[first] === "]" ? -1 : 0;
|
|
479
|
+
}
|
|
480
|
+
const item = ITEM_START.test(prose.slice(first).trimStart());
|
|
481
|
+
for (let i = gate.index + gate[0].length; i < prose.length; i++) {
|
|
482
|
+
const c = prose[i];
|
|
483
|
+
if (c === "(" || c === "[") brackets += 1;
|
|
484
|
+
else if (c === ")" || c === "]") brackets -= 1;
|
|
485
|
+
else if (c === "{") braces += 1;
|
|
486
|
+
else if (c === "}") braces -= 1;
|
|
487
|
+
if (braces < 0 || brackets < 0) { end = i; break; } // the block or call the attribute sits in closes first
|
|
488
|
+
if (!item && c === "," && brackets === 0 && braces === 0) { end = i + 1; break; } // a match arm or field ends at its comma
|
|
489
|
+
if (brackets === 0 && braces === 0 && (c === ";" || c === "}")) { end = i + 1; break; }
|
|
490
|
+
}
|
|
491
|
+
spans.push([gate.index, end]);
|
|
492
|
+
}
|
|
493
|
+
return spans;
|
|
494
|
+
}
|
|
495
|
+
|
|
384
496
|
function ffmpegReleaseEscape({ root, files }) {
|
|
385
497
|
const out = [];
|
|
386
498
|
for (const file of rustFiles(files)) {
|
|
387
499
|
if (/(^|\/)tests\//.test(file) || /(^|\/)(?:tests|[^/]*_tests?)\.rs$/.test(file) || /(^|\/)crates\/rightkit-ffmpeg\//.test(file)) continue;
|
|
388
500
|
const text = readText(root, file);
|
|
389
|
-
if (!text ||
|
|
501
|
+
if (!text || !/allow_in_release_builds|DevFfmpeg\s*::\s*unverified/.test(text) || /^\s*#!\[cfg\(test\)\]/m.test(text)) continue;
|
|
390
502
|
const testModule = /^#\[cfg\(test\)\]/m.exec(text)?.index ?? Infinity;
|
|
391
|
-
const
|
|
392
|
-
|
|
503
|
+
const prose = blankNonCode(text);
|
|
504
|
+
const gated = devOverrideItems(text, prose);
|
|
505
|
+
const allow = [...blankComments(text).matchAll(/allow_in_release_builds/g)].find((m) => m.index < testModule);
|
|
506
|
+
// Only DevFfmpeg::unverified is excused under a dev-overrides gate: allow_in_release_builds is being removed from the crate
|
|
507
|
+
// and from every app, gated or not.
|
|
508
|
+
// A file that opens with the inner attribute #![cfg(feature = "dev-overrides")] is not compiled without the feature at all.
|
|
509
|
+
const fileGated = DEV_OVERRIDES_FILE_GATE.test(text.replace(/\/\/[^\n]*|\/\*[\s\S]*?\*\//g, (m) => m.replace(/[^\n]/g, " ")));
|
|
510
|
+
const unverified = fileGated ? undefined : [...prose.matchAll(DEV_UNVERIFIED)].find((m) => m.index < testModule && !gated.some(([from, to]) => m.index >= from && m.index < to));
|
|
511
|
+
const uses = [];
|
|
512
|
+
const fixes = [];
|
|
513
|
+
if (allow) { uses.push(`${file}:${lineOf(text, allow.index)} uses allow_in_release_builds`); fixes.push("remove it from non-test code; only tests and crates/rightkit-ffmpeg itself may name it"); }
|
|
514
|
+
if (unverified) { uses.push(`${file}:${lineOf(text, unverified.index)} uses DevFfmpeg::unverified`); fixes.push('resolve the binary with rightkit_ffmpeg::resolve_ffmpeg_handle, which refuses in release builds; keep DevFfmpeg::unverified in tests or under #[cfg(any(test, feature = "dev-overrides"))] (or #![cfg(...)] at the top of the file)'); }
|
|
515
|
+
if (uses.length) out.push({ file, subject: file, problem: `${uses.join(" and ")} outside test code: a release build could ship what the ffmpeg gate refuses`, fix: fixes.join("; ") });
|
|
393
516
|
}
|
|
394
517
|
return out;
|
|
395
518
|
}
|
|
@@ -444,16 +567,22 @@ function speechLink({ root, files }) {
|
|
|
444
567
|
// RustRight client as `rustright cargo`, or as `rightkit` while the retired alias lasts) run the test binary or the program inside the
|
|
445
568
|
// broker job, which holds the compile slot for the whole run (Pulse, 2026-10-23: 55 minutes with 15 builds queued) and has no GUI
|
|
446
569
|
// session or unscrubbed environment a real-app test needs. The fix is `--no-run` to compile and `rightkit-qa run-tests` to run
|
|
447
|
-
// outside the broker (crates/rightkit-qa/docs/run-tests.md)
|
|
570
|
+
// outside the broker (crates/rightkit-qa/docs/run-tests.md); for `cargo run`, which has no --no-run, `cargo build --bin` in the broker and the
|
|
571
|
+
// built binary outside it (or `rightkit-qa journeys run`); for a whole-workspace run, also leaving the crate out (--exclude).
|
|
572
|
+
// Found from files alone, in three steps:
|
|
448
573
|
// 1. the end-to-end crates: a Cargo package that depends on rightkit-qa not optionally (the optional dependency is the in-app control
|
|
449
574
|
// server behind a qa-native / dev-control feature) and has a .rs file, outside examples/, that names a driver module of it;
|
|
450
575
|
// 2. the commands: `cargo test|run|nextest` in a package.json script, a shell, PowerShell, batch, Node or Python script, a Makefile
|
|
451
576
|
// or justfile recipe, a GitHub workflow `run:` step, a git hook, or a code span of a runbook document;
|
|
452
577
|
// 3. the selection: a command that is not `--no-run` or a rightkit-qa run-tests / journeys run, and names an end-to-end crate by
|
|
453
|
-
// -p / --package, --manifest-path, --workspace / --all (a member of the workspace in the working directory), --test or --bin
|
|
578
|
+
// -p / --package, --manifest-path, --workspace / --all (a member of the workspace in the working directory), --test or --bin, or by
|
|
579
|
+
// naming nothing in a virtual workspace (`cargo test`, `cargo test --manifest-path Cargo.toml`: every member, or the default-members).
|
|
454
580
|
// Known limits, deliberate for an offline check: no cargo binary (a path dependency that cargo adds to a workspace implicitly is not
|
|
455
581
|
// a member here), a command assembled from variables is matched on its literal text only, a package.json script that calls another
|
|
456
|
-
// script is judged where the cargo text is, a cargo alias (`cargo t`) is not matched, and a bare `cargo test` selects nothing
|
|
582
|
+
// script is judged where the cargo text is, a cargo alias (`cargo t`) is not matched, and a bare `cargo test` selects nothing unless the
|
|
583
|
+
// directory's manifest is a virtual workspace (a [workspace] with no [package]): there it tests every member, or the default-members.
|
|
584
|
+
// A code span of a runbook that is only the command (no argument) is a mention unless it is told to be run (INSTRUCTION_VERB), and a PowerShell
|
|
585
|
+
// `<# #>` block is a comment: the same command in a fenced block, a script, a package.json script, a recipe, a workflow or a hook is a command line.
|
|
457
586
|
const E2E_DRIVER_MODULES = new Set(["harness", "control", "suite", "scenario", "engine", "in_app", "interactive", "page", "browser", "android"]);
|
|
458
587
|
const E2E_GUARD_TARGET = "qa_surface_guard"; // the one test target of an end-to-end crate that only runs the static guard
|
|
459
588
|
const E2E_NAME_ONLY = /-e2e$|^qa-/; // a name is not evidence: crates that match only this are listed for a reviewer by e2eCrates()
|
|
@@ -467,12 +596,12 @@ const uncomment = (line) => line.replace(/^\s*#.*$/, "").replace(/\s+#.*$/, "");
|
|
|
467
596
|
|
|
468
597
|
/**
|
|
469
598
|
* What the end-to-end check reads from a Cargo manifest, literal values only: the package name, whether it holds a [workspace] and its
|
|
470
|
-
* members and
|
|
599
|
+
* members, exclude and default-members (null when it declares none), the dependencies of [dependencies], [dev-dependencies] and their [target.*] tables (inline, dotted-key and
|
|
471
600
|
* `[dependencies.name]` forms, each with optional, package and workspace), [workspace.dependencies] as key -> package, and the
|
|
472
601
|
* [[test]] and [[bin]] tables. Build dependencies are not dependencies of the tests.
|
|
473
602
|
*/
|
|
474
603
|
function parseCargoManifest(text) {
|
|
475
|
-
const manifest = { name: null, workspace: false, members: [], exclude: [], dependencies: [], workspaceDependencies: new Map(), tests: [], bins: [] };
|
|
604
|
+
const manifest = { name: null, workspace: false, members: [], exclude: [], defaultMembers: null, dependencies: [], workspaceDependencies: new Map(), tests: [], bins: [] };
|
|
476
605
|
const entries = new Map();
|
|
477
606
|
const dependency = (table, key) => {
|
|
478
607
|
const id = `${table}\0${key}`;
|
|
@@ -522,10 +651,11 @@ function parseCargoManifest(text) {
|
|
|
522
651
|
if (key === "name" && attribute === undefined) manifest.name = tomlStrings(value)[0] ?? null;
|
|
523
652
|
break;
|
|
524
653
|
case "workspace":
|
|
525
|
-
if ((key === "members" || key === "exclude") && attribute === undefined) {
|
|
654
|
+
if ((key === "members" || key === "exclude" || key === "default-members") && attribute === undefined) {
|
|
526
655
|
let list = value;
|
|
527
656
|
if (list.startsWith("[")) while (!list.includes("]") && i + 1 < lines.length) list += ` ${uncomment(lines[++i])}`;
|
|
528
|
-
manifest[
|
|
657
|
+
if (key === "default-members") manifest.defaultMembers = [...(manifest.defaultMembers ?? []), ...tomlStrings(list)];
|
|
658
|
+
else manifest[key].push(...tomlStrings(list));
|
|
529
659
|
}
|
|
530
660
|
break;
|
|
531
661
|
case "target":
|
|
@@ -642,11 +772,13 @@ export function e2eCrates(root, files) {
|
|
|
642
772
|
});
|
|
643
773
|
const nameOnly = [...manifests].filter(([dir, m]) => m.name !== null && E2E_NAME_ONLY.test(m.name) && !driving.has(dir)).map(([, m]) => m.name).sort();
|
|
644
774
|
/** The end-to-end crates that are members of the workspace rooted at `wsDir`: its root package, and every crate that matches a
|
|
645
|
-
* `members` glob, is not under `exclude` and is not a workspace root
|
|
646
|
-
|
|
775
|
+
* `members` glob (or, with `defaultOnly`, the `default-members` list when there is one), is not under `exclude` and is not a workspace root
|
|
776
|
+
* itself (a nested [workspace] makes a crate its own root). */
|
|
777
|
+
const membersOf = (wsDir, { defaultOnly = false } = {}) => {
|
|
647
778
|
const ws = manifests.get(wsDir);
|
|
648
779
|
if (!ws?.workspace) return [];
|
|
649
|
-
|
|
780
|
+
// `defaultOnly`: what a bare `cargo test` in a virtual workspace selects, its default-members when it declares them, else every member.
|
|
781
|
+
const included = (defaultOnly && ws.defaultMembers !== null ? ws.defaultMembers : ws.members).map(memberPattern);
|
|
650
782
|
const excluded = ws.exclude.map((x) => String(x).replace(/^\.\//, "").replace(/\/+$/, ""));
|
|
651
783
|
return crates.filter((c) => {
|
|
652
784
|
if (c.dir === wsDir) return true;
|
|
@@ -667,6 +799,40 @@ const HOOK_FILE = /(?:^|\/)(?:\.githooks|\.husky|githooks)\/(?!_\/)[^/]+/;
|
|
|
667
799
|
const WORKFLOW_FILE = /^\.github\/(?:workflows\/[^/]+|actions\/.+\/action)\.ya?ml$/;
|
|
668
800
|
const BUILD_FILE = /(?:^|\/)(?:Makefile|makefile|GNUmakefile|[Jj]ustfile|\.justfile)$|\.mk$/;
|
|
669
801
|
const SCRIPT_FILE = /\.(?:sh|bash|zsh|ps1|psm1|bat|cmd|mjs|cjs|js|py)$/i;
|
|
802
|
+
// A code span in running text that is only the cargo command (`cargo test`, `rustright cargo test`: no argument) names the command as often as it
|
|
803
|
+
// tells a reader to run it ("`cargo test` unit tests: ...", "`cargo test` would hold the compile slot", "only CI runs `cargo test`", a list item
|
|
804
|
+
// "`cargo test`: Rust unit tests"). It is a command line only where an imperative verb stands right before it in its clause and nothing in that
|
|
805
|
+
// clause negates it: "Run `cargo test` before you push", not "Do not run `cargo test`". With an argument, in a fenced block, a script, a package.json
|
|
806
|
+
// script, a recipe, a workflow step or a hook, the same words are a command line whatever stands around them.
|
|
807
|
+
const INSTRUCTION_VERB = /\b(?:run|use|execute|invoke|call|type)[*_]*\s*$/i;
|
|
808
|
+
const NEGATION = /\b(?:not|never|no longer|avoid|without|instead of|rather than)\b|n't\b/i;
|
|
809
|
+
const instructsToRun = (before) => {
|
|
810
|
+
const clause = String(before ?? "").split(/[.;!?]\s+|:\s+/).pop();
|
|
811
|
+
return INSTRUCTION_VERB.test(clause) && !NEGATION.test(clause);
|
|
812
|
+
};
|
|
813
|
+
|
|
814
|
+
/** The physical lines ({ no, text }) of a PowerShell script with its `<# ... #>` block comments blanked, line numbers kept. A block opens only
|
|
815
|
+
* where `<#` is the first thing on a line, so a `<#` met after code (in a string, say) never hides what follows it; code after the `#>` that
|
|
816
|
+
* closes a block on the same line is kept. */
|
|
817
|
+
function withoutPowerShellBlockComments(lines) {
|
|
818
|
+
let inBlock = false;
|
|
819
|
+
return lines.map(({ no, text }) => {
|
|
820
|
+
let rest = text;
|
|
821
|
+
for (;;) {
|
|
822
|
+
if (!inBlock) {
|
|
823
|
+
const open = /^\s*<#/.exec(rest);
|
|
824
|
+
if (!open) break;
|
|
825
|
+
inBlock = true;
|
|
826
|
+
rest = rest.slice(open[0].length);
|
|
827
|
+
}
|
|
828
|
+
const end = rest.indexOf("#>");
|
|
829
|
+
if (end < 0) { rest = ""; break; }
|
|
830
|
+
inBlock = false;
|
|
831
|
+
rest = rest.slice(end + 2);
|
|
832
|
+
}
|
|
833
|
+
return { no, text: rest };
|
|
834
|
+
});
|
|
835
|
+
}
|
|
670
836
|
|
|
671
837
|
/** A script's `cd` / `Set-Location` target applied to `cwd` (a posix path from the repository root). A target that is not a plain
|
|
672
838
|
* relative path (a variable, a substitution, an absolute path, one that leaves the repository) leaves the directory as it was: the
|
|
@@ -739,22 +905,22 @@ function workflowSteps(text) {
|
|
|
739
905
|
return steps;
|
|
740
906
|
}
|
|
741
907
|
|
|
742
|
-
/** The command-bearing units of a runbook document: each fenced code block (
|
|
743
|
-
* outside code is never a command. */
|
|
908
|
+
/** The command-bearing units of a runbook document: each fenced code block ({ lines }) and each inline code span ({ lines: one line, inline, before: the
|
|
909
|
+
* text before it on its line }). Prose outside code is never a command. */
|
|
744
910
|
function markdownUnits(text) {
|
|
745
911
|
const units = [];
|
|
746
912
|
let fence = null;
|
|
747
913
|
text.split("\n").forEach((raw, i) => {
|
|
748
914
|
if (fence) {
|
|
749
915
|
const close = /^\s*(`{3,}|~{3,})\s*$/.exec(raw);
|
|
750
|
-
if (close && close[1][0] === fence.char && close[1].length >= fence.length) { units.push(fence.lines); fence = null; } else fence.lines.push({ no: i + 1, text: raw });
|
|
916
|
+
if (close && close[1][0] === fence.char && close[1].length >= fence.length) { units.push({ lines: fence.lines }); fence = null; } else fence.lines.push({ no: i + 1, text: raw });
|
|
751
917
|
return;
|
|
752
918
|
}
|
|
753
919
|
const open = /^\s*(`{3,}|~{3,})/.exec(raw);
|
|
754
920
|
if (open) { fence = { char: open[1][0], length: open[1].length, lines: [] }; return; }
|
|
755
|
-
for (const span of raw.matchAll(/(`+)(.+?)\1(?!`)/g)) units.push([{ no: i + 1, text: span[2].trim() }]);
|
|
921
|
+
for (const span of raw.matchAll(/(`+)(.+?)\1(?!`)/g)) units.push({ lines: [{ no: i + 1, text: span[2].trim() }], inline: true, before: raw.slice(0, span.index) });
|
|
756
922
|
});
|
|
757
|
-
if (fence) units.push(fence.lines);
|
|
923
|
+
if (fence) units.push({ lines: fence.lines });
|
|
758
924
|
return units;
|
|
759
925
|
}
|
|
760
926
|
|
|
@@ -775,26 +941,50 @@ function e2eUnderCargoTest({ root, files }) {
|
|
|
775
941
|
}
|
|
776
942
|
return null;
|
|
777
943
|
};
|
|
778
|
-
/** The
|
|
779
|
-
const
|
|
944
|
+
/** The nearest directory at or above `cwd` that holds a Cargo.toml, as cargo finds the manifest with no --manifest-path; null when none. */
|
|
945
|
+
const nearestManifestDirectory = (cwd) => {
|
|
946
|
+
for (let dir = cwd; ; dir = dirOf(dir)) {
|
|
947
|
+
if (layout.manifests.has(dir)) return dir;
|
|
948
|
+
if (dir === "." || dir === "") return null;
|
|
949
|
+
}
|
|
950
|
+
};
|
|
951
|
+
/**
|
|
952
|
+
* The end-to-end crates a cargo command selects, each with the flag that did it: [{ crate, selector, wide, manifestPath }], first rule first.
|
|
953
|
+
* `wide` marks a selection of a whole workspace (--workspace / --all, or a bare test run in a virtual workspace), whose fix is to leave the crate
|
|
954
|
+
* out of the run; `manifestPath` is the --manifest-path the selection went through, if any.
|
|
955
|
+
*/
|
|
956
|
+
const selected = (tokens, verb, cwd) => {
|
|
957
|
+
const isRun = verb === "run";
|
|
780
958
|
const picks = new Map();
|
|
781
|
-
const pick = (crate, selector) => { if (!picks.has(crate)) picks.set(crate, selector); };
|
|
959
|
+
const pick = (crate, selector, how = {}) => { if (!picks.has(crate)) picks.set(crate, { selector, ...how }); };
|
|
782
960
|
const packages = flagValues(tokens, "--package", "-p");
|
|
783
961
|
const manifestPaths = flagValues(tokens, "--manifest-path");
|
|
784
962
|
const tests = flagValues(tokens, "--test");
|
|
785
963
|
const wholeWorkspace = tokens.includes("--workspace") || tokens.includes("--all");
|
|
786
964
|
const excluded = new Set(flagValues(tokens, "--exclude"));
|
|
787
965
|
const guardOnly = tests.length > 0 && tests.every((t) => t === E2E_GUARD_TARGET); // `--test qa_surface_guard` runs the static guard only
|
|
788
|
-
const members = (dir) => layout.membersOf(dir).filter((c) => !excluded.has(c.name));
|
|
966
|
+
const members = (dir, defaultOnly = false) => layout.membersOf(dir, { defaultOnly }).filter((c) => !excluded.has(c.name));
|
|
789
967
|
if (!guardOnly) {
|
|
790
968
|
for (const p of packages) for (const c of layout.crates.filter((crate) => crate.name === p)) pick(c, `-p ${p}`);
|
|
791
969
|
for (const mp of manifestPaths) {
|
|
792
970
|
const dir = manifestDirectory(mp, cwd);
|
|
793
971
|
if (dir === null) continue;
|
|
794
|
-
for (const c of layout.crates.filter((crate) => crate.dir === dir)) pick(c, `--manifest-path ${mp}
|
|
795
|
-
if (wholeWorkspace) for (const c of members(dir)) pick(c, `--manifest-path ${mp} ${tokens.includes("--workspace") ? "--workspace" : "--all"}
|
|
972
|
+
for (const c of layout.crates.filter((crate) => crate.dir === dir)) pick(c, `--manifest-path ${mp}`, { manifestPath: mp });
|
|
973
|
+
if (wholeWorkspace) for (const c of members(dir)) pick(c, `--manifest-path ${mp} ${tokens.includes("--workspace") ? "--workspace" : "--all"}`, { wide: true, manifestPath: mp });
|
|
974
|
+
}
|
|
975
|
+
if (wholeWorkspace && !manifestPaths.length) for (const c of members(cwd)) pick(c, tokens.includes("--workspace") ? "--workspace" : "--all", { wide: true });
|
|
976
|
+
// No -p, no --workspace, no named target: in a virtual workspace (a [workspace] with no [package]) cargo tests every member, or the
|
|
977
|
+
// default-members when it declares them. GenRight's `rustright cargo test --manifest-path Cargo.toml`, its root a virtual workspace that holds
|
|
978
|
+
// genright-e2e, names no crate and no flag, and runs the end-to-end crate in the broker all the same. A root with a package of its own tests
|
|
979
|
+
// that package only, so it selects nothing here (the limit below). `cargo run` needs a target, which --bin/-p already select.
|
|
980
|
+
const targeted = tests.length > 0 || flagValues(tokens, "--bin", "--example", "--bench").length > 0;
|
|
981
|
+
if (!isRun && !packages.length && !wholeWorkspace && !targeted) {
|
|
982
|
+
for (const [dir, mp] of manifestPaths.length ? manifestPaths.map((m) => [manifestDirectory(m, cwd), m]) : [[nearestManifestDirectory(cwd), null]]) {
|
|
983
|
+
const manifest = dir === null ? undefined : layout.manifests.get(dir);
|
|
984
|
+
if (!manifest?.workspace || manifest.name !== null) continue;
|
|
985
|
+
for (const c of members(dir, true)) pick(c, mp === null ? "" : `--manifest-path ${mp}`, { wide: true, ...(mp === null ? {} : { manifestPath: mp }) });
|
|
986
|
+
}
|
|
796
987
|
}
|
|
797
|
-
if (wholeWorkspace && !manifestPaths.length) for (const c of members(cwd)) pick(c, tokens.includes("--workspace") ? "--workspace" : "--all");
|
|
798
988
|
}
|
|
799
989
|
if (!packages.length && !manifestPaths.length) {
|
|
800
990
|
for (const t of tests) {
|
|
@@ -803,10 +993,26 @@ function e2eUnderCargoTest({ root, files }) {
|
|
|
803
993
|
}
|
|
804
994
|
}
|
|
805
995
|
if (isRun) for (const b of flagValues(tokens, "--bin")) for (const c of layout.crates.filter((crate) => crate.bins.has(b))) pick(c, `--bin ${b}`);
|
|
806
|
-
return [...picks].map(([crate,
|
|
996
|
+
return [...picks].map(([crate, how]) => ({ crate, ...how }));
|
|
997
|
+
};
|
|
998
|
+
/**
|
|
999
|
+
* What to do about a finding. The test binary of `cargo test` / `nextest` is compiled with --no-run and run by rightkit-qa outside the broker; a
|
|
1000
|
+
* workspace-wide run must also leave the crate out (a --no-run on the whole workspace compiles it all and tests none). `cargo run` has no --no-run:
|
|
1001
|
+
* the program is built in the broker and the built binary runs outside it.
|
|
1002
|
+
*/
|
|
1003
|
+
const fixFor = (crate, verb, tokens, { wide, manifestPath }) => {
|
|
1004
|
+
if (verb === "run") {
|
|
1005
|
+
const named = flagValues(tokens, "--bin").find((b) => crate.bins.has(b));
|
|
1006
|
+
const bin = named ?? (crate.bins.size === 1 ? [...crate.bins][0] : "<bin>");
|
|
1007
|
+
return `build it in the broker (rustright cargo build${manifestPath === undefined ? "" : ` --manifest-path ${manifestPath}`} -p ${crate.name} --bin ${bin}) and run the built binary outside the broker (or rightkit-qa journeys run)`;
|
|
1008
|
+
}
|
|
1009
|
+
const runner = `rightkit-qa run-tests -p ${crate.name} [--test <target>] -- <libtest args>`;
|
|
1010
|
+
return wide
|
|
1011
|
+
? `leave it out of the workspace run (--workspace --exclude ${crate.name}), compile it with --no-run and run it outside the broker: ${runner}`
|
|
1012
|
+
: `compile it with --no-run and run it outside the broker: ${runner}`;
|
|
807
1013
|
};
|
|
808
1014
|
/** Scan logical lines ({ no, text }) that run in order from `cwd`; `what` names the place and `subject` overrides the crate name in the identity. */
|
|
809
|
-
const scan = (file, lines, cwd, what, subject) => {
|
|
1015
|
+
const scan = (file, lines, cwd, what, subject, span = null) => {
|
|
810
1016
|
for (const { no, text } of lines) {
|
|
811
1017
|
if (!/cargo|\bcd\b|Set-Location/i.test(text)) continue; // a line with neither a command nor a directory change cannot matter
|
|
812
1018
|
const events = [...text.matchAll(CD_COMMAND)].map((m) => ({ at: m.index, cd: m[1] })).concat([...text.matchAll(CARGO_COMMAND)].map((m) => ({ at: m.index, m })));
|
|
@@ -821,15 +1027,17 @@ function e2eUnderCargoTest({ root, files }) {
|
|
|
821
1027
|
const tokens = (rest.match(/(?:[^\s"']+|"[^"]*"|'[^']*')+/g) ?? []).map((t) => t.replace(/["']/g, "").replace(/\)+$/, ""));
|
|
822
1028
|
const ownArguments = tokens.indexOf("--") < 0 ? tokens : tokens.slice(0, tokens.indexOf("--"));
|
|
823
1029
|
const segment = text.slice(Math.max(text.lastIndexOf("&", event.at), text.lastIndexOf(";", event.at), text.lastIndexOf("|", event.at), text.lastIndexOf("`", event.at)) + 1, event.at + event.m[0].length);
|
|
1030
|
+
// A code span in running text that is only the command is a mention unless it is told to be run (INSTRUCTION_VERB).
|
|
1031
|
+
if (span && tokens.length === 0 && text.trim() === event.m[0].trim() && !instructsToRun(span.before)) continue;
|
|
824
1032
|
if (ownArguments.includes("--no-run") || QA_RUNNER.test(segment)) continue;
|
|
825
1033
|
if (ownArguments.some((t) => t === "--help" || t === "-h" || t === "--version" || t === "-V")) continue;
|
|
826
1034
|
if (verb === "nextest" && NEXTEST_NO_TESTS.has(tokens[0])) continue;
|
|
827
|
-
for (const { crate, selector } of selected(ownArguments, verb
|
|
1035
|
+
for (const { crate, selector, wide, manifestPath } of selected(ownArguments, verb, cwd)) {
|
|
828
1036
|
const key = `${file}\0${no}\0${crate.name}`;
|
|
829
1037
|
if (seen.has(key)) continue;
|
|
830
1038
|
seen.add(key);
|
|
831
|
-
const via =
|
|
832
|
-
out.push({ file, subject: subject ?? crate.name, problem: `${file}:${no} ${what} runs the end-to-end crate ${crate.name} under cargo test (${via}): the test binary runs in the broker's context and holds its compile slot for the whole run`, fix:
|
|
1039
|
+
const via = [client.replace(/\s+/g, " "), verb, selector].filter(Boolean).join(" ");
|
|
1040
|
+
out.push({ file, subject: subject ?? crate.name, problem: `${file}:${no} ${what} runs the end-to-end crate ${crate.name} under cargo test (${via}): the test binary runs in the broker's context and holds its compile slot for the whole run`, fix: fixFor(crate, verb, ownArguments, { wide, manifestPath }) });
|
|
833
1041
|
}
|
|
834
1042
|
}
|
|
835
1043
|
}
|
|
@@ -852,8 +1060,8 @@ function e2eUnderCargoTest({ root, files }) {
|
|
|
852
1060
|
const continuation = /\.(?:ps1|psm1)$/i.test(file) ? "`" : /\.(?:bat|cmd)$/i.test(file) ? "^" : "\\";
|
|
853
1061
|
const physical = content.split("\n").map((line, i) => ({ no: i + 1, text: line }));
|
|
854
1062
|
if (workflow) for (const step of workflowSteps(content)) scan(file, logicalLines(step.lines, continuation), step.cwd, "workflow step");
|
|
855
|
-
else if (runbook) for (const unit of markdownUnits(content)) scan(file, logicalLines(unit, continuation), ".", "runbook command");
|
|
856
|
-
else scan(file, logicalLines(physical, continuation), ".", hook ? "git hook" : recipe ? "recipe" : "command");
|
|
1063
|
+
else if (runbook) for (const unit of markdownUnits(content)) scan(file, logicalLines(unit.lines, continuation), ".", "runbook command", undefined, unit.inline ? unit : null);
|
|
1064
|
+
else scan(file, logicalLines(/\.(?:ps1|psm1)$/i.test(file) ? withoutPowerShellBlockComments(physical) : physical, continuation), ".", hook ? "git hook" : recipe ? "recipe" : "command");
|
|
857
1065
|
}
|
|
858
1066
|
return out;
|
|
859
1067
|
}
|
|
@@ -872,6 +1080,17 @@ export const RULES = Object.freeze([
|
|
|
872
1080
|
{ id: "switches-inventory", gap: "G20", run: switchesInventory },
|
|
873
1081
|
{ id: "speech-link", gap: "G21", run: speechLink },
|
|
874
1082
|
{ id: "e2e-under-cargo-test", gap: "G22", run: e2eUnderCargoTest },
|
|
1083
|
+
{ id: "suite-key-literal", gap: "C3-04", run: ({ root, files }) => suiteKeyLiteral(root, files) },
|
|
1084
|
+
{ id: "suite-updater-key", gap: "C3-14", run: ({ root, files }) => suiteUpdaterKey(root, files) },
|
|
1085
|
+
{ id: "engine-record-store", gap: "C3-05", run: ({ root, files }) => engineRecordStore(root, files) },
|
|
1086
|
+
...SHELL_CONTRACT_RULES,
|
|
1087
|
+
...CONSUMER_CONTRACT_RULES,
|
|
1088
|
+
...CONSUMER_INPUT_RULES,
|
|
1089
|
+
...CONSUMER_SOURCE_RULES,
|
|
1090
|
+
...LEGION_SEMANTIC_RULES,
|
|
1091
|
+
...CONSUMER_POLICY_RULES,
|
|
1092
|
+
...SOURCE_CLOSURE_RULES,
|
|
1093
|
+
...OWNERSHIP_CLASS_RULES,
|
|
875
1094
|
]);
|
|
876
1095
|
|
|
877
1096
|
// ---- export: the committed files, by git alone ----------------------------------------------------------------------------------
|
|
@@ -966,14 +1185,14 @@ export function resolveCommit(repo, rev) {
|
|
|
966
1185
|
}
|
|
967
1186
|
|
|
968
1187
|
/** Run the selected rules over an exported tree. Returns { failures: [{ rule, gap, problem, fix, file?, subject?, identity? }], timings }. */
|
|
969
|
-
export function runRules({ root, commit, files, repo = process.cwd(), only = null, rules = RULES }) {
|
|
1188
|
+
export function runRules({ root, commit, files, repo = process.cwd(), only = null, rules = RULES, now = new Date() }) {
|
|
970
1189
|
const failures = [];
|
|
971
1190
|
const timings = [];
|
|
972
1191
|
for (const rule of rules) {
|
|
973
1192
|
if (only && !only.includes(rule.id)) continue;
|
|
974
1193
|
const started = Date.now();
|
|
975
1194
|
let found;
|
|
976
|
-
try { found = rule.run({ root, commit, files, repo }); }
|
|
1195
|
+
try { found = rule.run({ root, commit, files, repo, now }); }
|
|
977
1196
|
catch (error) { found = [{ problem: `the rule itself failed: ${error.message}` }]; }
|
|
978
1197
|
timings.push({ rule: rule.id, ms: Date.now() - started, failures: found.length });
|
|
979
1198
|
for (const f of found) failures.push({ rule: rule.id, gap: rule.gap, ...f, ...(f.file && f.subject !== undefined ? { identity: `${rule.id}|${f.file}|${f.subject}` } : {}) });
|
|
@@ -1115,7 +1334,7 @@ export function runAppCheck({ repo, mode = "commit", commit, only = null, baseli
|
|
|
1115
1334
|
return finish(2);
|
|
1116
1335
|
}
|
|
1117
1336
|
try {
|
|
1118
|
-
const { failures: found } = runRules({ root, commit: checked, files, repo, only, rules });
|
|
1337
|
+
const { failures: found } = runRules({ root, commit: checked, files, repo, only, rules, now });
|
|
1119
1338
|
if (printBaseline) {
|
|
1120
1339
|
lines.push(JSON.stringify([...new Set(found.map((f) => f.identity).filter(Boolean))].sort(), null, 2));
|
|
1121
1340
|
const unkeyed = found.filter((f) => !f.identity).length;
|
package/bin/darwin/rightkit-swap
CHANGED
|
Binary file
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Claude Code PreToolUse hook for the one door (door-policy.mjs): reads the hook input on stdin and prints the decision. Registered in
|
|
3
|
+
// the repository's .claude/settings.json by `rightkit-gate install-hook`. Any input it cannot read allows (a hook never breaks the tool).
|
|
4
|
+
import fs from "node:fs";
|
|
5
|
+
import { claudeDoorDecision } from "../door-policy.mjs";
|
|
6
|
+
|
|
7
|
+
let input = {};
|
|
8
|
+
try { input = JSON.parse(fs.readFileSync(0, "utf8") || "{}"); } catch { input = {}; }
|
|
9
|
+
process.stdout.write(`${JSON.stringify(claudeDoorDecision(input))}\n`);
|