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.
Files changed (122) hide show
  1. package/agents/skills/README.md +308 -0
  2. package/agents/skills/canary:generate.md +49 -0
  3. package/agents/skills/canary:init.md +37 -0
  4. package/agents/skills/canary:migrate.md +66 -0
  5. package/agents/skills/claude-code/canary-add-framework/SKILL.md +248 -0
  6. package/agents/skills/claude-code/canary-blackhawk/SKILL.md +170 -0
  7. package/agents/skills/claude-code/canary-blackhawk/scripts/cli.mjs +188 -0
  8. package/agents/skills/claude-code/canary-blackhawk/scripts/rules.mjs +120 -0
  9. package/agents/skills/claude-code/canary-blackhawk/scripts/scanner.mjs +244 -0
  10. package/agents/skills/claude-code/canary-blackhawk/scripts/string-literals.mjs +116 -0
  11. package/agents/skills/claude-code/canary-cassandra/SKILL.md +180 -0
  12. package/agents/skills/claude-code/canary-cassandra/scripts/cli.mjs +268 -0
  13. package/agents/skills/claude-code/canary-cassandra/scripts/engine.mjs +95 -0
  14. package/agents/skills/claude-code/canary-ci-ready/SKILL.md +161 -0
  15. package/agents/skills/claude-code/canary-ci-ready/skill.yaml +14 -0
  16. package/agents/skills/claude-code/canary-company-knowledge/SKILL.md +196 -0
  17. package/agents/skills/claude-code/canary-critical-areas/SKILL.md +142 -0
  18. package/agents/skills/claude-code/canary-critical-areas/skill.yaml +16 -0
  19. package/agents/skills/claude-code/canary-edge-case-discovery/SKILL.md +160 -0
  20. package/agents/skills/claude-code/canary-edge-case-discovery/skill.yaml +16 -0
  21. package/agents/skills/claude-code/canary-fail-fast/SKILL.md +75 -0
  22. package/agents/skills/claude-code/canary-fail-fast/scripts/cli.mjs +118 -0
  23. package/agents/skills/claude-code/canary-fail-fast/scripts/digest.mjs +69 -0
  24. package/agents/skills/claude-code/canary-fail-fast/scripts/failures.mjs +60 -0
  25. package/agents/skills/claude-code/canary-fail-fast/scripts/fastfail_check.mjs +43 -0
  26. package/agents/skills/claude-code/canary-fail-fast/scripts/parse.mjs +149 -0
  27. package/agents/skills/claude-code/canary-failure-impact/SKILL.md +153 -0
  28. package/agents/skills/claude-code/canary-failure-impact/skill.yaml +15 -0
  29. package/agents/skills/claude-code/canary-fleet-health/SKILL.md +196 -0
  30. package/agents/skills/claude-code/canary-generate-test/SKILL.md +185 -0
  31. package/agents/skills/claude-code/canary-instrument/SKILL.md +157 -0
  32. package/agents/skills/claude-code/canary-instrument/scripts/cli.mjs +178 -0
  33. package/agents/skills/claude-code/canary-instrument/scripts/otel_bootstrap/instrument.mjs +96 -0
  34. package/agents/skills/claude-code/canary-instrument/scripts/otel_bootstrap/playwright-fixture.ts +44 -0
  35. package/agents/skills/claude-code/canary-instrument/scripts/run_types.mjs +81 -0
  36. package/agents/skills/claude-code/canary-instrument/scripts/span_reader.mjs +187 -0
  37. package/agents/skills/claude-code/canary-katana/SKILL.md +243 -0
  38. package/agents/skills/claude-code/canary-katana/scripts/alarm.mjs +296 -0
  39. package/agents/skills/claude-code/canary-katana/scripts/cli.mjs +247 -0
  40. package/agents/skills/claude-code/canary-katana/scripts/diffscan.mjs +0 -0
  41. package/agents/skills/claude-code/canary-katana/scripts/ledger.mjs +183 -0
  42. package/agents/skills/claude-code/canary-pr-guardian/SKILL.md +129 -0
  43. package/agents/skills/claude-code/canary-pr-guardian/skill.yaml +17 -0
  44. package/agents/skills/claude-code/canary-promote-test/SKILL.md +228 -0
  45. package/agents/skills/claude-code/canary-savant/SKILL.md +233 -0
  46. package/agents/skills/claude-code/canary-savant/scripts/cli.mjs +274 -0
  47. package/agents/skills/claude-code/canary-savant/scripts/restoration.mjs +274 -0
  48. package/agents/skills/claude-code/canary-savant/scripts/rules.mjs +168 -0
  49. package/agents/skills/claude-code/canary-savant/scripts/runner.mjs +572 -0
  50. package/agents/skills/claude-code/canary-savant/scripts/scanner.mjs +374 -0
  51. package/agents/skills/claude-code/canary-savant/scripts/string-literals.mjs +116 -0
  52. package/agents/skills/claude-code/canary-setup-harness/SKILL.md +263 -0
  53. package/agents/skills/claude-code/canary-shadow/SKILL.md +131 -0
  54. package/agents/skills/claude-code/canary-shadow/scripts/cases.example.json +32 -0
  55. package/agents/skills/claude-code/canary-shadow/scripts/cli.mjs +195 -0
  56. package/agents/skills/claude-code/canary-ship/SKILL.md +177 -0
  57. package/agents/skills/claude-code/canary-ship/skill.yaml +16 -0
  58. package/agents/skills/claude-code/canary-strix/SKILL.md +130 -0
  59. package/agents/skills/claude-code/canary-strix/scripts/cli.mjs +255 -0
  60. package/agents/skills/claude-code/canary-strix/scripts/scanner.mjs +252 -0
  61. package/agents/skills/claude-code/canary-strix/scripts/terms.mjs +132 -0
  62. package/agents/skills/claude-code/canary-test-pipeline/SKILL.md +159 -0
  63. package/agents/skills/claude-code/canary-test-pipeline/skill.yaml +19 -0
  64. package/agents/skills/claude-code/canary-test-reporter/SKILL.md +138 -0
  65. package/agents/skills/claude-code/canary-test-reporter/scripts/cli.mjs +98 -0
  66. package/agents/skills/claude-code/canary-test-reporter/scripts/json_report.mjs +58 -0
  67. package/agents/skills/claude-code/canary-test-reporter/scripts/parse.mjs +216 -0
  68. package/agents/skills/claude-code/canary-test-reporter/scripts/render.mjs +114 -0
  69. package/agents/skills/lib/parse-args.mjs +214 -0
  70. package/dist/engine/analysis/cli.js +116 -54
  71. package/dist/engine/analysis/engine.js +34 -16
  72. package/dist/engine/analysis/reports.js +5 -4
  73. package/dist/engine/cli-commands.js +252 -106
  74. package/dist/engine/cli-common.js +15 -24
  75. package/dist/engine/cli.core.js +37 -11
  76. package/dist/engine/cli.js +2 -2
  77. package/dist/engine/company-knowledge-cli.js +2 -2
  78. package/dist/engine/core/adoption.js +408 -0
  79. package/dist/engine/core/framework-probes.js +7 -7
  80. package/dist/engine/core/fs-glob.js +2 -2
  81. package/dist/engine/core/gate-result.js +17 -0
  82. package/dist/engine/core/migrator.js +9 -17
  83. package/dist/engine/core/pattern-matcher.js +23 -5
  84. package/dist/engine/core/persona.js +421 -0
  85. package/dist/engine/core/promotion-verdict.js +261 -0
  86. package/dist/engine/core/quality-scorer.js +15 -2
  87. package/dist/engine/core/reporter.js +1 -9
  88. package/dist/engine/core/skill-dispatch.js +115 -0
  89. package/dist/engine/core/skill-examples.js +392 -0
  90. package/dist/engine/core/skill-registry.js +59 -4
  91. package/dist/engine/core/skill-surfaces.js +307 -0
  92. package/dist/engine/core/static-linter.js +310 -38
  93. package/dist/engine/core/test-files.js +77 -0
  94. package/dist/engine/core/ticket-updater.js +1 -7
  95. package/dist/engine/core/vacuity-scanner.js +726 -0
  96. package/dist/engine/core/workflow-discovery.js +2 -8
  97. package/dist/engine/core/workspace-detect.js +7 -6
  98. package/dist/engine/data/personas/registry.json +36 -0
  99. package/dist/engine/guardian/adjudication.js +5 -5
  100. package/dist/engine/guardian/analysis-emit.js +19 -28
  101. package/dist/engine/guardian/cli.js +150 -51
  102. package/dist/engine/guardian/coverage.js +1 -1
  103. package/dist/engine/guardian/diff-coverage/heuristic-tier.js +1 -1
  104. package/dist/engine/guardian/diff-coverage/orchestrator.js +2 -2
  105. package/dist/engine/guardian/pr-check.js +322 -33
  106. package/dist/engine/guardian/pr-comment.js +4 -3
  107. package/dist/engine/history/cli.js +210 -6
  108. package/dist/engine/history/ndjson-store.js +9 -5
  109. package/dist/engine/history/record.js +34 -5
  110. package/dist/engine/history/run-recorder.js +165 -0
  111. package/dist/engine/history/schema.js +25 -7
  112. package/dist/engine/history/store.js +9 -0
  113. package/dist/engine/mcp-server.js +35 -13
  114. package/dist/engine/skills-cli.js +249 -23
  115. package/dist/engine/util/ensure-ascii.js +37 -0
  116. package/dist/engine/workflow-cli.js +6 -6
  117. package/dist/gate-result.d.ts +11 -0
  118. package/dist/gate-result.js +18 -0
  119. package/dist/reporters/testtracker.d.ts +1 -1
  120. package/dist/reporters/testtracker.js +1 -1
  121. package/dist/uninstall.js +12 -5
  122. package/package.json +3 -2
@@ -0,0 +1,118 @@
1
+ #!/usr/bin/env node
2
+ // canary-fail-fast -- surface test failures fast and loud. Ported behavior-for-
3
+ // behavior from the Python original.
4
+ //
5
+ // Two halves:
6
+ // --config <playwright.config.*> audit fail-fast knobs (maxFailures/forbidOnly/
7
+ // retries); print recommendations (read-only).
8
+ // --results <playwright.json> print a loud, categorized failure digest to
9
+ // the CI log + ::error annotations; exit non-
10
+ // zero on any failure so the step fails.
11
+ //
12
+ // At least one of --config / --results is required. Self-contained -- no
13
+ // external skill dependency.
14
+ //
15
+ // Invoked via `canary skills run canary-fail-fast -- --results <json> [--config <path>]`.
16
+
17
+ import fs from 'node:fs';
18
+
19
+ import {
20
+ createParser,
21
+ formatUsageError,
22
+ EXIT_USAGE,
23
+ } from '../../../lib/parse-args.mjs';
24
+ import { parseFailures } from './parse.mjs';
25
+ import { buildDigest } from './digest.mjs';
26
+ import { checkConfig } from './fastfail_check.mjs';
27
+
28
+ const PREFIX = 'canary-fail-fast:';
29
+ const DASH = '\u2014'; // em dash (see digest.mjs)
30
+
31
+ const USAGE =
32
+ 'usage: canary-fail-fast [-h] [--results PATH] [--config PATH]\n' +
33
+ '\n' +
34
+ 'Fail-fast config audit + loud run-end failure digest.';
35
+
36
+ /**
37
+ * Both value flags are optional here -- main enforces "at least one of
38
+ * --results / --config" itself, because argparse has no vocabulary for that.
39
+ * The four shared invariants live in the shared parser (#479).
40
+ */
41
+ export const CLI_SPEC = {
42
+ prog: 'canary-fail-fast',
43
+ values: {
44
+ '--results': { key: 'results' },
45
+ '--config': { key: 'config' },
46
+ },
47
+ };
48
+
49
+ const parseArgs = createParser(CLI_SPEC);
50
+
51
+ export function main(argv = []) {
52
+ const { opts: args, help, error } = parseArgs(argv);
53
+
54
+ if (help) {
55
+ console.log(USAGE);
56
+ return 0;
57
+ }
58
+ if (error) {
59
+ console.error(formatUsageError(CLI_SPEC.prog, error));
60
+ return EXIT_USAGE;
61
+ }
62
+
63
+ if (!args.results && !args.config) {
64
+ console.error(
65
+ `${PREFIX} nothing to do ${DASH} pass --results and/or --config.`,
66
+ );
67
+ return 1;
68
+ }
69
+
70
+ // ---- config audit -----------------------------------------------------
71
+ if (args.config) {
72
+ let text;
73
+ try {
74
+ text = fs.readFileSync(args.config, 'utf8');
75
+ } catch (exc) {
76
+ console.error(`${PREFIX} cannot read config: ${exc.message}`);
77
+ return 1;
78
+ }
79
+ const recs = checkConfig(text);
80
+ if (recs.length) {
81
+ console.log('Fail-fast config recommendations:');
82
+ for (const r of recs) console.log(` - ${r}`);
83
+ } else {
84
+ console.log('Fail-fast config OK.');
85
+ }
86
+ }
87
+
88
+ // ---- failure digest ---------------------------------------------------
89
+ let exitCode = 0;
90
+ if (args.results) {
91
+ if (!fs.existsSync(args.results)) {
92
+ console.error(`${PREFIX} results file not found: ${args.results}`);
93
+ return 1;
94
+ }
95
+ let failures;
96
+ try {
97
+ failures = parseFailures(args.results);
98
+ } catch (exc) {
99
+ console.error(`${PREFIX} ${exc.message}`);
100
+ return 1;
101
+ }
102
+ const d = buildDigest(failures);
103
+ console.log(d.text);
104
+ for (const ann of d.annotations) console.log(ann);
105
+ exitCode = d.exitCode;
106
+ }
107
+
108
+ return exitCode;
109
+ }
110
+
111
+ // Direct execution (the skill runner execs this file via its shebang).
112
+ //
113
+ // `process.exitCode`, not `process.exit()`: a large `--json` payload exceeds
114
+ // the pipe buffer, and `process.exit` tears the process down mid-write, leaving
115
+ // truncated JSON that still exits 0 (#791).
116
+ if (import.meta.url === `file://${process.argv[1]}`) {
117
+ process.exitCode = main(process.argv.slice(2));
118
+ }
@@ -0,0 +1,69 @@
1
+ // digest -- loud, categorized failure digest (pure). Ported behavior-for-
2
+ // behavior from the Python original.
3
+ //
4
+ // Turns a list of failures into a terse CI-log digest + `::error` workflow
5
+ // annotations + a non-zero exit code, so an engineer triages from the run log
6
+ // without opening the HTML report.
7
+
8
+ import { FAILURE_CATEGORIES, categorizeFailure } from './failures.mjs';
9
+
10
+ // Output glyphs kept as \u escapes so the source stays ASCII while the emitted
11
+ // text is byte-identical to the Python original.
12
+ const CHECK = '\u2705'; // white heavy check mark
13
+ const CROSS = '\u274c'; // cross mark
14
+ const DASH = '\u2014'; // em dash
15
+
16
+ /**
17
+ * @typedef {{text: string, annotations: string[], exitCode: number}} Digest
18
+ */
19
+
20
+ function firstLine(error, limit = 160) {
21
+ if (!error) return '(no error message)';
22
+ for (const raw of error.split(/\r\n|\r|\n/)) {
23
+ const line = raw.trim();
24
+ if (line) return line.slice(0, limit);
25
+ }
26
+ return '(no error message)';
27
+ }
28
+
29
+ /** Build the digest text, `::error` annotations, and exit code from failures. */
30
+ export function buildDigest(failures) {
31
+ if (!failures.length) {
32
+ return { text: `${CHECK} 0 failing tests.`, annotations: [], exitCode: 0 };
33
+ }
34
+
35
+ const n = failures.length;
36
+ const byCat = new Map();
37
+ for (const f of failures) {
38
+ const cat = categorizeFailure(f.error);
39
+ if (!byCat.has(cat)) byCat.set(cat, []);
40
+ byCat.get(cat).push(f);
41
+ }
42
+
43
+ const lines = [
44
+ `${CROSS} ${n} failing test${n !== 1 ? 's' : ''} ${DASH} triage by category:`,
45
+ '',
46
+ ];
47
+ for (const cat of FAILURE_CATEGORIES) {
48
+ const bucket = byCat.get(cat);
49
+ if (!bucket || !bucket.length) continue;
50
+ lines.push(` ${cat} (${bucket.length}):`);
51
+ for (const f of bucket) {
52
+ lines.push(` - ${f.title} ${DASH} ${firstLine(f.error)}`);
53
+ }
54
+ }
55
+ const text = lines.join('\n');
56
+
57
+ const annotations = [];
58
+ for (const f of failures) {
59
+ const cat = categorizeFailure(f.error);
60
+ let loc = '';
61
+ if (f.file) loc += `file=${f.file},`;
62
+ if (f.line !== null && f.line !== undefined) loc += `line=${f.line},`;
63
+ annotations.push(
64
+ `::error ${loc}title=Test failure::${f.title} ${DASH} ${cat}: ${firstLine(f.error)}`,
65
+ );
66
+ }
67
+
68
+ return { text, annotations, exitCode: 1 };
69
+ }
@@ -0,0 +1,60 @@
1
+ // failures -- heuristic failure categorization (self-contained, pure). Ported
2
+ // behavior-for-behavior from the Python original.
3
+ //
4
+ // Order matters -- the most distinctive signals are checked first so 4xx/5xx
5
+ // status-code patterns don't swallow schema errors that happen to mention a
6
+ // status code in a response preview.
7
+
8
+ export const FAILURE_CATEGORIES = [
9
+ 'schema',
10
+ 'auth',
11
+ 'server',
12
+ 'client',
13
+ 'timeout',
14
+ 'network',
15
+ 'other',
16
+ ];
17
+
18
+ // [category, pattern] pairs in match-priority order (distinct from the display
19
+ // order in FAILURE_CATEGORIES). Patterns are case-insensitive and stateless
20
+ // (no `g` flag), mirroring Python's re.search with re.IGNORECASE.
21
+ //
22
+ // Intentional deviation (exotic input only): the `\b` word boundaries use
23
+ // JavaScript's ASCII-only word-character definition, whereas Python's `re` is
24
+ // Unicode-aware by default. An accented/non-ASCII letter glued directly to a
25
+ // status code (a "u"-with-umlaut immediately before `401`, say) can therefore
26
+ // categorize differently than the Python original. This is negligible for real
27
+ // Playwright error messages, which are
28
+ // ASCII around status codes; documented so the behavior reads as deliberate.
29
+ const RULES = [
30
+ [
31
+ 'schema',
32
+ /ZodError|invalid[_ ]type|unrecognized key|expected .+ received|at path "|\bzod\b/i,
33
+ ],
34
+ [
35
+ 'auth',
36
+ /\b401\b|unauthorized|\b403\b|forbidden|invalid(?: auth)? token|token expired/i,
37
+ ],
38
+ ['timeout', /timeout|timed out|etimedout|deadline exceeded/i],
39
+ [
40
+ 'network',
41
+ /econnrefused|enotfound|econnreset|socket hang up|getaddrinfo|network request failed/i,
42
+ ],
43
+ [
44
+ 'server',
45
+ /\b5\d{2}\b|internal server error|bad gateway|service unavailable|gateway timeout/i,
46
+ ],
47
+ [
48
+ 'client',
49
+ /\b4(?:0[045-9]|1\d|2\d)\b|bad request|not found|unprocessable|conflict/i,
50
+ ],
51
+ ];
52
+
53
+ /** Return the category of a failure error message ('other' when unknown). */
54
+ export function categorizeFailure(error) {
55
+ if (!error) return 'other';
56
+ for (const [category, pattern] of RULES) {
57
+ if (pattern.test(error)) return category;
58
+ }
59
+ return 'other';
60
+ }
@@ -0,0 +1,43 @@
1
+ // fastfail_check -- fail-fast config audit for a Playwright config (pure, read-
2
+ // only). Ported behavior-for-behavior from the Python original.
3
+ //
4
+ // Scans a `playwright.config.*` for the fail-fast knobs a suite should set so a
5
+ // broken CI run aborts early instead of burning the whole matrix. Never edits
6
+ // the config -- it recommends, and CANONICAL is the block to paste in.
7
+
8
+ const DASH = '\u2014'; // em dash (see digest.mjs)
9
+
10
+ // The block to paste into playwright.config.ts. Exported for reference; the CLI
11
+ // does not emit it.
12
+ export const CANONICAL = `export default defineConfig({
13
+ // Fail fast in CI: abort once enough has clearly broken, never on local runs.
14
+ forbidOnly: !!process.env.CI, // a stray test.only fails the build
15
+ maxFailures: process.env.CI ? 10 : 0, // stop the run after 10 failures in CI
16
+ retries: process.env.CI ? 2 : 0, // absorb flakes in CI; surface them locally
17
+ // ...your existing config
18
+ });
19
+ `;
20
+
21
+ // knob name -> why it matters, in the Python dict's insertion order.
22
+ const KNOBS = [
23
+ ['forbidOnly', 'a stray `test.only` silently skips the rest of the suite'],
24
+ [
25
+ 'maxFailures',
26
+ 'a broken run keeps burning the matrix instead of aborting early',
27
+ ],
28
+ [
29
+ 'retries',
30
+ 'flakes either fail the build or hide locally without a CI retry policy',
31
+ ],
32
+ ];
33
+
34
+ /**
35
+ * Return one recommendation per fail-fast knob missing from the config text.
36
+ * Empty array means all knobs are present. Substring scan -- good enough to flag
37
+ * absence; it does not validate the knob's value.
38
+ */
39
+ export function checkConfig(text) {
40
+ return KNOBS.filter(([knob]) => !text.includes(knob)).map(
41
+ ([knob, why]) => `Add \`${knob}\` ${DASH} without it, ${why}.`,
42
+ );
43
+ }
@@ -0,0 +1,149 @@
1
+ // parse -- minimal Playwright JSON parser (failing tests only, self-contained).
2
+ // Ported behavior-for-behavior from the Python original.
3
+ //
4
+ // Walks the Playwright JSON reporter's nested suites/specs/tests and returns the
5
+ // real failures. A failed/unexpected test with a passing retry is flaky and
6
+ // excluded; without one it is a failure. Leading non-JSON banners are stripped
7
+ // before parsing (matches the reporter's defensive indexOf('{')).
8
+
9
+ import fs from 'node:fs';
10
+
11
+ /**
12
+ * @typedef {{title: string, status: string, file: (string|null),
13
+ * line: (number|null), error: (string|null)}} Failure
14
+ */
15
+
16
+ /** Build a Failure with the Python dataclass defaults (absent -> null). */
17
+ export function Failure(fields) {
18
+ return {
19
+ title: fields.title,
20
+ status: fields.status,
21
+ file: fields.file ?? null,
22
+ line: fields.line ?? null,
23
+ error: fields.error ?? null,
24
+ };
25
+ }
26
+
27
+ function isObject(x) {
28
+ return x !== null && typeof x === 'object' && !Array.isArray(x);
29
+ }
30
+
31
+ // Mirror Python dict.get: `.get` on a non-dict (str/list/number/None) raises
32
+ // AttributeError, which parseFailures converts into a structured error. Here a
33
+ // non-object access throws TypeError, caught by the same conversion.
34
+ function dget(obj, key, dflt) {
35
+ if (!isObject(obj)) throw new TypeError('value is not an object');
36
+ return key in obj ? obj[key] : dflt;
37
+ }
38
+
39
+ // Mirror Python's `container or []` for a for-loop target. Python truthiness
40
+ // differs from JS: an empty object `{}` and empty string `""` are FALSY in
41
+ // Python but TRUTHY in JS. A naive `x || []` would turn `{"suites": {}}` into a
42
+ // thrown "unexpected structure" (JS iterates the truthy `{}`) where Python
43
+ // quietly iterates nothing -- flipping a CI pass into a fail. So:
44
+ // - any Python-falsy value (null/undefined/false/0/""/[]/{}) -> [] (iterate nothing)
45
+ // - an Array -> the array
46
+ // - any other truthy value (non-empty object/string/number) -> returned as-is,
47
+ // so `for...of` reproduces Python's iterate-then-`.get`-fails path (a string
48
+ // iterates its chars; a non-empty object/number is non-iterable and throws),
49
+ // which surfaces as the same "unexpected structure" error.
50
+ function asItems(v) {
51
+ if (Array.isArray(v)) return v;
52
+ if (v && typeof v === 'object') return Object.keys(v).length ? v : [];
53
+ if (v && typeof v !== 'object') return v; // non-empty string / truthy number
54
+ return []; // null/undefined/false/0/""
55
+ }
56
+
57
+ /** Parse a Playwright JSON results file, returning only the real failures. */
58
+ export function parseFailures(resultsPath) {
59
+ if (!fs.existsSync(resultsPath)) return [];
60
+
61
+ let text = fs.readFileSync(resultsPath, 'utf8');
62
+ const braceAt = text.indexOf('{');
63
+ if (braceAt > 0) text = text.slice(braceAt);
64
+
65
+ let data;
66
+ try {
67
+ data = JSON.parse(text);
68
+ } catch (exc) {
69
+ throw new Error(`results file is not valid JSON: ${exc.message}`);
70
+ }
71
+
72
+ if (!isObject(data)) {
73
+ throw new Error("results file's top-level value must be an object");
74
+ }
75
+
76
+ const failures = [];
77
+ try {
78
+ for (const suite of asItems(data.suites)) {
79
+ processSuite(suite, failures, '', '');
80
+ }
81
+ } catch (exc) {
82
+ if (exc instanceof TypeError) {
83
+ throw new Error(
84
+ `results file has an unexpected structure: ${exc.message}`,
85
+ );
86
+ }
87
+ throw exc;
88
+ }
89
+ return failures;
90
+ }
91
+
92
+ function processSuite(suite, failures, parentPath, suiteFile) {
93
+ const suiteTitle = dget(suite, 'title', '');
94
+ const suitePath = parentPath ? `${parentPath} > ${suiteTitle}` : suiteTitle;
95
+ const currentFile = dget(suite, 'file', undefined) || suiteFile;
96
+
97
+ for (const child of asItems(dget(suite, 'suites', []))) {
98
+ processSuite(child, failures, suitePath, currentFile);
99
+ }
100
+
101
+ for (const spec of asItems(dget(suite, 'specs', []))) {
102
+ const specPath = `${suitePath} > ${dget(spec, 'title', '')}`;
103
+ const specLocation = dget(spec, 'location', undefined) || {};
104
+ for (const test of asItems(dget(spec, 'tests', []))) {
105
+ const testTitle =
106
+ dget(test, 'title', undefined) || dget(spec, 'title', '');
107
+ const testLocation = dget(test, 'location', undefined) || {};
108
+ const results = asItems(dget(test, 'results', undefined));
109
+
110
+ const status = dget(test, 'status', 'unknown');
111
+ if (status !== 'unexpected' && status !== 'failed') continue;
112
+
113
+ const hasPassingRetry = results.some((r) => {
114
+ const s = dget(r, 'status', undefined);
115
+ return s === 'passed' || s === 'expected';
116
+ });
117
+ if (hasPassingRetry) continue; // flaky -- excluded from the failure count
118
+
119
+ let error = null;
120
+ if (results.length) {
121
+ const last = results[results.length - 1];
122
+ const err = dget(last, 'error', undefined) || {};
123
+ error = dget(err, 'message', undefined);
124
+ if (error === undefined || error === null) {
125
+ const errs = dget(last, 'errors', undefined) || [];
126
+ if (errs.length) {
127
+ error = dget(errs[0], 'message', undefined);
128
+ }
129
+ }
130
+ }
131
+
132
+ failures.push(
133
+ Failure({
134
+ title: `${specPath} > ${testTitle}`,
135
+ status,
136
+ file:
137
+ dget(testLocation, 'file', undefined) ||
138
+ dget(specLocation, 'file', undefined) ||
139
+ currentFile,
140
+ line:
141
+ dget(testLocation, 'line', undefined) ||
142
+ dget(specLocation, 'line', undefined) ||
143
+ null,
144
+ error,
145
+ }),
146
+ );
147
+ }
148
+ }
149
+ }
@@ -0,0 +1,153 @@
1
+ ---
2
+ name: canary-failure-impact
3
+ description: >
4
+ For a given test, function, or code path, traces downstream effects and
5
+ produces a severity label. Investigates config/auth failures using the
6
+ consuming repo's declared user_catalog_skill. Optionally focuses on critical
7
+ paths when critical-areas.json is present.
8
+ ---
9
+
10
+ # Canary: Failure Impact
11
+
12
+ Answers "what actually breaks if this code fails and no test catches it?"
13
+ Produces a severity label and a concrete description of downstream effects to
14
+ help prioritise where to invest test coverage.
15
+
16
+ ## When to Use
17
+
18
+ - Before deciding which gap to close first: "which of these matters most?"
19
+
20
+ - When a test fails and you need to understand the blast radius
21
+
22
+ - As Phase 3 of `/canary-test-pipeline`
23
+
24
+ - When asked "what's the impact if this breaks?"
25
+
26
+ ## This skill vs. `canary guardian analyze`
27
+
28
+ This skill discovers downstream dependents with harness's `compute_blast_radius`
29
+ primitive when the MCP is present (degrading to plain `grep -r` when it is not),
30
+ then applies a **domain-keyword heuristic** (Steps 3–4 below) to turn that
31
+ dependent set into a severity label. The severity labeling is the heuristic part
32
+ — keyword matching over dependent file and function names.
33
+ `canary guardian analyze` (`ts/src/guardian/`, wired to
34
+ `canary guardian analyze` in `ts/src/cli.ts`) is a **real OpenAPI-diff
35
+ blast-radius engine** — it diffs two OpenAPI specs (`--spec-before` /
36
+ `--spec-after`), extracts the actual added/removed/changed endpoints, and maps
37
+ each to coverage gaps against a `coverage-report.json`. For the class of change
38
+ it covers, guardian is strictly higher-fidelity than the heuristics here.
39
+
40
+ - **Use `canary guardian analyze`** when the change is an API/schema change and
41
+ you have (or can generate) before/after OpenAPI specs — it gives exact
42
+ endpoint-level impact and coverage-gap data instead of a keyword guess.
43
+ - **Use this skill** for everything guardian doesn't cover: non-API code paths
44
+ (services, UI components, internal functions), impact tracing where no OpenAPI
45
+ spec exists, or when you need the broader billing/auth/compliance
46
+ domain-severity labeling in Step 3 rather than a strict API diff.
47
+
48
+ They are complementary, not competing — do not duplicate guardian's spec-diff
49
+ logic here if an OpenAPI change is in scope; delegate to
50
+ `canary guardian analyze` instead.
51
+
52
+ ## Input
53
+
54
+ Provide one of:
55
+
56
+ - A test file path: `tests/loyalty/points.spec.ts`
57
+
58
+ - A function name: `accruePoints`
59
+
60
+ - A code path: `src/loyalty/points.service.ts`
61
+
62
+ If `.canary/critical-areas.json` is present, focus tracing on paths with
63
+ `risk_score ≥ 0.7`.
64
+
65
+ ## Tracing Logic
66
+
67
+ ### Step 1 — Identify the code path
68
+
69
+ Resolve the input to a specific file and function. If ambiguous, ask before
70
+ proceeding.
71
+
72
+ ### Step 2 — Walk downstream dependents
73
+
74
+ **With harness MCP available:** call `compute_blast_radius` for the target file
75
+ (`file`, `mode: "detailed"`). It simulates cascading failure with a
76
+ probability-weighted BFS and returns each affected node with a cumulative
77
+ failure probability — this is the purpose-built blast-radius primitive, so use
78
+ it instead of hand-walking `get_relationships` hop-by-hop. Feed the returned
79
+ node set into Step 3, and let the cumulative probability weight the severity
80
+ (high-probability nodes dominate). When you additionally need the affected set
81
+ grouped by kind (tests vs docs vs code), call `get_impact` for the same target.
82
+
83
+ **Fallback:** use `grep -r` to find files that import or call the target. Limit
84
+ to direct dependents (1 hop) when MCP is unavailable.
85
+
86
+ ### Step 3 — Classify each dependent by domain
87
+
88
+ Apply these heuristics to the dependent paths and function names:
89
+
90
+ | Domain signal | Severity modifier |
91
+ | --------------------------------------- | ----------------------------------- |
92
+ | billing / payment / charge / invoice | +2 (financial impact) |
93
+ | auth / session / token / permission | +2 (security/access) |
94
+ | compliance / audit / PHI / PII / HIPAA | +2 (regulatory) |
95
+ | data / persist / write / store / commit | +1 (data integrity) |
96
+ | UI / render / display / format / label | −1 (user-facing only, no data risk) |
97
+
98
+ ### Step 4 — Aggregate to severity label
99
+
100
+ Base score starts at 2 (Medium). Sum modifiers from step 3. Cap at 4 (Critical).
101
+
102
+ | Score | Label |
103
+ | ----- | -------- |
104
+ | 5+ | Critical |
105
+ | 3–4 | High |
106
+ | 2 | Medium |
107
+ | 0–1 | Low |
108
+
109
+ ### Step 5 — User catalog investigation
110
+
111
+ When a test failure in the target path involves an auth, permission, or
112
+ configuration error:
113
+
114
+ 1. Read `user_catalog_skill` from `.canary/company.json`
115
+ 2. If present: invoke `canary skills run <user_catalog_skill>` with the required
116
+ attributes from the error context
117
+ 3. If a matching user/config is found: surface it as a suggestion
118
+ 4. If absent or no match: present constructively —
119
+
120
+ > "This failure may be a test user or test data configuration issue. Check
121
+ > your user catalog if you have one, or set up the required test data before
122
+ > re-running."
123
+
124
+ ## Output Format
125
+
126
+ ```text
127
+ Failure impact — src/loyalty/points.service.ts::accruePoints
128
+
129
+ Severity: HIGH
130
+
131
+ If this breaks undetected:
132
+ · Members see incorrect balance in the partner portal (user-facing)
133
+ · Points journal diverges from the ledger (data integrity)
134
+ · Downstream: redemption.service.ts · tier-upgrade.service.ts ·
135
+ reporting.service.ts (3 dependents)
136
+
137
+ Priority: write failure-path tests before next release
138
+ Suggested: /canary-write-test "test failure paths for accruePoints"
139
+ ```
140
+
141
+ ## Related skills
142
+
143
+ - `/canary-critical-areas` — produces `critical-areas.json` used for focus
144
+
145
+ - `/canary-ci-ready` — uses the same user-catalog investigation pattern
146
+
147
+ - `/canary-write-test` — generates tests for the identified high-impact gaps
148
+
149
+ - `/canary-test-pipeline` — Phase 3
150
+
151
+ - `canary guardian analyze` (CLI, `ts/src/guardian/`) — higher-fidelity
152
+ OpenAPI-diff blast-radius engine; use instead of this skill's heuristics when
153
+ the change is an API/schema change with before/after specs available
@@ -0,0 +1,15 @@
1
+ name: canary-failure-impact
2
+ version: '1.0.0'
3
+ description:
4
+ Trace the downstream blast radius of an undetected failure for a given test,
5
+ function, or code path and assign a severity label.
6
+ stability: static
7
+ triggers:
8
+ - manual
9
+ platforms:
10
+ - claude-code
11
+ type: rigid
12
+ tools: []
13
+ tier: 1
14
+ depends_on:
15
+ - canary-ci-ready