argusqa-os 9.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 (57) hide show
  1. package/.mcp.json +8 -0
  2. package/LICENSE +21 -0
  3. package/README.md +879 -0
  4. package/package.json +69 -0
  5. package/src/adapters/browser.js +82 -0
  6. package/src/argus.js +8 -0
  7. package/src/batch-runner.js +8 -0
  8. package/src/cli/init.js +314 -0
  9. package/src/config/schema.js +108 -0
  10. package/src/config/targets.js +309 -0
  11. package/src/domain/finding.js +25 -0
  12. package/src/mcp-server.js +156 -0
  13. package/src/orchestration/crawl-and-report.js +16 -0
  14. package/src/orchestration/dispatcher.js +263 -0
  15. package/src/orchestration/env-comparison.js +498 -0
  16. package/src/orchestration/orchestrator.js +1128 -0
  17. package/src/orchestration/report-processor.js +134 -0
  18. package/src/orchestration/slack-notifier.js +337 -0
  19. package/src/orchestration/watch-mode.js +316 -0
  20. package/src/registry.js +18 -0
  21. package/src/server/index.js +94 -0
  22. package/src/server/interaction-handler.js +126 -0
  23. package/src/server/slash-command-handler.js +185 -0
  24. package/src/utils/api-frequency.js +128 -0
  25. package/src/utils/baseline-manager.js +255 -0
  26. package/src/utils/codebase-analyzer.js +299 -0
  27. package/src/utils/content-analyzer.js +155 -0
  28. package/src/utils/contract-validator.js +178 -0
  29. package/src/utils/css-analyzer.js +407 -0
  30. package/src/utils/diff.js +189 -0
  31. package/src/utils/flakiness-detector.js +82 -0
  32. package/src/utils/flow-runner.js +572 -0
  33. package/src/utils/github-reporter.js +310 -0
  34. package/src/utils/hover-analyzer.js +214 -0
  35. package/src/utils/html-reporter.js +301 -0
  36. package/src/utils/issues-analyzer.js +171 -0
  37. package/src/utils/keyboard-analyzer.js +141 -0
  38. package/src/utils/lighthouse-checker.js +120 -0
  39. package/src/utils/logger.js +39 -0
  40. package/src/utils/login-orchestrator.js +99 -0
  41. package/src/utils/mcp-client.js +264 -0
  42. package/src/utils/mcp-parsers.js +57 -0
  43. package/src/utils/memory-analyzer.js +270 -0
  44. package/src/utils/network-timing-analyzer.js +76 -0
  45. package/src/utils/parallel-crawler.js +28 -0
  46. package/src/utils/responsive-analyzer.js +253 -0
  47. package/src/utils/retry.js +36 -0
  48. package/src/utils/route-discoverer.js +306 -0
  49. package/src/utils/security-analyzer.js +302 -0
  50. package/src/utils/seo-analyzer.js +164 -0
  51. package/src/utils/session-manager.js +12 -0
  52. package/src/utils/session-persistence.js +214 -0
  53. package/src/utils/severity-overrides.js +91 -0
  54. package/src/utils/slack-guard.js +18 -0
  55. package/src/utils/slug.js +8 -0
  56. package/src/utils/snapshot-analyzer.js +330 -0
  57. package/src/utils/telemetry.js +190 -0
@@ -0,0 +1,263 @@
1
+ /**
2
+ * Argus Report Dispatcher (v9.3.0)
3
+ *
4
+ * Dispatches a completed report to Slack, GitHub, and/or HTML.
5
+ * Extracted from crawl-and-report.js god object.
6
+ */
7
+
8
+ import path from 'path';
9
+ import { execFile } from 'child_process';
10
+ import { childLogger } from '../utils/logger.js';
11
+ import { startSpan } from '../utils/telemetry.js';
12
+ import { postBugReport } from './slack-notifier.js';
13
+ import { isSlackConfigured } from '../utils/slack-guard.js';
14
+ import { isGitHubConfigured, reportToGitHub } from '../utils/github-reporter.js';
15
+ import { generateHtmlReport } from '../utils/html-reporter.js';
16
+
17
+ const logger = childLogger('dispatcher');
18
+
19
+ // ── Helpers ───────────────────────────────────────────────────────────────────
20
+
21
+ /**
22
+ * Open a local file in the OS default browser (best-effort, skipped in CI).
23
+ */
24
+ function openInBrowser(filePath) {
25
+ if (process.env.CI) return;
26
+ try {
27
+ const abs = path.resolve(filePath);
28
+ if (process.platform === 'win32') {
29
+ execFile('cmd', ['/c', 'start', '', abs], () => {});
30
+ } else if (process.platform === 'darwin') {
31
+ execFile('open', [abs], () => {});
32
+ } else {
33
+ execFile('xdg-open', [abs], () => {});
34
+ }
35
+ } catch {
36
+ // no display available — skip silently
37
+ }
38
+ }
39
+
40
+ /**
41
+ * Safely extract the display message from any finding object.
42
+ */
43
+ function errorText(e) {
44
+ return e.message
45
+ ?? e.description
46
+ ?? (e.requestUrl ? `HTTP ${e.status ?? '?'} — ${e.method ?? 'GET'} ${e.requestUrl}` : null)
47
+ ?? `${e.type ?? 'unknown error'}`;
48
+ }
49
+
50
+ // ── Slack Dispatch ────────────────────────────────────────────────────────────
51
+
52
+ /**
53
+ * Send Slack notifications for bugs found in the report.
54
+ *
55
+ * Criticals → one message per route with screenshot attached
56
+ * Warnings → one message per route (grouped)
57
+ * Info → single digest message summarising all routes
58
+ */
59
+ async function dispatchToSlack(report, diff) {
60
+ const { summary } = report;
61
+
62
+ // ── Criticals: one message per affected route ─────────────────────────────
63
+ for (const routeResult of report.routes) {
64
+ const criticals = routeResult.errors.filter(e => e.severity === 'critical' && e.isNew === true);
65
+ if (criticals.length === 0) continue;
66
+
67
+ const description = criticals
68
+ .map(e => `• *[${e.type}]* ${errorText(e)}`)
69
+ .join('\n');
70
+
71
+ await postBugReport({
72
+ severity: 'critical',
73
+ title: `${criticals.length} critical issue(s) on ${routeResult.route}`,
74
+ description,
75
+ url: routeResult.url,
76
+ screenshotPath: routeResult.screenshot,
77
+ details: { route: routeResult.route, errors: criticals },
78
+ }).catch(err => logger.warn(`[ARGUS] Slack: critical report failed for ${routeResult.route}: ${err.message}`));
79
+ }
80
+
81
+ // ── Warnings: one message per affected route ──────────────────────────────
82
+ for (const routeResult of report.routes) {
83
+ const warnings = routeResult.errors.filter(e => e.severity === 'warning' && e.isNew === true);
84
+ if (warnings.length === 0) continue;
85
+
86
+ const description = warnings
87
+ .map(e => `• *[${e.type}]* ${errorText(e)}`)
88
+ .join('\n');
89
+
90
+ await postBugReport({
91
+ severity: 'warning',
92
+ title: `${warnings.length} warning(s) on ${routeResult.route}`,
93
+ description,
94
+ url: routeResult.url,
95
+ screenshotPath: routeResult.screenshot,
96
+ details: { route: routeResult.route, errors: warnings },
97
+ }).catch(err => logger.warn(`[ARGUS] Slack: warning report failed for ${routeResult.route}: ${err.message}`));
98
+ }
99
+
100
+ // ── Responsive screenshots: mobile view for routes with responsive findings
101
+ for (const routeResult of report.routes) {
102
+ const responsiveErrors = routeResult.errors.filter(e =>
103
+ e.type === 'responsive_overflow' || e.type === 'responsive_small_touch_target'
104
+ );
105
+ const mobileShot = routeResult.responsiveScreenshots?.['375x812'];
106
+ if (responsiveErrors.length === 0 || !mobileShot) continue;
107
+
108
+ const description = responsiveErrors.map(e => `• *[${e.type}]* ${errorText(e)}`).join('\n');
109
+ await postBugReport({
110
+ severity: 'warning',
111
+ title: `Responsive layout issues — ${routeResult.route} (mobile screenshot)`,
112
+ description: `${description}\n\n_375px mobile view attached. Full grid: ${
113
+ Object.keys(routeResult.responsiveScreenshots ?? {}).join(', ')
114
+ }_`,
115
+ url: routeResult.url,
116
+ screenshotPath: mobileShot,
117
+ details: { responsiveFindings: responsiveErrors },
118
+ }).catch(err => logger.warn(`[ARGUS] Slack: responsive report failed for ${routeResult.route}: ${err.message}`));
119
+ }
120
+
121
+ // ── Flow failures ─────────────────────────────────────────────────────────
122
+ for (const flowResult of (report.flows ?? [])) {
123
+ const flowCriticals = (flowResult.findings ?? []).filter(f => f.severity === 'critical' && f.isNew === true);
124
+ if (flowCriticals.length > 0) {
125
+ await postBugReport({
126
+ severity: 'critical',
127
+ title: `Flow "${flowResult.flowName}" failed — ${flowCriticals.length} critical issue(s)`,
128
+ description: flowCriticals.map(f => `• *[${f.type}]* ${errorText(f)}`).join('\n'),
129
+ url: report.baseUrl,
130
+ screenshotPath: null,
131
+ details: { flow: flowResult.flowName, errors: flowCriticals },
132
+ }).catch(err => logger.warn(`[ARGUS] Slack: flow critical report failed for ${flowResult.flowName}: ${err.message}`));
133
+ }
134
+ const flowWarnings = (flowResult.findings ?? []).filter(f => f.severity === 'warning' && f.isNew === true);
135
+ if (flowWarnings.length > 0) {
136
+ await postBugReport({
137
+ severity: 'warning',
138
+ title: `Flow "${flowResult.flowName}" — ${flowWarnings.length} warning(s)`,
139
+ description: flowWarnings.map(f => `• *[${f.type}]* ${errorText(f)}`).join('\n'),
140
+ url: report.baseUrl,
141
+ screenshotPath: null,
142
+ details: { flow: flowResult.flowName, errors: flowWarnings },
143
+ }).catch(err => logger.warn(`[ARGUS] Slack: flow warning report failed for ${flowResult.flowName}: ${err.message}`));
144
+ }
145
+ }
146
+
147
+ // ── Codebase criticals + warnings ─────────────────────────────────────────
148
+ const cbCriticals = (report.codebase ?? []).filter(f => f.severity === 'critical' && f.isNew === true);
149
+ if (cbCriticals.length > 0) {
150
+ await postBugReport({
151
+ severity: 'critical',
152
+ title: `${cbCriticals.length} codebase critical(s) — ${report.baseUrl}`,
153
+ description: cbCriticals.map(f => `• *[${f.type}]* ${errorText(f)}`).join('\n'),
154
+ url: report.baseUrl,
155
+ screenshotPath: null,
156
+ details: { codebase: cbCriticals },
157
+ }).catch(err => logger.warn(`[ARGUS] Slack: codebase critical report failed: ${err.message}`));
158
+ }
159
+ const cbWarnings = (report.codebase ?? []).filter(f => f.severity === 'warning' && f.isNew === true);
160
+ if (cbWarnings.length > 0) {
161
+ await postBugReport({
162
+ severity: 'warning',
163
+ title: `${cbWarnings.length} codebase warning(s) — ${report.baseUrl}`,
164
+ description: cbWarnings.map(f => `• *[${f.type}]* ${errorText(f)}`).join('\n'),
165
+ url: report.baseUrl,
166
+ screenshotPath: null,
167
+ details: { codebase: cbWarnings },
168
+ }).catch(err => logger.warn(`[ARGUS] Slack: codebase warning report failed: ${err.message}`));
169
+ }
170
+
171
+ // ── Info digest: one summary message across all routes ────────────────────
172
+ const allInfos = report.routes.flatMap(r =>
173
+ r.errors.filter(e => e.severity === 'info' && e.isNew !== false).map(e => ({ ...e, routeName: r.route }))
174
+ );
175
+
176
+ const digestLines = [];
177
+ for (const routeResult of report.routes) {
178
+ const routeInfos = allInfos.filter(e => e.routeName === routeResult.route);
179
+ if (routeInfos.length === 0) continue;
180
+ digestLines.push(`*${routeResult.route}* (${routeResult.url})`);
181
+ for (const e of routeInfos) {
182
+ const flakyTag = e.flaky ? ' :zap: _flaky_' : '';
183
+ digestLines.push(` • [${e.type}]${flakyTag} ${errorText(e)}`);
184
+ }
185
+ }
186
+
187
+ for (const flowResult of (report.flows ?? [])) {
188
+ const flowInfos = (flowResult.findings ?? []).filter(e => e.severity === 'info');
189
+ if (flowInfos.length === 0) continue;
190
+ digestLines.push(`*Flow: ${flowResult.flowName}* (${flowResult.stepsCompleted}/${flowResult.totalSteps} steps — ${flowResult.status})`);
191
+ for (const e of flowInfos) {
192
+ digestLines.push(` • [${e.type}] ${errorText(e)}`);
193
+ }
194
+ }
195
+
196
+ const cbInfos = (report.codebase ?? []).filter(f => f.severity === 'info');
197
+ if (cbInfos.length > 0) {
198
+ digestLines.push('*Codebase (C1)*');
199
+ for (const f of cbInfos) digestLines.push(` • [${f.type}] ${errorText(f)}`);
200
+ }
201
+
202
+ const allFlowInfos = (report.flows ?? []).flatMap(f => (f.findings ?? []).filter(e => e.severity === 'info'));
203
+
204
+ if (allInfos.length > 0 || allFlowInfos.length > 0 || cbInfos.length > 0) {
205
+ const runDate = new Date(report.generatedAt).toLocaleString();
206
+ const trendLine = diff
207
+ ? diff.isFirstRun
208
+ ? '_Baseline established — future runs will show new / resolved counts._'
209
+ : `:chart_with_upwards_trend: ${diff.newCount} new :white_check_mark: ${diff.resolvedCount} resolved since last baseline` +
210
+ ((diff.flowNewCount ?? 0) > 0 || (diff.flowResolvedCount ?? 0) > 0
211
+ ? ` _(flows: ${diff.flowNewCount ?? 0} new, ${diff.flowResolvedCount ?? 0} resolved)_`
212
+ : '')
213
+ : '';
214
+
215
+ await postBugReport({
216
+ severity: 'info',
217
+ title: `Argus crawl digest — ${report.baseUrl} (${runDate})`,
218
+ description:
219
+ `Summary: ${summary.total} findings across ${report.routes.length} routes\n` +
220
+ `:red_circle: ${summary.critical} critical :large_yellow_circle: ${summary.warning} warnings :large_blue_circle: ${summary.info} info\n` +
221
+ (trendLine ? trendLine + '\n' : '') + '\n' +
222
+ (digestLines.length > 0 ? digestLines.join('\n') : '_No info-level findings._'),
223
+ url: report.baseUrl,
224
+ screenshotPath: null,
225
+ details: { summary, infos: allInfos },
226
+ }).catch(err => logger.warn(`[ARGUS] Slack: info digest report failed: ${err.message}`));
227
+ }
228
+ }
229
+
230
+ // ── Public API ────────────────────────────────────────────────────────────────
231
+
232
+ /**
233
+ * Dispatch the completed report to all configured channels (Slack + GitHub + HTML).
234
+ * Each channel is best-effort — a failure in one does not block the others.
235
+ *
236
+ * @param {object} report - Completed report object (with baseline diff applied)
237
+ * @param {object} diff - Baseline diff returned by processReport
238
+ * @param {string} reportPath - Path to the written JSON report (for HTML generation)
239
+ */
240
+ export async function dispatchAll(report, diff, reportPath) {
241
+ return startSpan('argus.dispatch', { baseUrl: report?.baseUrl ?? '' }, async () => {
242
+ if (isSlackConfigured()) {
243
+ try {
244
+ await startSpan('argus.dispatch', { channel: 'slack' }, () => dispatchToSlack(report, diff));
245
+ } catch (err) {
246
+ logger.error(`[ARGUS] Slack dispatch failed: ${err.message}`);
247
+ }
248
+ } else {
249
+ logger.info('\n[ARGUS] No Slack credentials — generating HTML report...');
250
+ const htmlPath = await startSpan('argus.dispatch', { channel: 'html' }, () => generateHtmlReport(reportPath));
251
+ logger.info(`[ARGUS] ✓ Open in browser: ${htmlPath}\n`);
252
+ openInBrowser(htmlPath);
253
+ }
254
+
255
+ if (isGitHubConfigured()) {
256
+ try {
257
+ await startSpan('argus.dispatch', { channel: 'github' }, () => reportToGitHub(report, diff));
258
+ } catch (err) {
259
+ logger.error(`[ARGUS] GitHub reporting failed: ${err.message}`);
260
+ }
261
+ }
262
+ }); // end argus.dispatch span
263
+ }