canary-test-cli 7.1.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/cli-commands.js +3 -65
- package/dist/engine/core/quality-scorer.js +15 -2
- package/dist/engine/core/skill-dispatch.js +115 -0
- package/dist/engine/core/skill-examples.js +103 -3
- package/dist/engine/core/skill-registry.js +59 -4
- package/dist/engine/core/test-files.js +77 -0
- package/dist/engine/core/vacuity-scanner.js +184 -14
- package/dist/engine/guardian/analysis-emit.js +7 -2
- package/dist/engine/guardian/cli.js +121 -9
- package/dist/engine/guardian/pr-check.js +317 -18
- package/dist/engine/skills-cli.js +123 -19
- package/dist/reporters/testtracker.d.ts +1 -1
- package/dist/reporters/testtracker.js +1 -1
- package/package.json +3 -2
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// canary-cassandra -- vacuous-test detection (#755).
|
|
3
|
+
//
|
|
4
|
+
// Finds tests that PASS WITHOUT PROVING ANYTHING: an assertion that compares a
|
|
5
|
+
// value with itself (VAC-001), a test that never invokes the target it claims
|
|
6
|
+
// to cover (VAC-002), and a test whose every assertion is an absence observed
|
|
7
|
+
// on a bystander (VAC-003).
|
|
8
|
+
//
|
|
9
|
+
// <paths> files or directories to scan (default: the current directory).
|
|
10
|
+
// --json emit machine-readable findings instead of human text.
|
|
11
|
+
// --strict exit 1 when there are findings (default is advisory: exit 0).
|
|
12
|
+
//
|
|
13
|
+
// Deterministic: no LLM, no network, no test execution. The rules themselves
|
|
14
|
+
// live in the engine (`core/vacuity-scanner`), which is also what `canary
|
|
15
|
+
// vacuity-check` and the promotion gate run -- see engine.mjs for why this one
|
|
16
|
+
// skill delegates where its three siblings self-host.
|
|
17
|
+
//
|
|
18
|
+
// Invoked via `canary skills run canary-cassandra -- [paths] [--json] [--strict]`.
|
|
19
|
+
|
|
20
|
+
import fs from 'node:fs';
|
|
21
|
+
|
|
22
|
+
import {
|
|
23
|
+
createParser,
|
|
24
|
+
formatUsageError,
|
|
25
|
+
EXIT_USAGE,
|
|
26
|
+
} from '../../../lib/parse-args.mjs';
|
|
27
|
+
import { loadEngine } from './engine.mjs';
|
|
28
|
+
|
|
29
|
+
export const SCHEMA_VERSION = 1;
|
|
30
|
+
|
|
31
|
+
const PREFIX = 'canary-cassandra:';
|
|
32
|
+
|
|
33
|
+
/** Reserved CLI-wide: exit 3 means "abstained -- verified zero items". */
|
|
34
|
+
const EXIT_ABSTAINED = 3;
|
|
35
|
+
|
|
36
|
+
// U+2192 written as an escape so this source stays ASCII, matching the family.
|
|
37
|
+
const ARROW = '\u{2192}';
|
|
38
|
+
|
|
39
|
+
const USAGE =
|
|
40
|
+
'usage: canary-cassandra [-h] [--json] [--strict] [--] [path ...]\n' +
|
|
41
|
+
'\n' +
|
|
42
|
+
'Vacuous-test detection: finds tests that pass without proving anything.\n' +
|
|
43
|
+
'\n' +
|
|
44
|
+
'positional arguments:\n' +
|
|
45
|
+
' path files or directories to scan (default: the current directory)\n' +
|
|
46
|
+
'\n' +
|
|
47
|
+
'options:\n' +
|
|
48
|
+
' -h, --help show this help message and exit\n' +
|
|
49
|
+
' --json emit machine-readable findings instead of human text\n' +
|
|
50
|
+
' --strict exit 1 when there are findings (default is advisory: exit 0)\n' +
|
|
51
|
+
'\n' +
|
|
52
|
+
'rules:\n' +
|
|
53
|
+
' VAC-001 (critical) assertion compares a value with itself\n' +
|
|
54
|
+
' VAC-002 (warning) the test never invokes the target it covers\n' +
|
|
55
|
+
' VAC-003 (warning) every assertion is an absence, on a bystander\n' +
|
|
56
|
+
'\n' +
|
|
57
|
+
'The denominator is TESTS read, not files. A zero denominator exits 3 under\n' +
|
|
58
|
+
'--strict; it is never reported as a clean scan.';
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* The `--` terminator and a lone `-` come with declaring positionals, so a file
|
|
62
|
+
* literally named `--json` stays reachable. Shared with the other skill CLIs
|
|
63
|
+
* via `lib/parse-args.mjs`; `test/skill-cli-conformance.test.ts` asserts this
|
|
64
|
+
* export exists so a hand-rolled parser cannot land.
|
|
65
|
+
*/
|
|
66
|
+
export const CLI_SPEC = {
|
|
67
|
+
prog: 'canary-cassandra',
|
|
68
|
+
booleans: { '--json': 'json', '--strict': 'strict' },
|
|
69
|
+
positionals: { key: 'paths', defaults: ['.'] },
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const parseArgs = createParser(CLI_SPEC);
|
|
73
|
+
|
|
74
|
+
// Resolved once, at load, so `main` stays synchronous like every sibling's --
|
|
75
|
+
// the conformance suite calls `main(argv)` and reads a number back. A failed
|
|
76
|
+
// resolution is carried, not thrown: `--help` must answer even with no engine.
|
|
77
|
+
const ENGINE = await loadEngine();
|
|
78
|
+
|
|
79
|
+
/** Every test file the given paths contribute, de-duplicated and ordered. */
|
|
80
|
+
function collectFiles(paths, engine) {
|
|
81
|
+
const seen = new Set();
|
|
82
|
+
const files = [];
|
|
83
|
+
for (const entry of paths) {
|
|
84
|
+
const found = engine.isDir(entry)
|
|
85
|
+
? engine.collectTestFiles(entry)
|
|
86
|
+
: [entry];
|
|
87
|
+
for (const file of found) {
|
|
88
|
+
if (seen.has(file)) continue;
|
|
89
|
+
seen.add(file);
|
|
90
|
+
files.push(file);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return files;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Run the engine scanner over each file, accumulating one gate result. */
|
|
97
|
+
function scanFiles(files, engine) {
|
|
98
|
+
const findings = [];
|
|
99
|
+
const skipped = [];
|
|
100
|
+
let checked = 0;
|
|
101
|
+
for (const file of files) {
|
|
102
|
+
const r = engine.scanVacuity(file);
|
|
103
|
+
checked += r.checked;
|
|
104
|
+
findings.push(...r.findings);
|
|
105
|
+
if (r.skipped) skipped.push(...r.skipped);
|
|
106
|
+
}
|
|
107
|
+
return { checked, findings, skipped };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** The sibling finding envelope, plus the two fields only cassandra has. */
|
|
111
|
+
function toJson(f) {
|
|
112
|
+
return {
|
|
113
|
+
file: f.file,
|
|
114
|
+
line: f.line,
|
|
115
|
+
rule_id: f.rule,
|
|
116
|
+
severity: f.severity,
|
|
117
|
+
// `snippet` in the sibling envelope is "the locus, in one line". For a
|
|
118
|
+
// vacuity finding that is the test, not the source line: the defect is the
|
|
119
|
+
// test as a whole, and a single line of it reads as a lint hit.
|
|
120
|
+
snippet: f.test,
|
|
121
|
+
why: f.message,
|
|
122
|
+
suggestion: f.suggestion,
|
|
123
|
+
fidelity: f.fidelity ?? null,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function summary(result, filesScanned, outcome) {
|
|
128
|
+
const bySeverity = {};
|
|
129
|
+
for (const f of result.findings) {
|
|
130
|
+
bySeverity[f.severity] = (bySeverity[f.severity] || 0) + 1;
|
|
131
|
+
}
|
|
132
|
+
return {
|
|
133
|
+
files_scanned: filesScanned,
|
|
134
|
+
// The denominator that matters. A healthy `files_scanned` over zero tests
|
|
135
|
+
// is the subtler zero, and the one a file-counting scanner prints a clean
|
|
136
|
+
// tick on.
|
|
137
|
+
tests_checked: result.checked,
|
|
138
|
+
abstained: outcome.abstained,
|
|
139
|
+
findings: result.findings.length,
|
|
140
|
+
by_severity: bySeverity,
|
|
141
|
+
skipped: result.skipped.length,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function renderText(result, outcome) {
|
|
146
|
+
const lines = [];
|
|
147
|
+
for (const f of result.findings) {
|
|
148
|
+
const tier = f.fidelity ? ` [${f.fidelity}]` : '';
|
|
149
|
+
lines.push(
|
|
150
|
+
`[${f.severity.toUpperCase()}] ${f.file}:${f.line} (${f.rule})${tier}`,
|
|
151
|
+
);
|
|
152
|
+
lines.push(` ${f.test}: ${f.message}`);
|
|
153
|
+
lines.push(` ${ARROW} ${f.suggestion}`);
|
|
154
|
+
lines.push('');
|
|
155
|
+
}
|
|
156
|
+
lines.push(outcome.summaryLine);
|
|
157
|
+
if (outcome.abstained) {
|
|
158
|
+
lines.push(
|
|
159
|
+
'No test was read, so nothing here is proven. Point at a directory ' +
|
|
160
|
+
'holding test files, or pass one directly.',
|
|
161
|
+
);
|
|
162
|
+
} else if (!result.findings.length) {
|
|
163
|
+
lines.push(
|
|
164
|
+
'Advisory by default. Re-run with --strict to fail on findings.',
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
return lines.join('\n');
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Exit 1 with a named reason. "Cannot verify" is a finding, not a skip. */
|
|
171
|
+
function fail(message) {
|
|
172
|
+
console.error(`${PREFIX} ${message}`);
|
|
173
|
+
return 1;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** Emit the result in the caller's mode. */
|
|
177
|
+
function report(result, files, outcome, json) {
|
|
178
|
+
if (!json) {
|
|
179
|
+
console.log(renderText(result, outcome));
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
console.log(
|
|
183
|
+
JSON.stringify(
|
|
184
|
+
{
|
|
185
|
+
schema_version: SCHEMA_VERSION,
|
|
186
|
+
findings: result.findings.map(toJson),
|
|
187
|
+
skipped: result.skipped,
|
|
188
|
+
summary: summary(result, files.length, outcome),
|
|
189
|
+
},
|
|
190
|
+
null,
|
|
191
|
+
2,
|
|
192
|
+
),
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Scan the given paths.
|
|
198
|
+
*
|
|
199
|
+
* Returns the scan plus its gate outcome, or a numeric exit code when nothing
|
|
200
|
+
* could be scanned at all -- an unresolvable engine and a missing path are
|
|
201
|
+
* FAILURES, never a clean result with an empty finding list.
|
|
202
|
+
*/
|
|
203
|
+
function scan(paths) {
|
|
204
|
+
if (!ENGINE.ok) return fail(ENGINE.error);
|
|
205
|
+
for (const entry of paths) {
|
|
206
|
+
if (!fs.existsSync(entry)) return fail(`path not found: ${entry}`);
|
|
207
|
+
}
|
|
208
|
+
const files = collectFiles(paths, ENGINE);
|
|
209
|
+
const result = scanFiles(files, ENGINE);
|
|
210
|
+
if (files.length === 0) {
|
|
211
|
+
result.skipped.push({
|
|
212
|
+
name: paths.join(', '),
|
|
213
|
+
reason: `no test file matched (looked for ${ENGINE.SCANNABLE_DESC})`,
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
// The same helper the engine's own surfaces use, rather than a hand-copied
|
|
217
|
+
// abstention line: this CLI already imports the engine, so the doctrine is
|
|
218
|
+
// enforced by the code instead of by convention.
|
|
219
|
+
const outcome = ENGINE.gateOutcome(
|
|
220
|
+
{
|
|
221
|
+
checked: result.checked,
|
|
222
|
+
findings: result.findings,
|
|
223
|
+
skipped: result.skipped,
|
|
224
|
+
},
|
|
225
|
+
'advisory',
|
|
226
|
+
{ noun: 'test(s)' },
|
|
227
|
+
);
|
|
228
|
+
return { files, result, outcome };
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export function main(argv = []) {
|
|
232
|
+
const { positionals: paths, opts, help, error } = parseArgs(argv);
|
|
233
|
+
|
|
234
|
+
// Usage resolves before any filesystem or engine work, so `--help` answers
|
|
235
|
+
// even in an install where the engine never resolved.
|
|
236
|
+
if (help) {
|
|
237
|
+
console.log(USAGE);
|
|
238
|
+
return 0;
|
|
239
|
+
}
|
|
240
|
+
if (error) {
|
|
241
|
+
console.error(formatUsageError(CLI_SPEC.prog, error));
|
|
242
|
+
return EXIT_USAGE;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const scanned = scan(paths);
|
|
246
|
+
if (typeof scanned === 'number') return scanned;
|
|
247
|
+
const { files, result, outcome } = scanned;
|
|
248
|
+
report(result, files, outcome, opts.json);
|
|
249
|
+
|
|
250
|
+
// Advisory by default (D3): findings are loud, the exit is not. Under
|
|
251
|
+
// --strict the exit-code contract applies, and a collapsed denominator takes
|
|
252
|
+
// EXIT_ABSTAINED (3) -- distinct from 1, "found something real".
|
|
253
|
+
if (!opts.strict) return 0;
|
|
254
|
+
if (outcome.abstained) return EXIT_ABSTAINED;
|
|
255
|
+
return result.findings.length ? 1 : 0;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Direct execution (the skill runner execs this file via its shebang).
|
|
259
|
+
//
|
|
260
|
+
// `process.exitCode` rather than `process.exit()`: a `--json` payload over a
|
|
261
|
+
// large suite exceeds the pipe buffer, and `process.exit` tears the process
|
|
262
|
+
// down mid-write, truncating it to ~64KB. Truncated JSON that still exits 0 is
|
|
263
|
+
// a machine-readable result a consumer cannot parse but a shell reads as
|
|
264
|
+
// success -- the exact class of quiet failure the whole family guards against.
|
|
265
|
+
// Setting the code lets node drain stdout and exit with the same status.
|
|
266
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
267
|
+
process.exitCode = main(process.argv.slice(2));
|
|
268
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// Engine resolution for the cassandra skill CLI (#755).
|
|
2
|
+
//
|
|
3
|
+
// WHY THIS SKILL IMPORTS THE ENGINE AND ITS SIBLINGS DO NOT
|
|
4
|
+
//
|
|
5
|
+
// savant, blackhawk and katana each own their detection outright, so their CLIs
|
|
6
|
+
// are self-contained by construction. Cassandra's detection is NOT its own: it
|
|
7
|
+
// already lives in `ts/src/core/vacuity-scanner.ts` and is already exercised by
|
|
8
|
+
// `canary vacuity-check` and by the promotion gate. That module's docstring
|
|
9
|
+
// states the position deliberately -- #605 accepted that `static_linter` and
|
|
10
|
+
// `quality_scorer` already overlap and a third half-enforcer would be the real
|
|
11
|
+
// defect -- so shipping a second, hand-copied vacuity scanner inside this skill
|
|
12
|
+
// would trade #755's asymmetry for a worse one: two detectors that disagree.
|
|
13
|
+
//
|
|
14
|
+
// So the missing piece was never the detection. It was the `cli:` entry point,
|
|
15
|
+
// and this file is what lets one exist without forking the rules.
|
|
16
|
+
//
|
|
17
|
+
// The engine sits three-plus levels above this script in both layouts canary
|
|
18
|
+
// ships, and the lookup is a fixed, ordered list rather than a search: an
|
|
19
|
+
// ambiguous resolution would make which rules ran depend on the install.
|
|
20
|
+
//
|
|
21
|
+
// repo checkout : <root>/ts/dist/core/... (after `npm --prefix ts run build`)
|
|
22
|
+
// npm package : <pkg>/dist/engine/core/... (staged by npm/scripts/build-engine.mjs)
|
|
23
|
+
//
|
|
24
|
+
// A failure to resolve is reported as a failure. It is never absorbed into a
|
|
25
|
+
// clean scan -- "the detector could not load" and "the detector found nothing"
|
|
26
|
+
// must not print the same thing.
|
|
27
|
+
|
|
28
|
+
import fs from 'node:fs';
|
|
29
|
+
import path from 'node:path';
|
|
30
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
31
|
+
|
|
32
|
+
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
33
|
+
|
|
34
|
+
// scripts -> canary-cassandra -> claude-code -> skills -> agents -> <root>
|
|
35
|
+
const ROOT = path.resolve(HERE, '..', '..', '..', '..', '..');
|
|
36
|
+
|
|
37
|
+
/** Candidate engine directories, highest priority first. */
|
|
38
|
+
export function engineCandidates(root = ROOT, env = process.env) {
|
|
39
|
+
const candidates = [];
|
|
40
|
+
// An explicit override exists for overlay installs, where the skill is
|
|
41
|
+
// deployed away from the engine that owns its rules.
|
|
42
|
+
if (env.CANARY_ENGINE_DIR)
|
|
43
|
+
candidates.push(path.resolve(env.CANARY_ENGINE_DIR));
|
|
44
|
+
candidates.push(path.join(root, 'ts', 'dist'));
|
|
45
|
+
candidates.push(path.join(root, 'dist', 'engine'));
|
|
46
|
+
return candidates;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const MODULES = [
|
|
50
|
+
['core', 'vacuity-scanner.js'],
|
|
51
|
+
['core', 'test-files.js'],
|
|
52
|
+
['core', 'gate-result.js'],
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
/** The first candidate directory holding every module we need, or null. */
|
|
56
|
+
export function resolveEngineDir(candidates = engineCandidates()) {
|
|
57
|
+
for (const dir of candidates) {
|
|
58
|
+
if (MODULES.every((parts) => fs.existsSync(path.join(dir, ...parts)))) {
|
|
59
|
+
return dir;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Load the engine halves this CLI delegates to.
|
|
67
|
+
*
|
|
68
|
+
* @returns `{ ok: true, ... }` on success, or `{ ok: false, error }` naming
|
|
69
|
+
* every directory that was tried -- a zero-denominator report of its own.
|
|
70
|
+
*/
|
|
71
|
+
export async function loadEngine(candidates = engineCandidates()) {
|
|
72
|
+
const dir = resolveEngineDir(candidates);
|
|
73
|
+
if (dir === null) {
|
|
74
|
+
return {
|
|
75
|
+
ok: false,
|
|
76
|
+
error:
|
|
77
|
+
'the vacuity engine could not be located, so nothing was scanned. ' +
|
|
78
|
+
`Tried: ${candidates.join(', ')}. In a Canary checkout run ` +
|
|
79
|
+
'`npm --prefix ts run build`; otherwise reinstall canary-test-cli, or ' +
|
|
80
|
+
'set CANARY_ENGINE_DIR to the directory holding core/.',
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
const load = (parts) => import(pathToFileURL(path.join(dir, ...parts)).href);
|
|
84
|
+
const [vacuity, testFiles, gate] = await Promise.all(MODULES.map(load));
|
|
85
|
+
return {
|
|
86
|
+
ok: true,
|
|
87
|
+
dir,
|
|
88
|
+
scanVacuity: vacuity.scanVacuity,
|
|
89
|
+
collectTestFiles: testFiles.collectTestFiles,
|
|
90
|
+
isDir: testFiles.isDir,
|
|
91
|
+
SCANNABLE_DESC: testFiles.SCANNABLE_DESC,
|
|
92
|
+
gateOutcome: gate.gateOutcome,
|
|
93
|
+
EXIT_ABSTAINED: gate.EXIT_ABSTAINED,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: canary-ci-ready
|
|
3
|
+
description: >
|
|
4
|
+
Analyses a test suite for CI readiness: coverage depth, flakiness, assertion
|
|
5
|
+
quality, critical path coverage, and suite runtime. Accepts documented
|
|
6
|
+
failures (quarantined tests with linked open issues count as verified).
|
|
7
|
+
Investigates config/auth failures using the consuming repo's declared
|
|
8
|
+
user_catalog_skill.
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# Canary: CI Ready
|
|
12
|
+
|
|
13
|
+
Analyses a test suite across five dimensions and produces a readiness score. Use
|
|
14
|
+
this before promoting a suite to CI, or as the convergence gate in
|
|
15
|
+
`/canary-test-pipeline`.
|
|
16
|
+
|
|
17
|
+
## When to Use
|
|
18
|
+
|
|
19
|
+
- Before wiring a new test suite into CI for the first time
|
|
20
|
+
|
|
21
|
+
- When a CI run is failing and you need to understand why
|
|
22
|
+
|
|
23
|
+
- As part of `/canary-test-pipeline` (Phase 0 and convergence gate)
|
|
24
|
+
|
|
25
|
+
- When asked "is this suite ready for CI?"
|
|
26
|
+
|
|
27
|
+
## The Five Checks
|
|
28
|
+
|
|
29
|
+
Run all five checks and score each pass / warn / fail.
|
|
30
|
+
|
|
31
|
+
### 1. Coverage depth
|
|
32
|
+
|
|
33
|
+
Read `.canary/test-inventory.json` if present. If absent or older than 7 days,
|
|
34
|
+
run `canary coverage` to generate fresh data.
|
|
35
|
+
|
|
36
|
+
Default threshold: depth ≥ 2 for all endpoints in critical areas. Override with
|
|
37
|
+
`--threshold <n>`.
|
|
38
|
+
|
|
39
|
+
- **pass** — all critical-area endpoints at depth ≥ threshold
|
|
40
|
+
|
|
41
|
+
- **warn** — some endpoints at depth 1 (hit but unasserted)
|
|
42
|
+
|
|
43
|
+
- **fail** — any critical-area endpoint at depth 0
|
|
44
|
+
|
|
45
|
+
### 2. Flakiness
|
|
46
|
+
|
|
47
|
+
Read `test-results/quarantine-ledger.json` (or the path in
|
|
48
|
+
`.canary/company.json` under `quarantine_ledger_path` if set).
|
|
49
|
+
|
|
50
|
+
A quarantined test is acceptable only when it has a linked open issue (Jira or
|
|
51
|
+
GitHub). Check issue state:
|
|
52
|
+
|
|
53
|
+
- Linked issue **open** → counts as verified (documented, tracked)
|
|
54
|
+
|
|
55
|
+
- Linked issue **closed** → flag: quarantine should be resolved
|
|
56
|
+
|
|
57
|
+
- **No linked issue** → fail: unlinked quarantine blocks CI-ready
|
|
58
|
+
|
|
59
|
+
### 3. Assertion quality
|
|
60
|
+
|
|
61
|
+
Read depth scores from the inventory. In critical-area endpoints:
|
|
62
|
+
|
|
63
|
+
- **pass** — all tests at depth ≥ 2 (shaped assertions: result.ok or equivalent)
|
|
64
|
+
|
|
65
|
+
- **warn** — some tests at depth 1 (status-only assertions)
|
|
66
|
+
|
|
67
|
+
- **fail** — majority of critical-path tests at depth 1
|
|
68
|
+
|
|
69
|
+
### 4. Critical path coverage
|
|
70
|
+
|
|
71
|
+
Only run this check if `.canary/critical-areas.json` is present.
|
|
72
|
+
|
|
73
|
+
Cross-reference the top 5 risk-scored areas from `critical-areas.json` against
|
|
74
|
+
`test-inventory.json`:
|
|
75
|
+
|
|
76
|
+
- **pass** — all top-5 areas have at least one test at depth ≥ 1
|
|
77
|
+
|
|
78
|
+
- **warn** — one area uncovered
|
|
79
|
+
|
|
80
|
+
- **fail** — two or more top areas uncovered
|
|
81
|
+
|
|
82
|
+
- **skip** — `critical-areas.json` absent (note this in output, not a failure)
|
|
83
|
+
|
|
84
|
+
### 5. Suite runtime
|
|
85
|
+
|
|
86
|
+
Read `test-results/run-history.ndjson`. Use the p95 of the last 10 runs.
|
|
87
|
+
|
|
88
|
+
**With harness MCP available:** score the p95 against trend history rather than
|
|
89
|
+
an absolute clock. Call `get_perf_baselines` and compare this run's p95 to the
|
|
90
|
+
recorded baseline for the suite:
|
|
91
|
+
|
|
92
|
+
- **pass** — p95 within the baseline's tolerance, or an improvement
|
|
93
|
+
- **warn** — p95 regressed past tolerance but under 2× the baseline
|
|
94
|
+
- **fail** — p95 at or over 2× the baseline
|
|
95
|
+
- **skip** — no baseline recorded yet (this is a baseline-capture run, not a
|
|
96
|
+
failure — say so in the output)
|
|
97
|
+
|
|
98
|
+
After scoring, record the run back into the baseline with
|
|
99
|
+
`update_perf_baselines` so the trend keeps moving. Do **not** record a run that
|
|
100
|
+
failed for unrelated reasons — a broken run's runtime is not a data point.
|
|
101
|
+
|
|
102
|
+
Regression beats absolute here. A suite that has always taken 11 minutes is a
|
|
103
|
+
fact of life; a suite that went from 3 minutes to 11 this week is the actual
|
|
104
|
+
signal, and an absolute threshold cannot tell those apart — it fails the first
|
|
105
|
+
forever and stays silent on the second until it crosses the line.
|
|
106
|
+
|
|
107
|
+
**Fallback (no MCP):** judge against absolute thresholds.
|
|
108
|
+
|
|
109
|
+
- **pass** — p95 under the configured timeout (default: 5 minutes)
|
|
110
|
+
- **warn** — p95 between 5–10 minutes
|
|
111
|
+
- **fail** — p95 over 10 minutes, or no run history (cannot assess)
|
|
112
|
+
|
|
113
|
+
State which mode was used in the output — "runtime vs. baseline" or "runtime vs.
|
|
114
|
+
absolute threshold" — so a reader knows whether a pass means "no regression" or
|
|
115
|
+
merely "under the clock".
|
|
116
|
+
|
|
117
|
+
## User Catalog Investigation
|
|
118
|
+
|
|
119
|
+
When a test fails with an auth, permission, or configuration error:
|
|
120
|
+
|
|
121
|
+
1. Read `user_catalog_skill` from `.canary/company.json`
|
|
122
|
+
2. If present: invoke `canary skills run <user_catalog_skill>` with the required
|
|
123
|
+
attributes from the error context; surface any matching user as a suggestion
|
|
124
|
+
3. If absent, or no matching user found: present constructively —
|
|
125
|
+
|
|
126
|
+
> "This failure may be a test user or test data configuration issue. Check
|
|
127
|
+
> your user catalog if you have one, or set up the required test data before
|
|
128
|
+
> re-running."
|
|
129
|
+
|
|
130
|
+
Never reference a specific catalog skill by name in output.
|
|
131
|
+
|
|
132
|
+
## Output Format
|
|
133
|
+
|
|
134
|
+
```text
|
|
135
|
+
CI Readiness — <repo-name>
|
|
136
|
+
|
|
137
|
+
✓ / ⚠ / ✗ <check name> <brief finding>
|
|
138
|
+
...
|
|
139
|
+
|
|
140
|
+
Score: N/5 — CI-READY or NOT CI-READY
|
|
141
|
+
|
|
142
|
+
Runtime scored vs. baseline | vs. absolute threshold
|
|
143
|
+
|
|
144
|
+
<gap list with suggested next actions>
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Score of 5/5 = CI-READY. Any fail = NOT CI-READY. Warns do not block.
|
|
148
|
+
|
|
149
|
+
## Flags
|
|
150
|
+
|
|
151
|
+
- `--threshold <n>` — minimum depth for coverage check (default: 2)
|
|
152
|
+
|
|
153
|
+
## Related skills
|
|
154
|
+
|
|
155
|
+
- `/canary-test-pipeline` — orchestrates this skill as Phase 0 and convergence
|
|
156
|
+
gate
|
|
157
|
+
|
|
158
|
+
- `/canary-critical-areas` — produces `critical-areas.json` used by check 4
|
|
159
|
+
|
|
160
|
+
- `canary-unquarantine` (overlay) — resolves quarantined tests once bugs are
|
|
161
|
+
fixed
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
name: canary-ci-ready
|
|
2
|
+
version: '1.0.0'
|
|
3
|
+
description:
|
|
4
|
+
Analyse a test suite for CI readiness across coverage depth, flakiness,
|
|
5
|
+
assertion quality, critical-path coverage, and runtime; documented/quarantined
|
|
6
|
+
failures count as verified.
|
|
7
|
+
stability: static
|
|
8
|
+
triggers:
|
|
9
|
+
- manual
|
|
10
|
+
platforms:
|
|
11
|
+
- claude-code
|
|
12
|
+
type: rigid
|
|
13
|
+
tools: []
|
|
14
|
+
tier: 1
|