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 CHANGED
@@ -52,7 +52,7 @@ Then ask Claude (or any MCP client):
52
52
  Run argus_audit on http://localhost:3000
53
53
  ```
54
54
 
55
- **Six tools are exposed:**
55
+ **Seven tools are exposed:**
56
56
 
57
57
  | Tool | What it does |
58
58
  | --- | --- |
@@ -62,6 +62,7 @@ Run argus_audit on http://localhost:3000
62
62
  | `argus_last_report` | Return the last saved JSON report without re-running a scan |
63
63
  | `argus_watch_snapshot` | Snapshot the currently open Chrome tab without navigating — raw console + network capture |
64
64
  | `argus_get_context` | Capture everything broken on the open tab, formatted as a diagnostic context for Claude to diagnose and suggest fixes |
65
+ | `argus_design_audit` | Full Figma design-to-implementation fidelity audit — 13 finding types across color, typography, spacing, per-corner radius, position drift, stroke, shadow (color+spread), opacity, gap, and text. Selector fallback: `[data-testid]` → `[aria-label]` → `#id` → `.class` |
65
66
 
66
67
  > **Requires**: Node.js ≥ 20.19, Chrome (desktop or headless), and the `chrome-devtools-mcp` server registered alongside Argus (shown above).
67
68
 
@@ -79,7 +80,7 @@ The `landing/` directory contains the product landing page (React + Vite + Tailw
79
80
 
80
81
  | 🔴 Critical / 🟡 Warning / 🔵 Info | ⚙️ | 🧪 | 📋 |
81
82
  | :---: | :---: | :---: | :---: |
82
- | **114 distinct issue types detected** | **24 analysis engines** | **528 test assertions** | **126 test blocks** |
83
+ | **114 distinct issue types detected** | **24 analysis engines** | **565 test assertions** | **128 test blocks** |
83
84
 
84
85
  </div>
85
86
 
@@ -348,7 +349,8 @@ Argus watches your running application and automatically surfaces issues that te
348
349
  | **Slack Notifications** | Rich Block Kit reports with inline screenshots routed to `#bugs-critical`, `#bugs-warnings`, `#bugs-digest` |
349
350
  | **Slash Command** | `/argus-retest <url>` triggers an on-demand test from any Slack channel |
350
351
  | **CI Integration** | GitHub Actions workflow runs daily at 6 AM UTC and on every push to `main` |
351
- | **MCP Server (AI-callable Argus)** | Register Argus as an MCP server via `.mcp.json`; Claude (or any MCP client) can call `argus_audit`, `argus_audit_full`, `argus_compare`, `argus_last_report`, `argus_watch_snapshot`, and `argus_get_context` directly from a conversation — no CLI, no terminal required. Published to npm as **[argusqa-os](https://www.npmjs.com/package/argusqa-os)** — add via `{ "command": "npx", "args": ["-y", "argusqa-os"] }` in `.mcp.json` |
352
+ | **MCP Server (AI-callable Argus)** | Register Argus as an MCP server via `.mcp.json`; Claude (or any MCP client) can call `argus_audit`, `argus_audit_full`, `argus_compare`, `argus_last_report`, `argus_watch_snapshot`, `argus_get_context`, and `argus_design_audit` directly from a conversation — no CLI, no terminal required. Published to npm as **[argusqa-os](https://www.npmjs.com/package/argusqa-os)** — add via `{ "command": "npx", "args": ["-y", "argusqa-os"] }` in `.mcp.json` |
353
+ | **Figma Design Fidelity** | `argus_design_audit(url, figmaFrameUrl)` compares every extracted Figma property — 13 mismatch finding types: CSS token values, component presence, per-node fill/text color (RGB distance), typography (fontSize/fontWeight/lineHeight/fontFamily/letterSpacing), Auto Layout padding and gap, border-radius (per-corner), bounding-box overflow, **absolute position drift** (scroll-corrected x/y vs Figma bounds, 20px), border stroke (color+weight), box-shadow (offset+blur+**spread**+**color**), opacity, and text content. Selector fallback: tries `[data-testid]`, `[aria-label]`, `#id`, `.class` per node. Requires `FIGMA_API_TOKEN` env var. |
352
354
 
353
355
  Works with **React + SCSS**, CSS Modules, CSS-in-JS (styled-components / emotion), and plain HTML/CSS apps.
354
356
 
@@ -576,6 +578,7 @@ Ask Claude directly — no terminal needed.
576
578
  | `argus_last_report` | Return the last saved JSON report without re-running a scan |
577
579
  | `argus_watch_snapshot` | Snapshot the currently open Chrome tab without navigating — raw console + network capture |
578
580
  | `argus_get_context` | Capture everything broken on the open tab, formatted as a diagnostic context for Claude to diagnose and suggest fixes |
581
+ | `argus_design_audit` | Figma design-to-implementation fidelity audit — 13 finding types across color, typography, spacing, per-corner radius, position drift, stroke, shadow (color+spread), opacity, gap, and text content |
579
582
 
580
583
  **`argus_audit`** — fast audit of any URL:
581
584
 
@@ -632,7 +635,7 @@ Then follow with: *"Here's the context — what's causing these errors and how d
632
635
  | `npm run server` | Start the Slack slash command + interaction server (port 3001) |
633
636
  | `npm run init` | Interactive setup wizard — generates `.env` + `targets.js` |
634
637
  | `npm run test:unit` | Run 61 unit tests (no Chrome required) |
635
- | `npm run test:harness` | Run 126-block correctness harness (requires Chrome) |
638
+ | `npm run test:harness` | Run 128-block correctness harness (requires Chrome) |
636
639
 
637
640
  **`npm run crawl`** — full audit of all configured routes:
638
641
 
@@ -867,11 +870,11 @@ argus/
867
870
  │ └── argus.yml # CI pipeline
868
871
  ├── .vscode/
869
872
  │ └── mcp.json # Chrome DevTools MCP config for VS Code
870
- ├── .mcp.json # Argus MCP server registration — exposes argus_audit/argus_audit_full/argus_compare/argus_last_report/argus_watch_snapshot/argus_get_context to Claude
873
+ ├── .mcp.json # Argus MCP server registration — exposes all 7 tools to Claude: argus_audit/argus_audit_full/argus_compare/argus_last_report/argus_watch_snapshot/argus_get_context/argus_design_audit
871
874
  ├── src/
872
875
  │ ├── argus.js # Single-page audit entry point
873
876
  │ ├── batch-runner.js # Multi-page batch audit
874
- │ ├── mcp-server.js # Argus MCP server — argus_audit / argus_audit_full / argus_compare / argus_last_report / argus_watch_snapshot / argus_get_context
877
+ │ ├── mcp-server.js # Argus MCP server — argus_audit / argus_audit_full / argus_compare / argus_last_report / argus_watch_snapshot / argus_get_context / argus_design_audit
875
878
  │ ├── adapters/
876
879
  │ │ └── browser.js # CdpBrowserAdapter — facade over all chrome-devtools-mcp calls
877
880
  │ ├── domain/
@@ -944,12 +947,12 @@ argus/
944
947
  │ └── README.md # Setup guide, Supabase SQL schema, env vars, deployment
945
948
  ├── scripts/
946
949
  │ └── dispatch-report.js # Standalone Slack re-dispatch script (re-posts last report.json to Slack)
947
- ├── test-harness/ # Fixture server + test runner (126 blocks, 528 hard assertions, 53 fixture pages)
950
+ ├── test-harness/ # Fixture server + test runner (128 blocks, 565 hard assertions, 55 fixture pages)
948
951
  │ ├── README.md
949
952
  │ ├── server.js # Express fixture server (ports 3100 dev / 3101 staging)
950
953
  │ ├── harness-config.js # Route definitions + expected findings
951
- │ ├── validate.js # Test runner — 126 numbered blocks ([80]–[84] MCP/createFinding/withRetry/watch/init, [85]–[93] Sprint 0.5 Tier 3, [94]–[126] gap-close Sections 1–6)
952
- │ ├── pages/ # 53 fixture HTML pages (one per detection category)
954
+ │ ├── validate.js # Test runner — 128 numbered blocks ([80]–[84] MCP/createFinding/withRetry/watch/init, [85]–[93] Sprint 0.5 Tier 3, [94]–[126] gap-close, [127] A7 theme, [128] D9 design fidelity)
955
+ │ ├── pages/ # 55 fixture HTML pages (one per detection category)
953
956
  │ ├── nextjs-fixture/ # Next.js app structure for C3 discovery tests (10 files)
954
957
  │ ├── source-fixture/ # Minimal app.js for C1 codebase-analyzer tests (env var audit)
955
958
  │ └── static/
@@ -989,7 +992,7 @@ argus/
989
992
 
990
993
  ## Known MCP Tool Limitations
991
994
 
992
- The Chrome DevTools MCP behavioral constraints below cause **3 permanent test failures** in the harness (`525/528` pass). These are MCP-layer restrictions — they cannot be fixed in Argus code. `validate.js` now exits with code 0 when only these 3 failures remain, making the CI harness gate reliable.
995
+ The Chrome DevTools MCP behavioral constraints below cause **3 permanent test failures** in the harness (`562/565` pass). These are MCP-layer restrictions — they cannot be fixed in Argus code. `validate.js` now exits with code 0 when only these 3 failures remain, making the CI harness gate reliable.
993
996
 
994
997
  > **`type_text` clarification**: `type_text` does fire DOM `input` events when the element is properly focused first with `mcp.click({ uid })`. Always use uid-based focus — passing `{ selector }` to `mcp.click` silently does nothing.
995
998
 
@@ -1000,6 +1003,8 @@ The Chrome DevTools MCP behavioral constraints below cause **3 permanent test fa
1000
1003
 
1001
1004
  These constraints are documented with workarounds in [SKILL.md §10](SKILL.md).
1002
1005
 
1006
+ The harness passes **562/565** assertions (exits 0). The 3 failures are the permanent MCP-limited ones listed above.
1007
+
1003
1008
  ---
1004
1009
 
1005
1010
  ## Environment Variables Reference
package/glama.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://glama.ai/mcp/schemas/server.json",
3
3
  "name": "argus",
4
- "description": "AI-powered QA harness that audits web apps via Chrome DevTools Protocol. Catches JS errors, network failures, a11y violations, SEO issues, security headers, CSS regressions, and more — directly from Claude conversations. 6 MCP tools: argus_audit (fast 8-analyzer pass), argus_audit_full (Lighthouse + memory + responsive), argus_compare (dev vs staging diff), argus_last_report (retrieve last JSON report), argus_watch_snapshot (live tab snapshot without navigating), argus_get_context (LLM-optimized context + fix loop with snapshot_id diff). 126 test blocks, 528 hard assertions, 54 detection categories.",
4
+ "description": "AI-powered QA harness that audits web apps via Chrome DevTools Protocol. Catches JS errors, network failures, a11y violations, SEO issues, security headers, CSS regressions, and more — directly from Claude conversations. 7 MCP tools: argus_audit (fast 8-analyzer pass), argus_audit_full (Lighthouse + memory + responsive), argus_compare (dev vs staging diff), argus_last_report (retrieve last JSON report), argus_watch_snapshot (live tab snapshot without navigating), argus_get_context (LLM-optimized context + fix loop with snapshot_id diff), argus_design_audit (Figma design fidelity — 13 finding types). 128 test blocks, 565 hard assertions, 56 detection categories.",
5
5
  "maintainers": ["ironclawdevs27"],
6
6
  "tools": [
7
7
  {
@@ -27,6 +27,10 @@
27
27
  {
28
28
  "name": "argus_get_context",
29
29
  "description": "LLM-optimized diagnostic context for the open Chrome tab. Returns snapshot_id for fix-loop diffing: pass it back on the next call to get resolved/new_issues/persisting arrays. Accepts optional tabId for multi-tab workflows."
30
+ },
31
+ {
32
+ "name": "argus_design_audit",
33
+ "description": "Full Figma design-to-implementation fidelity audit. Fetches design spec from a Figma frame URL (requires FIGMA_API_TOKEN) and compares every extracted property against live DOM computed styles. Detects 13 mismatch finding types: CSS token values, component presence, fill/text color (RGB distance), typography (fontSize/fontWeight/lineHeight/fontFamily/letterSpacing), Auto Layout padding and gap, border-radius (per-corner), bounding-box overflow, absolute position drift (scroll-corrected x/y vs Figma bounds), border stroke (color+weight), box-shadow (offset+blur+spread+color), opacity, and text content. Selector fallback: tries [data-testid], [aria-label], #id, .class per node."
30
34
  }
31
35
  ]
32
36
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "argusqa-os",
3
- "version": "9.5.1",
3
+ "version": "9.5.3",
4
4
  "mcpName": "io.github.ironclawdevs27/argus",
5
5
  "description": "Argus — AI-powered automated dev-testing platform using Chrome DevTools MCP and Claude Code",
6
6
  "keywords": [
@@ -25,7 +25,7 @@ export class CdpBrowserAdapter {
25
25
 
26
26
  // ── Evaluation & snapshots ──────────────────────────────────────────────────
27
27
  evaluate(fn) { return this._mcp.evaluate_script({ function: fn }); }
28
- snapshot() { return this._mcp.take_snapshot(); }
28
+ snapshot(opts = {}) { return this._mcp.take_snapshot(opts); }
29
29
  screenshot(opts = {}) { return this._mcp.take_screenshot(opts); }
30
30
  heapSnapshot(opts = {}) { return this._mcp.take_heapsnapshot(opts); }
31
31
 
@@ -45,9 +45,10 @@ export class CdpBrowserAdapter {
45
45
  waitFor(opts) { return this._mcp.wait_for(opts); }
46
46
 
47
47
  // ── Viewport ────────────────────────────────────────────────────────────────
48
- emulate(viewport) { return this._mcp.emulate({ viewport }); }
49
- emulateCpu(rate) { return this._mcp.emulate({ cpuThrottlingRate: rate }); }
50
- resize(w, h) { return this._mcp.resize_page({ width: w, height: h }); }
48
+ emulate(viewport) { return this._mcp.emulate({ viewport }); }
49
+ emulateCpu(rate) { return this._mcp.emulate({ cpuThrottlingRate: rate }); }
50
+ emulateColorScheme(scheme) { return this._mcp.emulate({ colorScheme: scheme }); }
51
+ resize(w, h) { return this._mcp.resize_page({ width: w, height: h }); }
51
52
 
52
53
  // ── Network & performance ───────────────────────────────────────────────────
53
54
  getNetworkRequest(reqId) { return this._mcp.get_network_request({ requestId: reqId }); }
@@ -0,0 +1,336 @@
1
+ /**
2
+ * Figma REST adapter — extracts the full design spec from a Figma frame.
3
+ *
4
+ * For each node in the frame tree, extracts:
5
+ * bounds — x, y, width, height relative to the frame origin
6
+ * fill — primary solid fill color (r,g,b,a — 0-255)
7
+ * stroke — border color + weight
8
+ * typography — fontSize, fontWeight, lineHeightPx, letterSpacing (TEXT nodes)
9
+ * spacing — Auto Layout padding + gap
10
+ * cornerRadius
11
+ * shadow — primary DROP_SHADOW effect
12
+ * opacity
13
+ *
14
+ * Returns null gracefully when FIGMA_API_TOKEN is absent, the URL is invalid,
15
+ * or the API call fails — callers skip analysis without crashing.
16
+ *
17
+ * Supported Figma URL formats:
18
+ * https://www.figma.com/file/<fileKey>/Name?node-id=42%3A0
19
+ * https://www.figma.com/design/<fileKey>/Name?node-id=42-0
20
+ *
21
+ * Requires env: FIGMA_API_TOKEN (Personal Access Token from figma.com/settings)
22
+ */
23
+
24
+ import { childLogger } from '../utils/logger.js';
25
+
26
+ const logger = childLogger('figma-adapter');
27
+
28
+ const FIGMA_API = 'https://api.figma.com/v1';
29
+
30
+ // Node types that carry no useful layout/style data — skip during tree walk
31
+ const SKIP_TYPES = new Set(['VECTOR', 'STAR', 'LINE', 'BOOLEAN_OPERATION', 'REGULAR_POLYGON']);
32
+
33
+ // ── URL parsing ───────────────────────────────────────────────────────────────
34
+
35
+ /**
36
+ * Parse fileKey and nodeId from a Figma frame URL.
37
+ * Returns null if the URL is not a recognisable Figma frame URL.
38
+ */
39
+ export function parseFigmaUrl(url) {
40
+ if (!url || typeof url !== 'string') return null;
41
+ const fileMatch = url.match(/figma\.com\/(?:file|design)\/([A-Za-z0-9]+)/);
42
+ const nodeMatch = url.match(/node-id=([^&]+)/);
43
+ if (!fileMatch) return null;
44
+ const fileKey = fileMatch[1];
45
+ const rawNode = nodeMatch?.[1];
46
+ if (!rawNode) return null;
47
+ // node-id can be URL-encoded "42%3A0" or dash-separated "42-0" — normalise to "42:0"
48
+ const nodeId = decodeURIComponent(rawNode).replace('-', ':');
49
+ return { fileKey, nodeId };
50
+ }
51
+
52
+ // ── Selector inference ────────────────────────────────────────────────────────
53
+
54
+ /**
55
+ * Infer a prioritised list of CSS selector candidates from a Figma layer name.
56
+ * The analyzer tries each in order and uses the first that matches a DOM element.
57
+ *
58
+ * Priority:
59
+ * 1. Explicit selector — if the name starts with #, ., or [ use it verbatim
60
+ * 2. data-testid attribute (slug form)
61
+ * 3. aria-label attribute (raw name)
62
+ * 4. ID selector (slug form)
63
+ * 5. Class selector (BEM slug: spaces→-, /→--)
64
+ *
65
+ * Examples:
66
+ * "Button / Primary" → [data-testid="button--primary"], [aria-label="Button / Primary"], #button--primary, .button--primary
67
+ * "#hero" → ["#hero"] (explicit — used verbatim)
68
+ * ".card" → [".card"] (explicit)
69
+ */
70
+ function inferSelectors(node) {
71
+ const name = node.name;
72
+
73
+ // Designer typed an explicit selector — honour it and skip inference
74
+ if (name.startsWith('#') || name.startsWith('.') || name.startsWith('[')) {
75
+ return [name];
76
+ }
77
+
78
+ const slug = name
79
+ .toLowerCase()
80
+ .replace(/\s*\/\s*/g, '--')
81
+ .replace(/[^a-z0-9-]+/g, '-')
82
+ .replace(/^-+|-+$/g, '');
83
+
84
+ if (!slug) return [];
85
+
86
+ return [
87
+ `[data-testid="${slug}"]`,
88
+ `[aria-label="${name}"]`,
89
+ `#${slug}`,
90
+ `.${slug}`,
91
+ ];
92
+ }
93
+
94
+ // ── Node extraction ───────────────────────────────────────────────────────────
95
+
96
+ /**
97
+ * Extract all design properties from a single Figma node into a flat object.
98
+ * All color channels are 0-255. Bounds are relative to the frame origin.
99
+ */
100
+ function extractNode(node, frameX, frameY) {
101
+ if (!node) return null;
102
+
103
+ const selectors = inferSelectors(node);
104
+ const result = {
105
+ id: node.id,
106
+ name: node.name,
107
+ type: node.type,
108
+ selectors: selectors, // ordered candidates — analyzer tries each until one matches
109
+ selector: selectors[0] ?? `.${node.name.toLowerCase().replace(/[^a-z0-9]+/g, '-')}`,
110
+ opacity: node.opacity ?? 1,
111
+
112
+ // Populated below
113
+ bounds: null,
114
+ fill: null,
115
+ stroke: null,
116
+ typography: null,
117
+ spacing: null,
118
+ cornerRadius: null,
119
+ shadow: null,
120
+ };
121
+
122
+ // Bounds — relative to frame origin so they map to viewport coords when the
123
+ // browser viewport matches the frame dimensions.
124
+ if (node.absoluteBoundingBox) {
125
+ result.bounds = {
126
+ x: node.absoluteBoundingBox.x - frameX,
127
+ y: node.absoluteBoundingBox.y - frameY,
128
+ width: node.absoluteBoundingBox.width,
129
+ height: node.absoluteBoundingBox.height,
130
+ };
131
+ }
132
+
133
+ // Primary solid fill (first visible solid fill)
134
+ for (const fill of (node.fills ?? [])) {
135
+ if (fill.type === 'SOLID' && fill.color && fill.visible !== false) {
136
+ result.fill = {
137
+ r: Math.round(fill.color.r * 255),
138
+ g: Math.round(fill.color.g * 255),
139
+ b: Math.round(fill.color.b * 255),
140
+ a: Math.round((fill.opacity ?? 1) * 255),
141
+ };
142
+ break;
143
+ }
144
+ }
145
+
146
+ // Primary stroke
147
+ for (const stroke of (node.strokes ?? [])) {
148
+ if (stroke.type === 'SOLID' && stroke.color && stroke.visible !== false) {
149
+ result.stroke = {
150
+ r: Math.round(stroke.color.r * 255),
151
+ g: Math.round(stroke.color.g * 255),
152
+ b: Math.round(stroke.color.b * 255),
153
+ a: Math.round((stroke.opacity ?? 1) * 255),
154
+ weight: node.strokeWeight ?? 1,
155
+ };
156
+ break;
157
+ }
158
+ }
159
+
160
+ // Typography (TEXT nodes only)
161
+ if (node.type === 'TEXT' && node.style) {
162
+ const s = node.style;
163
+ result.typography = {
164
+ fontFamily: s.fontFamily ?? null,
165
+ fontSize: s.fontSize ?? null,
166
+ fontWeight: s.fontWeight ?? null,
167
+ lineHeightPx: s.lineHeightPx ?? null,
168
+ letterSpacing: s.letterSpacing ?? 0,
169
+ };
170
+ }
171
+
172
+ // Text content — actual Figma copy; compared against DOM textContent
173
+ if (node.type === 'TEXT' && typeof node.characters === 'string') {
174
+ result.characters = node.characters.trim() || null;
175
+ }
176
+
177
+ // Auto Layout spacing — maps to CSS padding + gap.
178
+ // layoutMode ('HORIZONTAL'|'VERTICAL') lets the analyzer pick columnGap vs rowGap.
179
+ if (node.layoutMode && node.layoutMode !== 'NONE') {
180
+ result.spacing = {
181
+ paddingTop: node.paddingTop ?? 0,
182
+ paddingRight: node.paddingRight ?? 0,
183
+ paddingBottom: node.paddingBottom ?? 0,
184
+ paddingLeft: node.paddingLeft ?? 0,
185
+ gap: node.itemSpacing ?? 0,
186
+ layoutMode: node.layoutMode,
187
+ };
188
+ }
189
+
190
+ // Corner radius — uniform number or per-corner object.
191
+ // Figma rectangleCornerRadii = [topLeft, topRight, bottomRight, bottomLeft].
192
+ if (node.cornerRadius != null) {
193
+ result.cornerRadius = node.cornerRadius; // uniform
194
+ } else if (node.rectangleCornerRadii) {
195
+ const [tl, tr, br, bl] = node.rectangleCornerRadii;
196
+ // Collapse to a single number if all corners are equal (avoids noise in findings)
197
+ result.cornerRadius = (tl === tr && tr === br && br === bl)
198
+ ? tl
199
+ : { topLeft: tl, topRight: tr, bottomRight: br, bottomLeft: bl };
200
+ }
201
+
202
+ // Primary DROP_SHADOW effect
203
+ for (const eff of (node.effects ?? [])) {
204
+ if (eff.type === 'DROP_SHADOW' && eff.visible !== false) {
205
+ result.shadow = {
206
+ offsetX: eff.offset?.x ?? 0,
207
+ offsetY: eff.offset?.y ?? 0,
208
+ blur: eff.radius ?? 0,
209
+ spread: eff.spread ?? 0,
210
+ r: Math.round((eff.color?.r ?? 0) * 255),
211
+ g: Math.round((eff.color?.g ?? 0) * 255),
212
+ b: Math.round((eff.color?.b ?? 0) * 255),
213
+ a: Math.round((eff.color?.a ?? 0.25) * 255),
214
+ };
215
+ break;
216
+ }
217
+ }
218
+
219
+ return result;
220
+ }
221
+
222
+ // ── Tree walker ───────────────────────────────────────────────────────────────
223
+
224
+ function parseFigmaNodes(data, nodeId) {
225
+ const nodeKey = nodeId.replace(':', '-');
226
+ const nodeData = data?.nodes?.[nodeKey] ?? data?.nodes?.[nodeId];
227
+ if (!nodeData?.document) return null;
228
+
229
+ const doc = nodeData.document;
230
+ const frameX = doc.absoluteBoundingBox?.x ?? 0;
231
+ const frameY = doc.absoluteBoundingBox?.y ?? 0;
232
+
233
+ const nodes = [];
234
+ const tokens = {}; // legacy CSS-var format
235
+ const components = []; // legacy component presence format
236
+
237
+ function walk(node) {
238
+ if (!node || SKIP_TYPES.has(node.type)) return;
239
+
240
+ const extracted = extractNode(node, frameX, frameY);
241
+ if (extracted) {
242
+ nodes.push(extracted);
243
+
244
+ // Build legacy tokens map for backward compat
245
+ if (extracted.fill) {
246
+ const hex = '#' +
247
+ extracted.fill.r.toString(16).padStart(2, '0') +
248
+ extracted.fill.g.toString(16).padStart(2, '0') +
249
+ extracted.fill.b.toString(16).padStart(2, '0');
250
+ tokens[`--figma-${extracted.selector.slice(1)}-fill`] = hex;
251
+ }
252
+
253
+ // Legacy component presence list
254
+ if (node.type === 'COMPONENT' || node.type === 'INSTANCE') {
255
+ components.push({ name: node.name, selector: extracted.selector });
256
+ }
257
+ }
258
+
259
+ for (const child of (node.children ?? [])) walk(child);
260
+ }
261
+
262
+ walk(doc);
263
+
264
+ return {
265
+ nodes,
266
+ frame: {
267
+ name: doc.name ?? '',
268
+ x: frameX,
269
+ y: frameY,
270
+ width: doc.absoluteBoundingBox?.width ?? 0,
271
+ height: doc.absoluteBoundingBox?.height ?? 0,
272
+ },
273
+ // Legacy fields — still consumed by backward-compat token comparison path
274
+ tokens,
275
+ components,
276
+ };
277
+ }
278
+
279
+ // ── Public API ────────────────────────────────────────────────────────────────
280
+
281
+ /**
282
+ * Fetch the full design spec for a Figma frame URL.
283
+ *
284
+ * Returns null when FIGMA_API_TOKEN is unset, the URL is unparseable,
285
+ * or the Figma API returns an error. All errors are logged at warn level.
286
+ *
287
+ * @param {string} figmaFrameUrl
288
+ * @returns {Promise<{nodes, frame, tokens, components}|null>}
289
+ */
290
+ export async function getFigmaFrame(figmaFrameUrl) {
291
+ const token = process.env.FIGMA_API_TOKEN;
292
+ if (!token) {
293
+ logger.debug('[ARGUS] figma-adapter: FIGMA_API_TOKEN not set — skipping design fidelity fetch');
294
+ return null;
295
+ }
296
+
297
+ const parsed = parseFigmaUrl(figmaFrameUrl);
298
+ if (!parsed) {
299
+ logger.warn(`[ARGUS] figma-adapter: cannot parse Figma URL: ${figmaFrameUrl}`);
300
+ return null;
301
+ }
302
+
303
+ const { fileKey, nodeId } = parsed;
304
+ const encodedId = nodeId.replace(':', '-');
305
+ const apiUrl = `${FIGMA_API}/files/${fileKey}/nodes?ids=${encodedId}&geometry=paths`;
306
+
307
+ try {
308
+ const res = await fetch(apiUrl, {
309
+ headers: { 'X-Figma-Token': token },
310
+ signal: AbortSignal.timeout(15000),
311
+ });
312
+
313
+ if (!res.ok) {
314
+ logger.warn(`[ARGUS] figma-adapter: Figma API ${res.status} for ${figmaFrameUrl}`);
315
+ return null;
316
+ }
317
+
318
+ const data = await res.json();
319
+ const result = parseFigmaNodes(data, nodeId);
320
+
321
+ if (!result) {
322
+ logger.warn(`[ARGUS] figma-adapter: no node data for nodeId "${nodeId}" in ${figmaFrameUrl}`);
323
+ return null;
324
+ }
325
+
326
+ logger.info(
327
+ `[ARGUS] figma-adapter: extracted ${result.nodes.length} node(s) from "${result.frame.name}" ` +
328
+ `(${result.frame.width}×${result.frame.height})`
329
+ );
330
+ return result;
331
+
332
+ } catch (err) {
333
+ logger.warn(`[ARGUS] figma-adapter: fetch failed for ${figmaFrameUrl}: ${err.message}`);
334
+ return null;
335
+ }
336
+ }
@@ -14,7 +14,22 @@ const VALID_SEVERITIES = new Set(['critical', 'warning', 'info']);
14
14
  /**
15
15
  * Create an immutable finding object.
16
16
  *
17
- * @param {{ type: string, severity: string, message: string, url?: string }} opts
17
+ * Required fields: type, severity ('critical'|'warning'|'info'), message.
18
+ * Common optional fields passed via ...rest (use these names for consistency):
19
+ * url — affected URL (defaults to '')
20
+ * selector — CSS selector of the offending element
21
+ * requestUrl — URL of the offending network request
22
+ * status — HTTP status code (number)
23
+ * method — HTTP method string
24
+ * element — human-readable element description (e.g. 'button#submit')
25
+ * property — CSS property name
26
+ * metric — performance metric name (e.g. 'LCP')
27
+ * value — measured value
28
+ * budget — expected threshold value
29
+ * count — numeric count of occurrences
30
+ * source — source file or stylesheet path
31
+ *
32
+ * @param {{ type: string, severity: string, message: string, url?: string, [key: string]: any }} opts
18
33
  * @returns {Readonly<object>}
19
34
  */
20
35
  export function createFinding({ type, severity, message, url = '', ...rest }) {
package/src/mcp-server.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * Argus MCP Server (v9.5.1)
3
+ * Argus MCP Server (v9.5.3)
4
4
  *
5
5
  * Exposes Argus as an MCP server so Claude (or any MCP client) can call
6
6
  * argus_audit, argus_audit_full, argus_compare, argus_last_report, and
@@ -30,6 +30,8 @@ import { crawlRouteCheap, runCrawl } from './orchestration/crawl-and-re
30
30
  import { runComparison } from './orchestration/env-comparison.js';
31
31
  import { WatchSession } from './orchestration/watch-mode.js';
32
32
  import { CdpBrowserAdapter } from './adapters/browser.js';
33
+ import { getFigmaFrame } from './adapters/figma.js';
34
+ import { analyzeDesignFidelity } from './utils/design-fidelity-analyzer.js';
33
35
 
34
36
  const REPORTS_DIR = path.resolve(process.cwd(), 'reports');
35
37
 
@@ -117,6 +119,18 @@ const TOOLS = [
117
119
  },
118
120
  },
119
121
  },
122
+ {
123
+ name: 'argus_design_audit',
124
+ description: 'Full design-to-implementation fidelity audit against a Figma frame. 13 mismatch finding types: CSS token values, component presence, fill/text color (RGB delta), typography (fontSize/fontWeight/lineHeight/fontFamily/letterSpacing), Auto Layout padding and gap, border-radius (per-corner), bounding-box overflow, absolute position drift (scroll-corrected x/y, 20px threshold), border stroke (color+weight), box-shadow (offset+blur+spread+color), opacity, and text content. Selector fallback: tries [data-testid], [aria-label], #id, .class per node. Requires FIGMA_API_TOKEN env var and Chrome on --remote-debugging-port=9222. Returns { findings, summary } where summary includes 13 mismatch-type counts.',
125
+ inputSchema: {
126
+ type: 'object',
127
+ properties: {
128
+ url: { type: 'string', description: 'Full URL of the page to audit (e.g. http://localhost:3000/dashboard). Must be reachable by the running Chrome instance.' },
129
+ figmaFrameUrl: { type: 'string', description: 'Figma frame URL to fetch design tokens from (e.g. https://www.figma.com/file/ABC123/Name?node-id=42%3A0). Must include the node-id query parameter pointing to the specific frame.' },
130
+ },
131
+ required: ['url', 'figmaFrameUrl'],
132
+ },
133
+ },
120
134
  ];
121
135
 
122
136
  // ── Helpers ───────────────────────────────────────────────────────────────────
@@ -129,7 +143,7 @@ async function withMcp(fn) {
129
143
  logger.error('[ARGUS] MCP tool handler error:', err.message);
130
144
  throw err;
131
145
  } finally {
132
- try { mcp.close(); } catch { /* ignore process already gone */ }
146
+ try { mcp.close(); } catch (e) { logger.debug({ err: e }, 'mcp close (ignored)'); }
133
147
  }
134
148
  }
135
149
 
@@ -268,6 +282,42 @@ async function handleGetContext({ url, snapshot_id: prevId, tabId } = {}) {
268
282
  });
269
283
  }
270
284
 
285
+ async function handleDesignAudit({ url, figmaFrameUrl }) {
286
+ if (!url) throw new Error('argus_design_audit: url is required');
287
+ if (!figmaFrameUrl) throw new Error('argus_design_audit: figmaFrameUrl is required');
288
+
289
+ const figmaData = await getFigmaFrame(figmaFrameUrl);
290
+ if (!figmaData) {
291
+ return { content: [{ type: 'text', text: JSON.stringify({
292
+ error: 'Could not fetch Figma data. Ensure FIGMA_API_TOKEN is set and the figmaFrameUrl is valid.',
293
+ findings: [],
294
+ summary: { tokenMismatches: 0, missingComponents: 0, colorMismatches: 0, typographyMismatches: 0, spacingMismatches: 0, radiusMismatches: 0, boundsOverflows: 0, positionDrifts: 0, strokeMismatches: 0, shadowMismatches: 0, opacityMismatches: 0, gapMismatches: 0, textMismatches: 0 },
295
+ }) }] };
296
+ }
297
+
298
+ return withMcp(async (mcp) => {
299
+ const browser = new CdpBrowserAdapter(mcp);
300
+ const findings = await analyzeDesignFidelity(browser, url, figmaData);
301
+ const count = (type) => findings.filter(f => f.type === type).length;
302
+ const summary = {
303
+ tokenMismatches: count('design_token_mismatch'),
304
+ missingComponents: count('design_component_missing'),
305
+ colorMismatches: count('design_color_mismatch'),
306
+ typographyMismatches: count('design_typography_mismatch'),
307
+ spacingMismatches: count('design_spacing_mismatch'),
308
+ radiusMismatches: count('design_radius_mismatch'),
309
+ boundsOverflows: count('design_bounds_overflow'),
310
+ positionDrifts: count('design_position_drift'),
311
+ strokeMismatches: count('design_stroke_mismatch'),
312
+ shadowMismatches: count('design_shadow_mismatch'),
313
+ opacityMismatches: count('design_opacity_mismatch'),
314
+ gapMismatches: count('design_gap_mismatch'),
315
+ textMismatches: count('design_text_mismatch'),
316
+ };
317
+ return { content: [{ type: 'text', text: JSON.stringify({ findings, summary }, null, 2) }] };
318
+ });
319
+ }
320
+
271
321
  async function handleLastReport() {
272
322
  if (!fs.existsSync(REPORTS_DIR)) {
273
323
  return { content: [{ type: 'text', text: '{"error":"No reports found in reports/"}' }] };
@@ -286,7 +336,7 @@ async function handleLastReport() {
286
336
  // ── Server bootstrap ──────────────────────────────────────────────────────────
287
337
 
288
338
  const server = new Server(
289
- { name: 'argus', version: '9.5.1' },
339
+ { name: 'argus', version: '9.5.3' },
290
340
  { capabilities: { tools: {} } },
291
341
  );
292
342
 
@@ -301,6 +351,7 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
301
351
  case 'argus_last_report': return await handleLastReport();
302
352
  case 'argus_watch_snapshot': return await handleWatchSnapshot(req.params.arguments ?? {});
303
353
  case 'argus_get_context': return await handleGetContext(req.params.arguments ?? {});
354
+ case 'argus_design_audit': return await handleDesignAudit(req.params.arguments ?? {});
304
355
  default: throw new Error(`Unknown tool: ${req.params.name}`);
305
356
  }
306
357
  } catch (err) {
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Argus Report Dispatcher (v9.3.0)
2
+ * Argus Report Dispatcher
3
3
  *
4
4
  * Dispatches a completed report to Slack, GitHub, and/or HTML.
5
5
  * Extracted from crawl-and-report.js god object.