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,68 @@
1
+ /**
2
+ * Flakiness and regression detection on top of history query results.
3
+ *
4
+ * Faithful TS port of `agent/history/detector.py`. Pure functions — no I/O.
5
+ * Input is the timeline output from the history store.
6
+ */
7
+ export var FlakeTrend;
8
+ (function (FlakeTrend) {
9
+ FlakeTrend["Rising"] = "rising";
10
+ FlakeTrend["Falling"] = "falling";
11
+ FlakeTrend["Stable"] = "stable";
12
+ })(FlakeTrend || (FlakeTrend = {}));
13
+ const TREND_THRESHOLD = 0.1;
14
+ /** Classify a time-ordered list of per-run flake rates (0.0–1.0). */
15
+ export function classifyFlakeTrend(rates) {
16
+ if (rates.length < 2)
17
+ return FlakeTrend.Stable;
18
+ const mid = Math.floor(rates.length / 2);
19
+ const firstHalf = rates.slice(0, mid);
20
+ const secondHalf = rates.slice(mid);
21
+ const mean = (xs) => xs.reduce((a, b) => a + b, 0) / xs.length;
22
+ const delta = mean(secondHalf) - mean(firstHalf);
23
+ if (delta >= TREND_THRESHOLD)
24
+ return FlakeTrend.Rising;
25
+ if (delta <= -TREND_THRESHOLD)
26
+ return FlakeTrend.Falling;
27
+ return FlakeTrend.Stable;
28
+ }
29
+ const BAD_STATUSES = new Set(['failed', 'flaky']);
30
+ /**
31
+ * Detect whether a test regressed: green for `minGreen` runs, then failing for
32
+ * the last `recentFailures` runs.
33
+ */
34
+ export function detectRegressions(timeline, minGreen = 5, recentFailures = 3) {
35
+ const none = {
36
+ is_regression: false,
37
+ green_streak: 0,
38
+ first_failure_commit: null,
39
+ };
40
+ if (timeline.length === 0)
41
+ return none;
42
+ const tail = timeline.length >= recentFailures ? timeline.slice(-recentFailures) : [];
43
+ if (tail.length < recentFailures)
44
+ return none;
45
+ if (!tail.every((r) => BAD_STATUSES.has(r.status)))
46
+ return none;
47
+ const firstFailIdx = timeline.length - recentFailures;
48
+ let streak = 0;
49
+ for (let i = firstFailIdx - 1; i >= 0; i--) {
50
+ if (timeline[i].status === 'passed')
51
+ streak++;
52
+ else
53
+ break;
54
+ }
55
+ if (streak < minGreen) {
56
+ return {
57
+ is_regression: false,
58
+ green_streak: streak,
59
+ first_failure_commit: null,
60
+ };
61
+ }
62
+ return {
63
+ is_regression: true,
64
+ green_streak: streak,
65
+ first_failure_commit: timeline[firstFailIdx].commit_sha ?? null,
66
+ };
67
+ }
68
+ //# sourceMappingURL=detector.js.map
@@ -0,0 +1,177 @@
1
+ /**
2
+ * Local NDJSON-backed history store — the TS side of the TS↔Python seam.
3
+ *
4
+ * Reads `history-v2.jsonl` (one JSON RunRecord per line) written by the Python
5
+ * `LocalHistoryStore`. Faithful port of the query_* semantics from
6
+ * `agent/history/local_store.py`.
7
+ *
8
+ * Deviation from the Python reader (deliberate): where Python's `_read_all`
9
+ * swallows any parse error and returns `[]`, this reader throws on malformed
10
+ * JSON and on an explicit unrecognized `schema_version`, so a corrupt or
11
+ * future-version history fails loudly rather than silently analysing nothing.
12
+ */
13
+ import { appendFileSync, mkdirSync, readFileSync } from 'node:fs';
14
+ import { dirname } from 'node:path';
15
+ import { def } from '../util/coalesce.js';
16
+ import { round1 } from '../util/round.js';
17
+ import { SCHEMA_VERSION } from './record.js';
18
+ import { serializeLocalRecord } from './schema.js';
19
+ export class NdjsonHistoryStore {
20
+ path;
21
+ constructor(path) {
22
+ this.path = path;
23
+ }
24
+ readAll() {
25
+ let text;
26
+ try {
27
+ text = readFileSync(this.path, 'utf-8');
28
+ }
29
+ catch (err) {
30
+ // Missing file → empty history (matches Python's exists() guard).
31
+ if (err.code === 'ENOENT')
32
+ return [];
33
+ throw err;
34
+ }
35
+ const records = [];
36
+ for (const raw of text.split('\n')) {
37
+ const line = raw.trim();
38
+ if (!line)
39
+ continue;
40
+ const record = JSON.parse(line);
41
+ const version = record.schema_version;
42
+ if (version !== undefined && version !== SCHEMA_VERSION) {
43
+ throw new Error(`Unsupported history schema_version ${version} (expected ${SCHEMA_VERSION})`);
44
+ }
45
+ records.push(record);
46
+ }
47
+ return records;
48
+ }
49
+ /**
50
+ * Append a run + its results as one NDJSON line. Idempotent: a run whose
51
+ * `run_id` is already present is silently skipped (matches Python
52
+ * `LocalHistoryStore.push_run`).
53
+ */
54
+ pushRun(run, results) {
55
+ const existingIds = new Set(this.readAll().map((r) => r.run_id));
56
+ if (existingIds.has(run.run_id))
57
+ return;
58
+ const record = serializeLocalRecord(run, results);
59
+ mkdirSync(dirname(this.path), { recursive: true });
60
+ appendFileSync(this.path, JSON.stringify(record) + '\n', 'utf-8');
61
+ }
62
+ queryFlaky(window, suite, minRate) {
63
+ let records = this.readAll();
64
+ if (suite)
65
+ records = records.filter((r) => r.suite === suite);
66
+ records = records.slice(-window);
67
+ const counts = new Map();
68
+ for (const record of records) {
69
+ for (const t of def(record.tests, [])) {
70
+ let c = counts.get(t.test_name);
71
+ if (!c) {
72
+ c = newFlakyCounter(record, t);
73
+ counts.set(t.test_name, c);
74
+ }
75
+ c.total_runs += 1;
76
+ applyStatus(c, def(t.status, ''));
77
+ c.last_seen_run = record.run_id;
78
+ }
79
+ }
80
+ const results = [];
81
+ for (const c of counts.values()) {
82
+ if (c.total_runs === 0)
83
+ continue;
84
+ const rate = round1((c.flake_count / c.total_runs) * 100);
85
+ if (rate >= minRate)
86
+ results.push({ ...c, flake_rate_pct: rate });
87
+ }
88
+ return results.sort((a, b) => b.flake_rate_pct - a.flake_rate_pct);
89
+ }
90
+ queryTimeline(testName) {
91
+ const timeline = [];
92
+ for (const record of this.readAll()) {
93
+ for (const t of def(record.tests, [])) {
94
+ if (t.test_name === testName)
95
+ timeline.push(toTimelineEntry(record, t));
96
+ }
97
+ }
98
+ return timeline.sort((a, b) => cmp(a.timestamp, b.timestamp));
99
+ }
100
+ querySummary(suite, runs) {
101
+ const suiteRecords = this.readAll().filter((r) => r.suite === suite);
102
+ const recent = runs > 0 ? suiteRecords.slice(-runs) : suiteRecords;
103
+ if (recent.length === 0) {
104
+ return { suite, total_runs: 0, avg_pass_rate: 0.0 };
105
+ }
106
+ return {
107
+ suite,
108
+ total_runs: recent.length,
109
+ avg_pass_rate: avgPassRate(recent),
110
+ runs: recent.map(toSummaryRun),
111
+ };
112
+ }
113
+ }
114
+ // ---------------------------------------------------------------------------
115
+ // Row-mapping helpers — extracted so the query methods stay under the arch
116
+ // complexity threshold. Each isolates one dense `??`-fallback cluster.
117
+ // ---------------------------------------------------------------------------
118
+ function newFlakyCounter(record, t) {
119
+ return {
120
+ test_name: t.test_name,
121
+ test_file: def(t.test_file, ''),
122
+ suite: def(t.suite, def(record.suite, '')),
123
+ area: def(t.area, null),
124
+ flake_count: 0,
125
+ pass_count: 0,
126
+ fail_count: 0,
127
+ total_runs: 0,
128
+ last_seen_run: null,
129
+ };
130
+ }
131
+ function applyStatus(c, status) {
132
+ if (status === 'flaky')
133
+ c.flake_count += 1;
134
+ else if (status === 'passed')
135
+ c.pass_count += 1;
136
+ else if (status === 'failed')
137
+ c.fail_count += 1;
138
+ }
139
+ function toTimelineEntry(record, t) {
140
+ return {
141
+ run_id: record.run_id,
142
+ suite: def(record.suite, ''),
143
+ branch: def(record.branch, ''),
144
+ commit_sha: def(record.commit_sha, ''),
145
+ timestamp: def(record.timestamp, ''),
146
+ status: def(t.status, ''),
147
+ failure_category: def(t.failure_category, null),
148
+ error_text: def(t.error_text, null),
149
+ retry_count: def(t.retry_count, 0),
150
+ };
151
+ }
152
+ function toSummaryRun(r) {
153
+ return {
154
+ run_id: r.run_id,
155
+ branch: def(r.branch, ''),
156
+ timestamp: def(r.timestamp, ''),
157
+ passed: def(r.passed, 0),
158
+ failed: def(r.failed, 0),
159
+ flaky: def(r.flaky, 0),
160
+ total: def(r.total, 0),
161
+ };
162
+ }
163
+ function avgPassRate(recent) {
164
+ const rates = [];
165
+ for (const r of recent) {
166
+ const total = def(r.total, 0);
167
+ if (total > 0)
168
+ rates.push((def(r.passed, 0) / total) * 100);
169
+ }
170
+ if (rates.length === 0)
171
+ return 0.0;
172
+ return round1(rates.reduce((a, b) => a + b, 0) / rates.length);
173
+ }
174
+ function cmp(a, b) {
175
+ return a < b ? -1 : a > b ? 1 : 0;
176
+ }
177
+ //# sourceMappingURL=ndjson-store.js.map
@@ -0,0 +1,14 @@
1
+ /**
2
+ * TypeScript types for the v2 run-history record schema.
3
+ *
4
+ * Mirrors `agent/history/schema.py` (RunRecord + TestResult) as persisted, one
5
+ * JSON object per line, in `test-results/reports/history-v2.jsonl`.
6
+ *
7
+ * The on-disk records written by the Python engine do NOT carry a per-record
8
+ * version field (the "v2" lives in the filename). The reader therefore treats a
9
+ * missing `schema_version` as the current version, but throws on an explicit
10
+ * unrecognized one — a forward-compat guard, exercised by the store tests.
11
+ */
12
+ /** The schema version this reader understands. */
13
+ export const SCHEMA_VERSION = 2;
14
+ //# sourceMappingURL=record.js.map
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Write-side schema for run-history records.
3
+ *
4
+ * Faithful TS port of `agent/history/schema.py`. `record.ts` holds the *read*
5
+ * shapes (loose, as parsed from disk); this module holds the *write* inputs
6
+ * (the dataclass field sets) and the serializers that turn them into the exact
7
+ * dict shapes Python's `asdict()` produces — so a record written by TS is read
8
+ * identically by the Python `LocalHistoryStore` (the write-seam proof).
9
+ */
10
+ import { def } from '../util/coalesce.js';
11
+ /** `{suite}-{commit[:8]}-{epoch}` — identical to Python `make_run_id`. */
12
+ export function makeRunId(suite, commitSha, timestampEpoch) {
13
+ return `${suite}-${commitSha.slice(0, 8)}-${timestampEpoch}`;
14
+ }
15
+ /** `asdict(run)` — every RunRecord field, optionals defaulted to null. */
16
+ export function serializeRun(run) {
17
+ return {
18
+ run_id: run.run_id,
19
+ suite: run.suite,
20
+ repo: run.repo,
21
+ branch: run.branch,
22
+ commit_sha: run.commit_sha,
23
+ timestamp: run.timestamp,
24
+ total: run.total,
25
+ passed: run.passed,
26
+ failed: run.failed,
27
+ flaky: run.flaky,
28
+ skipped: run.skipped,
29
+ commit_message: def(run.commit_message, null),
30
+ env: def(run.env, null),
31
+ base_url: def(run.base_url, null),
32
+ duration_ms: def(run.duration_ms, null),
33
+ };
34
+ }
35
+ /** `asdict(t)` — every TestResult field, optionals/defaults applied. */
36
+ export function serializeTestResult(t) {
37
+ return {
38
+ run_id: t.run_id,
39
+ suite: t.suite,
40
+ repo: t.repo,
41
+ test_name: t.test_name,
42
+ test_file: t.test_file,
43
+ status: t.status,
44
+ area: def(t.area, null),
45
+ failure_category: def(t.failure_category, null),
46
+ error_text: def(t.error_text, null),
47
+ retry_count: def(t.retry_count, 0),
48
+ duration_ms: def(t.duration_ms, null),
49
+ tags: def(t.tags, []),
50
+ };
51
+ }
52
+ /**
53
+ * The nested NDJSON line shape written by the local store: a serialized run
54
+ * with its `tests` embedded (matches Python `LocalHistoryStore.push_run`).
55
+ */
56
+ export function serializeLocalRecord(run, results) {
57
+ return { ...serializeRun(run), tests: results.map(serializeTestResult) };
58
+ }
59
+ //# sourceMappingURL=schema.js.map
@@ -0,0 +1,47 @@
1
+ /**
2
+ * History store factory + the async store contract.
3
+ *
4
+ * Port of `agent/history/store.py` (`HistoryStore` ABC + `make_store`).
5
+ *
6
+ * DISCOVERED BOUNDARY CONSTRAINT (differs from Python): the Python stores are
7
+ * synchronous because `supabase-py`'s `.execute()` is blocking. The JS SDK
8
+ * (`@supabase/supabase-js`) is Promise-based, so a remote store cannot expose
9
+ * synchronous queries. This module therefore defines an ASYNC store contract
10
+ * (`AsyncHistoryStore`) that `makeStore` returns. The analysis pilot's
11
+ * synchronous `NdjsonHistoryStore` is left untouched (the analysis engine reads
12
+ * it directly); here it is wrapped in a thin async adapter so both backends
13
+ * present one uniform async surface.
14
+ */
15
+ import { NdjsonHistoryStore, } from './ndjson-store.js';
16
+ import { SupabaseHistoryStore } from './supabase-store.js';
17
+ const DEFAULT_NDJSON_PATH = 'test-results/reports/history-v2.jsonl';
18
+ /** Async adapter over the synchronous local NDJSON store. */
19
+ export class LocalAsyncAdapter {
20
+ inner;
21
+ constructor(inner) {
22
+ this.inner = inner;
23
+ }
24
+ async pushRun(run, results) {
25
+ this.inner.pushRun(run, results);
26
+ }
27
+ async queryFlaky(window, suite, minRate) {
28
+ return this.inner.queryFlaky(window, suite, minRate);
29
+ }
30
+ async queryTimeline(testName) {
31
+ return this.inner.queryTimeline(testName);
32
+ }
33
+ async querySummary(suite, runs) {
34
+ return this.inner.querySummary(suite, runs);
35
+ }
36
+ }
37
+ /**
38
+ * Return a Supabase-backed store when a db-url is configured (arg then
39
+ * `CANARY_HISTORY_DB_URL`), else a local NDJSON store. Mirrors `make_store`.
40
+ */
41
+ export function makeStore(dbUrl, ndjsonPath) {
42
+ const resolvedUrl = dbUrl ?? process.env.CANARY_HISTORY_DB_URL;
43
+ if (resolvedUrl)
44
+ return new SupabaseHistoryStore(resolvedUrl);
45
+ return new LocalAsyncAdapter(new NdjsonHistoryStore(ndjsonPath ?? DEFAULT_NDJSON_PATH));
46
+ }
47
+ //# sourceMappingURL=store.js.map
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Supabase-backed history store.
3
+ *
4
+ * Port of `agent/history/supabase_store.py`, using `@supabase/supabase-js`.
5
+ * The JS SDK is Promise-based, so every method is async (see the boundary note
6
+ * in `store.ts`). The client is injectable so tests can mock it — no live
7
+ * network is ever required.
8
+ */
9
+ import { createClient } from '@supabase/supabase-js';
10
+ import { def } from '../util/coalesce.js';
11
+ import { round1 } from '../util/round.js';
12
+ import { serializeRun, serializeTestResult, } from './schema.js';
13
+ const ERROR_TEXT_MAX = 2000;
14
+ /**
15
+ * Resolve the Supabase project URL. A plain `https://…` url passes through; a
16
+ * `postgresql+asyncpg://user:pass@host/db` url yields `https://<host>`. Never
17
+ * returns the raw connection string (it embeds credentials). Pure + exported
18
+ * for direct parity testing against Python `_parse_project_url`.
19
+ */
20
+ export function parseProjectUrl(dbUrl) {
21
+ if (dbUrl.startsWith('https://'))
22
+ return dbUrl;
23
+ try {
24
+ const host = new URL(dbUrl).hostname;
25
+ return `https://${host}`;
26
+ }
27
+ catch {
28
+ return '<redacted-unparseable-url>';
29
+ }
30
+ }
31
+ function toResultRow(t) {
32
+ const row = serializeTestResult(t);
33
+ const err = row.error_text;
34
+ if (typeof err === 'string' && err.length > ERROR_TEXT_MAX) {
35
+ row.error_text = err.slice(0, ERROR_TEXT_MAX);
36
+ }
37
+ return row;
38
+ }
39
+ function flattenTimelineRow(row) {
40
+ const runInfo = def(row.canary_runs, {});
41
+ return {
42
+ run_id: String(def(row.run_id, '')),
43
+ suite: String(def(row.suite, '')),
44
+ branch: String(def(runInfo.branch, '')),
45
+ commit_sha: String(def(runInfo.commit_sha, '')),
46
+ timestamp: String(def(runInfo.timestamp, '')),
47
+ status: String(def(row.status, '')),
48
+ failure_category: def(row.failure_category, null) ?? null,
49
+ error_text: def(row.error_text, null) ?? null,
50
+ retry_count: Number(def(row.retry_count, 0)),
51
+ };
52
+ }
53
+ export class SupabaseHistoryStore {
54
+ client;
55
+ constructor(dbUrl, client) {
56
+ this.client =
57
+ client ??
58
+ createClient(parseProjectUrl(dbUrl), def(process.env.SUPABASE_ANON_KEY, ''));
59
+ }
60
+ async pushRun(run, results) {
61
+ await this.client.from('canary_runs').upsert(serializeRun(run));
62
+ if (results.length > 0) {
63
+ await this.client
64
+ .from('canary_test_results')
65
+ .upsert(results.map(toResultRow));
66
+ }
67
+ }
68
+ async queryFlaky(_window, suite, minRate) {
69
+ let query = this.client
70
+ .from('canary_flake_summary')
71
+ .select('*')
72
+ .gte('flake_rate_pct', minRate)
73
+ .order('flake_rate_pct', { ascending: false });
74
+ if (suite)
75
+ query = query.eq('suite', suite);
76
+ const { data } = await query;
77
+ return def(data, []);
78
+ }
79
+ async queryTimeline(testName) {
80
+ const { data } = await this.client
81
+ .from('canary_test_results')
82
+ .select('run_id, suite, status, failure_category, error_text, retry_count, ' +
83
+ 'canary_runs!inner(branch, commit_sha, timestamp)')
84
+ .eq('test_name', testName)
85
+ .order('canary_runs(timestamp)');
86
+ return def(data, []).map(flattenTimelineRow);
87
+ }
88
+ async querySummary(suite, runs) {
89
+ const { data } = await this.client
90
+ .from('canary_runs')
91
+ .select('run_id, branch, timestamp, passed, failed, flaky, total')
92
+ .eq('suite', suite)
93
+ .order('timestamp', { ascending: false })
94
+ .limit(runs);
95
+ const recent = [...def(data, [])].reverse();
96
+ if (recent.length === 0) {
97
+ return { suite, total_runs: 0, avg_pass_rate: 0.0 };
98
+ }
99
+ const rates = recent
100
+ .filter((r) => Number(def(r.total, 0)) > 0)
101
+ .map((r) => (Number(r.passed) / Number(r.total)) * 100);
102
+ const avg = rates.length > 0
103
+ ? round1(rates.reduce((a, b) => a + b, 0) / rates.length)
104
+ : 0.0;
105
+ return {
106
+ suite,
107
+ total_runs: recent.length,
108
+ avg_pass_rate: avg,
109
+ runs: recent,
110
+ };
111
+ }
112
+ }
113
+ //# sourceMappingURL=supabase-store.js.map
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Shared injectable dependencies + glyph constants for the main `canary` CLI
3
+ * and its inline sub-apps (skills / workflow / company-knowledge).
4
+ *
5
+ * Every command reaches the outside world through {@link MainDeps} so tests run
6
+ * with capturing sinks, a fixed env/cwd/home, and fake class factories -- the
7
+ * commander analog of the Python CLI tests' `mock.patch(...)` seams. The
8
+ * production {@link defaultMainDeps} wires process-backed real classes.
9
+ *
10
+ * Output glyphs are declared as `\u{...}` escapes (ASCII-source rule) and
11
+ * emitted verbatim; picocolors strips its color on a non-TTY sink so the plain
12
+ * text matches rich's markup stripping byte-for-byte (see `guardian/cli.ts`).
13
+ */
14
+ import { spawnSync } from 'node:child_process';
15
+ import { readFileSync } from 'node:fs';
16
+ import { homedir } from 'node:os';
17
+ import { TestClassifier } from './core/classifier.js';
18
+ import { CompanyKnowledge } from './core/company-knowledge.js';
19
+ import { CanaryTestExecutor } from './core/executor.js';
20
+ import { FrameworkRegistry } from './core/framework-registry.js';
21
+ import { HarnessMigrator } from './core/migrator.js';
22
+ import { PatternHealer } from './core/pattern-healer.js';
23
+ import { FrameworkRecommender } from './core/recommender.js';
24
+ import { Scaffolder } from './core/scaffolder.js';
25
+ import { SkillRegistry } from './core/skill-registry.js';
26
+ import { StaticLinter } from './core/static-linter.js';
27
+ import { TicketUpdater } from './core/ticket-updater.js';
28
+ import { WorkflowDiscovery } from './core/workflow-discovery.js';
29
+ // --- output glyphs (emitted verbatim; see module docstring) -------------------
30
+ export const CHECK_MARK = '\u{2705}'; // white heavy check mark
31
+ export const WARN = '\u{26a0}'; // warning sign
32
+ export const CROSS = '\u{2717}'; // ballot X
33
+ export const ROCKET = '\u{1f680}';
34
+ export const HAMMER = '\u{1f6e0}';
35
+ export const NEXT = '\u{23ed}\u{fe0f}'; // next-track button + VS16
36
+ export const REDX = '\u{274c}'; // cross mark
37
+ export const WRENCH = '\u{1f527}';
38
+ export const MAGNIFIER = '\u{1f50d}';
39
+ export const CHECK = '\u{2713}'; // light check mark
40
+ export const ARROW = '\u{2192}'; // rightwards arrow
41
+ export const EM_DASH = '\u{2014}';
42
+ export const ELLIPSIS = '\u{2026}';
43
+ /** A stdin-backed prompt: reads piped lines once, returns `def` when exhausted. */
44
+ function makeStdinPrompt() {
45
+ let lines = null;
46
+ let idx = 0;
47
+ return (_text, def) => {
48
+ if (lines === null) {
49
+ try {
50
+ lines = readFileSync(0, 'utf-8').split('\n');
51
+ }
52
+ catch {
53
+ lines = [];
54
+ }
55
+ }
56
+ const raw = idx < lines.length ? lines[idx++] : '';
57
+ return raw === '' ? def : raw;
58
+ };
59
+ }
60
+ /** Process-backed defaults for production. */
61
+ export function defaultMainDeps() {
62
+ return {
63
+ out: (s) => process.stdout.write(`${s}\n`),
64
+ err: (s) => process.stderr.write(`${s}\n`),
65
+ env: process.env,
66
+ cwd: () => process.cwd(),
67
+ home: () => homedir(),
68
+ // The npm bin injects the real package version; the bare factory default
69
+ // reports 'unknown' (Python's PackageNotFoundError fallback shape).
70
+ pkgVersion: () => 'unknown',
71
+ openBrowser: () => {
72
+ // Best-effort no-op default; a desktop launcher is wired by the bin.
73
+ },
74
+ runSubprocess: (cmd, args, opts = {}) => {
75
+ const res = spawnSync(cmd, args, {
76
+ encoding: 'utf-8',
77
+ maxBuffer: Infinity,
78
+ ...(opts.cwd ? { cwd: opts.cwd } : {}),
79
+ ...(opts.inherit ? { stdio: 'inherit' } : {}),
80
+ });
81
+ if (res.error)
82
+ return { status: null, stdout: '', stderr: '' };
83
+ return {
84
+ status: res.status,
85
+ stdout: res.stdout ?? '',
86
+ stderr: res.stderr ?? '',
87
+ };
88
+ },
89
+ prompt: makeStdinPrompt(),
90
+ pythonExe: () => 'python3',
91
+ makeClassifier: () => new TestClassifier(),
92
+ makeRecommender: () => new FrameworkRecommender(),
93
+ makeRegistry: () => new FrameworkRegistry(),
94
+ makeExecutor: () => new CanaryTestExecutor(),
95
+ makeScaffolder: () => new Scaffolder(),
96
+ makeMigrator: () => new HarnessMigrator(),
97
+ makeLinter: () => new StaticLinter(),
98
+ makeHealer: () => new PatternHealer(),
99
+ makeSkillRegistry: () => new SkillRegistry(),
100
+ makeWorkflowDiscovery: () => new WorkflowDiscovery(),
101
+ makeTicketUpdater: () => new TicketUpdater(),
102
+ loadCompanyKnowledge: (env) => CompanyKnowledge.load(undefined, env),
103
+ };
104
+ }
105
+ //# sourceMappingURL=main-deps.js.map