canary-test-cli 6.4.0 → 6.5.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/doctor.d.ts +51 -3
- package/dist/doctor.js +76 -9
- package/dist/engine/analysis/cli.js +69 -6
- package/dist/engine/cli-commands.js +33 -0
- package/dist/engine/core/gate-result.js +9 -2
- package/dist/engine/guardian/adjudication.js +364 -0
- package/dist/engine/guardian/analysis-emit.js +2 -0
- package/dist/engine/guardian/cli.js +282 -15
- package/dist/engine/guardian/hard-gate.js +15 -2
- package/dist/engine/guardian/pr-check.js +5 -12
- package/dist/engine/history/cli.js +67 -0
- package/dist/engine/history/ndjson-store.js +4 -0
- package/dist/engine/history/store.js +3 -0
- package/dist/gate-result.d.ts +67 -0
- package/dist/gate-result.js +73 -0
- package/dist/overlay-commands.js +17 -1
- package/package.json +2 -1
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared gate-abstention helper (issue #508, no-silent-abstention spec).
|
|
3
|
+
*
|
|
4
|
+
* Doctrine: a check that verified zero items has ABSTAINED, not passed.
|
|
5
|
+
* Every gate reports its denominator (`checked`); zero is a distinct loud
|
|
6
|
+
* outcome. "Skipped" renders in every summary line and never aggregates
|
|
7
|
+
* into "passed" (D7).
|
|
8
|
+
*
|
|
9
|
+
* `gateOutcome` is the only path to a summary line for swept commands, so
|
|
10
|
+
* the refusal to print bare success on a zero denominator is structural.
|
|
11
|
+
* Surfaces append their own remediation text (why the denominator
|
|
12
|
+
* collapsed, first fix step) after the summary line.
|
|
13
|
+
*
|
|
14
|
+
* Output glyphs are written as `\u{...}` escapes so this source stays
|
|
15
|
+
* ASCII while the emitted bytes match the rest of the CLI (warning sign
|
|
16
|
+
* U+26A0, em dash U+2014).
|
|
17
|
+
*/
|
|
18
|
+
/** A check that was not run, and why. Always visible, never "passed". */
|
|
19
|
+
export interface SkipEntry {
|
|
20
|
+
name: string;
|
|
21
|
+
reason: string;
|
|
22
|
+
}
|
|
23
|
+
/** What a gate actually verified: its denominator and what it found. */
|
|
24
|
+
export interface GateResult<F> {
|
|
25
|
+
/** How many items were actually verified. Skipped items do NOT count. */
|
|
26
|
+
checked: number;
|
|
27
|
+
findings: F[];
|
|
28
|
+
skipped?: SkipEntry[];
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Reserved CLI-wide (D4): exit 3 always means "abstained -- verified zero
|
|
32
|
+
* items", distinct from 0 (clean), 1 (findings), 2 (surface-specific).
|
|
33
|
+
*/
|
|
34
|
+
export declare const EXIT_ABSTAINED = 3;
|
|
35
|
+
/**
|
|
36
|
+
* D3: a "gate" has an exit-code contract and fails loud (exit 3) on a zero
|
|
37
|
+
* denominator; an "advisory" command warns unmissably but exits 0 -- an
|
|
38
|
+
* empty answer honestly labeled is not an error.
|
|
39
|
+
*/
|
|
40
|
+
export type GateKind = 'gate' | 'advisory';
|
|
41
|
+
export interface GateOutcome {
|
|
42
|
+
exitCode: number;
|
|
43
|
+
abstained: boolean;
|
|
44
|
+
summaryLine: string;
|
|
45
|
+
}
|
|
46
|
+
/** Copy hooks: surfaces adapt wording without re-owning the decision. */
|
|
47
|
+
export interface GateOutcomeOptions {
|
|
48
|
+
/** Unit noun for the clean-pass line. Default: `'check(s)'`. */
|
|
49
|
+
noun?: string;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* D7: skipped entries render in EVERY summary line.
|
|
53
|
+
*
|
|
54
|
+
* Exported so a surface with its own failure vocabulary (doctor says
|
|
55
|
+
* "check(s) failed", not "finding(s)") can render the identical skip suffix
|
|
56
|
+
* instead of re-deriving the format -- the decision still comes from
|
|
57
|
+
* {@link gateOutcome}, only the noun differs.
|
|
58
|
+
*/
|
|
59
|
+
export declare function skippedSuffix(skipped?: SkipEntry[]): string;
|
|
60
|
+
/**
|
|
61
|
+
* The single summary-line/exit-code path for swept commands.
|
|
62
|
+
*
|
|
63
|
+
* Non-abstained exit codes are helper defaults (findings -> 1 for gates);
|
|
64
|
+
* surfaces with richer contracts (e.g. freshness 2 = local edits) apply
|
|
65
|
+
* their own mapping AFTER checking `abstained`.
|
|
66
|
+
*/
|
|
67
|
+
export declare function gateOutcome<F>(result: GateResult<F>, kind: GateKind, opts?: GateOutcomeOptions): GateOutcome;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// GENERATED FILE — DO NOT EDIT.
|
|
3
|
+
// Verbatim copy of ts/src/core/gate-result.ts, mirrored into this CommonJS
|
|
4
|
+
// package by scripts/sync-gate-result.mjs because the staged engine bundle is
|
|
5
|
+
// ESM and unavailable at test time. Edit the engine source and re-run:
|
|
6
|
+
// node scripts/sync-gate-result.mjs
|
|
7
|
+
// `npm test` verifies this copy has not drifted (--check runs as pretest).
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.EXIT_ABSTAINED = void 0;
|
|
10
|
+
exports.skippedSuffix = skippedSuffix;
|
|
11
|
+
exports.gateOutcome = gateOutcome;
|
|
12
|
+
/**
|
|
13
|
+
* Reserved CLI-wide (D4): exit 3 always means "abstained -- verified zero
|
|
14
|
+
* items", distinct from 0 (clean), 1 (findings), 2 (surface-specific).
|
|
15
|
+
*/
|
|
16
|
+
exports.EXIT_ABSTAINED = 3;
|
|
17
|
+
const WARN = '\u{26A0}'; // warning sign
|
|
18
|
+
const EMDASH = '\u{2014}'; // em dash
|
|
19
|
+
// C0 controls (incl. \n, ESC) and DEL: a skip name must never be able to
|
|
20
|
+
// forge output lines or smuggle ANSI sequences into the summary.
|
|
21
|
+
const CONTROL_CHARS = /[\u0000-\u001F\u007F]/g;
|
|
22
|
+
/**
|
|
23
|
+
* D7: skipped entries render in EVERY summary line.
|
|
24
|
+
*
|
|
25
|
+
* Exported so a surface with its own failure vocabulary (doctor says
|
|
26
|
+
* "check(s) failed", not "finding(s)") can render the identical skip suffix
|
|
27
|
+
* instead of re-deriving the format -- the decision still comes from
|
|
28
|
+
* {@link gateOutcome}, only the noun differs.
|
|
29
|
+
*/
|
|
30
|
+
function skippedSuffix(skipped) {
|
|
31
|
+
if (!skipped || skipped.length === 0)
|
|
32
|
+
return '';
|
|
33
|
+
const names = skipped
|
|
34
|
+
.map((s) => s.name.replace(CONTROL_CHARS, ''))
|
|
35
|
+
.join(', ');
|
|
36
|
+
return ` (${skipped.length} skipped: ${names})`;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* The single summary-line/exit-code path for swept commands.
|
|
40
|
+
*
|
|
41
|
+
* Non-abstained exit codes are helper defaults (findings -> 1 for gates);
|
|
42
|
+
* surfaces with richer contracts (e.g. freshness 2 = local edits) apply
|
|
43
|
+
* their own mapping AFTER checking `abstained`.
|
|
44
|
+
*/
|
|
45
|
+
function gateOutcome(result, kind, opts = {}) {
|
|
46
|
+
const noun = opts.noun ?? 'check(s)';
|
|
47
|
+
const suffix = skippedSuffix(result.skipped);
|
|
48
|
+
// Findings outrank abstention: a finding proves something was checked,
|
|
49
|
+
// so it must never be masked by a collapsed/invalid denominator.
|
|
50
|
+
if (result.findings.length > 0) {
|
|
51
|
+
return {
|
|
52
|
+
exitCode: kind === 'gate' ? 1 : 0,
|
|
53
|
+
abstained: false,
|
|
54
|
+
summaryLine: `${result.findings.length} finding(s) across ` +
|
|
55
|
+
`${result.checked} checked${suffix}`,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
// Negated comparison so 0, negatives, and NaN all abstain: an invalid
|
|
59
|
+
// denominator must never render as success.
|
|
60
|
+
if (!(result.checked > 0)) {
|
|
61
|
+
return {
|
|
62
|
+
exitCode: kind === 'gate' ? exports.EXIT_ABSTAINED : 0,
|
|
63
|
+
abstained: true,
|
|
64
|
+
summaryLine: `${WARN} Abstained ${EMDASH} verified zero items; ` +
|
|
65
|
+
`this is not a pass.${suffix}`,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
return {
|
|
69
|
+
exitCode: 0,
|
|
70
|
+
abstained: false,
|
|
71
|
+
summaryLine: `All ${result.checked} run ${noun} passed${suffix}`,
|
|
72
|
+
};
|
|
73
|
+
}
|
package/dist/overlay-commands.js
CHANGED
|
@@ -51,6 +51,7 @@ const registry = __importStar(require("./overlays-registry.js"));
|
|
|
51
51
|
const doctor_manifest_js_1 = require("./doctor-manifest.js");
|
|
52
52
|
const overlay_conflicts_js_1 = require("./overlay-conflicts.js");
|
|
53
53
|
const overlay_lint_js_1 = require("./overlay-lint.js");
|
|
54
|
+
const gate_result_js_1 = require("./gate-result.js");
|
|
54
55
|
const defaultGit = (args, opts = {}) => {
|
|
55
56
|
const r = (0, node_child_process_1.spawnSync)('git', args, { cwd: opts.cwd, encoding: 'utf8' });
|
|
56
57
|
if (r.error) {
|
|
@@ -373,12 +374,27 @@ function lint(nameOrPath, deps = {}, opts = {}) {
|
|
|
373
374
|
}
|
|
374
375
|
const result = (0, overlay_lint_js_1.lintOverlay)(dir);
|
|
375
376
|
const errors = result.findings.filter((f) => f.level === 'error');
|
|
377
|
+
// #508: linting zero skills is an ABSENT verdict, not a clean bill of health.
|
|
378
|
+
// Advisory (D3) -- a workflows-only overlay is legitimate, so the exit stays
|
|
379
|
+
// 0 -- but the line is unmissable and `--json` says so. Findings outrank
|
|
380
|
+
// abstention inside `gateOutcome`, so a missing skills dir still exits 1.
|
|
381
|
+
const outcome = (0, gate_result_js_1.gateOutcome)({ checked: result.skillsChecked, findings: result.findings }, 'advisory');
|
|
376
382
|
if (opts.json) {
|
|
377
|
-
out.write(`${JSON.stringify(
|
|
383
|
+
out.write(`${JSON.stringify({
|
|
384
|
+
...result,
|
|
385
|
+
checked: result.skillsChecked,
|
|
386
|
+
abstained: outcome.abstained,
|
|
387
|
+
}, null, 2)}\n`);
|
|
378
388
|
return errors.length === 0 ? 0 : 1;
|
|
379
389
|
}
|
|
380
390
|
const symbol = (f) => (f.level === 'error' ? '✗' : '⚠');
|
|
381
391
|
out.write(`canary overlay lint: ${nameOrPath}\n`);
|
|
392
|
+
if (outcome.abstained) {
|
|
393
|
+
out.write(`\n${outcome.summaryLine}\n`);
|
|
394
|
+
out.write(` No skill directories under ${dir}/.canary/skills, so nothing was ` +
|
|
395
|
+
`linted. Add a skill, or lint the overlay that actually ships them.\n`);
|
|
396
|
+
return 0;
|
|
397
|
+
}
|
|
382
398
|
if (result.findings.length === 0) {
|
|
383
399
|
out.write(`\n✓ ${result.skillsChecked} skill(s) — no issues.\n`);
|
|
384
400
|
return 0;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "canary-test-cli",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.5.0",
|
|
4
4
|
"description": "Canary — AI-powered test automation agent",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
"scripts": {
|
|
31
31
|
"build": "tsc && node scripts/build-engine.mjs",
|
|
32
32
|
"prepare": "npm run build",
|
|
33
|
+
"pretest": "node scripts/sync-gate-result.mjs --check",
|
|
33
34
|
"test": "tsc && node --test \"scripts/__tests__/*.test.js\""
|
|
34
35
|
},
|
|
35
36
|
"engines": {
|