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,74 @@
1
+ /**
2
+ * Shared fail-loud-but-not-hard-fail JSON config reading.
3
+ *
4
+ * Faithful TypeScript port of `agent/core/config_validation.py`. Callers that
5
+ * read a config file (`harness.config.json`, `.mcp.json`,
6
+ * `.canary/company.json`, ...) have historically caught every read/parse error
7
+ * in one blanket `catch` and treated the result the same as "file doesn't
8
+ * exist" — silently falling back to defaults. That collapses two very different
9
+ * situations into one:
10
+ *
11
+ * - The file genuinely doesn't exist. Totally normal; fall back silently.
12
+ * - The file exists but is malformed or unreadable. The user has a broken
13
+ * config and deserves to know — silently treating it as absent produces
14
+ * wrong-but-confident output.
15
+ *
16
+ * {@link readJsonWithWarning} distinguishes the two. It never raises: a
17
+ * malformed/unreadable file degrades to `[null, "<warning message>"]` rather
18
+ * than an exception, so existing call sites can adopt it without introducing a
19
+ * new failure mode. Callers are responsible for surfacing the warning message
20
+ * (CLI output, log line, etc.) — this module only detects and describes the
21
+ * problem.
22
+ */
23
+ import { existsSync, readFileSync } from 'node:fs';
24
+ /**
25
+ * Read and parse a JSON file, distinguishing "absent" from "malformed".
26
+ *
27
+ * Returns a `[data, warning]` tuple:
28
+ *
29
+ * - `[null, null]` — the file does not exist. Not an error; the caller
30
+ * should proceed as if there is no config.
31
+ * - `[null, "<message>"]` — the file exists but could not be read or parsed.
32
+ * The caller should surface this warning to the user instead of silently
33
+ * treating the config as absent.
34
+ * - `[data, null]` — the file exists and parsed successfully.
35
+ *
36
+ * Never raises.
37
+ */
38
+ export function readJsonWithWarning(path) {
39
+ let exists;
40
+ try {
41
+ exists = existsSync(path);
42
+ }
43
+ catch {
44
+ // A broken symlink or similar can make even existence checks raise on some
45
+ // platforms; treat it as "can't confirm presence" -> warn, don't crash.
46
+ return [null, `${path} could not be accessed`];
47
+ }
48
+ if (!exists) {
49
+ return [null, null];
50
+ }
51
+ let text;
52
+ try {
53
+ // ACCEPTED DIVERGENCE: Python's `read_text(encoding="utf-8")` raises an
54
+ // UNCAUGHT `UnicodeDecodeError` on a non-UTF-8 file (the Python only catches
55
+ // `OSError`), which contradicts this function's own "Never raises" contract.
56
+ // Node's `readFileSync(path, 'utf-8')` substitutes U+FFFD instead, so a
57
+ // non-UTF-8 config degrades to an invalid-JSON warning below rather than
58
+ // crashing. We intentionally KEEP the safe behavior over the latent crash.
59
+ text = readFileSync(path, 'utf-8');
60
+ }
61
+ catch (e) {
62
+ return [null, `${path} exists but could not be read: ${errMessage(e)}`];
63
+ }
64
+ try {
65
+ return [JSON.parse(text), null];
66
+ }
67
+ catch (e) {
68
+ return [null, `${path} exists but is not valid JSON: ${errMessage(e)}`];
69
+ }
70
+ }
71
+ function errMessage(e) {
72
+ return e instanceof Error ? e.message : String(e);
73
+ }
74
+ //# sourceMappingURL=config-validation.js.map
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Fail-loud auto-detection messaging.
3
+ *
4
+ * Faithful TypeScript port of `agent/core/detection.py`.
5
+ *
6
+ * Several CLI paths auto-detect something (a test framework, a doctor persona)
7
+ * and, when detection is uncertain, used to silently fall back to `unknown` /
8
+ * `None` or a bare "flag required" failure. That erodes user trust: the caller
9
+ * has no idea *what* was being detected, *why* it failed, or *what to do next*.
10
+ *
11
+ * {@link uncertainDetectionMessage} renders one clear, actionable message from
12
+ * the pieces a call site has on hand, so every uncertain-detection path reads
13
+ * the same way instead of each inventing its own ad hoc string. It is pure
14
+ * string-building — it never prints, raises, or decides control flow; the
15
+ * caller owns those. Message text is byte-for-byte identical to the oracle.
16
+ */
17
+ /**
18
+ * Build a clear, actionable message for an uncertain/failed detection.
19
+ *
20
+ * Python: `uncertain_detection_message`. The keyword-only Python args
21
+ * (`reason`, `candidates`, `override_hint`) map to the {@link
22
+ * UncertainDetectionOptions} object; `override_hint` → `overrideHint`.
23
+ *
24
+ * @param what What we tried to detect, as a human-readable noun phrase (e.g.
25
+ * "test framework", "doctor persona").
26
+ * @returns A single string. Always names `what` and always gives at least one
27
+ * next step when an override hint or candidate list is provided — never a
28
+ * bare `unknown`.
29
+ */
30
+ export function uncertainDetectionMessage(what, options = {}) {
31
+ const { reason, candidates, overrideHint } = options;
32
+ const parts = [`Could not confidently auto-detect the ${what}.`];
33
+ // Python `if reason:` — an empty string is falsy and omits the clause.
34
+ if (reason) {
35
+ parts.push(`Reason: ${reason}.`);
36
+ }
37
+ // Python `if candidates:` — a missing or empty list omits the "Known …"
38
+ // clause entirely (no bare "Known …: ." string).
39
+ if (candidates && candidates.length > 0) {
40
+ const joined = candidates.join(', ');
41
+ parts.push(`Known ${what}(s): ${joined}.`);
42
+ }
43
+ if (overrideHint) {
44
+ parts.push(`Set it explicitly with ${overrideHint}.`);
45
+ }
46
+ return parts.join(' ');
47
+ }
48
+ //# sourceMappingURL=detection.js.map
@@ -0,0 +1,212 @@
1
+ /**
2
+ * Domain knowledge scanner — faithful TS port of
3
+ * `agent/core/domain_scanner.py`.
4
+ *
5
+ * Scans a project's source files (not test files) to extract components, public
6
+ * functions, and API routes. Regex-based, no AST. Pure filesystem reads.
7
+ */
8
+ import { readdirSync, readFileSync, statSync } from 'node:fs';
9
+ import { relative, resolve } from 'node:path';
10
+ const IGNORED_DIRS = new Set([
11
+ 'node_modules',
12
+ '.git',
13
+ '__pycache__',
14
+ '.venv',
15
+ 'venv',
16
+ 'dist',
17
+ 'build',
18
+ '.next',
19
+ '.nuxt',
20
+ '.harness',
21
+ 'tests',
22
+ 'test',
23
+ '__tests__',
24
+ 'spec',
25
+ 'coverage',
26
+ ]);
27
+ const TEST_FILE_RE = /(^test_|_test\.py$|\.spec\.|\.test\.)/i;
28
+ const MAX_SOURCE_FILES = 20;
29
+ const MAX_FILE_BYTES = 32_768;
30
+ const MAX_ITEMS = 15;
31
+ const PY_EXTS = ['.py'];
32
+ const JS_EXTS = ['.ts', '.tsx', '.js', '.jsx'];
33
+ export function isEmpty(ctx) {
34
+ return ctx.sourceFiles === 0;
35
+ }
36
+ function emptyContext() {
37
+ return {
38
+ sourceFiles: 0,
39
+ modules: [],
40
+ components: [],
41
+ functions: [],
42
+ apiRoutes: [],
43
+ };
44
+ }
45
+ export class DomainScanner {
46
+ scan(projectRoot = '.') {
47
+ const root = resolve(projectRoot);
48
+ const all = walkSourceFiles(root);
49
+ const pyFiles = all.filter((p) => p.endsWith('.py')).sort(cmp);
50
+ const jsFiles = all
51
+ .filter((p) => JS_EXTS.some((e) => p.endsWith(e)))
52
+ .sort(cmp);
53
+ const files = [...pyFiles, ...jsFiles].slice(0, MAX_SOURCE_FILES);
54
+ if (files.length === 0)
55
+ return emptyContext();
56
+ const ctx = emptyContext();
57
+ ctx.sourceFiles = files.length;
58
+ ctx.modules = deriveModules(root, files);
59
+ for (const path of files) {
60
+ const text = safeRead(path);
61
+ if (text === null)
62
+ continue;
63
+ if (path.endsWith('.py'))
64
+ extractPython(text, ctx);
65
+ else
66
+ extractJs(text, ctx);
67
+ }
68
+ ctx.components = dedupCap(ctx.components);
69
+ ctx.functions = dedupCap(ctx.functions);
70
+ ctx.apiRoutes = dedupCap(ctx.apiRoutes);
71
+ return ctx;
72
+ }
73
+ }
74
+ function walkSourceFiles(root) {
75
+ const found = [];
76
+ const walk = (dir) => {
77
+ let entries;
78
+ try {
79
+ entries = readdirSync(dir, { withFileTypes: true });
80
+ }
81
+ catch {
82
+ return;
83
+ }
84
+ for (const entry of entries) {
85
+ const full = resolve(dir, entry.name);
86
+ if (entry.isDirectory()) {
87
+ if (IGNORED_DIRS.has(entry.name))
88
+ continue;
89
+ walk(full);
90
+ }
91
+ else if (entry.isFile()) {
92
+ if (!isSourceExt(entry.name))
93
+ continue;
94
+ // Faithful to Python: reject if ANY segment of the absolute path is an
95
+ // ignored dir — this includes ancestors above the scan root, not just
96
+ // dirs skipped during the walk.
97
+ if (hasIgnoredSegment(full))
98
+ continue;
99
+ if (TEST_FILE_RE.test(entry.name))
100
+ continue;
101
+ if (oversized(full))
102
+ continue;
103
+ found.push(full);
104
+ }
105
+ }
106
+ };
107
+ walk(root);
108
+ return found;
109
+ }
110
+ function isSourceExt(name) {
111
+ return [...PY_EXTS, ...JS_EXTS].some((e) => name.endsWith(e));
112
+ }
113
+ function hasIgnoredSegment(absPath) {
114
+ return absPath.split(/[/\\]/).some((seg) => IGNORED_DIRS.has(seg));
115
+ }
116
+ function oversized(path) {
117
+ try {
118
+ return statSync(path).size > MAX_FILE_BYTES;
119
+ }
120
+ catch {
121
+ return true;
122
+ }
123
+ }
124
+ function deriveModules(root, files) {
125
+ const modules = [];
126
+ for (const path of files) {
127
+ const rel = relative(root, path);
128
+ if (rel.startsWith('..'))
129
+ continue;
130
+ const parts = rel.split(/[/\\]/);
131
+ parts[parts.length - 1] = parts[parts.length - 1].replace(/\.(py|tsx?|jsx?)$/, '');
132
+ if (parts[parts.length - 1] === '__init__')
133
+ parts.pop();
134
+ if (parts.length > 0)
135
+ modules.push(parts.join('/'));
136
+ }
137
+ return modules.slice(0, MAX_ITEMS);
138
+ }
139
+ function extractPython(text, ctx) {
140
+ for (const m of text.matchAll(/^class\s+([A-Za-z]\w*)/gm)) {
141
+ ctx.components.push(m[1]);
142
+ }
143
+ for (const m of text.matchAll(/^def\s+([a-z]\w*)\s*\(/gm)) {
144
+ const name = m[1];
145
+ if (!name.startsWith('_'))
146
+ ctx.functions.push(name);
147
+ }
148
+ const routeRe = /@(?:app|router|bp)\.(get|post|put|patch|delete|route)\s*\(\s*['"]([^'"]+)['"]/gi;
149
+ for (const m of text.matchAll(routeRe)) {
150
+ const method = m[1].toUpperCase();
151
+ const pathVal = m[2];
152
+ ctx.apiRoutes.push(method === 'ROUTE' ? pathVal : `${method} ${pathVal}`);
153
+ }
154
+ }
155
+ function extractJs(text, ctx) {
156
+ extractJsClasses(text, ctx);
157
+ extractJsCallables(text, ctx);
158
+ extractJsRoutes(text, ctx);
159
+ }
160
+ function extractJsClasses(text, ctx) {
161
+ for (const m of text.matchAll(/\bexport\s+(?:default\s+)?class\s+([A-Za-z]\w*)/g)) {
162
+ ctx.components.push(m[1]);
163
+ }
164
+ }
165
+ function extractJsCallables(text, ctx) {
166
+ for (const m of text.matchAll(/\bexport\s+(?:default\s+)?(?:async\s+)?function\s+([A-Za-z]\w*)/g)) {
167
+ pushNamed(ctx, m[1]);
168
+ }
169
+ for (const m of text.matchAll(/\bexport\s+const\s+([A-Za-z]\w*)\s*(?::[^=]+)?\s*=/g)) {
170
+ pushNamed(ctx, m[1]);
171
+ }
172
+ }
173
+ function extractJsRoutes(text, ctx) {
174
+ const routeRe = /\b(?:router|app)\.(get|post|put|patch|delete)\s*\(\s*['"]([^'"]+)['"]/gi;
175
+ for (const m of text.matchAll(routeRe)) {
176
+ ctx.apiRoutes.push(`${m[1].toUpperCase()} ${m[2]}`);
177
+ }
178
+ }
179
+ /** PascalCase → component; else function (matches the Python heuristic). */
180
+ function pushNamed(ctx, name) {
181
+ if (name[0] === name[0].toUpperCase() && /[A-Z]/.test(name[0])) {
182
+ ctx.components.push(name);
183
+ }
184
+ else {
185
+ ctx.functions.push(name);
186
+ }
187
+ }
188
+ function dedupCap(items) {
189
+ const seen = new Set();
190
+ const result = [];
191
+ for (const item of items) {
192
+ if (!seen.has(item)) {
193
+ seen.add(item);
194
+ result.push(item);
195
+ }
196
+ if (result.length >= MAX_ITEMS)
197
+ break;
198
+ }
199
+ return result;
200
+ }
201
+ function cmp(a, b) {
202
+ return a < b ? -1 : a > b ? 1 : 0;
203
+ }
204
+ function safeRead(path) {
205
+ try {
206
+ return readFileSync(path, 'utf-8');
207
+ }
208
+ catch {
209
+ return null;
210
+ }
211
+ }
212
+ //# sourceMappingURL=domain-scanner.js.map