Skip to content

Sprint 0+1: Security, UX, Architecture & Infrastructure - #3

Merged
lemone112 merged 11 commits into
mainfrom
feat/sprint-0-1-security-ux-devops
Mar 13, 2026
Merged

Sprint 0+1: Security, UX, Architecture & Infrastructure#3
lemone112 merged 11 commits into
mainfrom
feat/sprint-0-1-security-ux-devops

Conversation

@lemone112

@lemone112 lemone112 commented Mar 13, 2026

Copy link
Copy Markdown
Owner

Summary

Consolidates findings from the 16-agent audit and 3-agent pipeline evaluation into two implementation sprints:

Sprint 0 — P0 Coherence Fixes (6 items)

  • Wire 3 hidden AI review categories (visualBalance, microcopyQuality, cognitiveLoad) into UI
  • Wire 2 hidden lint categories (visualQuality, microcopy) into score computation and issue display
  • Connect orphaned token analysis data to backend AI context
  • Include radius errors in fix-all batch operation
  • Wire real hasAutoLayout/childCount into backend metadata
  • Expand score model from 5→7 categories with proper weights

Sprint 1 — P1 UX Improvements (6 items)

  • Selection change detection with stale results banner
  • 4-phase analysis progress indicator (lint → screenshot → AI review → Refero)
  • Persistent ignored state via document storage
  • Enhanced onboarding empty state with 3-step guide
  • AI-generated badge on AI review card
  • Score-update message for rescan results

Sprint 2 — Security, Backend, UX Polish, Infrastructure (5 agent tracks)

Security hardening:

  • Security headers (HSTS, X-Frame-Options, nosniff) in Caddyfile
  • 25MB body size limit to prevent OOM
  • Error message redaction (no internals leaked to client)
  • API key redaction in plugin console logs
  • CORS fix for disallowed origins

UX & Accessibility:

  • Auto-rescan after batch fixes with score delta
  • Fix stale closure bug in handleRescan
  • Refero polling cleanup on unmount
  • aria-hidden on all decorative SVGs
  • aria-label on settings controls
  • Explicit PluginEvent types (remove catch-all)

Backend Architecture:

  • Shared Anthropic client singleton (eliminate 3 duplicate clients)
  • Shared MODEL constant
  • appendConversation race condition fix (immediate transaction)
  • SQLite busy_timeout for concurrent access
  • Remove duplicate conversation history from system prompt

Infrastructure:

  • Pin Docker images (node:22.15-alpine3.21, caddy:2.9-alpine)
  • Docker HEALTHCHECK on /api/health
  • .dockerignore for backend
  • .env.example with documentation
  • .gitignore for data/, *.db, .env
  • Delete dead ui-enhanced.html (568KB total)

Verification

  • Plugin bundle builds (npm run bundle) — 374KB
  • UI typecheck (tsc --noEmit) — 0 errors
  • UI build (npm run build) — 253KB
  • Backend typecheck (tsc --noEmit) — 0 errors
  • Snyk SAST scan — 0 issues

Test plan

  • Load plugin in Figma, select a component, run analysis — verify 7-category score card
  • Verify AI review shows visualBalance, microcopyQuality, cognitiveLoad when present
  • Run fix-all — verify auto-rescan triggers and score updates
  • Change selection after analysis — verify stale banner appears
  • Check aria attributes with screen reader or browser DevTools
  • Deploy backend with Docker — verify healthcheck passes
  • Verify security headers with curl -I

🤖 Generated with Claude Code

Summary by CodeRabbit

Заметки о выпуске

  • Новые функции

    • Индикатор фаз анализа и баннер «устаревшие результаты»
    • Событие selection-changed и отслеживание выбора
    • Новые оценки: Visual Quality, Microcopy, Conversion, Cognitive
    • Передача сводки по дизайнерским токенам (tokenSummary)
  • Улучшения

    • Массовые исправления расширены радиусами
    • Восстановление/сохранение игнорированных элементов и доступность UI
    • Единые пользовательские сообщения об ошибках и улучшенная устойчивость потоков
  • Документация

    • Добавлены AUDIT-REPORT, TEST_PLAN и PIPELINE-EVALUATION
  • Хаускипинг

    • Обновлены .gitignore/.dockerignore, CI и контейнерная конфигурация

lemone112 and others added 2 commits March 13, 2026 13:49
…on awareness, progressive loading

P0 (coherence fixes):
- Wire all 7 AI review categories to UI (was 4 — 40% of Claude tokens wasted)
- Add labels for visualQuality + microcopy lint categories in IssuesList
- Expand Design Health Score to 7 weighted categories (25/18/10/25/7/8/7)
- Forward token analysis summary to backend for AI context
- Fix hardcoded hasAutoLayout/childCount — now reads real values from Figma node
- Add selection change detection with stale results banner + Re-analyze button

P1 (UX improvements):
- Expose radius fixes in Fix All + walkthrough (was spacing-only)
- Progressive 4-phase loading indicator (rules → screenshot → AI → Refero)
- Persistent ignore state via figma.root.setPluginData (survives restarts)
- 3-step onboarding flow replacing blank empty state
- "AI-generated" badge on AiReviewCard to distinguish facts from opinions

Audit reports:
- AUDIT-REPORT.md: 16-agent consolidated audit (~200 findings)
- PIPELINE-EVALUATION.md: 3-agent pipeline coherence/completeness evaluation
- backend/TEST_PLAN.md: 107 test cases across 7 endpoints

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Security:
- Add security headers (HSTS, X-Frame-Options, nosniff) to Caddyfile
- Add 25MB body size limit to prevent OOM from large screenshots
- Redact error internals from API responses (no stack traces to client)
- Redact API key from plugin console logs
- Fix CORS to return empty string for disallowed origins
- Validate ANTHROPIC_API_KEY at startup with warning

UX & Accessibility:
- Auto-rescan after batch fixes to show updated score
- Fix stale closure in handleRescan (read prev.score inside setState)
- Add cleanup for Refero polling interval on unmount
- Add aria-hidden to all decorative SVG icons
- Add aria-label to settings close button and API key input
- Replace PluginEvent catch-all with explicit screenshot-error type
- Add text-10 fontSize to Tailwind config

Backend Architecture:
- Consolidate Anthropic client into shared singleton (getAnthropicClient)
- Export MODEL constant from claude.ts, remove duplicates
- Fix appendConversation race condition with immediate transaction
- Add SQLite busy_timeout=5000 for concurrent access
- Remove duplicate conversation history from system prompt
- Add tokenSummary to AnalyzeRequest type

Infrastructure:
- Pin Docker images (node:22.15-alpine3.21, caddy:2.9-alpine)
- Add Docker HEALTHCHECK hitting /api/health
- Add backend/.dockerignore
- Improve backend/.env.example with documentation
- Add data/, *.db, .env to .gitignore
- Delete dead ui-enhanced.html (284KB x2)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Mar 13, 2026

Copy link
Copy Markdown

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Этот PR добавляет документацию и тест-план, обновляет DevOps (Docker/Caddy/.gitignore), централизует Anthropic-клиент, вводит tokenSummary и события selection-changed, усиливает SQLite транзакции и таймауты MCP/Refero, унифицирует обработку ошибок и расширяет логику линта и UI типы/интерфейсы.

Changes

Cohort / File(s) Summary
Игнорирование & README
/.gitignore, README.md, backend/.dockerignore
Обновлён .gitignore (data/*.db, *.env* с исключением .env.example), README указывает на ui/; добавлен backend/.dockerignore.
Документы & планы
AUDIT-REPORT.md, PIPELINE-EVALUATION.md, backend/TEST_PLAN.md
Добавлены крупные отчёты: аудит, оценка конвейера и подробный тест-план (документация).
DevOps / Docker / Caddy
backend/Dockerfile, backend/docker-compose.yml, backend/Caddyfile, backend/.env.example, backend/package.json
Обновлён базовый образ, добавлен HEALTHCHECK, Caddy security headers и тег, .env.example скорректирован; build-скрипт копирует schema.sql.
DB / транзакции / индексы
backend/src/db/queries.ts, backend/src/db/schema.sql
Добавлен busy_timeout pragma; appendConversation переписан на immediate transaction; добавлены индексы для sessions.
Anthropic / Refero централизация
backend/src/services/claude.ts, backend/src/services/flow-analyzer.ts, backend/src/services/refero.ts, backend/src/services/analyzer.ts
Введён экспортируемый getAnthropicClient() и MODEL; сервисы используют централизованный клиент; analyzer принимает опциональный tokenSummary.
MCP / Refero timeouts
backend/src/mcp/*.ts, backend/src/mcp/parse-tool-result.ts
Добавлена утилита withTimeout и применена к MCP/Refero/tool-вызовам (15s) с логированием ошибок и безопасными фолбэками.
Middleware: auth & rate-limit
backend/src/middleware/auth.ts, backend/src/middleware/rate-limit.ts, backend/src/index.ts
Добавлены bearer-аутентификация и in-memory rate limiter; в index.ts добавлены bodyLimit (25MB), стартап-логика проверки API-ключа и периодическая очистка сессий.
Маршруты: унификация ошибок
backend/src/routes/*.ts (analyze.ts,chat.ts,flow.ts,stream.ts)
Унифицирована обработка ошибок (более общие сообщения / 500), удалена детальная утечка ошибок; buildFollowupPrompt сигнатура изменена (удалён history).
MCP клиент сборки / schema включение
backend/package.json, backend/Dockerfile
Build скрипт копирует schema.sql в dist/db; Dockerfile копирует schema и dist, добавлен HEALTHCHECK.
Плагин: выбор и состояние
src/code.ts, src/ui/message-handler.ts
Добавлен selectionchange listener и событие selection-changed; сохранение/восстановление ignored state; screenshot включает tokenSummary, hasAutoLayout и childCount; generateComponentHash учитывает lintSettings.
UI API & типы
ui/src/lib/messages.ts, ui/src/lib/api.ts
Добавлены AnalysisPhase и analysis-phase сообщения; расширен ScoreBreakdown и AiReviewData; analyzeComponent и streamChat сигнатуры расширены (tokenSummary, AbortSignal).
UI: поведение, доступность, отчёты
ui/src/App.tsx, ui/src/hooks/useChat.ts, ui/src/components/**
Поддержка stale selection и Re-analyze, отмена потоков через AbortController, расширены категории оценок (visualQuality, microcopy, conversion, cognitive), ARIA-улучшения и новые UI элементы/баннеры/отчёты.
Граф потока
src/flow/graph-builder.ts
Учёт BACK/CLOSE-ребёр при детекции dead-ends (исключает фреймы с back-edge из dead-ends).
Мелкие изменения
ui/tailwind.config.js, src/core/*, package.json, .github/workflows/ci.yml
Добавлен fontSize '10'; мелкие импорты/путь изменения; CI workflow добавлен; esbuild minify флаг в package.json.

Sequence Diagram(s)

sequenceDiagram
  participant UI as "Plugin UI (ui/)"
  participant Plugin as "Figma Plugin\n(code.ts / message-handler)"
  participant Backend as "Backend API"
  participant Anthropic as "Anthropic (getAnthropicClient)"
  participant DB as "SQLite DB"

  UI->>Plugin: Пользователь → Analyze / меняет selection
  Plugin->>UI: отправляет `selection-changed`
  Plugin->>Backend: POST /analyze (screenshot + lint + tokenSummary + metadata)
  Backend->>Anthropic: вызывает ML (getAnthropicClient / MODEL)
  Anthropic-->>Backend: возвращает AI-ответ (ai-review, scoreBreakdown)
  Backend->>DB: appendConversation (immediate transaction)
  DB-->>Backend: подтверждение записи
  Backend-->>Plugin: ответ анализа (ai-review, scoreBreakdown)
  Plugin->>UI: обновить UI (issues, score, пометить stale)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐰 Я — кролик, что баги ловит в сети,
Кэш и транзакции чиню на лету.
Клиент собран, токены посчитаны в счёте,
Выбор сменился — UI скажет: «вперёд!»,
Пусть линт и отчёт растут, как свежий салат.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 48.98% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed Заголовок PR чётко отражает основные направления изменений: безопасность, UX, архитектура и инфраструктура, что соответствует консолидированным результатам спринтов 0-1.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/sprint-0-1-security-ux-devops
📝 Coding Plan
  • Generate coding plan for human review comments

Comment @coderabbitai help to get the list of available commands and usage tips.

@qodo-code-review

Copy link
Copy Markdown
ⓘ You are approaching your monthly quota for Qodo. Upgrade your plan

Review Summary by Qodo

Sprint 0+1: Security hardening, UX improvements, architecture consolidation, and infrastructure automation

✨ Enhancement 🐞 Bug fix 📝 Documentation

Grey Divider

Walkthroughs

Description
  **Core Coherence Fixes (Sprint 0)**
• Expanded score model from 5 to 7 categories by wiring visualQuality and microcopy lint
  categories into UI display and score computation with adjusted weights (25/18/10/25/7/8/7)
• Wired 3 hidden AI review categories (visualBalance, microcopyQuality, cognitiveLoad) into
  AiReviewCard with conditional rendering
• Connected orphaned token analysis data to backend AI context via tokenSummary field in requests
• Included radius errors in batch fix operations and fixable count filtering
• Wired real hasAutoLayout and childCount metadata into backend analysis requests
  **UX Improvements (Sprint 1)**
• Added selection change detection with stale results banner and re-analyze button when selection
  changes to different node
• Implemented 4-phase analysis progress indicator (lint → screenshot → ai-review → refero) with
  AnalysisPhaseIndicator component
• Persistent ignored state via figma.root.setPluginData() for document-level storage
• Enhanced onboarding empty state with 3-step numbered guide (Select, Analyze, Fix)
• Added "AI-generated" badge on AI Design Review card
• Auto-rescan after batch fixes with 500ms delay to display updated score
  **Security Hardening**
• Added security headers to Caddyfile (HSTS, X-Frame-Options, X-Content-Type-Options,
  Referrer-Policy, XSS-Protection)
• Implemented 25MB request body size limit to prevent OOM attacks
• Redacted error messages across all API routes to prevent internal detail leakage
• Redacted API keys in plugin console logs
• Fixed CORS origin handling to return empty string instead of 'null' for disallowed origins
  **Backend Architecture**
• Consolidated 3 duplicate Anthropic client instances into shared getAnthropicClient() singleton
  from claude.ts
• Consolidated duplicate MODEL constant exports for shared use across services
• Fixed appendConversation race condition with atomic transaction and immediate locking
• Added SQLite busy_timeout = 5000 pragma for concurrent access handling
• Removed duplicate conversation history from system prompt (now passed via messages array only)
  **Infrastructure & DevOps**
• Pinned Docker images: node:22.15-alpine3.21 and caddy:2.9-alpine for reproducible builds
• Added Docker HEALTHCHECK on /api/health endpoint with 30s interval, 5s timeout, 3 retries
• Created .dockerignore to optimize build context
• Created .env.example with documented environment variables
• Updated .gitignore for data/, *.db, .env files
• Deleted legacy ui-enhanced.html (568KB) in favor of modular source structure
  **Accessibility Improvements**
• Added aria-hidden="true" to all decorative SVG icons (settings gear, checkmark, send button)
• Added aria-label attributes to settings controls (close button, API key input)
• Replaced catch-all PluginEvent type with explicit event types (selection-changed,
  screenshot-error)
  **Documentation**
• Added comprehensive 753-line backend API test plan with 107 test cases, infrastructure
  recommendations, and 5 example tests
• Added consolidated 573-line security and quality audit from 16 agents covering security,
  architecture, frontend, accessibility, UX, UI design, and DevOps
• Added 390-line pipeline evaluation report analyzing 3-layer architecture, coherence gaps,
  competitive landscape, and strategic roadmap with value estimation ($76k annual value)
• Updated README to reference modular ui/ source structure instead of legacy artifact
  **Bug Fixes**
• Fixed stale closure bug in handleRescan by moving state access inside setState callback
• Fixed Refero polling cleanup on component unmount to prevent memory leaks
• Added startup validation warning for missing ANTHROPIC_API_KEY
Diagram
flowchart LR
  A["Score Model<br/>5→7 Categories"] -->|"visualQuality<br/>microcopy"| B["ScoreCard<br/>Display"]
  C["AI Review<br/>Hidden Categories"] -->|"visualBalance<br/>microcopyQuality<br/>cognitiveLoad"| D["AiReviewCard<br/>Render"]
  E["Token Analysis<br/>Orphaned Data"] -->|"tokenSummary"| F["Backend AI<br/>Context"]
  G["Selection<br/>Change Event"] -->|"stale detection"| H["Stale Banner<br/>+ Re-analyze"]
  I["Batch Fixes<br/>spacing + radius"] -->|"auto-rescan"| J["Score Update<br/>Display"]
  K["3 Anthropic<br/>Clients"] -->|"consolidate"| L["Shared<br/>getAnthropicClient"]
  M["Error Messages<br/>Internal Details"] -->|"redact"| N["Generic<br/>Error Response"]
  O["Security Headers<br/>+ Body Limits"] -->|"harden"| P["Caddyfile<br/>+ Middleware"]
  Q["Docker Images<br/>Generic Versions"] -->|"pin"| R["Reproducible<br/>Builds"]
Loading

Grey Divider

File Changes

1. ui/src/hooks/useChat.ts Bug fix, enhancement +57/-41

Expand scoring categories and fix stale closure in rescan

• Expanded score breakdown from 5 to 7 categories by adding visualQuality and microcopy with
 adjusted weights
• Fixed stale closure bug in handleRescan by moving state access inside setState callback
• Removed dependency on state.score in handleRescan to prevent capturing stale references
• Filtered out analysis-phase messages when AI review completes to replace progress indicators
 with actual results

ui/src/hooks/useChat.ts


2. src/ui/message-handler.ts Security, enhancement +51/-3

Security hardening and persistent ignore state

• Added API key redaction in console logs to prevent plaintext credential exposure
• Implemented persistent ignored state via figma.root.setPluginData() for document-level storage
• Added token analysis extraction and metadata (hasAutoLayout, childCount) to screenshot export
• Restored ignored state on plugin initialization from document storage

src/ui/message-handler.ts


3. ui/src/lib/messages.ts ✨ Enhancement +13/-4

Wire hidden AI categories and explicit event types

• Added visualQuality and microcopy to LintErrorType union
• Added AnalysisPhase type for 4-phase progress tracking (lint, screenshot, ai-review, refero)
• Extended AiReviewData with optional visualBalance, microcopyQuality, cognitiveLoad
 categories
• Added visualQuality and microcopy to ScoreBreakdown interface
• Replaced catch-all PluginEvent type with explicit selection-changed and screenshot-error
 event types

ui/src/lib/messages.ts


View more (37)
4. backend/src/services/analyzer.ts ✨ Enhancement +10/-11

Use shared Anthropic client and wire token analysis

• Imported shared getAnthropicClient from claude.ts instead of creating local instance
• Added tokenSummary field to AnalyzeRequest interface for token analysis data
• Included token summary in component info context sent to Claude for AI review

backend/src/services/analyzer.ts


5. backend/src/db/queries.ts 🐞 Bug fix +23/-12

Fix race condition in conversation append with transactions

• Added busy_timeout = 5000 pragma for SQLite concurrent access handling
• Refactored appendConversation to use transaction with immediate locking to prevent race
 conditions
• Moved read-modify-write operation inside atomic transaction block

backend/src/db/queries.ts


6. backend/src/index.ts Security, enhancement +13/-2

Add body limits, env validation, and CORS fixes

• Added bodyLimit middleware to cap request size at 25MB to prevent OOM attacks
• Added startup validation warning for missing ANTHROPIC_API_KEY
• Fixed CORS origin handling to return empty string instead of 'null' for disallowed origins
• Improved CORS origin comments for clarity

backend/src/index.ts


7. backend/src/routes/chat.ts Bug fix, security +4/-5

Remove duplicate history and redact error messages

• Removed duplicate conversation history from system prompt (now passed via messages array only)
• Changed error responses to generic message instead of leaking internal error details
• Updated buildFollowupPrompt call to not pass history parameter

backend/src/routes/chat.ts


8. backend/src/services/flow-analyzer.ts ✨ Enhancement +2/-11

Use shared Anthropic client and MODEL constant

• Replaced local Anthropic client singleton with shared getAnthropicClient() from claude.ts
• Imported MODEL constant from claude.ts instead of duplicating it

backend/src/services/flow-analyzer.ts


9. backend/src/routes/stream.ts Bug fix, security +3/-2

Remove duplicate history and improve error handling

• Updated buildFollowupPrompt call to not pass history parameter (removed duplicate)
• Added error logging and generic error message to prevent internal detail leakage

backend/src/routes/stream.ts


10. src/code.ts ✨ Enhancement +13/-0

Add selection change detection for stale results

• Added selectionchange event listener to notify UI when selection changes
• Sends selection state (hasSelection, nodeId, nodeName) to UI for stale result detection

src/code.ts


11. backend/src/services/claude.ts ✨ Enhancement +6/-2

Export shared Anthropic client and MODEL constant

• Exported getAnthropicClient() function as public API for use across services
• Exported MODEL constant for shared use instead of duplication
• Added internal alias for backward compatibility within the file

backend/src/services/claude.ts


12. backend/src/routes/analyze.ts Security +1/-3

Redact error messages to prevent information leakage

• Changed error responses to generic message instead of exposing internal error details

backend/src/routes/analyze.ts


13. backend/src/routes/flow.ts Security +1/-2

Redact error messages to prevent information leakage

• Changed error responses to generic message instead of exposing internal error details

backend/src/routes/flow.ts


14. backend/src/services/refero.ts ✨ Enhancement +1/-2

Use shared MODEL constant

• Imported MODEL constant from claude.ts instead of duplicating it locally

backend/src/services/refero.ts


15. ui/src/lib/api.ts ✨ Enhancement +6/-0

Wire token analysis to backend request

• Added tokenSummary field to request payload for token analysis data

ui/src/lib/api.ts


16. ui/tailwind.config.js ✨ Enhancement +1/-0

Add missing text-10 Tailwind utility class

• Added '10': ['10px', { lineHeight: '14px' }] to fontSize configuration for small text sizing

ui/tailwind.config.js


17. backend/TEST_PLAN.md Tests, documentation +753/-0

Add comprehensive backend API test plan and examples

• Comprehensive 753-line test plan covering all 7 backend endpoints with 107 test cases
• Includes test infrastructure recommendations (Vitest + Hono test client), mock strategy, and
 priority matrix
• Provides example test code for 5 critical tests (analyze happy path, input validation, SQL
 injection, CORS, SSE errors)
• Documents identified risks including missing body size limits, lack of authentication, and JSON
 parsing without error handling

backend/TEST_PLAN.md


18. AUDIT-REPORT.md Documentation, security +573/-0

Add consolidated 16-agent security and quality audit

• 573-line consolidated audit from 16 agents covering security (12 findings), architecture (15),
 frontend (34), accessibility (28), UX (23), UI design (12), and DevOps (19)
• Top-10 critical issues including missing backend auth, API key logging, XSS in legacy HTML,
 accessibility failures
• Detailed action plan across 4 sprints (quick wins, security, architecture, UX/accessibility)
• Positive findings section highlighting parameterized SQL, non-root Docker user, CORS restrictions,
 graceful degradation

AUDIT-REPORT.md


19. ui/src/App.tsx ✨ Enhancement +97/-26

Add selection change detection and auto-rescan after fixes

• Added selection change tracking with selectionStale state and analyzedNodeId ref to detect
 stale results
• Implemented stale selection banner with re-analyze button when selection changes to different node
• Added auto-rescan after batch fixes with 500ms delay to show updated score
• Extended batch fix handler to include radius fixes alongside spacing fixes
• Added cleanup for Refero polling interval on component unmount
• Wired hasAutoLayout, childCount, and tokenSummary from screenshot to backend analysis
 request
• Added aria-hidden="true" to settings gear SVG icon

ui/src/App.tsx


20. ui/src/components/chat/MessageList.tsx ✨ Enhancement +67/-7

Add 4-phase progress indicator and onboarding guide

• Enhanced empty state with 3-step onboarding guide (Select, Analyze, Fix) with numbered badges
• Added AnalysisPhaseIndicator component showing 4-phase progress (lint, screenshot, ai-review,
 refero)
• Added aria-hidden="true" to decorative SVG icons
• Integrated phase indicator rendering in message list to replace progress messages

ui/src/components/chat/MessageList.tsx


21. ui/src/components/chat/ChatContainer.tsx ✨ Enhancement +1/-1

Include radius errors in fixable count

• Updated hasFixable filter to include both spacing and radius error types in quick actions

ui/src/components/chat/ChatContainer.tsx


22. PIPELINE-EVALUATION.md 📝 Documentation +390/-0

Comprehensive pipeline audit and strategic roadmap documentation

• Comprehensive 390-line audit report analyzing FigmaLint's 3-layer architecture (deterministic lint
 + AI review + Refero benchmarking) and identifying 6 coherence gaps where built capabilities are
 hidden or disconnected
• Documents 5 major information loss points: dual score computation with divergent weights, 3 AI
 categories computed but not rendered, 4 lint categories missing UI labels, token analysis orphaned
 from pipeline, consistency engine disconnected
• Competitive landscape analysis showing FigmaLint's unique positioning with no direct competitor
 combining all three layers, plus feature gap matrix and blue ocean opportunities
• UX research perspective on designer workflows, information architecture recommendations, and
 feature value by persona (junior/senior designers, leads, system maintainers)
• Unified priority matrix with P0-P4 roadmap: P0 coherence fixes (15-25h), P1 UX improvements
 (30-40h), P2 team features (40-60h), P3 next month (60-80h), P4 strategic (3-6 months)
• Value map estimating 18.3 hours/week time savings for 10-person design team (~$76k annual value)
 after implementing P0-P2 improvements

PIPELINE-EVALUATION.md


23. ui/src/components/chat/StickyHeader.tsx Accessibility +1/-1

Add aria-hidden to decorative settings icon

• Added aria-hidden="true" attribute to decorative settings gear icon SVG to improve accessibility
 by hiding it from screen readers

ui/src/components/chat/StickyHeader.tsx


24. ui/src/components/shared/SettingsPanel.tsx Accessibility +3/-1

Enhance settings panel with accessibility labels

• Added aria-label="Close settings" to close button for improved screen reader accessibility
• Added aria-hidden="true" to decorative close icon SVG
• Added aria-label="API key" to API key input field for better form accessibility

ui/src/components/shared/SettingsPanel.tsx


25. ui/src/components/messages/AiReviewCard.tsx ✨ Enhancement +13/-1

Wire hidden AI review categories and add AI badge

• Added "AI-generated" badge next to "AI Design Review" title to clearly indicate AI-sourced content
• Wired 3 previously hidden AI review categories: visualBalance, microcopyQuality, and
 cognitiveLoad with conditional rendering
• Each new category renders as a CategoryRow component when data is present

ui/src/components/messages/AiReviewCard.tsx


26. ui/src/components/messages/ScoreCard.tsx ✨ Enhancement +6/-4

Expand score model from 5 to 7 categories

• Updated category weights from 5-category model (30/20/10/30/10) to 7-category model
 (25/18/10/25/7/8/7) for better distribution
• Added two new lint categories to score display: visualQuality (8% weight) and microcopy (7%
 weight)
• Adjusted existing category weights: Tokens 30%→25%, Spacing 20%→18%, Accessibility 30%→25%, Naming
 10%→7%

ui/src/components/messages/ScoreCard.tsx


27. ui/src/components/messages/IssuesList.tsx ✨ Enhancement +4/-0

Add labels for missing lint categories

• Added visualQuality and microcopy entries to TYPE_SEVERITY_MAP with warning and info
 severity levels respectively
• Added visualQuality and microcopy entries to TYPE_LABELS with human-readable labels "Visual
 Quality" and "Microcopy"
• Enables proper labeling and severity styling for previously unlabeled lint categories in the
 issues list

ui/src/components/messages/IssuesList.tsx


28. ui/src/components/messages/FixResult.tsx Accessibility +1/-1

Add aria-hidden to decorative checkmark icon

• Added aria-hidden="true" attribute to decorative checkmark SVG icon to improve accessibility

ui/src/components/messages/FixResult.tsx


29. ui/src/components/chat/InputBar.tsx Accessibility +1/-1

Add aria-hidden to decorative send icon

• Added aria-hidden="true" attribute to decorative send button SVG icon for improved screen reader
 accessibility

ui/src/components/chat/InputBar.tsx


30. backend/docker-compose.yml Dependencies +1/-1

Pin Caddy Docker image version

• Pinned Caddy image from generic caddy:2 to specific version caddy:2.9-alpine for reproducible
 deployments

backend/docker-compose.yml


31. backend/Dockerfile ⚙️ Configuration changes +5/-1

Pin Node image and add health check

• Pinned Node.js base image from node:22-alpine to specific version node:22.15-alpine3.21 for
 reproducible builds
• Added Docker HEALTHCHECK instruction with 30-second interval, 5-second timeout, 3 retries
 checking /api/health endpoint

backend/Dockerfile


32. backend/.env.example 📝 Documentation +7/-3

Document environment variables with comments

• Added descriptive comments for each environment variable explaining purpose and defaults
• Updated ANTHROPIC_API_KEY placeholder from generic sk-ant-... to sk-ant-your-key-here for
 clarity
• Commented out DATABASE_PATH and DESIGN_SYSTEMS_MCP_URL to show they are optional with defaults
• Improved documentation for PORT variable

backend/.env.example


33. backend/Caddyfile Security +9/-0

Add security headers to reverse proxy

• Added security headers block with 8 headers: X-Content-Type-Options, X-Frame-Options,
 Strict-Transport-Security, Referrer-Policy, X-XSS-Protection, and removed Server header
• Implements HSTS with 1-year max-age and subdomains, clickjacking protection (DENY), MIME-type
 sniffing prevention, and XSS protection

backend/Caddyfile


34. backend/.dockerignore ⚙️ Configuration changes +7/-0

Add Docker build context exclusions

• New file created to optimize Docker build context by excluding unnecessary files and directories
• Excludes: node_modules, .git, markdown files, environment files, data/ directory, and
 compiled dist/ directory

backend/.dockerignore


35. README.md 📝 Documentation +1/-1

Update README directory structure reference

• Updated directory structure documentation to reference ui/ source directory instead of legacy
 ui-enhanced.html single-file artifact
• Reflects architectural change from monolithic HTML file to modular source file structure

README.md


36. backend/src/prompts/chat-followup.ts Additional files +0/-9

...

backend/src/prompts/chat-followup.ts


37. dist/code.js Additional files +2684/-1303

...

dist/code.js


38. dist/ui-enhanced.html Additional files +0/-7291

...

dist/ui-enhanced.html


39. dist/ui.html Additional files +35/-35

...

dist/ui.html


40. ui-enhanced.html Additional files +0/-7300

...

ui-enhanced.html


Grey Divider

Qodo Logo

@qodo-code-review

qodo-code-review Bot commented Mar 13, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (2) 📘 Rule violations (0) 📎 Requirement gaps (0)

Grey Divider


Action required

1. Double rescan after batch fix🐞 Bug ✓ Correctness
Description
App schedules a rescan-lint after receiving batch-fix-v2-result, but the plugin already auto-rescans
immediately after executing the batch. This produces two successive design-lint-result events,
duplicating score-update/messages and wasting work.
Code

ui/src/App.tsx[R143-149]

        case 'batch-fix-v2-result':
          chat.handleBatchFixResult(event.data as any);
+            // Auto-rescan after batch fixes to show updated score
+            setTimeout(() => {
+              chat.addMessage({ kind: 'ai-text', content: 'Re-scanning to verify fixes...' });
+              post('rescan-lint');
+            }, 500);
Evidence
The UI explicitly posts a rescan after batch-fix-v2-result, while the plugin batch fix handler
already calls handleRescanLint(), which emits design-lint-result/rescan-complete; the UI treats any
subsequent design-lint-result as a rescan when chat.lintResult is present, so the second rescan will
append another score-update/message set.

ui/src/App.tsx[143-149]
src/ui/message-handler.ts[1338-1354]
src/ui/message-handler.ts[1359-1366]
ui/src/App.tsx[107-116]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Batch fix currently causes *two* lint rescans: one initiated by the plugin after executing the batch fix, and another initiated by the UI after receiving `batch-fix-v2-result`. This duplicates `design-lint-result` events, adds duplicate score-update/messages, and wastes time.
### Issue Context
The plugin already calls `handleRescanLint()` inside `handleBatchFixV2()` after `executeBatchFix`, and emits `design-lint-result` + `rescan-complete`. The UI additionally schedules `post(&amp;amp;#x27;rescan-lint&amp;amp;#x27;)` on `batch-fix-v2-result`.
### Fix Focus Areas
- ui/src/App.tsx[143-149]
- src/ui/message-handler.ts[1338-1366]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Stuck progress indicator🐞 Bug ⛯ Reliability
Description
tryBackendAnalysis adds an analysis-phase message before calling analyzeComponent, but if
analyzeComponent throws, the error path never removes/finishes that phase indicator. This leaves the
progress UI permanently visible after an AI analysis failure.
Code

ui/src/App.tsx[R29-35]

) => {
  if (!backendAvailable) return;
-    chat.addMessage({
-      kind: 'ai-text',
-      content: 'Running AI visual analysis...',
-    });
+    chat.addMessage({ kind: 'analysis-phase', phase: 'ai-review' });
  try {
    const result = await analyzeComponent({
Evidence
The progress indicator is added unconditionally, and only the success path (handleAiReview) filters
out analysis-phase messages. The failure path just appends an ai-text message, so the analysis-phase
message remains and is rendered as the AnalysisPhaseIndicator component.

ui/src/App.tsx[26-69]
ui/src/hooks/useChat.ts[342-349]
ui/src/components/chat/MessageList.tsx[93-99]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
When AI analysis fails, the chat retains the `analysis-phase` progress message, leaving the AnalysisPhaseIndicator visible indefinitely.
### Issue Context
`tryBackendAnalysis()` adds `{ kind: &amp;amp;#x27;analysis-phase&amp;amp;#x27;, phase: &amp;amp;#x27;ai-review&amp;amp;#x27; }` before calling `analyzeComponent()`. On success, `handleAiReview()` removes phase messages; on failure, no cleanup occurs.
### Fix Focus Areas
- ui/src/App.tsx[26-69]
- ui/src/hooks/useChat.ts[342-349]
- ui/src/components/chat/MessageList.tsx[93-99]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

ⓘ The new review experience is currently in Beta. Learn more

Grey Divider

Qodo Logo

Comment thread ui/src/App.tsx Outdated
Comment thread ui/src/App.tsx

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 11

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
ui/src/components/chat/StickyHeader.tsx (1)

45-50: ⚠️ Potential issue | 🟡 Minor

Добавьте явный aria-label на кнопку с иконкой.

Сейчас у кнопки есть title, но для стабильной доступности лучше задать доступное имя через aria-label.

💡 Предлагаемое изменение
       <button
         onClick={onOpenSettings}
         className="shrink-0 w-7 h-7 flex items-center justify-center text-fg-tertiary hover:text-fg rounded-md hover:bg-bg-hover transition-colors"
         title="Settings"
+        aria-label="Open settings"
       >
         <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden="true">
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/src/components/chat/StickyHeader.tsx` around lines 45 - 50, В компоненте
StickyHeader добавьте явный доступный ярлык для кнопки настроек: в JSX кнопки,
где используется onClick={onOpenSettings} и есть title="Settings", добавьте
атрибут aria-label="Settings" (или локализованную строку) чтобы обеспечить
доступное имя для вспомогательных технологий; сохраните существующий title и
остальные атрибуты и убедитесь, что aria-label отражает ту же семантику, что и
title.
🧹 Nitpick comments (3)
backend/src/db/queries.ts (1)

75-99: Транзакционный подход корректен, но есть незначительная неэффективность.

Исправление race condition через immediate-транзакцию — правильное решение. Однако newMessage сериализуется в JSON на строке 77, а затем парсится обратно на строке 94, что создаёт лишний цикл сериализации.

♻️ Предлагаемый рефакторинг для устранения двойной сериализации
 export function appendConversation(id: string, role: string, content: string): void {
   const db = getDb();
-  const newMessage = JSON.stringify({ role, content, timestamp: Date.now() });
+  const newMessage = { role, content, timestamp: Date.now() };

   // Use a transaction with immediate locking to prevent lost updates
   // under concurrent requests. The read + modify + write is atomic.
   const append = db.transaction(() => {
     const session = db.prepare('SELECT conversation FROM sessions WHERE id = ?').get(id) as
       | { conversation: string }
       | undefined;
     if (!session) return;

     let conversation: Array<{ role: string; content: string; timestamp: number }> = [];
     try {
       const parsed = JSON.parse(session.conversation || '[]');
       conversation = Array.isArray(parsed) ? parsed : [];
     } catch {
       conversation = [];
     }
-    conversation.push(JSON.parse(newMessage));
+    conversation.push(newMessage);
     db.prepare('UPDATE sessions SET conversation = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?')
       .run(JSON.stringify(conversation), id);
   });

   append.immediate();
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/src/db/queries.ts` around lines 75 - 99, The code currently creates
newMessage as a JSON string then immediately JSON.parses it inside
appendConversation, causing unnecessary double serialization; change newMessage
to be an object (e.g., const newMessage = { role, content, timestamp: Date.now()
}) and push that object into conversation instead of JSON.parse(newMessage),
keeping the final JSON.stringify(conversation) only once when running the
UPDATE; update references in the append transaction (db.prepare SELECT/UPDATE,
conversation parsing) accordingly so appendConversation performs a single
serialization on write.
backend/src/services/analyzer.ts (1)

157-166: Рассмотрите логирование ошибок в фоновой задаче Refero.

В режиме quick ошибка Refero полностью подавляется пустым .catch(). Для диагностики рекомендуется хотя бы логировать ошибку.

📝 Предлагаемое улучшение
     runReferoComparison(pageType, componentInfo, req.screenshot, getAnthropicClient())
       .then(result => {
         if (result) {
           saveReferoResult(sessionId, result);
         }
       })
-      .catch(() => { /* Refero failure is non-critical */ });
+      .catch((err) => {
+        console.warn('Background Refero comparison failed:', err);
+      });
🤖 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 157 - 166, The background
Refero call swallows all errors in an empty .catch(), so update the promise
chain around runReferoComparison(pageType, componentInfo, req.screenshot,
getAnthropicClient()) to log failures instead of ignoring them; in the
.catch(...) call record the error (including context like sessionId and
pageType) using the project's logger (or console.error if no logger exists) and
still avoid throwing so quick-mode remains non-blocking, and keep the existing
saveReferoResult(sessionId, result) behavior in the .then branch.
ui/src/lib/messages.ts (1)

204-204: Добавьте tokenSummary в общий тип screenshot-result.

src/ui/message-handler.ts уже кладет tokenSummary в payload, а ui/src/App.tsx поэтому читает его через as any. Пока поле не описано в PluginEvent, контракт между main thread и UI остается незащищенным и легко разъедется при следующем рефакторинге.

Предлагаемое изменение
-  | { type: 'screenshot-result'; data: { nodeId: string; nodeName: string; screenshot: string; width: number; height: number; hasAutoLayout?: boolean; childCount?: number } }
+  | { type: 'screenshot-result'; data: { nodeId: string; nodeName: string; screenshot: string; width: number; height: number; hasAutoLayout?: boolean; childCount?: number; tokenSummary?: { totalTokens: number; boundToVariables: number; boundToStyles: number; hardCoded: number } } }

Лучше вынести этот payload в отдельный интерфейс и переиспользовать его в ui/src/App.tsx, чтобы убрать as any.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/src/lib/messages.ts` at line 204, Update the PluginEvent union to include
tokenSummary on the 'screenshot-result' payload: add tokenSummary?: <appropriate
type> to the screenshot-result variant (or better, define a new interface like
ScreenshotResultPayload with nodeId, nodeName, screenshot, width, height,
hasAutoLayout?, childCount?, tokenSummary?) and replace the inline payload union
member with that interface so ui/src/App.tsx can import/reuse
ScreenshotResultPayload instead of casting to any; ensure the same type name is
used where App.tsx reads the payload to remove the as any cast.
🤖 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/.dockerignore`:
- Line 7: В .dockerignore есть запись "dist/" которая исключает артефакты, а
Dockerfile выполняет "COPY dist/ ..." — из-за этого COPY падает; исправьте либо
удалив/закомментировав строку "dist/" в .dockerignore, либо явно разрешив её
копирование добавлением исключения (например, восстанавливая dist в контекст
через исключение), либо изменив Dockerfile чтобы копировать нужные файлы из
текущего контекста после сборки; найдите запись "dist/" в файле .dockerignore и
соответствующий "COPY dist/ ..." в Dockerfile и приведите их в согласие.

In `@backend/src/index.ts`:
- Around line 42-43: The CORS behavior differs between tests and production
because the CORS origin fallback returns an empty string; change the logic in
the CORS handler (the function containing "return allowed.includes(origin) ?
origin : ''") to return the literal string 'null' for disallowed origins so it
matches TEST_PLAN.md and test X-05 expectations; update the same branch used by
buildApp()/your app initialization so both test and prod CORS configurations
produce "Access-Control-Allow-Origin: null" for invalid origins.

In `@backend/src/routes/flow.ts`:
- Around line 24-26: Replace the raw error logging in the catch block that
currently calls console.error('Flow analysis error:', error) so you don't emit
full error objects to prod logs; either pass the error through your redaction
helper (e.g., console.error('Flow analysis error:', redactError(error))) or log
a generic message (e.g., console.error('Flow analysis error: <redacted>')) while
keeping the existing generic response returned via c.json({ error: 'Flow
analysis failed. Please try again.' }, 500).

In `@backend/src/services/flow-analyzer.ts`:
- Around line 102-104: Move the API key existence check before creating the
Anthropic client: instead of calling getAnthropicClient() first, verify
process.env.ANTHROPIC_API_KEY and return null if missing, then call
getAnthropicClient(); update the code around the client initialization in
flow-analyzer.ts (the getAnthropicClient() call and the
process.env.ANTHROPIC_API_KEY check) so the fail-fast check runs first.

In `@backend/TEST_PLAN.md`:
- Around line 18-21: The table-of-contents link fragments for entries like "POST
/api/stream/:sessionId", "GET /api/session/:id", "GET /api/session/:id/refero",
and "POST /api/analyze-flow" are likely not matching the generated markdown
header IDs; update the anchors in TEST_PLAN.md so they exactly match the
automatic slug rules (lowercase, spaces → hyphens, remove/encode punctuation and
colons) or replace them with explicit HTML anchors immediately above the
corresponding headings; verify each link points to the exact header text (or add
explicit <a id="..."> anchors) and adjust the TOC entries to use those IDs.
- Line 745: The "No request body size limit" entry in TEST_PLAN.md is outdated
because the server now registers bodyLimit({ maxSize: 25 * 1024 * 1024 }) in the
app initialization; update TEST_PLAN.md to mark that risk as mitigated (or
remove/annotate the entry) and reference the actual mitigation (the bodyLimit({
maxSize: 25 * 1024 * 1024 }) middleware added to the server startup code) so the
test plan accurately reflects the current codebase.

In `@ui/src/App.tsx`:
- Around line 179-186: The selection-changed handler only sets selectionStale to
true when the new selection differs from analyzedNodeId.current, but never
clears it when the selection returns to the analyzed node; update the case
'selection-changed' logic (where selData is derived and setCurrentNodeName is
called) to also setSelectionStale(false) when selData.nodeId equals
analyzedNodeId.current (and chat.lintResult exists), so the stale banner is
cleared once the selection matches the previously analyzed node.
- Around line 143-149: The case 'batch-fix-v2-result' handler currently calls
chat.handleBatchFixResult and then schedules a setTimeout that adds an
"Re-scanning..." message and calls post('rescan-lint'), causing a duplicate
rescan because src/ui/message-handler.ts already triggers a lint after
batch-fix; remove the setTimeout block (or at minimum remove the
post('rescan-lint') call inside it) so only chat.handleBatchFixResult is relied
on to trigger the rescan and avoid duplicate score-update/issues-list and extra
selection passes.
- Around line 194-202: The refero polling interval is only cleared on unmount
(useEffect cleanup) which allows an old interval to continue when a new analysis
is started; modify the function that starts analysis (e.g., the handler that
triggers Refero/analysis start — locate names like startAnalysis, handleAnalyze,
onAnalyze or startReferoPolling) to proactively stop any existing interval by
checking referoPollingRef.current, calling
clearInterval(referoPollingRef.current) and setting referoPollingRef.current =
null before initiating a new run; also reset any refero-related transient state
(e.g., refero-gallery/suggestions buffers) so results from the old sessionId
cannot leak into the new chat.

In `@ui/src/components/chat/MessageList.tsx`:
- Around line 144-156: The SVG used as a decorative status icon in the
AnalysisPhaseIndicator output (the <svg> with the checkmark/polyline) is missing
aria-hidden="true"; update AnalysisPhaseIndicator to add aria-hidden="true" to
that SVG (and any other purely decorative SVGs in the same component) so screen
readers ignore them, matching the pattern used elsewhere in MessageList (lines
with similar decorative icons).

In `@ui/src/components/messages/ScoreCard.tsx`:
- Around line 61-67: The UI weights in ScoreCard do not match the weights used
to compute designHealthScore in analyzer.ts; update the CategoryBar props in
ScoreCard so their weight values match the backend: set Spacing weight="15%",
Visual Quality weight="10%", Microcopy weight="10%", and Naming weight="5%"
(leave Tokens at 25%, Accessibility 25%, Layout 10% unchanged) so the displayed
weights align with designHealthScore.

---

Outside diff comments:
In `@ui/src/components/chat/StickyHeader.tsx`:
- Around line 45-50: В компоненте StickyHeader добавьте явный доступный ярлык
для кнопки настроек: в JSX кнопки, где используется onClick={onOpenSettings} и
есть title="Settings", добавьте атрибут aria-label="Settings" (или
локализованную строку) чтобы обеспечить доступное имя для вспомогательных
технологий; сохраните существующий title и остальные атрибуты и убедитесь, что
aria-label отражает ту же семантику, что и title.

---

Nitpick comments:
In `@backend/src/db/queries.ts`:
- Around line 75-99: The code currently creates newMessage as a JSON string then
immediately JSON.parses it inside appendConversation, causing unnecessary double
serialization; change newMessage to be an object (e.g., const newMessage = {
role, content, timestamp: Date.now() }) and push that object into conversation
instead of JSON.parse(newMessage), keeping the final
JSON.stringify(conversation) only once when running the UPDATE; update
references in the append transaction (db.prepare SELECT/UPDATE, conversation
parsing) accordingly so appendConversation performs a single serialization on
write.

In `@backend/src/services/analyzer.ts`:
- Around line 157-166: The background Refero call swallows all errors in an
empty .catch(), so update the promise chain around runReferoComparison(pageType,
componentInfo, req.screenshot, getAnthropicClient()) to log failures instead of
ignoring them; in the .catch(...) call record the error (including context like
sessionId and pageType) using the project's logger (or console.error if no
logger exists) and still avoid throwing so quick-mode remains non-blocking, and
keep the existing saveReferoResult(sessionId, result) behavior in the .then
branch.

In `@ui/src/lib/messages.ts`:
- Line 204: Update the PluginEvent union to include tokenSummary on the
'screenshot-result' payload: add tokenSummary?: <appropriate type> to the
screenshot-result variant (or better, define a new interface like
ScreenshotResultPayload with nodeId, nodeName, screenshot, width, height,
hasAutoLayout?, childCount?, tokenSummary?) and replace the inline payload union
member with that interface so ui/src/App.tsx can import/reuse
ScreenshotResultPayload instead of casting to any; ensure the same type name is
used where App.tsx reads the payload to remove the as any cast.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b5135ba6-a808-49e2-ad3e-46effc2dfe6a

📥 Commits

Reviewing files that changed from the base of the PR and between 811dec3 and ad771d9.

⛔ Files ignored due to path filters (3)
  • dist/code.js is excluded by !**/dist/**
  • dist/ui-enhanced.html is excluded by !**/dist/**
  • dist/ui.html is excluded by !**/dist/**
📒 Files selected for processing (38)
  • .gitignore
  • AUDIT-REPORT.md
  • PIPELINE-EVALUATION.md
  • README.md
  • backend/.dockerignore
  • backend/.env.example
  • backend/Caddyfile
  • backend/Dockerfile
  • backend/TEST_PLAN.md
  • backend/docker-compose.yml
  • backend/src/db/queries.ts
  • backend/src/index.ts
  • backend/src/prompts/chat-followup.ts
  • backend/src/routes/analyze.ts
  • backend/src/routes/chat.ts
  • backend/src/routes/flow.ts
  • backend/src/routes/stream.ts
  • backend/src/services/analyzer.ts
  • backend/src/services/claude.ts
  • backend/src/services/flow-analyzer.ts
  • backend/src/services/refero.ts
  • src/code.ts
  • src/ui/message-handler.ts
  • ui-enhanced.html
  • ui/src/App.tsx
  • ui/src/components/chat/ChatContainer.tsx
  • ui/src/components/chat/InputBar.tsx
  • ui/src/components/chat/MessageList.tsx
  • ui/src/components/chat/StickyHeader.tsx
  • ui/src/components/messages/AiReviewCard.tsx
  • ui/src/components/messages/FixResult.tsx
  • ui/src/components/messages/IssuesList.tsx
  • ui/src/components/messages/ScoreCard.tsx
  • ui/src/components/shared/SettingsPanel.tsx
  • ui/src/hooks/useChat.ts
  • ui/src/lib/api.ts
  • ui/src/lib/messages.ts
  • ui/tailwind.config.js
💤 Files with no reviewable changes (1)
  • backend/src/prompts/chat-followup.ts

Comment thread backend/.dockerignore Outdated
Comment thread backend/src/index.ts
Comment on lines +42 to +43
// Return the origin if allowed; empty string tells Hono to omit the header
return allowed.includes(origin) ? origin : '';

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Несоответствие CORS-логики между тестами и продакшен-кодом.

В TEST_PLAN.md тестовый buildApp() возвращает 'null' для недопустимых origin (строки 458-459), а продакшен-код возвращает пустую строку. Тест X-05 ожидает Access-Control-Allow-Origin: null, но продакшен опустит заголовок целиком.

Рекомендуется синхронизировать CORS-конфигурацию в тестах с продакшен-кодом.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/src/index.ts` around lines 42 - 43, The CORS behavior differs between
tests and production because the CORS origin fallback returns an empty string;
change the logic in the CORS handler (the function containing "return
allowed.includes(origin) ? origin : ''") to return the literal string 'null' for
disallowed origins so it matches TEST_PLAN.md and test X-05 expectations; update
the same branch used by buildApp()/your app initialization so both test and prod
CORS configurations produce "Access-Control-Allow-Origin: null" for invalid
origins.

Comment thread backend/src/routes/flow.ts
Comment thread backend/src/services/flow-analyzer.ts Outdated
Comment thread backend/TEST_PLAN.md
Comment on lines +18 to +21
- [POST /api/stream/:sessionId](#34-post-apistreamssessionid-sse)
- [GET /api/session/:id](#35-get-apisessionid)
- [GET /api/session/:id/refero](#36-get-apisessionidrefero)
- [POST /api/analyze-flow](#37-post-apianalyze-flow)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Статический анализ: возможно неверные фрагменты ссылок.

markdownlint предупреждает о потенциально недействительных link fragments в оглавлении (строки 18-21). Рекомендуется проверить, что якоря соответствуют автоматически сгенерированным ID заголовков.

🧰 Tools
🪛 markdownlint-cli2 (0.21.0)

[warning] 18-18: Link fragments should be valid

(MD051, link-fragments)


[warning] 19-19: Link fragments should be valid

(MD051, link-fragments)


[warning] 20-20: Link fragments should be valid

(MD051, link-fragments)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/TEST_PLAN.md` around lines 18 - 21, The table-of-contents link
fragments for entries like "POST /api/stream/:sessionId", "GET
/api/session/:id", "GET /api/session/:id/refero", and "POST /api/analyze-flow"
are likely not matching the generated markdown header IDs; update the anchors in
TEST_PLAN.md so they exactly match the automatic slug rules (lowercase, spaces →
hyphens, remove/encode punctuation and colons) or replace them with explicit
HTML anchors immediately above the corresponding headings; verify each link
points to the exact header text (or add explicit <a id="..."> anchors) and
adjust the TOC entries to use those IDs.

Comment thread ui/src/App.tsx Outdated
Comment thread ui/src/App.tsx
Comment thread ui/src/App.tsx
Comment thread ui/src/components/chat/MessageList.tsx
Comment on lines +61 to +67
<CategoryBar label="Tokens" weight="25%" score={data.tokens.score} failed={data.tokens.failed} />
<CategoryBar label="Spacing" weight="18%" score={data.spacing.score} failed={data.spacing.failed} />
<CategoryBar label="Layout" weight="10%" score={data.layout.score} failed={data.layout.failed} />
<CategoryBar label="Accessibility" weight="30%" score={data.accessibility.score} failed={data.accessibility.failed} />
<CategoryBar label="Naming" weight="10%" score={data.naming.score} failed={data.naming.failed} />
<CategoryBar label="Accessibility" weight="25%" score={data.accessibility.score} failed={data.accessibility.failed} />
<CategoryBar label="Naming" weight="7%" score={data.naming.score} failed={data.naming.failed} />
<CategoryBar label="Visual Quality" weight="8%" score={data.visualQuality.score} failed={data.visualQuality.failed} />
<CategoryBar label="Microcopy" weight="7%" score={data.microcopy.score} failed={data.microcopy.failed} />

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Несоответствие весов категорий между UI и backend.

Веса, отображаемые в UI, не соответствуют весам, используемым при расчёте designHealthScore в backend/src/services/analyzer.ts (строки 169, 196-202):

Категория UI (ScoreCard) Backend (analyzer.ts)
Spacing 18% 15%
Visual Quality 8% 10%
Microcopy 7% 10%
Naming 7% 5%

Это приводит к тому, что пользователь видит одни веса, а score рассчитывается по другим.

🔧 Предлагаемое исправление
-        <CategoryBar label="Tokens" weight="25%" score={data.tokens.score} failed={data.tokens.failed} />
-        <CategoryBar label="Spacing" weight="18%" score={data.spacing.score} failed={data.spacing.failed} />
+        <CategoryBar label="Tokens" weight="25%" score={data.tokens.score} failed={data.tokens.failed} />
+        <CategoryBar label="Spacing" weight="15%" score={data.spacing.score} failed={data.spacing.failed} />
         <CategoryBar label="Layout" weight="10%" score={data.layout.score} failed={data.layout.failed} />
-        <CategoryBar label="Accessibility" weight="25%" score={data.accessibility.score} failed={data.accessibility.failed} />
-        <CategoryBar label="Naming" weight="7%" score={data.naming.score} failed={data.naming.failed} />
-        <CategoryBar label="Visual Quality" weight="8%" score={data.visualQuality.score} failed={data.visualQuality.failed} />
-        <CategoryBar label="Microcopy" weight="7%" score={data.microcopy.score} failed={data.microcopy.failed} />
+        <CategoryBar label="Accessibility" weight="25%" score={data.accessibility.score} failed={data.accessibility.failed} />
+        <CategoryBar label="Naming" weight="5%" score={data.naming.score} failed={data.naming.failed} />
+        <CategoryBar label="Visual Quality" weight="10%" score={data.visualQuality.score} failed={data.visualQuality.failed} />
+        <CategoryBar label="Microcopy" weight="10%" score={data.microcopy.score} failed={data.microcopy.failed} />
📝 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.

Suggested change
<CategoryBar label="Tokens" weight="25%" score={data.tokens.score} failed={data.tokens.failed} />
<CategoryBar label="Spacing" weight="18%" score={data.spacing.score} failed={data.spacing.failed} />
<CategoryBar label="Layout" weight="10%" score={data.layout.score} failed={data.layout.failed} />
<CategoryBar label="Accessibility" weight="30%" score={data.accessibility.score} failed={data.accessibility.failed} />
<CategoryBar label="Naming" weight="10%" score={data.naming.score} failed={data.naming.failed} />
<CategoryBar label="Accessibility" weight="25%" score={data.accessibility.score} failed={data.accessibility.failed} />
<CategoryBar label="Naming" weight="7%" score={data.naming.score} failed={data.naming.failed} />
<CategoryBar label="Visual Quality" weight="8%" score={data.visualQuality.score} failed={data.visualQuality.failed} />
<CategoryBar label="Microcopy" weight="7%" score={data.microcopy.score} failed={data.microcopy.failed} />
<CategoryBar label="Tokens" weight="25%" score={data.tokens.score} failed={data.tokens.failed} />
<CategoryBar label="Spacing" weight="15%" score={data.spacing.score} failed={data.spacing.failed} />
<CategoryBar label="Layout" weight="10%" score={data.layout.score} failed={data.layout.failed} />
<CategoryBar label="Accessibility" weight="25%" score={data.accessibility.score} failed={data.accessibility.failed} />
<CategoryBar label="Naming" weight="5%" score={data.naming.score} failed={data.naming.failed} />
<CategoryBar label="Visual Quality" weight="10%" score={data.visualQuality.score} failed={data.visualQuality.failed} />
<CategoryBar label="Microcopy" weight="10%" score={data.microcopy.score} failed={data.microcopy.failed} />
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/src/components/messages/ScoreCard.tsx` around lines 61 - 67, The UI
weights in ScoreCard do not match the weights used to compute designHealthScore
in analyzer.ts; update the CategoryBar props in ScoreCard so their weight values
match the backend: set Spacing weight="15%", Visual Quality weight="10%",
Microcopy weight="10%", and Naming weight="5%" (leave Tokens at 25%,
Accessibility 25%, Layout 10% unchanged) so the displayed weights align with
designHealthScore.

Critical:
- Add missing `history` to fallback chat payload (prevents crash)

Bugs:
- Exclude BACK-only frames from dead-end detection in flow graph
- Return success:false when analyzeFlow() returns null (no API key)

Major:
- Store lint scope snapshot for consistent re-scans after ignore/jump
- Reset to DEFAULT_LINT_SETTINGS before applying team config
- Propagate real nodeType from plugin instead of hardcoded 'FRAME'

Minor:
- Add issue-detail renderer to MessageList switch
- Copy schema.sql to dist/ in backend build script

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

♻️ Duplicate comments (3)
ui/src/App.tsx (3)

179-186: ⚠️ Potential issue | 🟡 Minor

selectionStale не сбрасывается при возврате к проанализированному узлу.

Флаг selectionStale только устанавливается в true, но не сбрасывается, когда пользователь возвращается к analyzedNodeId.current. Баннер будет висеть, даже если результаты снова актуальны.

🐛 Предлагаемое исправление
          case 'selection-changed': {
            const selData = event.data as { hasSelection: boolean; nodeId: string | null; nodeName: string | null };
            setCurrentNodeName(selData.nodeName);
-            // Mark results as stale if we have results and selection changed to a different node
-            if (chat.lintResult && analyzedNodeId.current && selData.nodeId !== analyzedNodeId.current) {
-              setSelectionStale(true);
-            }
+            setSelectionStale(
+              Boolean(chat.lintResult && analyzedNodeId.current && selData.nodeId !== analyzedNodeId.current)
+            );
            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 179 - 186, The selection-stale flag is only set
to true in the 'selection-changed' handler and never cleared; update the
'selection-changed' case (look for the switch handling event.type
'selection-changed' in App.tsx) to explicitly clear selectionStale when the new
selection matches analyzedNodeId.current — e.g., check selData.nodeId ===
analyzedNodeId.current and call setSelectionStale(false) (use the existing
setSelectionStale setter) while keeping the existing logic that sets it true
when chat.lintResult exists and the node differs.

194-202: ⚠️ Potential issue | 🟠 Major

Polling Refero не останавливается при запуске нового анализа.

Очистка интервала происходит только при unmount. Если пользователь запустит новый анализ, пока предыдущий polling ещё активен, старые результаты refero-gallery могут попасть в новый чат.

🐛 Предлагаемое исправление

Извлеките логику очистки в отдельную функцию и вызывайте её в handleAnalyze:

+  const clearReferoPolling = useCallback(() => {
+    if (referoPollingRef.current) {
+      clearInterval(referoPollingRef.current);
+      referoPollingRef.current = null;
+    }
+  }, []);
+
   // Clean up Refero polling interval on unmount
   useEffect(() => {
-    return () => {
-      if (referoPollingRef.current) {
-        clearInterval(referoPollingRef.current);
-        referoPollingRef.current = null;
-      }
-    };
-  }, []);
+    return clearReferoPolling;
+  }, [clearReferoPolling]);

   // ...

   const handleAnalyze = useCallback(() => {
+    clearReferoPolling();
     chat.startAnalysis();
     walkthroughIndex.current = 0;
     setSelectionStale(false);
     post('run-design-lint');
-  }, [chat, post]);
+  }, [chat, post, clearReferoPolling]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/src/App.tsx` around lines 194 - 202, The refero polling interval is only
cleared on unmount via the useEffect cleanup, so starting a new analysis can
leave the old interval running; extract the cleanup into a new helper (e.g.,
clearReferoPolling) that calls clearInterval(referoPollingRef.current) and sets
referoPollingRef.current = null, then replace the inline cleanup in the
useEffect with a call to clearReferoPolling and also call clearReferoPolling at
the start of handleAnalyze to ensure any previous polling is stopped before
starting a new one; reference useEffect, referoPollingRef, clearInterval, and
handleAnalyze when making the change.

145-149: ⚠️ Potential issue | 🟡 Minor

Двойной rescan после batch-fix.

setTimeout с post('rescan-lint') вызывает повторное сканирование, если src/ui/message-handler.ts уже отправляет lint после batch-fix-v2-result. Это приведёт к дублирующимся сообщениям score-update/issues-list.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/src/App.tsx` around lines 145 - 149, The auto-rescan setTimeout block with
post('rescan-lint') in App.tsx is causing duplicate rescans because
src/ui/message-handler.ts already triggers a lint rescan on the
batch-fix-v2-result event; remove the unconditional setTimeout call (or replace
it with a guard/debounce) so only one rescan is triggered: either delete the
setTimeout/post('rescan-lint') block in App.tsx or add a boolean flag or short
debounce keyed to batch-fix actions (check symbols setTimeout and
post('rescan-lint') in App.tsx and batch-fix-v2-result handling in
message-handler.ts) to ensure a single rescan/score-update is emitted.
🧹 Nitpick comments (5)
backend/package.json (1)

8-8: Рассмотрите выделение inline-скрипта в отдельный файл для улучшения читаемости.

Inline Node.js код в JSON-строке сложно читать и поддерживать. Можно вынести логику копирования в отдельный скрипт.

♻️ Предложенный рефакторинг

Создайте файл scripts/copy-schema.mjs:

import { mkdirSync, copyFileSync } from 'fs';
import { join } from 'path';

mkdirSync(join('dist', 'db'), { recursive: true });
copyFileSync(join('src', 'db', 'schema.sql'), join('dist', 'db', 'schema.sql'));

Затем обновите package.json:

-    "build": "tsc && node -e \"const fs=require('fs');const p=require('path');fs.mkdirSync(p.join('dist','db'),{recursive:true});fs.copyFileSync(p.join('src','db','schema.sql'),p.join('dist','db','schema.sql'))\"",
+    "build": "tsc && node scripts/copy-schema.mjs",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/package.json` at line 8, The inline Node.js command in the
package.json "build" script is hard to read and maintain; extract that logic
into a separate script (e.g., create scripts/copy-schema.mjs containing the
mkdir/copy logic) and then update the "build" script to invoke that new module
(node scripts/copy-schema.mjs) so the build entry in package.json is concise and
the file-copy logic is isolated in scripts/copy-schema.mjs.
ui/src/components/chat/MessageList.tsx (1)

137-178: Рассмотрите вынесение AnalysisPhaseIndicator в отдельный файл.

Компонент AnalysisPhaseIndicator вместе с константами PHASE_LABELS и PHASE_ORDER определён после экспорта по умолчанию. Для лучшей организации кода и возможности повторного использования рекомендуется:

  • Переместить компонент и константы в отдельный файл (например, ui/src/components/messages/AnalysisPhaseIndicator.tsx)
  • Или разместить их перед основным компонентом MessageList
🤖 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 137 - 178, The
AnalysisPhaseIndicator component and its related constants PHASE_LABELS and
PHASE_ORDER are defined after the default export in MessageList which harms
organization and reusability; move them into their own module (e.g., create
ui/src/components/messages/AnalysisPhaseIndicator.tsx) exporting
AnalysisPhaseIndicator, PHASE_LABELS, and PHASE_ORDER (or at least the
component) and then import and use AnalysisPhaseIndicator from MessageList, or
alternatively relocate the declarations above the MessageList component so they
are defined before the default export; ensure the prop type AnalysisPhase is
imported/available and update any imports/exports accordingly.
ui/src/App.tsx (2)

460-473: Рассмотрите добавление aria-live для объявления баннера скринридерам.

Баннер об устаревших результатах полезен, но динамически появляющийся контент лучше аннонсировать через aria-live.

♿ Предлагаемое улучшение accessibility
       {/* Stale selection banner */}
       {selectionStale && (
-        <div className="flex items-center gap-2 px-3 py-1.5 bg-bg-warning text-fg-warning text-11 border-b border-border">
+        <div className="flex items-center gap-2 px-3 py-1.5 bg-bg-warning text-fg-warning text-11 border-b border-border" role="alert" aria-live="polite">
           <span className="flex-1">
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/src/App.tsx` around lines 460 - 473, Add an ARIA live region to the
stale-selection banner so screen readers announce it when it appears: update the
container rendered when selectionStale is true (the div that references
selectionStale and currentNodeName and contains the Re-analyze button which
calls handleAnalyze) to include appropriate attributes such as
aria-live="polite" and role="status" (and aria-atomic="true" if you want the
entire message spoken) so the dynamic message about currentNodeName is announced
to assistive technologies.

42-48: Уточните тип параметра screenshot вместо множественных as any.

Параметр screenshot в tryBackendAnalysis (строка 28) не включает поля nodeType, hasAutoLayout, childCount, tokenSummary, но они используются через as any. Это снижает типобезопасность и скрывает потенциальные ошибки.

♻️ Предлагаемое исправление

Обновите тип параметра в строке 28:

   const tryBackendAnalysis = useCallback(async (
     lintResult: LintResult,
-    screenshot: { screenshot: string; nodeId: string; nodeName: string; width: number; height: number }
+    screenshot: { screenshot: string; nodeId: string; nodeName: string; width: number; height: number; nodeType?: string; hasAutoLayout?: boolean; childCount?: number; tokenSummary?: unknown }
   ) => {

Затем уберите приведения as any:

         metadata: {
           nodeId: screenshot.nodeId,
-          nodeType: (screenshot as any).nodeType ?? 'FRAME',
+          nodeType: screenshot.nodeType ?? 'FRAME',
           width: screenshot.width,
           height: screenshot.height,
-          hasAutoLayout: (screenshot as any).hasAutoLayout ?? false,
-          childCount: (screenshot as any).childCount ?? 0,
+          hasAutoLayout: screenshot.hasAutoLayout ?? false,
+          childCount: screenshot.childCount ?? 0,
         },
-        tokenSummary: (screenshot as any).tokenSummary,
+        tokenSummary: screenshot.tokenSummary,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/src/App.tsx` around lines 42 - 48, Change the loose casting to a proper
typed parameter by updating the type of the screenshot argument in
tryBackendAnalysis to include nodeType, width, height, hasAutoLayout, childCount
and tokenSummary (or create an interface like ScreenshotWithMeta and use it for
the function signature), then remove the multiple "(screenshot as any)" casts in
the object construction where nodeType, hasAutoLayout, childCount and
tokenSummary are used so the fields are accessed with real typings; keep the
existing nullish defaults (?? 'FRAME', ?? false, ?? 0) where needed to preserve
behavior.
src/ui/message-handler.ts (1)

1013-1035: Повторяющийся post-action код в ignore-handler'ах лучше вынести в helper.

Сейчас один и тот же шаблон (persistIgnoredState(); handleRunDesignLint();) повторяется в нескольких местах. Лучше централизовать, чтобы не словить расхождения при следующем изменении.

♻️ Вариант упрощения
+function rerunLintAfterIgnoreMutation(mutator: () => void): void {
+  mutator();
+  persistIgnoredState();
+  handleRunDesignLint();
+}
+
 function handleLintIgnoreNode(data: { nodeId: string }): void {
-  ignoreNode(data.nodeId);
-  persistIgnoredState();
-  handleRunDesignLint();
+  rerunLintAfterIgnoreMutation(() => ignoreNode(data.nodeId));
 }
🤖 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 1013 - 1035, Multiple handlers
(handleLintIgnoreNode, handleLintIgnoreError, handleLintIgnoreAllOfType,
handleLintClearIgnored) repeat the same post-action calls
(persistIgnoredState(); handleRunDesignLint()); extract these into a single
helper (e.g., persistIgnoredAndRerun or persistAndRunDesignLint) and call it
from each handler instead of duplicating the two lines; ensure the helper
preserves the exact call order and is used in handleLintIgnoreNode,
handleLintIgnoreError, handleLintIgnoreAllOfType, and handleLintClearIgnored so
behavior remains identical.
🤖 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/routes/flow.ts`:
- Around line 23-25: The handler currently returns a 200 with an internal
message when flow analysis is unavailable; change the if (!result) branch to
return an appropriate error status (prefer 503 Service Unavailable) and replace
the detailed text with a generic message like "Flow analysis service is
currently unavailable". Update the response from c.json(...) to use
c.status(503).json(...) (or c.status(501) if you prefer Not Implemented) and
remove any mention of configuration or API keys to avoid leaking internals;
locate the if (!result) check in the flow route handler in flow.ts to apply this
change.

In `@src/flow/graph-builder.ts`:
- Around line 136-147: The dead-end detection only excludes frames with BACK
edges by checking destinationFrameId === '__BACK__'; update the loop over
validEdges (and the hasBackEdge set) to also treat CLOSE navigation as a
non-dead-end: check edge.navigation === 'CLOSE' (and/or destinationFrameId ===
'__CLOSE__' if that form exists) and add edge.sourceFrameId to the set when
either condition matches; this keeps analyzeFlowGraph's behavior consistent
(which already checks e.navigation === 'BACK' || e.navigation === 'CLOSE') and
prevents CLOSE-linked frames from being classified as dead ends.

In `@src/ui/message-handler.ts`:
- Around line 89-90: The current console.log in message-handler.ts exposes user
payloads via the logData variable (const logData = ...) and
console.log('Received message:', type, logData); — stop logging full payloads
for sensitive event types (e.g., 'chat-message') and only log non-sensitive
metadata (e.g., the event type, timestamps, or a count) or a masked summary;
keep the existing masking for 'save-api-key' but explicitly branch on type ===
'chat-message' (and any other sensitive types) to either omit payload logging or
replace it with a sanitized placeholder before calling console.log or the app
logger.
- Around line 986-1001: handleRescanLint bypasses the preserved lint scope by
calling lintSelection(currentLintSettings) directly, causing different results
after batch-fix; update handleRescanLint to reuse the same scope logic in
handleRunDesignLint (or delegate to it) so lintScopeNodeIds is respected — e.g.,
have handleRescanLint call handleRunDesignLint({ settings: currentLintSettings,
resetScope: false }) or change lintSelection invocation in handleRescanLint to
honor lintScopeNodeIds when present (use the same selection-restoration logic as
handleRunDesignLint) so the fixed scope is applied consistently.

---

Duplicate comments:
In `@ui/src/App.tsx`:
- Around line 179-186: The selection-stale flag is only set to true in the
'selection-changed' handler and never cleared; update the 'selection-changed'
case (look for the switch handling event.type 'selection-changed' in App.tsx) to
explicitly clear selectionStale when the new selection matches
analyzedNodeId.current — e.g., check selData.nodeId === analyzedNodeId.current
and call setSelectionStale(false) (use the existing setSelectionStale setter)
while keeping the existing logic that sets it true when chat.lintResult exists
and the node differs.
- Around line 194-202: The refero polling interval is only cleared on unmount
via the useEffect cleanup, so starting a new analysis can leave the old interval
running; extract the cleanup into a new helper (e.g., clearReferoPolling) that
calls clearInterval(referoPollingRef.current) and sets referoPollingRef.current
= null, then replace the inline cleanup in the useEffect with a call to
clearReferoPolling and also call clearReferoPolling at the start of
handleAnalyze to ensure any previous polling is stopped before starting a new
one; reference useEffect, referoPollingRef, clearInterval, and handleAnalyze
when making the change.
- Around line 145-149: The auto-rescan setTimeout block with post('rescan-lint')
in App.tsx is causing duplicate rescans because src/ui/message-handler.ts
already triggers a lint rescan on the batch-fix-v2-result event; remove the
unconditional setTimeout call (or replace it with a guard/debounce) so only one
rescan is triggered: either delete the setTimeout/post('rescan-lint') block in
App.tsx or add a boolean flag or short debounce keyed to batch-fix actions
(check symbols setTimeout and post('rescan-lint') in App.tsx and
batch-fix-v2-result handling in message-handler.ts) to ensure a single
rescan/score-update is emitted.

---

Nitpick comments:
In `@backend/package.json`:
- Line 8: The inline Node.js command in the package.json "build" script is hard
to read and maintain; extract that logic into a separate script (e.g., create
scripts/copy-schema.mjs containing the mkdir/copy logic) and then update the
"build" script to invoke that new module (node scripts/copy-schema.mjs) so the
build entry in package.json is concise and the file-copy logic is isolated in
scripts/copy-schema.mjs.

In `@src/ui/message-handler.ts`:
- Around line 1013-1035: Multiple handlers (handleLintIgnoreNode,
handleLintIgnoreError, handleLintIgnoreAllOfType, handleLintClearIgnored) repeat
the same post-action calls (persistIgnoredState(); handleRunDesignLint());
extract these into a single helper (e.g., persistIgnoredAndRerun or
persistAndRunDesignLint) and call it from each handler instead of duplicating
the two lines; ensure the helper preserves the exact call order and is used in
handleLintIgnoreNode, handleLintIgnoreError, handleLintIgnoreAllOfType, and
handleLintClearIgnored so behavior remains identical.

In `@ui/src/App.tsx`:
- Around line 460-473: Add an ARIA live region to the stale-selection banner so
screen readers announce it when it appears: update the container rendered when
selectionStale is true (the div that references selectionStale and
currentNodeName and contains the Re-analyze button which calls handleAnalyze) to
include appropriate attributes such as aria-live="polite" and role="status" (and
aria-atomic="true" if you want the entire message spoken) so the dynamic message
about currentNodeName is announced to assistive technologies.
- Around line 42-48: Change the loose casting to a proper typed parameter by
updating the type of the screenshot argument in tryBackendAnalysis to include
nodeType, width, height, hasAutoLayout, childCount and tokenSummary (or create
an interface like ScreenshotWithMeta and use it for the function signature),
then remove the multiple "(screenshot as any)" casts in the object construction
where nodeType, hasAutoLayout, childCount and tokenSummary are used so the
fields are accessed with real typings; keep the existing nullish defaults (??
'FRAME', ?? false, ?? 0) where needed to preserve behavior.

In `@ui/src/components/chat/MessageList.tsx`:
- Around line 137-178: The AnalysisPhaseIndicator component and its related
constants PHASE_LABELS and PHASE_ORDER are defined after the default export in
MessageList which harms organization and reusability; move them into their own
module (e.g., create ui/src/components/messages/AnalysisPhaseIndicator.tsx)
exporting AnalysisPhaseIndicator, PHASE_LABELS, and PHASE_ORDER (or at least the
component) and then import and use AnalysisPhaseIndicator from MessageList, or
alternatively relocate the declarations above the MessageList component so they
are defined before the default export; ensure the prop type AnalysisPhase is
imported/available and update any imports/exports accordingly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 99f4b4ac-3a57-4d09-8677-19640b243f77

📥 Commits

Reviewing files that changed from the base of the PR and between ad771d9 and 73d1b73.

⛔ Files ignored due to path filters (2)
  • dist/code.js is excluded by !**/dist/**
  • dist/ui.html is excluded by !**/dist/**
📒 Files selected for processing (6)
  • backend/package.json
  • backend/src/routes/flow.ts
  • src/flow/graph-builder.ts
  • src/ui/message-handler.ts
  • ui/src/App.tsx
  • ui/src/components/chat/MessageList.tsx

Comment thread backend/src/routes/flow.ts
Comment thread src/flow/graph-builder.ts
Comment on lines +136 to 147
// Frames that have BACK/CLOSE edges are not truly dead ends
const hasBackEdge = new Set<string>();
for (const edge of validEdges) {
if (edge.destinationFrameId === '__BACK__') {
hasBackEdge.add(edge.sourceFrameId);
}
}

// Dead ends: frames with no outgoing connections AND no BACK navigation
const deadEnds = frames
.filter(f => (outgoing.get(f.id)?.size || 0) === 0)
.filter(f => (outgoing.get(f.id)?.size || 0) === 0 && !hasBackEdge.has(f.id))
.map(f => f.id);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Проверка текущей обработки BACK/CLOSE в графе
rg -n -C3 --type=ts "collectReactions|action\\.type === 'BACK'|action\\.type === 'CLOSE'|destinationFrameId: '__BACK__'|navigation:\\s*'CLOSE'" src/flow/graph-builder.ts

# Проверка, где вообще создаются FlowEdge с CLOSE
rg -n -C2 --type=ts "navigation:\\s*'CLOSE'|type:\\s*'CLOSE'|destinationFrameId:\\s*'__BACK__'" src

Repository: lemone112/figmalint

Length of output: 1522


🏁 Script executed:

# Check the FlowEdge interface definition
rg -n -B2 -A5 "interface FlowEdge|type FlowEdge" src/flow/graph-builder.ts

# Check the analyzeFlowGraph function (around line 334-336)
sed -n '330,350p' src/flow/graph-builder.ts

# Search entire codebase for CLOSE references (not just in graph-builder)
rg -n "CLOSE|'CLOSE'|\"CLOSE\"" src --type=ts

# Check if there are any Figma API type definitions
fd -e ts -e d.ts | xargs grep -l "type.*CLOSE\|action.*CLOSE" 2>/dev/null | head -5

Repository: lemone112/figmalint

Length of output: 1172


🏁 Script executed:

# Check if CLOSE edges are created anywhere (not just from BACK actions)
rg -n "navigation.*CLOSE|navigation:\s*'CLOSE'" src --type=ts

# Look at entire collectReactions function to see all edge creation
sed -n '27,71p' src/flow/graph-builder.ts

# Check if there are other places where FlowEdge is created besides BACK
rg -n "edges\.push" src/flow/graph-builder.ts -B5 -A3

# Search for any mention of Figma close/CLOSE action in documentation or comments
rg -n "close|CLOSE" src/flow/graph-builder.ts

Repository: lemone112/figmalint

Length of output: 2951


CLOSE-навигация не исключается из dead-end фреймов.

На строке 136–147 проверяется только destinationFrameId === '__BACK__', но фреймы с исходящей CLOSE-навигацией (из NODE-действий) не исключаются из dead-ends, хотя функция analyzeFlowGraph (строка 335) уже учитывает обе: (e.navigation === 'BACK' || e.navigation === 'CLOSE').

Предлагаемая правка
   // Frames that have BACK/CLOSE edges are not truly dead ends
-  const hasBackEdge = new Set<string>();
+  const hasBackOrCloseEdge = new Set<string>();
   for (const edge of validEdges) {
-    if (edge.destinationFrameId === '__BACK__') {
-      hasBackEdge.add(edge.sourceFrameId);
+    if (edge.destinationFrameId === '__BACK__' || edge.navigation === 'CLOSE') {
+      hasBackOrCloseEdge.add(edge.sourceFrameId);
     }
   }

   // Dead ends: frames with no outgoing connections AND no BACK navigation
   const deadEnds = frames
-    .filter(f => (outgoing.get(f.id)?.size || 0) === 0 && !hasBackEdge.has(f.id))
+    .filter(f => (outgoing.get(f.id)?.size || 0) === 0 && !hasBackOrCloseEdge.has(f.id))
     .map(f => f.id);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/flow/graph-builder.ts` around lines 136 - 147, The dead-end detection
only excludes frames with BACK edges by checking destinationFrameId ===
'__BACK__'; update the loop over validEdges (and the hasBackEdge set) to also
treat CLOSE navigation as a non-dead-end: check edge.navigation === 'CLOSE'
(and/or destinationFrameId === '__CLOSE__' if that form exists) and add
edge.sourceFrameId to the set when either condition matches; this keeps
analyzeFlowGraph's behavior consistent (which already checks e.navigation ===
'BACK' || e.navigation === 'CLOSE') and prevents CLOSE-linked frames from being
classified as dead ends.

Comment thread src/ui/message-handler.ts Outdated
Comment thread src/ui/message-handler.ts
lemone112 and others added 3 commits March 13, 2026 14:23
Bugs:
- Remove duplicate rescan after batch fix (plugin already rescans)
- Clean up analysis-phase indicator on AI failure (prevents stuck state)
- Reset selectionStale when user returns to analyzed node

Major:
- Clear Refero polling on new analysis start (prevents stale injection)
- Remove dist/ from .dockerignore (was breaking Docker build)
- Move API key check before getAnthropicClient() in flow-analyzer
- Align backend score weights with UI (25/18/10/25/7/8/7)

Minor:
- Add aria-hidden to analysis phase checkmark SVG

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Resolves merge conflicts between our security/UX branch and main's
flow analysis features. Adopts 9-category design health scoring
(+conversion, +cognitive) across backend, UI hooks, and ScoreCard.
Adds flow analysis event types to PluginEvent union.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- flow.ts: return 503 (not 200) when API key missing, redact error logs
- message-handler: strip payload from console logs to prevent data leaks
- message-handler: handleRescanLint now restores lintScopeNodeIds for
  consistent re-scan results after batch fixes
- StickyHeader: add aria-label="Open settings" on icon button
- Rebuild dist artifacts

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
backend/src/services/analyzer.ts (1)

169-207: ⚠️ Potential issue | 🟡 Minor

Обновить комментарий с весами в соответствии с реализацией

Комментарий содержит устаревшие данные о 7 категориях. Фактический код использует 9 категорий с правильными весами, которые уже синхронизированы с UI-частью проекта.

Рекомендуемое исправление
-  // Weights aligned with UI: Tokens 25%, A11y 25%, Spacing 18%, Layout 10%, Naming 7%, Visual 8%, Microcopy 7%
+  // Weights: Tokens 20%, A11y 20%, Spacing 12%, Visual 10%, Conversion 10%, Microcopy 8%, Cognitive 8%, Layout 8%, Naming 4%
🤖 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 169 - 207, Update the outdated
top comment that lists seven weights to reflect the nine categories and their
actual weights used in the computation (as implemented in the designHealthScore
expression): tokens 20%, accessibility 20%, spacing 12%, visualQuality 10%,
conversion 10%, microcopy 8%, cognitive 8%, layout 8%, naming 4%; locate the
comment near SEVERITY_WEIGHT and the severityScore/designHealthScore logic and
replace the old sentence with a concise line describing these nine categories
and their percentages so the comment matches the code.
ui/src/App.tsx (3)

510-518: ⚠️ Potential issue | 🟡 Minor

Кнопке настроек всё ещё не хватает доступного имени.

title недостаточен для icon-only controls. Добавьте aria-label на сам <button>, иначе screen reader получит безымянную кнопку.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/src/App.tsx` around lines 510 - 518, The settings button (the button that
calls setShowSettings(true) and renders the gear SVG) lacks an accessible name:
add an aria-label attribute to the same <button> (e.g., aria-label="Settings" or
a localized equivalent) so screen readers receive a clear name for this
icon-only control; keep the existing title if desired but ensure aria-label is
present on that button element in App.tsx.

268-287: ⚠️ Potential issue | 🟠 Major

Не отправляйте chat.messages в plugin как history без преобразования.

chat.messages — это UI-структуры из ui/src/lib/messages.ts ({ id, timestamp, message }), а plugin-side handleChatMessage() ожидает элементы с role / content и сразу читает msg.role и msg.content в createChatPromptWithContext(). В fallback-режиме история сейчас уходит в неправильном формате, и контекст чата деградирует.

Минимальный вариант преобразования
-        post('chat-message', { message: text, history: chat.messages });
+        const history = chat.messages.flatMap((m) => {
+          if (m.message.kind === 'user-text') {
+            return [{
+              id: m.id,
+              role: 'user' as const,
+              content: m.message.content,
+              timestamp: m.timestamp,
+            }];
+          }
+          if (m.message.kind === 'ai-text' && !m.message.streaming) {
+            return [{
+              id: m.id,
+              role: 'assistant' as const,
+              content: m.message.content,
+              timestamp: m.timestamp,
+            }];
+          }
+          return [];
+        });
+        post('chat-message', { message: text, history });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/src/App.tsx` around lines 268 - 287, The fallback branch in
handleSendMessage is sending UI message objects (chat.messages) directly to
post('chat-message'), but the plugin's
handleChatMessage/createChatPromptWithContext expect messages with {role,
content}; convert chat.messages to that shape before posting: map UI kinds
(e.g., 'user-text' -> role: 'user', 'ai-text' -> role: 'assistant', any
system/metadata -> 'system' or appropriate role) and set content from the UI
message text field, then call post('chat-message', { message: text, history:
transformedHistory }); ensure this transformation happens in the else branch
where post('chat-message') is called so plugin receives role/content pairs.

153-160: ⚠️ Potential issue | 🟠 Major

Свяжите screenshot-result с конкретным запуском анализа.

Сейчас любой пришедший screenshot сразу склеивается с текущим pendingLintResult.current. Если поздний ответ от предыдущего export-screenshot доедет после нового Analyze, backend получит mixed payload из двух разных selection.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/src/App.tsx` around lines 153 - 160, The screenshot-result must be
associated with the specific analysis run instead of blindly pairing any
pendingScreenshot.current with pendingLintResult.current; modify the event
handling around pendingScreenshot.current / pendingLintResult.current and
tryBackendAnalysis so each pending item carries a runId (or unique request id)
and you only call tryBackendAnalysis when the runId on pendingScreenshot.current
matches the runId on pendingLintResult.current; drop or ignore screenshots whose
runId does not match the current pending lint result (or vice versa) to prevent
mixing payloads from different exports.
♻️ Duplicate comments (2)
src/ui/message-handler.ts (2)

989-999: ⚠️ Potential issue | 🟠 Major

Вынесите восстановление lintScopeNodeIds в общий helper.

Сейчас fixed scope живёт только в этом пути. handleRescanLint() и handleLintIgnoreAllOfType() всё ещё идут в lintSelection(...) напрямую, поэтому после batch-fix/ignore-all результаты снова расходятся с исходным scope.

🤖 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 989 - 999, The current code restores
the selection/lint scope inline using lintScopeNodeIds, causing other flows
(handleRescanLint, handleLintIgnoreAllOfType) that call lintSelection(...) to
miss that restoration; extract the restore logic into a shared helper (e.g.,
restoreLintScope or applyLintScope) that takes lintScopeNodeIds and sets
figma.currentPage.selection after mapping/filtering with figma.getNodeById and
SceneNode/type checks, then replace the inline block in message-handler.ts with
a call to that helper and update handleRescanLint and handleLintIgnoreAllOfType
to call the same helper before or after invoking lintSelection(...) so all paths
consistently restore the fixed scope.

90-91: ⚠️ Potential issue | 🟠 Major

Не логируйте весь payload входящих UI-сообщений.

Здесь по-прежнему утекают chat-message (текст + history), screenshot payload-ы и прочие пользовательские данные. Оставьте в логах только type и безопасный allowlist метаданных.

🤖 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 90 - 91, The current console.log in
message handler exposes full incoming payload via logData (const logData = ...
and console.log('Received message:', type, logData)); change it to only log the
message type and a safe allowlist of metadata fields (e.g., messageId, userId,
timestamp) and explicitly omit or redact sensitive payloads like chat-message
text, chat history, screenshot/image blobs and any `payload`/`data` contents;
implement a small helper (e.g., buildSafeLogMeta(type, data)) used in place of
logData so only allowlisted keys are output and everything else is removed or
replaced with a fixed placeholder like "[REDACTED]".
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@ui/src/App.tsx`:
- Line 32: В App.tsx добавляется только одно сообщение chat.addMessage({ kind:
'analysis-phase', phase: 'ai-review' }), поэтому индикатор проходит лишь фазу
ai-review; нужно эмитить все четыре фазы (lint, screenshot, refero, ai-review) в
правиль порядке перед запуском обработки и/или изменить логику в
useChat.handleAiReview так, чтобы он не удалял все messages с
kind==='analysis-phase' целиком до завершения всех фаз; обновите места вызова
chat.addMessage (в App.tsx) чтобы создать по одному сообщению для каждой фазы
(phase: 'lint'|'screenshot'|'refero'|'ai-review') или измените handleAiReview
чтобы помечать фазы как завершённые по-отдельности, чтобы AnalysisPhaseIndicator
видел полный end-to-end прогресс.

In `@ui/src/components/chat/MessageList.tsx`:
- Around line 156-158: The isComplete logic marks future phases complete when
done is true; change it so only previous phases (i < currentIdx) and the current
phase (i === currentIdx) become complete when done is true. Update the
isComplete computation used in MessageList (referencing variables i, currentIdx,
done and the computed isActive/isPending) to: isComplete should be true when i <
currentIdx OR when i === currentIdx && done, leaving isActive as i ===
currentIdx && !done and isPending as i > currentIdx && !done.

In `@ui/src/hooks/useChat.ts`:
- Around line 453-454: computeScoreBreakdown() already counts conversion and
cognitive issues but the UI string builds only visualQuality and microcopy, so
extend the parts assembly in useChat (where parts is populated) to also push
entries for byType.conversion and byType.cognitive when their counts are > 0;
update the same block that currently checks byType.visualQuality and
byType.microcopy to include checks for byType.conversion and byType.cognitive
and format them like the others (e.g., `${byType.conversion} conversion issues`,
`${byType.cognitive} cognitive issues`) so the "Found N issues ..." string
includes those categories.

In `@ui/src/lib/messages.ts`:
- Line 207: Расширьте контракт union-посылки для варианта 'screenshot-result' в
ui/src/lib/messages.ts: добавьте в объект payload явные поля nodeType: string и
tokenSummary: Record<string, any> (или более конкретную структуру, если
известна), чтобы соответствовать тому, что отправляет src/ui/message-handler.ts
и что читает ui/src/App.tsx; обновите типизацию там, где используется этот
вариант, чтобы больше не требовался приведение через any.

---

Outside diff comments:
In `@backend/src/services/analyzer.ts`:
- Around line 169-207: Update the outdated top comment that lists seven weights
to reflect the nine categories and their actual weights used in the computation
(as implemented in the designHealthScore expression): tokens 20%, accessibility
20%, spacing 12%, visualQuality 10%, conversion 10%, microcopy 8%, cognitive 8%,
layout 8%, naming 4%; locate the comment near SEVERITY_WEIGHT and the
severityScore/designHealthScore logic and replace the old sentence with a
concise line describing these nine categories and their percentages so the
comment matches the code.

In `@ui/src/App.tsx`:
- Around line 510-518: The settings button (the button that calls
setShowSettings(true) and renders the gear SVG) lacks an accessible name: add an
aria-label attribute to the same <button> (e.g., aria-label="Settings" or a
localized equivalent) so screen readers receive a clear name for this icon-only
control; keep the existing title if desired but ensure aria-label is present on
that button element in App.tsx.
- Around line 268-287: The fallback branch in handleSendMessage is sending UI
message objects (chat.messages) directly to post('chat-message'), but the
plugin's handleChatMessage/createChatPromptWithContext expect messages with
{role, content}; convert chat.messages to that shape before posting: map UI
kinds (e.g., 'user-text' -> role: 'user', 'ai-text' -> role: 'assistant', any
system/metadata -> 'system' or appropriate role) and set content from the UI
message text field, then call post('chat-message', { message: text, history:
transformedHistory }); ensure this transformation happens in the else branch
where post('chat-message') is called so plugin receives role/content pairs.
- Around line 153-160: The screenshot-result must be associated with the
specific analysis run instead of blindly pairing any pendingScreenshot.current
with pendingLintResult.current; modify the event handling around
pendingScreenshot.current / pendingLintResult.current and tryBackendAnalysis so
each pending item carries a runId (or unique request id) and you only call
tryBackendAnalysis when the runId on pendingScreenshot.current matches the runId
on pendingLintResult.current; drop or ignore screenshots whose runId does not
match the current pending lint result (or vice versa) to prevent mixing payloads
from different exports.

---

Duplicate comments:
In `@src/ui/message-handler.ts`:
- Around line 989-999: The current code restores the selection/lint scope inline
using lintScopeNodeIds, causing other flows (handleRescanLint,
handleLintIgnoreAllOfType) that call lintSelection(...) to miss that
restoration; extract the restore logic into a shared helper (e.g.,
restoreLintScope or applyLintScope) that takes lintScopeNodeIds and sets
figma.currentPage.selection after mapping/filtering with figma.getNodeById and
SceneNode/type checks, then replace the inline block in message-handler.ts with
a call to that helper and update handleRescanLint and handleLintIgnoreAllOfType
to call the same helper before or after invoking lintSelection(...) so all paths
consistently restore the fixed scope.
- Around line 90-91: The current console.log in message handler exposes full
incoming payload via logData (const logData = ... and console.log('Received
message:', type, logData)); change it to only log the message type and a safe
allowlist of metadata fields (e.g., messageId, userId, timestamp) and explicitly
omit or redact sensitive payloads like chat-message text, chat history,
screenshot/image blobs and any `payload`/`data` contents; implement a small
helper (e.g., buildSafeLogMeta(type, data)) used in place of logData so only
allowlisted keys are output and everything else is removed or replaced with a
fixed placeholder like "[REDACTED]".

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 9fc10db6-dc8b-480e-bd85-69714ab81861

📥 Commits

Reviewing files that changed from the base of the PR and between 73d1b73 and 96c5abe.

⛔ Files ignored due to path filters (2)
  • dist/code.js is excluded by !**/dist/**
  • dist/ui.html is excluded by !**/dist/**
📒 Files selected for processing (10)
  • backend/.dockerignore
  • backend/src/services/analyzer.ts
  • backend/src/services/flow-analyzer.ts
  • src/ui/message-handler.ts
  • ui/src/App.tsx
  • ui/src/components/chat/MessageList.tsx
  • ui/src/components/messages/ScoreCard.tsx
  • ui/src/hooks/useChat.ts
  • ui/src/lib/api.ts
  • ui/src/lib/messages.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • ui/src/components/messages/ScoreCard.tsx
  • backend/.dockerignore
  • ui/src/lib/api.ts

Comment thread ui/src/components/chat/MessageList.tsx Outdated
Comment thread ui/src/hooks/useChat.ts
Comment on lines +453 to +454
if (byType.visualQuality > 0) parts.push(`${byType.visualQuality} visual quality issues`);
if (byType.microcopy > 0) parts.push(`${byType.microcopy} microcopy issues`);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Сводка lint всё ещё теряет conversion и cognitive.

computeScoreBreakdown() уже учитывает эти категории, но здесь parts пополняется только visualQuality и microcopy. Если ошибки будут только в новых bucket-ах, пользователь увидит пустое Found N issues ... : ..

Минимальная правка
   if (byType.visualQuality > 0) parts.push(`${byType.visualQuality} visual quality issues`);
   if (byType.microcopy > 0) parts.push(`${byType.microcopy} microcopy issues`);
+  if (byType.conversion > 0) parts.push(`${byType.conversion} conversion issues`);
+  if (byType.cognitive > 0) parts.push(`${byType.cognitive} cognitive load issues`);
📝 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.

Suggested change
if (byType.visualQuality > 0) parts.push(`${byType.visualQuality} visual quality issues`);
if (byType.microcopy > 0) parts.push(`${byType.microcopy} microcopy issues`);
if (byType.visualQuality > 0) parts.push(`${byType.visualQuality} visual quality issues`);
if (byType.microcopy > 0) parts.push(`${byType.microcopy} microcopy issues`);
if (byType.conversion > 0) parts.push(`${byType.conversion} conversion issues`);
if (byType.cognitive > 0) parts.push(`${byType.cognitive} cognitive load issues`);
🤖 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 453 - 454, computeScoreBreakdown()
already counts conversion and cognitive issues but the UI string builds only
visualQuality and microcopy, so extend the parts assembly in useChat (where
parts is populated) to also push entries for byType.conversion and
byType.cognitive when their counts are > 0; update the same block that currently
checks byType.visualQuality and byType.microcopy to include checks for
byType.conversion and byType.cognitive and format them like the others (e.g.,
`${byType.conversion} conversion issues`, `${byType.cognitive} cognitive
issues`) so the "Found N issues ..." string includes those categories.

Comment thread ui/src/lib/messages.ts
| { type: 'api-key-saved'; data: { success: boolean } }
| { type: 'screenshot-result'; data: { nodeId: string; nodeName: string; screenshot: string; width: number; height: number } }
| { type: string; data: unknown };
| { type: 'screenshot-result'; data: { nodeId: string; nodeName: string; screenshot: string; width: number; height: number; hasAutoLayout?: boolean; childCount?: number } }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Дополните контракт screenshot-result реальными полями payload.

src/ui/message-handler.ts уже отправляет сюда nodeType и tokenSummary, а ui/src/App.tsx из-за этого читает payload через as any. Пока union их не описывает, граница Plugin ↔ UI теряет type-safety, и следующая регрессия здесь пройдёт мимо компилятора.

Минимальный вариант правки
-  | { type: 'screenshot-result'; data: { nodeId: string; nodeName: string; screenshot: string; width: number; height: number; hasAutoLayout?: boolean; childCount?: number } }
+  | {
+      type: 'screenshot-result';
+      data: {
+        nodeId: string;
+        nodeName: string;
+        nodeType: string;
+        screenshot: string;
+        width: number;
+        height: number;
+        hasAutoLayout?: boolean;
+        childCount?: number;
+        tokenSummary?: {
+          totalTokens: number;
+          boundToVariables: number;
+          boundToStyles: number;
+          hardCoded: number;
+        };
+      };
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ui/src/lib/messages.ts` at line 207, Расширьте контракт union-посылки для
варианта 'screenshot-result' в ui/src/lib/messages.ts: добавьте в объект payload
явные поля nodeType: string и tokenSummary: Record<string, any> (или более
конкретную структуру, если известна), чтобы соответствовать тому, что отправляет
src/ui/message-handler.ts и что читает ui/src/App.tsx; обновите типизацию там,
где используется этот вариант, чтобы больше не требовался приведение через any.

Beyond the original Sprint 0-2 plan (fully completed), this adds:

Security:
- Bearer auth middleware (BACKEND_AUTH_TOKEN env var)
- In-memory rate limiter per IP (60 req/min default)
- AbortController on streaming chat (cancel previous on new request)
- MCP client timeout wrappers (15s Promise.race)

Architecture:
- Merge src/fixes/ into src/fix/ (single directory)
- Include lint settings in consistency engine cache hash
- Settings panel → proper dialog (role, aria-modal, focus trap, Escape)
- aria-live="polite" + role="log" on message list

Infrastructure:
- GitHub Actions CI/CD (typecheck + build + Docker)
- esbuild --minify (405KB → 212KB, -48%)
- DB indexes on node_id, created_at, updated_at
- Session TTL cleanup (7d, runs on startup + every 6h)
- .env.example updated with auth + rate limit docs

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

♻️ Duplicate comments (1)
ui/src/components/chat/MessageList.tsx (1)

156-158: ⚠️ Potential issue | 🟡 Minor

Логика isComplete помечает будущие фазы как завершённые.

Когда done=true, выражение i < currentIdx || done будет истинно для всех фаз, включая те, которые ещё не выполнялись. Например, если phase='lint' и done=true, фазы screenshot, ai-review и refero также отобразятся как завершённые.

🐛 Предлагаемое исправление
         const isActive = i === currentIdx && !done;
-        const isComplete = i < currentIdx || done;
-        const isPending = i > currentIdx && !done;
+        const isComplete = i < currentIdx || (done && i === currentIdx);
+        const isPending = i > currentIdx;
🤖 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 156 - 158, The
isComplete expression currently uses "i < currentIdx || done" which marks all
phases complete when done is true; change it to "i < currentIdx || (done && i <=
currentIdx)" so only phases before currentIdx or the current phase when done is
true are treated as complete; update the variable in MessageList.tsx where
isComplete is defined (referencing isActive, isComplete, isPending, currentIdx,
done, and loop index i).
🧹 Nitpick comments (5)
backend/src/mcp/design-systems-client.ts (1)

15-43: Опционально: рассмотреть создание фабричной функции для MCP-клиентов.

Структура getDesignSystemsClient и getReferoClientclient.ts) практически идентична. При добавлении новых MCP-серверов имеет смысл выделить общую логику в фабрику createMcpClientFactory(url, name).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/src/mcp/design-systems-client.ts` around lines 15 - 43, Extract the
duplicated connection logic from getDesignSystemsClient and getReferoClient into
a factory function createMcpClientFactory(url, name) that returns an async
getter; the factory should create a Client with provided name/version, use
StreamableHTTPClientTransport(new URL(url)), call
withTimeout(client.connect(transport), 15_000, '... connect') and manage shared
state (cached client, connectionFailed flag, and retry setTimeout) exactly as
getDesignSystemsClient does; replace getDesignSystemsClient/getReferoClient
bodies to call the factory-produced getter and pass the corresponding
DESIGN_SYSTEMS_MCP_URL and the service name so both functions delegate to the
single implementation.
backend/src/mcp/parse-tool-result.ts (1)

5-16: Утечка таймера: setTimeout не очищается при успешном завершении промиса.

Когда исходный промис разрешается раньше таймаута, setTimeout продолжает висеть в очереди событий до истечения ms. При частых вызовах это приводит к накоплению "мёртвых" таймеров. Рекомендуется очищать таймер при завершении промиса.

♻️ Предлагаемое исправление с очисткой таймера
 export function withTimeout<T>(
   promise: Promise<T>,
   ms = 15_000,
   label = 'MCP call',
 ): Promise<T> {
+  let timeoutId: ReturnType<typeof setTimeout>;
+  const timeoutPromise = new Promise<never>((_, reject) => {
+    timeoutId = setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms);
+  });
+
   return Promise.race([
-    promise,
-    new Promise<never>((_, reject) =>
-      setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms),
-    ),
-  ]);
+    promise.finally(() => clearTimeout(timeoutId)),
+    timeoutPromise,
+  ]) as Promise<T>;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/src/mcp/parse-tool-result.ts` around lines 5 - 16, The withTimeout
function creates a timer with setTimeout that is never cleared when the input
promise settles; modify withTimeout to store the timer id (from setTimeout) and
ensure clearTimeout(timer) is invoked once either the original promise resolves
or rejects so the timer is not left pending. Specifically, change Promise.race
to create the timeout via a let timer = setTimeout(...), then attach handlers
(e.g., wrap the original promise with .then/.catch or use finally) to
clearTimeout(timer) before resolving/rejecting so the timer is always cleared;
references: withTimeout, the setTimeout-created timer and the Promise.race
wrapper.
backend/src/mcp/design-systems-tools.ts (1)

157-164: Опционально: extractText дублирует часть логики parseToolResult.

Функция extractText извлекает текст из MCP-ответа аналогично началу parseToolResult. Можно переиспользовать общую логику, вынеся извлечение textBlock в отдельный хелпер или параметризовав parseToolResult.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/src/mcp/design-systems-tools.ts` around lines 157 - 164, extractText
duplicates the initial extraction logic in parseToolResult; refactor to remove
duplication by extracting the shared logic into a small helper (e.g.,
getTextBlock or getContentArray) or by making parseToolResult accept a
flag/utility to return the textBlock, then have extractText call that helper;
update references to use the helper and remove the duplicated find(c => c.type
=== 'text') logic so extractText simply delegates to the new helper (referencing
extractText, parseToolResult, and the textBlock lookup).
.github/workflows/ci.yml (1)

59-59: Сделайте docker-job зависимым от всех проверок, а не только от backend.

Сейчас docker на Line 59 стартует после backend, даже если plugin или ui уже упали. Рекомендуется дождаться всех quality gates.

Предлагаемое изменение
-    needs: [backend]
+    needs: [plugin, ui, backend]
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/ci.yml at line 59, В job "docker" в
.github/workflows/ci.yml текущая зависимость указана только как needs:
[backend]; обновите ключ needs для job'а docker так, чтобы он перечислял все
quality-gate job'ы (например needs: [backend, plugin, ui]) — найдите блок с
именем/идентификатором docker и измените массив в поле needs, чтобы дождаться
успешного завершения всех перечисленных задач перед запуском docker.
backend/src/middleware/rate-limit.ts (1)

29-31: Рассмотрите использование встроенных механизмов Hono для определения IP вместо прямого чтения заголовков.

Текущая реализация опирается на X-Forwarded-For/X-Real-IP, что работает с Caddy, но создаёт скрытые зависимости от конфигурации reverse proxy. Лучше использовать ConnInfo из @hono/node-server/conninfo для получения реального IP клиента или встроенное ipRestriction middleware Hono. Это сделает код более явным и защитит от случайного деплоя без reverse proxy, когда все запросы попадут в bucket unknown.

Текущая архитектура (Caddy → backend) защищена от подделки IP, поскольку Caddy по умолчанию генерирует X-Forwarded-For на основе фактического подключения и игнорирует значения, отправленные клиентом.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/src/middleware/rate-limit.ts` around lines 29 - 31, Replace the
manual header parsing that creates the "const ip = ..." value with Hono's
supported client-IP mechanisms: import and use ConnInfo from
"@hono/node-server/conninfo" to derive the real client address (replace the
"const ip = ..." assignment in the rate-limit middleware) or alternatively
switch to Hono's built-in ipRestriction middleware; update the rate-limit
middleware function (the code that computes/uses the ip variable and any bucket
lookup keys) to obtain the client IP via ConnInfo (or ipRestriction) instead of
reading x-forwarded-for/x-real-ip and remove the fallback to 'unknown'.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.github/workflows/ci.yml:
- Around line 14-16: В файле .github/workflows/ci.yml замените все плавающие
теги в ключах uses (например actions/checkout@v4, actions/setup-node@v4 и
остальные occurrences с `@v3/`@v6) на фиксированные полные commit SHA для каждого
action; откройте каждую строку с uses, найдите соответствующий action reference
(например actions/checkout, actions/setup-node и т.д.), заменить '@vX' на
'@<full-commit-sha>' и при желании добавить комментарий с соответствующей
семантической версией (например "# v4") для ясности.

In `@backend/src/db/queries.ts`:
- Around line 106-109: The cleanupExpiredSessions function is deleting rows by
comparing updated_at (stored as SQLite CURRENT_TIMESTAMP "YYYY-MM-DD HH:MM:SS")
against a cutoff produced with toISOString("YYYY-MM-DDTHH:MM:SS.sssZ"), causing
incorrect lexical comparisons; fix cleanupExpiredSessions by producing a cutoff
string in the same format as SQLite CURRENT_TIMESTAMP (YYYY-MM-DD HH:MM:SS) —
e.g., create the cutoff Date, convert it to ISO, replace the 'T' with a space
and strip milliseconds and trailing 'Z' (or format via Date methods) and then
pass that normalized cutoff to db.prepare('DELETE FROM sessions WHERE updated_at
< ?').run(cutoff) so comparisons are correct.

In `@backend/src/index.ts`:
- Around line 35-56: The middleware order causes CORS preflight to be rejected
and unauthenticated floods to bypass rate limiting; reorder the middleware so
CORS runs first, then rateLimit(), then bearerAuth(), and keep bodyLimit() after
auth/body parsing where appropriate. Concretely, move the cors(...) app.use('*',
cors(...)) above the '/api/*' middlewares, ensure app.use('/api/*', rateLimit())
is registered before app.use('/api/*', bearerAuth()), and place
app.use('/api/*', bodyLimit(...)) after authentication/limits (or at least after
cors) so preflight OPTIONS isn’t blocked by bearerAuth() and rate limiting
applies to unauthenticated requests.

In `@backend/src/middleware/auth.ts`:
- Around line 10-12: The middleware currently fail-opens when
process.env.BACKEND_AUTH_TOKEN is empty (the token variable in
backend/src/middleware/auth.ts), which makes /api/* public if the env is
forgotten; change this to fail-closed by validating configuration at startup and
only allowing auth to be disabled via an explicit dev flag. Concretely: add a
startup config check that reads BACKEND_AUTH_TOKEN and a clear opt-out (e.g.
NODE_ENV === "development" or a BACKEND_AUTH_DISABLED=true flag), throw an error
and abort start if BACKEND_AUTH_TOKEN is missing in non-dev environments, and
update the auth middleware (the code path that currently does if (!token) return
next()) to treat a missing token as unauthorized (return 401/forbidden) rather
than bypassing auth unless the explicit dev flag was set and validated at
startup.

In `@ui/src/App.tsx`:
- Line 33: Нужно эмитить все четыре фазы анализа вместо только 'ai-review':
добавьте вызовы chat.addMessage({ kind: 'analysis-phase', phase: 'lint' }) в
начало функции handleRunDesignLint, chat.addMessage({ kind: 'analysis-phase',
phase: 'screenshot' }) в том месте, где инициируется запрос скриншота (тот же
обработчик/функция, которая вызывает получение скриншота), оставьте существующий
chat.addMessage({ kind: 'analysis-phase', phase: 'ai-review' }) при отправке на
бэкенд, и добавьте chat.addMessage({ kind: 'analysis-phase', phase: 'refero' })
при получении Refero-данных или при завершении polling; это обеспечит, что
компонент AnalysisPhaseIndicator увидит последовательность фаз lint → screenshot
→ ai-review → refero.

---

Duplicate comments:
In `@ui/src/components/chat/MessageList.tsx`:
- Around line 156-158: The isComplete expression currently uses "i < currentIdx
|| done" which marks all phases complete when done is true; change it to "i <
currentIdx || (done && i <= currentIdx)" so only phases before currentIdx or the
current phase when done is true are treated as complete; update the variable in
MessageList.tsx where isComplete is defined (referencing isActive, isComplete,
isPending, currentIdx, done, and loop index i).

---

Nitpick comments:
In @.github/workflows/ci.yml:
- Line 59: В job "docker" в .github/workflows/ci.yml текущая зависимость указана
только как needs: [backend]; обновите ключ needs для job'а docker так, чтобы он
перечислял все quality-gate job'ы (например needs: [backend, plugin, ui]) —
найдите блок с именем/идентификатором docker и измените массив в поле needs,
чтобы дождаться успешного завершения всех перечисленных задач перед запуском
docker.

In `@backend/src/mcp/design-systems-client.ts`:
- Around line 15-43: Extract the duplicated connection logic from
getDesignSystemsClient and getReferoClient into a factory function
createMcpClientFactory(url, name) that returns an async getter; the factory
should create a Client with provided name/version, use
StreamableHTTPClientTransport(new URL(url)), call
withTimeout(client.connect(transport), 15_000, '... connect') and manage shared
state (cached client, connectionFailed flag, and retry setTimeout) exactly as
getDesignSystemsClient does; replace getDesignSystemsClient/getReferoClient
bodies to call the factory-produced getter and pass the corresponding
DESIGN_SYSTEMS_MCP_URL and the service name so both functions delegate to the
single implementation.

In `@backend/src/mcp/design-systems-tools.ts`:
- Around line 157-164: extractText duplicates the initial extraction logic in
parseToolResult; refactor to remove duplication by extracting the shared logic
into a small helper (e.g., getTextBlock or getContentArray) or by making
parseToolResult accept a flag/utility to return the textBlock, then have
extractText call that helper; update references to use the helper and remove the
duplicated find(c => c.type === 'text') logic so extractText simply delegates to
the new helper (referencing extractText, parseToolResult, and the textBlock
lookup).

In `@backend/src/mcp/parse-tool-result.ts`:
- Around line 5-16: The withTimeout function creates a timer with setTimeout
that is never cleared when the input promise settles; modify withTimeout to
store the timer id (from setTimeout) and ensure clearTimeout(timer) is invoked
once either the original promise resolves or rejects so the timer is not left
pending. Specifically, change Promise.race to create the timeout via a let timer
= setTimeout(...), then attach handlers (e.g., wrap the original promise with
.then/.catch or use finally) to clearTimeout(timer) before resolving/rejecting
so the timer is always cleared; references: withTimeout, the setTimeout-created
timer and the Promise.race wrapper.

In `@backend/src/middleware/rate-limit.ts`:
- Around line 29-31: Replace the manual header parsing that creates the "const
ip = ..." value with Hono's supported client-IP mechanisms: import and use
ConnInfo from "@hono/node-server/conninfo" to derive the real client address
(replace the "const ip = ..." assignment in the rate-limit middleware) or
alternatively switch to Hono's built-in ipRestriction middleware; update the
rate-limit middleware function (the code that computes/uses the ip variable and
any bucket lookup keys) to obtain the client IP via ConnInfo (or ipRestriction)
instead of reading x-forwarded-for/x-real-ip and remove the fallback to
'unknown'.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 55414141-4984-417b-ada0-70d9c814698f

📥 Commits

Reviewing files that changed from the base of the PR and between ecf934f and f485069.

⛔ Files ignored due to path filters (2)
  • dist/code.js is excluded by !**/dist/**
  • dist/ui.html is excluded by !**/dist/**
📒 Files selected for processing (23)
  • .github/workflows/ci.yml
  • backend/.env.example
  • backend/src/db/queries.ts
  • backend/src/db/schema.sql
  • backend/src/index.ts
  • backend/src/mcp/client.ts
  • backend/src/mcp/design-systems-client.ts
  • backend/src/mcp/design-systems-tools.ts
  • backend/src/mcp/parse-tool-result.ts
  • backend/src/mcp/refero-tools.ts
  • backend/src/middleware/auth.ts
  • backend/src/middleware/rate-limit.ts
  • package.json
  • src/core/component-analyzer.ts
  • src/core/consistency-engine.ts
  • src/fix/color-distance.ts
  • src/fix/naming-fixer.ts
  • src/fix/token-fixer.ts
  • src/ui/message-handler.ts
  • ui/src/App.tsx
  • ui/src/components/chat/MessageList.tsx
  • ui/src/components/shared/SettingsPanel.tsx
  • ui/src/lib/api.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • backend/.env.example
  • ui/src/lib/api.ts

Comment thread .github/workflows/ci.yml Outdated
Comment thread backend/src/db/queries.ts
Comment thread backend/src/index.ts Outdated
Comment thread backend/src/middleware/auth.ts Outdated
Comment on lines +10 to +12
const token = process.env.BACKEND_AUTH_TOKEN;
// If no token configured, auth is disabled (development mode)
if (!token) return next();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Аутентификация сейчас fail-open при пустом BACKEND_AUTH_TOKEN.

Если переменную забыли задать в проде, весь /api/* становится публичным. Лучше валидировать конфиг на старте и разрешать отключение auth только через явный dev-флаг, а не через отсутствие секрета.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@backend/src/middleware/auth.ts` around lines 10 - 12, The middleware
currently fail-opens when process.env.BACKEND_AUTH_TOKEN is empty (the token
variable in backend/src/middleware/auth.ts), which makes /api/* public if the
env is forgotten; change this to fail-closed by validating configuration at
startup and only allowing auth to be disabled via an explicit dev flag.
Concretely: add a startup config check that reads BACKEND_AUTH_TOKEN and a clear
opt-out (e.g. NODE_ENV === "development" or a BACKEND_AUTH_DISABLED=true flag),
throw an error and abort start if BACKEND_AUTH_TOKEN is missing in non-dev
environments, and update the auth middleware (the code path that currently does
if (!token) return next()) to treat a missing token as unauthorized (return
401/forbidden) rather than bypassing auth unless the explicit dev flag was set
and validated at startup.

Comment thread ui/src/App.tsx
lemone112 and others added 4 commits March 13, 2026 15:09
Sprint 5 (Baseline & Diff):
- Baseline snapshot storage with LZ-string compression + chunked pluginData
- Diff engine comparing current scan vs saved baseline
- DiffCard UI with score delta, per-category was→now, new/fixed/remaining
- Trend indicator (arrow + delta) in StickyHeader
- Save Baseline / Diff buttons in QuickActions bar
- Export reports enhanced with baseline comparison section

CodeRabbit review round 3 fixes:
- Pin CI actions to full commit SHAs
- Fix SQLite date format in cleanupExpiredSessions
- Reorder middleware: CORS → bodyLimit → rateLimit → bearerAuth
- Auth fail-closed in production (bypass only in dev)
- Emit all 4 analysis phases (lint → screenshot → ai-review → refero)
- Fix AnalysisPhaseIndicator isComplete logic for future phases
- Fix withTimeout timer leak on promise settlement
- Docker job depends on all quality gates

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Remove unused `setIntersection` in cross-screen-checks.ts
- Remove unused `LintSeverity` import in visual-quality.ts

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- docker/build-push-action SHA was non-existent, fixed to v6 tag SHA
- Updated checkout and setup-buildx-action to current tag SHAs

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The Docker build was failing because it expected a pre-built dist/
directory. Switch to multi-stage build: build stage compiles TypeScript,
production stage copies only the compiled output.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@lemone112
lemone112 merged commit 276d2b3 into main Mar 13, 2026
5 checks passed
@lemone112
lemone112 deleted the feat/sprint-0-1-security-ux-devops branch March 27, 2026 12:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant