feat: Sprint 6 — Advanced Analysis Engine (18 features) - #4
Conversation
Add comprehensive design analysis capabilities spanning deterministic lint rules, AI-powered review pipelines, and design system compliance: **Deterministic Lint Modules (plugin sandbox):** - Fitts's Law calculator (target size, consecutive click difficulty, thumb zones) - Gestalt proximity/continuity checker (spatial clustering, alignment) - Detached instance detection (via detachedInfo API + heuristic fallback) - Dark mode validation (pure black/white, missing mode values, contrast) - Responsive design prediction (fixed widths, truncation risk, layoutWrap) - Real-time incremental linting via documentchange events **Design System Compliance (plugin sandbox):** - DTCG token parser (W3C .tokens.json with $type inheritance) - Token compliance engine (Levenshtein fuzzy matching, adoption scoring) - Variable system collector (collections, modes, consumers, adoption rate) - Mode comparator (light/dark value diffing) - Design debt composite score (orphaned styles, detached instances, hardcoded values) **AI Analysis Pipelines (backend):** - Attention prediction + Nielsen's 10 Heuristics (6 automatable) - Cognitive Walkthrough (multi-image, 4 CW questions per step) - PURE scoring (3 parallel evaluators: UX Designer, A11y, Business Analyst) - Brand consistency analysis (color tolerance, typography, spacing, personality) - Copy/tone consistency across flow (text-only, no screenshots) - Persona-based mock research (5 parallel personas, universal barrier detection) - Dark mode AI validation (side-by-side light/dark comparison) - Responsive design AI validation - Accessibility spec generator - Whole-page sweep (lint + screenshot all frames, File Health Report) **UI:** - PageSweepCard component (file health, frame grid, AI insights) - Sweep Page button in QuickActions and bottom bar Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: ⛔ Files ignored due to path filters (2)
📒 Files selected for processing (6)
📝 WalkthroughWalkthroughДобавлены многочисленные подсказки, сервисы и маршруты для расширенного анализа дизайна (a11y, бренд, когнитивная прогулка, тон копии, персоны, pure‑scoring, responsive, dark‑mode, page‑sweep и др.), расширена система lint‑проверок и поддержка переменных/токенов; UI получил новые карточки и страницы отчётов. Changes
Sequence Diagram(s)sequenceDiagram
participant UI as "Plugin UI"
participant Router as "Backend /api/analyze-page"
participant Analyzer as "PageSweepAnalyzer"
participant Model as "Anthropic / Claude"
participant DB as "Optional storage / session"
UI->>Router: POST /api/analyze-page (frames, sessionId)
Router->>Analyzer: analyzePageSweep(request)
Analyzer->>Model: send images + prompt (per-frame summaries)
Model-->>Analyzer: JSON insights (strengths, weaknesses, recommendations)
Analyzer->>DB: (opt) store aiInsights / fileHealth
Analyzer-->>Router: PageSweepResult (fileHealth, frames, aiInsights)
Router-->>UI: 200 { pageSweepRawData / aiInsights }
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Possibly related PRs
Poem
✨ Finishing Touches
🧪 Generate unit tests (beta)
|
ⓘ You are approaching your monthly quota for Qodo. Upgrade your plan Review Summary by QodoSprint 6 — Advanced Analysis Engine with 18 Design Analysis Features
WalkthroughsDescriptionComprehensive Sprint 6 implementation adding 18 advanced design analysis capabilities across the full stack: **Deterministic Lint Modules (Plugin)** • fitts-law.ts — Interactive target size validation (44x44px minimum) • gestalt.ts — Gestalt proximity principle checking for spacing consistency • detached-instance.ts — Detached component instance detection with heuristics • responsive.ts — Responsive design validation with breakpoint detection • dark-mode.ts — Dark mode validation (pure black/white, contrast, elevation checks) • realtime-lint.ts — Real-time incremental linting with debounced updates **Design System Compliance (Plugin)** • dtcg-parser.ts — W3C DTCG .tokens.json format parser with type inheritance • token-compliance.ts — Token compliance engine with Levenshtein fuzzy matching • variable-collector.ts — Full Figma variable system extraction and adoption tracking • mode-comparator.ts — Light/dark variable value diffing and comparison • design-debt.ts — Composite design debt score calculator (0-100) **AI Analysis Pipelines (Backend — 11 new routes)** • extended-analyzer.ts — Attention analysis + Nielsen's 6 automatable heuristics • cognitive-walkthrough.ts — Multi-frame cognitive walkthrough with 4 CW questions • pure-scoring.ts — 3 parallel evaluators (UX Designer, A11y Specialist, Business Analyst) • brand-consistency.ts — Color, typography, spacing, personality evaluation • copy-tone.ts — Text-only copy/tone consistency analysis (no screenshots) • persona-research.ts — 5 parallel personas with universal barrier detection • responsive-validator.ts — Responsive design validation across breakpoints • a11y-spec-generator.ts — WCAG 2.2 accessibility specification generation • page-sweep-analyzer.ts — Whole-page sweep with frame scoring and AI insights **UI Components & Integration** • PageSweepCard.tsx — File health overview with 3-column frame grid and collapsible insights • "Sweep Page" button in QuickActions and action bar • Message handlers for page sweep, variable collection, DTCG compliance, dark mode comparison • Real-time lint enable/disable with debounce configuration • Design debt calculation handler **Type System & API** • Extended LintErrorType with 4 new types: fittsLaw, gestalt, detachedInstance, responsive • Added PageSweepData and PageSweepRawData types for sweep results • Added 8 new UIMessageType entries for advanced analysis features • 11 new backend routes with comprehensive validation • Figma API type definitions for variable system **Verification** • TypeScript compilation passes (npx tsc --noEmit) • Plugin builds successfully (240KB) • UI builds successfully (282KB gzipped) • Snyk code scan: 0 security issues Diagramflowchart LR
Plugin["Plugin Layer<br/>(Lint + Extract)"]
Backend["Backend Layer<br/>(AI Analysis)"]
UI["UI Layer<br/>(Display)"]
Plugin -->|Lint Results| Backend
Plugin -->|Variable Data| Backend
Backend -->|Analysis Results| UI
LintModules["Lint Modules<br/>Fitts/Gestalt/Detached<br/>Responsive/DarkMode"]
Extract["Extract Services<br/>Variables/DTCG<br/>DesignDebt"]
LintModules -->|Deterministic Checks| Plugin
Extract -->|System Data| Plugin
AIServices["AI Services<br/>Attention/Nielsen<br/>CogWalk/PURE<br/>Brand/Copy/Persona<br/>A11y/Responsive"]
Prompts["Prompt Builders<br/>Multi-evaluator<br/>Multi-persona<br/>Multi-dimension"]
Prompts -->|Structured Prompts| AIServices
AIServices -->|Scored Results| Backend
PageSweep["Page Sweep<br/>Frame Scoring<br/>Consistency Analysis"]
Backend -->|Aggregated Data| PageSweep
PageSweep -->|File Health Report| UI
PageSweepCard["PageSweepCard<br/>Grid + Insights<br/>Scores + Issues"]
UI -->|Render| PageSweepCard
File Changes1. src/lint/responsive.ts
|
Code Review by Qodo
1. Realtime lint invalid nodes
|
There was a problem hiding this comment.
Actionable comments posted: 1
Note
Due to the large number of review comments, Critical severity comments were prioritized as inline comments.
🟠 Major comments (26)
backend/src/routes/dark-mode.ts-129-129 (1)
129-129:⚠️ Potential issue | 🟠 MajorОтсутствует обработка ошибок при парсинге JSON от AI.
JSON.parseможет выбросить исключение, если AI вернёт невалидный JSON (даже если regex нашёл{...}). Это приведёт к 500 ошибке с generic сообщением вместо информативного.🛡️ Предлагаемое исправление
- const parsed = JSON.parse(jsonMatch[0]) as AiComparisonResult; + let parsed: AiComparisonResult; + try { + parsed = JSON.parse(jsonMatch[0]) as AiComparisonResult; + } catch { + return c.json({ error: 'Invalid JSON in AI response' }, 500); + }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@backend/src/routes/dark-mode.ts` at line 129, The code calls JSON.parse directly on jsonMatch[0] (assigned to parsed as AiComparisonResult) without error handling; wrap the JSON.parse(jsonMatch[0]) call in a try/catch around the area where jsonMatch is used (the block that assigns parsed) to catch SyntaxError or other parse issues, log the raw jsonMatch[0] and the error (using the same logger used in this file), and return a controlled response (e.g., a 400/422 with an informative message or a structured error) instead of letting an uncaught exception propagate; also validate jsonMatch exists before parsing and consider using a small schema/type check on the resulting parsed object to ensure it matches the expected AiComparisonResult shape.src/extract/variable-collector.ts-57-90 (1)
57-90:⚠️ Potential issue | 🟠 Major
consumersсейчас считает не узлы, а отдельные привязки.По контракту
VariableData.consumers— это количество узлов, использующих переменную. Здесь счётчик увеличивается на каждое найденное binding-вхождение, поэтому один узел с той же переменной в нескольких свойствах завышает число; одновременно реально проверяются толькоfills, хотя комментарий обещает ещёstrokes. Из-за этогоconsumersиunusedVariablesстановятся недостоверными.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/extract/variable-collector.ts` around lines 57 - 90, The code currently increments consumerMap for every binding occurrence, inflating VariableData.consumers; also it inspects fills but not strokes. Change the logic in the block that examines boundVariables (and the subsequent fills loop) to collect unique variable ids seen for the node (e.g., use a local Set seenIds), include strokes in the same scan (n.strokes similar to n.fills), and after scanning node.boundVariables, n.fills and n.strokes, increment consumerMap once per unique id for that node (and set nodeBound = true if seenIds non-empty). Update references around consumerMap, nodeBound, n.boundVariables, n.fills and n.strokes so consumers counts reflect number of nodes using each variable, not number of bindings.backend/src/routes/persona-research.ts-26-32 (1)
26-32:⚠️ Potential issue | 🟠 MajorОграничьте размер
screenshotдо запуска анализа.Сейчас любая base64-строка уходит дальше без лимита, а в
backend/src/services/persona-research.ts(Lines 286-289) один запрос веером превращается сразу в 5 вызовов Anthropic. Большой payload здесь легко раздует память процесса, стоимость и шанс получить upstream rejection.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@backend/src/routes/persona-research.ts` around lines 26 - 32, Добавьте ограничение размера для поля screenshot в обработчике запроса в backend/src/routes/persona-research.ts: помимо проверки на строку, валидируйте что это корректный base64 и что декодированный размер не превышает разумного лимита (например 2MB) — можно оценить размер по длине base64 (approxBytes = Math.floor(s.length * 3 / 4)) или попыткой декодирования и проверки длины байтов; при превышении возвращайте c.json({ error: 'screenshot too large' }, 400). Это предотвратит отправку больших payload-ов в сервис persona-research (backend/src/services/persona-research.ts) который фаново делает несколько вызовов Anthropic.backend/src/services/responsive-validator.ts-54-61 (1)
54-61:⚠️ Potential issue | 🟠 MajorОтклоняйте запросы без минимум двух вариантов.
Сейчас сервис строит сравнительный prompt даже для 0/1 скриншота, поэтому модель вернёт правдоподобный, но недостоверный отчёт. Лучше валидировать
req.variants.length >= 2до вызова Anthropic.💡 Локальная правка
export async function validateResponsiveDesign( req: ResponsiveValidationRequest, ): Promise<ResponsiveValidationResult> { + if (req.variants.length < 2) { + throw new Error('Responsive validation requires at least two variants'); + } + const client = getAnthropicClient();🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@backend/src/services/responsive-validator.ts` around lines 54 - 61, validateResponsiveDesign currently builds and sends a comparison prompt even when req.variants has fewer than two items; add an early validation at the start of validateResponsiveDesign that checks req.variants.length >= 2 and rejects the request (throw an Error or return a failed ResponsiveValidationResult) before calling getAnthropicClient or buildResponsiveComparisonPrompt, referencing the req.variants array and the validateResponsiveDesign function so callers get a clear error for insufficient variants.src/baseline/design-debt.ts-75-78 (1)
75-78:⚠️ Potential issue | 🟠 Major
detachedInstancesнадо считать поnodeId, а не по количеству ошибок.Если один и тот же detached node породит несколько lint-сообщений с
detachв тексте, итоговый debt score снимет штраф несколько раз за один инстанс. Здесь безопаснее дедуплицировать поnodeId, как вы уже делаете дляnamingViolations.💡 Локальная правка
- const detachedInstanceErrors = errors.filter( - e => e.message.toLowerCase().includes('detach') - ); - const detachedInstances = detachedInstanceErrors.length; + const detachedInstances = new Set( + errors + .filter(e => e.message.toLowerCase().includes('detach')) + .map(e => e.nodeId), + ).size;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/baseline/design-debt.ts` around lines 75 - 78, The current computation of detachedInstances counts error entries instead of unique nodes, causing multiple "detach" messages from the same node to be counted multiple times; update the logic that builds detachedInstanceErrors (filter on errors where e.message.toLowerCase().includes('detach')) to extract e.nodeId, deduplicate by nodeId (e.g., via a Set or similar) and set detachedInstances to the number of unique nodeIds—mirror how namingViolations is deduplicated so each detached node only contributes once to the debt score.backend/src/services/persona-research.ts-285-290 (1)
285-290:⚠️ Potential issue | 🟠 MajorОдин сбой persona-call сейчас роняет весь отчёт.
Promise.allfail-fast: таймаут, 429 или невалидный JSON от одной персоны превращают весь запрос в 500, хотя остальные ответы уже готовы. Для такого LLM fan-out надёжнееPromise.allSettledс частичной деградацией результата.💡 Локальная правка
- const results = await Promise.all( - PERSONAS.map((persona) => - runSinglePersona(persona, screenshot, taskDescription, lintContext), - ), - ); + const settled = await Promise.allSettled( + PERSONAS.map((persona) => + runSinglePersona(persona, screenshot, taskDescription, lintContext), + ), + ); + + const results = settled.flatMap((result) => + result.status === 'fulfilled' ? [result.value] : [], + ); + if (results.length === 0) { + throw new Error('All persona evaluations failed'); + }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@backend/src/services/persona-research.ts` around lines 285 - 290, The current use of Promise.all over PERSONAS with runSinglePersona causes a single failure to reject the whole batch; replace Promise.all(...) with Promise.allSettled(...) and update the handling of the returned array (currently assigned to results) to iterate over the settled outcomes, keeping successful values (result.value) and logging or collecting failures (result.reason) so the service can produce a partial report instead of throwing; ensure you reference PERSONAS, runSinglePersona and the results variable when making these changes and convert downstream code that expects the previous all-success array to tolerate missing/partial persona outputs.backend/src/services/responsive-validator.ts-104-113 (1)
104-113:⚠️ Potential issue | 🟠 MajorНе приводите
recommendationsчерез голый cast.Категории вы нормализуете, а рекомендации пропускаете как есть. Любой кривой объект из модели уйдёт в API-ответ с
undefinedполями и сломает downstream-клиент, поэтому здесь нужен такой жеmap/filterс валидацией полей.💡 Локальная правка
- recommendations: Array.isArray(parsed.recommendations) - ? (parsed.recommendations as ResponsiveValidationResult['recommendations']) - : [], + recommendations: Array.isArray(parsed.recommendations) + ? parsed.recommendations.flatMap((item) => { + if (!item || typeof item !== 'object') return []; + const rec = item as Record<string, unknown>; + if (typeof rec.title !== 'string' || typeof rec.description !== 'string') { + return []; + } + return [{ + title: rec.title, + description: rec.description, + severity: + rec.severity === 'critical' || rec.severity === 'warning' || rec.severity === 'info' + ? rec.severity + : 'info', + breakpoints: Array.isArray(rec.breakpoints) + ? rec.breakpoints.filter((bp): bp is string => typeof bp === 'string') + : [], + }]; + }) + : [],🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@backend/src/services/responsive-validator.ts` around lines 104 - 113, The code currently casts parsed.recommendations directly to ResponsiveValidationResult['recommendations'], which can pass malformed objects downstream; instead validate and sanitize each entry: replace the bare cast with Array.isArray(parsed.recommendations) ? parsed.recommendations.map(normalizeRecommendation).filter(Boolean) : [], and implement a helper normalizeRecommendation(recommendation: unknown) that checks required fields/types (e.g., string fields, enums, numeric ratings) and returns a properly shaped recommendation or null/undefined for invalid items; reference normalizeRatingCategory and ResponsiveValidationResult['recommendations'] when adding the helper so the mapping mirrors your category normalization.backend/src/prompts/pure-scoring.ts-12-27 (1)
12-27:⚠️ Potential issue | 🟠 MajorРазрешите пустые массивы в формате ответа, иначе модель начнёт выдумывать пункты.
Сейчас пример выглядит так, будто
strengthsвсегда содержит несколько элементов, аissues— минимум один. Для чистых экранов модель обычно подгоняет ответ под такой шаблон и начинает придумывать лишние замечания вместо честного[].🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@backend/src/prompts/pure-scoring.ts` around lines 12 - 27, Update the SHARED_RESPONSE_FORMAT template in pure-scoring.ts to explicitly allow empty arrays for "strengths" and "issues" so the model can return [] instead of inventing items; modify the JSON example to show both "strengths": [] and "issues": [] as valid values (or annotate with "[] or [...items]") and ensure the description text no longer implies there must be multiple strengths or at least one issue, referencing the SHARED_RESPONSE_FORMAT constant to locate the change.backend/src/prompts/cognitive-walkthrough.ts-20-27 (1)
20-27:⚠️ Potential issue | 🟠 MajorНе заставляйте walkthrough идти по порядку кадров вместо реальных переходов.
Фраза
transition from one frame to the nextлинейризует flow поframeLabels, даже еслиedgeDescriptionsописывает ветвления или возвраты. В итоге модель начнёт анализировать вымышленные шаги между соседними экранами, которых в графе нет.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@backend/src/prompts/cognitive-walkthrough.ts` around lines 20 - 27, The prompt currently forces a linear walkthrough by saying "transition from one frame to the next" which ignores branching described in edgeDescriptions; change the instruction to iterate over the actual graph edges (use edgeDescriptions) — e.g., replace "For EACH step (transition from one frame to the next)" with wording like "For EACH transition described in the edgeDescriptions (each edge in the flow graph, including branches and returns)" and ensure the code that assembles the prompt (the template using edgeDescriptions and interactiveElementDescriptions) explicitly instructs the model to use edgeDescriptions as the source of steps and only fall back to linear neighboring frames if edgeDescriptions is empty.src/lint/detached-instance.ts-47-76 (1)
47-76:⚠️ Potential issue | 🟠 MajorДетектор почти ничего не найдёт: единственная рабочая эвристика — слово
detachв имени.Ветка
looksLikeComponentна Lines 68-76 сейчас ничего не репортит, поэтому реальный issue создаётся только если дизайнер сам переименовал frame сdetachв названии. Большинство реальных detached-инстансов так не называются, значит модуль даст очень низкий recall.src/lint/gestalt.ts-53-68 (1)
53-68:⚠️ Potential issue | 🟠 MajorТекущая proximity-эвристика даст много ложных срабатываний на сетках и микро-сдвигах.
Вы сортируете всех детей только по
Y, поэтому multi-column layout перемешивается в один список, аMath.round(g)считает 8px/9px/10px тремя разными gap'ами. Это начнёт флагать нормальные grid/карточки как нарушение близости.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/lint/gestalt.ts` around lines 53 - 68, The proximity heuristic is over-triggering because children are simply sorted by 'y' (mixing columns) and gaps are distinctified with Math.round; update the logic in the block using visible, sorted, gaps and uniqueGaps to first cluster children into columns (e.g., group items whose x positions overlap or are within a small horizontal tolerance) and compute vertical gaps per-column rather than across the whole list, and replace Math.round(g) with a tolerance-based bucketing (e.g., Math.round(g / T) or comparing Math.abs(delta) <= T) so micro-shifts (8/9/10px) are treated as the same gap; keep the resulting uniqueGaps size check (> 2) but apply it per-column or on clustered gap buckets.src/lint/fitts-law.ts-5-11 (1)
5-11:⚠️ Potential issue | 🟠 MajorСейчас это только проверка min-size, а не полноценный Fitts's Law lint.
Модуль считает
totalChecked/passed/failedкак будто покрыл весь набор Fitts-проверок, но код ниже анализирует только44x44. Расстояние между последовательными действиями и thumb-zone здесь вообще не учитываются, поэтому downstream UI получит вводящий в заблуждение итог.Also applies to: 46-67, 81-107
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/lint/fitts-law.ts` around lines 5 - 11, Модуль сейчас делает только проверку минимального размера (44x44) но всё равно заполняет агрегаты totalChecked/passed/failed как будто выполнил полный набор Fitts-проверок — исправь это: либо ограничь отчётность только выполненной проверкой (корректно инкрементировать totalChecked/passed/failed в функции, которая реально проверяет targetSize и убрать/не использовать имена и поля, предполагающие полный Fitts-анализ), либо реализуй недостающие проверки Fitts (расчёт расстояния между последовательными действиями, учёт thumb-zone/handedness и соответствующие пороги) и включи их в ту же агрегирующую логику; конкретные символы для правки: totalChecked, passed, failed, место где сравнивается с 44 (threshold/targetSize check) и основная экспортируемая функция/класс в модуле fitts-law (обнови логику подсчёта и/или название/докстринг модуля чтобы отражать, что это только min-size lint, если не реализуешь остальные проверки).ui/src/App.tsx-594-597 (1)
594-597:⚠️ Potential issue | 🟠 MajorПеред новым page sweep нужно останавливать незавершённые async-процессы.
Эта ветка, в отличие от
handleAnalyze, не abort'ит streaming chat и не очищаетreferoPollingRef. Если пользователь запустит sweep поверх предыдущего анализа, старые callbacks продолжат писать в чат и перемешают результаты.Предлагаемое исправление
case 'analyze-page': { + abortControllerRef.current?.abort(); + abortControllerRef.current = null; + if (referoPollingRef.current) { + clearInterval(referoPollingRef.current); + referoPollingRef.current = null; + } chat.addMessage({ kind: 'ai-text', content: 'Starting whole-page sweep...' }); post('analyze-page'); break; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@ui/src/App.tsx` around lines 594 - 597, В ветке case 'analyze-page' нужно перед запуском нового sweep останавливать незавершённые async-процессы — как это делает handleAnalyze: прервать/abort текущий streaming chat (вызвать тот же abort/cleanup для стриминга, например функцию, которая останавливает streaming chat) и очистить referoPollingRef (clearInterval/clearTimeout и обнулить referoPollingRef.current) перед добавлением сообщения и вызовом post('analyze-page'), чтобы старые коллбэки не писали в чат и не мешали новому анализу.ui/src/App.tsx-806-810 (1)
806-810:⚠️ Potential issue | 🟠 MajorФормула fallback-score смешивает количество ошибок и количество узлов.
На Line 808 вы вычитаете
errors.lengthизtotalNodes, хотя одна нода может иметь несколько lint-ошибок. На маленьких фреймах это резко занижает score; в raw-контракте уже естьnodesWithErrors, который соответствует этой шкале намного лучше.Предлагаемое исправление
const errors = frame.lintResult.errors; const total = Math.max(frame.lintResult.summary.totalNodes, 1); const weightedFailed = errors.reduce((sum, e) => sum + (SEVERITY_WEIGHT[e.severity || 'warning'] || 3), 0); - const weightedPassed = Math.max(0, total - errors.length) * 10; + const failedNodes = Math.min(frame.lintResult.summary.nodesWithErrors, total); + const weightedPassed = Math.max(0, total - failedNodes) * 10; const t = weightedPassed + weightedFailed; const score = t > 0 ? Math.round((weightedPassed / t) * 100) : 100;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@ui/src/App.tsx` around lines 806 - 810, The fallback-score calculation mixes error count and node count by subtracting errors.length from frame.lintResult.summary.totalNodes; replace that logic to use the number of nodes that actually have errors (frame.lintResult.summary.nodesWithErrors) so one node with multiple errors isn't over-penalized: update the computation that derives weightedPassed (and therefore total t and score) to use nodesWithErrors instead of errors.length, keeping SEVERITY_WEIGHT and the rest of the weighting logic unchanged (refer to variables total, weightedFailed, weightedPassed, t, score and frame.lintResult.summary.nodesWithErrors).src/lint/dark-mode.ts-45-64 (1)
45-64:⚠️ Potential issue | 🟠 MajorНе завязывайте проверку на конкретные названия режимов.
Если коллекция называет режимы не
Light/Dark/Default,darkModeName/lightModeNameостанутсяnull,darkValueвыпадет из анализа, и модуль вернет “0 проблем” по фактически непроверенным цветам. Лучше получать явную пару режимов отcompareModesили падать явно, а не silently skip.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/lint/dark-mode.ts` around lines 45 - 64, The current logic in lint/dark-mode.ts relies on matching mode names (darkModeName/lightModeName) from modeData.modes which can be null and causes silent skips; change the flow to accept an explicit mode pair from compareModes (or another upstream function) and use those names to index modeData.variableDiffs, and if compareModes does not provide a definitive pair, fail loudly (throw or processLogger.error + return non-zero) instead of silently continuing; update references to darkModeName/lightModeName and the loop over modeData.variableDiffs to use the provided pair, and ensure the function/documentation signature reflects the required explicit mode pair.backend/src/services/extended-analyzer.ts-160-163 (1)
160-163:⚠️ Potential issue | 🟠 MajorЗдесь конфликтуют system prompt и ожидаемая схема Nielsen.
SYSTEM_PROMPTизbackend/src/prompts/system.tsзапрещает numeric scores, аbuildNielsenHeuristicsPrompt()требуетoverallCompliance: 0-100. В такой конфигурации модель с заметной вероятностью вернет текстовый рейтинг или пропустит поле, аnormalizeNielsen()тихо сведет это к0.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@backend/src/services/extended-analyzer.ts` around lines 160 - 163, The SYSTEM_PROMPT currently forbids numeric scores which conflicts with buildNielsenHeuristicsPrompt that requires overallCompliance: 0-100 and causes normalizeNielsen to collapse missing/textual ratings to 0; fix by ensuring the Nielsen heuristic instructions are authoritative and allow numeric outputs—either remove/relax the numeric prohibition in SYSTEM_PROMPT or pass buildNielsenHeuristicsPrompt as the system-level prompt (or prepend it) when calling anthropic.messages.create so the model is explicitly instructed to return numeric overallCompliance (0-100); verify normalizeNielsen still expects and handles numeric values.src/lint/dark-mode.ts-157-171 (1)
157-171:⚠️ Potential issue | 🟠 Major
summary.failedсейчас считает issues, а не проверенные элементы.Один
variableDiffможет породить несколько issues, поэтомуpassed = totalChecked - failedздесь легко уходит в отрицательные или просто вводящие в заблуждение значения. Сводку нужно строить по уникальным проверяемым сущностям, а не по длинеissues.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/lint/dark-mode.ts` around lines 157 - 171, summary.failed currently uses the length of issues (issues) which can be >1 per checked item; compute failed as the number of unique checked entities that produced at least one issue instead. Build a Set of unique identifiers from issues (e.g., variable identifier or missing value key referenced in each issue) and set summary.failed = uniqueSet.size; then compute passed = totalChecked - summary.failed; update references around totalChecked, modeData.variableDiffs and modeData.missingValues to ensure the identifiers you collect map to those checked items.src/baseline/token-compliance.ts-55-74 (1)
55-74:⚠️ Potential issue | 🟠 MajorНе помечайте неиспользуемые переменные как использованные токены.
matchedDTCGNamesпополняется даже дляvariable.consumers === 0. Из-за этогоorphanTokensзанижается: токен считается “used in design”, хотя в дизайне он нигде не применяется.💡 Пример правки
if (dtcgByName.has(normalizedName)) { - matchedDTCGNames.add(normalizedName); + if (variable.consumers > 0) { + matchedDTCGNames.add(normalizedName); + } matched.push({ token: variable.name, nodeCount: variable.consumers, @@ const nearest = findNearestToken(normalizedName, dtcgTokens); if (nearest && nearest.distance <= 3) { - matchedDTCGNames.add(normalizeName(nearest.token.name)); + if (variable.consumers > 0) { + matchedDTCGNames.add(normalizeName(nearest.token.name)); + } matched.push({ token: variable.name, nodeCount: variable.consumers,Also applies to: 85-91
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/baseline/token-compliance.ts` around lines 55 - 74, matchedDTCGNames is being populated even when a variable is unused (variable.consumers === 0), causing orphanTokens to be underestimated; change both the exact-match branch (where dtcgByName.has(normalizedName)) and the fuzzy-match branch (where findNearestToken(...) is used) to only add normalizeName(...) to matchedDTCGNames when variable.consumers > 0, and ensure the matched.push usage field remains 'overridden' for consumers === 0 and 'correct' otherwise; update references: matchedDTCGNames, normalizeName, variable.consumers, findNearestToken, and the matched.push blocks accordingly.src/baseline/token-compliance.ts-137-142 (1)
137-142:⚠️ Potential issue | 🟠 MajorСначала
trim, потом нормализация пробелов.Текущий порядок превращает строки с внешними пробелами в другие имена. Например,
' color / primary 'послеreplace(/\s+/g, '-')уже не совпадет ни exact-, ни fuzzy-matching'ом так, как ожидается.💡 Пример правки
function normalizeName(name: string): string { return name - .replace(/\//g, '.') - .replace(/\s+/g, '-') - .toLowerCase() - .trim(); + .trim() + .replace(/\s*\/\s*/g, '.') + .replace(/\s+/g, '-') + .toLowerCase(); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/baseline/token-compliance.ts` around lines 137 - 142, The normalizeName function currently trims last which causes leading/trailing spaces to be converted into dashes and break matching; update normalizeName to call trim() first, then toLowerCase(), then perform .replace(/\//g, '.') and .replace(/\s+/g, '-') so input like ' color / primary ' becomes 'color.primary' as expected; ensure you only change the call order in normalizeName and keep the same replacement regexes.backend/src/services/page-sweep-analyzer.ts-91-149 (1)
91-149:⚠️ Potential issue | 🟠 MajorПустой запрос не должен давать
excellent.При
req.frames.length === 0сервис возвращаетoverallScore = 100иgrade = 'excellent'. Для backend endpoint это превращает невалидный payload в заведомо ложный health report.💡 Пример правки
export async function analyzePageSweep(req: PageSweepRequest): Promise<PageSweepResult> { + if (req.frames.length === 0) { + throw new Error('Page sweep requires at least one frame'); + } + // 1. Compute per-frame scores const frameResults: PageSweepFrameResult[] = req.frames.map((frame) => {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@backend/src/services/page-sweep-analyzer.ts` around lines 91 - 149, The analyzePageSweep logic treats an empty req.frames as a perfect file (overallScore = 100 and grade = getGrade(100)); add an explicit guard in analyzePageSweep that detects req.frames.length === 0 and returns a fileHealth/result representing an invalid/failed scan (e.g., set overallScore = 0, grade = getGrade(0) or a specific 'invalid' marker, consistencyScore = 0, totalFrames = 0, totalIssues = 0 and empty topIssues) instead of proceeding to compute averages; update any uses of overallScore/mean/variance (and the final PageSweepFileHealth construction) to rely on this early-return path so empty payloads no longer produce "excellent".backend/src/prompts/brand-consistency.ts-17-62 (1)
17-62:⚠️ Potential issue | 🟠 MajorНе смешивайте пользовательский brand guide с управляющими инструкциями.
Сейчас
usage,personality,rules.descriptionиlintContextпопадают в prompt как обычный prose-текст. Любой неожиданный markdown/переносы строк/фраза вроде “ignore previous instructions” сможет изменить поведение модели и сломать ожидаемый JSON-ответ.💡 Пример правки
export function buildBrandConsistencyPrompt( brandGuide: BrandGuide, lintContext: string, ): string { + const guideJson = JSON.stringify(brandGuide, null, 2); + const lintJson = JSON.stringify({ lintContext }, null, 2); + // Serialize color palette for the prompt const colorLines = Object.entries(brandGuide.colors) @@ - return `You are a brand design auditor. Evaluate the screenshot against the brand guidelines provided below. Be precise and cite specific visual evidence from the screenshot. + return `You are a brand design auditor. Evaluate the screenshot against the brand guidelines provided below. Be precise and cite specific visual evidence from the screenshot. + +## Brand Guide Data +\`\`\`json +${guideJson} +\`\`\` ## Brand Color Palette ${colorLines} @@ ## Automated Lint Context -${lintContext} +\`\`\`json +${lintJson} +\`\`\`🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@backend/src/prompts/brand-consistency.ts` around lines 17 - 62, The prompt currently injects freeform fields (brandGuide.colors.usage, brandGuide.personality, brandGuide.rules[].description, and lintContext) directly into prose which lets user-supplied markdown or instructions break the model output; update buildBrandConsistencyPrompt to serialize these values instead of inlining them — e.g., build a JSON-safe payload (use JSON.stringify or explicit escaping) for fields usage, personality, rules, and lintContext and insert that payload inside a clearly delimited data block (e.g., a code block labeled BRAND_DATA or a single-line JSON token) so the model treats them as data, not instructions; ensure you reference buildBrandConsistencyPrompt, brandGuide.rules, brandGuide.personality, and lintContext when changing the serialization and escaping logic.src/ui/message-handler.ts-1541-1552 (1)
1541-1552:⚠️ Potential issue | 🟠 MajorPage Sweep сейчас возвращает неполный результат без явного сигнала пользователю.
Фильтр берет только
FRAME | COMPONENT_SET, поэтому top-levelCOMPONENTпропускаются; затем список молча режетсяslice(0, 50). В итоге UI получает “готовый” sweep, хотя часть страницы вообще не анализировалась.💡 Пример правки
- const frames = allChildren.filter( - (node): node is FrameNode | ComponentSetNode => - node.type === 'FRAME' || node.type === 'COMPONENT_SET' - ); + const roots = allChildren.filter( + (node): node is FrameNode | ComponentNode | ComponentSetNode => + node.type === 'FRAME' || + node.type === 'COMPONENT' || + node.type === 'COMPONENT_SET' + ); - if (frames.length === 0) { + if (roots.length === 0) { sendMessageToUI('analysis-error', { error: 'No top-level frames found on current page.' }); return; } - const framesToAnalyze = frames.slice(0, 50); + if (roots.length > 50) { + sendMessageToUI('analysis-error', { + error: `Page sweep supports up to 50 top-level frames/components. Found ${roots.length}.`, + }); + return; + } + + const framesToAnalyze = roots;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/ui/message-handler.ts` around lines 1541 - 1552, The current top-level sweep filters only FRAME and COMPONENT_SET (the frames constant) and then silently truncates results with framesToAnalyze = frames.slice(0, 50), causing top-level COMPONENT nodes to be skipped and the UI not to be informed of partial analysis; update the filter in the frames variable to also include node.type === 'COMPONENT' (or use a membership check for 'FRAME'|'COMPONENT_SET'|'COMPONENT') and add a user-visible notification when frames.length > 50 (e.g., sendMessageToUI with a 'partial-sweep' or 'analysis-warning' event that includes total and analyzed counts) so the UI knows the sweep was truncated rather than appearing complete.src/ui/message-handler.ts-2494-2500 (1)
2494-2500:⚠️ Potential issue | 🟠 MajorRealtime lint не должен сбрасывать пользовательские настройки до дефолтных.
Если UI не передаст
settings, обработчик беретDEFAULT_LINT_SETTINGS, а неcurrentLintSettings. После сохранения team/user config live-lint начнет считать по другой конфигурации, чем обычный lint.💡 Пример правки
-function handleEnableRealtimeLint(data: { debounceMs?: number; settings?: LintSettings }): void { - const settings = data.settings || DEFAULT_LINT_SETTINGS; +function handleEnableRealtimeLint( + data: { debounceMs?: number; settings?: LintSettings } = {}, +): void { + const settings = data.settings ?? currentLintSettings; enableRealtimeLint({ enabled: true, debounceMs: data.debounceMs || 500,🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/ui/message-handler.ts` around lines 2494 - 2500, The handler handleEnableRealtimeLint currently falls back to DEFAULT_LINT_SETTINGS when data.settings is missing, which resets user/team config; change it to use the live currentLintSettings fallback instead. Update handleEnableRealtimeLint so settings is assigned from data.settings if provided, otherwise from currentLintSettings (keep debounceMs defaulting to data.debounceMs || 500), and then call enableRealtimeLint with enabled:true, debounceMs, and that settings object.backend/src/services/cognitive-walkthrough.ts-106-115 (1)
106-115:⚠️ Potential issue | 🟠 MajorПорядок экранов сейчас берётся не из графа, а из произвольного массива
frames.Промпт в
backend/src/prompts/cognitive-walkthrough.ts(Line 4-Line 71) прямо говорит модели, что screens are “presented in order” и что нужно оценить переходы step-by-step. Здесь же этот порядок строится просто изreq.frames, тогда как фактическая структура сценария живёт вreq.edges. Для ветвлений, loop’ов и просто несортированного input модель получит противоречивый flow и вернёт walkthrough не по реальному пути. Либо выводите порядок изedges, либо уберите из prompt утверждение про линейную последовательность экранов.Also applies to: 132-137
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@backend/src/services/cognitive-walkthrough.ts` around lines 106 - 115, The code builds frameLabels from req.frames (unordered) while the true flow is defined by req.edges; change the logic in cognitive-walkthrough.ts (where frameLabels and edgeDescriptions are created) to derive a linearized screen order from the graph in req.edges (e.g., find start node(s) with no incoming edges and perform a deterministic traversal/topological order following edges to produce an ordered array of frame IDs), then map those ordered IDs to names to build frameLabels and use the same ordering when generating edgeDescriptions; apply the same fix for the duplicate logic around lines 132-137 so the prompt's “presented in order” claim matches the actual edge-defined flow (alternatively, if you prefer not to linearize, remove the prompt assertion about linear order in backend/src/prompts/cognitive-walkthrough.ts).backend/src/services/pure-scoring.ts-165-179 (1)
165-179:⚠️ Potential issue | 🟠 MajorНе схлопывайте разные findings по общим словам.
Эвристика
overlapRatio >= 0.5здесь слишком агрессивна: фразы вродеbutton label is unclearиbutton label has low contrastсольются в одинcombinedIssue, хотя это разные проблемы. После этого вы ещё и эскалируете severity в один объект, поэтому итоговый отчёт теряет независимые findings и занижает реальный набор проблем.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@backend/src/services/pure-scoring.ts` around lines 165 - 179, The current similarity heuristic that computes overlapRatio and merges into combined (variables: combined, existing, overlapRatio, aWords, bWords) is too aggressive and collapses distinct findings; change the merge condition to be stricter by requiring both a higher overlap threshold (e.g., >= 0.7) AND at least one shared high-value token such as the same noun or bigram (use simple POS-filtering or require shared tokens of length>4 that appear in both as exact matches or shared bigrams), or replace the simple ratio with a normalized similarity on lemmatized tokens; additionally, when merging avoid blindly escalating severity—either keep separate severity entries or merge metadata without raising severity in the combinedIssue (only update counts), and update the logic in the existing assignment (where existing is found) accordingly.backend/src/services/a11y-spec-generator.ts-76-156 (1)
76-156:⚠️ Potential issue | 🟠 MajorНормализация сейчас падает на не-объектах внутри массивов.
Во всех ветках
Array.isArray(...)? ...map(...) : []элементы сразу читаются как объекты. Если модель вернёт[null],['oops']или[42]вlandmarks,ariaAnnotations,liveRegionsи т.д., доступ кl.role/a.element/lr.typeвыброситTypeError, и весь route уйдёт в 500 вместо safe fallback. Передmapнуженfilter(isRecord).🛡️ Предлагаемый фикс
+const isRecord = (value: unknown): value is Record<string, unknown> => + typeof value === 'object' && value !== null; + function normalizeA11ySpec(parsed: Record<string, unknown>): A11ySpec { return { landmarks: Array.isArray(parsed.landmarks) - ? (parsed.landmarks as A11ySpec['landmarks']).map((l) => ({ + ? parsed.landmarks.filter(isRecord).map((l) => ({ role: typeof l.role === 'string' ? l.role : '', label: typeof l.label === 'string' ? l.label : '', element: typeof l.element === 'string' ? l.element : '', })) : [],Тот же паттерн нужен для остальных массивов ниже.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@backend/src/services/a11y-spec-generator.ts` around lines 76 - 156, normalizeA11ySpec currently maps array entries assuming they are objects, causing TypeError on null/primitives; update normalizeA11ySpec to pre-filter each parsed.X array with a guard like isRecord (e.g., item !== null && typeof item === 'object') before mapping (apply to landmarks, headingStructure, focusOrder, ariaAnnotations, keyboardShortcuts, liveRegions, colorContrastReport, recommendations), or add a small helper isRecord and call .filter(isRecord) on each Array.isArray(parsed.xxx) branch so map only sees objects and fallback logic remains intact.
🟡 Minor comments (6)
backend/src/routes/persona-research.ts-42-69 (1)
42-69:⚠️ Potential issue | 🟡 Minor
nullне должен проходить как опциональная строка.Валидация явно пропускает
lintContext === null, после чегоnullуходит в сервис, где контракт ужеstring | undefined. Это либо вставит"null"в prompt, либо добавит скрытый runtime-case; то же самое стоит нормализовать дляsessionIdперед вызовомrunPersonaResearch.💡 Локальная правка
- const result = await runPersonaResearch( - body.screenshot, - body.taskDescription, - body.lintContext, - body.sessionId, - ); + const lintContext = + typeof body.lintContext === 'string' ? body.lintContext : undefined; + const sessionId = + typeof body.sessionId === 'string' ? body.sessionId : undefined; + + const result = await runPersonaResearch( + body.screenshot, + body.taskDescription, + lintContext, + sessionId, + );🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@backend/src/routes/persona-research.ts` around lines 42 - 69, The current validation allows body.lintContext === null to pass and then passes null into runPersonaResearch (which expects string | undefined); update the handler to either reject null values or normalize them to undefined before calling runPersonaResearch — specifically, tighten the validation around body.lintContext so null is treated as invalid (or convert null to undefined), and apply the same normalization/validation for body.sessionId, ensuring only string | undefined is sent to runPersonaResearch.backend/src/routes/cognitive-walkthrough.ts-86-95 (1)
86-95:⚠️ Potential issue | 🟡 MinorПроверка
interactiveElementsпропускает массивы.Условие
typeof body.interactiveElements !== 'object'не исключает массивы, так какtypeof [] === 'object'. Согласно интерфейсуCognitiveWalkthroughRequest,interactiveElementsдолжен бытьRecord<string, Array<...>>, а не массивом.🐛 Предлагаемое исправление
// Validate interactiveElements (optional but must be correct shape if present) if ( body.interactiveElements !== undefined && body.interactiveElements !== null && - typeof body.interactiveElements !== 'object' + (typeof body.interactiveElements !== 'object' || + Array.isArray(body.interactiveElements)) ) { return c.json( { error: 'interactiveElements must be an object if provided' }, 400, ); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@backend/src/routes/cognitive-walkthrough.ts` around lines 86 - 95, The current validation for body.interactiveElements allows arrays because typeof [] === 'object'; update the check in the cognitive-walkthrough request handling so that interactiveElements is rejected if it's an array and validate it matches the expected CognitiveWalkthroughRequest shape: ensure body.interactiveElements is a non-null plain object (reject Array.isArray(body.interactiveElements)) and also iterate its properties to confirm each value is an Array (i.e., Record<string, Array<...>>) before accepting it.ui/src/components/messages/PageSweepCard.tsx-124-141 (1)
124-141:⚠️ Potential issue | 🟡 MinorСостояние сворачиваемого блока нужно отдать assistive tech.
Кнопка
AI Insightsвизуально переключает секцию, но не сообщает это screen reader'ам. Здесь не хватает хотя быaria-expanded.Предлагаемое исправление
<button + type="button" + aria-expanded={showInsights} className="flex items-center gap-1 text-11 font-medium text-fg hover:text-fg-secondary w-full text-left" onClick={() => setShowInsights(!showInsights)} >🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@ui/src/components/messages/PageSweepCard.tsx` around lines 124 - 141, The toggle button for "AI Insights" doesn't expose its expanded state to assistive tech; update the button (the onClick that calls setShowInsights and the JSX for the "AI Insights" button) to include aria-expanded={showInsights} and aria-controls pointing to the collapsible panel's id, and give the panel element (the collapsible section rendered by PageSweepCard) a matching id plus role="region" and aria-hidden={!showInsights} so screen readers get the correct state.backend/src/services/page-sweep-analyzer.ts-127-140 (1)
127-140:⚠️ Potential issue | 🟡 MinorАгрегированная severity должна брать максимум, а не первый встретившийся уровень.
Сейчас
topIssuesзапоминает severity только из первой ошибки данного типа. Если позже приходитcriticalтой же категории, сводка продолжит показыватьwarning, и UI занизит приоритет.💡 Пример правки
+ const severityRank: Record<string, number> = { + critical: 3, + warning: 2, + info: 1, + }; const issueTypeCounts: Record<string, { count: number; severity: string }> = {}; for (const frame of req.frames) { for (const err of frame.lintResult.errors) { const key = err.errorType; + const severity = err.severity || 'warning'; if (!issueTypeCounts[key]) { - issueTypeCounts[key] = { count: 0, severity: err.severity || 'warning' }; + issueTypeCounts[key] = { count: 0, severity }; + } else if ( + severityRank[severity] > severityRank[issueTypeCounts[key].severity] + ) { + issueTypeCounts[key].severity = severity; } issueTypeCounts[key].count++; } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@backend/src/services/page-sweep-analyzer.ts` around lines 127 - 140, В текущем агрегировании в issueTypeCounts вы сохраняете severity из первой встретившейся ошибки для ключа (errorType) — исправьте логику в цикле, чтобы при добавлении каждой ошибки (в блоке, который использует req.frames, frame.lintResult.errors, err.errorType и err.severity) обновлять severity на «максимальную» степень критичности: заведите порядок уровней (например: info < warning < error < critical) и при каждом обнаружении существующего ключа сравнивайте ранги текущего err.severity и сохранённого issueTypeCounts[key].severity и присваивайте более высокий (более строгий) уровень; итоговая сборка topIssues остаётся той же, но severity будет вычислено как максимум по всем ошибкам данного типа.ui/src/lib/messages.ts-219-220 (1)
219-220:⚠️ Potential issue | 🟡 MinorКонтракт
page-sweep-progressописан двумя разными payload’ами.
PluginEventздесь шлёт только{ current, total, frameName }, аPageSweepProgressниже делает обязательными ещёphaseиmessage. Сейчас это два несовместимых публичных типа для одного и того же события, поэтому потребитель либо не сможет переиспользоватьPageSweepProgress, либо начнёт читать поля, которые никогда не приходят.🧭 Предлагаемый фикс
- | { type: 'page-sweep-progress'; data: { current: number; total: number; frameName: string } } + | { type: 'page-sweep-progress'; data: PageSweepProgress & { frameName: string } } | { type: 'page-sweep-result'; data: PageSweepRawData }; export interface PageSweepProgress { phase: 'collecting' | 'comparing' | 'validating' | 'complete'; current: number; total: number; message: string; + frameName?: string; }Also applies to: 423-428
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@ui/src/lib/messages.ts` around lines 219 - 220, The 'page-sweep-progress' event is defined twice with incompatible payloads: the PluginEvent variant sends { current, total, frameName } while the PageSweepProgress type requires additional fields (phase, message), causing a mismatched public contract; update types so they match by either adding optional phase and message to the PluginEvent payload or changing PageSweepProgress to exactly { current: number; total: number; frameName: string } (or make phase/message optional) and ensure the union variant in PluginEvent (the line with "{ type: 'page-sweep-progress'; data: { current: number; total: number; frameName: string } }") and the PageSweepProgress type use the same shape so consumers can reliably reuse PageSweepProgress when handling the 'page-sweep-progress' event.src/core/design-lint.ts-587-595 (1)
587-595:⚠️ Potential issue | 🟡 MinorНе затирайте исходный
nodeTypeзначением'FRAME'.Здесь все новые rule results насильно мапятся к
FRAME, хотя как минимумcheckResponsiveвsrc/lint/responsive.ts(Line 300-Line 348) иcheckFittsLawвsrc/lint/fitts-law.ts(Line 80-Line 106) обходят произвольныеSceneNode. Для проблем наTEXT/INSTANCE/COMPONENTdownstream UI получит неверный тип узла.🎯 Предлагаемый фикс
+ const nodeType = figma.getNodeById(issue.nodeId)?.type ?? 'FRAME'; errors.push({ nodeId: issue.nodeId, nodeName: issue.nodeName, - nodeType: 'FRAME', + nodeType, errorType: 'fittsLaw',Тот же паттерн нужен в остальных трёх блоках.
Also applies to: 608-617, 629-638, 650-659
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/core/design-lint.ts` around lines 587 - 595, Замените жёстко заданное значение 'FRAME' в объектах, которые вы пушите в errors (в тех же местах, где вызывается errors.push), на реальный тип узла из результата проверки — используйте issue.nodeType (или issue.node?.type как fallback) вместо литерала 'FRAME' — и продублируйте эту правку во всех четырёх блоках аналогичной структуры; это гарантирует, что проверки, такие как checkResponsive и checkFittsLaw, будут передавать корректный nodeType (с запасным значением 'FRAME' только если nodeType отсутствует).
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 70393b7d-36dd-4601-8120-0dcc410099c9
⛔ Files ignored due to path filters (2)
dist/code.jsis excluded by!**/dist/**dist/ui.htmlis excluded by!**/dist/**
📒 Files selected for processing (54)
backend/src/index.tsbackend/src/prompts/a11y-spec.tsbackend/src/prompts/attention.tsbackend/src/prompts/brand-consistency.tsbackend/src/prompts/cognitive-walkthrough.tsbackend/src/prompts/copy-tone.tsbackend/src/prompts/dark-mode.tsbackend/src/prompts/nielsen-heuristics.tsbackend/src/prompts/page-sweep.tsbackend/src/prompts/persona-research.tsbackend/src/prompts/pure-scoring.tsbackend/src/prompts/responsive.tsbackend/src/routes/a11y-spec.tsbackend/src/routes/analyze.tsbackend/src/routes/brand-consistency.tsbackend/src/routes/cognitive-walkthrough.tsbackend/src/routes/copy-tone.tsbackend/src/routes/dark-mode.tsbackend/src/routes/page-sweep.tsbackend/src/routes/persona-research.tsbackend/src/routes/pure-scoring.tsbackend/src/routes/responsive.tsbackend/src/services/a11y-spec-generator.tsbackend/src/services/brand-consistency.tsbackend/src/services/cognitive-walkthrough.tsbackend/src/services/copy-tone.tsbackend/src/services/extended-analyzer.tsbackend/src/services/page-sweep-analyzer.tsbackend/src/services/persona-research.tsbackend/src/services/pure-scoring.tsbackend/src/services/responsive-validator.tsfigma.d.tssrc/baseline/design-debt.tssrc/baseline/dtcg-parser.tssrc/baseline/storage.tssrc/baseline/token-compliance.tssrc/core/design-lint.tssrc/extract/mode-comparator.tssrc/extract/variable-collector.tssrc/lint/dark-mode.tssrc/lint/detached-instance.tssrc/lint/fitts-law.tssrc/lint/gestalt.tssrc/lint/realtime-lint.tssrc/lint/responsive.tssrc/lint/types.tssrc/types.tssrc/ui/message-handler.tsui/src/App.tsxui/src/components/chat/MessageList.tsxui/src/components/messages/PageSweepCard.tsxui/src/components/shared/QuickActions.tsxui/src/lib/api.tsui/src/lib/messages.ts
| // Count consumers from all nodes on the current page | ||
| const allNodes = figma.currentPage.findAll(() => true); | ||
| const { consumerMap, totalEligible, boundCount } = countVariableConsumers(allNodes); |
There was a problem hiding this comment.
Здесь идёт повторный обход уже плоского списка узлов.
figma.currentPage.findAll(() => true) уже возвращает всех потомков страницы, а countVariableConsumers() затем ещё раз рекурсивно идёт по children. Вложенные узлы будут посчитаны по нескольку раз, поэтому totalEligible, boundCount и все consumer totals раздуваются почти на любой вложенной иерархии.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/extract/variable-collector.ts` around lines 119 - 121, The code is
double-traversing nodes: figma.currentPage.findAll(() => true) returns a
flattened list but countVariableConsumers() also recursively walks children,
causing nested nodes to be counted multiple times; fix by passing the page's
top-level children to countVariableConsumers instead of a flattened list
(replace figma.currentPage.findAll(() => true) with figma.currentPage.children
or otherwise pass only root-level nodes), so variables computed into
consumerMap, totalEligible and boundCount are correct.
| function onDocumentChange(event: DocumentChangeEvent): void { | ||
| if (!realtimeConfig || !realtimeConfig.enabled) return; | ||
|
|
||
| for (const change of event.documentChanges) { | ||
| // Only react to property changes and creations — not deletes | ||
| if (change.type === 'PROPERTY_CHANGE' || change.type === 'CREATE' || change.type === 'STYLE_PROPERTY_CHANGE') { | ||
| if ('id' in change && typeof change.id === 'string') { | ||
| pendingNodeIds.add(change.id); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| if (pendingNodeIds.size === 0) return; | ||
|
|
||
| // Debounce: clear previous timer and start new one | ||
| if (debounceTimer !== null) { | ||
| clearTimeout(debounceTimer); | ||
| } | ||
|
|
||
| const debounceMs = realtimeConfig.debounceMs || 500; | ||
| debounceTimer = setTimeout(() => { | ||
| processChangedNodes(); | ||
| }, debounceMs) as unknown as number; | ||
| } | ||
|
|
||
| /** | ||
| * Fetch each changed node, filter to valid SceneNodes, and re-lint. | ||
| */ | ||
| async function processChangedNodes(): Promise<void> { | ||
| if (!realtimeConfig) return; | ||
|
|
||
| const nodeIds = Array.from(pendingNodeIds); | ||
| pendingNodeIds.clear(); | ||
| debounceTimer = null; | ||
|
|
||
| const changedNodes: SceneNode[] = []; | ||
| const changedNodeIds: string[] = []; | ||
|
|
||
| for (const id of nodeIds) { | ||
| try { | ||
| const node = await figma.getNodeByIdAsync(id); | ||
| if (node && 'type' in node && node.type !== 'PAGE' && node.type !== 'DOCUMENT') { | ||
| changedNodes.push(node as SceneNode); | ||
| changedNodeIds.push(id); | ||
| } | ||
| } catch { | ||
| // Node may have been deleted between change event and processing — skip | ||
| } | ||
| } | ||
|
|
||
| if (changedNodes.length === 0) return; | ||
|
|
||
| try { | ||
| const result = runDesignLint(changedNodes, realtimeConfig.settings); | ||
|
|
There was a problem hiding this comment.
1. Realtime lint invalid nodes 🐞 Bug ⛯ Reliability
Realtime lint adds STYLE_PROPERTY_CHANGE IDs and then casts any non-PAGE/DOCUMENT node returned by getNodeByIdAsync to SceneNode before calling runDesignLint, which can lint non-scene entities or cause runtime errors/meaningless updates.
Agent Prompt
### Issue description
Realtime lint casts arbitrary nodes to `SceneNode` and lints them. Because it also tracks `STYLE_PROPERTY_CHANGE`, it can end up linting objects that are not part of the scene graph.
### Issue Context
`runDesignLint` operates on `SceneNode[]`. The realtime pipeline must guarantee only those are passed.
### Fix Focus Areas
- src/lint/realtime-lint.ts[31-90]
- src/core/design-lint.ts[414-425]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| // Only check text nodes with fixed width (not auto-width or auto-height) | ||
| if (textResize === 'NONE' || textResize === 'TRUNCATE') { | ||
| const charCount = textNode.characters.length; | ||
| const estimatedTextWidth = charCount * (fontSize as number) * AVG_CHAR_WIDTH_RATIO; | ||
| const nodeWidth = textNode.width; | ||
|
|
||
| // Flag if text fills > 80% of available width | ||
| if (charCount > 5 && estimatedTextWidth > nodeWidth * 0.8) { | ||
| riskCount++; | ||
| failed++; | ||
| issues.push({ | ||
| id: nextId(), | ||
| type: 'responsive', | ||
| severity: 'info', | ||
| nodeId: node.id, | ||
| nodeName: node.name, | ||
| message: `Text "${node.name}" may truncate — content fills ~${Math.round((estimatedTextWidth / nodeWidth) * 100)}% of fixed width (${Math.round(nodeWidth)}px). Translations or dynamic content could overflow.`, | ||
| currentValue: `${charCount} chars in ${Math.round(nodeWidth)}px`, | ||
| suggestions: [ | ||
| 'Use auto-width or auto-height text resizing', | ||
| 'Allow text to wrap by setting textAutoResize to HEIGHT', | ||
| 'Add ellipsis handling if truncation is intentional', | ||
| ], | ||
| autoFixable: false, | ||
| }); |
There was a problem hiding this comment.
2. Responsive percent divides zero 🐞 Bug ✓ Correctness
Responsive text truncation messaging divides by textNode.width without guarding for 0/invalid widths, producing Infinity%/NaN% in lint output.
Agent Prompt
### Issue description
The responsive lint module can emit invalid percentages due to division by zero/invalid widths.
### Issue Context
The % is used only for messaging, but broken output reduces trust and can leak into UI displays.
### Fix Focus Areas
- src/lint/responsive.ts[158-185]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
…nfig Tier 0 (core UX): - Ambient quality badge: quick-lint on selection change shows mini-score in StickyHeader before any full analysis - All auto-fixable types exposed (fill/stroke/effect/text/autoLayout) with per-category fix buttons - Summary-first hierarchy: lint summary sorted by impact, top 3 shown first - Score delta from previous scan shown in StickyHeader (not just baseline) - Persistent ignore state already implemented (verified) Tier 1 (8 new result cards): - DesignDebtCard, DarkModeCard, A11ySpecCard, TokenComplianceCard - BrandConsistencyCard, CopyToneCard, PersonaResearchCard, AttentionHeatmapCard - All wired into MessageList.tsx with new ChatMessageType kinds Tier 2 (team config UI): - TeamConfigPanel, SeveritySelector, ScaleEditor components - Enable team-wide lint configuration (spacing/radius scales, severity overrides) Tier 3 (8 new lint modules): - layout-sizing, constraints, typography, style-audit (3a) - component-props, variable-scope, multi-theme, grid-check (3b) - Standalone modules with own LintIssue types for extended analysis Tier 4 (backend AI improvements): - Grounding instructions for all prompts - Three-layer explanations (rule/why/real-world) - Confidence scoring and filtering for AI findings - Improved page-type detection with signals - Enhanced attention, nielsen-heuristics, review prompts Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 14
Note
Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.
🟡 Minor comments (13)
src/lint/grid-check.ts-263-279 (1)
263-279:⚠️ Potential issue | 🟡 MinorФрейм с несколькими COLUMNS-сетками добавляется в
columnCountsмногократно.Если фрейм имеет несколько сеток типа COLUMNS с разным количеством колонок, он будет добавлен несколько раз. Это может привести к дублированию issues и искажению подсчёта «наиболее частого» значения.
🐛 Предлагаемое исправление (использовать только первую COLUMNS-сетку)
for (const frame of frames) { const layoutGrids = (frame as any).layoutGrids as unknown[] | undefined; if (!Array.isArray(layoutGrids)) continue; + let foundColumns = false; for (const grid of layoutGrids) { const info = extractGridInfo(grid); if (!info || info.pattern !== 'COLUMNS') continue; + if (foundColumns) continue; // Skip additional COLUMNS grids + foundColumns = true; if (info.count !== undefined && isFinite(info.count)) { columnCounts.push({ nodeId: frame.id, nodeName: frame.name, count: info.count, }); } } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/lint/grid-check.ts` around lines 263 - 279, The loop over layoutGrids in the frames processing can push the same frame multiple times when it contains multiple COLUMNS grids; update the logic in the for (const grid of layoutGrids) block (where extractGridInfo(grid) is called and columnCounts is appended with { nodeId: frame.id, nodeName: frame.name, count: info.count }) so that once you find the first valid COLUMNS grid for a given frame you push a single entry and then stop checking further grids for that frame (e.g., break the inner loop or set a flag and break) to ensure each frame is added at most once.src/lint/grid-check.ts-219-222 (1)
219-222:⚠️ Potential issue | 🟡 MinorПустой блок
if— мёртвый код.Блок на строках 220-222 не выполняет никаких действий. Если рекурсия предполагалась, она отсутствует.
🗑️ Предлагаемое исправление
- // If this is a SECTION, look inside for top-level frames - if (node.type === 'SECTION' || (!isTopLevelFrame(node) && 'children' in node)) { - // Only recurse into sections and non-top-level containers to find top-level frames - }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/lint/grid-check.ts` around lines 219 - 222, The if block checking node.type === 'SECTION' || (!isTopLevelFrame(node) && 'children' in node) is empty and should recurse into children to find top-level frames; inside that block call the existing traversal/processing function (e.g., traverseNode, visitNode, or the local function used elsewhere in src/lint/grid-check.ts) for each child (node.children) so sections and non-top-level containers are processed—use the same parameters/context as other recursion sites and ensure you guard for node.children being an array before iterating.src/lint/style-audit.ts-184-200 (1)
184-200:⚠️ Potential issue | 🟡 MinorПроверка текстовых стилей в rich text неполная — возможны ложные срабатывания.
Текстовые узлы с форматированием по сегментам могут применять стили только на уровне отдельных участков текста, не устанавливая top-level
textStyleId. Код проверяет толькоtextStyleIdузла (строка 194), но пропускает стили в сегментах, доступные черезgetStyledTextSegments(). Это приводит к ложным срабатываниям об "осиротевших" текстовых стилях.Рекомендуется для TextNode дополнительно собирать ID стилей из сегментов текста через
getStyledTextSegments(['textStyleId']).🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/lint/style-audit.ts` around lines 184 - 200, The current collection of text style IDs only checks top-level textStyleId on nodes (usedStyleIds population around the loop over allNodes), missing segment-level styles inside rich TextNode; update the loop so that when a node is a TextNode (check instance/type), call getStyledTextSegments(['textStyleId']) and add each segment.textStyleId (if non-empty string) into usedStyleIds in addition to the existing top-level checks for textStyleId, leaving fillStyleId/strokeStyleId/effectStyleId handling unchanged.src/lint/layout-sizing.ts-297-328 (1)
297-328:⚠️ Potential issue | 🟡 MinorЛогика проверки constraint'ов не учитывает направление оси.
При
primaryMode === 'FILL'проверяются сразу оба constraint'а (width и height). ОднакоprimaryAxisSizingModeзависит отlayoutModeродителя:
- В
HORIZONTALродителе primary axis — ширина- В
VERTICALродителе primary axis — высотаТекущая логика
!hasWidthConstraint && !hasHeightConstraintне выдаст предупреждение, если есть constraint на нерелевантной оси.🔧 Предлагаемое исправление
Необходимо передать информацию о
layoutModeродителя в функцию и проверять только релевантную ось:-function checkMissingMinMax(node: SceneNode, issues: LintIssue[]): number { +function checkMissingMinMax(node: SceneNode, parentLayoutMode: 'HORIZONTAL' | 'VERTICAL' | 'NONE' | undefined, issues: LintIssue[]): number { if (!isFrameLike(node)) return 0; // ... + const primaryIsWidth = parentLayoutMode === 'HORIZONTAL'; + const relevantConstraint = primaryIsWidth ? hasWidthConstraint : hasHeightConstraint; + if (primaryMode === 'FILL' && !relevantConstraint) { // flag issue }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/lint/layout-sizing.ts` around lines 297 - 328, The current check uses both hasWidthConstraint and hasHeightConstraint for primaryMode/counterMode; update the logic to consider only the relevant axis using the parent's layoutMode: add a layoutMode parameter to the function (or access parent layoutMode) and compute primaryAxisIsWidth = layoutMode === 'HORIZONTAL'; then when primaryMode === 'FILL' check the corresponding constraint (if primaryAxisIsWidth use hasWidthConstraint else hasHeightConstraint) before calling pushIssue, and when counterMode === 'FILL' check the opposite axis (if primaryAxisIsWidth use hasHeightConstraint else hasWidthConstraint); keep existing pushIssue calls and messages but only trigger them when the relevant axis lacks constraints.src/lint/layout-sizing.ts-207-253 (1)
207-253:⚠️ Potential issue | 🟡 MinorДобавьте проверку для вертикальных auto-layout.
Функция проверяет только
HORIZONTALlayouts (строка 211), ноlayoutGrowодинаково влияет на обе направления. Конфликт между детьми сlayoutGrow: 1иlayoutGrow: 0является проблемой в вертикальных auto-layout точно так же, как в горизонтальных. Рекомендуется удалить или расширить проверкуparent.layoutMode !== 'HORIZONTAL'на обе ориентации:if (!hasAutoLayout(parent as unknown as SceneNode)) return 0; // Проверять обе ориентации: // if (parent.layoutMode === 'NONE') return 0;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/lint/layout-sizing.ts` around lines 207 - 253, The function checkLayoutGrowMismatch only runs for HORIZONTAL layouts; change the early layoutMode guard (currently checking parent.layoutMode !== 'HORIZONTAL') to instead skip only when layoutMode === 'NONE' (e.g., if (parent.layoutMode === 'NONE') return 0;) so both HORIZONTAL and VERTICAL auto-layouts are checked; also update the pushIssue message string that currently hardcodes "horizontal layout" to reference parent.layoutMode (or a generic phrase) so the warning is correct for vertical containers; leave the rest of the logic intact (symbols: checkLayoutGrowMismatch, parent.layoutMode, hasAutoLayout, pushIssue, growNodes).src/lint/constraints.ts-112-122 (1)
112-122:⚠️ Potential issue | 🟡 MinorНеполная диагностика при
SCALEпо двум осямНа Line 114-122 при
horizontal === 'SCALE'иvertical === 'SCALE'вcurrentValueпопадает только одна ось, из‑за чего issue описывает состояние неточно.Предлагаемое исправление
- const hasScale = constraints.horizontal === 'SCALE' || constraints.vertical === 'SCALE'; + const hasScale = constraints.horizontal === 'SCALE' || constraints.vertical === 'SCALE'; if (hasScale) { - const axis = constraints.horizontal === 'SCALE' ? 'horizontal' : 'vertical'; + const axes = [ + constraints.horizontal === 'SCALE' ? 'horizontal' : null, + constraints.vertical === 'SCALE' ? 'vertical' : null, + ].filter((v): v is string => Boolean(v)); pushIssue( issues, 'critical', node.id, node.name, `SCALE constraint on text node will distort text — use MIN or STRETCH instead`, - `SCALE (${axis})`, + `SCALE (${axes.join(' + ')})`, ['MIN', 'STRETCH'], );🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/lint/constraints.ts` around lines 112 - 122, В диагностике для SCALE нужно учитывать обе оси: в функции/блоке где вычисляются hasScale и axis (используются constraints.horizontal, constraints.vertical, переменная axis и вызов pushIssue) изменить логику так, чтобы когда обе оси равны 'SCALE' формировать currentValue/axis как "horizontal and vertical" (или "horizontal, vertical"), а не только одну ось; то есть вычислить axis = [horizontal === 'SCALE' ? 'horizontal' : null, vertical === 'SCALE' ? 'vertical' : null].filter(Boolean).join(' and ') и передать этот строковый результат в pushIssue (и/или в метаданные текущего значения), чтобы сообщение об ошибке правильно отражало обе оси.backend/src/services/claude.ts-61-65 (1)
61-65:⚠️ Potential issue | 🟡 MinorНормализуйте JSON-ветку до контракта
PageTypeResult.
parsed.typeнеtrim()-ится,confidenceне ограничивается диапазоном, аsignalsпринимает любой массив. В результате наружу могут уйти" landing ",2или массив объектов, хотя интерфейс сверху обещает нормализованныйstring[].Предлагаемое исправление
const parsed = JSON.parse(jsonMatch[0]); + const type = typeof parsed.type === 'string' ? parsed.type.trim().toLowerCase() : 'other'; return { - type: typeof parsed.type === 'string' ? parsed.type.toLowerCase() : 'other', - confidence: typeof parsed.confidence === 'number' ? parsed.confidence : 0.5, - signals: Array.isArray(parsed.signals) ? parsed.signals : [], + type, + confidence: + Number.isFinite(parsed.confidence) && parsed.confidence >= 0 && parsed.confidence <= 1 + ? parsed.confidence + : 0, + signals: Array.isArray(parsed.signals) + ? parsed.signals.filter((signal): signal is string => typeof signal === 'string') + : [], };🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@backend/src/services/claude.ts` around lines 61 - 65, Normalize the JSON branch to conform to the PageTypeResult contract: after parsing jsonMatch[0], normalize parsed.type by checking it's a string, trimming it, converting to lower-case and defaulting to 'other' if empty; clamp parsed.confidence to the [0,1] range (default 0.5) instead of accepting any number; normalize parsed.signals into a string[] by ensuring it's an array, mapping each item to a string (e.g., String(item)), trimming and filtering out empty strings and non-stringy values, and defaulting to an empty array if nothing valid remains; update the return object (the block handling parsed, jsonMatch and producing type/confidence/signals) to use these normalized values.ui/src/components/messages/TokenComplianceCard.tsx-122-144 (1)
122-144:⚠️ Potential issue | 🟡 MinorВетвь
+N moreнедостижима из-за внешнего условия рендера.При
data.unmatched.length > 4иshowUnmatched === falseконтейнер списка не рендерится, поэтому блок+N moreникогда не появляется.✅ Исправление рендера превью/expand
- {(showUnmatched || data.unmatched.length <= 4) && ( - <div className="space-y-1"> - {displayUnmatched.map((u, i) => ( - ... - ))} - {!showUnmatched && data.unmatched.length > 4 && ( - <button - className="text-10 text-bg-brand hover:underline" - onClick={() => setShowUnmatched(true)} - > - +{data.unmatched.length - 4} more - </button> - )} - </div> - )} + <div className="space-y-1"> + {displayUnmatched.map((u, i) => ( + ... + ))} + {!showUnmatched && data.unmatched.length > 4 && ( + <button + type="button" + className="text-10 text-bg-brand hover:underline" + onClick={() => setShowUnmatched(true)} + > + +{data.unmatched.length - 4} more + </button> + )} + </div>🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@ui/src/components/messages/TokenComplianceCard.tsx` around lines 122 - 144, The "+N more" button is unreachable because the outer render condition uses (showUnmatched || data.unmatched.length <= 4), which prevents rendering when showUnmatched is false and data.unmatched.length > 4; change the outer condition to render whenever there are unmatched items (e.g., (showUnmatched || data.unmatched.length > 0)) and keep the inner logic that maps displayUnmatched and the conditional button (!showUnmatched && data.unmatched.length > 4) with setShowUnmatched so the preview list shows the first items and the "+N more" button is displayed when collapsed; ensure displayUnmatched still returns the correct slice when showUnmatched is false.ui/src/components/messages/DesignDebtCard.tsx-52-53 (1)
52-53:⚠️ Potential issue | 🟡 MinorНормализуйте знак
deltaперед рендером тренда.Сейчас знак добавляется строкой, но
deltaуже может быть отрицательным, из‑за чего возможен вывод вроде+-2. Лучше рендеритьMath.abs(delta)и знак задавать только поdirection.💡 Предлагаемый фикс
function TrendIndicator({ trend }: { trend: DesignDebtData['trend'] }) { if (!trend) return null; const { direction, delta } = trend; + const absDelta = Math.abs(delta); if (direction === 'improving') { return ( <span className="flex items-center gap-0.5 text-10 text-fg-success"> @@ - +{delta} + +{absDelta} </span> ); } if (direction === 'declining') { @@ - -{delta} + -{absDelta} </span> ); }Also applies to: 60-61, 70-71
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@ui/src/components/messages/DesignDebtCard.tsx` around lines 52 - 53, The trend rendering currently concatenates a sign with delta but delta may already be negative (const { direction, delta } = trend), producing outputs like "+-2"; normalize by rendering Math.abs(delta) and determine the sign solely from direction (e.g., use direction to choose "+" or "−"), and apply this change wherever delta is rendered (the destructured trend usage at lines around the const { direction, delta } = trend and the other trend render sites referenced in the review).ui/src/App.tsx-843-846 (1)
843-846:⚠️ Potential issue | 🟡 MinorДля
varianceлучше считать среднее поscores, а не использовать округлённыйoverallScore.Текущий расчёт слегка смещает
consistencyScore, особенно при небольшом числе кадров.💡 Предлагаемый фикс
- const mean = overallScore; + const mean = scores.length > 0 + ? scores.reduce((a, b) => a + b, 0) / scores.length + : 0;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@ui/src/App.tsx` around lines 843 - 846, Вместо использования already-rounded overallScore при вычислении variance, вычисли реальное среднее по массиву scores и используй его в формуле: если scores.length > 0, присвой mean как сумма элементов scores делённая на scores.length и затем вычисли variance через reduce с этим mean; при пустом массиве оставь variance = 0; обнови участки с переменными mean и variance (используются в overallScore/consistencyScore) чтобы ссылались на новый mean.ui/src/hooks/useChat.ts-155-162 (1)
155-162:⚠️ Potential issue | 🟡 MinorДублирование
FIXABLE_TYPESс разным содержимым.
FIXABLE_TYPESопределён дважды с разными значениями:
- Строки 156-157: включает
textиautoLayout- Строка 527: аналогичный набор
В
handleRescan(строка 288) используется более узкий набор['spacing', 'radius'], что приводит к несогласованности подсчёта fixable между первичным сканированием и ресканом.♻️ Предложение: вынести в общую константу
+/** Types that support auto-fix. Shared across lint result and rescan handlers. */ +const FIXABLE_TYPES = new Set(['spacing', 'radius', 'fill', 'stroke', 'effect', 'text', 'autoLayout']); + export function useChat() { // ... } -const TYPE_LABEL: Record<string, string> = {Затем обновите
handleRescan(строка 288):- const fixableCount = result.errors.filter(e => e.errorType === 'spacing' || e.errorType === 'radius').length; + const fixableCount = result.errors.filter(e => FIXABLE_TYPES.has(e.errorType)).length;Also applies to: 527-528
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@ui/src/hooks/useChat.ts` around lines 155 - 162, There are two conflicting FIXABLE_TYPES definitions causing inconsistent counts between initial scan and handleRescan; consolidate by creating a single shared constant (e.g., FIXABLE_TYPES) used everywhere, update any duplicate definitions (the one used to compute fixableCount, fixableSpacing, fixableRadius, fixableStyles and the one referenced in handleRescan) so they reference that single constant, and ensure handleRescan uses the full set (including 'text' and 'autoLayout') rather than the narrower ['spacing','radius'] so all filter operations (in useChat's fixableCount, fixableSpacing, fixableRadius, fixableStyles and handleRescan) remain consistent.src/ui/message-handler.ts-1547-1553 (1)
1547-1553:⚠️ Potential issue | 🟡 MinorНет уведомления пользователя об усечении списка фреймов.
При количестве фреймов более 50, функция тихо обрезает список до 50. Пользователь не получает информации о том, что часть фреймов была пропущена.
🔔 Предложение: добавить уведомление об усечении
const framesToAnalyze = frames.slice(0, 50); const total = framesToAnalyze.length; + + if (frames.length > 50) { + figma.notify(`Analyzing first 50 of ${frames.length} frames`, { timeout: 3000 }); + } const { runDesignLint } = await import('../core/design-lint');🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/ui/message-handler.ts` around lines 1547 - 1553, When frames is larger than the truncation limit, notify the UI before slicing so the user knows some frames were skipped; in the block where frames is checked and framesToAnalyze is created (referencing the frames array, framesToAnalyze, and sendMessageToUI), add a conditional that if frames.length > 50 calls sendMessageToUI with a clear event (e.g., 'frames-truncated' or 'analysis-warning') and payload including original count and truncated count, then proceed to set framesToAnalyze = frames.slice(0, 50) and total = framesToAnalyze.length.src/ui/message-handler.ts-1569-1576 (1)
1569-1576:⚠️ Potential issue | 🟡 MinorПотенциальная гонка при отправке прогресса.
sendMessageToUI('page-sweep-progress', ...)вызывается внутри параллельногоbatch.map, что может привести к неупорядоченным сообщениям о прогрессе (например, "frame 3" перед "frame 2"). Рекомендуется отправлять прогресс после завершения батча.🔄 Предложение: переместить прогресс за пределы batch
for (let i = 0; i < total; i += BATCH_SIZE) { const batch = framesToAnalyze.slice(i, i + BATCH_SIZE); - const promises = batch.map(async (frame, batchIdx) => { - const current = i + batchIdx + 1; - - sendMessageToUI('page-sweep-progress', { - current, - total, - frameName: frame.name, - }); + const promises = batch.map(async (frame) => { // ... lint and screenshot logic }); const batchResults = await Promise.all(promises); frameResults.push(...batchResults); + + // Send progress after batch completes + sendMessageToUI('page-sweep-progress', { + current: Math.min(i + BATCH_SIZE, total), + total, + frameName: batch[batch.length - 1].name, + }); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/ui/message-handler.ts` around lines 1569 - 1576, The progress message is sent inside the parallel batch.map (using sendMessageToUI('page-sweep-progress', ...)), which can emit out-of-order updates; change the logic in the batch processing around batch.map/Promise.all so the sendMessageToUI call is moved outside the parallel map and is executed after the batch has completed (e.g., await Promise.all(promises) then iterate the resolved results or compute current using i and batch length to send ordered 'page-sweep-progress' messages for each frame in sequence), referencing the existing variables i, batchIdx, current, total and frame.name to preserve the same payload.
🧹 Nitpick comments (22)
src/lint/grid-check.ts (3)
15-18: Модульное состояниеissueCounterможет вызвать проблемы при конкурентных вызовах.Счётчик сбрасывается в
checkGrid, но если функция вызывается параллельно (например, черезPromise.all), ID могут конфликтовать. Для плагина Figma это маловероятно, но рекомендуется передавать счётчик через замыкание.♻️ Предлагаемое исправление
-let issueCounter = 0; -function nextId(): string { - return `grid-${++issueCounter}`; -} +function createIdGenerator(): () => string { + let counter = 0; + return () => `grid-${++counter}`; +}И в
checkGrid:+ const nextId = createIdGenerator(); - issueCounter = 0;Затем передавайте
nextIdв функции проверки.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/lint/grid-check.ts` around lines 15 - 18, The module-level mutable `issueCounter` and `nextId` function can cause ID collisions when `checkGrid` is called concurrently; move the counter into the `checkGrid` scope (remove or stop using the module-level `issueCounter`) and create a local `nextId` closure inside `checkGrid` that closes over a local counter variable, then pass that `nextId` function into any helper/check functions that previously relied on the global `nextId` so IDs are isolated per `checkGrid` invocation (refer to `issueCounter`, `nextId`, and `checkGrid` to locate the spots to change).
381-384: Подсчёт summary через поиск подстрок в сообщениях — хрупкий подход.Если текст сообщений изменится, подсчёт в summary сломается. Рекомендуется отслеживать счётчики напрямую в функциях проверки.
♻️ Предлагаемое исправление
Альтернативный подход — возвращать структурированный результат из функций проверки:
+interface CheckCounters { + hardCodedGrids: number; + gutterMismatches: number; + inconsistentColumns: number; +}Или добавить поле
categoryвLintIssue:issues.push({ id: nextId(), type: 'spacing', + category: 'hard-coded-grid', // для фильтрации severity: 'info', // ... });Тогда подсчёт будет:
- inconsistentColumns: issues.filter(i => i.message.includes('column grid while')).length, + inconsistentColumns: issues.filter(i => i.category === 'inconsistent-columns').length,🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/lint/grid-check.ts` around lines 381 - 384, The summary counters (inconsistentColumns, hardCodedGrids, gutterMismatches) currently derive from message substrings on issues which is fragile; update the linting flow so each checker returns structured metadata (e.g., add a `category` enum/string on the LintIssue or have check functions return { issues, counts }) and increment counters directly inside the check functions (or map issues by their new `category`) instead of using message.includes; update the aggregation logic that computes inconsistentColumns, hardCodedGrids, and gutterMismatches to use the new `category` or returned counts from the check functions (refer to symbols: LintIssue, issues array, the specific summary keys inconsistentColumns/hardCodedGrids/gutterMismatches, and the individual checker functions) to make counts robust to message text changes.
161-164: Пустой блокelse if— мёртвый код.Блок содержит только комментарий. Рекомендуется либо удалить его, либо преобразовать в явный ранний возврат с комментарием для ясности.
♻️ Предлагаемое исправление
if (!gridStyleId || gridStyleId === '') { issues.push({ // ... }); - } else if (localGridStyleIds.size > 0 && !localGridStyleIds.has(gridStyleId)) { - // Style ID exists but doesn't match any local style — may be from a library - // This is fine, don't flag it } + // Note: If style ID exists but isn't local, it may be from a library — this is acceptable🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/lint/grid-check.ts` around lines 161 - 164, The else-if block that checks "localGridStyleIds.size > 0 && !localGridStyleIds.has(gridStyleId)" only contains a comment (dead code); remove the empty else-if or replace it with an explicit early return or a clearly documented no-op (e.g., "return;" with a comment) inside the function that contains this logic so intent is explicit—locate the check around the "localGridStyleIds" and "gridStyleId" usage in the function in src/lint/grid-check.ts and either delete the else-if branch or convert it to a documented early exit to avoid a silent empty block.src/lint/style-audit.ts (2)
18-21: Модульный счётчикissueCounterможет привести к коллизии ID.Счётчик сбрасывается в
checkStyleAudit(строка 393), что приведёт к повторению ID при повторных вызовах функции. Если результаты агрегируются (например, при анализе нескольких страниц), ID будут дублироваться.♻️ Предлагаемое исправление: использовать локальный счётчик
-let issueCounter = 0; -function nextId(): string { - return `style-audit-${++issueCounter}`; -} +function createIdGenerator(): () => string { + let counter = 0; + return () => `style-audit-${++counter}`; +}Затем в
checkStyleAudit:+ const nextId = createIdGenerator(); - issueCounter = 0;И передавать
nextIdв helper-функции или использовать замыкание.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/lint/style-audit.ts` around lines 18 - 21, The module-level mutable issueCounter and nextId() cause ID collisions because checkStyleAudit resets the counter; instead, move the counter into checkStyleAudit (e.g., create a local let issueCounter = 0 inside checkStyleAudit) and create a local nextId function/closure there, then pass that nextId into helper functions that currently call the module-level nextId (or have helpers accept a generated id). Update references from the global nextId/issueCounter to use the local nextId in checkStyleAudit and its downstream helpers (referencing function name checkStyleAudit and helper functions that generate issues) so IDs remain unique across aggregated runs.
36-56: Тип'fill'жёстко закодирован для всех категорий проблем.Функция
pushIssueвсегда устанавливаетtype: 'fill', но используется также для проблем с текстовыми стилями (строки 222-231) и стилями эффектов (строки 238-247). Это может нарушить фильтрацию или группировку в UI.♻️ Предлагаемое исправление: добавить параметр типа
function pushIssue( issues: LintIssue[], + type: LintIssue['type'], severity: LintSeverity, nodeId: string, nodeName: string, message: string, currentValue?: string, suggestions?: string[], ): void { issues.push({ id: nextId(), - type: 'fill', + type, severity, ... }); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/lint/style-audit.ts` around lines 36 - 56, The pushIssue helper always sets type: 'fill', which breaks UI filtering; modify pushIssue signature to accept a new parameter (e.g., issueType: string or a LintIssueType union) with a default of 'fill' and propagate that value into the pushed object (retain nextId(), severity: LintSeverity, nodeId, nodeName, etc.); then update all call sites that report text-style and effect-style problems (the callers you added around text style handling and effect style handling) to pass the appropriate type value (e.g., 'text' and 'effect') so LintIssue consumers can correctly group/filter the issues.src/lint/layout-sizing.ts (1)
16-19: Глобальный счётчик может вызвать коллизии ID при конкурентных вызовах.
issueCounter— модульный глобальный стейт, который сбрасывается вcheckLayoutSizing. При параллельных или перекрывающихся вызовах (например, из real-time linting черезfigma.on('documentchange')) возможны дублирующиеся ID.♻️ Рекомендуемое исправление: локальный счётчик или фабрика
-let issueCounter = 0; -function nextId(): string { - return `layout-sizing-${++issueCounter}`; -} +function createIdGenerator(): () => string { + let counter = 0; + return () => `layout-sizing-${++counter}`; +}Затем в
checkLayoutSizing:export function checkLayoutSizing( nodes: readonly SceneNode[], opts?: { settings?: { skipLockedLayers?: boolean; skipHiddenLayers?: boolean } }, ): LayoutSizingLintResult { const skipLocked = opts?.settings?.skipLockedLayers ?? true; const skipHidden = opts?.settings?.skipHiddenLayers ?? true; - issueCounter = 0; + const nextId = createIdGenerator(); const issues: LintIssue[] = [];И передавать
nextIdвtraverse/pushIssue.Also applies to: 424-424
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/lint/layout-sizing.ts` around lines 16 - 19, Глобальный issueCounter/nextId приводит к коллизиям при параллельных вызовах; вместо модуля-уровневого state создайте локальный счётчик или фабрику nextId внутри функции checkLayoutSizing (например замкнутый генератор идентификаторов) и передайте этот локальный nextId во все места, которые создают issues (traverse / pushIssue), чтобы каждый запуск checkLayoutSizing имел свой независимый диапазон ID.src/lint/multi-theme.ts (5)
193-216: Аналогично дляgetVariableInfo: тихое поглощение ошибок.Рекомендуется добавить логирование для упрощения диагностики.
♻️ Предложенное улучшение
- } catch { + } catch (err) { + console.warn(`[multi-theme] Failed to get variable ${varId}:`, err); return null; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/lint/multi-theme.ts` around lines 193 - 216, The getVariableInfo function currently swallows all errors; update its catch block to log the caught error (including the varId and error details) before returning null so failures are diagnosable—inside getVariableInfo catch, capture the error (e) and call an existing logger or console.error with a message like "getVariableInfo failed for varId=<varId>" plus the error stack/details, then return null as before.
164-191: Тихое поглощение ошибок затрудняет отладку.Функция
getLocalCollectionsперехватывает все исключения и возвращает пустой массив, скрывая потенциальные проблемы. Рекомендуется как минимум логировать ошибку.♻️ Предложенное улучшение: добавить логирование
- } catch { + } catch (err) { + console.warn('[multi-theme] Failed to get local collections:', err); return []; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/lint/multi-theme.ts` around lines 164 - 191, The catch block in getLocalCollections silently swallows errors making debugging hard; update getLocalCollections to catch the error into a variable and log it (e.g., using console.error or the project logger) along with context (mentioning getLocalVariableCollectionsAsync/getLocalVariableCollections and figma.variables) before returning the empty array so failures are visible while preserving the current fallback behavior.
16-19: Глобальное изменяемое состояние может привести к некорректным ID при параллельных вызовах.
issueCounter— модульная переменная, которая сбрасывается вcheckMultiTheme. Если функция вызывается параллельно из разных контекстов, счётчик может привести к коллизиям ID.♻️ Предложенное исправление: инкапсулировать счётчик внутри функции
-let issueCounter = 0; -function nextId(): string { - return `mtheme-${++issueCounter}`; -} +function createIdGenerator(): () => string { + let counter = 0; + return () => `mtheme-${++counter}`; +}Затем в
checkMultiTheme:export async function checkMultiTheme(...): Promise<MultiThemeLintResult> { - issueCounter = 0; + const nextId = createIdGenerator(); const issues: LintIssue[] = [];🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/lint/multi-theme.ts` around lines 16 - 19, The module-level mutable issueCounter and nextId cause ID collisions under concurrent calls; remove the top-level issueCounter and instead define a local counter (and a local nextId closure) inside checkMultiTheme so each invocation gets its own encapsulated counter; update any uses of nextId within checkMultiTheme to call the local function and remove/replace the global nextId and issueCounter symbols.
237-240: Избыточная логика: severity устанавливается в 'info', затем сразу происходит возврат.Условие можно упростить — достаточно просто проверить тип переменной в начале.
♻️ Предложенное упрощение
if (allIdentical && firstValue !== undefined) { - // Only flag COLOR variables as warning (most common multi-theme issue) - // Other types as info since they may be intentionally identical - const severity = variable.resolvedType === 'COLOR' ? 'warning' as const : 'info' as const; - - // Skip if it's only info and not COLOR — too noisy for non-color vars - if (severity === 'info') return; + // Only flag COLOR variables — non-color vars are often intentionally identical + if (variable.resolvedType !== 'COLOR') return; const modeNames = collection.modes.map(m => m.name).join(', '); issues.push({ id: nextId(), type: 'naming', - severity, + severity: 'warning',🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/lint/multi-theme.ts` around lines 237 - 240, Упростить логику: вместо присваивания const severity = variable.resolvedType === 'COLOR' ? 'warning' : 'info' и сразу же проверки if (severity === 'info') return; — сразу проверяйте variable.resolvedType !== 'COLOR' и возвращайте; удалите переменную severity и связанный возврат, оставив только ранний exit по условию variable.resolvedType !== 'COLOR' и продолжение для COLOR-переменных.
444-449: Хрупкий подсчёт summary через string matching по сообщениям.Если текст сообщений изменится, подсчёт статистики сломается. Рекомендуется отслеживать счётчики непосредственно при создании issues.
♻️ Предложенное улучшение: инкрементировать счётчики при создании issues
+ const summary = { + totalVariables: 0, + identicalAcrossModes: 0, + missingModeValues: 0, + contrastDegradation: 0, + modeCountMismatch: 0, + }; // В каждой check-функции возвращать флаг или увеличивать счётчик: // checkIdenticalAcrossModes → summary.identicalAcrossModes++ // checkMissingModeValues → summary.missingModeValues++ // и т.д. return { issues, - summary: { - totalVariables, - identicalAcrossModes: issues.filter(i => i.message.includes('identical value across modes')).length, - missingModeValues: issues.filter(i => i.message.includes('missing values for modes')).length, - contrastDegradation: issues.filter(i => i.message.includes('Contrast passes')).length, - modeCountMismatch: issues.filter(i => i.message.includes('modes defined')).length, - }, + summary, };🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/lint/multi-theme.ts` around lines 444 - 449, Текущий подсчёт полей summary (identicalAcrossModes, missingModeValues, contrastDegradation, modeCountMismatch) хрупок, потому что фильтрует массив issues по тексту сообщения; вместо этого добавьте явные счётчики и инкрементируйте их в момент создания issue (там, где формируются и пушатся элементы в issues — найдите функцию/блок, который вызывает issues.push или createIssue). Объявите переменные-счётчики рядом с totalVariables и увеличивайте соответствующий счётчик при каждом создании конкретного типа issue (например для "identical value across modes", "missing values for modes", "Contrast passes", "modes defined"), затем используйте эти счётчики в summary (заменив фильтры по строкам) чтобы статистика оставалась корректной при изменении текста сообщений.ui/src/components/messages/A11ySpecCard.tsx (1)
247-277: Добавьте семантику tablist/tab/panel для вкладок.Сейчас вкладки визуально переключаются, но для скринридеров не передаётся выбранное состояние и связь кнопки с панелью.
♿ Предложение по семантике вкладок
- <div className="flex border-y border-border bg-bg-tertiary/30"> + <div role="tablist" aria-label="A11y sections" className="flex border-y border-border bg-bg-tertiary/30"> {TABS.map(({ key, label }) => ( <button key={key} + type="button" + role="tab" + id={`a11y-tab-${key}`} + aria-selected={activeTab === key} + aria-controls={`a11y-panel-${key}`} className={`flex-1 px-1 py-1.5 text-10 text-center transition-colors ${ activeTab === key ? 'text-fg font-semibold border-b-2 border-fg' : 'text-fg-tertiary hover:text-fg-secondary' }`} onClick={() => setActiveTab(key)} > {label} </button> ))} </div> - <div className="px-3 py-2 max-h-56 overflow-y-auto"> + <div + role="tabpanel" + id={`a11y-panel-${activeTab}`} + aria-labelledby={`a11y-tab-${activeTab}`} + className="px-3 py-2 max-h-56 overflow-y-auto" + >🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@ui/src/components/messages/A11ySpecCard.tsx` around lines 247 - 277, Wrap the tab buttons container with role="tablist" and update each button (in the TABS.map) to include role="tab", id={`tab-${key}`}, aria-controls={`panel-${key}`}, aria-selected={activeTab===key}, and tabIndex={activeTab===key?0:-1}, and call setActiveTab on click/keyboard events; for each rendered panel (the tab content divs that render LandmarksTab, HeadingsTab, etc.) give role="tabpanel", id={`panel-${key}`}, aria-labelledby={`tab-${key}`}, and set hidden or not rendered for non-active panels so screen readers see the active relationship; reference TABS, activeTab, setActiveTab and the panel components (LandmarksTab, HeadingsTab, FocusTab, ContrastTab, RecommendationsTab) when making these changes.ui/src/components/chat/MessageList.tsx (1)
129-144: Требуется добавить явные типы для новых видов сообщений вместоas any.Сейчас в
messages.tsэти типы определены какdata: unknown(строки 122–129), что заставляет использоватьas anyпри рендеринге в MessageList. Пропадает проверка контракта между моделью сообщения и компонентами карточек. Каждая карточка уже определяет свой интерфейс (например,DesignDebtDataв DesignDebtCard.tsx), но эти типы не экспортируются и не используются в объединённомChatMessageType.Решение: экспортировать типы данных из каждого компонента карточки и обновить объявления в
messages.ts:| { kind: 'design-debt'; data: DesignDebtData } | { kind: 'dark-mode'; data: DarkModeData } | { kind: 'a11y-spec'; data: A11ySpecData }и т. д., что автоматически сделает
as anyненужным.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@ui/src/components/chat/MessageList.tsx` around lines 129 - 144, The MessageList uses unsafe casts (e.g., data={m.data as any}) because message payloads in messages.ts are typed as data: unknown; export the specific data interfaces from each card component (e.g., export interface DesignDebtData from DesignDebtCard.tsx, DarkModeData from DarkModeCard.tsx, A11ySpecData from A11ySpecCard.tsx, etc.), update the ChatMessageType union in messages.ts to use those concrete types for each kind (e.g., { kind: 'design-debt'; data: DesignDebtData } etc.), then remove the as any casts in MessageList.tsx so each card (DesignDebtCard, DarkModeCard, A11ySpecCard, TokenComplianceCard, BrandConsistencyCard, CopyToneCard, PersonaResearchCard, AttentionHeatmapCard) receives a properly typed data prop.ui/src/components/messages/DarkModeCard.tsx (1)
12-17: Используйте общий типDarkModeMetricsизui/src/lib/messages.ts(Line 402-407), чтобы избежать дрейфа контрактов.Сейчас интерфейс продублирован локально; лучше импортировать единый источник типа.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@ui/src/components/messages/DarkModeCard.tsx` around lines 12 - 17, The local DarkModeMetrics interface in DarkModeCard.tsx is a duplicate; remove this definition and import the shared DarkModeMetrics type from ui/src/lib/messages.ts instead, then update the DarkModeCard props/type annotations (e.g., any props or function signatures referencing DarkModeMetrics) to use the imported type so the component consumes the canonical contract.ui/src/components/messages/PersonaResearchCard.tsx (1)
78-81: Добавьте accessibility-атрибуты на кнопки раскрытия секций.Рекомендуется добавить
type="button"иaria-expandedдля всех toggle-кнопок в карточке.Also applies to: 137-139, 181-183
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@ui/src/components/messages/PersonaResearchCard.tsx` around lines 78 - 81, The toggle buttons that call setExpandedPersona (e.g., the button using onClick={() => setExpandedPersona(isExpanded ? null : i)}) need explicit accessibility attributes: add type="button" to prevent accidental form submits and add aria-expanded={isExpanded} (or the equivalent boolean expression) so screen readers know the current state; apply the same changes to the other toggle buttons in the component (the ones around the handlers that use setExpandedPersona or similar isExpanded checks at the other mentioned locations).ui/src/components/messages/CopyToneCard.tsx (1)
66-69: Добавьтеtype="button"иaria-expandedдля кнопок секций.Для collapsible-секций стоит явно указывать тип кнопки и состояние раскрытия для скринридеров.
♿ Предлагаемый фикс
- <button + <button + type="button" className="flex items-center gap-1 text-11 font-medium text-fg mb-1 w-full" onClick={() => setShowTerms(!showTerms)} + aria-expanded={showTerms} > @@ - <button + <button + type="button" className="flex items-center gap-1 text-11 font-medium text-fg mb-1 w-full" onClick={() => setShowShifts(!showShifts)} + aria-expanded={showShifts} > @@ - <button + <button + type="button" className="flex items-center gap-1 text-11 font-medium text-fg mb-1 w-full" onClick={() => setShowRecs(!showRecs)} + aria-expanded={showRecs} >Also applies to: 110-113, 146-149
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@ui/src/components/messages/CopyToneCard.tsx` around lines 66 - 69, Update the toggle buttons in the CopyToneCard component to include explicit type and ARIA state: for the button that calls setShowTerms(!showTerms) add type="button" and aria-expanded={showTerms} (and similarly for the other toggles that call setShowExamples and any setShowX handlers), ensuring aria-expanded reflects the corresponding boolean state (showTerms, showExamples, etc.) so screen readers know the collapsed/expanded state; apply the same change to the other two button instances mentioned.ui/src/components/messages/AttentionHeatmapCard.tsx (1)
157-160: Для кнопки раскрытия рекомендаций добавьтеtype="button"иaria-expanded.Это улучшит предсказуемость поведения и доступность для ассистивных технологий.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@ui/src/components/messages/AttentionHeatmapCard.tsx` around lines 157 - 160, The toggle button in AttentionHeatmapCard (the <button> that calls setShowRecs(!showRecs)) needs explicit type and accessibility state: set type="button" to prevent it acting as a submit in forms, and add aria-expanded with the current showRecs value (e.g., aria-expanded={showRecs}) so screen readers know the panel state; update the button element where onClick={() => setShowRecs(!showRecs)} is defined to include these attributes.ui/src/lib/messages.ts (2)
347-372: Дублирование типов сsrc/extract/variable-collector.ts.Интерфейсы
VariableCollectionData,VariableData,VariableSystemReportопределены и здесь, и вsrc/extract/variable-collector.ts. Это может привести к рассинхронизации при изменениях.Если архитектура позволяет, рассмотрите вынесение общих типов в shared пакет или реэкспорт из единого источника. Если UI не имеет доступа к src/, оставьте как есть, но добавьте комментарий о необходимости синхронизации:
// NOTE: Keep in sync with src/extract/variable-collector.ts export interface VariableCollectionData {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@ui/src/lib/messages.ts` around lines 347 - 372, The interfaces VariableCollectionData, VariableData, and VariableSystemReport are duplicated here and in src/extract/variable-collector.ts; either extract and re-export these types from a single shared module (preferred) or, if the UI cannot import src/, add a clear sync comment above each interface (e.g., "NOTE: Keep in sync with src/extract/variable-collector.ts") to avoid drift—update VariableCollectionData, VariableData, and VariableSystemReport definitions accordingly and remove duplication by importing/re-exporting from the shared source when possible.
122-129: Использованиеunknownдля новых типов сообщений снижает типобезопасность.Новые message types (
design-debt,dark-mode,a11y-spec,token-compliance,brand-consistency,copy-tone,persona-research,attention-heatmap) используютdata: unknown. Это затрудняет автокомплит и проверку типов при обработке этих сообщений в UI.Рекомендуется определить конкретные интерфейсы для каждого типа данных. Например:
| { kind: 'design-debt'; data: DesignDebtData } | { kind: 'dark-mode'; data: DarkModeResult } // и т.д.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@ui/src/lib/messages.ts` around lines 122 - 129, The union message variants currently use data: unknown which reduces type safety; define concrete interfaces (e.g., DesignDebtData, DarkModeResult, A11ySpecData, TokenComplianceData, BrandConsistencyData, CopyToneResult, PersonaResearchData, AttentionHeatmapData) and replace each union member’s data: unknown with the corresponding interface (update the Message union in ui/src/lib/messages.ts so { kind: 'design-debt'; data: DesignDebtData } etc.), add exported type/interface definitions for each new Data shape, and adjust any places that construct or consume Message (handlers, switch/case) to use the new typed fields so consumers get proper autocomplete and compile-time checks.ui/src/components/messages/BrandConsistencyCard.tsx (2)
98-101: Неоптимальная обработка disabled кнопки.Кнопка имеет
disabled={!hasContent}, но также вызываетonClickс проверкойhasContent &&. Это избыточно — если кнопка disabled, клик не должен обрабатываться браузером. Однако для надёжности можно оставить проверку в onClick.♻️ Альтернативный вариант: упростить onClick
<button className="flex items-center justify-between w-full text-11 py-0.5" - onClick={() => hasContent && toggle(key)} + onClick={() => toggle(key)} disabled={!hasContent} >🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@ui/src/components/messages/BrandConsistencyCard.tsx` around lines 98 - 101, The button currently uses both disabled={!hasContent} and a redundant onClick check (hasContent && toggle(key)); simplify by removing the redundant conditional in the onClick handler so it simply calls toggle(key) (e.g., onClick={() => toggle(key)}) while keeping disabled={!hasContent} for accessibility; reference the BrandConsistencyCard component, the button element, the hasContent variable, and the toggle(key) call when making this change.
98-121: Отсутствуетaria-expandedдля доступности.Кнопки раскрывающихся секций должны сообщать скринридерам о своём состоянии. Добавьте атрибут
aria-expandedдля улучшения доступности.♿ Предложение: добавить aria-expanded
<button className="flex items-center justify-between w-full text-11 py-0.5" onClick={() => hasContent && toggle(key)} disabled={!hasContent} + aria-expanded={isOpen} >🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@ui/src/components/messages/BrandConsistencyCard.tsx` around lines 98 - 121, The button in BrandConsistencyCard.tsx is missing an accessibility state; add aria-expanded to the collapsible button element (the button that uses hasContent, toggle(key), isOpen, label, count, color) so screen readers know whether the section is open—set aria-expanded to the current open state (e.g., true/false driven by isOpen, or false when !hasContent) and keep the existing disabled logic intact.ui/src/hooks/useChat.ts (1)
500-507:CATEGORY_WEIGHTне содержит весов для новых типов ошибок.
TYPE_LABELвключаетfittsLaw,gestalt,detachedInstance,responsive, ноCATEGORY_WEIGHTне имеет соответствующих весов. Эти типы будут использовать дефолтное значение0.04(строка 520), что может неточно отражать их реальное влияние на UX.📊 Предложение: добавить веса для новых категорий
const CATEGORY_WEIGHT: Record<string, number> = { fill: 0.20, stroke: 0.20, effect: 0.20, text: 0.20, // tokens bucket accessibility: 0.20, spacing: 0.12, visualQuality: 0.10, conversion: 0.10, microcopy: 0.08, cognitive: 0.08, autoLayout: 0.08, radius: 0.04, // naming bucket + fittsLaw: 0.10, gestalt: 0.08, detachedInstance: 0.06, responsive: 0.10, };🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@ui/src/hooks/useChat.ts` around lines 500 - 507, CATEGORY_WEIGHT is missing entries for new error types referenced by TYPE_LABEL (fittsLaw, gestalt, detachedInstance, responsive), so those currently fall back to the default 0.04; add explicit weight entries to CATEGORY_WEIGHT for these keys with appropriate impact values (e.g., fittsLaw: 0.12, gestalt: 0.10, detachedInstance: 0.06, responsive: 0.10) so sorting reflects their true UX impact and avoids unintended defaults.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 21249695-7eb0-480d-8154-b9716ba0077c
⛔ Files ignored due to path filters (2)
dist/code.jsis excluded by!**/dist/**dist/ui.htmlis excluded by!**/dist/**
📒 Files selected for processing (39)
backend/src/prompts/attention.tsbackend/src/prompts/chat-followup.tsbackend/src/prompts/nielsen-heuristics.tsbackend/src/prompts/page-type.tsbackend/src/prompts/review.tsbackend/src/prompts/shared/grounding-instructions.tsbackend/src/prompts/three-layer.tsbackend/src/routes/analyze.tsbackend/src/routes/chat.tsbackend/src/services/analyzer.tsbackend/src/services/claude.tsbackend/src/services/confidence-filter.tssrc/code.tssrc/lint/component-props.tssrc/lint/constraints.tssrc/lint/grid-check.tssrc/lint/layout-sizing.tssrc/lint/multi-theme.tssrc/lint/style-audit.tssrc/lint/typography.tssrc/lint/variable-scope.tssrc/ui/message-handler.tsui/src/App.tsxui/src/components/chat/ChatContainer.tsxui/src/components/chat/MessageList.tsxui/src/components/chat/StickyHeader.tsxui/src/components/messages/A11ySpecCard.tsxui/src/components/messages/AttentionHeatmapCard.tsxui/src/components/messages/BrandConsistencyCard.tsxui/src/components/messages/CopyToneCard.tsxui/src/components/messages/DarkModeCard.tsxui/src/components/messages/DesignDebtCard.tsxui/src/components/messages/PersonaResearchCard.tsxui/src/components/messages/TokenComplianceCard.tsxui/src/components/shared/ScaleEditor.tsxui/src/components/shared/SeveritySelector.tsxui/src/components/shared/TeamConfigPanel.tsxui/src/hooks/useChat.tsui/src/lib/messages.ts
✅ Files skipped from review due to trivial changes (1)
- backend/src/prompts/three-layer.ts
🚧 Files skipped from review as they are similar to previous changes (2)
- backend/src/routes/analyze.ts
- backend/src/prompts/attention.ts
| sessionContext: string, | ||
| analysisJson?: string, |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
fd 'session\.ts$' backend/src/services --exec sed -n '1,260p' {}
echo
rg -n -C3 --type ts '\bai_review\b|\baiReview\b|saveAnalysisResult\s*\(|loadSession\s*\(' backend/srcRepository: lemone112/figmalint
Length of output: 13474
В saveAnalysisResult() не сериализуется aiReview перед сохранением в БД.
analyzer.ts передаёт aiReview как объект AiReviewResult, но saveAnalysisResult() (строка 25) сохраняет его напрямую без JSON.stringify(). База данных ожидает ai_review: string | null (backend/src/db/queries.ts:34), поэтому объект будет сохранён как "[object Object]" или аналогичная поломанная представление. Когда позже loadSession() возвращает эту строку и buildFollowupPrompt() пытается её распарсить, JSON.parse() молча падает в catch, и данные AiReviewResult теряются.
Исправление: в saveAnalysisResult() нужно добавить условное сериализацию для aiReview и lintResult:
ai_review: typeof aiReview === 'string' ? aiReview : JSON.stringify(aiReview),
lint_result: typeof lintResult === 'string' ? lintResult : JSON.stringify(lintResult),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@backend/src/prompts/chat-followup.ts` around lines 8 - 9, saveAnalysisResult
currently writes aiReview and lintResult objects directly to the DB causing them
to be stored as "[object Object]" and later fail to parse in
loadSession/buildFollowupPrompt; update saveAnalysisResult to conditionally
serialize these fields before saving (e.g., for aiReview and lintResult use
typeof value === 'string' ? value : JSON.stringify(value)) so the ai_review
column receives a proper JSON string or null.
| ## Response Format (JSON) | ||
| { | ||
| "heuristics": [ | ||
| { | ||
| "id": "H1", | ||
| "name": "Visibility of System Status", | ||
| "rating": "pass|needs_improvement|fail", | ||
| "evidence": ["<specific observation from the screenshot>"], | ||
| "recommendation": "<actionable fix or null if pass>" |
There was a problem hiding this comment.
Не считайте неоцениваемую эвристику за pass.
Для статичного скриншота pass означает “эвристика соблюдена”, а не “мы не смогли это проверить”. В таком виде H7/H10 и похожие случаи будут искусственно завышать итоговую оценку и искажать topViolations/summary; здесь нужен отдельный статус вроде not_assessable либо исключение таких эвристик из агрегирования.
Also applies to: 79-80
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@backend/src/prompts/nielsen-heuristics.ts` around lines 55 - 63, The current
JSON schema and rating assignment in the heuristics array treats unassessable
items as "pass"; change the logic so ratings can be "not_assessable" (or omit
the heuristic from scoring) instead of "pass" for cases like H7/H10 where a
static screenshot cannot be evaluated. Update the code that builds each
heuristic object (the objects with fields id, name, rating, evidence,
recommendation in backend/src/prompts/nielsen-heuristics.ts) to assign
"not_assessable" when evidence is insufficient, and modify the aggregation logic
that produces topViolations/summary to exclude "not_assessable" entries from
pass/fail counts and rankings.
| severityScore(namingErrors, total) * 0.04 | ||
| ); | ||
|
|
||
| // Phase 3: Optional extended features (non-blocking, run in parallel) |
There was a problem hiding this comment.
Это всё ещё блокирует основной ответ /analyze.
await Promise.allSettled(extendedPromises) держит request open до завершения обоих дополнительных Claude-вызовов. При включённых threeLayerExplanations/confidenceScoring это добавляет синхронную latency к базовому анализу и фактически нивелирует идею “non-blocking” enrichment; если нужен именно фоновой добор данных, этот блок не должен await-иться на request path.
Also applies to: 331-334
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@backend/src/services/analyzer.ts` at line 235, The current await
Promise.allSettled(extendedPromises) (and the similar await at the other
location) blocks the /analyze request; change it to fire-and-forget so
extendedClaude calls (triggered by threeLayerExplanations and confidenceScoring)
run in background without awaiting on the request path. Implement this by moving
the Promise.allSettled logic into a separate async helper (e.g.,
handleExtendedPromises) and invoke it without awaiting (e.g., void
handleExtendedPromises(extendedPromises) or schedule via
setImmediate/process.nextTick), ensuring the helper still awaits
Promise.allSettled and logs errors/results; update references where
Promise.allSettled is currently awaited (the block managing extendedPromises and
the duplicate at lines ~331-334) to use the non-blocking helper instead.
| const handleAdd = useCallback(() => { | ||
| const num = parseInt(inputValue.trim(), 10); | ||
| if (isNaN(num) || num < 0) return; | ||
| if (values.includes(num)) { |
There was a problem hiding this comment.
onBlur может оставлять редактор в режиме добавления при невалидном вводе.
Если значение невалидно/пустое, handleAdd выходит раньше и не сбрасывает isAdding, из-за чего UI может зависнуть в состоянии input.
💡 Предлагаемый фикс
const handleAdd = useCallback(() => {
const num = parseInt(inputValue.trim(), 10);
if (isNaN(num) || num < 0) return;
@@
setInputValue('');
setIsAdding(false);
}, [inputValue, values, onChange]);
+
+ const handleBlur = useCallback(() => {
+ handleAdd();
+ setInputValue('');
+ setIsAdding(false);
+ }, [handleAdd]);
@@
- onBlur={handleAdd}
+ onBlur={handleBlur}Also applies to: 139-140
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@ui/src/components/shared/ScaleEditor.tsx` around lines 34 - 37, handleAdd
currently returns early on invalid/duplicate input without clearing isAdding,
leaving the UI stuck in "adding" mode; update handleAdd to call
setIsAdding(false) (and optionally clear inputValue) on every early return and
after successful add, and also ensure the onBlur handler similarly clears
isAdding so blur with invalid/empty input exits add mode; reference the
handleAdd function and the onBlur handler in ScaleEditor and add
setIsAdding(false) to all exit paths (including duplicate/value NaN checks and
empty input cases).
Backend (4 fixes): - CRITICAL: safe fallback for page-type parsing — empty/garbage text returns 'other' instead of random tokens (claude.ts) - Nielsen heuristics: add 'not_assessable' rating for heuristics that can't be evaluated from a static screenshot (H7, H10) - Extended features (three-layer, confidence) now fire-and-forget — no longer block /analyze response - Verified saveAnalysisResult already stringifies objects via queries.ts Lint modules (6 fixes): - component-props: check node.description instead of non-existent ComponentPropertyDefinition.description field - constraints: use layoutSizingHorizontal/Vertical === 'FIXED' instead of width/height > 0; fix counter to track actual issue count - grid-check: add skipLocked/skipHidden checks for root-level nodes - multi-theme: add 'theme' to LintIssueType, replace 'naming' type - typography: cache collectTextNodes results by node.id to avoid redundant tree traversals on large documents - variable-scope: field-specific color scope validation (fills vs strokes vs textRangeFills) instead of blanket isColorField check UI (4 fixes): - App.tsx: race condition guard for concurrent page sweep requests - DarkModeCard: show N/A badge + neutral bar when totalChecked === 0 - ScaleEditor: handleBlur always resets isAdding state - TeamConfigPanel: useEffect syncs initialConfig prop changes Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 10
♻️ Duplicate comments (1)
src/lint/variable-scope.ts (1)
37-40:⚠️ Potential issue | 🟠 MajorПроверка color-scope всё ещё пропускает часть mismatch’ей.
На Line 37-40
fillsпо-прежнему смешиваетFRAME_FILL,SHAPE_FILLиTEXT_FILLв один bucket, а вVariableUsageна Line 119-124 нет контекстаnode.type, чтобы их различить. ПоэтомуhasMatchingScopeна Line 254 становитсяtrueдаже дляTEXT_FILL, привязанного к обычному fill, и ветка сCOLOR_FIELD_EXPECTED_SCOPESне выполняется. ПлюсtextRangeFillsесть на Line 239, но отсутствует вFIELD_TO_EXPECTED_SCOPES, из-за чего на Line 247-248 функция выходит раньше и этот кейс вообще не валидируется.Also applies to: 119-124, 236-266
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/lint/variable-scope.ts` around lines 37 - 40, FIELD_TO_EXPECTED_SCOPES currently lumps fills together and omits textRangeFills, and VariableUsage matching lacks node.type context so hasMatchingScope returns true incorrectly; update FIELD_TO_EXPECTED_SCOPES to include separate keys for FRAME_FILL, SHAPE_FILL, TEXT_FILL and textRangeFills (mapping each to their correct expected scopes), then modify the VariableUsage matching logic (the code that constructs/uses hasMatchingScope and the branch that checks COLOR_FIELD_EXPECTED_SCOPES) to consider the node.type or node.kind when selecting which fill-key to check so TEXT_FILL bindings are only matched against TEXT-specific expected scopes; ensure the early-return path that checks FIELD_TO_EXPECTED_SCOPES covers textRangeFills too so the COLOR_FIELD_EXPECTED_SCOPES branch executes for text-range fills as intended.
🧹 Nitpick comments (8)
ui/src/App.tsx (1)
279-283: Проверка на устаревший запрос вelse-ветке неэффективна.На строке 280 проверка
pageSweepRequestId.current !== requestIdвсегда будетfalse, потому чтоrequestIdбыл только что присвоен из++pageSweepRequestId.currentна строке 258. Эта проверка имеет смысл только в асинхронных колбэках (.then/.catch), где между запросом и ответом мог прийти новый запрос.В синхронной
else-ветке проверка не нужна и может быть удалена для ясности кода.♻️ Предлагаемое исправление
} else { - if (pageSweepRequestId.current !== requestId) break; const deterministicResult = buildDeterministicSweepResult(sweepData); chat.addMessage({ kind: 'page-sweep-result', data: deterministicResult }); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@ui/src/App.tsx` around lines 279 - 283, The check for a stale request inside the synchronous else branch is redundant because requestId was just set from ++pageSweepRequestId.current; remove the conditional "if (pageSweepRequestId.current !== requestId) break;" from that else branch and simply compute deterministicResult via buildDeterministicSweepResult(sweepData) and call chat.addMessage({ kind: 'page-sweep-result', data: deterministicResult }); — keep the stale-request check only in asynchronous callbacks (e.g., .then/.catch) where requestId can become outdated.ui/src/components/shared/TeamConfigPanel.tsx (2)
84-88: Улучшение доступности: добавьтеaria-controlsиidдля связи кнопки с контентом.Для полной семантики ARIA рекомендуется связать кнопку с раскрываемым контентом через
aria-controls.♿ Предлагаемое улучшение
+import { useId } from 'react'; + function Section({ title, defaultOpen = false, children, }: { title: string; defaultOpen?: boolean; children: React.ReactNode; }) { const [open, setOpen] = useState(defaultOpen); + const contentId = useId(); return ( <section className="border-b border-border last:border-b-0"> <button type="button" onClick={() => setOpen((o) => !o)} className="w-full flex items-center justify-between px-3 py-2 hover:bg-bg-hover transition-colors" aria-expanded={open} + aria-controls={contentId} > ... </button> - {open && <div className="px-3 pb-3 space-y-2">{children}</div>} + {open && <div id={contentId} className="px-3 pb-3 space-y-2">{children}</div>} </section> ); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@ui/src/components/shared/TeamConfigPanel.tsx` around lines 84 - 88, The button toggling the panel in TeamConfigPanel.tsx (the button using onClick={() => setOpen(o => !o)} and aria-expanded={open}) should be linked to the collapsible content with aria-controls and an id on the content element; generate or use a stable unique id (e.g., via React's useId or a panelId constant) and set the button's aria-controls to that id and the content container's id to the same value (ensure the content container is the element that shows/hides and/or has role="region"). Update references to the button, open state, and the content container (panel element) accordingly so the ARIA relationship is present and unique per panel.
10-10: Рассмотрите импортDEFAULT_SPACING_SCALEиз общего модуля.Значение
DEFAULT_SPACING_SCALEдублирует константу изsrc/lint/types.ts(строка 69). При изменении одной из них другая может остаться устаревшей.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@ui/src/components/shared/TeamConfigPanel.tsx` at line 10, В файле TeamConfigPanel.tsx удалите локальное дублирование константы DEFAULT_SPACING_SCALE и импортируйте её из единого источника (тот модуль, где она уже объявлена, например lint/types), заменив локальное const DEFAULT_SPACING_SCALE = [...] на: import { DEFAULT_SPACING_SCALE } from 'that-module'; убедитесь, что все использования в компоненте продолжают ссылаться на DEFAULT_SPACING_SCALE и выполните сборку/тесты чтобы проверить отсутствие регрессий.backend/src/services/analyzer.ts (1)
258-263: Вынесите парсинг JSON-ответа LLM в общий helper.Одинаковая логика regex+
JSON.parseповторяется в двух местах. Это лучше централизовать (с единым обработчиком ошибок и валидацией структуры), чтобы снизить риск расхождений.Also applies to: 315-320
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@backend/src/services/analyzer.ts` around lines 258 - 263, Extract the repeated regex + JSON.parse logic into a shared helper (e.g., parseLLMJsonResponse or parseJsonFromContent) that accepts the LLM response content (the same object checked in the analyzer code) and returns either the parsed object or null, performs the /\{[\s\S]*\}/ match, wraps JSON.parse in try/catch, and validates the expected shape (e.g., ensures parsed.explanations is an array); then replace the inline code in analyzer.ts that sets threeLayerExplanations (the block using response.content[0].text.match and JSON.parse) and the similar block at the other location (around the second occurrence) to call this helper, handle nulls consistently, and centralize logging/errors in the helper.src/lint/multi-theme.ts (2)
423-433: Последовательныйawaitна каждыйvarIdухудшает производительность на больших коллекциях.Сейчас запросы к переменным выполняются строго по одному; здесь уместна батчевая загрузка через
Promise.allс последующей фильтрациейnull.Предложение исправления
- for (const varId of collection.variableIds) { - const variable = await getVariableInfo(varId); - if (!variable) continue; - - collectionVars.push(variable); - totalVariables++; - - checkIdenticalAcrossModes(variable, collection, issues); - checkMissingModeValues(variable, collection, issues); - checkModeCountMismatch(variable, collection, issues); - } + const loadedVars = (await Promise.all( + collection.variableIds.map(varId => getVariableInfo(varId)) + )).filter((v): v is VariableInfo => v !== null); + + for (const variable of loadedVars) { + collectionVars.push(variable); + totalVariables++; + checkIdenticalAcrossModes(variable, collection, issues); + checkMissingModeValues(variable, collection, issues); + checkModeCountMismatch(variable, collection, issues); + }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/lint/multi-theme.ts` around lines 423 - 433, The loop is awaiting getVariableInfo sequentially for each collection.variableIds which hurts performance; instead, batch-load all variables with Promise.all on collection.variableIds mapped to getVariableInfo, then filter out null/undefined results, push those into collectionVars and increment totalVariables accordingly, and finally run checkIdenticalAcrossModes, checkMissingModeValues, and checkModeCountMismatch for each loaded variable (use the same function names: getVariableInfo, collectionVars, totalVariables, checkIdenticalAcrossModes, checkMissingModeValues, checkModeCountMismatch).
446-449: Summary считается поmessage.includes(...), это хрупко.Любое изменение текста сообщений сломает метрики без ошибок компиляции. Лучше считать агрегаты по явному признаку (например, отдельный
subtype/counter в моментissues.push).🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/lint/multi-theme.ts` around lines 446 - 449, The current aggregates (identicalAcrossModes, missingModeValues, contrastDegradation, modeCountMismatch) compute counts by string-matching issue.message which is brittle; update the issue creation site (where issues.push is called) to include an explicit discriminant (e.g., issue.subtype or issue.code) for each kind of lint error, then change the aggregations in multi-theme.ts to count by that discriminant (e.g., issues.filter(i => i.subtype === 'IDENTICAL_ACROSS_MODES').length) instead of message.includes; ensure all existing pushes that create these issue objects are updated to set the new field and adjust any consumers accordingly.ui/src/components/messages/DarkModeCard.tsx (2)
146-151: Добавьте семантику кнопке раскрытия списка.У кнопки лучше явно задать
type="button"и состояниеaria-expanded, чтобы избежать побочных submit-эффектов и улучшить доступность.♿ Небольшой a11y/поведенческий фикс
{issues.length > 4 && ( <button + type="button" + aria-expanded={expanded} className="w-full text-11 text-bg-brand hover:underline mt-1 text-center" onClick={() => setExpanded(!expanded)} >🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@ui/src/components/messages/DarkModeCard.tsx` around lines 146 - 151, In DarkModeCard.tsx update the toggle button element that uses expanded and setExpanded to include explicit type="button" and an aria-expanded={expanded} attribute; locate the button using the expanded state and setExpanded handler (and the issues array usage for the label) and add these attributes to prevent accidental form submits and improve accessibility.
3-33: Стоит убрать дублирование контрактов и импортировать типы изui/src/lib/messages.ts.Сейчас типы данных для карточки определены локально, хотя общие dark-mode типы уже есть в
ui/src/lib/messages.ts(см.DarkModeMetrics, Line 402-407). Это повышает риск расхождения схем.♻️ Предлагаемый рефактор
-interface DarkModeIssue { - type: string; - severity: string; - nodeName: string; - message: string; - currentValue?: string; - suggestions?: string[]; -} - -interface DarkModeMetrics { - pureBlackBackgrounds: number; - pureWhiteText: number; - lowContrastOnDark: number; - missingModeValues: number; -} - -interface DarkModeSummary { - totalChecked: number; - passed: number; - failed: number; -} - -interface DarkModeData { - issues: DarkModeIssue[]; - metrics: DarkModeMetrics; - summary: DarkModeSummary; -} +import type { DarkModeData } from '../../lib/messages';🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@ui/src/components/messages/DarkModeCard.tsx` around lines 3 - 33, The file defines local duplicate types (DarkModeIssue, DarkModeMetrics, DarkModeSummary, DarkModeData, DarkModeCardProps) that already exist in ui/src/lib/messages.ts; replace these local definitions by importing the shared types from that module (e.g., import { DarkModeMetrics, DarkModeIssue, DarkModeSummary, DarkModeData } from 'ui/src/lib/messages') and update DarkModeCardProps to reference the imported DarkModeData (or export a Props type that uses the shared types) so the component in DarkModeCard.tsx uses the centralized contracts and avoids schema drift.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@backend/src/services/analyzer.ts`:
- Around line 331-347: The extended feature results are built into
extendedUpdates inside the fire-and-forget async IIFE (extendedPromises,
threeLayerExplanations, confidencedFindings, sessionId) but never persisted or
returned; fix by actually saving extendedUpdates to the session/store after the
log (use the existing session persistence method used elsewhere in analyzer.ts)
and ensure the outer function’s return (the block that currently returns other
results at the end) includes threeLayerExplanations and confidencedFindings so
callers receive those computed values.
- Around line 56-57: The JSDoc for the boolean option threeLayerExplanations is
misleading ("Default: 0 (disabled)"); update the comment to use a boolean
default wording (e.g., "Default: false (disabled)") and, if there's any default
value assignment or config initialization for threeLayerExplanations elsewhere,
ensure it uses false rather than 0 so the type and docs are consistent with the
boolean signature.
In `@backend/src/services/claude.ts`:
- Around line 66-70: Validate and sanitize the fields from parsed before
returning: ensure parsed.type is a string and matches the allowed set (normalize
with .toLowerCase() and switch/whitelist, otherwise default to 'other'); coerce
parsed.confidence to a number and clamp it to the [0,1] range (fallback to 0.5
if missing/NaN); and ensure parsed.signals is an array of strings by filtering
out non-string elements and trimming entries (or map values to String and filter
empty strings) so the returned object (type, confidence, signals) always
conforms to the legacy fallback contract.
In `@src/lint/grid-check.ts`:
- Around line 204-216: The current traversal can push the same frame twice (via
walk and the root loop), so before returning ensure top-level frames are
deduplicated: collect frames from result and from the root-level nodes into a
single list (referencing result, walk(), isTopLevelFrame, and the root-level
nodes variable), deduplicate by node.id (or id property) and then return that
deduplicated array; apply the same dedup step to the analogous block at lines
~239-259 to avoid duplicated issues and summaries.
- Around line 293-323: Сейчас блок, который вычисляет mostCommonCount
(переменные countFreq, mostCommonCount, maxFreq) не обрабатывает ситуацию, когда
несколько значений колонок имеют одинаковую максимальную частоту, и в таком
случае выдается ложное предупреждение при добавлении записи в issues (см.
columnCounts и вызов issues.push). Исправьте это: при обходе countFreq
определяйте все значения с freq === maxFreq (например собрать их в массив), и
если их больше одного — не помечать кадры как использующие «большинство» (т.е.
пропустить генерацию предупреждений в цикле по columnCounts или заменить текст
сообщения на нейтральный), иначе продолжать как сейчас; обновите логику вокруг
mostCommonCount/uniqueCounts и места, где используется nextId/issues.push, чтобы
не выдавать warning при ничье.
In `@src/lint/multi-theme.ts`:
- Around line 16-19: The global issueCounter/nextId causes ID collisions across
parallel runs; remove the module-level issueCounter and either generate IDs
locally per invocation of checkMultiTheme (e.g., a local counter variable inside
checkMultiTheme) or use a collision-safe generator such as
crypto.randomUUID()/nanoid. Replace references to the module-level nextId with a
new local generator (or call to crypto.randomUUID()) and ensure issues are
assigned IDs from that local generator so each checkMultiTheme run produces
unique, non-overlapping IDs.
- Around line 272-284: The current check only reports when some mode-values are
missing but not when all are missing or when values exist only as stale keys;
update the condition around the issues.push block (where missingModes,
collection.modes, variable, nextId and issues.push are used) to also trigger
when missingModes.length === collection.modes.length or when all present keys
are stale, and adjust the message/currentValue/suggestions to reflect a fully
missing case (e.g., "no values defined for any modes" and "Defined in 0/N modes"
or include stale-state note). Apply the same change to the analogous block
handling modes at the other location (the block around lines 298-318).
In `@src/lint/typography.ts`:
- Around line 79-97: collectTextNodes currently caches and returns all TEXT
nodes under a node without respecting skipHidden/skipLocked, causing
checkInconsistentAlignment and checkParagraphSpacing to report hidden/locked
text; change collectTextNodes to accept the same skip flags (e.g., skipHidden:
boolean, skipLocked: boolean) or a predicate, apply those checks when recursing
and before pushing TextNode, and include the flags in the cache key (or maintain
separate cache maps) so cached results respect the filters; update callers
(checkInconsistentAlignment, checkParagraphSpacing, any other callers) to pass
the correct flags or predicate so aggregate checks only consider
visible/unlocked text.
- Around line 128-156: The code picks the first encountered align as majority
even on ties; change the logic around alignments/majorityAlign so a strict
majority is required: compute maxCount, then check how many groups have
nodes.length === maxCount and if that count > 1 (tie) do not set a majority (or
return 0) and skip flagging; otherwise keep the current majorityAlign and
proceed to call pushIssue for non-majority items. Update the block that
calculates majorityAlign (using alignments and maxCount) and the subsequent loop
that uses majorityAlign so ties produce no issues.
In `@ui/src/components/messages/DarkModeCard.tsx`:
- Around line 120-123: The list is using array index as React keys in
displayIssues.map which can cause incorrect DOM reuse; update the DarkModeIssue
interface to include a unique id: string, ensure all producers of DarkModeIssue
supply that id, and change the map to use key={issue.id} instead of key={i}
(look for DarkModeIssue definition and the displayIssues.map rendering in
DarkModeCard.tsx to make the edits).
---
Duplicate comments:
In `@src/lint/variable-scope.ts`:
- Around line 37-40: FIELD_TO_EXPECTED_SCOPES currently lumps fills together and
omits textRangeFills, and VariableUsage matching lacks node.type context so
hasMatchingScope returns true incorrectly; update FIELD_TO_EXPECTED_SCOPES to
include separate keys for FRAME_FILL, SHAPE_FILL, TEXT_FILL and textRangeFills
(mapping each to their correct expected scopes), then modify the VariableUsage
matching logic (the code that constructs/uses hasMatchingScope and the branch
that checks COLOR_FIELD_EXPECTED_SCOPES) to consider the node.type or node.kind
when selecting which fill-key to check so TEXT_FILL bindings are only matched
against TEXT-specific expected scopes; ensure the early-return path that checks
FIELD_TO_EXPECTED_SCOPES covers textRangeFills too so the
COLOR_FIELD_EXPECTED_SCOPES branch executes for text-range fills as intended.
---
Nitpick comments:
In `@backend/src/services/analyzer.ts`:
- Around line 258-263: Extract the repeated regex + JSON.parse logic into a
shared helper (e.g., parseLLMJsonResponse or parseJsonFromContent) that accepts
the LLM response content (the same object checked in the analyzer code) and
returns either the parsed object or null, performs the /\{[\s\S]*\}/ match,
wraps JSON.parse in try/catch, and validates the expected shape (e.g., ensures
parsed.explanations is an array); then replace the inline code in analyzer.ts
that sets threeLayerExplanations (the block using response.content[0].text.match
and JSON.parse) and the similar block at the other location (around the second
occurrence) to call this helper, handle nulls consistently, and centralize
logging/errors in the helper.
In `@src/lint/multi-theme.ts`:
- Around line 423-433: The loop is awaiting getVariableInfo sequentially for
each collection.variableIds which hurts performance; instead, batch-load all
variables with Promise.all on collection.variableIds mapped to getVariableInfo,
then filter out null/undefined results, push those into collectionVars and
increment totalVariables accordingly, and finally run checkIdenticalAcrossModes,
checkMissingModeValues, and checkModeCountMismatch for each loaded variable (use
the same function names: getVariableInfo, collectionVars, totalVariables,
checkIdenticalAcrossModes, checkMissingModeValues, checkModeCountMismatch).
- Around line 446-449: The current aggregates (identicalAcrossModes,
missingModeValues, contrastDegradation, modeCountMismatch) compute counts by
string-matching issue.message which is brittle; update the issue creation site
(where issues.push is called) to include an explicit discriminant (e.g.,
issue.subtype or issue.code) for each kind of lint error, then change the
aggregations in multi-theme.ts to count by that discriminant (e.g.,
issues.filter(i => i.subtype === 'IDENTICAL_ACROSS_MODES').length) instead of
message.includes; ensure all existing pushes that create these issue objects are
updated to set the new field and adjust any consumers accordingly.
In `@ui/src/App.tsx`:
- Around line 279-283: The check for a stale request inside the synchronous else
branch is redundant because requestId was just set from
++pageSweepRequestId.current; remove the conditional "if
(pageSweepRequestId.current !== requestId) break;" from that else branch and
simply compute deterministicResult via buildDeterministicSweepResult(sweepData)
and call chat.addMessage({ kind: 'page-sweep-result', data: deterministicResult
}); — keep the stale-request check only in asynchronous callbacks (e.g.,
.then/.catch) where requestId can become outdated.
In `@ui/src/components/messages/DarkModeCard.tsx`:
- Around line 146-151: In DarkModeCard.tsx update the toggle button element that
uses expanded and setExpanded to include explicit type="button" and an
aria-expanded={expanded} attribute; locate the button using the expanded state
and setExpanded handler (and the issues array usage for the label) and add these
attributes to prevent accidental form submits and improve accessibility.
- Around line 3-33: The file defines local duplicate types (DarkModeIssue,
DarkModeMetrics, DarkModeSummary, DarkModeData, DarkModeCardProps) that already
exist in ui/src/lib/messages.ts; replace these local definitions by importing
the shared types from that module (e.g., import { DarkModeMetrics,
DarkModeIssue, DarkModeSummary, DarkModeData } from 'ui/src/lib/messages') and
update DarkModeCardProps to reference the imported DarkModeData (or export a
Props type that uses the shared types) so the component in DarkModeCard.tsx uses
the centralized contracts and avoids schema drift.
In `@ui/src/components/shared/TeamConfigPanel.tsx`:
- Around line 84-88: The button toggling the panel in TeamConfigPanel.tsx (the
button using onClick={() => setOpen(o => !o)} and aria-expanded={open}) should
be linked to the collapsible content with aria-controls and an id on the content
element; generate or use a stable unique id (e.g., via React's useId or a
panelId constant) and set the button's aria-controls to that id and the content
container's id to the same value (ensure the content container is the element
that shows/hides and/or has role="region"). Update references to the button,
open state, and the content container (panel element) accordingly so the ARIA
relationship is present and unique per panel.
- Line 10: В файле TeamConfigPanel.tsx удалите локальное дублирование константы
DEFAULT_SPACING_SCALE и импортируйте её из единого источника (тот модуль, где
она уже объявлена, например lint/types), заменив локальное const
DEFAULT_SPACING_SCALE = [...] на: import { DEFAULT_SPACING_SCALE } from
'that-module'; убедитесь, что все использования в компоненте продолжают
ссылаться на DEFAULT_SPACING_SCALE и выполните сборку/тесты чтобы проверить
отсутствие регрессий.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: edf36142-f746-435a-aa22-3a6777f1d40b
⛔ Files ignored due to path filters (1)
dist/ui.htmlis excluded by!**/dist/**
📒 Files selected for processing (14)
backend/src/prompts/nielsen-heuristics.tsbackend/src/services/analyzer.tsbackend/src/services/claude.tssrc/lint/component-props.tssrc/lint/constraints.tssrc/lint/grid-check.tssrc/lint/multi-theme.tssrc/lint/types.tssrc/lint/typography.tssrc/lint/variable-scope.tsui/src/App.tsxui/src/components/messages/DarkModeCard.tsxui/src/components/shared/ScaleEditor.tsxui/src/components/shared/TeamConfigPanel.tsx
🚧 Files skipped from review as they are similar to previous changes (2)
- backend/src/prompts/nielsen-heuristics.ts
- ui/src/components/shared/ScaleEditor.tsx
| /** Generate three-layer explanations (rule / why / real-world) for the top-N lint issues. Default: 0 (disabled). */ | ||
| threeLayerExplanations?: boolean; |
There was a problem hiding this comment.
Комментарий о default для boolean неконсистентен.
На Line 56 указано Default: 0 (disabled) для threeLayerExplanations?: boolean. Лучше использовать false, чтобы не вводить в заблуждение.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@backend/src/services/analyzer.ts` around lines 56 - 57, The JSDoc for the
boolean option threeLayerExplanations is misleading ("Default: 0 (disabled)");
update the comment to use a boolean default wording (e.g., "Default: false
(disabled)") and, if there's any default value assignment or config
initialization for threeLayerExplanations elsewhere, ensure it uses false rather
than 0 so the type and docs are consistent with the boolean signature.
| // Fire-and-forget extended features — save results to session in background | ||
| if (extendedPromises.length > 0) { | ||
| void (async () => { | ||
| try { | ||
| await Promise.allSettled(extendedPromises); | ||
| // After all extended features resolve, persist to session | ||
| const extendedUpdates: Record<string, unknown> = {}; | ||
| if (threeLayerExplanations) { | ||
| extendedUpdates.three_layer_explanations = threeLayerExplanations; | ||
| } | ||
| if (confidencedFindings) { | ||
| extendedUpdates.confidenced_findings = confidencedFindings; | ||
| } | ||
| // Only update if there's something to save | ||
| if (Object.keys(extendedUpdates).length > 0) { | ||
| console.log(`[extended] Saving extended features for session ${sessionId}`); | ||
| } |
There was a problem hiding this comment.
Результаты extended-фич сейчас теряются.
На Line 337–343 формируется extendedUpdates, но далее на Line 345–347 идёт только лог, без фактического сохранения. Плюс в return (Line 357–367) не возвращаются threeLayerExplanations/confidencedFindings. В итоге expensive-вычисления выполняются, но результат недоступен.
Also applies to: 357-367
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@backend/src/services/analyzer.ts` around lines 331 - 347, The extended
feature results are built into extendedUpdates inside the fire-and-forget async
IIFE (extendedPromises, threeLayerExplanations, confidencedFindings, sessionId)
but never persisted or returned; fix by actually saving extendedUpdates to the
session/store after the log (use the existing session persistence method used
elsewhere in analyzer.ts) and ensure the outer function’s return (the block that
currently returns other results at the end) includes threeLayerExplanations and
confidencedFindings so callers receive those computed values.
| return { | ||
| type: typeof parsed.type === 'string' ? parsed.type.toLowerCase() : 'other', | ||
| confidence: typeof parsed.confidence === 'number' ? parsed.confidence : 0.5, | ||
| signals: Array.isArray(parsed.signals) ? parsed.signals : [], | ||
| }; |
There was a problem hiding this comment.
Нужно валидировать поля из JSON так же строго, как в legacy fallback.
Сейчас на Line 67–69 значения из parsed принимаются почти без санитаризации: type может быть произвольной строкой, confidence — вне диапазона, signals — с нестроковыми элементами. Это ломает контракт результата и может дать невалидные данные дальше по пайплайну.
🔧 Предлагаемое исправление
+function normalizePageType(value: unknown): string {
+ if (typeof value !== 'string') return 'other';
+ const normalized = value.trim().toLowerCase();
+ return /^[a-z0-9_-]+$/i.test(normalized) ? normalized : 'other';
+}
+
+function normalizeConfidence(value: unknown): number {
+ if (typeof value !== 'number' || !Number.isFinite(value)) return 0.5;
+ return Math.min(1, Math.max(0, value));
+}
+
+function normalizeSignals(value: unknown): string[] {
+ return Array.isArray(value) ? value.filter((v): v is string => typeof v === 'string') : [];
+}
+
// Try to parse as JSON first (new structured format)
try {
const jsonMatch = text.match(/\{[\s\S]*\}/);
if (jsonMatch) {
const parsed = JSON.parse(jsonMatch[0]);
return {
- type: typeof parsed.type === 'string' ? parsed.type.toLowerCase() : 'other',
- confidence: typeof parsed.confidence === 'number' ? parsed.confidence : 0.5,
- signals: Array.isArray(parsed.signals) ? parsed.signals : [],
+ type: normalizePageType(parsed.type),
+ confidence: normalizeConfidence(parsed.confidence),
+ signals: normalizeSignals(parsed.signals),
};
}
} catch {📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| return { | |
| type: typeof parsed.type === 'string' ? parsed.type.toLowerCase() : 'other', | |
| confidence: typeof parsed.confidence === 'number' ? parsed.confidence : 0.5, | |
| signals: Array.isArray(parsed.signals) ? parsed.signals : [], | |
| }; | |
| function normalizePageType(value: unknown): string { | |
| if (typeof value !== 'string') return 'other'; | |
| const normalized = value.trim().toLowerCase(); | |
| return /^[a-z0-9_-]+$/i.test(normalized) ? normalized : 'other'; | |
| } | |
| function normalizeConfidence(value: unknown): number { | |
| if (typeof value !== 'number' || !Number.isFinite(value)) return 0.5; | |
| return Math.min(1, Math.max(0, value)); | |
| } | |
| function normalizeSignals(value: unknown): string[] { | |
| return Array.isArray(value) ? value.filter((v): v is string => typeof v === 'string') : []; | |
| } | |
| // Try to parse as JSON first (new structured format) | |
| try { | |
| const jsonMatch = text.match(/\{[\s\S]*\}/); | |
| if (jsonMatch) { | |
| const parsed = JSON.parse(jsonMatch[0]); | |
| return { | |
| type: normalizePageType(parsed.type), | |
| confidence: normalizeConfidence(parsed.confidence), | |
| signals: normalizeSignals(parsed.signals), | |
| }; | |
| } | |
| } catch { |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@backend/src/services/claude.ts` around lines 66 - 70, Validate and sanitize
the fields from parsed before returning: ensure parsed.type is a string and
matches the allowed set (normalize with .toLowerCase() and switch/whitelist,
otherwise default to 'other'); coerce parsed.confidence to a number and clamp it
to the [0,1] range (fallback to 0.5 if missing/NaN); and ensure parsed.signals
is an array of strings by filtering out non-string elements and trimming entries
(or map values to String and filter empty strings) so the returned object (type,
confidence, signals) always conforms to the legacy fallback contract.
| const result: SceneNode[] = []; | ||
|
|
||
| function walk(node: SceneNode, parentLocked: boolean): void { | ||
| const isLocked = parentLocked || ('locked' in node && (node as any).locked === true); | ||
| const isHidden = 'visible' in node && !node.visible; | ||
|
|
||
| if (skipLocked && isLocked) return; | ||
| if (skipHidden && isHidden) return; | ||
|
|
||
| if (node.type === 'FRAME' || node.type === 'COMPONENT' || node.type === 'COMPONENT_SET') { | ||
| if (isTopLevelFrame(node)) { | ||
| result.push(node); | ||
| } |
There was a problem hiding this comment.
Дедуплицируйте topFrames перед возвратом.
Один и тот же frame может попасть в result дважды: через walk() и через корневой цикл, если в nodes одновременно есть SECTION/GROUP и его дочерний frame. Дальше это удваивает issues и summary для одного nodeId.
🐛 Возможный фикс
function collectTopLevelFrames(
nodes: readonly SceneNode[],
skipLocked: boolean,
skipHidden: boolean
): SceneNode[] {
const result: SceneNode[] = [];
+ const seen = new Set<string>();
+
+ function pushOnce(node: SceneNode): void {
+ if (!seen.has(node.id)) {
+ seen.add(node.id);
+ result.push(node);
+ }
+ }
function walk(node: SceneNode, parentLocked: boolean): void {
@@
if (node.type === 'FRAME' || node.type === 'COMPONENT' || node.type === 'COMPONENT_SET') {
if (isTopLevelFrame(node)) {
- result.push(node);
+ pushOnce(node);
}
}
@@
if (node.type === 'FRAME' || node.type === 'COMPONENT' || node.type === 'COMPONENT_SET') {
- result.push(node);
+ pushOnce(node);
} else if ('children' in node) {Also applies to: 239-259
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/lint/grid-check.ts` around lines 204 - 216, The current traversal can
push the same frame twice (via walk and the root loop), so before returning
ensure top-level frames are deduplicated: collect frames from result and from
the root-level nodes into a single list (referencing result, walk(),
isTopLevelFrame, and the root-level nodes variable), deduplicate by node.id (or
id property) and then return that deduplicated array; apply the same dedup step
to the analogous block at lines ~239-259 to avoid duplicated issues and
summaries.
| // Find the most common column count | ||
| const countFreq = new Map<number, number>(); | ||
| for (const { count } of columnCounts) { | ||
| countFreq.set(count, (countFreq.get(count) ?? 0) + 1); | ||
| } | ||
|
|
||
| let mostCommonCount = uniqueCounts[0]; | ||
| let maxFreq = 0; | ||
| for (const [count, freq] of countFreq) { | ||
| if (freq > maxFreq) { | ||
| mostCommonCount = count; | ||
| maxFreq = freq; | ||
| } | ||
| } | ||
|
|
||
| // Flag frames that deviate from the most common count | ||
| for (const entry of columnCounts) { | ||
| if (entry.count !== mostCommonCount) { | ||
| issues.push({ | ||
| id: nextId(), | ||
| type: 'spacing', | ||
| severity: 'warning', | ||
| nodeId: entry.nodeId, | ||
| nodeName: entry.nodeName, | ||
| message: `Frame "${entry.nodeName}" uses ${entry.count}-column grid while most frames use ${mostCommonCount} columns`, | ||
| currentValue: `${entry.count} columns`, | ||
| suggestions: [`Change to ${mostCommonCount} columns for consistency`], | ||
| autoFixable: false, | ||
| }); | ||
| } | ||
| } |
There was a problem hiding this comment.
Не объявляйте «большинство» при ничьей по частотам.
Если два значения колонок встречаются одинаково часто, на Line 299-306 выбирается первое попавшееся значение, а на Line 317 формируется сообщение, что “most frames use …”. Для набора 6/12 это даст ложный warning, хотя единого стандарта тут просто нет.
🐛 Возможный фикс
- let mostCommonCount = uniqueCounts[0];
- let maxFreq = 0;
+ let mostCommonCount = uniqueCounts[0];
+ let maxFreq = 0;
+ let hasTie = false;
for (const [count, freq] of countFreq) {
if (freq > maxFreq) {
mostCommonCount = count;
maxFreq = freq;
+ hasTie = false;
+ } else if (freq === maxFreq) {
+ hasTie = true;
}
}
+
+ if (hasTie) return;📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // Find the most common column count | |
| const countFreq = new Map<number, number>(); | |
| for (const { count } of columnCounts) { | |
| countFreq.set(count, (countFreq.get(count) ?? 0) + 1); | |
| } | |
| let mostCommonCount = uniqueCounts[0]; | |
| let maxFreq = 0; | |
| for (const [count, freq] of countFreq) { | |
| if (freq > maxFreq) { | |
| mostCommonCount = count; | |
| maxFreq = freq; | |
| } | |
| } | |
| // Flag frames that deviate from the most common count | |
| for (const entry of columnCounts) { | |
| if (entry.count !== mostCommonCount) { | |
| issues.push({ | |
| id: nextId(), | |
| type: 'spacing', | |
| severity: 'warning', | |
| nodeId: entry.nodeId, | |
| nodeName: entry.nodeName, | |
| message: `Frame "${entry.nodeName}" uses ${entry.count}-column grid while most frames use ${mostCommonCount} columns`, | |
| currentValue: `${entry.count} columns`, | |
| suggestions: [`Change to ${mostCommonCount} columns for consistency`], | |
| autoFixable: false, | |
| }); | |
| } | |
| } | |
| // Find the most common column count | |
| const countFreq = new Map<number, number>(); | |
| for (const { count } of columnCounts) { | |
| countFreq.set(count, (countFreq.get(count) ?? 0) + 1); | |
| } | |
| let mostCommonCount = uniqueCounts[0]; | |
| let maxFreq = 0; | |
| let hasTie = false; | |
| for (const [count, freq] of countFreq) { | |
| if (freq > maxFreq) { | |
| mostCommonCount = count; | |
| maxFreq = freq; | |
| hasTie = false; | |
| } else if (freq === maxFreq) { | |
| hasTie = true; | |
| } | |
| } | |
| if (hasTie) return; | |
| // Flag frames that deviate from the most common count | |
| for (const entry of columnCounts) { | |
| if (entry.count !== mostCommonCount) { | |
| issues.push({ | |
| id: nextId(), | |
| type: 'spacing', | |
| severity: 'warning', | |
| nodeId: entry.nodeId, | |
| nodeName: entry.nodeName, | |
| message: `Frame "${entry.nodeName}" uses ${entry.count}-column grid while most frames use ${mostCommonCount} columns`, | |
| currentValue: `${entry.count} columns`, | |
| suggestions: [`Change to ${mostCommonCount} columns for consistency`], | |
| autoFixable: false, | |
| }); | |
| } | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/lint/grid-check.ts` around lines 293 - 323, Сейчас блок, который
вычисляет mostCommonCount (переменные countFreq, mostCommonCount, maxFreq) не
обрабатывает ситуацию, когда несколько значений колонок имеют одинаковую
максимальную частоту, и в таком случае выдается ложное предупреждение при
добавлении записи в issues (см. columnCounts и вызов issues.push). Исправьте
это: при обходе countFreq определяйте все значения с freq === maxFreq (например
собрать их в массив), и если их больше одного — не помечать кадры как
использующие «большинство» (т.е. пропустить генерацию предупреждений в цикле по
columnCounts или заменить текст сообщения на нейтральный), иначе продолжать как
сейчас; обновите логику вокруг mostCommonCount/uniqueCounts и места, где
используется nextId/issues.push, чтобы не выдавать warning при ничье.
| let issueCounter = 0; | ||
| function nextId(): string { | ||
| return `mtheme-${++issueCounter}`; | ||
| } |
There was a problem hiding this comment.
Глобальный счётчик ID может давать коллизии при параллельных запусках.
На Line 396 общий issueCounter сбрасывается для каждого вызова checkMultiTheme, из-за чего при одновременных запусках ID в issues становятся нестабильными/дублирующимися.
Предложение исправления
-let issueCounter = 0;
-function nextId(): string {
- return `mtheme-${++issueCounter}`;
-}
+function makeNextId(prefix: string): () => string {
+ let issueCounter = 0;
+ return () => `${prefix}-${++issueCounter}`;
+}
...
export async function checkMultiTheme(
_nodes: readonly SceneNode[],
_options: { skipLocked?: boolean; skipHidden?: boolean } = {}
): Promise<MultiThemeLintResult> {
- issueCounter = 0;
+ const nextId = makeNextId(`mtheme-${Date.now()}`);
const issues: LintIssue[] = [];Also applies to: 396-396
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/lint/multi-theme.ts` around lines 16 - 19, The global issueCounter/nextId
causes ID collisions across parallel runs; remove the module-level issueCounter
and either generate IDs locally per invocation of checkMultiTheme (e.g., a local
counter variable inside checkMultiTheme) or use a collision-safe generator such
as crypto.randomUUID()/nanoid. Replace references to the module-level nextId
with a new local generator (or call to crypto.randomUUID()) and ensure issues
are assigned IDs from that local generator so each checkMultiTheme run produces
unique, non-overlapping IDs.
| if (missingModes.length > 0 && missingModes.length < collection.modes.length) { | ||
| issues.push({ | ||
| id: nextId(), | ||
| type: 'theme', | ||
| severity: 'critical', | ||
| nodeId: variable.id, | ||
| nodeName: variable.name, | ||
| message: `Variable "${variable.name}" is missing values for modes: ${missingModes.join(', ')}`, | ||
| currentValue: `Defined in ${collection.modes.length - missingModes.length}/${collection.modes.length} modes`, | ||
| suggestions: [`Add values for: ${missingModes.join(', ')}`], | ||
| autoFixable: false, | ||
| }); | ||
| } |
There was a problem hiding this comment.
Проверка пропускает кейсы с полностью отсутствующими актуальными mode-values.
Сейчас issue создаётся только при частично отсутствующих значениях. Если все значения для modeId коллекции отсутствуют (или ключи только stale), проверка молчит, хотя это как раз серьёзная несогласованность тем.
Предложение исправления
- if (missingModes.length > 0 && missingModes.length < collection.modes.length) {
+ if (missingModes.length > 0) {
issues.push({
...
- currentValue: `Defined in ${collection.modes.length - missingModes.length}/${collection.modes.length} modes`,
+ currentValue: `Defined in ${collection.modes.length - missingModes.length}/${collection.modes.length} modes`,- const definedModeCount = Object.keys(variable.valuesByMode).filter(
- modeId => variable.valuesByMode[modeId] !== undefined
- ).length;
-
- if (definedModeCount > 0 && definedModeCount < collection.modes.length) {
+ const definedModeCount = Object.keys(variable.valuesByMode).filter(
+ modeId => variable.valuesByMode[modeId] !== undefined
+ ).length;
+ if (definedModeCount > 0) {
const definedKeys = new Set(Object.keys(variable.valuesByMode));
const collectionModeIds = new Set(collection.modes.map(m => m.modeId));Also applies to: 298-318
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/lint/multi-theme.ts` around lines 272 - 284, The current check only
reports when some mode-values are missing but not when all are missing or when
values exist only as stale keys; update the condition around the issues.push
block (where missingModes, collection.modes, variable, nextId and issues.push
are used) to also trigger when missingModes.length === collection.modes.length
or when all present keys are stale, and adjust the
message/currentValue/suggestions to reflect a fully missing case (e.g., "no
values defined for any modes" and "Defined in 0/N modes" or include stale-state
note). Apply the same change to the analogous block handling modes at the other
location (the block around lines 298-318).
| function collectTextNodes( | ||
| node: SceneNode, | ||
| cache: Map<string, TextNode[]>, | ||
| ): TextNode[] { | ||
| const cached = cache.get(node.id); | ||
| if (cached) return cached; | ||
|
|
||
| const texts: TextNode[] = []; | ||
| if (node.type === 'TEXT') { | ||
| texts.push(node as TextNode); | ||
| } | ||
| if ('children' in node) { | ||
| for (const child of (node as any).children as SceneNode[]) { | ||
| texts.push(...collectTextNodes(child, cache)); | ||
| } | ||
| } | ||
|
|
||
| cache.set(node.id, texts); | ||
| return texts; |
There was a problem hiding this comment.
skipHidden/skipLocked сейчас не действуют на агрегатные text-check’и.
collectTextNodes() кеширует всё поддерево без фильтрации, поэтому checkInconsistentAlignment() и checkParagraphSpacing() учитывают скрытые и locked TEXT, хотя traverse() выше их пропускает. Это даёт ложные issues на видимых frame’ах, если внутри есть скрытый caption или locked layer.
🐛 Возможный фикс
function collectTextNodes(
node: SceneNode,
cache: Map<string, TextNode[]>,
+ skipLocked: boolean,
+ skipHidden: boolean,
+ parentLocked = false,
): TextNode[] {
+ const isLocked = parentLocked || ('locked' in node && (node as any).locked === true);
+ const isHidden = 'visible' in node && !node.visible;
+ if ((skipLocked && isLocked) || (skipHidden && isHidden)) return [];
+
const cached = cache.get(node.id);
if (cached) return cached;
@@
if ('children' in node) {
for (const child of (node as any).children as SceneNode[]) {
- texts.push(...collectTextNodes(child, cache));
+ texts.push(...collectTextNodes(child, cache, skipLocked, skipHidden, isLocked));
}
}
@@
- const textNodes = collectTextNodes(parent, cache);
+ const textNodes = collectTextNodes(parent, cache, skipLocked, skipHidden);
@@
- const textNodes = collectTextNodes(parent, cache);
+ const textNodes = collectTextNodes(parent, cache, skipLocked, skipHidden);Also applies to: 102-110, 220-229
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/lint/typography.ts` around lines 79 - 97, collectTextNodes currently
caches and returns all TEXT nodes under a node without respecting
skipHidden/skipLocked, causing checkInconsistentAlignment and
checkParagraphSpacing to report hidden/locked text; change collectTextNodes to
accept the same skip flags (e.g., skipHidden: boolean, skipLocked: boolean) or a
predicate, apply those checks when recursing and before pushing TextNode, and
include the flags in the cache key (or maintain separate cache maps) so cached
results respect the filters; update callers (checkInconsistentAlignment,
checkParagraphSpacing, any other callers) to pass the correct flags or predicate
so aggregate checks only consider visible/unlocked text.
| // Find majority alignment | ||
| let maxCount = 0; | ||
| let majorityAlign = 'LEFT'; | ||
| for (const [align, nodes] of alignments) { | ||
| if (nodes.length > maxCount) { | ||
| maxCount = nodes.length; | ||
| majorityAlign = align; | ||
| } | ||
| } | ||
|
|
||
| let checked = 0; | ||
| for (const [align, nodes] of alignments) { | ||
| if (align !== majorityAlign) { | ||
| for (const t of nodes) { | ||
| checked++; | ||
| pushIssue( | ||
| issues, | ||
| 'info', | ||
| t.id, | ||
| t.name, | ||
| `Text alignment "${align}" differs from majority body text alignment "${majorityAlign}" in "${parent.name}"`, | ||
| align, | ||
| [majorityAlign], | ||
| ); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| return checked; |
There was a problem hiding this comment.
При равенстве alignments здесь нет majority.
Если внутри frame один body text LEFT, а другой CENTER, на Line 129-148 код всё равно выберет первый встретившийся align и зафлагит второй как отклонение от majority. Это ложное срабатывание — нужен строгий победитель.
🐛 Возможный фикс
let maxCount = 0;
let majorityAlign = 'LEFT';
+ let hasTie = false;
for (const [align, nodes] of alignments) {
if (nodes.length > maxCount) {
maxCount = nodes.length;
majorityAlign = align;
+ hasTie = false;
+ } else if (nodes.length === maxCount) {
+ hasTie = true;
}
}
+
+ if (hasTie) return 0;📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // Find majority alignment | |
| let maxCount = 0; | |
| let majorityAlign = 'LEFT'; | |
| for (const [align, nodes] of alignments) { | |
| if (nodes.length > maxCount) { | |
| maxCount = nodes.length; | |
| majorityAlign = align; | |
| } | |
| } | |
| let checked = 0; | |
| for (const [align, nodes] of alignments) { | |
| if (align !== majorityAlign) { | |
| for (const t of nodes) { | |
| checked++; | |
| pushIssue( | |
| issues, | |
| 'info', | |
| t.id, | |
| t.name, | |
| `Text alignment "${align}" differs from majority body text alignment "${majorityAlign}" in "${parent.name}"`, | |
| align, | |
| [majorityAlign], | |
| ); | |
| } | |
| } | |
| } | |
| return checked; | |
| // Find majority alignment | |
| let maxCount = 0; | |
| let majorityAlign = 'LEFT'; | |
| let hasTie = false; | |
| for (const [align, nodes] of alignments) { | |
| if (nodes.length > maxCount) { | |
| maxCount = nodes.length; | |
| majorityAlign = align; | |
| hasTie = false; | |
| } else if (nodes.length === maxCount) { | |
| hasTie = true; | |
| } | |
| } | |
| if (hasTie) return 0; | |
| let checked = 0; | |
| for (const [align, nodes] of alignments) { | |
| if (align !== majorityAlign) { | |
| for (const t of nodes) { | |
| checked++; | |
| pushIssue( | |
| issues, | |
| 'info', | |
| t.id, | |
| t.name, | |
| `Text alignment "${align}" differs from majority body text alignment "${majorityAlign}" in "${parent.name}"`, | |
| align, | |
| [majorityAlign], | |
| ); | |
| } | |
| } | |
| } | |
| return checked; |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/lint/typography.ts` around lines 128 - 156, The code picks the first
encountered align as majority even on ties; change the logic around
alignments/majorityAlign so a strict majority is required: compute maxCount,
then check how many groups have nodes.length === maxCount and if that count > 1
(tie) do not set a majority (or return 0) and skip flagging; otherwise keep the
current majorityAlign and proceed to call pushIssue for non-majority items.
Update the block that calculates majorityAlign (using alignments and maxCount)
and the subsequent loop that uses majorityAlign so ties produce no issues.
| {displayIssues.map((issue, i) => { | ||
| const sev = SEVERITY_STYLES[issue.severity] || SEVERITY_STYLES.info; | ||
| return ( | ||
| <div key={i} className="space-y-0.5"> |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
rg -n -C3 --type=ts 'interface\\s+DarkModeIssue|id\\??:\\s*string' ui/src/lib/messages.ts ui/src/components/messages/DarkModeCard.tsx
rg -n -C2 --type=tsx 'displayIssues\\.map|key=\\{i\\}' ui/src/components/messages/DarkModeCard.tsxRepository: lemone112/figmalint
Length of output: 203
🏁 Script executed:
#!/bin/bash
# Find DarkModeIssue interface definition
echo "=== Searching for DarkModeIssue interface ==="
rg -n "interface\s+DarkModeIssue" --type ts,tsx
# Find the file and check its contents
echo -e "\n=== Checking DarkModeCard.tsx around the problematic lines ==="
rg -n -A 15 "displayIssues.map" ui/src/components/messages/DarkModeCard.tsx
# Check lib/messages.ts for interface definition
echo -e "\n=== Checking messages.ts for DarkModeIssue definition ==="
fd "messages.ts" ui/src/ --exec cat -n {}Repository: lemone112/figmalint
Length of output: 21399
🏁 Script executed:
#!/bin/bash
# Check DarkModeCard.tsx for displayIssues definition and interface usage
echo "=== Checking DarkModeCard.tsx for displayIssues and type definitions ==="
head -80 ui/src/components/messages/DarkModeCard.tsx
echo -e "\n=== Searching for interface definitions in DarkModeCard.tsx ==="
rg -n "interface|type" ui/src/components/messages/DarkModeCard.tsx | head -30Repository: lemone112/figmalint
Length of output: 2893
🏁 Script executed:
#!/bin/bash
# Search for where DarkModeCard is used and how data is passed
echo "=== Searching for DarkModeCard usage ==="
rg -n "DarkModeCard" ui/src --type tsx,ts -B 3 -A 3
# Also check if there's any type casting or interface extending
echo -e "\n=== Full DarkModeCard.tsx to understand data flow ==="
wc -l ui/src/components/messages/DarkModeCard.tsxRepository: lemone112/figmalint
Length of output: 254
🏁 Script executed:
#!/bin/bash
# Read the full DarkModeCard.tsx file
echo "=== Full DarkModeCard.tsx content ==="
cat -n ui/src/components/messages/DarkModeCard.tsx
# Search for 'dark-mode' kind usage (where component is rendered)
echo -e "\n=== Searching for dark-mode message kind usage ==="
rg -n "dark-mode" ui/src --type-add "tsx:*.tsx" --type-add "ts:*.ts" -B 2 -A 2Repository: lemone112/figmalint
Length of output: 7569
Используйте стабильный идентификатор вместо индекса массива в качестве key.
key={i} приводит к некорректному переиспользованию DOM-узлов при изменении порядка или фильтрации списка. Добавьте id: string; в интерфейс DarkModeIssue и используйте key={issue.id}:
Предлагаемый фикс
interface DarkModeIssue {
+ id: string;
type: string;
severity: string;
nodeName: string;
message: string;
currentValue?: string;
suggestions?: string[];
}- {displayIssues.map((issue, i) => {
+ {displayIssues.map((issue) => {
const sev = SEVERITY_STYLES[issue.severity] || SEVERITY_STYLES.info;
return (
- <div key={i} className="space-y-0.5">
+ <div key={issue.id} className="space-y-0.5">🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@ui/src/components/messages/DarkModeCard.tsx` around lines 120 - 123, The list
is using array index as React keys in displayIssues.map which can cause
incorrect DOM reuse; update the DarkModeIssue interface to include a unique id:
string, ensure all producers of DarkModeIssue supply that id, and change the map
to use key={issue.id} instead of key={i} (look for DarkModeIssue definition and
the displayIssues.map rendering in DarkModeCard.tsx to make the edits).
- Wire 8 extended lint modules (layout-sizing, constraints, typography, component-props, style-audit, variable-scope, multi-theme, grid-check) into message-handler with run-extended-lint command - Add PluginEvent types for dark-mode-card-result, extended-lint-result, extended-lint-error - Wire App.tsx handlers for all new plugin events (design-debt, dark-mode, token-compliance, variable-system, extended-lint, team-config) - Add QuickActions buttons for Design Debt, Dark Mode, Token Audit in expandable "More..." section - Fix handleCompareModes to auto-detect first multi-mode collection when no collectionId provided - Fix handleCheckDTCGCompliance to work without dtcgJson (self-compliance) - Separate raw mode-comparison-result (text summary) from dark-mode-card-result (DarkModeCard-compatible transform) - Add backend API functions for extended analysis features - All 3 projects typecheck clean, build successfully, Snyk SAST clean Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Summary
Adds 18 advanced design analysis capabilities across the full stack:
Deterministic Lint Modules (plugin sandbox)
detachedInfoAPI + heuristic fallback for FRAME nodes with component naminglayoutWrap, breakpoint variant checkingfigma.on('documentchange')with debounced re-lint of affected nodesDesign System Compliance (plugin sandbox)
.tokens.jsonformat with$typeinheritanceAI Analysis Pipelines (backend — 11 new routes)
UI
PageSweepCard— file health overview, frame grid (3-column), top issues, collapsible AI insightsVerification
npx tsc --noEmit— plugin, backend, UI all passnpm run bundle— plugin builds (240KB)cd ui && npm run build— UI builds (282KB gzipped)Test plan
/api/cognitive-walkthrough,/api/pure-scoring, etc.) with valid payloads.tokens.jsonfile🤖 Generated with Claude Code
Summary by CodeRabbit