canary-test-cli 7.0.0 → 7.2.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/agents/skills/README.md +308 -0
- package/agents/skills/canary:generate.md +49 -0
- package/agents/skills/canary:init.md +37 -0
- package/agents/skills/canary:migrate.md +66 -0
- package/agents/skills/claude-code/canary-add-framework/SKILL.md +248 -0
- package/agents/skills/claude-code/canary-blackhawk/SKILL.md +170 -0
- package/agents/skills/claude-code/canary-blackhawk/scripts/cli.mjs +188 -0
- package/agents/skills/claude-code/canary-blackhawk/scripts/rules.mjs +120 -0
- package/agents/skills/claude-code/canary-blackhawk/scripts/scanner.mjs +244 -0
- package/agents/skills/claude-code/canary-blackhawk/scripts/string-literals.mjs +116 -0
- package/agents/skills/claude-code/canary-cassandra/SKILL.md +180 -0
- package/agents/skills/claude-code/canary-cassandra/scripts/cli.mjs +268 -0
- package/agents/skills/claude-code/canary-cassandra/scripts/engine.mjs +95 -0
- package/agents/skills/claude-code/canary-ci-ready/SKILL.md +161 -0
- package/agents/skills/claude-code/canary-ci-ready/skill.yaml +14 -0
- package/agents/skills/claude-code/canary-company-knowledge/SKILL.md +196 -0
- package/agents/skills/claude-code/canary-critical-areas/SKILL.md +142 -0
- package/agents/skills/claude-code/canary-critical-areas/skill.yaml +16 -0
- package/agents/skills/claude-code/canary-edge-case-discovery/SKILL.md +160 -0
- package/agents/skills/claude-code/canary-edge-case-discovery/skill.yaml +16 -0
- package/agents/skills/claude-code/canary-fail-fast/SKILL.md +75 -0
- package/agents/skills/claude-code/canary-fail-fast/scripts/cli.mjs +118 -0
- package/agents/skills/claude-code/canary-fail-fast/scripts/digest.mjs +69 -0
- package/agents/skills/claude-code/canary-fail-fast/scripts/failures.mjs +60 -0
- package/agents/skills/claude-code/canary-fail-fast/scripts/fastfail_check.mjs +43 -0
- package/agents/skills/claude-code/canary-fail-fast/scripts/parse.mjs +149 -0
- package/agents/skills/claude-code/canary-failure-impact/SKILL.md +153 -0
- package/agents/skills/claude-code/canary-failure-impact/skill.yaml +15 -0
- package/agents/skills/claude-code/canary-fleet-health/SKILL.md +196 -0
- package/agents/skills/claude-code/canary-generate-test/SKILL.md +185 -0
- package/agents/skills/claude-code/canary-instrument/SKILL.md +157 -0
- package/agents/skills/claude-code/canary-instrument/scripts/cli.mjs +178 -0
- package/agents/skills/claude-code/canary-instrument/scripts/otel_bootstrap/instrument.mjs +96 -0
- package/agents/skills/claude-code/canary-instrument/scripts/otel_bootstrap/playwright-fixture.ts +44 -0
- package/agents/skills/claude-code/canary-instrument/scripts/run_types.mjs +81 -0
- package/agents/skills/claude-code/canary-instrument/scripts/span_reader.mjs +187 -0
- package/agents/skills/claude-code/canary-katana/SKILL.md +243 -0
- package/agents/skills/claude-code/canary-katana/scripts/alarm.mjs +296 -0
- package/agents/skills/claude-code/canary-katana/scripts/cli.mjs +247 -0
- package/agents/skills/claude-code/canary-katana/scripts/diffscan.mjs +0 -0
- package/agents/skills/claude-code/canary-katana/scripts/ledger.mjs +183 -0
- package/agents/skills/claude-code/canary-pr-guardian/SKILL.md +129 -0
- package/agents/skills/claude-code/canary-pr-guardian/skill.yaml +17 -0
- package/agents/skills/claude-code/canary-promote-test/SKILL.md +228 -0
- package/agents/skills/claude-code/canary-savant/SKILL.md +233 -0
- package/agents/skills/claude-code/canary-savant/scripts/cli.mjs +274 -0
- package/agents/skills/claude-code/canary-savant/scripts/restoration.mjs +274 -0
- package/agents/skills/claude-code/canary-savant/scripts/rules.mjs +168 -0
- package/agents/skills/claude-code/canary-savant/scripts/runner.mjs +572 -0
- package/agents/skills/claude-code/canary-savant/scripts/scanner.mjs +374 -0
- package/agents/skills/claude-code/canary-savant/scripts/string-literals.mjs +116 -0
- package/agents/skills/claude-code/canary-setup-harness/SKILL.md +263 -0
- package/agents/skills/claude-code/canary-shadow/SKILL.md +131 -0
- package/agents/skills/claude-code/canary-shadow/scripts/cases.example.json +32 -0
- package/agents/skills/claude-code/canary-shadow/scripts/cli.mjs +195 -0
- package/agents/skills/claude-code/canary-ship/SKILL.md +177 -0
- package/agents/skills/claude-code/canary-ship/skill.yaml +16 -0
- package/agents/skills/claude-code/canary-strix/SKILL.md +130 -0
- package/agents/skills/claude-code/canary-strix/scripts/cli.mjs +255 -0
- package/agents/skills/claude-code/canary-strix/scripts/scanner.mjs +252 -0
- package/agents/skills/claude-code/canary-strix/scripts/terms.mjs +132 -0
- package/agents/skills/claude-code/canary-test-pipeline/SKILL.md +159 -0
- package/agents/skills/claude-code/canary-test-pipeline/skill.yaml +19 -0
- package/agents/skills/claude-code/canary-test-reporter/SKILL.md +138 -0
- package/agents/skills/claude-code/canary-test-reporter/scripts/cli.mjs +98 -0
- package/agents/skills/claude-code/canary-test-reporter/scripts/json_report.mjs +58 -0
- package/agents/skills/claude-code/canary-test-reporter/scripts/parse.mjs +216 -0
- package/agents/skills/claude-code/canary-test-reporter/scripts/render.mjs +114 -0
- package/agents/skills/lib/parse-args.mjs +214 -0
- package/dist/engine/analysis/cli.js +116 -54
- package/dist/engine/analysis/engine.js +34 -16
- package/dist/engine/analysis/reports.js +5 -4
- package/dist/engine/cli-commands.js +252 -106
- 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 +9 -17
- 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/quality-scorer.js +15 -2
- package/dist/engine/core/reporter.js +1 -9
- package/dist/engine/core/skill-dispatch.js +115 -0
- package/dist/engine/core/skill-examples.js +392 -0
- package/dist/engine/core/skill-registry.js +59 -4
- package/dist/engine/core/skill-surfaces.js +307 -0
- package/dist/engine/core/static-linter.js +310 -38
- package/dist/engine/core/test-files.js +77 -0
- package/dist/engine/core/ticket-updater.js +1 -7
- package/dist/engine/core/vacuity-scanner.js +726 -0
- package/dist/engine/core/workflow-discovery.js +2 -8
- package/dist/engine/core/workspace-detect.js +7 -6
- package/dist/engine/data/personas/registry.json +36 -0
- package/dist/engine/guardian/adjudication.js +5 -5
- package/dist/engine/guardian/analysis-emit.js +19 -28
- package/dist/engine/guardian/cli.js +150 -51
- package/dist/engine/guardian/coverage.js +1 -1
- package/dist/engine/guardian/diff-coverage/heuristic-tier.js +1 -1
- package/dist/engine/guardian/diff-coverage/orchestrator.js +2 -2
- package/dist/engine/guardian/pr-check.js +322 -33
- 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 +249 -23
- package/dist/engine/util/ensure-ascii.js +37 -0
- package/dist/engine/workflow-cli.js +6 -6
- package/dist/gate-result.d.ts +11 -0
- package/dist/gate-result.js +18 -0
- package/dist/reporters/testtracker.d.ts +1 -1
- package/dist/reporters/testtracker.js +1 -1
- package/dist/uninstall.js +12 -5
- package/package.json +3 -2
|
@@ -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
|
*
|
|
@@ -52,13 +52,14 @@ import pc from 'picocolors';
|
|
|
52
52
|
import { AuthoringContext, InSessionAgentProbe, InSessionAgentTier, decideBlock, } from './agent-tier.js';
|
|
53
53
|
import { RestReactionsClient, collectAdjudications, loadAdjudicationRecords, renderPrecision, summarizePrecision, } from './adjudication.js';
|
|
54
54
|
import { emitAnalysis } from './analysis-emit.js';
|
|
55
|
-
import { gateOutcome } from '../core/gate-result.js';
|
|
55
|
+
import { EXIT_ABSTAINED, gateOutcome, } from '../core/gate-result.js';
|
|
56
56
|
import { coverageDegradedNotice, resolveCoverage, resolveCoverageWithInput, validateCoverageJson, } from './coverage.js';
|
|
57
57
|
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 { MERGE_REF_WARNING, provenanceLine, applySuppressions, buildFindings, buildWeakTestFindings, computeExitCode, effectiveGraphDepth, filterHeuristicNoise, filterSkipped, filterTestSupportUnits, filterTestUnits, filterTypeOnlyUnits, findReexportOnly, isCoverageAbstention, 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'];
|
|
@@ -387,6 +374,59 @@ function readWorktreeDiff(deps) {
|
|
|
387
374
|
return unstaged;
|
|
388
375
|
return deps.runGit(['diff', '--staged'])?.stdout ?? '';
|
|
389
376
|
}
|
|
377
|
+
/**
|
|
378
|
+
* The PR head sha the CI event declares, if this is a `pull_request` event.
|
|
379
|
+
*
|
|
380
|
+
* Distinct from {@link eventBaseSha}: that answers "what are we diffing
|
|
381
|
+
* against", this answers "what SHOULD the diffed HEAD be". They are compared in
|
|
382
|
+
* {@link detectMergeRef} (#761).
|
|
383
|
+
*/
|
|
384
|
+
function eventHeadSha(env) {
|
|
385
|
+
const eventPath = env['GITHUB_EVENT_PATH'];
|
|
386
|
+
if (!eventPath)
|
|
387
|
+
return null;
|
|
388
|
+
let sha;
|
|
389
|
+
try {
|
|
390
|
+
const event = JSON.parse(readFileSync(eventPath, 'utf-8'));
|
|
391
|
+
sha = event?.pull_request?.head?.sha;
|
|
392
|
+
}
|
|
393
|
+
catch {
|
|
394
|
+
return null;
|
|
395
|
+
}
|
|
396
|
+
return typeof sha === 'string' && sha.trim() ? sha.trim() : null;
|
|
397
|
+
}
|
|
398
|
+
/** Resolve `HEAD` to a full sha, or null when git cannot answer. */
|
|
399
|
+
function resolveHeadSha(deps) {
|
|
400
|
+
const res = deps.runGit(['rev-parse', 'HEAD']);
|
|
401
|
+
if (res === null || res.code !== 0)
|
|
402
|
+
return null;
|
|
403
|
+
const sha = res.stdout.trim();
|
|
404
|
+
return sha || null;
|
|
405
|
+
}
|
|
406
|
+
/**
|
|
407
|
+
* True when the checked-out HEAD is a `pull_request` MERGE REF, not the PR head.
|
|
408
|
+
*
|
|
409
|
+
* This is the merge-ref diff defect (#761). `actions/checkout` on a
|
|
410
|
+
* `pull_request` event checks out `refs/pull/<n>/merge` — the base branch
|
|
411
|
+
* merged with the PR head — unless the caller passes an explicit `ref`. Any
|
|
412
|
+
* diff taken to that HEAD includes every commit merged into the base branch
|
|
413
|
+
* since the base sha, because the triple-dot merge base degenerates to the base
|
|
414
|
+
* sha itself (it is an ancestor of the merge commit). A one-file docs PR was
|
|
415
|
+
* analyzed as 43 files that way.
|
|
416
|
+
*
|
|
417
|
+
* Detection is a comparison, not a heuristic: the event payload states the PR
|
|
418
|
+
* head sha outright, so a HEAD that differs from it is diffing something else.
|
|
419
|
+
* Returns false whenever either side is unknown — an undetectable case must not
|
|
420
|
+
* masquerade as a detected-clean one.
|
|
421
|
+
*/
|
|
422
|
+
export function detectMergeRef(headSha, deps) {
|
|
423
|
+
if (deps.env['GITHUB_EVENT_NAME'] !== 'pull_request')
|
|
424
|
+
return false;
|
|
425
|
+
const declared = eventHeadSha(deps.env);
|
|
426
|
+
if (!declared || !headSha)
|
|
427
|
+
return false;
|
|
428
|
+
return declared !== headSha;
|
|
429
|
+
}
|
|
390
430
|
/** True when the process looks like a CI runner rather than a dev worktree. */
|
|
391
431
|
function isCiContext(env) {
|
|
392
432
|
return Boolean(env['GITHUB_ACTIONS'] || env['CI']);
|
|
@@ -483,6 +523,11 @@ export function readPrDiff(source, deps) {
|
|
|
483
523
|
}
|
|
484
524
|
return { text: readWorktreeDiff(deps), origin: 'worktree', base: null };
|
|
485
525
|
}
|
|
526
|
+
// Built from the shared fragment so the annotation and the rendered provenance
|
|
527
|
+
// line cannot drift into describing the same defect two different ways (#761).
|
|
528
|
+
const MERGE_REF_NOTICE = `guardian: ${MERGE_REF_WARNING} — findings may name files this PR never ` +
|
|
529
|
+
'touched. Check out with `ref: ${{ github.event.pull_request.head.sha }}`, ' +
|
|
530
|
+
'or diff to that sha instead of HEAD.';
|
|
486
531
|
const EMPTY_CI_DIFF_NOTICE = 'guardian: 0 changed paths — fell back to a working-tree `git diff`, which ' +
|
|
487
532
|
'is empty on a clean CI checkout, so NOTHING was verified. Pass ' +
|
|
488
533
|
'`--diff <base>...<head>`, or checkout with `fetch-depth: 0` so the PR base ' +
|
|
@@ -513,7 +558,7 @@ function warnIfEmptyCiDiff(resolved, unitCount, deps) {
|
|
|
513
558
|
function loadSpec(path, deps) {
|
|
514
559
|
if (!existsSync(path)) {
|
|
515
560
|
deps.err(`Spec file not found: ${path}`);
|
|
516
|
-
throw new
|
|
561
|
+
throw new CliExitError(2);
|
|
517
562
|
}
|
|
518
563
|
const text = readFileSync(path, 'utf-8');
|
|
519
564
|
// Python `_load_spec`: `.json` -> json.loads; otherwise yaml.safe_load (with a
|
|
@@ -627,16 +672,16 @@ function validateCoverageCmd(path, opts, deps) {
|
|
|
627
672
|
const st = statSync(path);
|
|
628
673
|
if (st.isDirectory() || st.size > MAX_COVERAGE_BYTES) {
|
|
629
674
|
deps.out(`${pc.red(pc.bold(`${CROSS} cannot read ${path}:`))} not a readable file within the size limit`);
|
|
630
|
-
throw new
|
|
675
|
+
throw new CliExitError(2);
|
|
631
676
|
}
|
|
632
677
|
text = readFileSync(path, 'utf-8');
|
|
633
678
|
}
|
|
634
679
|
catch (exc) {
|
|
635
|
-
if (exc instanceof
|
|
680
|
+
if (exc instanceof CliExitError)
|
|
636
681
|
throw exc;
|
|
637
682
|
const msg = exc instanceof Error ? exc.message : String(exc);
|
|
638
683
|
deps.out(`${pc.red(pc.bold(`${CROSS} cannot read ${path}:`))} ${msg}`);
|
|
639
|
-
throw new
|
|
684
|
+
throw new CliExitError(2);
|
|
640
685
|
}
|
|
641
686
|
let data;
|
|
642
687
|
try {
|
|
@@ -645,7 +690,7 @@ function validateCoverageCmd(path, opts, deps) {
|
|
|
645
690
|
catch (exc) {
|
|
646
691
|
const msg = exc instanceof Error ? exc.message : String(exc);
|
|
647
692
|
deps.out(`${pc.red(pc.bold(`${CROSS} ${path} is not valid JSON:`))} ${msg}`);
|
|
648
|
-
throw new
|
|
693
|
+
throw new CliExitError(2);
|
|
649
694
|
}
|
|
650
695
|
const problems = validateCoverageJson(data);
|
|
651
696
|
const errors = problems.filter((p) => p.severity === 'error');
|
|
@@ -692,7 +737,7 @@ function validateCoverageCmd(path, opts, deps) {
|
|
|
692
737
|
}
|
|
693
738
|
}
|
|
694
739
|
if (errors.length > 0 || (opts.strict && warnings.length > 0)) {
|
|
695
|
-
throw new
|
|
740
|
+
throw new CliExitError(1);
|
|
696
741
|
}
|
|
697
742
|
}
|
|
698
743
|
/**
|
|
@@ -713,7 +758,7 @@ async function hardenGateCmd(opts, deps) {
|
|
|
713
758
|
const repo = opts.repo;
|
|
714
759
|
if (!repo) {
|
|
715
760
|
deps.out(`${pc.red(pc.bold(`${CROSS} no repo`))} ${EM_DASH} pass --repo owner/repo or set GITHUB_REPOSITORY.`);
|
|
716
|
-
throw new
|
|
761
|
+
throw new CliExitError(2);
|
|
717
762
|
}
|
|
718
763
|
const playbook = renderPlaybook(repo, opts.branch, opts.check);
|
|
719
764
|
// #490: the readiness evidence the promotion is supposed to rest on.
|
|
@@ -730,7 +775,7 @@ async function hardenGateCmd(opts, deps) {
|
|
|
730
775
|
if (!opts.token) {
|
|
731
776
|
deps.out(`${pc.red(pc.bold(`${CROSS} --apply needs an admin token`))} (pass --token or set GITHUB_TOKEN).\n`);
|
|
732
777
|
deps.out(playbook);
|
|
733
|
-
throw new
|
|
778
|
+
throw new CliExitError(2);
|
|
734
779
|
}
|
|
735
780
|
const client = deps.buildBranchProtectionClient(repo, opts.token);
|
|
736
781
|
let plan;
|
|
@@ -745,12 +790,12 @@ async function hardenGateCmd(opts, deps) {
|
|
|
745
790
|
deps.out(outcome.summaryLine);
|
|
746
791
|
deps.out(`${pc.red(pc.bold(`${CROSS} ${exc.reason}`))}\n`);
|
|
747
792
|
deps.out(exc.playbook);
|
|
748
|
-
throw new
|
|
793
|
+
throw new CliExitError(outcome.exitCode); // 3, never 1
|
|
749
794
|
}
|
|
750
795
|
if (exc instanceof HardGateBlocked) {
|
|
751
796
|
deps.out(`${pc.red(pc.bold(`${CROSS} ${exc.reason}`))}\n`);
|
|
752
797
|
deps.out(exc.playbook);
|
|
753
|
-
throw new
|
|
798
|
+
throw new CliExitError(1);
|
|
754
799
|
}
|
|
755
800
|
throw exc;
|
|
756
801
|
}
|
|
@@ -785,7 +830,7 @@ async function collectAdjudicationsCmd(opts, deps) {
|
|
|
785
830
|
}
|
|
786
831
|
if (!repo || prNumber === undefined) {
|
|
787
832
|
deps.out(`${pc.red(pc.bold(`${CROSS} no PR context`))} ${EM_DASH} pass --repo and --pr, or run in Actions.`);
|
|
788
|
-
throw new
|
|
833
|
+
throw new CliExitError(2);
|
|
789
834
|
}
|
|
790
835
|
const client = deps.buildReactionsClient(repo, prNumber);
|
|
791
836
|
const res = await collectAdjudications(client, {
|
|
@@ -811,7 +856,7 @@ async function collectAdjudicationsCmd(opts, deps) {
|
|
|
811
856
|
}
|
|
812
857
|
if (res.action === 'unavailable') {
|
|
813
858
|
deps.out(pc.red(pc.bold(`${CROSS} ${res.notice ?? 'not persisted'}`)));
|
|
814
|
-
throw new
|
|
859
|
+
throw new CliExitError(1);
|
|
815
860
|
}
|
|
816
861
|
}
|
|
817
862
|
/** Aggregate the persisted adjudications into the promotion evidence (#490). */
|
|
@@ -835,7 +880,7 @@ function precisionCmd(opts, deps) {
|
|
|
835
880
|
* read-only-token degradation is surfaced LOUDLY.
|
|
836
881
|
*/
|
|
837
882
|
async function postStickyComment(findings, resolution, deps, gateMeta = null) {
|
|
838
|
-
const body =
|
|
883
|
+
const body = renderFindings(findings, 'comment', resolution.effective, resolution.degraded_notice, gateMeta, blobBaseFromEnv(deps.env));
|
|
839
884
|
const ctx = prContextFromEnv(deps.env);
|
|
840
885
|
if (ctx === null) {
|
|
841
886
|
deps.out(`guardian: no PR context in env ${EM_DASH} printing instead.`);
|
|
@@ -890,11 +935,18 @@ const PR_CHECK_ABSTAIN_REMEDIATION = [
|
|
|
890
935
|
'every path.',
|
|
891
936
|
];
|
|
892
937
|
/** Exit 3 with the structural abstention line + remediation (#508). */
|
|
893
|
-
function abstainPrCheck(skipped, format, deps) {
|
|
938
|
+
function abstainPrCheck(skipped, format, deps, provenance = null) {
|
|
894
939
|
const outcome = gateOutcome({ checked: 0, findings: [], skipped }, 'gate', {
|
|
895
940
|
noun: 'unit(s)',
|
|
896
941
|
});
|
|
897
942
|
deps.out(outcome.summaryLine);
|
|
943
|
+
// #761: an abstention says "I verified zero items" — the immediate next
|
|
944
|
+
// question is "over WHAT?", and the run that motivated this feature is
|
|
945
|
+
// precisely one that should have abstained. Stating the range here is what
|
|
946
|
+
// separates "correctly abstained on a docs-only PR" from "abstained because
|
|
947
|
+
// the diff was wrong", which read identically without it.
|
|
948
|
+
if (provenance)
|
|
949
|
+
deps.out(provenanceLine(provenance));
|
|
898
950
|
for (const line of PR_CHECK_ABSTAIN_REMEDIATION)
|
|
899
951
|
deps.out(line);
|
|
900
952
|
if (format === 'json') {
|
|
@@ -903,9 +955,18 @@ function abstainPrCheck(skipped, format, deps) {
|
|
|
903
955
|
// to. Without it a consumer sees `abstained: true` and cannot tell
|
|
904
956
|
// WHAT was dropped or why -- the #508 class one layer down, on the
|
|
905
957
|
// only surface a machine can read.
|
|
906
|
-
{
|
|
958
|
+
{
|
|
959
|
+
findings: [],
|
|
960
|
+
tier: 0,
|
|
961
|
+
checked: 0,
|
|
962
|
+
abstained: true,
|
|
963
|
+
skipped,
|
|
964
|
+
// #761: `null` when no diff was resolved -- never absent, so a
|
|
965
|
+
// reader can tell "not applicable" from "this producer is old".
|
|
966
|
+
provenance,
|
|
967
|
+
}, null, 2)));
|
|
907
968
|
}
|
|
908
|
-
throw new
|
|
969
|
+
throw new CliExitError(outcome.exitCode); // EXIT_ABSTAINED
|
|
909
970
|
}
|
|
910
971
|
/** Resolve the analyses-channel dir (test override, else repo-root default). */
|
|
911
972
|
function resolveAnalysesDir(override, deps) {
|
|
@@ -968,7 +1029,7 @@ async function prCheckCmd(opts, deps) {
|
|
|
968
1029
|
// entirely (no diff scoped, no comment posted, exit 0).
|
|
969
1030
|
if (opts.postComment && !config.pr_enabled) {
|
|
970
1031
|
deps.out(`guardian: pr.enabled is false ${EM_DASH} skipping PR surface.`);
|
|
971
|
-
throw new
|
|
1032
|
+
throw new CliExitError(0);
|
|
972
1033
|
}
|
|
973
1034
|
const effectiveGate = opts.gate ?? config.pr_gate;
|
|
974
1035
|
// #490: read reviewer 👍/👎 off the PREVIOUS run's sticky comment before this
|
|
@@ -983,6 +1044,27 @@ async function prCheckCmd(opts, deps) {
|
|
|
983
1044
|
const diffText = resolvedDiff.text;
|
|
984
1045
|
const units = scopeDiff(diffText);
|
|
985
1046
|
warnIfEmptyCiDiff(resolvedDiff, units.length, deps);
|
|
1047
|
+
// #761: capture what the diff was taken between, BEFORE the skip/test/
|
|
1048
|
+
// type-only filters run — `fileCount` is the size of the surface guardian was
|
|
1049
|
+
// handed, which is the number a reviewer can check against their own PR.
|
|
1050
|
+
// Populated even for an explicit `--diff` (where `base` is unknowable): the
|
|
1051
|
+
// merge-ref warning and the file count are exactly what was missing on
|
|
1052
|
+
// the consumer run that surfaced #761, which passed `--diff` from a file.
|
|
1053
|
+
const headSha = resolveHeadSha(deps);
|
|
1054
|
+
const mergeRef = detectMergeRef(headSha, deps);
|
|
1055
|
+
const provenance = {
|
|
1056
|
+
base: resolvedDiff.base,
|
|
1057
|
+
head: headSha,
|
|
1058
|
+
origin: resolvedDiff.origin,
|
|
1059
|
+
fileCount: units.length,
|
|
1060
|
+
...(mergeRef ? { mergeRef: true } : {}),
|
|
1061
|
+
};
|
|
1062
|
+
if (mergeRef) {
|
|
1063
|
+
// Loud, because it invalidates every count downstream — but non-blocking:
|
|
1064
|
+
// the caller owns the checkout, so guardian reports and carries on.
|
|
1065
|
+
deps.err(degradationAnnotation(MERGE_REF_NOTICE));
|
|
1066
|
+
appendStepSummary(deps.env, MERGE_REF_NOTICE);
|
|
1067
|
+
}
|
|
986
1068
|
// SC-2: drop docs/config-only units matching skipGlobs.
|
|
987
1069
|
const [keptSkip, skipped] = filterSkipped(units, config.skip_globs);
|
|
988
1070
|
// FIX A: drop test-path units -- a test does not itself need a test.
|
|
@@ -1013,7 +1095,7 @@ async function prCheckCmd(opts, deps) {
|
|
|
1013
1095
|
// number on this path.
|
|
1014
1096
|
const preCoverageSkips = prCheckSkipEntries(skipped, testUnits, barrelUnits, supportUnits, typeOnlyUnits);
|
|
1015
1097
|
if (kept.length === 0 && weakFindings.length === 0) {
|
|
1016
|
-
abstainPrCheck(preCoverageSkips, opts.format, deps);
|
|
1098
|
+
abstainPrCheck(preCoverageSkips, opts.format, deps, provenance);
|
|
1017
1099
|
}
|
|
1018
1100
|
const { results, coverage } = resolveCoverageWithInput(kept, {
|
|
1019
1101
|
coveragePath: opts.coverage ?? null,
|
|
@@ -1039,7 +1121,7 @@ async function prCheckCmd(opts, deps) {
|
|
|
1039
1121
|
// SKIP rather than rendering an empty "0 unaddressed" report -- an adopter
|
|
1040
1122
|
// must be able to tell "nothing was judgeable" from "everything passed".
|
|
1041
1123
|
if (scoredResults.length === 0 && findings.length === 0) {
|
|
1042
|
-
abstainPrCheck(allSkips, opts.format, deps);
|
|
1124
|
+
abstainPrCheck(allSkips, opts.format, deps, provenance);
|
|
1043
1125
|
}
|
|
1044
1126
|
// SC-5 (PR half): resolve the requested tier against actual capability. No
|
|
1045
1127
|
// agent runtime exists (default NoAgentProbe), so any `pr.tier > 0` drops to
|
|
@@ -1049,15 +1131,26 @@ async function prCheckCmd(opts, deps) {
|
|
|
1049
1131
|
deps.out(degradationAnnotation(resolution.degraded_notice));
|
|
1050
1132
|
appendStepSummary(deps.env, resolution.degraded_notice);
|
|
1051
1133
|
}
|
|
1134
|
+
// #761: the run judged units but VERIFIED no coverage, and every finding is a
|
|
1135
|
+
// naming guess. That is an abstention on the coverage denominator — a
|
|
1136
|
+
// different test from the findings-eligible one the two `abstainPrCheck`
|
|
1137
|
+
// calls above make, and the one that consumer run needed. It does not exit
|
|
1138
|
+
// through `abstainPrCheck`: those findings are worth showing, so the run keeps
|
|
1139
|
+
// every surface and changes only its headline and its exit code.
|
|
1140
|
+
const coverageAbstained = isCoverageAbstention(coverage, findings);
|
|
1052
1141
|
// Compute the gate result once, up front: the emitted record carries it and it
|
|
1053
1142
|
// is the process exit at the end (SC-4 -- emit never changes the exit logic).
|
|
1054
|
-
const exitCode =
|
|
1143
|
+
const exitCode = coverageAbstained
|
|
1144
|
+
? EXIT_ABSTAINED
|
|
1145
|
+
: computeExitCode(findings, effectiveGate);
|
|
1055
1146
|
// #554: every surface below carries the coverage-input state, so a run that
|
|
1056
1147
|
// never saw a coverage report cannot present as one that checked and passed.
|
|
1057
1148
|
const gateMeta = {
|
|
1058
1149
|
checked: scoredResults.length,
|
|
1059
|
-
abstained:
|
|
1150
|
+
abstained: coverageAbstained,
|
|
1060
1151
|
coverage,
|
|
1152
|
+
// #761: the endpoints every count above is scoped by.
|
|
1153
|
+
provenance,
|
|
1061
1154
|
// #582: `checked` is the numerator of a fraction whose denominator was
|
|
1062
1155
|
// never printed. This is the rest of it.
|
|
1063
1156
|
skipped: allSkips,
|
|
@@ -1085,9 +1178,15 @@ async function prCheckCmd(opts, deps) {
|
|
|
1085
1178
|
degraded_notice: resolution.degraded_notice,
|
|
1086
1179
|
exit_code: exitCode,
|
|
1087
1180
|
checked: scoredResults.length,
|
|
1088
|
-
|
|
1181
|
+
// #761: a coverage abstention DOES reach emit — unlike the
|
|
1182
|
+
// findings-eligible abstention, it keeps its findings, so a machine
|
|
1183
|
+
// consumer must see the flag rather than infer a result from the array.
|
|
1184
|
+
abstained: coverageAbstained,
|
|
1089
1185
|
coverage,
|
|
1090
1186
|
skipped: allSkips,
|
|
1187
|
+
// #761: the archived artifact is where an inflated diff gets diagnosed
|
|
1188
|
+
// long after the run, so it carries the endpoints too.
|
|
1189
|
+
provenance,
|
|
1091
1190
|
});
|
|
1092
1191
|
if (res.action === 'emitted') {
|
|
1093
1192
|
deps.out(`guardian: wrote analysis record ${RIGHT_ARROW} ${res.path}`);
|
|
@@ -1109,9 +1208,9 @@ async function prCheckCmd(opts, deps) {
|
|
|
1109
1208
|
}
|
|
1110
1209
|
else if (!opts.emitAnalysis && !opts.postComment) {
|
|
1111
1210
|
// Local, non-posting default: render to stdout in `--format`.
|
|
1112
|
-
deps.out(
|
|
1211
|
+
deps.out(renderFindings(findings, opts.format, resolution.effective, resolution.degraded_notice, gateMeta, blobBaseFromEnv(deps.env)));
|
|
1113
1212
|
}
|
|
1114
|
-
throw new
|
|
1213
|
+
throw new CliExitError(exitCode);
|
|
1115
1214
|
}
|
|
1116
1215
|
// --- author-plan --------------------------------------------------------------
|
|
1117
1216
|
/**
|
|
@@ -1246,7 +1345,7 @@ async function watchCmd(opts, deps) {
|
|
|
1246
1345
|
}
|
|
1247
1346
|
}
|
|
1248
1347
|
catch (exc) {
|
|
1249
|
-
if (exc instanceof
|
|
1348
|
+
if (exc instanceof WatchInterruptError) {
|
|
1250
1349
|
deps.out(`\n${pc.yellow('Watch stopped.')}`);
|
|
1251
1350
|
return;
|
|
1252
1351
|
}
|
|
@@ -1262,7 +1361,7 @@ function collect(value, previous) {
|
|
|
1262
1361
|
* Build a fresh `guardian` command wired to `depsInit` (process-backed defaults
|
|
1263
1362
|
* fill any gap). Every subcommand uses `.exitOverride()` so a usage error throws
|
|
1264
1363
|
* a `CommanderError` rather than terminating the process -- tests read the exit
|
|
1265
|
-
* code from the thrown error (or from {@link
|
|
1364
|
+
* code from the thrown error (or from {@link CliExitError} for business exits).
|
|
1266
1365
|
*/
|
|
1267
1366
|
export function createGuardianCommand(depsInit = {}) {
|
|
1268
1367
|
const deps = { ...defaultDeps(), ...depsInit };
|
|
@@ -36,7 +36,7 @@ export { coverageLimits } from './diff-coverage/formats/cobertura.js';
|
|
|
36
36
|
export { parseCoverageJson } from './diff-coverage/formats/coverage-json.js';
|
|
37
37
|
export { validateCoverageJson, } from './diff-coverage/formats/coverage-json-lint.js';
|
|
38
38
|
export { resolveFromGraph } from './diff-coverage/graph-tier.js';
|
|
39
|
-
export {
|
|
39
|
+
export { resolveFromHeuristic } from './diff-coverage/heuristic-tier.js';
|
|
40
40
|
export { coverageDegradedNotice, coverageStatus, resolveCoverage, resolveCoverageWithInput, } from './diff-coverage/orchestrator.js';
|
|
41
41
|
export { isSourcePath, isTestPath, isTestSupportPath, } from './diff-coverage/paths.js';
|
|
42
42
|
export { resolveFromReport } from './diff-coverage/report-tier.js';
|
|
@@ -109,7 +109,7 @@ function iterTestFiles(repoRoot) {
|
|
|
109
109
|
* A unit is heuristic-covered iff some test file under `repoRoot` references
|
|
110
110
|
* the unit's file stem or a top-level symbol name (word-boundary scan).
|
|
111
111
|
*/
|
|
112
|
-
export function
|
|
112
|
+
export function resolveFromHeuristic(units, repoRoot = '.') {
|
|
113
113
|
const testFiles = iterTestFiles(repoRoot);
|
|
114
114
|
const results = [];
|
|
115
115
|
for (const unit of units) {
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* clean" from "never checked".
|
|
5
5
|
*/
|
|
6
6
|
import { resolveFromGraph } from './graph-tier.js';
|
|
7
|
-
import {
|
|
7
|
+
import { resolveFromHeuristic } from './heuristic-tier.js';
|
|
8
8
|
import { matchUnitsToIndex, readReportIndex } from './report-tier.js';
|
|
9
9
|
/**
|
|
10
10
|
* SC-3 orchestrator: resolve each unit at the highest available fidelity.
|
|
@@ -116,7 +116,7 @@ export function resolveCoverageWithInput(units, options = {}) {
|
|
|
116
116
|
}
|
|
117
117
|
}
|
|
118
118
|
if (remaining.length > 0) {
|
|
119
|
-
for (const r of
|
|
119
|
+
for (const r of resolveFromHeuristic(remaining, repoRoot)) {
|
|
120
120
|
resolved.set(r.unit, r);
|
|
121
121
|
}
|
|
122
122
|
}
|