argusqa-os 9.5.0 → 9.5.3

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.
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Argus Orchestrator (v9.3.0)
2
+ * Argus Orchestrator
3
3
  *
4
4
  * Per-route crawl loop: cheap×2 flakiness pass + expensive×1 pass.
5
5
  * Extracted from crawl-and-report.js god object.
@@ -15,7 +15,6 @@ import 'dotenv/config';
15
15
  import { routes, config, auth, flows, apiContracts, severityOverrides, codebase, autoDiscover, thresholds } from '../config/targets.js';
16
16
  import { discoverRoutes } from '../utils/route-discoverer.js';
17
17
  import { analyzeCodebase, detectDeadRoutes, INTERNAL_LINKS_SCRIPT } from '../utils/codebase-analyzer.js';
18
- import { CSS_ANALYSIS_SCRIPT, parseCssAnalysisResult } from '../utils/css-analyzer.js';
19
18
  import { SEO_ANALYSIS_SCRIPT, parseSeoAnalysisResult } from '../utils/seo-analyzer.js';
20
19
  import { SECURITY_ANALYSIS_SCRIPT, parseSecurityAnalysisResult, analyzeSecurityConsole, analyzeSecurityNetwork } from '../utils/security-analyzer.js';
21
20
  import { CONTENT_ANALYSIS_SCRIPT, parseContentAnalysisResult } from '../utils/content-analyzer.js';
@@ -26,6 +25,7 @@ import { analyzeApiFrequency } from '.
26
25
  import { slugify } from '../utils/slug.js';
27
26
  import { unwrapEval, createMcpClient } from '../utils/mcp-client.js';
28
27
  import { CdpBrowserAdapter } from '../adapters/browser.js';
28
+ import { getFigmaFrame } from '../adapters/figma.js';
29
29
  import { chunkArray } from '../utils/parallel-crawler.js';
30
30
  import { validateApiContracts } from '../utils/contract-validator.js';
31
31
  import { checkLighthouse } from '../utils/lighthouse-checker.js';
@@ -33,13 +33,16 @@ import { parseIssues } from '.
33
33
  import { parseNetworkTiming } from '../utils/network-timing-analyzer.js';
34
34
 
35
35
  // Side-effect imports: each module calls registerExpensive() at load time.
36
- // lighthouse-checker.js also self-registers via its direct named import above (line 31).
36
+ // lighthouse-checker.js also self-registers via its direct named import above.
37
37
  // Order below controls iteration order in crawlAndAnalyzeRoute — must match original call order.
38
+ import '../utils/css-analyzer.js';
38
39
  import '../utils/responsive-analyzer.js';
39
40
  import '../utils/memory-analyzer.js';
40
41
  import '../utils/hover-analyzer.js';
41
42
  import '../utils/snapshot-analyzer.js';
42
43
  import '../utils/keyboard-analyzer.js';
44
+ import '../utils/theme-analyzer.js';
45
+ import '../utils/design-fidelity-analyzer.js';
43
46
 
44
47
  import { getExpensive } from '../registry.js';
45
48
  import { deduplicateFindings as deduplicateErrors } from './report-processor.js';
@@ -54,6 +57,7 @@ const logger = childLogger('orchestrator');
54
57
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
55
58
  const BASE_URL = process.env.TARGET_DEV_URL ?? 'http://localhost:3000';
56
59
  const OUTPUT_DIR = path.resolve(__dirname, '../../', config.outputDir);
60
+ const LIGHTHOUSE_TIMEOUT_MS = parseInt(process.env.ARGUS_LIGHTHOUSE_TIMEOUT ?? '120000', 10);
57
61
 
58
62
  // Thresholds for perf budgets and network analysis are centralized in targets.js.
59
63
 
@@ -362,7 +366,6 @@ function analyzeNetworkPerformance(perfEntries, pageUrl) {
362
366
 
363
367
  async function checkPerformanceBudgets(browser, url) {
364
368
  const violations = [];
365
- const LIGHTHOUSE_TIMEOUT_MS = parseInt(process.env.ARGUS_LIGHTHOUSE_TIMEOUT ?? '120000', 10);
366
369
 
367
370
  try {
368
371
  await browser.startTrace();
@@ -397,7 +400,6 @@ async function checkPerformanceBudgets(browser, url) {
397
400
  logger.warn(`[ARGUS] Performance trace skipped for ${url}: ${err.message}`);
398
401
  }
399
402
 
400
- void LIGHTHOUSE_TIMEOUT_MS; // referenced only here to prevent unused-var lint
401
403
  return violations;
402
404
  }
403
405
 
@@ -424,7 +426,8 @@ export async function crawlRouteCheap(route, baseUrl, mcp) {
424
426
 
425
427
  // 0. Snapshot session-wide baselines BEFORE this route starts (D5).
426
428
  const consoleBaseline = (await browser.listConsole().catch(() => [])).length;
427
- const networkBaseline = (await browser.listNetwork().catch(() => [])).length;
429
+ const baselineNetList = await browser.listNetwork().catch(() => []);
430
+ const networkMaxReqId = baselineNetList.reduce((max, r) => Math.max(max, r._reqid ?? 0), 0);
428
431
  // listConsoleRaw returns raw MCP response — normalizeArray required before .length
429
432
  const issuesBaselineRaw = await browser.listConsoleRaw({ types: ['issue'] }).catch(() => null);
430
433
  const issuesBaseline = normalizeArray(issuesBaselineRaw).length;
@@ -468,8 +471,14 @@ export async function crawlRouteCheap(route, baseUrl, mcp) {
468
471
  });
469
472
  }
470
473
 
471
- // 5. Console messages — sliced from per-route baseline
472
- const consoleMsgs = (await browser.listConsole().catch(() => [])).slice(consoleBaseline);
474
+ // 5. Console messages — sliced from per-route baseline.
475
+ // Guard: chrome-devtools-mcp list_console_messages resets per navigation. If the
476
+ // new page has fewer total messages than the pre-navigation baseline, the baseline
477
+ // refers to the previous page's context and slicing by it would give an empty array.
478
+ // Fall back to 0 (take all messages from the new page) in that case.
479
+ const allConsoleMsgs = await browser.listConsole().catch(() => []);
480
+ const consoleSliceAt = allConsoleMsgs.length > consoleBaseline ? consoleBaseline : 0;
481
+ const consoleMsgs = allConsoleMsgs.slice(consoleSliceAt);
473
482
  for (const msg of consoleMsgs) {
474
483
  const text = (msg.text ?? msg.message ?? '');
475
484
  if (text.toLowerCase().includes('has been blocked by cors policy')) continue;
@@ -500,9 +509,9 @@ export async function crawlRouteCheap(route, baseUrl, mcp) {
500
509
  }
501
510
  }
502
511
 
503
- // 6. Network requests — sliced from per-route baseline (cap AFTER slice, not before)
504
- const networkReqs = (await browser.listNetwork())
505
- .slice(networkBaseline).slice(0, 500);
512
+ // 6. Network requests — filtered from per-route baseline by _reqid (cap AFTER filter, not before)
513
+ const networkReqs = (await browser.listNetwork().catch(() => []))
514
+ .filter(r => (r._reqid ?? 0) > networkMaxReqId).slice(0, 500);
506
515
  for (const req of networkReqs) {
507
516
  const severity = classifyNetworkRequest(req, route.critical);
508
517
  if (severity !== null) {
@@ -717,15 +726,7 @@ export async function crawlRouteCheap(route, baseUrl, mcp) {
717
726
  }
718
727
  } catch { /* URL parse failure */ }
719
728
 
720
- // 10. CSS analysis
721
- try {
722
- const cssRaw = await browser.evaluate(CSS_ANALYSIS_SCRIPT);
723
- result.errors.push(...parseCssAnalysisResult(unwrapEval(cssRaw), url));
724
- } catch (err) {
725
- logger.warn(`[ARGUS] CSS analysis skipped for ${url}: ${err.message}`);
726
- }
727
-
728
- // 11. Deduplicate within this cheap run
729
+ // 10. Deduplicate within this cheap run
729
730
  result.errors = deduplicateErrors(result.errors);
730
731
 
731
732
  // 12. Screenshot
@@ -798,8 +799,14 @@ export async function crawlRouteExpensive(route, baseUrl, mcp) {
798
799
  // Performance budget check
799
800
  errors.push(...(await checkPerformanceBudgets(browser, url)));
800
801
 
801
- // Full Lighthouse audit
802
- errors.push(...(await checkLighthouse(browser, url)));
802
+ // Full Lighthouse audit (capped at LIGHTHOUSE_TIMEOUT_MS to prevent indefinite hang)
803
+ errors.push(...(await Promise.race([
804
+ checkLighthouse(browser, url),
805
+ new Promise((_, reject) => setTimeout(() => reject(new Error(`Lighthouse timed out after ${LIGHTHOUSE_TIMEOUT_MS}ms`)), LIGHTHOUSE_TIMEOUT_MS)),
806
+ ]).catch(err => {
807
+ logger.warn(`[ARGUS] Lighthouse skipped for ${url}: ${err.message}`);
808
+ return [];
809
+ })));
803
810
 
804
811
  // Broken internal link detection
805
812
  try {
@@ -873,24 +880,34 @@ async function crawlAndAnalyzeRoute(route, targetBaseUrl, mcp, sessionFile) {
873
880
  await refreshSession(browser, auth, targetBaseUrl);
874
881
  await restoreSession(browser, targetBaseUrl, sessionFile);
875
882
  } catch (err) {
876
- logger.warn(`[ARGUS] Auth: session restore skipped for ${route.name}: ${err.message}`);
883
+ logger.warn(`[ARGUS] Auth: session restore skipped for ${route.name} (${route.path}): ${err.message}`);
877
884
  }
878
885
  }
879
886
 
880
887
  // Cheap pass × 2 → merge for flakiness
881
- logger.info(`[ARGUS] ${route.name}: cheap run 1/2...`);
888
+ logger.info(`[ARGUS] ${route.name} (${route.path}): cheap run 1/2...`);
882
889
  const cheapRun1 = await startSpan('argus.crawl_route', { url, critical: String(!!route.critical), pass: 'cheap_1' }, () => crawlRouteCheap(route, targetBaseUrl, mcp));
883
- logger.info(`[ARGUS] ${route.name}: cheap run 2/2 (flakiness check)...`);
890
+ logger.info(`[ARGUS] ${route.name} (${route.path}): cheap run 2/2 (flakiness check)...`);
884
891
  const cheapRun2 = await startSpan('argus.crawl_route', { url, critical: String(!!route.critical), pass: 'cheap_2' }, () => crawlRouteCheap(route, targetBaseUrl, mcp));
885
892
  const result = mergeRunResults(cheapRun1, cheapRun2);
886
893
 
887
894
  // Expensive pass × 1
888
- logger.info(`[ARGUS] ${route.name}: expensive analyzers (once)...`);
895
+ logger.info(`[ARGUS] ${route.name} (${route.path}): expensive analyzers (once)...`);
889
896
  const expensiveErrors = await startSpan('argus.crawl_route', { url, critical: String(!!route.critical), pass: 'expensive' }, () => crawlRouteExpensive(route, targetBaseUrl, mcp));
890
897
  result.errors.push(...expensiveErrors);
891
898
  result.errors = deduplicateErrors(result.errors);
892
899
 
893
- // Post-crawl expensive analyzers via registry (responsive, memory, hover, snapshot, keyboard)
900
+ // D9: Pre-fetch Figma design tokens when route specifies a figmaFrameUrl.
901
+ // Attaches figmaData to the route object so design-fidelity-analyzer can consume it.
902
+ if (route.figmaFrameUrl && !route.figmaData) {
903
+ try {
904
+ route.figmaData = await getFigmaFrame(route.figmaFrameUrl);
905
+ } catch (err) {
906
+ logger.warn(`[ARGUS] D9: Figma fetch failed for ${route.name} (${route.path}): ${err.message}`);
907
+ }
908
+ }
909
+
910
+ // Post-crawl expensive analyzers via registry (css, responsive, memory, hover, snapshot, keyboard, theme, design-fidelity)
894
911
  for (const { name, analyze } of getExpensive()) {
895
912
  if (name === 'lighthouse') continue; // runs inside crawlRouteExpensive
896
913
  try {
@@ -916,7 +933,7 @@ async function crawlAndAnalyzeRoute(route, targetBaseUrl, mcp, sessionFile) {
916
933
  if (Object.keys(screenshotPaths).length > 0) result.responsiveScreenshots = screenshotPaths;
917
934
  }
918
935
  } catch (err) {
919
- logger.warn(`[ARGUS] ${name} skipped for ${route.name}: ${err.message}`);
936
+ logger.warn(`[ARGUS] ${name} skipped for ${route.name} (${route.path}): ${err.message}`);
920
937
  }
921
938
  }
922
939
 
@@ -948,7 +965,7 @@ async function crawlShardWithClient(shard, targetBaseUrl, mcp, sessionFile) {
948
965
  const result = await crawlAndAnalyzeRoute(route, targetBaseUrl, mcp, sessionFile);
949
966
  const flakyCount = result.errors.filter(e => e.flaky).length;
950
967
  if (flakyCount > 0) {
951
- logger.info(`[ARGUS/parallel] ${route.name}: ${flakyCount} finding(s) downgraded to info (flaky)`);
968
+ logger.info(`[ARGUS/parallel] ${route.name} (${route.path}): ${flakyCount} finding(s) downgraded to info (flaky)`);
952
969
  }
953
970
  results.push(result);
954
971
  }
@@ -1053,7 +1070,7 @@ export async function runCrawl(mcp, routeOverrides = null, baseUrlOverride = nul
1053
1070
 
1054
1071
  const flakyCount = result.errors.filter(e => e.flaky).length;
1055
1072
  if (flakyCount > 0) {
1056
- logger.info(`[ARGUS] ${route.name}: ${flakyCount} finding(s) downgraded to info (flaky — appeared in only one cheap run)`);
1073
+ logger.info(`[ARGUS] ${route.name} (${route.path}): ${flakyCount} finding(s) downgraded to info (flaky — appeared in only one cheap run)`);
1057
1074
  }
1058
1075
 
1059
1076
  report.routes.push(result);
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Argus Report Processor (v9.1.3)
2
+ * Argus Report Processor
3
3
  *
4
4
  * Post-crawl pipeline: dedup → severity overrides → summary rebuild →
5
5
  * baseline load/apply/save → trend append → JSON write.
@@ -54,7 +54,8 @@ async function slackPostWithBackoff(args) {
54
54
  const isRateLimit = err.code === 'slack_webapi_rate_limited'
55
55
  || err.message?.toLowerCase().includes('ratelimited');
56
56
  if (!isRateLimit || attempt === SLACK_RATE_LIMIT_RETRIES - 1) throw err;
57
- const retryAfterMs = (err.retryAfter ?? 1) * 1000;
57
+ const jitter = Math.floor(Math.random() * 1000);
58
+ const retryAfterMs = (err.retryAfter ?? 1) * 1000 + jitter;
58
59
  logger.warn(`[ARGUS] Slack rate limited — retrying in ${retryAfterMs}ms (attempt ${attempt + 1})`);
59
60
  await new Promise(r => setTimeout(r, retryAfterMs));
60
61
  }
@@ -172,7 +172,7 @@ const DASHBOARD_HTML = `<!DOCTYPE html>
172
172
  * @param {number} port — TCP port to listen on (default 3002)
173
173
  * @returns {http.Server}
174
174
  */
175
- function startDashboard(getFindings, target, port) {
175
+ export function startDashboard(getFindings, target, port) {
176
176
  const server = http.createServer((req, res) => {
177
177
  if (req.url === '/data' || req.url?.startsWith('/data?')) {
178
178
  const payload = JSON.stringify({
package/src/registry.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Argus Analyzer Plugin Registry (v9.1.2)
2
+ * Argus Analyzer Plugin Registry
3
3
  *
4
4
  * Analyzers self-register at module load time by calling registerCheap()
5
5
  * or registerExpensive(). The orchestrator iterates getCheap() / getExpensive()
@@ -26,6 +26,8 @@
26
26
  */
27
27
 
28
28
  import { childLogger } from './logger.js';
29
+ import { registerExpensive } from '../registry.js';
30
+ import { unwrapEval } from './mcp-client.js';
29
31
 
30
32
  const logger = childLogger('css-analyzer');
31
33
 
@@ -405,3 +407,8 @@ export function parseCssAnalysisResult(rawResult, url) {
405
407
 
406
408
  return bugs;
407
409
  }
410
+
411
+ registerExpensive({ name: 'css', analyze: async (browser, url) => {
412
+ const cssRaw = await browser.evaluate(CSS_ANALYSIS_SCRIPT);
413
+ return parseCssAnalysisResult(unwrapEval(cssRaw), url);
414
+ } });