canary-test-cli 5.15.0 → 6.0.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 (79) hide show
  1. package/agent/frameworks/registry.json +655 -0
  2. package/bin/canary.js +20 -15
  3. package/dist/doctor-manifest.d.ts +94 -0
  4. package/dist/doctor.d.ts +67 -0
  5. package/dist/engine/analysis/cli.js +270 -0
  6. package/dist/engine/analysis/engine.js +146 -0
  7. package/dist/engine/analysis/reports.js +0 -0
  8. package/dist/engine/analysis/rows.js +9 -0
  9. package/dist/engine/cli-commands.js +618 -0
  10. package/dist/engine/cli-common.js +60 -0
  11. package/dist/engine/cli.core.js +208 -0
  12. package/dist/engine/cli.js +31 -0
  13. package/dist/engine/company-knowledge-cli.js +201 -0
  14. package/dist/engine/core/ci-env.js +33 -0
  15. package/dist/engine/core/classifier.js +192 -0
  16. package/dist/engine/core/company-knowledge.js +765 -0
  17. package/dist/engine/core/config-validation.js +74 -0
  18. package/dist/engine/core/detection.js +48 -0
  19. package/dist/engine/core/domain-scanner.js +212 -0
  20. package/dist/engine/core/environment-detect.js +410 -0
  21. package/dist/engine/core/executor.js +181 -0
  22. package/dist/engine/core/feedback.js +93 -0
  23. package/dist/engine/core/fixture-scanner.js +173 -0
  24. package/dist/engine/core/framework-registry.js +123 -0
  25. package/dist/engine/core/mcp-validator.js +218 -0
  26. package/dist/engine/core/metadata-scanner.js +147 -0
  27. package/dist/engine/core/migrator.js +1112 -0
  28. package/dist/engine/core/overlays.js +176 -0
  29. package/dist/engine/core/pattern-healer.js +147 -0
  30. package/dist/engine/core/pattern-matcher.js +255 -0
  31. package/dist/engine/core/quality-scorer.js +213 -0
  32. package/dist/engine/core/recommender.js +152 -0
  33. package/dist/engine/core/reporter.js +211 -0
  34. package/dist/engine/core/scaffolder.js +236 -0
  35. package/dist/engine/core/skill-registry.js +522 -0
  36. package/dist/engine/core/static-linter.js +237 -0
  37. package/dist/engine/core/ticket-updater.js +639 -0
  38. package/dist/engine/core/workflow-discovery.js +693 -0
  39. package/dist/engine/guardian/agent-tier.js +338 -0
  40. package/dist/engine/guardian/analysis-emit.js +201 -0
  41. package/dist/engine/guardian/cli.js +787 -0
  42. package/dist/engine/guardian/coverage.js +1055 -0
  43. package/dist/engine/guardian/delta-emitter.js +46 -0
  44. package/dist/engine/guardian/diff-extractor.js +257 -0
  45. package/dist/engine/guardian/hard-gate.js +373 -0
  46. package/dist/engine/guardian/impact-mapper.js +121 -0
  47. package/dist/engine/guardian/pr-check.js +975 -0
  48. package/dist/engine/guardian/pr-comment.js +200 -0
  49. package/dist/engine/guardian/summary-emitter.js +94 -0
  50. package/dist/engine/guardian/tier.js +58 -0
  51. package/dist/engine/history/cli.js +303 -0
  52. package/dist/engine/history/detector.js +68 -0
  53. package/dist/engine/history/ndjson-store.js +177 -0
  54. package/dist/engine/history/record.js +14 -0
  55. package/dist/engine/history/schema.js +59 -0
  56. package/dist/engine/history/store.js +47 -0
  57. package/dist/engine/history/supabase-store.js +113 -0
  58. package/dist/engine/main-deps.js +105 -0
  59. package/dist/engine/mcp-server.js +647 -0
  60. package/dist/engine/package.json +4 -0
  61. package/dist/engine/skills-cli.js +181 -0
  62. package/dist/engine/ui/banner.js +50 -0
  63. package/dist/engine/util/coalesce.js +12 -0
  64. package/dist/engine/util/round.js +43 -0
  65. package/dist/engine/workflow-cli.js +242 -0
  66. package/dist/engine-checks.d.ts +49 -0
  67. package/dist/overlay-commands.d.ts +81 -0
  68. package/dist/overlay-conflicts.d.ts +33 -0
  69. package/dist/overlay-lint.d.ts +19 -0
  70. package/dist/overlays-registry.d.ts +74 -0
  71. package/dist/reporters/testtracker.d.ts +89 -0
  72. package/dist/reporters/testtracker.js +195 -0
  73. package/dist/router.d.ts +12 -0
  74. package/dist/router.js +4 -4
  75. package/dist/skill-requirements.d.ts +57 -0
  76. package/dist/source-spec.d.ts +20 -0
  77. package/package.json +30 -6
  78. package/bin/canary +0 -0
  79. package/scripts/install.js +0 -104
@@ -0,0 +1,237 @@
1
+ /**
2
+ * Static linter for test files — faithful TS port of
3
+ * `agent/core/static_linter.py`.
4
+ *
5
+ * Produces file:line findings without executing tests. Powers the static
6
+ * review + flake-check subsets. Regex/line based; pure filesystem reads.
7
+ */
8
+ import { readFileSync } from 'node:fs';
9
+ import { basename, extname } from 'node:path';
10
+ export function formatFinding(f) {
11
+ return `[${f.severity.toUpperCase()}] ${f.file}:${f.line} (${f.rule})\n ${f.message}\n → ${f.suggestion}`;
12
+ }
13
+ // Flakiness
14
+ const SLEEP = /time\.sleep\s*\(|page\.waitForTimeout\s*\(/;
15
+ const SETTIMEOUT = /(?<!\w)setTimeout\s*\(/;
16
+ const RANDOM = /Math\.random\s*\(|random\.random\s*\(|random\.choice\s*\(|random\.randint\s*\(/;
17
+ const TIMESTAMP = /Date\.now\s*\(|datetime\.now\s*\(|datetime\.utcnow\s*\(/;
18
+ // Brittle selectors
19
+ const CSS_CLASS_SELECTOR = /['"]\.[a-zA-Z][\w-]*['"]/;
20
+ const CSS_ID_SELECTOR = /['"]#[a-zA-Z][\w-]*['"]/;
21
+ const XPATH_SELECTOR = /['"]\/+[a-zA-Z[\]/@*]/;
22
+ const LOCATOR_METHODS = /\.(locator|querySelector)\s*\(/;
23
+ // Missing await
24
+ const BARE_PLAYWRIGHT_CALL = /(?<!await\s)(?<!return\s)(?<!\w)(?:page|frame|locator)\.(?:click|fill|type|check|uncheck|selectOption|hover|focus|press|tap|dblclick)\s*\(/;
25
+ // Assertion detection
26
+ const TEST_FN_PY = /^(\s*)def (test_\w+)\s*\(/gm;
27
+ const TEST_FN_JS = /(?:^|\s)(?:it|test)\s*\(\s*['"]([^'"]*)['"]/gm;
28
+ const ASSERT_PY = /\bassert\b|\bpytest\.raises\b/;
29
+ const ASSERT_JS = /\bexpect\s*\(|\bto(?:Be|Equal|Contain|Have|Match|Throw|Raise)\b/;
30
+ // Strippers
31
+ const STRING_LITERAL = /(['"])(?:\\.|(?!\1).)*?\1/g;
32
+ // Magic numbers
33
+ const NUMERIC_LITERAL = /(?<![\w.])-?\d+(?:\.\d+)?(?![\w.])/g;
34
+ const ALLOWED_NUMBERS = new Set(['0', '1', '2', '-1', '10', '100']);
35
+ const HTTP_STATUS = new Set([
36
+ '200',
37
+ '201',
38
+ '202',
39
+ '204',
40
+ '301',
41
+ '302',
42
+ '304',
43
+ '400',
44
+ '401',
45
+ '403',
46
+ '404',
47
+ '405',
48
+ '409',
49
+ '410',
50
+ '422',
51
+ '429',
52
+ '500',
53
+ '501',
54
+ '502',
55
+ '503',
56
+ '504',
57
+ ]);
58
+ function isAllowedNumber(token) {
59
+ if (ALLOWED_NUMBERS.has(token) || HTTP_STATUS.has(token))
60
+ return true;
61
+ const bare = token.replace(/^-+/, '');
62
+ return /^\d$/.test(bare);
63
+ }
64
+ function isComment(line) {
65
+ const s = line.trim();
66
+ return s.startsWith('#') || s.startsWith('//') || s.startsWith('*');
67
+ }
68
+ function mk(file, line, rule, severity, message, suggestion) {
69
+ return { file, line, rule, severity, message, suggestion };
70
+ }
71
+ const FLAKINESS_RULES = [
72
+ {
73
+ re: SLEEP,
74
+ rule: 'FLAKE-001',
75
+ severity: 'critical',
76
+ message: 'Hardcoded sleep/wait detected.',
77
+ suggestion: 'Replace with an event-based wait (e.g. expect(locator).toBeVisible(), page.waitForResponse(), waitFor()).',
78
+ },
79
+ {
80
+ re: SETTIMEOUT,
81
+ rule: 'FLAKE-002',
82
+ severity: 'critical',
83
+ message: 'setTimeout used without a corresponding waitFor.',
84
+ suggestion: 'Wrap in page.waitForFunction() or replace with an awaitable assertion.',
85
+ guard: (line) => !line.includes('waitFor'),
86
+ },
87
+ {
88
+ re: RANDOM,
89
+ rule: 'FLAKE-003',
90
+ severity: 'warning',
91
+ message: 'Non-deterministic random value in test.',
92
+ suggestion: 'Use a fixed seed or a static fixture value instead.',
93
+ },
94
+ {
95
+ re: TIMESTAMP,
96
+ rule: 'FLAKE-004',
97
+ severity: 'warning',
98
+ message: 'Timestamp-dependent value detected.',
99
+ suggestion: 'Mock Date.now()/datetime.now() or use a fixed reference date.',
100
+ },
101
+ ];
102
+ function scanFlakiness(lines, file) {
103
+ const out = [];
104
+ lines.forEach((line, idx) => {
105
+ if (isComment(line))
106
+ return;
107
+ for (const r of FLAKINESS_RULES) {
108
+ if (r.re.test(line) && (!r.guard || r.guard(line))) {
109
+ out.push(mk(file, idx + 1, r.rule, r.severity, r.message, r.suggestion));
110
+ }
111
+ }
112
+ });
113
+ return out;
114
+ }
115
+ function selectorFinding(line, i, file) {
116
+ if (CSS_CLASS_SELECTOR.test(line)) {
117
+ return mk(file, i, 'LINT-001', 'warning', 'CSS class selector is brittle.', 'Prefer getByRole(), getByLabel(), or data-testid attributes.');
118
+ }
119
+ if (CSS_ID_SELECTOR.test(line)) {
120
+ return mk(file, i, 'LINT-002', 'warning', 'CSS id selector may break if the id changes.', 'Prefer getByTestId() or getByRole() over id-based selectors.');
121
+ }
122
+ if (XPATH_SELECTOR.test(line)) {
123
+ return mk(file, i, 'LINT-003', 'warning', 'XPath selector is fragile.', 'Replace with role, label, or test-id based locators.');
124
+ }
125
+ return null;
126
+ }
127
+ function scanSelectors(lines, file) {
128
+ const out = [];
129
+ lines.forEach((line, idx) => {
130
+ if (isComment(line) || !LOCATOR_METHODS.test(line))
131
+ return;
132
+ const finding = selectorFinding(line, idx + 1, file);
133
+ if (finding)
134
+ out.push(finding);
135
+ });
136
+ return out;
137
+ }
138
+ function scanMissingAwait(lines, file) {
139
+ const out = [];
140
+ lines.forEach((line, idx) => {
141
+ if (isComment(line))
142
+ return;
143
+ if (BARE_PLAYWRIGHT_CALL.test(line) && !line.includes('await')) {
144
+ out.push(mk(file, idx + 1, 'LINT-004', 'critical', 'Playwright action called without await.', 'Add `await` before the call to ensure it completes before the next step.'));
145
+ }
146
+ });
147
+ return out;
148
+ }
149
+ function scanMagicNumbers(lines, file) {
150
+ const out = [];
151
+ lines.forEach((raw, idx) => {
152
+ if (isComment(raw))
153
+ return;
154
+ const scrubbed = raw.replace(STRING_LITERAL, '""');
155
+ for (const m of scrubbed.matchAll(NUMERIC_LITERAL)) {
156
+ if (isAllowedNumber(m[0]))
157
+ continue;
158
+ out.push(mk(file, idx + 1, 'LINT-005', 'info', `Magic number ${m[0]}.`, 'Extract to a named constant or derive from test data.'));
159
+ break; // one finding per line
160
+ }
161
+ });
162
+ return out;
163
+ }
164
+ function lineOf(code, offset) {
165
+ let n = 1;
166
+ for (let i = 0; i < offset && i < code.length; i++) {
167
+ if (code[i] === '\n')
168
+ n++;
169
+ }
170
+ return n;
171
+ }
172
+ function scanAssertionFreePy(code, file) {
173
+ const out = [];
174
+ for (const m of code.matchAll(TEST_FN_PY)) {
175
+ const indent = m[1].length;
176
+ const start = m.index;
177
+ const rest = code.slice(start + m[0].length);
178
+ const nextFn = rest.match(new RegExp(`^[ \\t]{${indent}}def `, 'm'));
179
+ const body = nextFn ? rest.slice(0, nextFn.index) : rest;
180
+ if (!ASSERT_PY.test(body)) {
181
+ out.push(mk(file, lineOf(code, start), 'LINT-006', 'warning', `\`${m[2]}\` contains no assertions.`, 'Add at least one assert statement; a test that never fails proves nothing.'));
182
+ }
183
+ }
184
+ return out;
185
+ }
186
+ function scanAssertionFreeJs(code, file) {
187
+ const out = [];
188
+ for (const m of code.matchAll(TEST_FN_JS)) {
189
+ const start = m.index;
190
+ const rest = code.slice(start + m[0].length, start + m[0].length + 2000);
191
+ if (!ASSERT_JS.test(rest)) {
192
+ out.push(mk(file, lineOf(code, start), 'LINT-006', 'warning', `Test "${m[1]}" contains no assertions.`, 'Add an expect() call; a test that never asserts always passes.'));
193
+ }
194
+ }
195
+ return out;
196
+ }
197
+ function detectFramework(path) {
198
+ const suffix = extname(path).toLowerCase();
199
+ const name = basename(path).toLowerCase();
200
+ if (suffix === '.py')
201
+ return 'pytest';
202
+ if (name.includes('playwright'))
203
+ return 'playwright';
204
+ if (suffix === '.ts' || suffix === '.js')
205
+ return 'vitest';
206
+ return 'pytest';
207
+ }
208
+ export class StaticLinter {
209
+ /** Full quality audit — all rules. */
210
+ lint(path, framework) {
211
+ const code = readFileSync(path, 'utf-8');
212
+ const lines = code.split('\n');
213
+ const fw = framework || detectFramework(path);
214
+ const findings = [
215
+ ...scanFlakiness(lines, path),
216
+ ...scanSelectors(lines, path),
217
+ ...scanMissingAwait(lines, path),
218
+ ...scanMagicNumbers(lines, path),
219
+ ...(fw === 'pytest'
220
+ ? scanAssertionFreePy(code, path)
221
+ : scanAssertionFreeJs(code, path)),
222
+ ];
223
+ findings.sort((a, b) => a.line - b.line || cmp(a.rule, b.rule));
224
+ return findings;
225
+ }
226
+ /** Flakiness-only subset. */
227
+ flakeCheck(path) {
228
+ const code = readFileSync(path, 'utf-8');
229
+ const findings = scanFlakiness(code.split('\n'), path);
230
+ findings.sort((a, b) => a.line - b.line);
231
+ return findings;
232
+ }
233
+ }
234
+ function cmp(a, b) {
235
+ return a < b ? -1 : a > b ? 1 : 0;
236
+ }
237
+ //# sourceMappingURL=static-linter.js.map