argusqa-os 9.5.1 → 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.
- package/README.md +15 -10
- package/glama.json +5 -1
- package/package.json +1 -1
- package/src/adapters/browser.js +5 -4
- package/src/adapters/figma.js +336 -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 +34 -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
|
@@ -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,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
|
|
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
|
|
|
@@ -724,15 +726,7 @@ export async function crawlRouteCheap(route, baseUrl, mcp) {
|
|
|
724
726
|
}
|
|
725
727
|
} catch { /* URL parse failure */ }
|
|
726
728
|
|
|
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
|
|
729
|
+
// 10. Deduplicate within this cheap run
|
|
736
730
|
result.errors = deduplicateErrors(result.errors);
|
|
737
731
|
|
|
738
732
|
// 12. Screenshot
|
|
@@ -805,8 +799,14 @@ export async function crawlRouteExpensive(route, baseUrl, mcp) {
|
|
|
805
799
|
// Performance budget check
|
|
806
800
|
errors.push(...(await checkPerformanceBudgets(browser, url)));
|
|
807
801
|
|
|
808
|
-
// Full Lighthouse audit
|
|
809
|
-
errors.push(...(await
|
|
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
|
+
})));
|
|
810
810
|
|
|
811
811
|
// Broken internal link detection
|
|
812
812
|
try {
|
|
@@ -880,24 +880,34 @@ async function crawlAndAnalyzeRoute(route, targetBaseUrl, mcp, sessionFile) {
|
|
|
880
880
|
await refreshSession(browser, auth, targetBaseUrl);
|
|
881
881
|
await restoreSession(browser, targetBaseUrl, sessionFile);
|
|
882
882
|
} catch (err) {
|
|
883
|
-
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}`);
|
|
884
884
|
}
|
|
885
885
|
}
|
|
886
886
|
|
|
887
887
|
// Cheap pass × 2 → merge for flakiness
|
|
888
|
-
logger.info(`[ARGUS] ${route.name}: cheap run 1/2...`);
|
|
888
|
+
logger.info(`[ARGUS] ${route.name} (${route.path}): cheap run 1/2...`);
|
|
889
889
|
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)...`);
|
|
890
|
+
logger.info(`[ARGUS] ${route.name} (${route.path}): cheap run 2/2 (flakiness check)...`);
|
|
891
891
|
const cheapRun2 = await startSpan('argus.crawl_route', { url, critical: String(!!route.critical), pass: 'cheap_2' }, () => crawlRouteCheap(route, targetBaseUrl, mcp));
|
|
892
892
|
const result = mergeRunResults(cheapRun1, cheapRun2);
|
|
893
893
|
|
|
894
894
|
// Expensive pass × 1
|
|
895
|
-
logger.info(`[ARGUS] ${route.name}: expensive analyzers (once)...`);
|
|
895
|
+
logger.info(`[ARGUS] ${route.name} (${route.path}): expensive analyzers (once)...`);
|
|
896
896
|
const expensiveErrors = await startSpan('argus.crawl_route', { url, critical: String(!!route.critical), pass: 'expensive' }, () => crawlRouteExpensive(route, targetBaseUrl, mcp));
|
|
897
897
|
result.errors.push(...expensiveErrors);
|
|
898
898
|
result.errors = deduplicateErrors(result.errors);
|
|
899
899
|
|
|
900
|
-
//
|
|
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)
|
|
901
911
|
for (const { name, analyze } of getExpensive()) {
|
|
902
912
|
if (name === 'lighthouse') continue; // runs inside crawlRouteExpensive
|
|
903
913
|
try {
|
|
@@ -923,7 +933,7 @@ async function crawlAndAnalyzeRoute(route, targetBaseUrl, mcp, sessionFile) {
|
|
|
923
933
|
if (Object.keys(screenshotPaths).length > 0) result.responsiveScreenshots = screenshotPaths;
|
|
924
934
|
}
|
|
925
935
|
} catch (err) {
|
|
926
|
-
logger.warn(`[ARGUS] ${name} skipped for ${route.name}: ${err.message}`);
|
|
936
|
+
logger.warn(`[ARGUS] ${name} skipped for ${route.name} (${route.path}): ${err.message}`);
|
|
927
937
|
}
|
|
928
938
|
}
|
|
929
939
|
|
|
@@ -955,7 +965,7 @@ async function crawlShardWithClient(shard, targetBaseUrl, mcp, sessionFile) {
|
|
|
955
965
|
const result = await crawlAndAnalyzeRoute(route, targetBaseUrl, mcp, sessionFile);
|
|
956
966
|
const flakyCount = result.errors.filter(e => e.flaky).length;
|
|
957
967
|
if (flakyCount > 0) {
|
|
958
|
-
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)`);
|
|
959
969
|
}
|
|
960
970
|
results.push(result);
|
|
961
971
|
}
|
|
@@ -1060,7 +1070,7 @@ export async function runCrawl(mcp, routeOverrides = null, baseUrlOverride = nul
|
|
|
1060
1070
|
|
|
1061
1071
|
const flakyCount = result.errors.filter(e => e.flaky).length;
|
|
1062
1072
|
if (flakyCount > 0) {
|
|
1063
|
-
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)`);
|
|
1064
1074
|
}
|
|
1065
1075
|
|
|
1066
1076
|
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
|
+
} });
|