canary-test-cli 6.8.1 → 7.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/engine/analysis/cli.js +261 -89
- package/dist/engine/analysis/engine.js +39 -21
- package/dist/engine/analysis/reports.js +0 -0
- package/dist/engine/cli-commands.js +251 -43
- package/dist/engine/cli-common.js +15 -24
- package/dist/engine/cli.core.js +37 -11
- package/dist/engine/cli.js +2 -2
- package/dist/engine/company-knowledge-cli.js +2 -2
- package/dist/engine/core/adoption.js +408 -0
- package/dist/engine/core/framework-probes.js +7 -7
- package/dist/engine/core/fs-glob.js +2 -2
- package/dist/engine/core/gate-result.js +17 -0
- package/dist/engine/core/migrator.js +151 -48
- package/dist/engine/core/pattern-matcher.js +23 -5
- package/dist/engine/core/persona.js +421 -0
- package/dist/engine/core/promotion-verdict.js +261 -0
- package/dist/engine/core/reporter.js +1 -9
- package/dist/engine/core/skill-examples.js +292 -0
- package/dist/engine/core/skill-surfaces.js +307 -0
- package/dist/engine/core/static-linter.js +312 -40
- package/dist/engine/core/ticket-updater.js +1 -7
- package/dist/engine/core/vacuity-scanner.js +556 -0
- package/dist/engine/core/workflow-discovery.js +2 -8
- package/dist/engine/core/workspace-detect.js +0 -0
- package/dist/engine/data/personas/registry.json +36 -0
- package/dist/engine/guardian/adjudication.js +6 -6
- package/dist/engine/guardian/agent-tier.js +3 -3
- package/dist/engine/guardian/analysis-emit.js +13 -27
- package/dist/engine/guardian/cli.js +30 -43
- package/dist/engine/guardian/coverage.js +15 -1420
- package/dist/engine/guardian/diff-coverage/formats/cobertura.js +130 -0
- package/dist/engine/guardian/diff-coverage/formats/coverage-json-lint.js +197 -0
- package/dist/engine/guardian/diff-coverage/formats/coverage-json.js +107 -0
- package/dist/engine/guardian/diff-coverage/formats/xml.js +151 -0
- package/dist/engine/guardian/diff-coverage/graph-tier.js +223 -0
- package/dist/engine/guardian/diff-coverage/heuristic-tier.js +150 -0
- package/dist/engine/guardian/diff-coverage/orchestrator.js +125 -0
- package/dist/engine/guardian/diff-coverage/paths.js +164 -0
- package/dist/engine/guardian/diff-coverage/report-tier.js +153 -0
- package/dist/engine/guardian/diff-coverage/type-only.js +150 -0
- package/dist/engine/guardian/diff-coverage/types.js +115 -0
- package/dist/engine/guardian/pr-check.js +10 -20
- package/dist/engine/guardian/pr-comment.js +4 -3
- package/dist/engine/history/cli.js +210 -6
- package/dist/engine/history/ndjson-store.js +9 -5
- package/dist/engine/history/record.js +34 -5
- package/dist/engine/history/run-recorder.js +165 -0
- package/dist/engine/history/schema.js +25 -7
- package/dist/engine/history/store.js +9 -0
- package/dist/engine/mcp-server.js +35 -13
- package/dist/engine/skills-cli.js +133 -11
- package/dist/engine/util/ensure-ascii.js +37 -0
- package/dist/engine/workflow-cli.js +6 -6
- package/dist/engine-checks.d.ts +15 -0
- package/dist/engine-checks.js +92 -1
- package/dist/gate-result.d.ts +11 -0
- package/dist/gate-result.js +18 -0
- package/dist/overlay-commands.d.ts +12 -1
- package/dist/overlay-commands.js +28 -2
- package/dist/router.js +17 -5
- package/dist/uninstall-render.d.ts +11 -0
- package/dist/uninstall-render.js +60 -0
- package/dist/uninstall-scan.d.ts +14 -0
- package/dist/uninstall-scan.js +273 -0
- package/dist/uninstall-types.d.ts +46 -0
- package/dist/uninstall-types.js +91 -0
- package/dist/uninstall.d.ts +13 -0
- package/dist/uninstall.js +181 -0
- package/package.json +1 -1
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* `tier.ts`), which imports it never (SC-11). Under the chosen **Option A**
|
|
9
9
|
* invocation mechanism it **never calls an LLM directly**: it (i) turns
|
|
10
10
|
* deterministic Tier-0 gaps into structured authoring intents, (ii) parses and
|
|
11
|
-
* validates agent results into `
|
|
11
|
+
* validates agent results into `GuardianFinding`/`GeneratedTest` records, and (iii)
|
|
12
12
|
* enforces the write-safety model (opt-in, fork, collision, loop-guard) and the
|
|
13
13
|
* block-once decision. It reaches an agent runtime only through an injected
|
|
14
14
|
* `AgentInvoker` **port**; the production default (`RecordingInvoker`) records
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
*/
|
|
27
27
|
import { existsSync } from 'node:fs';
|
|
28
28
|
import { Severity } from './impact-mapper.js';
|
|
29
|
-
import {
|
|
29
|
+
import { GuardianFinding } from './pr-check.js';
|
|
30
30
|
/** Construct a {@link ReviewRequest} (a frozen dataclass in Python). */
|
|
31
31
|
function reviewRequest(test_paths) {
|
|
32
32
|
return { test_paths };
|
|
@@ -98,7 +98,7 @@ function parseReviewFindings(transcript) {
|
|
|
98
98
|
if (severity === undefined)
|
|
99
99
|
continue;
|
|
100
100
|
const path = match.groups['path'];
|
|
101
|
-
findings.push(new
|
|
101
|
+
findings.push(new GuardianFinding({
|
|
102
102
|
path,
|
|
103
103
|
unit: path,
|
|
104
104
|
kind: 'weak-test',
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* canary's records -- they never clobber a harness record and always pass
|
|
15
15
|
* `AnalysisArchive.safePath` (no traversal).
|
|
16
16
|
* - {@link buildAnalysisRecord} -- the v1.0 envelope wrapping the verbatim
|
|
17
|
-
* `
|
|
17
|
+
* `renderFindings(fmt="json")` findings array.
|
|
18
18
|
*
|
|
19
19
|
* SC-11 boundary: this module is deterministic filesystem/JSON only. It imports
|
|
20
20
|
* no `AgentTier`/LLM-SDK module (only intra-guardian, agent-free helpers).
|
|
@@ -34,36 +34,21 @@
|
|
|
34
34
|
import { createHash, randomBytes } from 'node:crypto';
|
|
35
35
|
import { mkdirSync, renameSync, statSync, unlinkSync, writeFileSync, } from 'node:fs';
|
|
36
36
|
import { dirname, join } from 'node:path';
|
|
37
|
+
import { ensureAscii } from '../util/ensure-ascii.js';
|
|
37
38
|
import { coverageDegradedNotice, coverageStatus, } from './coverage.js';
|
|
38
|
-
import { combineNotices,
|
|
39
|
+
import { combineNotices, renderFindings } from './pr-check.js';
|
|
39
40
|
// 1.1 adds the additive `coverage` block (#554); readers of 1.0 are unaffected.
|
|
40
41
|
// 1.2 adds the additive `skipped` list (#582). Additive again, and bumped again
|
|
41
42
|
// for the reason recorded in #572: a reader that pins a version must be able to
|
|
42
43
|
// tell which fields it can rely on being present, and silence about a new field
|
|
43
44
|
// is indistinguishable from the field being absent for a real reason.
|
|
44
45
|
export const SCHEMA_VERSION = '1.2';
|
|
45
|
-
|
|
46
|
+
const ANALYSIS_SOURCE = 'canary-pr-guardian';
|
|
46
47
|
const REF_SAFE = /[^A-Za-z0-9._-]/g;
|
|
47
48
|
const REF_MAX = 100; // cap the sanitized ref so a long branch never hits ENAMETOOLONG
|
|
48
49
|
// The loud-fallback notices carry an em-dash (U+2014) as output data; written as
|
|
49
50
|
// an escape to honor the ASCII-source rule.
|
|
50
51
|
const EM_DASH = '\u{2014}';
|
|
51
|
-
/**
|
|
52
|
-
* Escape every non-ASCII (>= U+0080) code unit to a `\uXXXX` sequence, matching
|
|
53
|
-
* Python's `json.dumps(..., ensure_ascii=True)` (the library default).
|
|
54
|
-
*/
|
|
55
|
-
function ensureAscii(json) {
|
|
56
|
-
// Escape every UTF-16 code UNIT >= 0x80 to \uXXXX, matching Python
|
|
57
|
-
// json.dumps(ensure_ascii=True). Iterating by unit (not code point) means an
|
|
58
|
-
// astral char's surrogate pair emits \udXXX\udXXX, like Python; a code-point
|
|
59
|
-
// regex would stop at U+FFFF and leave astral chars raw.
|
|
60
|
-
let out = '';
|
|
61
|
-
for (let i = 0; i < json.length; i++) {
|
|
62
|
-
const c = json.charCodeAt(i);
|
|
63
|
-
out += c >= 0x80 ? '\\u' + c.toString(16).padStart(4, '0') : json[i];
|
|
64
|
-
}
|
|
65
|
-
return out;
|
|
66
|
-
}
|
|
67
52
|
/** Trim leading/trailing `-` characters (Python `str.strip("-")`). */
|
|
68
53
|
function stripDashes(value) {
|
|
69
54
|
return value.replace(/^-+/, '').replace(/-+$/, '');
|
|
@@ -76,7 +61,7 @@ function stripDashes(value) {
|
|
|
76
61
|
* `REF_MAX` chars with a short hash suffix appended to preserve uniqueness --
|
|
77
62
|
* only when truncation actually happens. Short refs are unchanged.
|
|
78
63
|
*/
|
|
79
|
-
export function analysisFilename(ref, source =
|
|
64
|
+
export function analysisFilename(ref, source = ANALYSIS_SOURCE) {
|
|
80
65
|
let safe = stripDashes(ref.replace(REF_SAFE, '-')) || 'local';
|
|
81
66
|
if (safe.length > REF_MAX) {
|
|
82
67
|
// sha1 is a filename disambiguator (not a security digest); it matches the
|
|
@@ -96,7 +81,8 @@ function isoUtcNow() {
|
|
|
96
81
|
return new Date().toISOString().replace('Z', '+00:00');
|
|
97
82
|
}
|
|
98
83
|
/**
|
|
99
|
-
* Build the v1.0 envelope. `findings` is exactly
|
|
84
|
+
* Build the v1.0 envelope. `findings` is exactly the array from
|
|
85
|
+
* `renderFindings(fmt='json')`.
|
|
100
86
|
*/
|
|
101
87
|
export function buildAnalysisRecord(findings, args) {
|
|
102
88
|
const { ref, gate, effective_tier, degraded_notice, exit_code } = args;
|
|
@@ -104,7 +90,7 @@ export function buildAnalysisRecord(findings, args) {
|
|
|
104
90
|
// input's — so "no findings" can never be read as "coverage said so".
|
|
105
91
|
const coverage = args.coverage ?? null;
|
|
106
92
|
const notice = combineNotices(degraded_notice, coverage ? coverageDegradedNotice(coverage) : null);
|
|
107
|
-
const inner = JSON.parse(
|
|
93
|
+
const inner = JSON.parse(renderFindings(findings, 'json', effective_tier, notice));
|
|
108
94
|
const active = findings.filter((f) => !f.suppressed);
|
|
109
95
|
const suppressed = findings.filter((f) => f.suppressed);
|
|
110
96
|
const byFidelity = {};
|
|
@@ -113,7 +99,7 @@ export function buildAnalysisRecord(findings, args) {
|
|
|
113
99
|
}
|
|
114
100
|
return {
|
|
115
101
|
schemaVersion: SCHEMA_VERSION,
|
|
116
|
-
source:
|
|
102
|
+
source: ANALYSIS_SOURCE,
|
|
117
103
|
ref,
|
|
118
104
|
gate,
|
|
119
105
|
exitCode: exit_code,
|
|
@@ -151,7 +137,7 @@ export const emitSeams = {
|
|
|
151
137
|
* The analyses dir itself is created on demand by {@link emitAnalysis} (mirroring
|
|
152
138
|
* harness `AnalysisArchive.save`'s recursive `mkdir`).
|
|
153
139
|
*/
|
|
154
|
-
export function
|
|
140
|
+
export function isChannelAvailable(analysesDir) {
|
|
155
141
|
try {
|
|
156
142
|
return statSync(dirname(analysesDir)).isDirectory();
|
|
157
143
|
}
|
|
@@ -168,7 +154,7 @@ export function channelAvailable(analysesDir) {
|
|
|
168
154
|
*/
|
|
169
155
|
export function emitAnalysis(findings, args) {
|
|
170
156
|
const { analysesDir } = args;
|
|
171
|
-
if (!
|
|
157
|
+
if (!isChannelAvailable(analysesDir)) {
|
|
172
158
|
return {
|
|
173
159
|
action: 'unavailable',
|
|
174
160
|
path: null,
|
|
@@ -178,8 +164,8 @@ export function emitAnalysis(findings, args) {
|
|
|
178
164
|
}
|
|
179
165
|
const target = join(analysesDir, analysisFilename(args.ref));
|
|
180
166
|
try {
|
|
181
|
-
// Build INSIDE the try: a non-I/O error (from JSON/
|
|
182
|
-
// crash pr-check instead of degrading.
|
|
167
|
+
// Build INSIDE the try: a non-I/O error (from JSON/renderFindings) would
|
|
168
|
+
// otherwise crash pr-check instead of degrading.
|
|
183
169
|
const record = emitSeams.buildAnalysisRecord(findings, args);
|
|
184
170
|
mkdirSync(analysesDir, { recursive: true });
|
|
185
171
|
// Atomic write: stage into a same-dir temp then rename (atomic on one
|
|
@@ -13,8 +13,8 @@
|
|
|
13
13
|
* `_branch_protection_client` seams were replaced.
|
|
14
14
|
* - Commands are THIN: parse -> call the already-ported guardian library ->
|
|
15
15
|
* emit. No business logic lives in a handler.
|
|
16
|
-
* - Business exit codes are carried by throwing {@link
|
|
17
|
-
* `typer.Exit(n)`); `parseAsync`
|
|
16
|
+
* - Business exit codes are carried by throwing {@link CliExitError}
|
|
17
|
+
* (Python's `typer.Exit(n)`); a test's `parseAsync` catches it to read it.
|
|
18
18
|
* `.exitOverride()` turns commander's own usage errors into throws too, so a
|
|
19
19
|
* test never terminates the process.
|
|
20
20
|
*
|
|
@@ -58,7 +58,8 @@ import { buildApiDelta, writeApiDelta } from './delta-emitter.js';
|
|
|
58
58
|
import { extractApiDiff } from './diff-extractor.js';
|
|
59
59
|
import { HardGateAbstained, HardGateBlocked, RestBranchProtectionClient, applyHardGate, renderPlaybook, } from './hard-gate.js';
|
|
60
60
|
import { mapImpact } from './impact-mapper.js';
|
|
61
|
-
import {
|
|
61
|
+
import { ensureAscii } from '../util/ensure-ascii.js';
|
|
62
|
+
import { applySuppressions, buildFindings, buildWeakTestFindings, computeExitCode, effectiveGraphDepth, filterHeuristicNoise, filterSkipped, filterTestSupportUnits, filterTestUnits, filterTypeOnlyUnits, findReexportOnly, loadGuardianConfig, renderFindings, scopeDiff, } from './pr-check.js';
|
|
62
63
|
import { RestGitHubClient, degradationAnnotation, upsertStickyComment, } from './pr-comment.js';
|
|
63
64
|
import { buildSummary } from './summary-emitter.js';
|
|
64
65
|
import { resolveTier } from './tier.js';
|
|
@@ -71,12 +72,12 @@ const CROSS = '\u{2717}';
|
|
|
71
72
|
* Business exit signal. Thrown from a handler to carry an exit code the way
|
|
72
73
|
* Python's `typer.Exit(code)` did; the runner catches it to read the code.
|
|
73
74
|
*/
|
|
74
|
-
export class
|
|
75
|
+
export class CliExitError extends Error {
|
|
75
76
|
code;
|
|
76
77
|
constructor(code) {
|
|
77
78
|
super(`exit ${code}`);
|
|
78
79
|
this.code = code;
|
|
79
|
-
this.name = '
|
|
80
|
+
this.name = 'CliExitError';
|
|
80
81
|
}
|
|
81
82
|
}
|
|
82
83
|
/**
|
|
@@ -94,10 +95,10 @@ function normalizeUsageExit(err) {
|
|
|
94
95
|
throw err;
|
|
95
96
|
}
|
|
96
97
|
/** Raised by `deps.sleep` to break the `watch` poll loop (Ctrl+C analog). */
|
|
97
|
-
export class
|
|
98
|
+
export class WatchInterruptError extends Error {
|
|
98
99
|
constructor() {
|
|
99
100
|
super('watch interrupted');
|
|
100
|
-
this.name = '
|
|
101
|
+
this.name = 'WatchInterruptError';
|
|
101
102
|
}
|
|
102
103
|
}
|
|
103
104
|
/** Process-backed defaults for production (the `guardianCommand` export). */
|
|
@@ -148,21 +149,6 @@ export function defaultDeps() {
|
|
|
148
149
|
sleep: (secs) => new Promise((resolve) => setTimeout(resolve, secs * 1000)),
|
|
149
150
|
};
|
|
150
151
|
}
|
|
151
|
-
/**
|
|
152
|
-
* Escape non-ASCII to `\uXXXX`, matching Python `json.dumps(ensure_ascii=True)`.
|
|
153
|
-
*/
|
|
154
|
-
function ensureAscii(json) {
|
|
155
|
-
// Escape every UTF-16 code UNIT >= 0x80 to \uXXXX, matching Python
|
|
156
|
-
// json.dumps(ensure_ascii=True). Iterating by unit (not code point) means an
|
|
157
|
-
// astral char's surrogate pair emits \udXXX\udXXX, like Python; a code-point
|
|
158
|
-
// regex would stop at U+FFFF and leave astral chars raw.
|
|
159
|
-
let out = '';
|
|
160
|
-
for (let i = 0; i < json.length; i++) {
|
|
161
|
-
const c = json.charCodeAt(i);
|
|
162
|
-
out += c >= 0x80 ? '\\u' + c.toString(16).padStart(4, '0') : json[i];
|
|
163
|
-
}
|
|
164
|
-
return out;
|
|
165
|
-
}
|
|
166
152
|
/** ISO-8601 UTC timestamp with a `+00:00` offset (Python `isoformat`-shaped). */
|
|
167
153
|
function isoUtcNow() {
|
|
168
154
|
return new Date().toISOString().replace('Z', '+00:00');
|
|
@@ -212,8 +198,9 @@ export function prContextFromEnv(env) {
|
|
|
212
198
|
* the contributor's branch and stays resolvable.
|
|
213
199
|
*
|
|
214
200
|
* Returns `null` rather than a partial URL whenever repo or SHA is missing, so
|
|
215
|
-
* {@link
|
|
216
|
-
* an unresolvable link still *looks* clickable, which is worse than
|
|
201
|
+
* {@link renderFindings} falls back to plain code text. That degradation is
|
|
202
|
+
* deliberate: an unresolvable link still *looks* clickable, which is worse than
|
|
203
|
+
* no link.
|
|
217
204
|
*/
|
|
218
205
|
export function blobBaseFromEnv(env) {
|
|
219
206
|
const repo = env['GITHUB_REPOSITORY'];
|
|
@@ -513,7 +500,7 @@ function warnIfEmptyCiDiff(resolved, unitCount, deps) {
|
|
|
513
500
|
function loadSpec(path, deps) {
|
|
514
501
|
if (!existsSync(path)) {
|
|
515
502
|
deps.err(`Spec file not found: ${path}`);
|
|
516
|
-
throw new
|
|
503
|
+
throw new CliExitError(2);
|
|
517
504
|
}
|
|
518
505
|
const text = readFileSync(path, 'utf-8');
|
|
519
506
|
// Python `_load_spec`: `.json` -> json.loads; otherwise yaml.safe_load (with a
|
|
@@ -627,16 +614,16 @@ function validateCoverageCmd(path, opts, deps) {
|
|
|
627
614
|
const st = statSync(path);
|
|
628
615
|
if (st.isDirectory() || st.size > MAX_COVERAGE_BYTES) {
|
|
629
616
|
deps.out(`${pc.red(pc.bold(`${CROSS} cannot read ${path}:`))} not a readable file within the size limit`);
|
|
630
|
-
throw new
|
|
617
|
+
throw new CliExitError(2);
|
|
631
618
|
}
|
|
632
619
|
text = readFileSync(path, 'utf-8');
|
|
633
620
|
}
|
|
634
621
|
catch (exc) {
|
|
635
|
-
if (exc instanceof
|
|
622
|
+
if (exc instanceof CliExitError)
|
|
636
623
|
throw exc;
|
|
637
624
|
const msg = exc instanceof Error ? exc.message : String(exc);
|
|
638
625
|
deps.out(`${pc.red(pc.bold(`${CROSS} cannot read ${path}:`))} ${msg}`);
|
|
639
|
-
throw new
|
|
626
|
+
throw new CliExitError(2);
|
|
640
627
|
}
|
|
641
628
|
let data;
|
|
642
629
|
try {
|
|
@@ -645,7 +632,7 @@ function validateCoverageCmd(path, opts, deps) {
|
|
|
645
632
|
catch (exc) {
|
|
646
633
|
const msg = exc instanceof Error ? exc.message : String(exc);
|
|
647
634
|
deps.out(`${pc.red(pc.bold(`${CROSS} ${path} is not valid JSON:`))} ${msg}`);
|
|
648
|
-
throw new
|
|
635
|
+
throw new CliExitError(2);
|
|
649
636
|
}
|
|
650
637
|
const problems = validateCoverageJson(data);
|
|
651
638
|
const errors = problems.filter((p) => p.severity === 'error');
|
|
@@ -692,7 +679,7 @@ function validateCoverageCmd(path, opts, deps) {
|
|
|
692
679
|
}
|
|
693
680
|
}
|
|
694
681
|
if (errors.length > 0 || (opts.strict && warnings.length > 0)) {
|
|
695
|
-
throw new
|
|
682
|
+
throw new CliExitError(1);
|
|
696
683
|
}
|
|
697
684
|
}
|
|
698
685
|
/**
|
|
@@ -713,7 +700,7 @@ async function hardenGateCmd(opts, deps) {
|
|
|
713
700
|
const repo = opts.repo;
|
|
714
701
|
if (!repo) {
|
|
715
702
|
deps.out(`${pc.red(pc.bold(`${CROSS} no repo`))} ${EM_DASH} pass --repo owner/repo or set GITHUB_REPOSITORY.`);
|
|
716
|
-
throw new
|
|
703
|
+
throw new CliExitError(2);
|
|
717
704
|
}
|
|
718
705
|
const playbook = renderPlaybook(repo, opts.branch, opts.check);
|
|
719
706
|
// #490: the readiness evidence the promotion is supposed to rest on.
|
|
@@ -730,7 +717,7 @@ async function hardenGateCmd(opts, deps) {
|
|
|
730
717
|
if (!opts.token) {
|
|
731
718
|
deps.out(`${pc.red(pc.bold(`${CROSS} --apply needs an admin token`))} (pass --token or set GITHUB_TOKEN).\n`);
|
|
732
719
|
deps.out(playbook);
|
|
733
|
-
throw new
|
|
720
|
+
throw new CliExitError(2);
|
|
734
721
|
}
|
|
735
722
|
const client = deps.buildBranchProtectionClient(repo, opts.token);
|
|
736
723
|
let plan;
|
|
@@ -745,12 +732,12 @@ async function hardenGateCmd(opts, deps) {
|
|
|
745
732
|
deps.out(outcome.summaryLine);
|
|
746
733
|
deps.out(`${pc.red(pc.bold(`${CROSS} ${exc.reason}`))}\n`);
|
|
747
734
|
deps.out(exc.playbook);
|
|
748
|
-
throw new
|
|
735
|
+
throw new CliExitError(outcome.exitCode); // 3, never 1
|
|
749
736
|
}
|
|
750
737
|
if (exc instanceof HardGateBlocked) {
|
|
751
738
|
deps.out(`${pc.red(pc.bold(`${CROSS} ${exc.reason}`))}\n`);
|
|
752
739
|
deps.out(exc.playbook);
|
|
753
|
-
throw new
|
|
740
|
+
throw new CliExitError(1);
|
|
754
741
|
}
|
|
755
742
|
throw exc;
|
|
756
743
|
}
|
|
@@ -785,7 +772,7 @@ async function collectAdjudicationsCmd(opts, deps) {
|
|
|
785
772
|
}
|
|
786
773
|
if (!repo || prNumber === undefined) {
|
|
787
774
|
deps.out(`${pc.red(pc.bold(`${CROSS} no PR context`))} ${EM_DASH} pass --repo and --pr, or run in Actions.`);
|
|
788
|
-
throw new
|
|
775
|
+
throw new CliExitError(2);
|
|
789
776
|
}
|
|
790
777
|
const client = deps.buildReactionsClient(repo, prNumber);
|
|
791
778
|
const res = await collectAdjudications(client, {
|
|
@@ -811,7 +798,7 @@ async function collectAdjudicationsCmd(opts, deps) {
|
|
|
811
798
|
}
|
|
812
799
|
if (res.action === 'unavailable') {
|
|
813
800
|
deps.out(pc.red(pc.bold(`${CROSS} ${res.notice ?? 'not persisted'}`)));
|
|
814
|
-
throw new
|
|
801
|
+
throw new CliExitError(1);
|
|
815
802
|
}
|
|
816
803
|
}
|
|
817
804
|
/** Aggregate the persisted adjudications into the promotion evidence (#490). */
|
|
@@ -835,7 +822,7 @@ function precisionCmd(opts, deps) {
|
|
|
835
822
|
* read-only-token degradation is surfaced LOUDLY.
|
|
836
823
|
*/
|
|
837
824
|
async function postStickyComment(findings, resolution, deps, gateMeta = null) {
|
|
838
|
-
const body =
|
|
825
|
+
const body = renderFindings(findings, 'comment', resolution.effective, resolution.degraded_notice, gateMeta, blobBaseFromEnv(deps.env));
|
|
839
826
|
const ctx = prContextFromEnv(deps.env);
|
|
840
827
|
if (ctx === null) {
|
|
841
828
|
deps.out(`guardian: no PR context in env ${EM_DASH} printing instead.`);
|
|
@@ -905,7 +892,7 @@ function abstainPrCheck(skipped, format, deps) {
|
|
|
905
892
|
// only surface a machine can read.
|
|
906
893
|
{ findings: [], tier: 0, checked: 0, abstained: true, skipped }, null, 2)));
|
|
907
894
|
}
|
|
908
|
-
throw new
|
|
895
|
+
throw new CliExitError(outcome.exitCode); // EXIT_ABSTAINED
|
|
909
896
|
}
|
|
910
897
|
/** Resolve the analyses-channel dir (test override, else repo-root default). */
|
|
911
898
|
function resolveAnalysesDir(override, deps) {
|
|
@@ -968,7 +955,7 @@ async function prCheckCmd(opts, deps) {
|
|
|
968
955
|
// entirely (no diff scoped, no comment posted, exit 0).
|
|
969
956
|
if (opts.postComment && !config.pr_enabled) {
|
|
970
957
|
deps.out(`guardian: pr.enabled is false ${EM_DASH} skipping PR surface.`);
|
|
971
|
-
throw new
|
|
958
|
+
throw new CliExitError(0);
|
|
972
959
|
}
|
|
973
960
|
const effectiveGate = opts.gate ?? config.pr_gate;
|
|
974
961
|
// #490: read reviewer 👍/👎 off the PREVIOUS run's sticky comment before this
|
|
@@ -1109,9 +1096,9 @@ async function prCheckCmd(opts, deps) {
|
|
|
1109
1096
|
}
|
|
1110
1097
|
else if (!opts.emitAnalysis && !opts.postComment) {
|
|
1111
1098
|
// Local, non-posting default: render to stdout in `--format`.
|
|
1112
|
-
deps.out(
|
|
1099
|
+
deps.out(renderFindings(findings, opts.format, resolution.effective, resolution.degraded_notice, gateMeta, blobBaseFromEnv(deps.env)));
|
|
1113
1100
|
}
|
|
1114
|
-
throw new
|
|
1101
|
+
throw new CliExitError(exitCode);
|
|
1115
1102
|
}
|
|
1116
1103
|
// --- author-plan --------------------------------------------------------------
|
|
1117
1104
|
/**
|
|
@@ -1246,7 +1233,7 @@ async function watchCmd(opts, deps) {
|
|
|
1246
1233
|
}
|
|
1247
1234
|
}
|
|
1248
1235
|
catch (exc) {
|
|
1249
|
-
if (exc instanceof
|
|
1236
|
+
if (exc instanceof WatchInterruptError) {
|
|
1250
1237
|
deps.out(`\n${pc.yellow('Watch stopped.')}`);
|
|
1251
1238
|
return;
|
|
1252
1239
|
}
|
|
@@ -1262,7 +1249,7 @@ function collect(value, previous) {
|
|
|
1262
1249
|
* Build a fresh `guardian` command wired to `depsInit` (process-backed defaults
|
|
1263
1250
|
* fill any gap). Every subcommand uses `.exitOverride()` so a usage error throws
|
|
1264
1251
|
* a `CommanderError` rather than terminating the process -- tests read the exit
|
|
1265
|
-
* code from the thrown error (or from {@link
|
|
1252
|
+
* code from the thrown error (or from {@link CliExitError} for business exits).
|
|
1266
1253
|
*/
|
|
1267
1254
|
export function createGuardianCommand(depsInit = {}) {
|
|
1268
1255
|
const deps = { ...defaultDeps(), ...depsInit };
|