argusqa-os 9.5.1 → 9.5.5
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.
- package/README.md +1100 -1089
- package/glama.json +36 -32
- package/package.json +1 -1
- package/src/adapters/browser.js +5 -4
- package/src/adapters/figma.js +336 -0
- package/src/config/targets.js +4 -0
- package/src/domain/finding.js +16 -1
- package/src/mcp-server.js +54 -3
- package/src/orchestration/dispatcher.js +1 -1
- package/src/orchestration/orchestrator.js +36 -24
- package/src/orchestration/report-processor.js +1 -1
- package/src/orchestration/slack-notifier.js +2 -1
- package/src/registry.js +1 -1
- package/src/utils/css-analyzer.js +7 -0
- package/src/utils/design-fidelity-analyzer.js +685 -0
- package/src/utils/flow-runner.js +2 -0
- package/src/utils/mcp-client.js +2 -17
- package/src/utils/retry.js +1 -1
- package/src/utils/session-persistence.js +16 -4
- package/src/utils/theme-analyzer.js +173 -0
- package/src/utils/visual-diff-analyzer.js +207 -0
- package/src/utils/web-vitals-analyzer.js +284 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Argus Orchestrator
|
|
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,18 @@ 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
|
|
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';
|
|
46
|
+
import '../utils/web-vitals-analyzer.js';
|
|
47
|
+
import '../utils/visual-diff-analyzer.js';
|
|
43
48
|
|
|
44
49
|
import { getExpensive } from '../registry.js';
|
|
45
50
|
import { deduplicateFindings as deduplicateErrors } from './report-processor.js';
|
|
@@ -54,6 +59,7 @@ const logger = childLogger('orchestrator');
|
|
|
54
59
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
55
60
|
const BASE_URL = process.env.TARGET_DEV_URL ?? 'http://localhost:3000';
|
|
56
61
|
const OUTPUT_DIR = path.resolve(__dirname, '../../', config.outputDir);
|
|
62
|
+
const LIGHTHOUSE_TIMEOUT_MS = parseInt(process.env.ARGUS_LIGHTHOUSE_TIMEOUT ?? '120000', 10);
|
|
57
63
|
|
|
58
64
|
// Thresholds for perf budgets and network analysis are centralized in targets.js.
|
|
59
65
|
|
|
@@ -362,7 +368,6 @@ function analyzeNetworkPerformance(perfEntries, pageUrl) {
|
|
|
362
368
|
|
|
363
369
|
async function checkPerformanceBudgets(browser, url) {
|
|
364
370
|
const violations = [];
|
|
365
|
-
const LIGHTHOUSE_TIMEOUT_MS = parseInt(process.env.ARGUS_LIGHTHOUSE_TIMEOUT ?? '120000', 10);
|
|
366
371
|
|
|
367
372
|
try {
|
|
368
373
|
await browser.startTrace();
|
|
@@ -397,7 +402,6 @@ async function checkPerformanceBudgets(browser, url) {
|
|
|
397
402
|
logger.warn(`[ARGUS] Performance trace skipped for ${url}: ${err.message}`);
|
|
398
403
|
}
|
|
399
404
|
|
|
400
|
-
void LIGHTHOUSE_TIMEOUT_MS; // referenced only here to prevent unused-var lint
|
|
401
405
|
return violations;
|
|
402
406
|
}
|
|
403
407
|
|
|
@@ -724,15 +728,7 @@ export async function crawlRouteCheap(route, baseUrl, mcp) {
|
|
|
724
728
|
}
|
|
725
729
|
} catch { /* URL parse failure */ }
|
|
726
730
|
|
|
727
|
-
// 10.
|
|
728
|
-
try {
|
|
729
|
-
const cssRaw = await browser.evaluate(CSS_ANALYSIS_SCRIPT);
|
|
730
|
-
result.errors.push(...parseCssAnalysisResult(unwrapEval(cssRaw), url));
|
|
731
|
-
} catch (err) {
|
|
732
|
-
logger.warn(`[ARGUS] CSS analysis skipped for ${url}: ${err.message}`);
|
|
733
|
-
}
|
|
734
|
-
|
|
735
|
-
// 11. Deduplicate within this cheap run
|
|
731
|
+
// 10. Deduplicate within this cheap run
|
|
736
732
|
result.errors = deduplicateErrors(result.errors);
|
|
737
733
|
|
|
738
734
|
// 12. Screenshot
|
|
@@ -805,8 +801,14 @@ export async function crawlRouteExpensive(route, baseUrl, mcp) {
|
|
|
805
801
|
// Performance budget check
|
|
806
802
|
errors.push(...(await checkPerformanceBudgets(browser, url)));
|
|
807
803
|
|
|
808
|
-
// Full Lighthouse audit
|
|
809
|
-
errors.push(...(await
|
|
804
|
+
// Full Lighthouse audit (capped at LIGHTHOUSE_TIMEOUT_MS to prevent indefinite hang)
|
|
805
|
+
errors.push(...(await Promise.race([
|
|
806
|
+
checkLighthouse(browser, url),
|
|
807
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error(`Lighthouse timed out after ${LIGHTHOUSE_TIMEOUT_MS}ms`)), LIGHTHOUSE_TIMEOUT_MS)),
|
|
808
|
+
]).catch(err => {
|
|
809
|
+
logger.warn(`[ARGUS] Lighthouse skipped for ${url}: ${err.message}`);
|
|
810
|
+
return [];
|
|
811
|
+
})));
|
|
810
812
|
|
|
811
813
|
// Broken internal link detection
|
|
812
814
|
try {
|
|
@@ -880,24 +882,34 @@ async function crawlAndAnalyzeRoute(route, targetBaseUrl, mcp, sessionFile) {
|
|
|
880
882
|
await refreshSession(browser, auth, targetBaseUrl);
|
|
881
883
|
await restoreSession(browser, targetBaseUrl, sessionFile);
|
|
882
884
|
} catch (err) {
|
|
883
|
-
logger.warn(`[ARGUS] Auth: session restore skipped for ${route.name}: ${err.message}`);
|
|
885
|
+
logger.warn(`[ARGUS] Auth: session restore skipped for ${route.name} (${route.path}): ${err.message}`);
|
|
884
886
|
}
|
|
885
887
|
}
|
|
886
888
|
|
|
887
889
|
// Cheap pass × 2 → merge for flakiness
|
|
888
|
-
logger.info(`[ARGUS] ${route.name}: cheap run 1/2...`);
|
|
890
|
+
logger.info(`[ARGUS] ${route.name} (${route.path}): cheap run 1/2...`);
|
|
889
891
|
const cheapRun1 = await startSpan('argus.crawl_route', { url, critical: String(!!route.critical), pass: 'cheap_1' }, () => crawlRouteCheap(route, targetBaseUrl, mcp));
|
|
890
|
-
logger.info(`[ARGUS] ${route.name}: cheap run 2/2 (flakiness check)...`);
|
|
892
|
+
logger.info(`[ARGUS] ${route.name} (${route.path}): cheap run 2/2 (flakiness check)...`);
|
|
891
893
|
const cheapRun2 = await startSpan('argus.crawl_route', { url, critical: String(!!route.critical), pass: 'cheap_2' }, () => crawlRouteCheap(route, targetBaseUrl, mcp));
|
|
892
894
|
const result = mergeRunResults(cheapRun1, cheapRun2);
|
|
893
895
|
|
|
894
896
|
// Expensive pass × 1
|
|
895
|
-
logger.info(`[ARGUS] ${route.name}: expensive analyzers (once)...`);
|
|
897
|
+
logger.info(`[ARGUS] ${route.name} (${route.path}): expensive analyzers (once)...`);
|
|
896
898
|
const expensiveErrors = await startSpan('argus.crawl_route', { url, critical: String(!!route.critical), pass: 'expensive' }, () => crawlRouteExpensive(route, targetBaseUrl, mcp));
|
|
897
899
|
result.errors.push(...expensiveErrors);
|
|
898
900
|
result.errors = deduplicateErrors(result.errors);
|
|
899
901
|
|
|
900
|
-
//
|
|
902
|
+
// D9: Pre-fetch Figma design tokens when route specifies a figmaFrameUrl.
|
|
903
|
+
// Attaches figmaData to the route object so design-fidelity-analyzer can consume it.
|
|
904
|
+
if (route.figmaFrameUrl && !route.figmaData) {
|
|
905
|
+
try {
|
|
906
|
+
route.figmaData = await getFigmaFrame(route.figmaFrameUrl);
|
|
907
|
+
} catch (err) {
|
|
908
|
+
logger.warn(`[ARGUS] D9: Figma fetch failed for ${route.name} (${route.path}): ${err.message}`);
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
// Post-crawl expensive analyzers via registry (css, responsive, memory, hover, snapshot, keyboard, theme, design-fidelity)
|
|
901
913
|
for (const { name, analyze } of getExpensive()) {
|
|
902
914
|
if (name === 'lighthouse') continue; // runs inside crawlRouteExpensive
|
|
903
915
|
try {
|
|
@@ -923,7 +935,7 @@ async function crawlAndAnalyzeRoute(route, targetBaseUrl, mcp, sessionFile) {
|
|
|
923
935
|
if (Object.keys(screenshotPaths).length > 0) result.responsiveScreenshots = screenshotPaths;
|
|
924
936
|
}
|
|
925
937
|
} catch (err) {
|
|
926
|
-
logger.warn(`[ARGUS] ${name} skipped for ${route.name}: ${err.message}`);
|
|
938
|
+
logger.warn(`[ARGUS] ${name} skipped for ${route.name} (${route.path}): ${err.message}`);
|
|
927
939
|
}
|
|
928
940
|
}
|
|
929
941
|
|
|
@@ -955,7 +967,7 @@ async function crawlShardWithClient(shard, targetBaseUrl, mcp, sessionFile) {
|
|
|
955
967
|
const result = await crawlAndAnalyzeRoute(route, targetBaseUrl, mcp, sessionFile);
|
|
956
968
|
const flakyCount = result.errors.filter(e => e.flaky).length;
|
|
957
969
|
if (flakyCount > 0) {
|
|
958
|
-
logger.info(`[ARGUS/parallel] ${route.name}: ${flakyCount} finding(s) downgraded to info (flaky)`);
|
|
970
|
+
logger.info(`[ARGUS/parallel] ${route.name} (${route.path}): ${flakyCount} finding(s) downgraded to info (flaky)`);
|
|
959
971
|
}
|
|
960
972
|
results.push(result);
|
|
961
973
|
}
|
|
@@ -1060,7 +1072,7 @@ export async function runCrawl(mcp, routeOverrides = null, baseUrlOverride = nul
|
|
|
1060
1072
|
|
|
1061
1073
|
const flakyCount = result.errors.filter(e => e.flaky).length;
|
|
1062
1074
|
if (flakyCount > 0) {
|
|
1063
|
-
logger.info(`[ARGUS] ${route.name}: ${flakyCount} finding(s) downgraded to info (flaky — appeared in only one cheap run)`);
|
|
1075
|
+
logger.info(`[ARGUS] ${route.name} (${route.path}): ${flakyCount} finding(s) downgraded to info (flaky — appeared in only one cheap run)`);
|
|
1064
1076
|
}
|
|
1065
1077
|
|
|
1066
1078
|
report.routes.push(result);
|
|
@@ -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
|
|
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
|
}
|
package/src/registry.js
CHANGED
|
@@ -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
|
+
} });
|