Skip to content

Auto-QA Agent

Auto-QA Agent #1207

Workflow file for this run

name: Auto-QA Agent
# Quality checks 4x/day with rotating focus areas.
# Layer 1 (always): Build, lint, Go build, bundle size, npm audit
# Layer 2 (always): Resilience, Inventory consistency, UI design principles, ROADMAP/governance
# Layer 3 (always): NFR coverage (testing, i18n, state, navigation, efficiency)
# Layer 4 (always): Flicker detection, code centralization, demo data coverage
# Layer 5 (always): Console error patterns, button/action consistency, stale data
# Layer 6 (8-day rotation via day-of-year % 8):
# 0=Performance, 1=Security, 2=Navigation/A11y,
# 3=Operator Usefulness, 4=SRE/Multi-Cluster,
# 5=Feature Recommendations, 6=Resilience, 7=Consistency
# Layer 7 (weekly): Self-improvement analysis based on recent PR patterns
# Layer 8 (always): Adoption psychology — curiosity, discovery, stickiness
#
# Issues auto-assign Copilot and feed into the automation pipeline.
#
# IMPORTANT: The CONSOLE_AUTO secret (PAT) must have these permissions:
# - repo (full control) - for contents, issues, pull requests
# - workflow - for actions access
# Or use a Fine-Grained PAT with: Contents (read/write), Issues (read/write),
# Pull requests (read/write), Actions (read), Metadata (read)
on:
schedule:
- cron: '17 1,7,13,19 * * *' # 4x/day at 01:17, 07:17, 13:17, 19:17 UTC (saves ~83% PRUs vs hourly)
workflow_dispatch:
inputs:
skip_audit:
description: 'Skip npm audit check'
required: false
default: false
type: boolean
focus_override:
description: 'Override focus (performance|security|a11y|operator|sre|features|resilience|consistency|adoption|none)'
required: false
default: ''
type: string
env:
BUNDLE_SIZE_LIMIT_KB: 5120
MAX_ISSUES_PER_RUN: 3
ASSIGN_COPILOT: 'false' # Set to 'true' to auto-assign Copilot to created issues
COPILOT_ASSIGNMENT_DELAY_S: 120 # Seconds between Copilot assignments to avoid rate limiting
ISSUE_PREFIX: "[Auto-QA]"
NODE_VERSION: "22"
GO_VERSION: "1.26.4"
permissions: read-all
jobs:
auto-qa:
permissions:
contents: read
issues: write
actions: read
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
# ── Setup ──────────────────────────────────────────────────────
- name: Checkout main
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: ${{ env.NODE_VERSION }}
cache: npm
cache-dependency-path: web/package-lock.json
- name: Setup Go
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
go-version: ${{ env.GO_VERSION }}
- name: Install frontend dependencies
working-directory: web
run: npm ci
- name: Read tuning config
id: tuning
run: |
CONFIG=".github/auto-qa-tuning.json"
if [ -f "$CONFIG" ]; then
BLOCKED=$(jq -r '[.categories | to_entries[] | select(.value.status == "blocked") | .key] | join(",")' "$CONFIG")
BOOSTED=$(jq -r '[.categories | to_entries[] | select(.value.status == "boosted") | .key] | join(",")' "$CONFIG")
MAX_ISSUES=$(jq -r '.max_issues_override // 0' "$CONFIG")
PR_GUIDANCE=$(jq -r '.pr_guidance // ""' "$CONFIG")
# Export rotation weights as JSON for the focus step
jq -r '.rotation_weights // {}' "$CONFIG" > /tmp/rotation-weights.json
echo "blocked=$BLOCKED" >> "$GITHUB_OUTPUT"
echo "boosted=$BOOSTED" >> "$GITHUB_OUTPUT"
echo "max_issues=$MAX_ISSUES" >> "$GITHUB_OUTPUT"
echo "pr_guidance=$PR_GUIDANCE" >> "$GITHUB_OUTPUT"
# Export regression-driven focus overrides (areas with repeat issues)
FOCUS_OVERRIDES=$(jq -r '.regression_insights.focus_overrides // [] | join(",")' "$CONFIG")
echo "focus_overrides=$FOCUS_OVERRIDES" >> "$GITHUB_OUTPUT"
echo "Tuning config loaded — blocked: ${BLOCKED:-none}, boosted: ${BOOSTED:-none}, max_issues: ${MAX_ISSUES:-default}, focus_overrides: ${FOCUS_OVERRIDES:-none}"
else
echo "blocked=" >> "$GITHUB_OUTPUT"
echo "boosted=" >> "$GITHUB_OUTPUT"
echo "max_issues=0" >> "$GITHUB_OUTPUT"
echo "pr_guidance=" >> "$GITHUB_OUTPUT"
echo '{}' > /tmp/rotation-weights.json
echo "No tuning config found, running all checks"
fi
- name: Determine daily focus
id: focus
env:
FOCUS_OVERRIDE: ${{ inputs.focus_override }}
FOCUS_OVERRIDES_REGRESSION: ${{ steps.tuning.outputs.focus_overrides }}
run: |
OVERRIDE="$FOCUS_OVERRIDE"
if [ -n "$OVERRIDE" ] && [ "$OVERRIDE" != "none" ]; then
echo "area=$OVERRIDE" >> "$GITHUB_OUTPUT"
echo "Focus override: $OVERRIDE"
else
# All 8 focus areas
AREAS=("performance" "security" "a11y" "operator" "sre" "features" "resilience" "consistency")
# Parse regression-driven focus overrides (comma-separated list of areas
# where repeat issues were detected in the past 7 days)
IFS=',' read -ra REGRESSION_AREAS <<< "${FOCUS_OVERRIDES_REGRESSION:-}"
# Try weighted selection from tuning config
WEIGHTS_FILE="/tmp/rotation-weights.json"
HAS_WEIGHTS=false
if [ -f "$WEIGHTS_FILE" ] && [ "$(jq 'length' "$WEIGHTS_FILE" 2>/dev/null)" -gt 0 ]; then
HAS_WEIGHTS=true
fi
if [ "$HAS_WEIGHTS" = true ]; then
# Weighted random: build a pool where each area appears N times based on weight
# Weight 0 = skip, weight 1.0 = 10 entries, weight 2.0 = 20 entries, weight 0.5 = 5 entries
POOL=()
for AREA in "${AREAS[@]}"; do
W=$(jq -r --arg a "$AREA" '.[$a] // 1.0' "$WEIGHTS_FILE")
# Convert weight to integer pool entries (weight * 10)
ENTRIES=$(echo "$W * 10" | bc 2>/dev/null | cut -d. -f1)
ENTRIES=${ENTRIES:-10}
[ "$ENTRIES" -le 0 ] && continue
[ "$ENTRIES" -gt 20 ] && ENTRIES=20
# Boost areas flagged by regression detector (2x pool entries)
for REGAREA in "${REGRESSION_AREAS[@]}"; do
if [ "$AREA" = "$REGAREA" ]; then
ENTRIES=$((ENTRIES * 2))
[ "$ENTRIES" -gt 40 ] && ENTRIES=40
echo " Regression boost: $AREA (pool entries doubled to $ENTRIES)"
break
fi
done
for ((i=0; i<ENTRIES; i++)); do
POOL+=("$AREA")
done
done
if [ ${#POOL[@]} -gt 0 ]; then
# Use time-based seed for reproducible-per-hour selection
HOUR_SEED=$(date -u +%Y%m%d%H)
IDX=$(( ( $(echo "$HOUR_SEED" | cksum | cut -d' ' -f1) ) % ${#POOL[@]} ))
FOCUS="${POOL[$IDX]}"
echo "area=$FOCUS" >> "$GITHUB_OUTPUT"
echo "Weighted selection: $FOCUS (pool size: ${#POOL[@]}, seed: $HOUR_SEED)"
# Log weights for debugging
for AREA in "${AREAS[@]}"; do
W=$(jq -r --arg a "$AREA" '.[$a] // 1.0' "$WEIGHTS_FILE")
echo " $AREA: weight=$W"
done
if [ -n "$FOCUS_OVERRIDES_REGRESSION" ]; then
echo " Regression-boosted areas: $FOCUS_OVERRIDES_REGRESSION"
fi
else
# All weights are 0 — fall back to fixed rotation
DOY=$(date -u +%j)
SLOT=$(( 10#$DOY % 8 ))
FOCUS="${AREAS[$SLOT]}"
echo "area=$FOCUS" >> "$GITHUB_OUTPUT"
echo "All weights zero, fallback to slot $SLOT: $FOCUS"
fi
else
# No tuning config — use original fixed rotation
DOY=$(date -u +%j) # Day of year (1-366)
SLOT=$(( 10#$DOY % 8 )) # 10# forces base-10 (date +%j zero-pads, which bash reads as octal)
case $SLOT in
0) FOCUS="performance" ;;
1) FOCUS="security" ;;
2) FOCUS="a11y" ;;
3) FOCUS="operator" ;;
4) FOCUS="sre" ;;
5) FOCUS="features" ;;
6) FOCUS="resilience" ;;
7) FOCUS="consistency" ;;
esac
echo "area=$FOCUS" >> "$GITHUB_OUTPUT"
echo "Day-of-year $(date -u +%j) slot $SLOT focus: $FOCUS (no tuning weights)"
fi
fi
# ── Layer 1: Baseline Quality Checks ──────────────────────────
- name: "Check: TypeScript build"
id: build_check
working-directory: web
continue-on-error: true
env:
NODE_OPTIONS: '--max-old-space-size=6144'
run: npm run build 2>&1 | tee /tmp/build-output.txt
- name: "Check: ESLint"
id: lint_check
working-directory: web
continue-on-error: true
run: npm run lint 2>&1 | tee /tmp/lint-output.txt
- name: "Check: Go backend build"
id: go_build_check
continue-on-error: true
run: |
sudo apt-get install -y -qq gcc musl-tools > /dev/null 2>&1
CGO_ENABLED=1 go build -o /dev/null ./cmd/console 2>&1 | tee /tmp/go-build-output.txt
- name: "Check: Bundle size"
id: bundle_check
if: steps.build_check.outcome == 'success'
run: |
if [ -d "web/dist" ]; then
SIZE_KB=$(du -sk web/dist | cut -f1)
echo "size_kb=$SIZE_KB" >> "$GITHUB_OUTPUT"
if [ "$SIZE_KB" -gt "$BUNDLE_SIZE_LIMIT_KB" ]; then
echo "exceeded=true" >> "$GITHUB_OUTPUT"
echo "Bundle size ${SIZE_KB}KB exceeds limit ${BUNDLE_SIZE_LIMIT_KB}KB"
du -sh web/dist/assets/* 2>/dev/null | sort -rh | head -20 > /tmp/bundle-breakdown.txt
else
echo "exceeded=false" >> "$GITHUB_OUTPUT"
echo "Bundle size ${SIZE_KB}KB is within limit ${BUNDLE_SIZE_LIMIT_KB}KB"
fi
else
echo "exceeded=false" >> "$GITHUB_OUTPUT"
echo "No dist directory found"
fi
- name: "Check: npm audit"
id: audit_check
if: inputs.skip_audit != true
working-directory: web
run: |
AUDIT_EXIT=0
npm audit --audit-level=high --json > /tmp/audit-output.json 2>/dev/null || AUDIT_EXIT=$?
if [ "$AUDIT_EXIT" -ne 0 ] && ! jq -e '.metadata' /tmp/audit-output.json > /dev/null 2>&1; then
echo "First audit attempt failed (possibly network), retrying in 30s..."
sleep 30
npm audit --audit-level=high --json > /tmp/audit-output.json 2>/dev/null || true
fi
CRITICAL=$(jq '.metadata.vulnerabilities.critical // 0' /tmp/audit-output.json 2>/dev/null || echo "0")
HIGH=$(jq '.metadata.vulnerabilities.high // 0' /tmp/audit-output.json 2>/dev/null || echo "0")
TOTAL=$((CRITICAL + HIGH))
echo "critical=$CRITICAL" >> "$GITHUB_OUTPUT"
echo "high=$HIGH" >> "$GITHUB_OUTPUT"
if [ "$TOTAL" -gt "0" ]; then
echo "has_vulnerabilities=true" >> "$GITHUB_OUTPUT"
echo "Found $CRITICAL critical + $HIGH high vulnerabilities"
npm audit --audit-level=high 2>/dev/null | tail -50 > /tmp/audit-summary.txt || true
else
echo "has_vulnerabilities=false" >> "$GITHUB_OUTPUT"
echo "No high/critical vulnerabilities found"
fi
# ── Layer 2: Daily Focus Checks ───────────────────────────────
# === PERFORMANCE (Monday) ===
- name: "Focus: Unused dependencies"
id: focus_unused_deps
if: steps.focus.outputs.area == 'performance'
working-directory: web
continue-on-error: true
run: |
echo "Checking for potentially unused dependencies..."
ISSUES=""
# Get all dependencies from package.json
DEPS=$(jq -r '.dependencies // {} | keys[]' package.json)
# Packages used via dynamic import(), web workers, Netlify Functions,
# peer dependencies, or build tooling — not discoverable by grepping
# src/ for static import statements. Maintain this list when adding
# new indirect deps to avoid false-positive auto-QA issues.
INDIRECT_DEPS="@netlify/blobs @sqlite.org/sqlite-wasm fflate googleapis react-is sucrase"
for dep in $DEPS; do
# Skip known framework deps that won't appear as direct imports
case "$dep" in
react|react-dom|react-scripts|vite|@vitejs/*|@types/*|typescript) continue ;;
esac
# Skip packages on the indirect-dependency allowlist
SKIP=false
for indirect in $INDIRECT_DEPS; do
if [ "$dep" = "$indirect" ]; then
SKIP=true
break
fi
done
if [ "$SKIP" = "true" ]; then
continue
fi
# Search for import/require in src/ AND netlify/functions/
COUNT=$(grep -rl "from ['\"]${dep}" src/ netlify/functions/ 2>/dev/null | wc -l || echo "0")
COUNT2=$(grep -rl "require(['\"]${dep}" src/ netlify/functions/ 2>/dev/null | wc -l || echo "0")
# Also check for dynamic import() calls
COUNT3=$(grep -rl "import(['\"]${dep}" src/ netlify/functions/ 2>/dev/null | wc -l || echo "0")
TOTAL=$((COUNT + COUNT2 + COUNT3))
if [ "$TOTAL" -eq 0 ]; then
ISSUES="${ISSUES} - \`${dep}\` — no direct imports found in src/ or netlify/functions/\n"
fi
done
if [ -n "$ISSUES" ]; then
echo "found=true" >> "$GITHUB_OUTPUT"
printf "Potentially unused dependencies:\n%b" "$ISSUES" > /tmp/focus-unused-deps.txt
cat /tmp/focus-unused-deps.txt
else
echo "found=false" >> "$GITHUB_OUTPUT"
echo "No unused dependencies detected"
fi
- name: "Focus: Missing lazy loading"
id: focus_lazy_loading
if: steps.focus.outputs.area == 'performance'
working-directory: web
continue-on-error: true
run: |
echo "Checking for large components that could benefit from lazy loading..."
ISSUES=""
# Find .tsx files larger than 300 lines in pages/ or views/ directories (excluding tests)
for f in $(find src -path "*/pages/*.tsx" -o -path "*/views/*.tsx" 2>/dev/null | grep -v "\.test\.tsx$" | grep -v "/__tests__/" | grep -v "\.actions\.tsx$" | grep -v "\.tabs\.tsx$" | grep -v "Section\.tsx$"); do
LINES=$(wc -l < "$f")
if [ "$LINES" -gt 300 ]; then
BASENAME=$(basename "$f")
# Check if it's already lazy-loaded (matches React.lazy, lazy, and safeLazy patterns)
LAZY=$(grep -rl -E "(React\.lazy|safeLazy|lazy).*${BASENAME%.*}" src/ 2>/dev/null | wc -l || echo "0")
if [ "$LAZY" -eq 0 ]; then
ISSUES="${ISSUES} - \`${f}\` (${LINES} lines) — not lazy-loaded\n"
fi
fi
done
if [ -n "$ISSUES" ]; then
echo "found=true" >> "$GITHUB_OUTPUT"
printf "Large components without lazy loading:\n%b" "$ISSUES" > /tmp/focus-lazy-loading.txt
cat /tmp/focus-lazy-loading.txt
else
echo "found=false" >> "$GITHUB_OUTPUT"
echo "No lazy loading gaps detected"
fi
- name: "Focus: Bundle chunk analysis"
id: focus_bundle_analysis
if: steps.focus.outputs.area == 'performance' && steps.build_check.outcome == 'success'
continue-on-error: true
run: |
echo "Analyzing bundle chunks for large assets..."
ISSUES=""
if [ -d "web/dist/assets" ]; then
# Find JS chunks over 300KB
LARGE_CHUNKS=$(find web/dist/assets -name "*.js" -size +300k 2>/dev/null | while IFS= read -r f; do
SIZE=$(du -sh "$f" | cut -f1)
echo " - $(basename "$f") (${SIZE})"
done | sort | head -20 || true)
if [ -n "$LARGE_CHUNKS" ]; then
ISSUES="${ISSUES}### JS chunks over 300KB\nLarge chunks increase initial load time — consider code splitting:\n${LARGE_CHUNKS}\n\n"
fi
# Top 10 largest assets
TOP_ASSETS=$(du -sh web/dist/assets/* 2>/dev/null | sort -rh | head -10 | \
awk '{gsub(/web\/dist\/assets\//, "", $2); print " - " $2 " (" $1 ")"}' || true)
if [ -n "$TOP_ASSETS" ]; then
ISSUES="${ISSUES}### Top 10 largest bundle assets\n${TOP_ASSETS}\n\n"
fi
CHUNK_COUNT=$(find web/dist/assets -name "*.js" 2>/dev/null | wc -l | tr -d ' ')
TOTAL_KB=$(du -sk web/dist 2>/dev/null | cut -f1)
ISSUES="${ISSUES}### Bundle summary\n - Total size: ${TOTAL_KB}KB\n - JS chunk count: ${CHUNK_COUNT}\n"
else
echo "No dist directory found — skipping bundle analysis"
fi
if echo "$ISSUES" | grep -q "JS chunks over"; then
echo "found=true" >> "$GITHUB_OUTPUT"
else
echo "found=false" >> "$GITHUB_OUTPUT"
fi
if [ -n "$ISSUES" ]; then
printf "%b" "$ISSUES" > /tmp/focus-bundle-analysis.txt
cat /tmp/focus-bundle-analysis.txt
fi
- name: "Focus: Large source files"
id: focus_large_files
if: steps.focus.outputs.area == 'performance'
working-directory: web
continue-on-error: true
run: |
echo "Checking for oversized source files..."
ISSUES=""
# Find TypeScript/TSX files over 500 lines
LARGE=$(find src -name "*.ts" -o -name "*.tsx" 2>/dev/null | while IFS= read -r f; do
LINES=$(wc -l < "$f" 2>/dev/null || echo "0")
if [ "$LINES" -gt 500 ]; then
echo "${LINES} ${f}"
fi
done | sort -rn | head -20 || true)
if [ -n "$LARGE" ]; then
COUNT=$(echo "$LARGE" | wc -l | tr -d ' ')
FORMATTED=$(echo "$LARGE" | awk '{print " - `" $2 "` (" $1 " lines)"}')
ISSUES="${ISSUES}### Source files over 500 lines (${COUNT} found)\nLarge files are harder to maintain and may impact compile performance:\n${FORMATTED}\n"
fi
if [ -n "$ISSUES" ]; then
echo "found=true" >> "$GITHUB_OUTPUT"
printf "%b" "$ISSUES" > /tmp/focus-large-files.txt
cat /tmp/focus-large-files.txt
else
echo "found=false" >> "$GITHUB_OUTPUT"
echo "No oversized source files detected"
fi
- name: "Focus: Import cost and tree-shaking"
id: focus_import_cost
if: steps.focus.outputs.area == 'performance'
working-directory: web
continue-on-error: true
run: |
echo "Checking for import patterns that prevent tree-shaking..."
ISSUES=""
# Namespace imports prevent tree-shaking
STAR_IMPORTS=$(grep -rn "import \* as" src/ --include="*.ts" --include="*.tsx" 2>/dev/null | \
grep -v "node_modules\|\.test\." | head -15 || true)
if [ -n "$STAR_IMPORTS" ]; then
COUNT=$(echo "$STAR_IMPORTS" | wc -l | tr -d ' ')
ISSUES="${ISSUES}### Namespace imports (\`import * as\`) that prevent tree-shaking (${COUNT} found)\nUse named imports instead — e.g. \`import { specific } from 'lib'\`:\n\`\`\`\n${STAR_IMPORTS}\n\`\`\`\n\n"
fi
# Full lodash imports pull in the entire library
LODASH_FULL=$(grep -rn "from 'lodash'" src/ --include="*.ts" --include="*.tsx" 2>/dev/null | \
grep -v "node_modules\|\.test\." | head -10 || true)
if [ -n "$LODASH_FULL" ]; then
ISSUES="${ISSUES}### Full lodash import (imports entire library)\nUse per-method imports instead — e.g. \`import debounce from 'lodash/debounce'\`:\n\`\`\`\n${LODASH_FULL}\n\`\`\`\n\n"
fi
if echo "$ISSUES" | grep -q "Namespace imports\|Full lodash"; then
echo "found=true" >> "$GITHUB_OUTPUT"
else
echo "found=false" >> "$GITHUB_OUTPUT"
fi
if [ -n "$ISSUES" ]; then
printf "%b" "$ISSUES" > /tmp/focus-import-cost.txt
cat /tmp/focus-import-cost.txt
else
echo "No import cost issues detected"
fi
# === SECURITY (Tuesday) ===
- name: "Focus: Hardcoded URLs and tokens"
id: focus_hardcoded
if: steps.focus.outputs.area == 'security'
working-directory: web
continue-on-error: true
run: |
echo "Scanning for hardcoded URLs, tokens, and secrets..."
ISSUES=""
# Look for hardcoded API URLs (not localhost, not relative)
# Exclude known-safe patterns:
# - config/externalApis.ts (contains intentional documentation URLs)
# - mocks/ directory (test/demo data only)
# - Lines with "SECURITY: Safe" comments
# - Lines with "example-org" (mock data)
# - example.com domains
URLS=$(grep -rn "https\?://[^localhost][^'\"]*api" src/ --include="*.ts" --include="*.tsx" 2>/dev/null | \
grep -v node_modules | \
grep -v "\.test\." | \
grep -v "config/externalApis.ts" | \
grep -v "mocks/" | \
grep -v "SECURITY: Safe" | \
grep -v "example-org" | \
grep -v "example\.com" | \
head -20 || true)
if [ -n "$URLS" ]; then
ISSUES="${ISSUES}### Hardcoded API URLs\n\`\`\`\n${URLS}\n\`\`\`\n\n"
fi
# Look for potential secrets/tokens
# Exclude known-safe patterns:
# - mocks/handlers.ts (contains mock tokens for testing)
# - Lines with "NOT A REAL TOKEN" comments
# - Lines with "mock" in the token value
TOKENS=$(grep -rn "token\s*[:=]\s*['\"][A-Za-z0-9]" src/ --include="*.ts" --include="*.tsx" 2>/dev/null | \
grep -v node_modules | \
grep -v "\.test\." | \
grep -v "mocks/handlers.ts" | \
grep -v "NOT A REAL TOKEN" | \
grep -v "mock-.*-token" | \
grep -vi "csrf" | \
grep -vi "type" | \
head -10 || true)
if [ -n "$TOKENS" ]; then
ISSUES="${ISSUES}### Potential Hardcoded Tokens\n\`\`\`\n${TOKENS}\n\`\`\`\n\n"
fi
# Look for hardcoded passwords
# Exclude known-safe patterns similar to tokens
PASSWORDS=$(grep -rn "password\s*[:=]\s*['\"][^'\"]\+" src/ --include="*.ts" --include="*.tsx" 2>/dev/null | \
grep -v node_modules | \
grep -v "\.test\." | \
grep -v "mocks/" | \
grep -vi "type\|interface\|placeholder\|label\|name=" | \
head -10 || true)
if [ -n "$PASSWORDS" ]; then
ISSUES="${ISSUES}### Potential Hardcoded Passwords\n\`\`\`\n${PASSWORDS}\n\`\`\`\n\n"
fi
if [ -n "$ISSUES" ]; then
echo "found=true" >> "$GITHUB_OUTPUT"
printf "%b" "$ISSUES" > /tmp/focus-hardcoded.txt
echo "Potential security issues found"
else
echo "found=false" >> "$GITHUB_OUTPUT"
echo "No hardcoded credentials detected"
fi
- name: "Focus: Dependency age check"
id: focus_dep_age
if: steps.focus.outputs.area == 'security'
working-directory: web
continue-on-error: true
run: |
echo "Checking for outdated dependencies..."
npm outdated --json > /tmp/outdated.json 2>/dev/null || true
MAJOR_OUTDATED=$(jq -r 'to_entries[] | select(.value.current != .value.latest) | select((.value.current | split(".")[0]) != (.value.latest | split(".")[0])) | "\(.key): \(.value.current) → \(.value.latest)"' /tmp/outdated.json 2>/dev/null | head -20 || true)
if [ -n "$MAJOR_OUTDATED" ]; then
echo "found=true" >> "$GITHUB_OUTPUT"
echo "Dependencies with major version updates available:" > /tmp/focus-dep-age.txt
echo "$MAJOR_OUTDATED" >> /tmp/focus-dep-age.txt
cat /tmp/focus-dep-age.txt
else
echo "found=false" >> "$GITHUB_OUTPUT"
echo "No major version gaps detected"
fi
# === NAVIGATION & ACCESSIBILITY (Wednesday) ===
- name: "Focus: Missing ARIA labels"
id: focus_aria
if: steps.focus.outputs.area == 'a11y'
working-directory: web
continue-on-error: true
run: |
echo "Checking for interactive elements missing ARIA labels..."
ISSUES=""
# Find buttons without aria-label or children text
BUTTONS=$(grep -rn "<button" src/ --include="*.tsx" 2>/dev/null | grep -v "aria-label" | grep -v "aria-labelledby" | grep -v node_modules | head -20 || true)
if [ -n "$BUTTONS" ]; then
ISSUES="${ISSUES}### Buttons possibly missing ARIA labels\n\`\`\`\n${BUTTONS}\n\`\`\`\n\n"
fi
# Find icon-only buttons (button with only an icon child)
ICON_BUTTONS=$(grep -rn "<button.*>" src/ --include="*.tsx" 2>/dev/null | grep -i "icon\|svg" | grep -v "aria-label" | grep -v node_modules | head -10 || true)
if [ -n "$ICON_BUTTONS" ]; then
ISSUES="${ISSUES}### Icon-only buttons without ARIA labels\n\`\`\`\n${ICON_BUTTONS}\n\`\`\`\n\n"
fi
# Find images without alt text
IMAGES=$(grep -rn "<img" src/ --include="*.tsx" 2>/dev/null | grep -v 'alt=' | grep -v node_modules | head -10 || true)
if [ -n "$IMAGES" ]; then
ISSUES="${ISSUES}### Images without alt text\n\`\`\`\n${IMAGES}\n\`\`\`\n\n"
fi
if [ -n "$ISSUES" ]; then
echo "found=true" >> "$GITHUB_OUTPUT"
printf "%b" "$ISSUES" > /tmp/focus-aria.txt
echo "Accessibility issues found"
else
echo "found=false" >> "$GITHUB_OUTPUT"
echo "No ARIA label issues detected"
fi
- name: "Focus: Keyboard navigation gaps"
id: focus_keyboard
if: steps.focus.outputs.area == 'a11y'
working-directory: web
continue-on-error: true
run: |
echo "Checking for keyboard navigation issues..."
ISSUES=""
# onClick without onKeyDown/onKeyPress
CLICK_NO_KEY=$(grep -rn "onClick=" src/ --include="*.tsx" 2>/dev/null | grep -v "onKey" | grep -v "<button" | grep -v "<a " | grep -v "<input" | grep -v "<select" | grep -v node_modules | head -15 || true)
if [ -n "$CLICK_NO_KEY" ]; then
ISSUES="${ISSUES}### Click handlers without keyboard equivalents\n\`\`\`\n${CLICK_NO_KEY}\n\`\`\`\n\n"
fi
# Divs with onClick (should be buttons)
DIV_CLICK=$(grep -rn "<div.*onClick" src/ --include="*.tsx" 2>/dev/null | grep -v 'role=' | grep -v 'tabIndex' | grep -v node_modules | head -10 || true)
if [ -n "$DIV_CLICK" ]; then
ISSUES="${ISSUES}### Clickable divs without role or tabIndex\n\`\`\`\n${DIV_CLICK}\n\`\`\`\n\n"
fi
if [ -n "$ISSUES" ]; then
echo "found=true" >> "$GITHUB_OUTPUT"
printf "%b" "$ISSUES" > /tmp/focus-keyboard.txt
echo "Keyboard navigation gaps found"
else
echo "found=false" >> "$GITHUB_OUTPUT"
echo "No keyboard navigation gaps detected"
fi
- name: "Focus: Color contrast risk patterns"
id: focus_color_contrast
if: steps.focus.outputs.area == 'a11y'
working-directory: web
continue-on-error: true
run: |
echo "Checking for potential color contrast issues..."
ISSUES=""
# Light gray text on potentially light backgrounds (likely below WCAG AA 4.5:1)
LOW_CONTRAST=$(grep -rn "text-gray-[23]00\b\|text-slate-[23]00\b\|text-zinc-[23]00\b\|text-neutral-[23]00\b" src/ --include="*.tsx" 2>/dev/null | \
grep -v "dark:\|node_modules\|\.test\." | head -20 || true)
if [ -n "$LOW_CONTRAST" ]; then
COUNT=$(echo "$LOW_CONTRAST" | wc -l | tr -d ' ')
ISSUES="${ISSUES}### Light gray text classes (${COUNT} found) — may fail WCAG AA on white/light backgrounds\nGray-200/300 text on light backgrounds often falls below the 4.5:1 contrast ratio:\n\`\`\`\n${LOW_CONTRAST}\n\`\`\`\n\n"
fi
# Text with opacity that could reduce contrast below threshold
OPACITY_TEXT=$(grep -rn "text-opacity-[1-5][0-9]\b\|text-[a-z]\+-[0-9]\+\/[1-5][0-9]\b" src/ --include="*.tsx" 2>/dev/null | \
grep "text-" | grep -v "node_modules\|\.test\." | head -15 || true)
if [ -n "$OPACITY_TEXT" ]; then
ISSUES="${ISSUES}### Text with reduced opacity — may fall below WCAG AA contrast\nOpacity below 60% on colored text can cause contrast failures:\n\`\`\`\n${OPACITY_TEXT}\n\`\`\`\n\n"
fi
# Inline color styles that bypass design tokens and may not have been contrast-checked
INLINE_COLOR=$(grep -rn "color:\s*['\"]#\|color:\s*rgb" src/ --include="*.tsx" 2>/dev/null | \
grep -v "node_modules\|\.test\.\|var(--\|backgroundColor" | head -15 || true)
if [ -n "$INLINE_COLOR" ]; then
COUNT=$(echo "$INLINE_COLOR" | wc -l | tr -d ' ')
if [ "$COUNT" -gt 3 ]; then
ISSUES="${ISSUES}### Inline color styles (${COUNT} found) — contrast not validated by design system\nUse Tailwind classes or CSS variables so contrast can be centrally verified:\n\`\`\`\n${INLINE_COLOR}\n\`\`\`\n\n"
fi
fi
if [ -n "$ISSUES" ]; then
echo "found=true" >> "$GITHUB_OUTPUT"
printf "%b" "$ISSUES" > /tmp/focus-color-contrast.txt
echo "Color contrast risks found"
else
echo "found=false" >> "$GITHUB_OUTPUT"
echo "No color contrast issues detected"
fi
# === OPERATOR USEFULNESS (Thursday) ===
- name: "Focus: Hardcoded thresholds and magic numbers"
id: focus_magic_numbers
if: steps.focus.outputs.area == 'operator'
working-directory: web
continue-on-error: true
run: |
echo "Checking for hardcoded thresholds and magic numbers..."
ISSUES=""
# Find numeric comparisons that look like thresholds
THRESHOLDS=$(grep -rn "[><=]\s*[0-9]\{2,\}" src/ --include="*.ts" --include="*.tsx" 2>/dev/null | grep -v node_modules | grep -v "\.test\." | grep -v "index\." | grep -v "z-index" | grep -v "width\|height\|padding\|margin\|size\|px\|rem\|em" | head -20 || true)
if [ -n "$THRESHOLDS" ]; then
ISSUES="${ISSUES}### Numeric thresholds that could be configurable\n\`\`\`\n${THRESHOLDS}\n\`\`\`\n\n"
fi
# Find setTimeout/setInterval with magic numbers
TIMERS=$(grep -rn "setTimeout\|setInterval" src/ --include="*.ts" --include="*.tsx" 2>/dev/null | grep -o "[0-9]\{4,\}" | sort -u | head -10 || true)
if [ -n "$TIMERS" ]; then
ISSUES="${ISSUES}### Timer values that could be named constants\nValues found: ${TIMERS}\n\n"
fi
if [ -n "$ISSUES" ]; then
echo "found=true" >> "$GITHUB_OUTPUT"
printf "%b" "$ISSUES" > /tmp/focus-magic-numbers.txt
echo "Magic numbers found"
else
echo "found=false" >> "$GITHUB_OUTPUT"
echo "No magic numbers detected"
fi
- name: "Focus: Missing tooltips and help text"
id: focus_tooltips
if: steps.focus.outputs.area == 'operator'
working-directory: web
continue-on-error: true
run: |
echo "Checking for technical abbreviations rendered as JSX text without a wrapping acronym/tooltip..."
ISSUES=""
# Issue 8970: the previous grep matched abbreviations anywhere
# (identifiers, state-machine strings, comparisons, type names),
# producing ~92% false positives (30-day Copilot acceptance 8%).
# Constrain to JSX text — an abbreviation appearing directly between
# > and < — which is what the user actually sees and what a tooltip
# would annotate. Skip matches already wrapped in <TechnicalAcronym>
# (the project's established affordance for acronym explanation).
ABBREV_PATTERN='CPU|RAM|OOM|CRD|RBAC|PVC|PV|HPA|VPA|SLO|SLI|SLA|MTTR|MTTF'
ABBREV=$(grep -rnP ">\s*(${ABBREV_PATTERN})\b[^<]{0,60}<" src/ --include="*.tsx" 2>/dev/null \
| grep -v node_modules \
| grep -v "\.test\." \
| grep -v "\.stories\." \
| grep -v "TechnicalAcronym" \
| head -15 || true)
if [ -n "$ABBREV" ]; then
ISSUES="${ISSUES}### Technical abbreviations rendered as JSX text without a wrapping acronym/tooltip\nWrap each in \`<TechnicalAcronym term=\"...\">\` (see \`web/src/components/ui/TechnicalAcronym.tsx\`) so hovering explains the term.\n\`\`\`\n${ABBREV}\n\`\`\`\n\n"
fi
if [ -n "$ISSUES" ]; then
echo "found=true" >> "$GITHUB_OUTPUT"
printf "%b" "$ISSUES" > /tmp/focus-tooltips.txt
echo "Missing tooltips found"
else
echo "found=false" >> "$GITHUB_OUTPUT"
echo "No tooltip gaps detected"
fi
# === SRE / MULTI-CLUSTER (Friday) ===
- name: "Focus: Single-cluster assumptions"
id: focus_single_cluster
if: steps.focus.outputs.area == 'sre'
working-directory: web
continue-on-error: true
run: |
echo "Checking for single-cluster-assumption red flags..."
ISSUES=""
# Issue 8970: the previous heuristics flagged legitimate per-cluster
# code (any .map(cluster => ...) rendering a list, any occurrence of
# the word "cluster" not followed by "s") as bugs, producing ~91%
# false positives (30-day Copilot acceptance 9%). Narrow to concrete
# red flags that reliably indicate assuming exactly one cluster:
# 1) Indexing [0] on a clusters-array variable (picking the first
# cluster silently — the known failure mode, see GitOps.tsx).
# 2) Hardcoded cluster-name comparisons against literal strings.
FIRST_CLUSTER=$(grep -rnP '\b(clusters|deduplicatedClusters|reachableClusters|activeClusters)\s*\[\s*0\s*\]' src/ --include="*.ts" --include="*.tsx" 2>/dev/null \
| grep -v node_modules \
| grep -v "\.test\." \
| grep -v "\.spec\." \
| grep -vE ":[0-9]+:\s*(//|\*)" \
| head -10 || true)
if [ -n "$FIRST_CLUSTER" ]; then
ISSUES="${ISSUES}### Picking the first cluster with \`[0]\`\nThese silently ignore all but the first cluster. Either iterate all clusters or make the choice explicit (e.g., a user-selected cluster prop).\n\`\`\`\n${FIRST_CLUSTER}\n\`\`\`\n\n"
fi
HARDCODED_CLUSTER=$(grep -rnP "clusterName\s*(===|==|!==|!=)\s*['\"][a-z][a-z0-9_-]{1,}['\"]" src/ --include="*.ts" --include="*.tsx" 2>/dev/null \
| grep -v node_modules \
| grep -v "\.test\." \
| grep -v "\.spec\." \
| head -10 || true)
if [ -n "$HARDCODED_CLUSTER" ]; then
ISSUES="${ISSUES}### Hardcoded cluster-name comparisons\nMatching against a literal cluster name breaks when the same code runs in clusters with different names.\n\`\`\`\n${HARDCODED_CLUSTER}\n\`\`\`\n\n"
fi
if [ -n "$ISSUES" ]; then
echo "found=true" >> "$GITHUB_OUTPUT"
printf "%b" "$ISSUES" > /tmp/focus-single-cluster.txt
echo "Single-cluster assumptions found"
else
echo "found=false" >> "$GITHUB_OUTPUT"
echo "No single-cluster assumptions detected"
fi
- name: "Focus: Missing health indicators"
id: focus_health
if: steps.focus.outputs.area == 'sre'
working-directory: web
continue-on-error: true
run: |
echo "Checking for missing health/status indicators in SRE views..."
ISSUES=""
# Find dashboard/overview components without health status
DASHBOARDS=$(find src -name "*Dashboard*" -o -name "*Overview*" -o -name "*Summary*" 2>/dev/null | grep -v node_modules | grep "\.tsx$")
for f in $DASHBOARDS; do
HAS_HEALTH=$(grep -l "health\|Health\|status\|Status\|alert\|Alert\|warning\|Warning" "$f" 2>/dev/null || true)
if [ -z "$HAS_HEALTH" ]; then
ISSUES="${ISSUES} - \`${f}\` — dashboard/overview without health indicators\n"
fi
done
if [ -n "$ISSUES" ]; then
echo "found=true" >> "$GITHUB_OUTPUT"
printf "Dashboard components missing health indicators:\n%b" "$ISSUES" > /tmp/focus-health.txt
cat /tmp/focus-health.txt
else
echo "found=false" >> "$GITHUB_OUTPUT"
echo "No missing health indicators detected"
fi
# === FEATURE RECOMMENDATIONS (Saturday) ===
- name: "Focus: TODO/FIXME/HACK comments"
id: focus_todos
if: steps.focus.outputs.area == 'features'
working-directory: web
continue-on-error: true
run: |
echo "Scanning for TODO, FIXME, HACK comments..."
ISSUES=""
TODOS=$(grep -rn "TODO\|FIXME\|HACK\|XXX\|WORKAROUND" src/ --include="*.ts" --include="*.tsx" 2>/dev/null | grep -v node_modules | head -30 || true)
if [ -n "$TODOS" ]; then
COUNT=$(echo "$TODOS" | wc -l)
echo "found=true" >> "$GITHUB_OUTPUT"
echo "count=$COUNT" >> "$GITHUB_OUTPUT"
echo "Found ${COUNT} TODO/FIXME/HACK comments:" > /tmp/focus-todos.txt
echo "$TODOS" >> /tmp/focus-todos.txt
cat /tmp/focus-todos.txt
else
echo "found=false" >> "$GITHUB_OUTPUT"
echo "No TODO/FIXME/HACK comments found"
fi
- name: "Focus: High-complexity components"
id: focus_complexity
if: steps.focus.outputs.area == 'features'
working-directory: web
continue-on-error: true
run: |
echo "Finding high-complexity components..."
ISSUES=""
# Components over 400 lines are candidates for splitting
for f in $(find src -name "*.tsx" 2>/dev/null | grep -v node_modules); do
LINES=$(wc -l < "$f")
if [ "$LINES" -gt 400 ]; then
# Count useState hooks as complexity indicator
HOOKS=$(grep -c "useState\|useEffect\|useCallback\|useMemo\|useRef" "$f" 2>/dev/null || echo "0")
ISSUES="${ISSUES} - \`${f}\` — ${LINES} lines, ${HOOKS} hooks\n"
fi
done
if [ -n "$ISSUES" ]; then
echo "found=true" >> "$GITHUB_OUTPUT"
printf "Components that could benefit from splitting:\n%b" "$ISSUES" > /tmp/focus-complexity.txt
cat /tmp/focus-complexity.txt
else
echo "found=false" >> "$GITHUB_OUTPUT"
echo "No overly complex components detected"
fi
# === CODE QUALITY (Saturday - part of features) ===
- name: "Focus: Debug console.log statements"
id: focus_console_logs
if: steps.focus.outputs.area == 'features'
working-directory: web
continue-on-error: true
run: |
echo "Scanning for debug console.log statements..."
ISSUES=""
# Find console.log that looks like debugging (not error handling)
DEBUG_LOGS=$(grep -rn "console\.log" src/ --include="*.ts" --include="*.tsx" 2>/dev/null | \
grep -v "node_modules" | \
grep -v "// eslint-disable" | \
grep -v "error\|Error\|warn\|Warn\|debug mode" | \
head -20 || true)
if [ -n "$DEBUG_LOGS" ]; then
COUNT=$(echo "$DEBUG_LOGS" | wc -l | tr -d ' ')
echo "found=true" >> "$GITHUB_OUTPUT"
echo "count=$COUNT" >> "$GITHUB_OUTPUT"
printf "Debug console.log statements found (${COUNT} occurrences):\n\`\`\`\n%s\n\`\`\`\n" "$DEBUG_LOGS" > /tmp/focus-console-logs.txt
cat /tmp/focus-console-logs.txt
else
echo "found=false" >> "$GITHUB_OUTPUT"
echo "No debug console.log statements found"
fi
- name: "Focus: Excessive any types"
id: focus_any_types
if: steps.focus.outputs.area == 'features'
working-directory: web
continue-on-error: true
run: |
echo "Scanning for excessive use of 'any' type..."
ISSUES=""
# Find explicit 'any' type annotations (excluding eslint comments)
ANY_TYPES=$(grep -rn ": any\|: any\[\]\|as any\|<any>" src/ --include="*.ts" --include="*.tsx" 2>/dev/null | \
grep -v "node_modules" | \
grep -v "eslint-disable" | \
grep -v "\.d\.ts" | \
head -25 || true)
if [ -n "$ANY_TYPES" ]; then
COUNT=$(echo "$ANY_TYPES" | wc -l | tr -d ' ')
if [ "$COUNT" -gt 10 ]; then
echo "found=true" >> "$GITHUB_OUTPUT"
echo "count=$COUNT" >> "$GITHUB_OUTPUT"
printf "Excessive 'any' type usage (${COUNT} occurrences) reduces type safety:\n\`\`\`\n%s\n\`\`\`\n" "$ANY_TYPES" > /tmp/focus-any-types.txt
cat /tmp/focus-any-types.txt
else
echo "found=false" >> "$GITHUB_OUTPUT"
echo "Only ${COUNT} 'any' types found (acceptable)"
fi
else
echo "found=false" >> "$GITHUB_OUTPUT"
echo "No excessive 'any' types found"
fi
- name: "Focus: Potential memory leaks"
id: focus_memory_leaks
if: steps.focus.outputs.area == 'features'
working-directory: web
continue-on-error: true
run: |
echo "Checking for potential memory leak patterns..."
ISSUES=""
# Find useEffect with setInterval/setTimeout but no cleanup
for f in $(find src -name "*.tsx" 2>/dev/null | grep -v node_modules); do
HAS_TIMER=$(grep -l "setInterval\|setTimeout" "$f" 2>/dev/null || true)
if [ -n "$HAS_TIMER" ]; then
HAS_CLEANUP=$(grep -l "clearInterval\|clearTimeout\|return.*=>" "$f" 2>/dev/null || true)
if [ -z "$HAS_CLEANUP" ]; then
ISSUES="${ISSUES} - \`${f}\` — uses setInterval/setTimeout without cleanup\n"
fi
fi
done
# Find addEventListener without removeEventListener
for f in $(find src -name "*.tsx" 2>/dev/null | grep -v node_modules); do
ADD_COUNT=$(grep -c "addEventListener" "$f" 2>/dev/null || echo "0")
REMOVE_COUNT=$(grep -c "removeEventListener" "$f" 2>/dev/null || echo "0")
if [ "$ADD_COUNT" -gt 0 ] && [ "$REMOVE_COUNT" -lt "$ADD_COUNT" ]; then
ISSUES="${ISSUES} - \`${f}\` — adds ${ADD_COUNT} event listeners but only removes ${REMOVE_COUNT}\n"
fi
done
if [ -n "$ISSUES" ]; then
echo "found=true" >> "$GITHUB_OUTPUT"
printf "Potential memory leak patterns:\n%b\n\nThese components may leak memory by not cleaning up timers or event listeners." "$ISSUES" > /tmp/focus-memory-leaks.txt
cat /tmp/focus-memory-leaks.txt
else
echo "found=false" >> "$GITHUB_OUTPUT"
echo "No obvious memory leak patterns detected"
fi
# === RESILIENCE & ERROR HANDLING (every run) ===
- name: "Check: Swallowed errors"
id: focus_swallowed
working-directory: web
continue-on-error: true
run: |
echo "Checking for swallowed errors and empty catch blocks..."
ISSUES=""
# Empty catch blocks
EMPTY_CATCH=$(grep -rn -A1 "catch\s*(" src/ --include="*.ts" --include="*.tsx" 2>/dev/null | grep -B1 "^\s*}" | grep "catch" | grep -v node_modules | head -15 || true)
if [ -n "$EMPTY_CATCH" ]; then
ISSUES="${ISSUES}### Empty catch blocks\n\`\`\`\n${EMPTY_CATCH}\n\`\`\`\n\n"
fi
# Catch with only console.log (no user feedback)
CONSOLE_CATCH=$(grep -rn -A2 "catch\s*(" src/ --include="*.ts" --include="*.tsx" 2>/dev/null | grep "console\.\(log\|warn\)" | grep -v node_modules | head -15 || true)
if [ -n "$CONSOLE_CATCH" ]; then
ISSUES="${ISSUES}### Catch blocks with only console logging (no user feedback)\n\`\`\`\n${CONSOLE_CATCH}\n\`\`\`\n\n"
fi
if [ -n "$ISSUES" ]; then
echo "found=true" >> "$GITHUB_OUTPUT"
printf "%b" "$ISSUES" > /tmp/focus-swallowed.txt
echo "Swallowed errors found"
else
echo "found=false" >> "$GITHUB_OUTPUT"
echo "No swallowed errors detected"
fi
- name: "Check: Missing loading and error states"
id: focus_loading_states
working-directory: web
continue-on-error: true
run: |
echo "Checking for missing loading/error states in data-fetching components..."
ISSUES=""
# Find fetch/useEffect with data fetching but no loading state
for f in $(find src -name "*.tsx" 2>/dev/null | grep -v node_modules); do
HAS_FETCH=$(grep -l "fetch\|axios\|useQuery\|useSWR\|useEffect.*fetch\|api\." "$f" 2>/dev/null || true)
if [ -n "$HAS_FETCH" ]; then
HAS_LOADING=$(grep -l "loading\|Loading\|isLoading\|spinner\|Spinner\|skeleton\|Skeleton" "$f" 2>/dev/null || true)
HAS_ERROR=$(grep -l "error\|Error\|isError\|errorMessage\|ErrorBoundary" "$f" 2>/dev/null || true)
MISSING=""
[ -z "$HAS_LOADING" ] && MISSING="loading"
[ -z "$HAS_ERROR" ] && MISSING="${MISSING:+$MISSING, }error"
if [ -n "$MISSING" ]; then
ISSUES="${ISSUES} - \`${f}\` — fetches data but missing ${MISSING} state\n"
fi
fi
done
if [ -n "$ISSUES" ]; then
echo "found=true" >> "$GITHUB_OUTPUT"
printf "Components fetching data without proper loading/error states:\n%b" "$ISSUES" > /tmp/focus-loading-states.txt
cat /tmp/focus-loading-states.txt
else
echo "found=false" >> "$GITHUB_OUTPUT"
echo "No missing loading/error states detected"
fi
# === GOVERNANCE & ROADMAP (every run) ===
- name: "Check: ROADMAP.md governance"
id: focus_roadmap
continue-on-error: true
run: |
echo "Checking ROADMAP.md governance health..."
ISSUES=""
HAS_ERRORS=false
if [ ! -f "ROADMAP.md" ]; then
ISSUES="${ISSUES}### ROADMAP.md not found\nCNCF incubation requires a public roadmap document.\n\n"
HAS_ERRORS=true
else
# Check if ROADMAP.md has been updated in the last 90 days
LAST_MODIFIED=$(git log -1 --format="%ci" -- ROADMAP.md 2>/dev/null || echo "")
if [ -n "$LAST_MODIFIED" ]; then
STALE_THRESHOLD_DAYS=90
LAST_EPOCH=$(date -d "$LAST_MODIFIED" +%s 2>/dev/null || date -jf "%Y-%m-%d %H:%M:%S %z" "$LAST_MODIFIED" +%s 2>/dev/null || echo "0")
DAYS_AGO=$(( ( $(date +%s) - LAST_EPOCH ) / 86400 ))
if [ "$DAYS_AGO" -gt "$STALE_THRESHOLD_DAYS" ]; then
ISSUES="${ISSUES}### ROADMAP.md is stale\nLast updated ${DAYS_AGO} days ago (threshold: ${STALE_THRESHOLD_DAYS} days). Roadmap should be reviewed and updated quarterly.\n\n"
HAS_ERRORS=true
fi
fi
# Check ROADMAP.md references current year
CURRENT_YEAR=$(date +%Y)
if ! grep -q "$CURRENT_YEAR" ROADMAP.md; then
ISSUES="${ISSUES}### ROADMAP.md does not reference ${CURRENT_YEAR}\nRoadmap should include current-year milestones.\n\n"
HAS_ERRORS=true
fi
# Check for essential sections
for SECTION_ENTRY in "Near-Term|Short-Term|Q[1-4]::Near-term / short-term" "Mid-Term|Medium-Term::Mid-term" "Long-Term|Future::Long-term / future" "Non-Goals|Out of Scope::Non-goals / out of scope"; do
SECTION_REGEX="${SECTION_ENTRY%%::*}"
SECTION_LABEL="${SECTION_ENTRY#*::}"
if ! grep -qiE "$SECTION_REGEX" ROADMAP.md; then
ISSUES="${ISSUES}### ROADMAP.md missing section: ${SECTION_LABEL}\nA well-structured roadmap should include near-term, mid-term, long-term, and non-goals sections.\n\n"
HAS_ERRORS=true
fi
done
fi
# Also check other CNCF governance files
for GOV_FILE in GOVERNANCE.md SECURITY.md CODE_OF_CONDUCT.md CONTRIBUTING.md; do
if [ ! -f "$GOV_FILE" ]; then
ISSUES="${ISSUES}### Missing governance file: ${GOV_FILE}\nCNCF incubation requires this file.\n\n"
HAS_ERRORS=true
fi
done
if [ "$HAS_ERRORS" = "true" ]; then
echo "found=true" >> "$GITHUB_OUTPUT"
printf "%b" "$ISSUES" > /tmp/focus-roadmap.txt
echo "Governance issues found"
else
echo "found=false" >> "$GITHUB_OUTPUT"
echo "ROADMAP.md and governance files verified"
fi
# === UI DESIGN PRINCIPLES (every run) ===
- name: "Check: Hardcoded colors instead of design tokens"
id: focus_hardcoded_colors
working-directory: web
continue-on-error: true
run: |
echo "Scanning for hardcoded color values..."
ISSUES=""
# Find hex colors not in CSS variable definitions or Tailwind config
HEX_COLORS=$(grep -rn "#[0-9a-fA-F]\{3,6\}\b" src/ --include="*.tsx" --include="*.ts" 2>/dev/null | \
grep -v "node_modules" | \
grep -v "\.test\." | \
grep -v "tailwind\|Tailwind" | \
grep -v "// eslint" | \
grep -v "className=" | \
head -25 || true)
if [ -n "$HEX_COLORS" ]; then
COUNT=$(echo "$HEX_COLORS" | wc -l | tr -d ' ')
if [ "$COUNT" -gt 5 ]; then
ISSUES="${ISSUES}### Hardcoded hex colors (${COUNT} found)\nThese should use CSS variables or Tailwind classes:\n\`\`\`\n${HEX_COLORS}\n\`\`\`\n\n"
fi
fi
# Find rgb/rgba colors in inline styles
RGB_COLORS=$(grep -rn "rgb\(a\)\?(" src/ --include="*.tsx" 2>/dev/null | \
grep -v "node_modules" | \
grep -v "\.test\." | \
grep -v "var(--" | \
head -15 || true)
if [ -n "$RGB_COLORS" ]; then
ISSUES="${ISSUES}### Inline RGB colors (should use design tokens)\n\`\`\`\n${RGB_COLORS}\n\`\`\`\n\n"
fi
if [ -n "$ISSUES" ]; then
echo "found=true" >> "$GITHUB_OUTPUT"
printf "%b" "$ISSUES" > /tmp/focus-hardcoded-colors.txt
echo "Hardcoded colors found"
else
echo "found=false" >> "$GITHUB_OUTPUT"
echo "No hardcoded colors detected"
fi
- name: "Check: Inconsistent spacing values"
id: focus_spacing
working-directory: web
continue-on-error: true
run: |
echo "Checking for inconsistent spacing patterns..."
ISSUES=""
# Find inline style px values that should use spacing scale
INLINE_PX=$(grep -rn "style={{" src/ --include="*.tsx" 2>/dev/null | \
grep -oE "['\"]?[0-9]+px['\"]?" | \
sort | uniq -c | sort -rn | head -15 || true)
if [ -n "$INLINE_PX" ]; then
# Count unique non-standard values (not 0, 4, 8, 12, 16, 20, 24, 32, 40, 48)
NONSTANDARD=$(echo "$INLINE_PX" | grep -vE "(^[^0-9]*[048]px|12px|16px|20px|24px|32px|40px|48px)" | head -10 || true)
if [ -n "$NONSTANDARD" ]; then
ISSUES="${ISSUES}### Non-standard spacing values in inline styles\nConsider using Tailwind spacing classes (p-1, m-2, gap-4, etc.):\n\`\`\`\n${NONSTANDARD}\n\`\`\`\n\n"
fi
fi
# Find margin/padding with magic numbers
MAGIC_SPACING=$(grep -rn "margin\|padding" src/ --include="*.tsx" 2>/dev/null | \
grep -E ":\s*[0-9]+(px|rem|em)" | \
grep -v "node_modules" | \
grep -v "0px\|0rem" | \
head -15 || true)
if [ -n "$MAGIC_SPACING" ]; then
COUNT=$(echo "$MAGIC_SPACING" | wc -l | tr -d ' ')
if [ "$COUNT" -gt 5 ]; then
ISSUES="${ISSUES}### Inline margin/padding values (${COUNT} found)\nConsider using Tailwind or CSS variables:\n\`\`\`\n${MAGIC_SPACING}\n\`\`\`\n\n"
fi
fi
if [ -n "$ISSUES" ]; then
echo "found=true" >> "$GITHUB_OUTPUT"
printf "%b" "$ISSUES" > /tmp/focus-spacing.txt
echo "Spacing inconsistencies found"
else
echo "found=false" >> "$GITHUB_OUTPUT"
echo "No spacing issues detected"
fi
- name: "Check: Missing dark mode support"
id: focus_dark_mode
working-directory: web
continue-on-error: true
run: |
echo "Checking for components missing dark mode support..."
ISSUES=""
# Find components with hardcoded light colors that don't have dark variants
LIGHT_ONLY=$(grep -rn "bg-white\|bg-gray-50\|bg-gray-100\|text-gray-900\|text-black\|border-gray-200\|border-gray-300" src/ --include="*.tsx" 2>/dev/null | \
grep -v "dark:" | \
grep -v "node_modules" | \
grep -v "\.test\." | \
head -20 || true)
if [ -n "$LIGHT_ONLY" ]; then
COUNT=$(echo "$LIGHT_ONLY" | wc -l | tr -d ' ')
if [ "$COUNT" -gt 3 ]; then
ISSUES="${ISSUES}### Light-only color classes without dark mode variants (${COUNT} found)\nAdd \`dark:\` variants for dark mode support:\n\`\`\`\n${LIGHT_ONLY}\n\`\`\`\n\n"
fi
fi
# Check for inline color styles
INLINE_COLORS=$(grep -rn "style={{" src/ --include="*.tsx" 2>/dev/null | \
grep -iE "background|color" | \
grep -v "backgroundColor: 'transparent'" | \
grep -v "var(--" | \
grep -v "node_modules" | \
head -10 || true)
if [ -n "$INLINE_COLORS" ]; then
ISSUES="${ISSUES}### Inline color styles (won't adapt to dark mode)\n\`\`\`\n${INLINE_COLORS}\n\`\`\`\n\n"
fi
if [ -n "$ISSUES" ]; then
echo "found=true" >> "$GITHUB_OUTPUT"
printf "%b" "$ISSUES" > /tmp/focus-dark-mode.txt
echo "Dark mode gaps found"
else
echo "found=false" >> "$GITHUB_OUTPUT"
echo "No dark mode issues detected"
fi
- name: "Check: Touch target sizes"
id: focus_touch_targets
working-directory: web
continue-on-error: true
run: |
echo "Checking for small touch targets..."
ISSUES=""
# Find small explicit sizes on interactive elements
SMALL_BUTTONS=$(grep -rn "<button\|<a \|onClick" src/ --include="*.tsx" 2>/dev/null | \
grep -E "w-[1-6]\b|h-[1-6]\b|size-[1-6]\b|p-0\b|p-1\b" | \
grep -v "node_modules" | \
grep -v "\.test\." | \
head -15 || true)
if [ -n "$SMALL_BUTTONS" ]; then
ISSUES="${ISSUES}### Potentially small touch targets (< 44px recommended)\n\`\`\`\n${SMALL_BUTTONS}\n\`\`\`\n\n"
fi
# Find icon-only buttons that might be too small
ICON_BUTTONS=$(grep -rn "Icon.*onClick\|onClick.*Icon" src/ --include="*.tsx" 2>/dev/null | \
grep -v "p-[2-9]\|p-1[0-9]\|size-[8-9]\|size-1[0-9]\|w-[8-9]\|w-1[0-9]\|h-[8-9]\|h-1[0-9]" | \
grep -v "node_modules" | \
head -10 || true)
if [ -n "$ICON_BUTTONS" ]; then
ISSUES="${ISSUES}### Icon buttons that may need larger click areas\n\`\`\`\n${ICON_BUTTONS}\n\`\`\`\n\n"
fi
if [ -n "$ISSUES" ]; then
echo "found=true" >> "$GITHUB_OUTPUT"
printf "%b" "$ISSUES" > /tmp/focus-touch-targets.txt
echo "Small touch targets found"
else
echo "found=false" >> "$GITHUB_OUTPUT"
echo "Touch targets look adequate"
fi
- name: "Check: Inconsistent component patterns"
id: focus_component_patterns
working-directory: web
continue-on-error: true
run: |
echo "Checking for inconsistent UI patterns..."
ISSUES=""
# Check for mixed button patterns (some with Button component, some with raw button)
BUTTON_COMPONENT=$(grep -rl "<Button" src/components --include="*.tsx" 2>/dev/null | wc -l || echo "0")
RAW_BUTTON=$(grep -rl "<button" src/components --include="*.tsx" 2>/dev/null | wc -l || echo "0")
if [ "$BUTTON_COMPONENT" -gt 0 ] && [ "$RAW_BUTTON" -gt 5 ]; then
RAW_EXAMPLES=$(grep -rn "<button" src/components --include="*.tsx" 2>/dev/null | \
grep -v "Button\|node_modules" | head -8 || true)
if [ -n "$RAW_EXAMPLES" ]; then
ISSUES="${ISSUES}### Mixed button patterns: ${BUTTON_COMPONENT} use <Button>, ${RAW_BUTTON} use raw <button>\nConsider using the Button component consistently:\n\`\`\`\n${RAW_EXAMPLES}\n\`\`\`\n\n"
fi
fi
# Check for inconsistent modal implementations
MODAL_PATTERNS=$(grep -rn "isOpen\|isVisible\|showModal\|show=\|visible=" src/ --include="*.tsx" 2>/dev/null | \
grep -v "node_modules" | \
cut -d: -f1 | sort -u | wc -l || echo "0")
if [ "$MODAL_PATTERNS" -gt 10 ]; then
MODAL_EXAMPLES=$(grep -rn "isOpen\|isVisible\|showModal\|show=\|visible=" src/ --include="*.tsx" 2>/dev/null | \
grep -v "node_modules" | head -8 || true)
ISSUES="${ISSUES}### Multiple modal visibility patterns detected\nConsider standardizing on one pattern (e.g., \`isOpen\`):\n\`\`\`\n${MODAL_EXAMPLES}\n\`\`\`\n\n"
fi
# Check for inline styles that could use Tailwind
INLINE_STYLES=$(grep -c "style={{" src/components/**/*.tsx 2>/dev/null || echo "0")
if [ "$INLINE_STYLES" -gt 50 ]; then
INLINE_EXAMPLES=$(grep -rn "style={{" src/components --include="*.tsx" 2>/dev/null | head -10 || true)
ISSUES="${ISSUES}### High use of inline styles (${INLINE_STYLES} occurrences)\nConsider using Tailwind classes for consistency:\n\`\`\`\n${INLINE_EXAMPLES}\n\`\`\`\n\n"
fi
if [ -n "$ISSUES" ]; then
echo "found=true" >> "$GITHUB_OUTPUT"
printf "%b" "$ISSUES" > /tmp/focus-component-patterns.txt
echo "Inconsistent patterns found"
else
echo "found=false" >> "$GITHUB_OUTPUT"
echo "No pattern inconsistencies detected"
fi
# === NFR COVERAGE & SELF-IMPROVEMENT (weekly on Sunday) ===
- name: "Meta: Analyze recent PRs for improvement opportunities"
id: meta_pr_analysis
if: github.event.schedule == '0 0 * * 0' || github.event_name == 'workflow_dispatch'
env:
GH_TOKEN: ${{ secrets.CONSOLE_AUTO }}
continue-on-error: true
run: |
echo "Analyzing recent merged PRs for Auto-QA improvement opportunities..."
ISSUES=""
# Get PRs merged in the last 7 days
WEEK_AGO=$(date -u -d '7 days ago' +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date -u -v-7d +%Y-%m-%dT%H:%M:%SZ)
# Fetch recent merged PRs
gh pr list --repo "${{ github.repository }}" --state merged --limit 50 --json number,title,labels,mergedAt,files > /tmp/recent-prs.json 2>/dev/null || echo "[]" > /tmp/recent-prs.json
# Analyze PR patterns
PR_COUNT=$(jq 'length' /tmp/recent-prs.json)
echo "Found $PR_COUNT recently merged PRs"
# Count PRs by category based on title prefixes
BUG_FIXES=$(jq '[.[] | select(.title | test("^🐛|^fix|^bug"; "i"))] | length' /tmp/recent-prs.json)
FEATURES=$(jq '[.[] | select(.title | test("^✨|^feat|^add"; "i"))] | length' /tmp/recent-prs.json)
REFACTORS=$(jq '[.[] | select(.title | test("^♻️|^refactor"; "i"))] | length' /tmp/recent-prs.json)
DOCS=$(jq '[.[] | select(.title | test("^📖|^📝|^doc"; "i"))] | length' /tmp/recent-prs.json)
TESTS=$(jq '[.[] | select(.title | test("^✅|^test"; "i"))] | length' /tmp/recent-prs.json)
# Identify NFR categories from PR titles and file paths
NFR_TESTING=$(jq '[.[] | select(.files[]?.path | test("test|spec|__tests__"; "i"))] | length' /tmp/recent-prs.json 2>/dev/null || echo "0")
NFR_SECURITY=$(jq '[.[] | select(.title | test("secur|auth|token|cred|permission"; "i"))] | length' /tmp/recent-prs.json)
NFR_PERF=$(jq '[.[] | select(.title | test("perf|optim|fast|slow|cache|lazy"; "i"))] | length' /tmp/recent-prs.json)
NFR_A11Y=$(jq '[.[] | select(.title | test("a11y|access|aria|keyboard|screen.?reader"; "i"))] | length' /tmp/recent-prs.json)
NFR_I18N=$(jq '[.[] | select(.title | test("i18n|l10n|local|translat|language"; "i"))] | length' /tmp/recent-prs.json)
NFR_UI=$(jq '[.[] | select(.title | test("ui|ux|design|style|theme|dark|color"; "i"))] | length' /tmp/recent-prs.json)
NFR_STORAGE=$(jq '[.[] | select(.title | test("storage|cache|persist|state|database"; "i"))] | length' /tmp/recent-prs.json)
NFR_NAV=$(jq '[.[] | select(.title | test("nav|route|link|breadcrumb|menu"; "i"))] | length' /tmp/recent-prs.json)
# Build NFR coverage report
ISSUES="${ISSUES}## Recent PR Analysis (Last 7 Days)\n\n"
ISSUES="${ISSUES}### PR Categories\n"
ISSUES="${ISSUES}| Category | Count |\n|----------|-------|\n"
ISSUES="${ISSUES}| 🐛 Bug Fixes | ${BUG_FIXES} |\n"
ISSUES="${ISSUES}| ✨ Features | ${FEATURES} |\n"
ISSUES="${ISSUES}| ♻️ Refactors | ${REFACTORS} |\n"
ISSUES="${ISSUES}| 📖 Docs | ${DOCS} |\n"
ISSUES="${ISSUES}| ✅ Tests | ${TESTS} |\n\n"
ISSUES="${ISSUES}### NFR Coverage Analysis\n"
ISSUES="${ISSUES}| NFR Area | PRs Touching | Coverage |\n|----------|--------------|----------|\n"
# Determine coverage status
nfr_status() {
if [ "$1" -eq 0 ]; then echo "⚠️ No coverage";
elif [ "$1" -lt 3 ]; then echo "🟡 Low";
else echo "🟢 Active"; fi
}
ISSUES="${ISSUES}| Testing | ${NFR_TESTING} | $(nfr_status $NFR_TESTING) |\n"
ISSUES="${ISSUES}| Security | ${NFR_SECURITY} | $(nfr_status $NFR_SECURITY) |\n"
ISSUES="${ISSUES}| Performance | ${NFR_PERF} | $(nfr_status $NFR_PERF) |\n"
ISSUES="${ISSUES}| Accessibility | ${NFR_A11Y} | $(nfr_status $NFR_A11Y) |\n"
ISSUES="${ISSUES}| Localization | ${NFR_I18N} | $(nfr_status $NFR_I18N) |\n"
ISSUES="${ISSUES}| UI/UX Design | ${NFR_UI} | $(nfr_status $NFR_UI) |\n"
ISSUES="${ISSUES}| Storage/State | ${NFR_STORAGE} | $(nfr_status $NFR_STORAGE) |\n"
ISSUES="${ISSUES}| Navigation | ${NFR_NAV} | $(nfr_status $NFR_NAV) |\n\n"
# Identify gaps and make recommendations
RECOMMENDATIONS=""
if [ "$NFR_TESTING" -lt 2 ]; then
RECOMMENDATIONS="${RECOMMENDATIONS}- **Testing**: Consider adding test coverage checks (unit test %, E2E coverage)\n"
fi
if [ "$NFR_SECURITY" -lt 2 ]; then
RECOMMENDATIONS="${RECOMMENDATIONS}- **Security**: Add checks for exposed secrets, unsafe innerHTML, eval usage\n"
fi
if [ "$NFR_A11Y" -lt 2 ]; then
RECOMMENDATIONS="${RECOMMENDATIONS}- **Accessibility**: Consider adding automated axe-core scan, focus-trap validation, semantic heading order checks\n"
fi
if [ "$NFR_I18N" -eq 0 ]; then
RECOMMENDATIONS="${RECOMMENDATIONS}- **Localization**: Consider adding missing-translation-key detection, pluralization pattern checks, date/number format audits\n"
fi
if [ "$NFR_PERF" -lt 2 ]; then
RECOMMENDATIONS="${RECOMMENDATIONS}- **Performance**: Consider adding render-count profiling, Web Vitals regression detection, memoization gap analysis\n"
fi
if [ "$NFR_STORAGE" -lt 2 ]; then
RECOMMENDATIONS="${RECOMMENDATIONS}- **Storage**: Add checks for localStorage usage patterns, state persistence\n"
fi
if [ "$NFR_NAV" -lt 2 ]; then
RECOMMENDATIONS="${RECOMMENDATIONS}- **Navigation**: Add broken link detection, route consistency checks\n"
fi
if [ -n "$RECOMMENDATIONS" ]; then
ISSUES="${ISSUES}### Recommended Auto-QA Improvements\n\nBased on recent activity, consider adding these checks:\n\n${RECOMMENDATIONS}\n"
echo "found=true" >> "$GITHUB_OUTPUT"
else
ISSUES="${ISSUES}### Status\n\n✅ All NFR areas have recent activity. No immediate improvements needed.\n"
echo "found=false" >> "$GITHUB_OUTPUT"
fi
printf "%b" "$ISSUES" > /tmp/meta-pr-analysis.txt
cat /tmp/meta-pr-analysis.txt
- name: "Check: Missing test coverage"
id: focus_test_coverage
working-directory: web
continue-on-error: true
run: |
echo "Checking for components without tests..."
ISSUES=""
# Find components without corresponding test files
UNTESTED=""
for f in $(find src/components -name "*.tsx" -not -name "*.test.*" -not -name "*.spec.*" 2>/dev/null | head -50); do
BASENAME=$(basename "$f" .tsx)
DIRNAME=$(dirname "$f")
# Check for test file variants
TEST_EXISTS=false
for ext in ".test.tsx" ".spec.tsx" ".test.ts" ".spec.ts"; do
if [ -f "${DIRNAME}/${BASENAME}${ext}" ] || [ -f "${DIRNAME}/__tests__/${BASENAME}${ext}" ]; then
TEST_EXISTS=true
break
fi
done
if [ "$TEST_EXISTS" = "false" ]; then
# Check if component is complex enough to warrant tests (>50 lines)
LINES=$(wc -l < "$f" 2>/dev/null || echo "0")
if [ "$LINES" -gt 50 ]; then
UNTESTED="${UNTESTED} - \`${f}\` (${LINES} lines)\n"
fi
fi
done
if [ -n "$UNTESTED" ]; then
COUNT=$(echo -e "$UNTESTED" | grep -c "^\s*-" || echo "0")
if [ "$COUNT" -gt 5 ]; then
echo "found=true" >> "$GITHUB_OUTPUT"
printf "Components over 50 lines without test files:\n%b" "$UNTESTED" > /tmp/focus-test-coverage.txt
cat /tmp/focus-test-coverage.txt
else
echo "found=false" >> "$GITHUB_OUTPUT"
echo "Test coverage looks adequate"
fi
else
echo "found=false" >> "$GITHUB_OUTPUT"
echo "All major components have tests"
fi
- name: "Check: Hardcoded user-facing strings (i18n)"
id: focus_i18n
working-directory: web
continue-on-error: true
run: |
echo "Checking for hardcoded user-facing strings..."
ISSUES=""
# Find JSX text content that looks like user-facing strings
# Exclude: className, imports, comments, type definitions
HARDCODED=$(grep -rn ">[A-Z][a-z].*</" src/ --include="*.tsx" 2>/dev/null | \
grep -v "node_modules\|\.test\.\|\.spec\." | \
grep -v "className\|import\|//\|/\*\|interface\|type " | \
grep -v "{.*}" | \
head -25 || true)
if [ -n "$HARDCODED" ]; then
COUNT=$(echo "$HARDCODED" | wc -l | tr -d ' ')
if [ "$COUNT" -gt 10 ]; then
ISSUES="${ISSUES}### Hardcoded strings in JSX (${COUNT} found)\nConsider extracting to a translations file for future i18n support:\n\`\`\`\n${HARDCODED}\n\`\`\`\n"
fi
fi
# Find button/label text
BUTTON_TEXT=$(grep -rn ">[A-Z][a-z]\+</button>\|>[A-Z][a-z]\+</Button>" src/ --include="*.tsx" 2>/dev/null | \
grep -v "node_modules" | head -15 || true)
if [ -n "$BUTTON_TEXT" ]; then
ISSUES="${ISSUES}\n### Hardcoded button labels\n\`\`\`\n${BUTTON_TEXT}\n\`\`\`\n"
fi
if [ -n "$ISSUES" ]; then
echo "found=true" >> "$GITHUB_OUTPUT"
printf "%b" "$ISSUES" > /tmp/focus-i18n.txt
echo "Hardcoded strings found"
else
echo "found=false" >> "$GITHUB_OUTPUT"
echo "No obvious i18n issues"
fi
- name: "Check: State management patterns"
id: focus_state_patterns
working-directory: web
continue-on-error: true
run: |
echo "Checking for state management issues..."
ISSUES=""
# Find localStorage usage without error handling
LOCALSTORAGE=$(grep -rn "localStorage\.\(get\|set\)Item" src/ --include="*.ts" --include="*.tsx" 2>/dev/null | \
grep -v "node_modules\|try\|catch" | head -15 || true)
if [ -n "$LOCALSTORAGE" ]; then
ISSUES="${ISSUES}### localStorage usage without try/catch\n\`\`\`\n${LOCALSTORAGE}\n\`\`\`\n\n"
fi
# Find excessive prop drilling (components with >6 props)
PROP_DRILLING=$(grep -rn "^export.*function\|^const.*=.*(" src/components --include="*.tsx" 2>/dev/null | \
grep -oE "\([^)]{200,}\)" | head -10 || true)
if [ -n "$PROP_DRILLING" ]; then
ISSUES="${ISSUES}### Components with many props (possible prop drilling)\nConsider using Context or state management:\n\`\`\`\n${PROP_DRILLING}\n\`\`\`\n\n"
fi
# Find components re-fetching data that could be cached
REFETCH=$(grep -rn "useEffect.*fetch\|useEffect.*api\." src/ --include="*.tsx" 2>/dev/null | \
grep -v "node_modules\|cache\|memo\|useSWR\|useQuery\|useCached" | head -10 || true)
if [ -n "$REFETCH" ]; then
COUNT=$(echo "$REFETCH" | wc -l | tr -d ' ')
if [ "$COUNT" -gt 3 ]; then
ISSUES="${ISSUES}### Data fetching without caching (${COUNT} found)\nConsider using useSWR, useQuery, or custom cache hooks:\n\`\`\`\n${REFETCH}\n\`\`\`\n"
fi
fi
if [ -n "$ISSUES" ]; then
echo "found=true" >> "$GITHUB_OUTPUT"
printf "%b" "$ISSUES" > /tmp/focus-state-patterns.txt
echo "State management issues found"
else
echo "found=false" >> "$GITHUB_OUTPUT"
echo "State management looks good"
fi
- name: "Check: Navigation and routing issues"
id: focus_navigation
working-directory: web
continue-on-error: true
run: |
echo "Checking for navigation and routing issues..."
ISSUES=""
# Find hardcoded route paths (should use constants)
HARDCODED_ROUTES=$(grep -rn "navigate(['\"]/" src/ --include="*.tsx" --include="*.ts" 2>/dev/null | \
grep -v "node_modules\|ROUTES\.\|routes\." | head -15 || true)
if [ -n "$HARDCODED_ROUTES" ]; then
ISSUES="${ISSUES}### Hardcoded route paths\nConsider using route constants for maintainability:\n\`\`\`\n${HARDCODED_ROUTES}\n\`\`\`\n\n"
fi
# Find links without proper handling
BARE_LINKS=$(grep -rn "<a href=" src/ --include="*.tsx" 2>/dev/null | \
grep -v "node_modules\|target=\|rel=\|Link" | head -10 || true)
if [ -n "$BARE_LINKS" ]; then
ISSUES="${ISSUES}### External links without target/rel attributes\n\`\`\`\n${BARE_LINKS}\n\`\`\`\n\n"
fi
# Find missing breadcrumb or back navigation in drill-down views
DRILLDOWNS=$(find src -path "*drilldown*" -name "*.tsx" -o -path "*detail*" -name "*.tsx" 2>/dev/null | head -20)
MISSING_NAV=""
for f in $DRILLDOWNS; do
HAS_BACK=$(grep -l "navigate\|history\|breadcrumb\|Breadcrumb\|goBack\|Back" "$f" 2>/dev/null || true)
if [ -z "$HAS_BACK" ]; then
MISSING_NAV="${MISSING_NAV} - \`${f}\`\n"
fi
done
if [ -n "$MISSING_NAV" ]; then
ISSUES="${ISSUES}### Drill-down views possibly missing back navigation\n${MISSING_NAV}\n"
fi
if [ -n "$ISSUES" ]; then
echo "found=true" >> "$GITHUB_OUTPUT"
printf "%b" "$ISSUES" > /tmp/focus-navigation.txt
echo "Navigation issues found"
else
echo "found=false" >> "$GITHUB_OUTPUT"
echo "Navigation looks good"
fi
- name: "Check: Efficiency patterns"
id: focus_efficiency
working-directory: web
continue-on-error: true
run: |
echo "Checking for efficiency issues..."
ISSUES=""
# Find re-renders from inline object/function creation
INLINE_OBJECTS=$(grep -rn "style={{" src/ --include="*.tsx" 2>/dev/null | \
grep -v "node_modules" | wc -l || echo "0")
INLINE_FUNCS=$(grep -rn "onClick={() =>" src/ --include="*.tsx" 2>/dev/null | \
grep -v "node_modules\|useCallback" | wc -l || echo "0")
if [ "$INLINE_OBJECTS" -gt 50 ] || [ "$INLINE_FUNCS" -gt 30 ]; then
ISSUES="${ISSUES}### Potential re-render triggers\n"
ISSUES="${ISSUES}- Inline style objects: ${INLINE_OBJECTS} (each creates new object on render)\n"
ISSUES="${ISSUES}- Inline arrow functions: ${INLINE_FUNCS} (consider useCallback)\n\n"
fi
# Find components importing entire libraries
FULL_IMPORTS=$(grep -rn "import \* as\|from 'lodash'\|from 'moment'" src/ --include="*.ts" --include="*.tsx" 2>/dev/null | \
grep -v "node_modules" | head -10 || true)
if [ -n "$FULL_IMPORTS" ]; then
ISSUES="${ISSUES}### Full library imports (tree-shaking issue)\nUse specific imports instead:\n\`\`\`\n${FULL_IMPORTS}\n\`\`\`\n\n"
fi
# Find components without memo that receive object props
UNMEMO=$(grep -rn "export const\|export function" src/components --include="*.tsx" 2>/dev/null | \
grep -v "memo\|React.memo" | head -20 || true)
UNMEMO_COUNT=$(echo "$UNMEMO" | grep -c "export" || echo "0")
if [ "$UNMEMO_COUNT" -gt 30 ]; then
ISSUES="${ISSUES}### Many components without React.memo (${UNMEMO_COUNT})\nConsider memoizing components that receive object/array props\n\n"
fi
if [ -n "$ISSUES" ]; then
echo "found=true" >> "$GITHUB_OUTPUT"
printf "%b" "$ISSUES" > /tmp/focus-efficiency.txt
echo "Efficiency issues found"
else
echo "found=false" >> "$GITHUB_OUTPUT"
echo "No major efficiency issues"
fi
# === INVENTORY CONSISTENCY (every run) ===
- name: "Check: Missing component files"
id: focus_missing_files
working-directory: web
continue-on-error: true
run: |
echo "Cross-referencing INVENTORY.md component files against source..."
ISSUES=""
HAS_ERRORS=false
# Extract component file paths from INVENTORY.md
if [ -f "../INVENTORY.md" ]; then
# Dashboard components section — extract .tsx/.ts paths
PATHS=$(grep -oE 'web/src/[^ ]+\.(tsx|ts)' ../INVENTORY.md | sed 's|^web/||' || true)
MISSING=""
FOUND=0
TOTAL=0
for p in $PATHS; do
TOTAL=$((TOTAL + 1))
if [ ! -f "$p" ]; then
MISSING="${MISSING} - \`${p}\` — listed in INVENTORY.md but file not found\n"
HAS_ERRORS=true
else
FOUND=$((FOUND + 1))
fi
done
if [ -n "$MISSING" ]; then
ISSUES="${ISSUES}### Component files listed in INVENTORY.md but missing from source\n${MISSING}\n"
fi
ISSUES="${ISSUES}### Summary: ${FOUND}/${TOTAL} listed component files exist\n\n"
else
ISSUES="${ISSUES}### INVENTORY.md not found in repo root\n\n"
HAS_ERRORS=true
fi
# Check that page component files referenced exist
PAGES=$(grep -oE '[A-Z][A-Za-z]+\.tsx' ../INVENTORY.md 2>/dev/null | sort -u || true)
PAGE_MISSING=""
for page in $PAGES; do
MATCH=$(find src -name "$page" 2>/dev/null | head -1)
if [ -z "$MATCH" ]; then
PAGE_MISSING="${PAGE_MISSING} - \`${page}\` — referenced in INVENTORY.md but not found in src/\n"
HAS_ERRORS=true
fi
done
if [ -n "$PAGE_MISSING" ]; then
ISSUES="${ISSUES}### Page components referenced but not found\n${PAGE_MISSING}\n"
fi
if [ "$HAS_ERRORS" = "true" ]; then
echo "found=true" >> "$GITHUB_OUTPUT"
printf "%b" "$ISSUES" > /tmp/focus-missing-files.txt
echo "Consistency issues found"
else
echo "found=false" >> "$GITHUB_OUTPUT"
echo "All INVENTORY.md references verified"
fi
- name: "Check: Card type registry consistency"
id: focus_card_registry
working-directory: web
continue-on-error: true
run: |
echo "Cross-referencing INVENTORY.md card types against source code..."
ISSUES=""
if [ -f "../INVENTORY.md" ]; then
# Extract card type identifiers from INVENTORY.md (backtick-wrapped values in Type column)
CARD_TYPES=$(grep -oE '`[a-z_]+`' ../INVENTORY.md | tr -d '`' | sort -u || true)
UNREGISTERED=""
for ct in $CARD_TYPES; do
# Skip non-card-type patterns (like field names)
case "$ct" in
total|score|healthy|warning|critical|*_util|firing|pending|resolved|bound|installed|*_cost) continue ;;
esac
# Search for this card type string in source (quoted strings or object keys)
FOUND=$(grep -rlE "'${ct}'|\"${ct}\"|${ct}\s*:" src/ 2>/dev/null | head -1 || true)
if [ -z "$FOUND" ]; then
UNREGISTERED="${UNREGISTERED} - \`${ct}\` — in INVENTORY.md but not found in source code\n"
fi
done
if [ -n "$UNREGISTERED" ]; then
ISSUES="${ISSUES}### Card types in INVENTORY.md not found in source\n${UNREGISTERED}\n"
fi
fi
if [ -n "$ISSUES" ]; then
echo "found=true" >> "$GITHUB_OUTPUT"
printf "%b" "$ISSUES" > /tmp/focus-card-registry.txt
echo "Card registry inconsistencies found"
else
echo "found=false" >> "$GITHUB_OUTPUT"
echo "All card types verified in source"
fi
- name: "Check: Route consistency"
id: focus_routes
working-directory: web
continue-on-error: true
run: |
echo "Cross-referencing INVENTORY.md routes against router config..."
ISSUES=""
if [ -f "../INVENTORY.md" ]; then
# Extract route paths from INVENTORY.md
ROUTES=$(grep -oE '`/[a-z-]+`' ../INVENTORY.md | tr -d '`' | sort -u || true)
# Find the router file
ROUTER_FILE=$(find src -name "App.tsx" -o -name "Router.tsx" -o -name "routes.tsx" -o -name "AppRoutes.tsx" 2>/dev/null | head -1)
if [ -n "$ROUTER_FILE" ]; then
MISSING_ROUTES=""
for route in $ROUTES; do
FOUND=$(grep -l "\"${route}\"\|'${route}'" "$ROUTER_FILE" 2>/dev/null || true)
if [ -z "$FOUND" ]; then
# Also check all tsx files for route definition
FOUND2=$(grep -rl "path.*['\"]${route}['\"]" src/ --include="*.tsx" 2>/dev/null | head -1 || true)
if [ -z "$FOUND2" ]; then
MISSING_ROUTES="${MISSING_ROUTES} - \`${route}\` — listed in INVENTORY.md but not found in router\n"
fi
fi
done
if [ -n "$MISSING_ROUTES" ]; then
ISSUES="${ISSUES}### Routes in INVENTORY.md not found in router config\n${MISSING_ROUTES}\n"
fi
else
ISSUES="${ISSUES}### Could not locate router file to verify routes\n\n"
fi
fi
if [ -n "$ISSUES" ]; then
echo "found=true" >> "$GITHUB_OUTPUT"
printf "%b" "$ISSUES" > /tmp/focus-routes.txt
echo "Route inconsistencies found"
else
echo "found=false" >> "$GITHUB_OUTPUT"
echo "All routes verified"
fi
- name: "Check: Modal and drill-down consistency"
id: focus_modal_consistency
working-directory: web
continue-on-error: true
run: |
echo "Cross-referencing INVENTORY.md modals/drill-downs against source..."
ISSUES=""
if [ -f "../INVENTORY.md" ]; then
# Extract modal/drill-down file names from INVENTORY.md
MODAL_FILES=$(grep -oE '[A-Z][A-Za-z]+Modal\.tsx\|[A-Z][A-Za-z]+Dialog\.tsx\|[A-Z][A-Za-z]+DrillDown\.tsx' ../INVENTORY.md | sort -u || true)
MISSING_MODALS=""
for modal in $MODAL_FILES; do
FOUND=$(find src -name "$modal" 2>/dev/null | head -1)
if [ -z "$FOUND" ]; then
MISSING_MODALS="${MISSING_MODALS} - \`${modal}\` — listed in INVENTORY.md but not found\n"
fi
done
if [ -n "$MISSING_MODALS" ]; then
ISSUES="${ISSUES}### Modals/dialogs/drill-downs in INVENTORY.md but missing\n${MISSING_MODALS}\n"
fi
# Check for drill-down views in source not listed in INVENTORY.md
ACTUAL_DRILLDOWNS=$(find src -path "*/drilldown/views/*.tsx" -exec basename {} \; 2>/dev/null | sort || true)
UNLISTED=""
for dd in $ACTUAL_DRILLDOWNS; do
LISTED=$(grep -c "$dd" ../INVENTORY.md 2>/dev/null || echo "0")
if [ "$LISTED" -eq 0 ]; then
UNLISTED="${UNLISTED} - \`${dd}\` — exists in source but not listed in INVENTORY.md\n"
fi
done
if [ -n "$UNLISTED" ]; then
ISSUES="${ISSUES}### Drill-down views in source but not in INVENTORY.md\n${UNLISTED}\n"
fi
fi
# Also check consistency.md for noted issues that may still be open
if [ -f "../consistency.md" ]; then
NOTED=$(grep -c "Noted\|Missing\|FIXED" ../consistency.md 2>/dev/null || echo "0")
ISSUES="${ISSUES}### consistency.md status: ${NOTED} noted items found\n"
fi
if [ -n "$ISSUES" ]; then
echo "found=true" >> "$GITHUB_OUTPUT"
printf "%b" "$ISSUES" > /tmp/focus-modal-consistency.txt
echo "Modal/drill-down inconsistencies found"
else
echo "found=false" >> "$GITHUB_OUTPUT"
echo "All modals and drill-downs verified"
fi
# === FLICKER DETECTION (every run) ===
- name: "Check: UI flicker patterns"
id: focus_flicker
working-directory: web
continue-on-error: true
run: |
echo "Checking for UI flicker patterns..."
ISSUES=""
# Find rapid consecutive setState calls (potential flicker)
RAPID_SETSTATE=$(grep -rn "set[A-Z][A-Za-z]*(" src/ --include="*.tsx" 2>/dev/null | \
grep -v "node_modules" | \
awk -F: '{file=$1; line=$2}
prev_file==file && line-prev_line<=2 {print prev_file":"prev_line" and "file":"line" - consecutive setState calls"}
{prev_file=file; prev_line=line}' | head -15 || true)
if [ -n "$RAPID_SETSTATE" ]; then
ISSUES="${ISSUES}### Potential flicker: consecutive setState calls\nBatch these with a single state update or useReducer:\n\`\`\`\n${RAPID_SETSTATE}\n\`\`\`\n\n"
fi
# Find components with loading state but no skeleton/spinner
LOADING_NO_SKELETON=$(for f in $(find src -name "*.tsx" 2>/dev/null | grep -v node_modules); do
HAS_LOADING=$(grep -l "isLoading\|loading" "$f" 2>/dev/null || true)
if [ -n "$HAS_LOADING" ]; then
HAS_SKELETON=$(grep -l "Skeleton\|Spinner\|Loading\|skeleton" "$f" 2>/dev/null || true)
if [ -z "$HAS_SKELETON" ]; then
echo " - \`${f}\`"
fi
fi
done | head -15)
if [ -n "$LOADING_NO_SKELETON" ]; then
ISSUES="${ISSUES}### Components with loading state but no skeleton/spinner\nAdd visual loading indicators to prevent flicker:\n${LOADING_NO_SKELETON}\n\n"
fi
# Find async data without Suspense boundaries
ASYNC_NO_SUSPENSE=$(grep -rln "await\|\.then(" src/components --include="*.tsx" 2>/dev/null | while read f; do
HAS_SUSPENSE=$(grep -l "Suspense" "$f" 2>/dev/null || true)
if [ -z "$HAS_SUSPENSE" ]; then
basename "$f"
fi
done | head -10 || true)
if [ -n "$ASYNC_NO_SUSPENSE" ]; then
COUNT=$(echo "$ASYNC_NO_SUSPENSE" | wc -l | tr -d ' ')
if [ "$COUNT" -gt 3 ]; then
ISSUES="${ISSUES}### Components with async operations but no Suspense\nConsider adding Suspense boundaries:\n\`\`\`\n${ASYNC_NO_SUSPENSE}\n\`\`\`\n\n"
fi
fi
# Find useLayoutEffect usage (can cause visible flicker if misused)
LAYOUT_EFFECT=$(grep -rn "useLayoutEffect" src/ --include="*.tsx" 2>/dev/null | \
grep -v "node_modules" | head -10 || true)
if [ -n "$LAYOUT_EFFECT" ]; then
COUNT=$(echo "$LAYOUT_EFFECT" | wc -l | tr -d ' ')
ISSUES="${ISSUES}### useLayoutEffect usage (${COUNT} instances)\nReview for correctness - only use for DOM measurements:\n\`\`\`\n${LAYOUT_EFFECT}\n\`\`\`\n\n"
fi
if [ -n "$ISSUES" ]; then
echo "found=true" >> "$GITHUB_OUTPUT"
printf "%b" "$ISSUES" > /tmp/focus-flicker.txt
echo "Flicker patterns found"
else
echo "found=false" >> "$GITHUB_OUTPUT"
echo "No obvious flicker patterns detected"
fi
# === CODE CENTRALIZATION (every run) ===
- name: "Check: Code centralization opportunities"
id: focus_centralization
working-directory: web
continue-on-error: true
run: |
echo "Checking for code centralization opportunities..."
ISSUES=""
# Find duplicate patterns in card components
CARD_PATTERNS=""
CARD_FILES=$(find src/components/cards -name "*.tsx" 2>/dev/null | grep -v "index\|test\|spec" | head -50)
# Check for repeated useCardData patterns
USECARD_VARIANTS=$(grep -rh "useCardData\|useCardDemoState" src/components/cards --include="*.tsx" 2>/dev/null | \
sed 's/.*useCard/useCard/' | sort | uniq -c | sort -rn | head -10 || true)
if [ -n "$USECARD_VARIANTS" ]; then
ISSUES="${ISSUES}### Card hook usage patterns (consider standardizing)\n\`\`\`\n${USECARD_VARIANTS}\n\`\`\`\n\n"
fi
# Find repeated stat block patterns
STAT_PATTERNS=$(grep -rn "grid grid-cols-\|flex.*gap-\|bg-.*rounded.*p-" src/components/cards --include="*.tsx" 2>/dev/null | \
grep -oE "grid grid-cols-[0-9]+|flex.*gap-[0-9]+|bg-[a-z]+-[0-9]+/[0-9]+ rounded" | \
sort | uniq -c | sort -rn | head -10 || true)
if [ -n "$STAT_PATTERNS" ]; then
REPEAT_THRESHOLD=5
HIGH_REPEAT=$(echo "$STAT_PATTERNS" | awk -v t=$REPEAT_THRESHOLD '$1 > t {print}')
if [ -n "$HIGH_REPEAT" ]; then
ISSUES="${ISSUES}### Repeated layout patterns (extract to shared components)\n\`\`\`\n${HIGH_REPEAT}\n\`\`\`\n\n"
fi
fi
# Find repeated modal open/close logic
MODAL_PATTERNS=$(grep -rn "useState.*false\|setIs.*Open\|setShow" src/components --include="*.tsx" 2>/dev/null | \
grep -v "node_modules" | wc -l || echo "0")
if [ "$MODAL_PATTERNS" -gt 30 ]; then
MODAL_EXAMPLES=$(grep -rn "const \[is.*Open, setIs.*Open\]\|const \[show.*Modal" src/components --include="*.tsx" 2>/dev/null | \
grep -v "node_modules" | head -15 || true)
ISSUES="${ISSUES}### Modal state patterns (${MODAL_PATTERNS} instances)\nConsider using a shared useModal hook:\n\`\`\`\n${MODAL_EXAMPLES}\n\`\`\`\n\n"
fi
# Find repeated dashboard grid patterns
DASHBOARD_GRIDS=$(grep -rn "grid.*grid-cols\|GridItem\|CardWrapper" src/pages --include="*.tsx" 2>/dev/null | \
grep -v "node_modules" | head -20 || true)
if [ -n "$DASHBOARD_GRIDS" ]; then
UNIQUE_PATTERNS=$(echo "$DASHBOARD_GRIDS" | grep -oE "grid-cols-[0-9]+|lg:grid-cols-[0-9]+" | sort | uniq -c | sort -rn)
if [ -n "$UNIQUE_PATTERNS" ]; then
ISSUES="${ISSUES}### Dashboard grid patterns (consider DashboardLayout component)\n\`\`\`\n${UNIQUE_PATTERNS}\n\`\`\`\n\n"
fi
fi
# Find cards not using the standardized card hooks
NON_STANDARD_CARDS=$(for f in $CARD_FILES; do
HAS_CARD_HOOK=$(grep -l "useCardData\|useCardDemoState" "$f" 2>/dev/null || true)
if [ -z "$HAS_CARD_HOOK" ]; then
HAS_STATE=$(grep -c "useState\|useEffect" "$f" 2>/dev/null || echo "0")
if [ "$HAS_STATE" -gt 2 ]; then
echo " - $(basename "$f") (${HAS_STATE} hooks, not using standardized card hooks)"
fi
fi
done | head -10)
if [ -n "$NON_STANDARD_CARDS" ]; then
ISSUES="${ISSUES}### Cards not using standardized hooks\nMigrate to useCardData/useCardDemoState:\n${NON_STANDARD_CARDS}\n\n"
fi
if [ -n "$ISSUES" ]; then
echo "found=true" >> "$GITHUB_OUTPUT"
printf "%b" "$ISSUES" > /tmp/focus-centralization.txt
echo "Centralization opportunities found"
else
echo "found=false" >> "$GITHUB_OUTPUT"
echo "Code appears well-centralized"
fi
# === INVENTORY DEMO DATA TESTING (every run) ===
- name: "Check: Inventory demo data coverage"
id: focus_inventory_demo
working-directory: web
continue-on-error: true
run: |
echo "Checking inventory items for demo data support..."
ISSUES=""
# Find card components without demo data functions
# Exclude: games (client-only), modals/dialogs (no data fetching), pure UI components
EXCLUDE_PATTERN="index\|test\|spec\|types\|utils\|KubeKong\|KubeSnake\|KubeBert\|WeatherAnimation\|NotificationVerifyIndicator\|Modal\|Dialog\|PremiumGauge\|HorseshoeGauge\|PortalTooltip\|AlertListItem\|LLMdFlowNodes\|EPPRoutingMetrics\|TreeRenderer\|NamespaceTreeNode\|GPUTaintFilter"
CARDS_NO_DEMO=""
for f in $(find src/components/cards -name "*.tsx" 2>/dev/null | grep -v "$EXCLUDE_PATTERN" | head -60); do
# Check if it's a card component (exports a component)
IS_CARD=$(grep -l "export.*function\|export const" "$f" 2>/dev/null || true)
if [ -n "$IS_CARD" ]; then
# Check for demo data patterns — includes hook-based demo data via useCached*/useCache
HAS_DEMO=$(grep -l "getDemoData\|DEMO_\|demoData\|useDemoMode\|isDemoMode\|useCardDemoState\|isDemoFallback\|isDemoData\|useCached" "$f" 2>/dev/null || true)
if [ -z "$HAS_DEMO" ]; then
LINES=$(wc -l < "$f" 2>/dev/null || echo "0")
if [ "$LINES" -gt 50 ]; then
CARDS_NO_DEMO="${CARDS_NO_DEMO} - \`$(basename "$f")\` (${LINES} lines) — no demo data support\n"
fi
fi
fi
done
if [ -n "$CARDS_NO_DEMO" ]; then
ISSUES="${ISSUES}### Cards without demo data support\nAdd demo data for offline/demo mode:\n${CARDS_NO_DEMO}\n\n"
fi
# Find components without Skeleton imports (for loading states)
# Cards using useCardLoadingState get skeletons via CardWrapper automatically
NO_SKELETON=""
for f in $(find src/components/cards -name "*.tsx" 2>/dev/null | grep -v "$EXCLUDE_PATTERN" | head -60); do
HAS_LOADING=$(grep -l "isLoading\|loading\|Loading" "$f" 2>/dev/null || true)
if [ -n "$HAS_LOADING" ]; then
HAS_SKELETON=$(grep -l "Skeleton\|CardSkeleton\|useCardLoadingState\|useReportCardDataState\|from.*Skeleton" "$f" 2>/dev/null || true)
if [ -z "$HAS_SKELETON" ]; then
NO_SKELETON="${NO_SKELETON} - \`$(basename "$f")\`\n"
fi
fi
done
if [ -n "$NO_SKELETON" ]; then
ISSUES="${ISSUES}### Cards with loading state but no Skeleton component\nUse Skeleton for consistent loading UX:\n${NO_SKELETON}\n\n"
fi
# Check INVENTORY.md listed items exist and have demo support
if [ -f "../INVENTORY.md" ]; then
# Extract card component names from inventory
INVENTORY_CARDS=$(grep -oE '[A-Z][A-Za-z]+Card\b|[A-Z][A-Za-z]+Monitor\b|[A-Z][A-Za-z]+Status\b' ../INVENTORY.md | sort -u | head -50 || true)
INVENTORY_NO_DEMO=""
for card in $INVENTORY_CARDS; do
CARD_FILE=$(find src -name "${card}.tsx" -o -name "${card}Card.tsx" 2>/dev/null | head -1)
if [ -n "$CARD_FILE" ]; then
# Check for demo data patterns — includes hook-based demo data via useCached*/useCache and useDemoMode
HAS_DEMO=$(grep -l "getDemoData\|DEMO_\|demoData\|useCardDemoState\|isDemoFallback\|isDemoData\|useCached\|useDemoMode\|isDemoMode" "$CARD_FILE" 2>/dev/null || true)
if [ -z "$HAS_DEMO" ]; then
INVENTORY_NO_DEMO="${INVENTORY_NO_DEMO} - \`${card}\`\n"
fi
fi
done
if [ -n "$INVENTORY_NO_DEMO" ]; then
ISSUES="${ISSUES}### INVENTORY.md items without demo data\nThese inventory-listed items need demo data:\n${INVENTORY_NO_DEMO}\n\n"
fi
fi
# Check for stat blocks without demo values
# Stat blocks in cards using useCached* hooks get demo data automatically
STAT_NO_DEMO=$(grep -rln "StatBlock\|StatCard\|stats\[" src/components --include="*.tsx" 2>/dev/null | while read f; do
HAS_DEMO=$(grep -l "demo\|Demo\|mock\|Mock\|sample\|Sample\|isDemoFallback\|useCached\|useClusters\|useMCP" "$f" 2>/dev/null || true)
if [ -z "$HAS_DEMO" ]; then
basename "$f"
fi
done | head -10 || true)
if [ -n "$STAT_NO_DEMO" ]; then
COUNT=$(echo "$STAT_NO_DEMO" | wc -l | tr -d ' ')
if [ "$COUNT" -gt 2 ]; then
ISSUES="${ISSUES}### Stat blocks without demo data (${COUNT} files)\n\`\`\`\n${STAT_NO_DEMO}\n\`\`\`\n\n"
fi
fi
if [ -n "$ISSUES" ]; then
echo "found=true" >> "$GITHUB_OUTPUT"
printf "%b" "$ISSUES" > /tmp/focus-inventory-demo.txt
echo "Demo data gaps found"
else
echo "found=false" >> "$GITHUB_OUTPUT"
echo "Demo data coverage looks good"
fi
# === CONSOLE ERROR PATTERNS (every run) ===
- name: "Check: Console error patterns"
id: focus_console_errors
working-directory: web
continue-on-error: true
run: |
echo "Checking for potential console error patterns..."
ISSUES=""
# Find unhandled promise rejections (async without catch)
UNHANDLED_ASYNC=$(grep -rn "async\s*(" src/ --include="*.tsx" --include="*.ts" 2>/dev/null | \
grep -v "node_modules\|\.test\.\|try\|catch" | head -20 || true)
# Cross-reference with files that don't have error handling
ASYNC_NO_ERROR=""
for f in $(echo "$UNHANDLED_ASYNC" | cut -d: -f1 | sort -u | head -15); do
if [ -n "$f" ] && [ -f "$f" ]; then
HAS_CATCH=$(grep -c "\.catch\|try.*catch\|onError" "$f" 2>/dev/null || echo "0")
HAS_ASYNC=$(grep -c "async\s*(" "$f" 2>/dev/null || echo "0")
if [ "$HAS_ASYNC" -gt 0 ] && [ "$HAS_CATCH" -eq 0 ]; then
ASYNC_NO_ERROR="${ASYNC_NO_ERROR} - \`${f}\` (${HAS_ASYNC} async functions, no error handling)\n"
fi
fi
done
if [ -n "$ASYNC_NO_ERROR" ]; then
ISSUES="${ISSUES}### Async functions without error handling\nThese may cause unhandled promise rejections:\n${ASYNC_NO_ERROR}\n\n"
fi
# Find fetch/API calls without error handling
FETCH_NO_ERROR=""
for f in $(grep -rln "fetch(\|axios\.\|api\." src/ --include="*.tsx" --include="*.ts" 2>/dev/null | head -20); do
if [ -f "$f" ]; then
HAS_CATCH=$(grep -c "\.catch\|try.*catch\|onError\|error:" "$f" 2>/dev/null || echo "0")
if [ "$HAS_CATCH" -eq 0 ]; then
FETCH_NO_ERROR="${FETCH_NO_ERROR} - \`$(basename "$f")\`\n"
fi
fi
done
if [ -n "$FETCH_NO_ERROR" ]; then
ISSUES="${ISSUES}### API calls without error handling\nThese may cause console errors on network failure:\n${FETCH_NO_ERROR}\n\n"
fi
# Find components without ErrorBoundary wrapping.
# Skip src/components/cards/ — all dynamic cards are wrapped by
# DynamicCardErrorBoundary in DynamicCard.tsx, so the per-file grep
# for "ErrorBoundary" produces false positives for registered cards.
COMPLEX_NO_BOUNDARY=$(for f in $(find src/components -name "*.tsx" -not -path "*/cards/*" 2>/dev/null | head -30); do
LINES=$(wc -l < "$f" 2>/dev/null || echo "0")
if [ "$LINES" -gt 100 ]; then
HAS_BOUNDARY=$(grep -l "ErrorBoundary\|error.*boundary\|componentDidCatch" "$f" 2>/dev/null || true)
if [ -z "$HAS_BOUNDARY" ]; then
echo " - \`$(basename "$f")\` (${LINES} lines)"
fi
fi
done | head -10)
if [ -n "$COMPLEX_NO_BOUNDARY" ]; then
ISSUES="${ISSUES}### Complex components without ErrorBoundary\nWrap in ErrorBoundary to prevent full-page crashes:\n${COMPLEX_NO_BOUNDARY}\n\n"
fi
# Find console.error usage without proper error handling.
# Check a 4-line window (the match line + 3 following) for user-visible
# handlers — the handler (showToast, setXxxError, fallback) is almost
# always on the next line, not the same one, so a line-only grep -v
# misses them. Patterns excluded: showToast, set<Name>Error,
# setYamlContent, generateMock* fallbacks, fallback/Fallback.
HANDLER_PATTERN='showToast|set[A-Z][a-zA-Z_]*Error|setYamlContent|generateMock|[Ff]allback'
CONSOLE_ERROR_CANDIDATES=$(grep -rn "console\.error" src/ --include="*.tsx" --include="*.ts" 2>/dev/null | \
grep -v "node_modules\|test\|spec" | \
grep -v "setError\|onError\|ErrorBoundary\|throw" || true)
CONSOLE_ERROR_ONLY=""
while IFS= read -r entry; do
[ -z "$entry" ] && continue
file="${entry%%:*}"
rest="${entry#*:}"
lineno="${rest%%:*}"
ctx=$(sed -n "${lineno},$((lineno + 3))p" "$file" 2>/dev/null)
if ! echo "$ctx" | grep -qE "$HANDLER_PATTERN"; then
CONSOLE_ERROR_ONLY="${CONSOLE_ERROR_ONLY}${entry}"$'\n'
fi
done <<< "$CONSOLE_ERROR_CANDIDATES"
CONSOLE_ERROR_ONLY=$(echo "$CONSOLE_ERROR_ONLY" | sed '/^$/d' | head -15)
if [ -n "$CONSOLE_ERROR_ONLY" ]; then
COUNT=$(echo "$CONSOLE_ERROR_ONLY" | wc -l | tr -d ' ')
if [ "$COUNT" -gt 5 ]; then
ISSUES="${ISSUES}### console.error without user-visible error handling (${COUNT})\nUsers should see error feedback, not just console:\n\`\`\`\n$(echo "$CONSOLE_ERROR_ONLY" | head -10)\n\`\`\`\n\n"
fi
fi
# Find null/undefined access patterns that cause console errors
UNSAFE_ACCESS=$(grep -rn "\.[a-zA-Z_]*\.[a-zA-Z_]*\.[a-zA-Z_]*" src/ --include="*.tsx" 2>/dev/null | \
grep -v "node_modules\|\?\.\|&&\|import\|from\|console\|window\." | head -15 || true)
if [ -n "$UNSAFE_ACCESS" ]; then
COUNT=$(echo "$UNSAFE_ACCESS" | wc -l | tr -d ' ')
if [ "$COUNT" -gt 10 ]; then
ISSUES="${ISSUES}### Deep property access without optional chaining\nMay cause 'Cannot read property of undefined' errors:\n\`\`\`\n$(echo "$UNSAFE_ACCESS" | head -8)\n\`\`\`\n\n"
fi
fi
if [ -n "$ISSUES" ]; then
echo "found=true" >> "$GITHUB_OUTPUT"
printf "%b" "$ISSUES" > /tmp/focus-console-errors.txt
echo "Console error patterns found"
else
echo "found=false" >> "$GITHUB_OUTPUT"
echo "No obvious console error patterns"
fi
# === NAVIGATION FLOW TESTING (every run) ===
- name: "Check: Button and action consistency"
id: focus_button_actions
working-directory: web
continue-on-error: true
run: |
echo "Checking button and action consistency..."
ISSUES=""
# Find onClick handlers that reference non-existent functions
UNDEFINED_HANDLERS=""
for f in $(find src -name "*.tsx" 2>/dev/null | grep -v node_modules | head -50); do
# Extract onClick handler names
HANDLERS=$(grep -oE "onClick=\{[A-Za-z_]+\}" "$f" 2>/dev/null | grep -oE "[A-Za-z_]+" | tail -n +2 || true)
for handler in $HANDLERS; do
# Check if handler is defined in the same file
DEFINED=$(grep -c "const ${handler}\|function ${handler}\|${handler}=" "$f" 2>/dev/null || echo "0")
# Check if it's a prop or hook return
IS_PROP=$(grep -c "${handler}.*:\|{ ${handler}\|${handler} }" "$f" 2>/dev/null || echo "0")
if [ "$DEFINED" -eq 0 ] && [ "$IS_PROP" -eq 0 ]; then
UNDEFINED_HANDLERS="${UNDEFINED_HANDLERS} - $(basename "$f"): \`${handler}\` - onClick references undefined function\n"
fi
done
done
if [ -n "$UNDEFINED_HANDLERS" ]; then
ISSUES="${ISSUES}### onClick handlers referencing potentially undefined functions\n${UNDEFINED_HANDLERS}\n\n"
fi
# Find buttons that open dialogs - check if dialog component exists
DIALOG_TRIGGERS=$(grep -rn "setIs.*Open(true)\|setShow.*Modal(true)\|open.*Dialog\|openModal" src/ --include="*.tsx" 2>/dev/null | \
grep -v "node_modules" | head -20 || true)
if [ -n "$DIALOG_TRIGGERS" ]; then
# Extract modal names and verify they exist
MODAL_NAMES=$(echo "$DIALOG_TRIGGERS" | grep -oE "setIs([A-Z][A-Za-z]+)Open\|setShow([A-Z][A-Za-z]+)Modal" | \
sed 's/setIs//;s/Open//;s/setShow//;s/Modal//' | sort -u || true)
MISSING_MODALS=""
for modal in $MODAL_NAMES; do
MODAL_FILE=$(find src -name "*${modal}*Modal*.tsx" -o -name "*${modal}*Dialog*.tsx" 2>/dev/null | head -1)
if [ -z "$MODAL_FILE" ]; then
MISSING_MODALS="${MISSING_MODALS} - \`${modal}\` — trigger found but no Modal/Dialog component\n"
fi
done
if [ -n "$MISSING_MODALS" ]; then
ISSUES="${ISSUES}### Modal triggers without corresponding components\n${MISSING_MODALS}\n\n"
fi
fi
# Find Link/navigate calls to routes - verify routes exist
ROUTE_REFS=$(grep -rn "navigate(['\"]/" src/ --include="*.tsx" 2>/dev/null | \
grep -oE "/[a-z-/]+" | sort -u | head -30 || true)
if [ -n "$ROUTE_REFS" ]; then
# Check if routes are defined in App.tsx or router
ROUTER_FILE=$(find src -name "App.tsx" -o -name "router.tsx" -o -name "routes.tsx" 2>/dev/null | head -1)
if [ -n "$ROUTER_FILE" ]; then
UNDEFINED_ROUTES=""
for route in $ROUTE_REFS; do
DEFINED=$(grep -c "path.*['\"]${route}['\"]" src/ -r --include="*.tsx" 2>/dev/null || echo "0")
if [ "$DEFINED" -eq 0 ]; then
UNDEFINED_ROUTES="${UNDEFINED_ROUTES} - \`${route}\`\n"
fi
done
if [ -n "$UNDEFINED_ROUTES" ]; then
ISSUES="${ISSUES}### navigate() calls to potentially undefined routes\n${UNDEFINED_ROUTES}\n\n"
fi
fi
fi
# Find empty onClick handlers
EMPTY_HANDLERS=$(grep -rn "onClick={() => {}}\|onClick={() => null}\|onClick={() => undefined}" src/ --include="*.tsx" 2>/dev/null | \
grep -v "node_modules" | head -10 || true)
if [ -n "$EMPTY_HANDLERS" ]; then
ISSUES="${ISSUES}### Empty onClick handlers (no-op buttons)\n\`\`\`\n${EMPTY_HANDLERS}\n\`\`\`\n\n"
fi
if [ -n "$ISSUES" ]; then
echo "found=true" >> "$GITHUB_OUTPUT"
printf "%b" "$ISSUES" > /tmp/focus-button-actions.txt
echo "Button action issues found"
else
echo "found=false" >> "$GITHUB_OUTPUT"
echo "Button actions look consistent"
fi
# === STALE DATA PATTERNS (every run) ===
- name: "Check: Stale data and freshness indicators"
id: focus_stale_data
working-directory: web
continue-on-error: true
run: |
echo "Checking for stale data patterns..."
ISSUES=""
# Find components with cache but no freshness indicator
CACHE_NO_TIMESTAMP=""
for f in $(grep -rln "useCached\|useQuery\|cache\|Cache" src/components --include="*.tsx" 2>/dev/null | head -30); do
HAS_TIMESTAMP=$(grep -l "lastUpdated\|lastFetched\|updatedAt\|refreshedAt\|timestamp\|Timestamp\|ago\|timeAgo" "$f" 2>/dev/null || true)
if [ -z "$HAS_TIMESTAMP" ]; then
CACHE_NO_TIMESTAMP="${CACHE_NO_TIMESTAMP} - \`$(basename "$f")\`\n"
fi
done
if [ -n "$CACHE_NO_TIMESTAMP" ]; then
ISSUES="${ISSUES}### Components with caching but no freshness indicator\nUsers should know when data was last updated:\n${CACHE_NO_TIMESTAMP}\n\n"
fi
# Find cards that don't respect demo mode toggle (check for demo mode subscription)
# Cards using useCached* hooks get demo mode handling automatically via useCache's useSyncExternalStore(subscribeDemoMode)
NO_DEMO_SUBSCRIPTION=""
for f in $(find src/components/cards -name "*.tsx" 2>/dev/null | grep -v "index\|test" | head -40); do
HAS_DEMO=$(grep -l "useDemoMode\|useCardDemoState\|isDemoMode\|subscribeDemoMode\|isDemoFallback\|isDemoData\|useCached" "$f" 2>/dev/null || true)
if [ -z "$HAS_DEMO" ]; then
# Only flag if it has data fetching (exclude useCached which handles demo internally)
HAS_FETCH=$(grep -l "fetch\|useQuery\|useEffect.*api" "$f" 2>/dev/null || true)
if [ -n "$HAS_FETCH" ]; then
NO_DEMO_SUBSCRIPTION="${NO_DEMO_SUBSCRIPTION} - \`$(basename "$f")\`\n"
fi
fi
done
if [ -n "$NO_DEMO_SUBSCRIPTION" ]; then
ISSUES="${ISSUES}### Cards with data fetching but no demo mode subscription\nMay not respond to demo mode toggle:\n${NO_DEMO_SUBSCRIPTION}\n\n"
fi
# Find components showing "Loading..." text instead of skeletons
LOADING_TEXT=$(grep -rn ">Loading\.\.\.<\|>Loading<\|Loading\.\.\.\`" src/components --include="*.tsx" 2>/dev/null | \
grep -v "Skeleton\|spinner\|Spinner" | \
grep -v "node_modules\|test" | head -15 || true)
if [ -n "$LOADING_TEXT" ]; then
ISSUES="${ISSUES}### 'Loading...' text instead of skeletons\nUse Skeleton components for better UX:\n\`\`\`\n${LOADING_TEXT}\n\`\`\`\n\n"
fi
# Find components that cache API data in localStorage without TTL validation.
# Uses file-level analysis: only flags files that both read AND write localStorage
# (indicating a caching pattern) but lack any TTL/expiry checking.
# Excludes: games, user preferences, auth tokens, UI state — these never expire by design.
STALE_STORAGE=""
for f in $(grep -rln "localStorage.setItem" src/components/ --include="*.tsx" --include="*.ts" 2>/dev/null); do
# Only check files that also read from localStorage (caching pattern)
HAS_GET=$(grep -l "localStorage.getItem" "$f" 2>/dev/null || true)
if [ -n "$HAS_GET" ]; then
# Skip if the file already has TTL/expiry checking
HAS_TTL=$(grep -lE "TTL|ttl|MAX_AGE|maxAge|expir|EXPIR|savedAt|WIZARD_STATE" "$f" 2>/dev/null || true)
if [ -z "$HAS_TTL" ]; then
# Skip game cards, user-preference storage, and UI state (never expire by design)
IS_PREF=$(grep -lE "Score|highScore|HighScore|BestTime|bestTime|high-score|Filter|filter|Repo|repo|History|history|width|height|sidebar|panel|world|location|stocks|draft|bookmark|Bookmark|pong|chess|kubedle|flappy|weather|Pref|pref|VIEW_MODE|BANNER|import\b" "$f" 2>/dev/null || true)
if [ -z "$IS_PREF" ]; then
STALE_STORAGE="${STALE_STORAGE} - $(basename "$f")\n"
fi
fi
fi
done
if [ -n "$STALE_STORAGE" ]; then
COUNT=$(echo -e "$STALE_STORAGE" | grep -c "^\s*-" || true)
if [ "$COUNT" -gt 2 ]; then
ISSUES="${ISSUES}### Components caching API data in localStorage without TTL (${COUNT})\nConsider adding TTL or timestamp validation:\n${STALE_STORAGE}\n\n"
fi
fi
if [ -n "$ISSUES" ]; then
echo "found=true" >> "$GITHUB_OUTPUT"
printf "%b" "$ISSUES" > /tmp/focus-stale-data.txt
echo "Stale data patterns found"
else
echo "found=false" >> "$GITHUB_OUTPUT"
echo "Data freshness looks good"
fi
# === COLOR CONSISTENCY (every run) ===
- name: "Check: Color consistency across components"
id: focus_color_consistency
working-directory: web
continue-on-error: true
run: |
echo "Checking color consistency across components..."
ISSUES=""
# Extract all color classes used in cards
CARD_COLORS=$(grep -rhoE "(text|bg|border|ring)-[a-z]+-[0-9]+(/[0-9]+)?" src/components/cards --include="*.tsx" 2>/dev/null | \
sort | uniq -c | sort -rn | head -30 || true)
# Find inconsistent status colors (success should be green, error should be red, etc.)
# Check for red being used for success or green for errors
WRONG_SUCCESS=$(grep -rn "success\|Success\|healthy\|Healthy" src/components --include="*.tsx" 2>/dev/null | \
grep -i "red-\|orange-\|yellow-" | grep -v "node_modules" | head -10 || true)
if [ -n "$WRONG_SUCCESS" ]; then
ISSUES="${ISSUES}### Success/healthy states using non-green colors\n\`\`\`\n${WRONG_SUCCESS}\n\`\`\`\n\n"
fi
WRONG_ERROR=$(grep -rn "error\|Error\|failed\|Failed\|critical\|Critical" src/components --include="*.tsx" 2>/dev/null | \
grep -i "green-\|blue-" | grep -v "node_modules\|ErrorBoundary\|errorMessage" | head -10 || true)
if [ -n "$WRONG_ERROR" ]; then
ISSUES="${ISSUES}### Error/failed states using non-red colors\n\`\`\`\n${WRONG_ERROR}\n\`\`\`\n\n"
fi
# Find inconsistent color shades for similar purposes
# Extract unique color patterns per component type
STATUS_COLORS=$(grep -rn "status\|Status" src/components --include="*.tsx" 2>/dev/null | \
grep -oE "(text|bg)-[a-z]+-[0-9]+(/[0-9]+)?" | sort | uniq -c | sort -rn | head -15 || true)
if [ -n "$STATUS_COLORS" ]; then
# Count unique shades per color family
GREEN_SHADES=$(echo "$STATUS_COLORS" | grep "green" | wc -l || echo "0")
RED_SHADES=$(echo "$STATUS_COLORS" | grep "red" | wc -l || echo "0")
YELLOW_SHADES=$(echo "$STATUS_COLORS" | grep "yellow" | wc -l || echo "0")
if [ "$GREEN_SHADES" -gt 3 ] || [ "$RED_SHADES" -gt 3 ] || [ "$YELLOW_SHADES" -gt 3 ]; then
ISSUES="${ISSUES}### Inconsistent color shades for status indicators\nStandardize on consistent shades:\n\`\`\`\nGreen variants: ${GREEN_SHADES}\nRed variants: ${RED_SHADES}\nYellow variants: ${YELLOW_SHADES}\n\nMost common:\n${STATUS_COLORS}\n\`\`\`\n\n"
fi
fi
# Find demo badge colors - should be consistent
DEMO_BADGE_COLORS=$(grep -rn "Demo\|demo" src/components --include="*.tsx" 2>/dev/null | \
grep -oE "(text|bg|border)-[a-z]+-[0-9]+(/[0-9]+)?" | sort | uniq -c | sort -rn | head -10 || true)
if [ -n "$DEMO_BADGE_COLORS" ]; then
UNIQUE_DEMO=$(echo "$DEMO_BADGE_COLORS" | grep -E "amber|yellow|orange" | wc -l || echo "0")
if [ "$UNIQUE_DEMO" -gt 4 ]; then
ISSUES="${ISSUES}### Multiple color variants for demo badges\nStandardize demo badge colors:\n\`\`\`\n${DEMO_BADGE_COLORS}\n\`\`\`\n\n"
fi
fi
# Find cards using different opacity patterns
OPACITY_PATTERNS=$(grep -rhoE "(bg|text|border)-[a-z]+-[0-9]+/[0-9]+" src/components/cards --include="*.tsx" 2>/dev/null | \
grep -oE "/[0-9]+" | sort | uniq -c | sort -rn | head -10 || true)
if [ -n "$OPACITY_PATTERNS" ]; then
UNIQUE_OPACITIES=$(echo "$OPACITY_PATTERNS" | wc -l | tr -d ' ')
if [ "$UNIQUE_OPACITIES" -gt 6 ]; then
ISSUES="${ISSUES}### Many different opacity values (${UNIQUE_OPACITIES})\nStandardize on fewer opacity levels (10, 20, 50, etc.):\n\`\`\`\n${OPACITY_PATTERNS}\n\`\`\`\n\n"
fi
fi
# Check for purple vs violet inconsistency (common in this codebase)
PURPLE_COUNT=$(grep -rc "purple-" src/components --include="*.tsx" 2>/dev/null | awk -F: '{sum+=$2} END {print sum}' || echo "0")
VIOLET_COUNT=$(grep -rc "violet-" src/components --include="*.tsx" 2>/dev/null | awk -F: '{sum+=$2} END {print sum}' || echo "0")
if [ "$PURPLE_COUNT" -gt 10 ] && [ "$VIOLET_COUNT" -gt 10 ]; then
ISSUES="${ISSUES}### Mixed purple (${PURPLE_COUNT}) and violet (${VIOLET_COUNT}) usage\nPick one color family for consistency\n\n"
fi
if [ -n "$ISSUES" ]; then
echo "found=true" >> "$GITHUB_OUTPUT"
printf "%b" "$ISSUES" > /tmp/focus-color-consistency.txt
echo "Color consistency issues found"
else
echo "found=false" >> "$GITHUB_OUTPUT"
echo "Color usage looks consistent"
fi
# === ACTIVE USER COUNT FEATURE (every run) ===
- name: "Check: Active user count feature (PERMANENTLY DISABLED)"
id: focus_user_count
continue-on-error: true
run: |
# Active user count in navbar is explicitly not wanted by maintainer.
# This check is permanently disabled to prevent re-filing.
echo "found=false" >> "$GITHUB_OUTPUT"
echo "Skipped — feature explicitly rejected"
# === TOKEN COUNTER FEATURE (every run) ===
- name: "Check: Token counter feature"
id: focus_token_counter
working-directory: web
continue-on-error: true
run: |
echo "Checking token counter feature implementation..."
ISSUES=""
# Find token counter component/feature
TOKEN_FILES=$(find src -name "*token*" -o -name "*Token*" 2>/dev/null | grep -i "count\|counter\|usage\|meter" | grep "\.tsx\|\.ts" || true)
TOKEN_HOOKS=$(grep -rln "tokenCount\|tokenUsage\|tokensUsed\|tokenLimit\|TokenCounter\|tokenMeter" src/ --include="*.tsx" --include="*.ts" 2>/dev/null || true)
if [ -z "$TOKEN_FILES" ] && [ -z "$TOKEN_HOOKS" ]; then
ISSUES="${ISSUES}### No token counter feature found\nConsider adding token usage tracking for LLM-D features\n\n"
else
# Check for visual progress indicator
TOKEN_VISUAL=$(grep -rln "Progress\|progress\|meter\|Meter\|bar\|Bar" src/ --include="*token*" --include="*Token*" 2>/dev/null | head -1 || true)
if [ -z "$TOKEN_VISUAL" ]; then
TOKEN_VISUAL2=$(grep -rln "tokenCount\|tokenUsage" src/ --include="*.tsx" 2>/dev/null | while read f; do
HAS_VISUAL=$(grep -l "Progress\|progress\|%\|percent" "$f" 2>/dev/null || true)
if [ -z "$HAS_VISUAL" ]; then
basename "$f"
fi
done | head -3 || true)
if [ -n "$TOKEN_VISUAL2" ]; then
ISSUES="${ISSUES}### Token counter without visual progress indicator\nAdd a progress bar or percentage display:\n\`\`\`\n${TOKEN_VISUAL2}\n\`\`\`\n\n"
fi
fi
# Check for limit/warning thresholds
TOKEN_LIMITS=$(grep -rn "tokenLimit\|maxTokens\|TOKEN_LIMIT\|token.*threshold" src/ --include="*.tsx" --include="*.ts" 2>/dev/null | head -5 || true)
if [ -z "$TOKEN_LIMITS" ]; then
ISSUES="${ISSUES}### Token counter without usage limits\nAdd configurable token limits with warnings\n\n"
fi
# Check for demo mode in token counter
TOKEN_DEMO=$(grep -rln "token" src/ --include="*.tsx" 2>/dev/null | grep -i "count\|usage\|meter" | while read f; do
HAS_DEMO=$(grep -l "demo\|Demo\|mock\|Mock\|isDemoMode" "$f" 2>/dev/null || true)
if [ -z "$HAS_DEMO" ]; then
basename "$f"
fi
done | head -5 || true)
if [ -n "$TOKEN_DEMO" ]; then
ISSUES="${ISSUES}### Token counter without demo mode handling\n\`\`\`\n${TOKEN_DEMO}\n\`\`\`\n\n"
fi
# Check for reset functionality
TOKEN_RESET=$(grep -rn "resetToken\|clearToken\|token.*reset\|reset.*token" src/ --include="*.tsx" --include="*.ts" 2>/dev/null | head -1 || true)
if [ -z "$TOKEN_RESET" ]; then
ISSUES="${ISSUES}### Token counter without reset functionality\nAllow users to reset/clear token count\n\n"
fi
# Check for persistence
TOKEN_PERSIST=$(grep -rln "localStorage.*token\|token.*localStorage\|persist.*token\|token.*persist" src/ --include="*.tsx" --include="*.ts" 2>/dev/null | head -1 || true)
if [ -z "$TOKEN_PERSIST" ]; then
ISSUES="${ISSUES}### Token usage not persisted\nSave token count across sessions\n\n"
fi
# Check for predictions/estimates integration
TOKEN_PREDICT=$(grep -rn "predict\|estimate\|forecast\|remaining" src/ --include="*token*" --include="*Token*" 2>/dev/null | head -3 || true)
if [ -z "$TOKEN_PREDICT" ]; then
ISSUES="${ISSUES}### Token counter without predictions\nConsider showing estimated tokens remaining or cost\n\n"
fi
fi
if [ -n "$ISSUES" ]; then
echo "found=true" >> "$GITHUB_OUTPUT"
printf "%b" "$ISSUES" > /tmp/focus-token-counter.txt
echo "Token counter issues found"
else
echo "found=false" >> "$GITHUB_OUTPUT"
echo "Token counter implementation looks good"
fi
# === TOUR AND ONBOARDING (every run) ===
- name: "Check: Tour and onboarding coverage"
id: focus_tour
working-directory: web
continue-on-error: true
run: |
echo "Checking tour and onboarding coverage..."
ISSUES=""
# Find tour step definitions
TOUR_FILES=$(find src -name "*tour*" -o -name "*Tour*" -o -name "*onboarding*" 2>/dev/null | grep "\.tsx\|\.ts" || true)
TOUR_STEPS=$(grep -rn "step\|Step\|target\|selector" src/ --include="*tour*" --include="*Tour*" 2>/dev/null | head -50 || true)
# Check if tour targets exist in the DOM
if [ -n "$TOUR_STEPS" ]; then
# Extract target selectors/IDs from tour definitions
TARGETS=$(echo "$TOUR_STEPS" | grep -oE "target:\s*['\"][^'\"]+['\"]|selector:\s*['\"][^'\"]+['\"]|id:\s*['\"][^'\"]+['\"]" | \
grep -oE "['\"][^'\"]+['\"]" | tr -d "'\"\`" | sort -u || true)
MISSING_TARGETS=""
for target in $TARGETS; do
# Skip CSS class selectors and data attributes
case "$target" in
.*|#*|\[*) continue ;;
esac
# Search for this ID in components
FOUND=$(grep -rl "id=['\"]${target}['\"]" src/ --include="*.tsx" 2>/dev/null | head -1 || true)
if [ -z "$FOUND" ]; then
MISSING_TARGETS="${MISSING_TARGETS} - \`${target}\` — tour target not found in DOM\n"
fi
done
if [ -n "$MISSING_TARGETS" ]; then
ISSUES="${ISSUES}### Tour targets not found in components\nThese tour steps may fail:\n${MISSING_TARGETS}\n\n"
fi
fi
# Check for pages/dashboards without tour coverage
PAGES_NO_TOUR=""
for f in $(find src/pages -name "*.tsx" 2>/dev/null | head -20); do
PAGE_NAME=$(basename "$f" .tsx)
# Check if this page is covered by a tour
TOUR_COVERAGE=$(grep -rl "${PAGE_NAME}\|$(echo "$PAGE_NAME" | tr '[:upper:]' '[:lower:]')" src/*tour* src/*Tour* 2>/dev/null | head -1 || true)
if [ -z "$TOUR_COVERAGE" ]; then
PAGES_NO_TOUR="${PAGES_NO_TOUR} - \`${PAGE_NAME}\`\n"
fi
done
if [ -n "$PAGES_NO_TOUR" ]; then
ISSUES="${ISSUES}### Pages without tour coverage\nConsider adding tours for new users:\n${PAGES_NO_TOUR}\n\n"
fi
# Check for tour reset/restart functionality
TOUR_RESET=$(grep -rln "resetTour\|restartTour\|clearTour\|tour.*reset" src/ --include="*.tsx" --include="*.ts" 2>/dev/null || true)
if [ -z "$TOUR_RESET" ]; then
ISSUES="${ISSUES}### No tour reset functionality found\nUsers should be able to replay tours from Settings\n\n"
fi
# Check for tour persistence (don't show again)
TOUR_PERSISTENCE=$(grep -rln "localStorage.*tour\|tour.*localStorage\|completedTour\|tourCompleted\|hasSeenTour" src/ --include="*.tsx" --include="*.ts" 2>/dev/null || true)
if [ -z "$TOUR_PERSISTENCE" ]; then
ISSUES="${ISSUES}### No tour completion persistence\nTours should remember if user has seen them\n\n"
fi
# Check for tour content accessibility
TOUR_A11Y=$(grep -rn "Tour\|tour" src/ --include="*.tsx" 2>/dev/null | \
grep -v "aria-\|role=\|tabIndex" | grep -v "node_modules\|import\|type" | head -10 || true)
if [ -n "$TOUR_A11Y" ]; then
COUNT=$(echo "$TOUR_A11Y" | wc -l | tr -d ' ')
if [ "$COUNT" -gt 3 ]; then
ISSUES="${ISSUES}### Tour components may need accessibility improvements\nEnsure tour modals have proper ARIA attributes\n\n"
fi
fi
# Check for tooltip components used in tours
TOUR_TOOLTIPS=$(grep -rln "Tooltip\|tooltip" src/*tour* src/*Tour* 2>/dev/null | wc -l || echo "0")
if [ "$TOUR_TOOLTIPS" -eq 0 ]; then
# Check if any tour system exists
HAS_TOUR=$(find src -name "*tour*" -o -name "*Tour*" 2>/dev/null | wc -l || echo "0")
if [ "$HAS_TOUR" -eq 0 ]; then
ISSUES="${ISSUES}### No tour/onboarding system detected\nConsider adding guided tours for new users (react-joyride, intro.js, etc.)\n\n"
fi
fi
if [ -n "$ISSUES" ]; then
echo "found=true" >> "$GITHUB_OUTPUT"
printf "%b" "$ISSUES" > /tmp/focus-tour.txt
echo "Tour issues found"
else
echo "found=false" >> "$GITHUB_OUTPUT"
echo "Tour coverage looks good"
fi
# === LOCAL CLUSTER DETECTION AND CREATION (every run) ===
- name: "Check: Local cluster detection and creation"
id: focus_local_clusters
working-directory: web
continue-on-error: true
run: |
echo "Checking local cluster detection and creation feature..."
ISSUES=""
# Check for local cluster hooks/components
LOCAL_CLUSTER_FILES=$(find src -name "*localCluster*" -o -name "*LocalCluster*" 2>/dev/null | grep "\.tsx\|\.ts" || true)
LOCAL_CLUSTER_HOOKS=$(grep -rln "useLocalCluster\|localClusterTools\|LocalClustersSection\|kind\|k3d\|minikube" src/ --include="*.tsx" --include="*.ts" 2>/dev/null | head -20 || true)
if [ -z "$LOCAL_CLUSTER_FILES" ] && [ -z "$LOCAL_CLUSTER_HOOKS" ]; then
ISSUES="${ISSUES}### No local cluster feature found\nConsider adding detection for kind, k3d, minikube clusters\n\n"
else
# Check for tool detection (kind, k3d, minikube)
TOOL_DETECTION=$(grep -rn "kind\|k3d\|minikube" src/ --include="*localCluster*" --include="*LocalCluster*" 2>/dev/null || true)
if [ -z "$TOOL_DETECTION" ]; then
TOOL_DETECTION2=$(grep -rn "'kind'\|'k3d'\|'minikube'" src/ --include="*.tsx" --include="*.ts" 2>/dev/null | head -10 || true)
if [ -z "$TOOL_DETECTION2" ]; then
ISSUES="${ISSUES}### Local cluster tool detection not implemented\nAdd detection for kind, k3d, and minikube\n\n"
fi
fi
# Check for cluster creation functionality
CREATE_CLUSTER=$(grep -rn "createCluster\|create.*cluster\|cluster.*create" src/ --include="*.tsx" --include="*.ts" 2>/dev/null | head -5 || true)
if [ -z "$CREATE_CLUSTER" ]; then
ISSUES="${ISSUES}### No cluster creation functionality found\nAllow users to create local clusters from the UI\n\n"
fi
# Check for cluster deletion functionality
DELETE_CLUSTER=$(grep -rn "deleteCluster\|delete.*cluster\|cluster.*delete\|removeCluster" src/ --include="*.tsx" --include="*.ts" 2>/dev/null | head -5 || true)
if [ -z "$DELETE_CLUSTER" ]; then
ISSUES="${ISSUES}### No cluster deletion functionality found\nAllow users to delete local clusters from the UI\n\n"
fi
# Check for cluster status display
CLUSTER_STATUS=$(grep -rn "cluster.*status\|status.*running\|status.*stopped" src/ --include="*localCluster*" --include="*LocalCluster*" 2>/dev/null | head -5 || true)
if [ -z "$CLUSTER_STATUS" ]; then
CLUSTER_STATUS2=$(grep -rn "'running'\|'stopped'\|'unknown'" src/ --include="*.tsx" 2>/dev/null | grep -i "cluster\|status" | head -5 || true)
if [ -z "$CLUSTER_STATUS2" ]; then
ISSUES="${ISSUES}### Cluster status not displayed\nShow running/stopped status for each cluster\n\n"
fi
fi
# Check for agent connectivity requirement
AGENT_CHECK=$(grep -rn "isConnected\|useLocalAgent\|agent.*connected" src/ --include="*localCluster*" --include="*LocalCluster*" 2>/dev/null | head -5 || true)
if [ -z "$AGENT_CHECK" ]; then
ISSUES="${ISSUES}### No agent connectivity check\nVerify local agent is connected before showing cluster management\n\n"
fi
# Check for loading states during operations
LOADING_STATES=$(grep -rn "isLoading\|isCreating\|isDeleting\|Loading\|Loader" src/ --include="*localCluster*" --include="*LocalCluster*" 2>/dev/null | head -5 || true)
if [ -z "$LOADING_STATES" ]; then
ISSUES="${ISSUES}### Loading states not shown during operations\nShow spinners during create/delete operations\n\n"
fi
# Check for error handling
ERROR_HANDLING=$(grep -rn "error\|Error\|setError\|catch" src/ --include="*localCluster*" --include="*LocalCluster*" 2>/dev/null | head -5 || true)
if [ -z "$ERROR_HANDLING" ]; then
ISSUES="${ISSUES}### Error handling not implemented\nShow errors when cluster operations fail\n\n"
fi
# Check for installation instructions when no tools found
INSTALL_HELP=$(grep -rn "brew install\|apt install\|install.*kind\|install.*k3d\|install.*minikube" src/ --include="*.tsx" 2>/dev/null | head -3 || true)
if [ -z "$INSTALL_HELP" ]; then
ISSUES="${ISSUES}### No installation instructions for missing tools\nGuide users to install kind/k3d/minikube when not detected\n\n"
fi
# Check for refresh functionality
REFRESH_FUNC=$(grep -rn "refresh\|Refresh\|fetchCluster" src/ --include="*localCluster*" --include="*LocalCluster*" 2>/dev/null | head -5 || true)
if [ -z "$REFRESH_FUNC" ]; then
ISSUES="${ISSUES}### No refresh functionality\nAllow users to refresh cluster list manually\n\n"
fi
# Check for demo mode handling
DEMO_MODE=$(grep -rln "demo\|Demo\|isDemoMode" src/ --include="*localCluster*" --include="*LocalCluster*" 2>/dev/null | head -3 || true)
if [ -z "$DEMO_MODE" ]; then
ISSUES="${ISSUES}### No demo mode handling in local clusters\nShow mock clusters when in demo mode\n\n"
fi
fi
if [ -n "$ISSUES" ]; then
echo "found=true" >> "$GITHUB_OUTPUT"
printf "%b" "$ISSUES" > /tmp/focus-local-clusters.txt
echo "Local cluster issues found"
else
echo "found=false" >> "$GITHUB_OUTPUT"
echo "Local cluster implementation looks good"
fi
# === REFRESH ICON ANIMATION (every run) ===
- name: "Check: Refresh icon animation consistency"
id: focus_refresh_spin
working-directory: web
continue-on-error: true
run: |
echo "Checking refresh icon animation consistency..."
ISSUES=""
# Find all refresh icon usages
REFRESH_ICONS=$(grep -rn "RefreshCw\|RefreshCcw\|refresh\|Refresh" src/ --include="*.tsx" 2>/dev/null | \
grep -v "node_modules\|\.test\." | head -100 || true)
if [ -n "$REFRESH_ICONS" ]; then
# Check for animate-spin on refresh icons
MISSING_SPIN=""
for f in $(grep -rl "RefreshCw\|RefreshCcw" src/ --include="*.tsx" 2>/dev/null | head -50); do
# Check if file has animate-spin with refresh icon
HAS_SPIN=$(grep -n "RefreshCw\|RefreshCcw" "$f" | while read line; do
LINE_NO=$(echo "$line" | cut -d: -f1)
# Check context around the line (5 lines before/after) for animate-spin
HAS_ANIMATE=$(sed -n "$((LINE_NO > 5 ? LINE_NO - 5 : 1)),$((LINE_NO + 5))p" "$f" | grep "animate-spin" || true)
if [ -z "$HAS_ANIMATE" ]; then
echo "missing:$LINE_NO"
fi
done | head -3 || true)
if [ -n "$HAS_SPIN" ]; then
RELATIVE_PATH=$(echo "$f" | sed 's|^src/||')
MISSING_SPIN="${MISSING_SPIN} - \`${RELATIVE_PATH}\` — refresh icon without animate-spin\n"
fi
done
if [ -n "$MISSING_SPIN" ]; then
ISSUES="${ISSUES}### Refresh icons without spin animation\nThese components may not show loading feedback:\n${MISSING_SPIN}\n\n"
fi
# Check that animation triggers on loading state
SPIN_WITHOUT_LOADING=""
for f in $(grep -rl "animate-spin" src/ --include="*.tsx" 2>/dev/null | head -50); do
# Check if animate-spin is tied to a loading/isLoading condition
HAS_LOADING_CONDITION=$(grep -n "animate-spin" "$f" | while read line; do
LINE_NO=$(echo "$line" | cut -d: -f1)
# Check if line or nearby lines have loading state check
CONTEXT=$(sed -n "$((LINE_NO > 3 ? LINE_NO - 3 : 1)),$((LINE_NO + 3))p" "$f")
HAS_CONDITION=$(echo "$CONTEXT" | grep -E "isLoading|loading|isRefreshing|isFetching|\? " || true)
if [ -z "$HAS_CONDITION" ]; then
echo "unconditional:$LINE_NO"
fi
done | head -1 || true)
if [ -n "$HAS_LOADING_CONDITION" ]; then
RELATIVE_PATH=$(echo "$f" | sed 's|^src/||')
SPIN_WITHOUT_LOADING="${SPIN_WITHOUT_LOADING} - \`${RELATIVE_PATH}\` — animate-spin may not be tied to loading state\n"
fi
done
if [ -n "$SPIN_WITHOUT_LOADING" ]; then
ISSUES="${ISSUES}### Spin animation not tied to loading state\nAnimation should only spin during loading:\n${SPIN_WITHOUT_LOADING}\n\n"
fi
# Check for consistent spin duration (full rotation)
# Look for any custom animation durations on refresh icons
CUSTOM_DURATION=$(grep -rn "RefreshCw\|RefreshCcw" src/ --include="*.tsx" 2>/dev/null | \
grep -E "duration|transition|animation:" | head -10 || true)
if [ -n "$CUSTOM_DURATION" ]; then
ISSUES="${ISSUES}### Custom animation durations on refresh icons\nEnsure all refresh icons complete a full rotation:\n\`\`\`\n$(echo "$CUSTOM_DURATION" | head -5)\n\`\`\`\n\n"
fi
# Check inventory items for refresh functionality
INVENTORY_FILES=$(find src -path "*card*" -name "*.tsx" 2>/dev/null | head -50)
CARDS_WITHOUT_REFRESH=""
for f in $INVENTORY_FILES; do
# Skip non-card files
case "$f" in
*index*|*types*|*utils*|*hooks*) continue ;;
esac
# Check if it's a card that might need refresh
HAS_DATA_FETCH=$(grep -l "useQuery\|useFetch\|fetch\|axios" "$f" 2>/dev/null || true)
if [ -n "$HAS_DATA_FETCH" ]; then
HAS_REFRESH=$(grep -l "RefreshCw\|RefreshCcw\|refetch\|refresh" "$f" 2>/dev/null || true)
if [ -z "$HAS_REFRESH" ]; then
RELATIVE_PATH=$(echo "$f" | sed 's|^src/||')
CARDS_WITHOUT_REFRESH="${CARDS_WITHOUT_REFRESH} - \`${RELATIVE_PATH}\`\n"
fi
fi
done
if [ -n "$CARDS_WITHOUT_REFRESH" ]; then
FIRST_FEW=$(echo -e "$CARDS_WITHOUT_REFRESH" | head -10)
COUNT=$(echo -e "$CARDS_WITHOUT_REFRESH" | grep -c "^" || echo "0")
ISSUES="${ISSUES}### Inventory cards without refresh icon\n${COUNT} cards fetch data but don't have refresh:\n${FIRST_FEW}\n\n"
fi
fi
if [ -n "$ISSUES" ]; then
echo "found=true" >> "$GITHUB_OUTPUT"
printf "%b" "$ISSUES" > /tmp/focus-refresh-spin.txt
echo "Refresh icon issues found"
else
echo "found=false" >> "$GITHUB_OUTPUT"
echo "Refresh icon animations look good"
fi
# === DOM NESTING AND STRUCTURE ERRORS (every run) ===
- name: "Check: DOM nesting and structure violations"
id: focus_dom_errors
working-directory: web
continue-on-error: true
run: |
echo "Checking for DOM nesting and structure violations..."
ISSUES=""
# Check for button inside button (most common violation)
BUTTON_NESTING=$(grep -rn "<button" src/ --include="*.tsx" 2>/dev/null | while read line; do
FILE=$(echo "$line" | cut -d: -f1)
LINE_NO=$(echo "$line" | cut -d: -f2)
# Get context: 20 lines after this button opening
CONTEXT=$(sed -n "${LINE_NO},$((LINE_NO + 20))p" "$FILE" 2>/dev/null)
# Check if there's another button before the closing tag
NESTED=$(echo "$CONTEXT" | grep -n "<button" | tail -n +2 | head -1 || true)
if [ -n "$NESTED" ]; then
RELATIVE=$(echo "$FILE" | sed 's|^src/||')
echo " - \`${RELATIVE}:${LINE_NO}\` — potential nested button"
fi
done | head -10 || true)
if [ -n "$BUTTON_NESTING" ]; then
ISSUES="${ISSUES}### Button inside button violations\nThese cause React DOM nesting warnings:\n${BUTTON_NESTING}\n\n"
fi
# Check for interactive elements inside anchor tags
ANCHOR_NESTING=$(grep -rn "<a " src/ --include="*.tsx" 2>/dev/null | while read line; do
FILE=$(echo "$line" | cut -d: -f1)
LINE_NO=$(echo "$line" | cut -d: -f2)
CONTEXT=$(sed -n "${LINE_NO},$((LINE_NO + 15))p" "$FILE" 2>/dev/null)
# Check for button/input/select inside anchor
NESTED=$(echo "$CONTEXT" | grep -E "<button|<input|<select" | head -1 || true)
if [ -n "$NESTED" ]; then
RELATIVE=$(echo "$FILE" | sed 's|^src/||')
echo " - \`${RELATIVE}:${LINE_NO}\` — interactive element inside anchor"
fi
done | head -10 || true)
if [ -n "$ANCHOR_NESTING" ]; then
ISSUES="${ISSUES}### Interactive elements inside anchor tags\nButtons/inputs cannot be inside <a> tags:\n${ANCHOR_NESTING}\n\n"
fi
# Check for div inside p (block in inline)
DIV_IN_P=$(grep -rn "<p[^>]*>" src/ --include="*.tsx" 2>/dev/null | while read line; do
FILE=$(echo "$line" | cut -d: -f1)
LINE_NO=$(echo "$line" | cut -d: -f2)
CONTEXT=$(sed -n "${LINE_NO},$((LINE_NO + 10))p" "$FILE" 2>/dev/null)
NESTED=$(echo "$CONTEXT" | grep -E "<div|<section|<article|<header|<footer" | head -1 || true)
if [ -n "$NESTED" ]; then
RELATIVE=$(echo "$FILE" | sed 's|^src/||')
echo " - \`${RELATIVE}:${LINE_NO}\` — block element inside <p>"
fi
done | head -10 || true)
if [ -n "$DIV_IN_P" ]; then
ISSUES="${ISSUES}### Block elements inside paragraph tags\nDiv/section/article cannot be inside <p>:\n${DIV_IN_P}\n\n"
fi
# Check for forms inside forms
FORM_NESTING=$(grep -rn "<form" src/ --include="*.tsx" 2>/dev/null | while read line; do
FILE=$(echo "$line" | cut -d: -f1)
LINE_NO=$(echo "$line" | cut -d: -f2)
CONTEXT=$(sed -n "${LINE_NO},$((LINE_NO + 50))p" "$FILE" 2>/dev/null)
NESTED=$(echo "$CONTEXT" | grep -n "<form" | tail -n +2 | head -1 || true)
if [ -n "$NESTED" ]; then
RELATIVE=$(echo "$FILE" | sed 's|^src/||')
echo " - \`${RELATIVE}:${LINE_NO}\` — nested form element"
fi
done | head -5 || true)
if [ -n "$FORM_NESTING" ]; then
ISSUES="${ISSUES}### Nested form elements\nForms cannot be nested inside other forms:\n${FORM_NESTING}\n\n"
fi
# Check for table structure violations
TABLE_ISSUES=$(grep -rn "<table" src/ --include="*.tsx" 2>/dev/null | while read line; do
FILE=$(echo "$line" | cut -d: -f1)
LINE_NO=$(echo "$line" | cut -d: -f2)
CONTEXT=$(sed -n "${LINE_NO},$((LINE_NO + 30))p" "$FILE" 2>/dev/null)
# Check for div directly inside table (should be tbody/thead)
BAD_CHILD=$(echo "$CONTEXT" | grep -E "<table[^>]*>\s*<div|</thead>\s*<div|</tbody>\s*<div" | head -1 || true)
if [ -n "$BAD_CHILD" ]; then
RELATIVE=$(echo "$FILE" | sed 's|^src/||')
echo " - \`${RELATIVE}:${LINE_NO}\` — invalid table child element"
fi
done | head -5 || true)
if [ -n "$TABLE_ISSUES" ]; then
ISSUES="${ISSUES}### Table structure violations\nTable must have proper thead/tbody/tr structure:\n${TABLE_ISSUES}\n\n"
fi
# Check for li outside ul/ol
LI_OUTSIDE=$(grep -rn "<li" src/ --include="*.tsx" 2>/dev/null | while read line; do
FILE=$(echo "$line" | cut -d: -f1)
LINE_NO=$(echo "$line" | cut -d: -f2)
# Check 10 lines before for ul/ol
CONTEXT=$(sed -n "$((LINE_NO > 10 ? LINE_NO - 10 : 1)),${LINE_NO}p" "$FILE" 2>/dev/null)
HAS_LIST=$(echo "$CONTEXT" | grep -E "<ul|<ol" || true)
if [ -z "$HAS_LIST" ]; then
RELATIVE=$(echo "$FILE" | sed 's|^src/||')
echo " - \`${RELATIVE}:${LINE_NO}\` — <li> may be outside <ul>/<ol>"
fi
done | head -5 || true)
if [ -n "$LI_OUTSIDE" ]; then
ISSUES="${ISSUES}### List item outside list container\n<li> elements must be inside <ul> or <ol>:\n${LI_OUTSIDE}\n\n"
fi
# Check for heading nesting violations (h1 inside h2, etc.)
HEADING_NESTING=$(grep -rn "<h[1-6]" src/ --include="*.tsx" 2>/dev/null | while read line; do
FILE=$(echo "$line" | cut -d: -f1)
LINE_NO=$(echo "$line" | cut -d: -f2)
CONTEXT=$(sed -n "${LINE_NO},$((LINE_NO + 5))p" "$FILE" 2>/dev/null)
NESTED=$(echo "$CONTEXT" | grep -oE "<h[1-6]" | wc -l | tr -d ' ')
if [ "$NESTED" -gt 1 ]; then
RELATIVE=$(echo "$FILE" | sed 's|^src/||')
echo " - \`${RELATIVE}:${LINE_NO}\` — nested heading elements"
fi
done | head -5 || true)
if [ -n "$HEADING_NESTING" ]; then
ISSUES="${ISSUES}### Nested heading elements\nHeadings (h1-h6) cannot contain other headings:\n${HEADING_NESTING}\n\n"
fi
# Check for common onClick on non-interactive elements without role
CLICK_NO_ROLE=$(grep -rn "onClick=" src/ --include="*.tsx" 2>/dev/null | \
grep -E "<div|<span|<td|<tr" | \
grep -v "role=" | \
grep -v "tabIndex" | head -20 || true)
if [ -n "$CLICK_NO_ROLE" ]; then
COUNT=$(echo "$CLICK_NO_ROLE" | wc -l | tr -d ' ')
if [ "$COUNT" -gt 5 ]; then
SAMPLE=$(echo "$CLICK_NO_ROLE" | head -5 | while read line; do
FILE=$(echo "$line" | cut -d: -f1 | sed 's|^src/||')
LINE_NO=$(echo "$line" | cut -d: -f2)
echo " - \`${FILE}:${LINE_NO}\`"
done)
ISSUES="${ISSUES}### Clickable elements without accessibility attributes\n${COUNT} elements have onClick but no role/tabIndex:\n${SAMPLE}\n\n"
fi
fi
if [ -n "$ISSUES" ]; then
echo "found=true" >> "$GITHUB_OUTPUT"
printf "%b" "$ISSUES" > /tmp/focus-dom-errors.txt
echo "DOM structure issues found"
else
echo "found=false" >> "$GITHUB_OUTPUT"
echo "DOM structure looks good"
fi
# === SILENT FAILURE DETECTION (every run) ===
- name: "Check: Silent failures in error handling"
id: focus_silent_failures
working-directory: web
continue-on-error: true
run: |
echo "Checking for silent failures in error handling..."
ISSUES=""
# Find catch blocks that log but don't notify user
SILENT_CATCHES=$(grep -rn "catch" src/ --include="*.tsx" --include="*.ts" 2>/dev/null | while read line; do
FILE=$(echo "$line" | cut -d: -f1)
LINE_NO=$(echo "$line" | cut -d: -f2)
# Get 5 lines after catch
CONTEXT=$(sed -n "${LINE_NO},$((LINE_NO + 5))p" "$FILE" 2>/dev/null)
# Check if it has console.log/error but no user notification
HAS_CONSOLE=$(echo "$CONTEXT" | grep -E "console\.(log|error|warn)" || true)
HAS_NOTIFY=$(echo "$CONTEXT" | grep -iE "toast|Toast|showError|setError|showNotification|addToast" || true)
if [ -n "$HAS_CONSOLE" ] && [ -z "$HAS_NOTIFY" ]; then
RELATIVE=$(echo "$FILE" | sed 's|^src/||')
echo " - \`${RELATIVE}:${LINE_NO}\` — logs error but no user notification"
fi
done | head -15 || true)
if [ -n "$SILENT_CATCHES" ]; then
ISSUES="${ISSUES}### Silent error handling\nThese catch blocks log errors but don't notify users:\n${SILENT_CATCHES}\n\n"
fi
# Find try blocks without user-facing error handling
TRY_NO_USER_ERROR=$(grep -rn "try {" src/ --include="*.tsx" 2>/dev/null | while read line; do
FILE=$(echo "$line" | cut -d: -f1)
LINE_NO=$(echo "$line" | cut -d: -f2)
# Get 20 lines for full try-catch block
CONTEXT=$(sed -n "${LINE_NO},$((LINE_NO + 20))p" "$FILE" 2>/dev/null)
HAS_CATCH=$(echo "$CONTEXT" | grep "catch" || true)
HAS_USER_ERROR=$(echo "$CONTEXT" | grep -iE "setError|showError|toast|Toast|setErrorMessage" || true)
if [ -n "$HAS_CATCH" ] && [ -z "$HAS_USER_ERROR" ]; then
RELATIVE=$(echo "$FILE" | sed 's|^src/||')
echo " - \`${RELATIVE}:${LINE_NO}\`"
fi
done | head -10 || true)
if [ -n "$TRY_NO_USER_ERROR" ]; then
ISSUES="${ISSUES}### Try-catch without user error display\n${TRY_NO_USER_ERROR}\n\n"
fi
if [ -n "$ISSUES" ]; then
echo "found=true" >> "$GITHUB_OUTPUT"
printf "%b" "$ISSUES" > /tmp/focus-silent-failures.txt
echo "Silent failure issues found"
else
echo "found=false" >> "$GITHUB_OUTPUT"
echo "Error handling looks good"
fi
# === MISSING USER FEEDBACK (every run) ===
- name: "Check: Missing user feedback on actions"
id: focus_feedback_gaps
working-directory: web
continue-on-error: true
run: |
echo "Checking for missing user feedback on actions..."
ISSUES=""
# Count toast usage
TOAST_COUNT=$(grep -rn "toast\|showToast\|addToast" src/ --include="*.tsx" 2>/dev/null | wc -l | tr -d ' ')
# Count action components (forms, buttons with handlers)
ACTION_COUNT=$(grep -rn "onSubmit\|onClick.*async\|handleSubmit\|handleSave\|handleDelete" src/ --include="*.tsx" 2>/dev/null | wc -l | tr -d ' ')
if [ "$ACTION_COUNT" -gt 0 ]; then
RATIO=$((TOAST_COUNT * 100 / ACTION_COUNT))
if [ "$RATIO" -lt 15 ]; then
ISSUES="${ISSUES}### Low toast/feedback usage\nOnly ${TOAST_COUNT} toast notifications for ${ACTION_COUNT} action handlers (${RATIO}%)\nRecommend adding success/error feedback to more user actions\n\n"
fi
fi
# Find form submissions without feedback
FORMS_NO_FEEDBACK=$(grep -rln "onSubmit=" src/ --include="*.tsx" 2>/dev/null | while read f; do
HAS_FEEDBACK=$(grep -l "toast\|Toast\|showSuccess\|setSuccess\|setMessage" "$f" 2>/dev/null || true)
if [ -z "$HAS_FEEDBACK" ]; then
RELATIVE=$(echo "$f" | sed 's|^src/||')
echo " - \`${RELATIVE}\`"
fi
done | head -10 || true)
if [ -n "$FORMS_NO_FEEDBACK" ]; then
ISSUES="${ISSUES}### Forms without success feedback\n${FORMS_NO_FEEDBACK}\n\n"
fi
# Find delete handlers without confirmation
DELETE_NO_CONFIRM=$(grep -rn "handleDelete\|onDelete\|deleteItem\|removeItem" src/ --include="*.tsx" 2>/dev/null | while read line; do
FILE=$(echo "$line" | cut -d: -f1)
LINE_NO=$(echo "$line" | cut -d: -f2)
CONTEXT=$(sed -n "$((LINE_NO > 5 ? LINE_NO - 5 : 1)),$((LINE_NO + 10))p" "$FILE" 2>/dev/null)
HAS_CONFIRM=$(echo "$CONTEXT" | grep -iE "confirm|Confirm|dialog|Dialog|modal|Modal" || true)
if [ -z "$HAS_CONFIRM" ]; then
RELATIVE=$(echo "$FILE" | sed 's|^src/||')
echo " - \`${RELATIVE}:${LINE_NO}\`"
fi
done | head -10 || true)
if [ -n "$DELETE_NO_CONFIRM" ]; then
ISSUES="${ISSUES}### Delete actions without confirmation\n${DELETE_NO_CONFIRM}\n\n"
fi
if [ -n "$ISSUES" ]; then
echo "found=true" >> "$GITHUB_OUTPUT"
printf "%b" "$ISSUES" > /tmp/focus-feedback-gaps.txt
echo "Feedback gap issues found"
else
echo "found=false" >> "$GITHUB_OUTPUT"
echo "User feedback looks good"
fi
# === LOADING STATE GAPS (every run) ===
- name: "Check: Missing loading states on async actions"
id: focus_loading_gaps
working-directory: web
continue-on-error: true
run: |
echo "Checking for missing loading states..."
ISSUES=""
# Find async onClick without loading state
ASYNC_NO_LOADING=$(grep -rn "onClick.*async" src/ --include="*.tsx" 2>/dev/null | while read line; do
FILE=$(echo "$line" | cut -d: -f1)
LINE_NO=$(echo "$line" | cut -d: -f2)
# Check component for loading state
HAS_LOADING=$(grep -l "isLoading\|loading\|isSubmitting\|isPending\|Loader\|Spinner" "$FILE" 2>/dev/null || true)
if [ -z "$HAS_LOADING" ]; then
RELATIVE=$(echo "$FILE" | sed 's|^src/||')
echo " - \`${RELATIVE}:${LINE_NO}\`"
fi
done | head -15 || true)
if [ -n "$ASYNC_NO_LOADING" ]; then
ISSUES="${ISSUES}### Async handlers without loading state\nThese components have async onClick but no loading indicator:\n${ASYNC_NO_LOADING}\n\n"
fi
# Find buttons that should have loading state
BUTTONS_NO_LOADING=$(grep -rn "<button.*onClick" src/ --include="*.tsx" 2>/dev/null | \
grep -iE "submit|save|deploy|create|delete|update|send" | \
while read line; do
FILE=$(echo "$line" | cut -d: -f1)
LINE_NO=$(echo "$line" | cut -d: -f2)
CONTEXT=$(sed -n "$((LINE_NO > 3 ? LINE_NO - 3 : 1)),$((LINE_NO + 3))p" "$FILE" 2>/dev/null)
HAS_DISABLED=$(echo "$CONTEXT" | grep "disabled=" || true)
if [ -z "$HAS_DISABLED" ]; then
RELATIVE=$(echo "$FILE" | sed 's|^src/||')
echo " - \`${RELATIVE}:${LINE_NO}\`"
fi
done | head -10 || true)
if [ -n "$BUTTONS_NO_LOADING" ]; then
ISSUES="${ISSUES}### Action buttons without disabled state\n${BUTTONS_NO_LOADING}\n\n"
fi
if [ -n "$ISSUES" ]; then
echo "found=true" >> "$GITHUB_OUTPUT"
printf "%b" "$ISSUES" > /tmp/focus-loading-gaps.txt
echo "Loading state issues found"
else
echo "found=false" >> "$GITHUB_OUTPUT"
echo "Loading states look good"
fi
# === KEYBOARD NAVIGATION GAPS (every run) ===
- name: "Check: Keyboard navigation gaps"
id: focus_keyboard_nav
working-directory: web
continue-on-error: true
run: |
echo "Checking for keyboard navigation gaps..."
ISSUES=""
# Find dropdown/menu without keyboard handlers
DROPDOWN_NO_KEYBOARD=$(grep -rln "dropdown\|Dropdown\|menu\|Menu\|popover\|Popover" src/ --include="*.tsx" 2>/dev/null | while read f; do
HAS_KEYBOARD=$(grep -l "onKeyDown\|onKeyUp\|useKeyboard\|ArrowDown\|ArrowUp" "$f" 2>/dev/null || true)
if [ -z "$HAS_KEYBOARD" ]; then
RELATIVE=$(echo "$f" | sed 's|^src/||')
# Only flag if it has interactive elements
HAS_INTERACTIVE=$(grep -l "onClick\|button\|Button" "$f" 2>/dev/null || true)
if [ -n "$HAS_INTERACTIVE" ]; then
echo " - \`${RELATIVE}\`"
fi
fi
done | head -10 || true)
if [ -n "$DROPDOWN_NO_KEYBOARD" ]; then
ISSUES="${ISSUES}### Dropdowns/menus without keyboard navigation\nAdd arrow key navigation:\n${DROPDOWN_NO_KEYBOARD}\n\n"
fi
# Find tab panels without keyboard support
TABS_NO_KEYBOARD=$(grep -rln "tab\|Tab\|TabPanel\|TabList" src/ --include="*.tsx" 2>/dev/null | while read f; do
HAS_ARROW_NAV=$(grep -l "ArrowLeft\|ArrowRight\|onKeyDown" "$f" 2>/dev/null || true)
if [ -z "$HAS_ARROW_NAV" ]; then
RELATIVE=$(echo "$f" | sed 's|^src/||')
echo " - \`${RELATIVE}\`"
fi
done | head -5 || true)
if [ -n "$TABS_NO_KEYBOARD" ]; then
ISSUES="${ISSUES}### Tab components without arrow key navigation\n${TABS_NO_KEYBOARD}\n\n"
fi
if [ -n "$ISSUES" ]; then
echo "found=true" >> "$GITHUB_OUTPUT"
printf "%b" "$ISSUES" > /tmp/focus-keyboard-nav.txt
echo "Keyboard navigation issues found"
else
echo "found=false" >> "$GITHUB_OUTPUT"
echo "Keyboard navigation looks good"
fi
# === ARIA COMPLETENESS (every run) ===
- name: "Check: ARIA label and role completeness"
id: focus_aria_gaps
working-directory: web
continue-on-error: true
run: |
echo "Checking for ARIA gaps..."
ISSUES=""
# Number of lines after the attribute to scan for aria-label/title.
# JSX props commonly span multiple lines; a 10-line window catches the
# common pattern of role="button" followed by aria-label and handlers.
ARIA_WINDOW_LINES=10
# Find role="button" without aria-label within the JSX element window.
# Exclude test files — they may construct synthetic role=button mocks.
ROLE_NO_LABEL=$(grep -rn 'role="button"' src/ --include="*.tsx" 2>/dev/null | \
grep -v "__tests__" | grep -v "\.test\.tsx:" | \
while read line; do
FILE=$(echo "$line" | cut -d: -f1)
LINE_NO=$(echo "$line" | cut -d: -f2)
# Look at a window around the match: 3 lines before (for aria-label
# listed above role) and ARIA_WINDOW_LINES after.
START=$((LINE_NO - 3))
if [ $START -lt 1 ]; then START=1; fi
END=$((LINE_NO + ARIA_WINDOW_LINES))
CONTEXT=$(sed -n "${START},${END}p" "$FILE" 2>/dev/null)
HAS_LABEL=$(echo "$CONTEXT" | grep -E "aria-label|aria-labelledby|title=" || true)
if [ -z "$HAS_LABEL" ]; then
RELATIVE=$(echo "$FILE" | sed 's|^src/||')
echo " - \`${RELATIVE}:${LINE_NO}\`"
fi
done | head -10 || true)
if [ -n "$ROLE_NO_LABEL" ]; then
ISSUES="${ISSUES}### Elements with role=\"button\" but no aria-label\n${ROLE_NO_LABEL}\n\n"
fi
# Find icon-only buttons without labels
# Window for scanning inside a <button ...> element for aria-label,
# title, or visible text. Wider than the role check because buttons
# with long className expressions push the content further down.
ICON_BUTTON_WINDOW_LINES=12
ICON_BUTTON_NO_LABEL=$(grep -rn "<button" src/ --include="*.tsx" 2>/dev/null | \
grep -v "__tests__" | grep -v "\.test\.tsx:" | \
while read line; do
FILE=$(echo "$line" | cut -d: -f1)
LINE_NO=$(echo "$line" | cut -d: -f2)
# Get button content (next N lines)
CONTEXT=$(sed -n "${LINE_NO},$((LINE_NO + ICON_BUTTON_WINDOW_LINES))p" "$FILE" 2>/dev/null)
# Check if it only contains an icon
HAS_ICON=$(echo "$CONTEXT" | grep -E "Icon|<[A-Z][a-z]+.*className.*w-[0-9]" || true)
HAS_TEXT=$(echo "$CONTEXT" | grep -E ">[A-Za-z]|{['\"]" || true)
HAS_LABEL=$(echo "$CONTEXT" | grep -E "aria-label|aria-labelledby|title=" || true)
if [ -n "$HAS_ICON" ] && [ -z "$HAS_TEXT" ] && [ -z "$HAS_LABEL" ]; then
RELATIVE=$(echo "$FILE" | sed 's|^src/||')
echo " - \`${RELATIVE}:${LINE_NO}\`"
fi
done | head -10 || true)
if [ -n "$ICON_BUTTON_NO_LABEL" ]; then
ISSUES="${ISSUES}### Icon-only buttons without aria-label\n${ICON_BUTTON_NO_LABEL}\n\n"
fi
# Find modals/dialogs without proper ARIA.
# Exclude test files, tooltip/popover components, and files that only
# re-export a dialog from a child component. True modals typically
# contain both a portal/overlay pattern AND an "isOpen" prop.
MODAL_NO_ARIA=$(grep -rln "Modal\|Dialog" src/ --include="*.tsx" 2>/dev/null | \
grep -v "__tests__" | grep -v "\.test\.tsx$" | \
grep -v "Tooltip" | grep -v "Popover" | grep -v "Dropdown" | \
while read f; do
HAS_ROLE=$(grep -l 'role="dialog"\|role="alertdialog"\|role="menu"\|role="listbox"\|role="tooltip"\|aria-modal' "$f" 2>/dev/null || true)
if [ -z "$HAS_ROLE" ]; then
# Require BOTH a portal/overlay indicator AND an isOpen-style prop
# to reduce false positives against non-modal panels.
HAS_PORTAL=$(grep -l "createPortal\|backdrop\|overlay\|Overlay" "$f" 2>/dev/null || true)
HAS_OPEN_PROP=$(grep -E "isOpen[ :=]|open[ :=][ ]*\{|onClose" "$f" 2>/dev/null || true)
if [ -n "$HAS_PORTAL" ] && [ -n "$HAS_OPEN_PROP" ]; then
RELATIVE=$(echo "$f" | sed 's|^src/||')
echo " - \`${RELATIVE}\`"
fi
fi
done | head -5 || true)
if [ -n "$MODAL_NO_ARIA" ]; then
ISSUES="${ISSUES}### Modal components without role=\"dialog\"\n${MODAL_NO_ARIA}\n\n"
fi
if [ -n "$ISSUES" ]; then
echo "found=true" >> "$GITHUB_OUTPUT"
printf "%b" "$ISSUES" > /tmp/focus-aria-gaps.txt
echo "ARIA gap issues found"
else
echo "found=false" >> "$GITHUB_OUTPUT"
echo "ARIA attributes look good"
fi
# === MODAL SAFETY (every run) ===
- name: "Check: Modal safety patterns"
id: focus_modal_safety
working-directory: web
continue-on-error: true
run: |
echo "Checking for modal safety issues..."
ISSUES=""
# Find modals with forms that allow backdrop close
UNSAFE_MODALS=$(grep -rln "Modal\|BaseModal" src/ --include="*.tsx" 2>/dev/null | while read f; do
# Check if file has form elements
HAS_FORM=$(grep -l "<form\|<input\|<textarea\|<select" "$f" 2>/dev/null || true)
if [ -n "$HAS_FORM" ]; then
# Check if backdrop close is prevented
PREVENTS_CLOSE=$(grep -l "closeOnBackdrop={false}\|closeOnBackdropClick={false}\|disableBackdropClose" "$f" 2>/dev/null || true)
if [ -z "$PREVENTS_CLOSE" ]; then
RELATIVE=$(echo "$f" | sed 's|^src/||')
echo " - \`${RELATIVE}\`"
fi
fi
done | head -10 || true)
if [ -n "$UNSAFE_MODALS" ]; then
ISSUES="${ISSUES}### Modals with forms that allow accidental close\nThese modals have forms but allow backdrop click to close:\n${UNSAFE_MODALS}\n\n"
fi
# Find modals without escape key handling
MODAL_NO_ESCAPE=$(grep -rln "isOpen\|isVisible\|showModal" src/ --include="*.tsx" 2>/dev/null | while read f; do
# Check if it's a modal-like component
IS_MODAL=$(grep -l "portal\|Portal\|overlay\|backdrop\|fixed.*inset" "$f" 2>/dev/null || true)
if [ -n "$IS_MODAL" ]; then
HAS_ESCAPE=$(grep -l "Escape\|onKeyDown\|useEscape\|closeOnEscape" "$f" 2>/dev/null || true)
if [ -z "$HAS_ESCAPE" ]; then
RELATIVE=$(echo "$f" | sed 's|^src/||')
echo " - \`${RELATIVE}\`"
fi
fi
done | head -5 || true)
if [ -n "$MODAL_NO_ESCAPE" ]; then
ISSUES="${ISSUES}### Modals without Escape key handling\n${MODAL_NO_ESCAPE}\n\n"
fi
if [ -n "$ISSUES" ]; then
echo "found=true" >> "$GITHUB_OUTPUT"
printf "%b" "$ISSUES" > /tmp/focus-modal-safety.txt
echo "Modal safety issues found"
else
echo "found=false" >> "$GITHUB_OUTPUT"
echo "Modal safety looks good"
fi
# === EVENT HANDLER PARITY (every run) ===
- name: "Check: Event handler accessibility parity"
id: focus_event_parity
working-directory: web
continue-on-error: true
run: |
echo "Checking for event handler parity..."
ISSUES=""
# Find onClick on non-button elements without keyboard equivalent
CLICK_NO_KEY=$(grep -rn "onClick=" src/ --include="*.tsx" 2>/dev/null | \
grep -E "<div|<span|<tr|<td|<li" | \
grep -v "role=" | \
while read line; do
FILE=$(echo "$line" | cut -d: -f1)
LINE_NO=$(echo "$line" | cut -d: -f2)
LINE_CONTENT=$(sed -n "${LINE_NO}p" "$FILE" 2>/dev/null)
HAS_KEY=$(echo "$LINE_CONTENT" | grep -E "onKeyDown|onKeyUp|onKeyPress" || true)
HAS_ROLE=$(echo "$LINE_CONTENT" | grep 'role=' || true)
if [ -z "$HAS_KEY" ] && [ -z "$HAS_ROLE" ]; then
RELATIVE=$(echo "$FILE" | sed 's|^src/||')
echo " - \`${RELATIVE}:${LINE_NO}\`"
fi
done | head -15 || true)
if [ -n "$CLICK_NO_KEY" ]; then
COUNT=$(echo "$CLICK_NO_KEY" | wc -l | tr -d ' ')
SAMPLE=$(echo "$CLICK_NO_KEY" | head -10)
ISSUES="${ISSUES}### Clickable elements without keyboard support\n${COUNT} elements have onClick but no role/onKeyDown:\n${SAMPLE}\n\n"
fi
if [ -n "$ISSUES" ]; then
echo "found=true" >> "$GITHUB_OUTPUT"
printf "%b" "$ISSUES" > /tmp/focus-event-parity.txt
echo "Event parity issues found"
else
echo "found=false" >> "$GITHUB_OUTPUT"
echo "Event parity looks good"
fi
# ── Layer 8: Adoption & Engagement Psychology ───────────────────
# Scans the codebase for opportunities to apply psychological
# techniques (variable rewards, curiosity gaps, progress loops,
# social proof, etc.) that improve discovery, stickiness, and
# long-term adoption — inspired by patterns that make social
# platforms engaging.
- name: "Adoption: Engagement psychology opportunities"
id: focus_adoption_psychology
working-directory: web
continue-on-error: true
run: |
echo "Scanning for adoption & engagement psychology opportunities..."
TOTAL=0
# ── 1. Variable Reward / Surprise & Delight ──
STATIC_PAGES=""
for page_dir in src/components/dashboard src/components/clusters src/components/deploy src/components/compliance src/components/ai-ml src/components/arcade; do
if [ -d "$page_dir" ]; then
PAGE_NAME=$(basename "$page_dir")
HAS_DYNAMIC=$(grep -rlE "RotatingTip|tip(s)?[Oo]f[Tt]he[Dd]ay|didYouKnow|randomTip|spotlight|featured|shuffle|random|rotate.*suggestion|surprise" "$page_dir" 2>/dev/null | head -1 || true)
if [ -z "$HAS_DYNAMIC" ]; then
STATIC_PAGES="${STATIC_PAGES} - \`${PAGE_NAME}\`: no variable-reward elements (tips, spotlights, rotating highlights)\n"
fi
fi
done
if [ -n "$STATIC_PAGES" ]; then
TOTAL=$((TOTAL + 1))
echo "adopt_variable_rewards=true" >> "$GITHUB_OUTPUT"
printf "%b" "These pages show the same content every visit — adding rotating tips, \"did you know\" facts, or spotlight features would create curiosity and repeat engagement:\n${STATIC_PAGES}\n**Psychology:** Variable ratio reinforcement (slot machine effect) keeps users coming back because they never know what they'll discover next." > /tmp/adopt-variable-rewards.txt
else
echo "adopt_variable_rewards=false" >> "$GITHUB_OUTPUT"
fi
# ── 2. Streak / Consistency Tracking ──
HAS_STREAK=$(grep -rl "streak\|consecutive.*day\|login.*count\|daily.*reward\|visit.*count" src/ --include="*.tsx" --include="*.ts" 2>/dev/null | head -1 || true)
if [ -z "$HAS_STREAK" ]; then
TOTAL=$((TOTAL + 1))
echo "adopt_streak=true" >> "$GITHUB_OUTPUT"
printf "%b" "Users have no reason to return daily. A \"3-day streak\" badge or activity heatmap creates commitment loops.\n\n**Implementation ideas:**\n- Track consecutive login days in localStorage\n- Show a streak counter in the sidebar or header\n- Fire \`ksc_streak_day\` GA4 event (already wired but unused)\n- Add a \"flame\" icon that grows with streak length\n\n**Psychology:** The endowed progress effect — users who see progress (even artificial) are more likely to continue." > /tmp/adopt-streak.txt
else
echo "adopt_streak=false" >> "$GITHUB_OUTPUT"
fi
# ── 3. Mastery / Progression System ──
HAS_PROGRESS_BEYOND_TOUR=$(grep -rlE "progress[Bb]ar|completionPercent|mastery|level[Uu]p|achievement|badge[s]?[Ee]arned|xp\b|experience.*point" src/ --include="*.tsx" --include="*.ts" 2>/dev/null | grep -v "tour\|welcome\|getting-started\|onboard" | head -1 || true)
if [ -z "$HAS_PROGRESS_BEYOND_TOUR" ]; then
TOTAL=$((TOTAL + 1))
echo "adopt_mastery=true" >> "$GITHUB_OUTPUT"
printf "%b" "Beyond onboarding, users have no sense of progression through the console.\n\n**Implementation ideas:**\n- \"Explorer\" percentage — track which dashboards/features the user has visited\n- Feature discovery badges (\"You found the Arcade!\")\n- Skill tree for Kubernetes operations (cluster management → GitOps → AI missions)\n- Show \"You've explored 40%% of the console\" in settings or profile\n\n**Psychology:** The Zeigarnik effect — people remember and feel compelled to complete unfinished tasks." > /tmp/adopt-mastery.txt
else
echo "adopt_mastery=false" >> "$GITHUB_OUTPUT"
fi
# ── 4. Setup Completeness Score ──
HAS_COMPLETION=$(grep -rl "checklist\|setup.*complete\|profile.*complete\|configuration.*score" src/ --include="*.tsx" --include="*.ts" 2>/dev/null | grep -v node_modules | head -1 || true)
if [ -z "$HAS_COMPLETION" ]; then
TOTAL=$((TOTAL + 1))
echo "adopt_completeness=true" >> "$GITHUB_OUTPUT"
printf "%b" "Users don't know how much of the platform they've configured.\n\n**Implementation ideas:**\n- \"Your console is 60%% set up\" progress bar on the home dashboard\n- Checklist: Connect cluster → Configure OAuth → Enable AI → Customize dashboards\n- Each completed step fills the progress bar and fires a GA4 conversion event\n- Gray out remaining steps with \"Complete this to unlock...\" teasers\n\n**Psychology:** The endowed progress effect — a 60%% complete bar is harder to abandon than a 0%% one." > /tmp/adopt-completeness.txt
else
echo "adopt_completeness=false" >> "$GITHUB_OUTPUT"
fi
# ── 5. Social Proof Signals ──
HAS_SOCIAL=$(grep -rlE "other.*users|popular|trending|most.*used|community|adopted.*by|used.*by.*teams|star[s]?.*count|fork[s]?.*count" src/ --include="*.tsx" --include="*.ts" 2>/dev/null | grep -v node_modules | head -1 || true)
if [ -z "$HAS_SOCIAL" ]; then
TOTAL=$((TOTAL + 1))
echo "adopt_social_proof=true" >> "$GITHUB_OUTPUT"
printf "%b" "No signals showing that other users are actively using the console.\n\n**Implementation ideas:**\n- \"Most popular card this week\" badge on the Add Card modal\n- \"Used by N teams\" on the marketplace cards (pull from GA4 install events)\n- \"N users online\" indicator already exists — make it more prominent\n- Show GitHub star count on the About/Settings page\n\n**Psychology:** Social proof (Cialdini) — people follow what others do. GitHub's star counts and npm's download badges leverage this." > /tmp/adopt-social-proof.txt
else
echo "adopt_social_proof=false" >> "$GITHUB_OUTPUT"
fi
# ── 6. Activity Feed ──
HAS_ACTIVITY_FEED=$(grep -rlE "activity.*feed|recent.*activity|live.*feed|event.*stream|what.*others" src/ --include="*.tsx" --include="*.ts" 2>/dev/null | head -1 || true)
if [ -z "$HAS_ACTIVITY_FEED" ]; then
TOTAL=$((TOTAL + 1))
echo "adopt_activity_feed=true" >> "$GITHUB_OUTPUT"
printf "%b" "No lightweight activity feed showing recent platform events.\n\n**Implementation ideas:**\n- \"What's happening\" sidebar card: recent deployments, alerts resolved, missions completed\n- Feed items link to the relevant dashboard for deeper investigation\n- Show anonymized activity from other users (\"Someone deployed nginx to prod-us-east\")\n- Update in real time via the existing WebSocket connection\n\n**Psychology:** FOMO and social presence — an alive-looking platform retains users better than a static one." > /tmp/adopt-activity-feed.txt
else
echo "adopt_activity_feed=false" >> "$GITHUB_OUTPUT"
fi
# ── 7. Feature Teasers & Discovery ──
HAS_TEASER=$(grep -rlE "unlock|discover|hidden|sneak.*peek|coming.*soon|preview|try.*new|new.*feature|beta.*badge|explore.*more|see.*what" src/ --include="*.tsx" --include="*.ts" 2>/dev/null | grep -v node_modules | head -3 || true)
if [ -z "$HAS_TEASER" ]; then
TOTAL=$((TOTAL + 1))
echo "adopt_feature_teasers=true" >> "$GITHUB_OUTPUT"
printf "%b" "No feature teasers that create curiosity about undiscovered capabilities.\n\n**Implementation ideas:**\n- \"Unlock AI insights by connecting a cluster\" prompt on demo mode\n- \"New\" badge on recently added sidebar items (auto-expires after 7 days)\n- \"Did you know you can...\" interstitials after completing a task\n- Blurred preview of premium/advanced features with \"Connect agent to enable\"\n\n**Psychology:** Information gap theory (Loewenstein) — curiosity is the gap between what we know and what we want to know." > /tmp/adopt-feature-teasers.txt
else
echo "adopt_feature_teasers=false" >> "$GITHUB_OUTPUT"
fi
# ── 8. Easter Eggs ──
HAS_EASTER_EGG=$(grep -rlE "easter.*egg|secret|konami|hidden.*feature|surprise.*mode" src/ --include="*.tsx" --include="*.ts" 2>/dev/null | head -1 || true)
if [ -z "$HAS_EASTER_EGG" ]; then
TOTAL=$((TOTAL + 1))
echo "adopt_easter_eggs=true" >> "$GITHUB_OUTPUT"
printf "%b" "No easter eggs or hidden discovery moments that create delight and word-of-mouth.\n\n**Implementation ideas:**\n- Konami code (↑↑↓↓←→←→BA) triggers a fun animation or secret theme\n- Click the logo 7 times to reveal a hidden credits page\n- Type \"kubectl\" in the search bar to get a CLI-style response\n- Hidden game in the Arcade dashboard (already has games — add a secret one)\n\n**Psychology:** Surprise and delight create emotional peaks that users share with others, driving organic word-of-mouth growth." > /tmp/adopt-easter-eggs.txt
else
echo "adopt_easter_eggs=false" >> "$GITHUB_OUTPUT"
fi
# ── 9. Welcome-Back Experience ──
HAS_IDLE_PROMPT=$(grep -rlE "idle|inactive|welcome.*back|time.*since|last.*visit|miss.*you|catch.*up" src/ --include="*.tsx" --include="*.ts" 2>/dev/null | head -1 || true)
if [ -z "$HAS_IDLE_PROMPT" ]; then
TOTAL=$((TOTAL + 1))
echo "adopt_welcome_back=true" >> "$GITHUB_OUTPUT"
printf "%b" "Users who return after absence see no acknowledgment or catch-up summary.\n\n**Implementation ideas:**\n- \"While you were away...\" card on the home dashboard (N alerts resolved, N deployments, N new features)\n- Track last visit timestamp in localStorage\n- Show a brief changelog digest if the console version changed since last visit\n- Personalized greeting: \"Welcome back, Andy — 3 alerts fired since Tuesday\"\n\n**Psychology:** Fogg's behavior model — triggers bring users back. A personalized summary creates internal motivation to check in regularly." > /tmp/adopt-welcome-back.txt
else
echo "adopt_welcome_back=false" >> "$GITHUB_OUTPUT"
fi
# ── Emit summary ──
echo "found=$( [ $TOTAL -gt 0 ] && echo true || echo false )" >> "$GITHUB_OUTPUT"
echo "count=$TOTAL" >> "$GITHUB_OUTPUT"
echo "Found $TOTAL adoption psychology opportunities"
# ── Ensure Labels Exist ────────────────────────────────────────
- name: Ensure labels exist
env:
GH_TOKEN: ${{ secrets.CONSOLE_AUTO }}
run: |
LABELS=(
"auto-qa:FF6B35:Issue detected by automated QA"
"auto-qa:performance:3B82F6:Performance improvement"
"auto-qa:security:EF4444:Security issue"
"auto-qa:a11y:A855F7:Accessibility improvement"
"auto-qa:operator:F59E0B:Operator experience improvement"
"auto-qa:sre:10B981:SRE/multi-cluster improvement"
"auto-qa:features:8B5CF6:Feature recommendation"
"auto-qa:resilience:F97316:Resilience/error handling improvement"
"auto-qa:consistency:6B7280:Inventory/documentation consistency"
"auto-qa:ui-design:EC4899:UI design principle violation"
"auto-qa:nfr:06B6D4:Non-functional requirement improvement"
"auto-qa:meta:9333EA:Auto-QA self-improvement recommendation"
"auto-qa:flicker:FF5722:UI flicker and layout shift issues"
"auto-qa:centralization:4CAF50:Code centralization opportunity"
"auto-qa:demo-data:2196F3:Demo mode data coverage"
"auto-qa:console-errors:B71C1C:Console error patterns"
"auto-qa:actions:7C4DFF:Button and action consistency"
"auto-qa:stale-data:FF9800:Stale data and freshness issues"
"auto-qa:color:E91E63:Color consistency issues"
"auto-qa:tour:00BCD4:Tour and onboarding issues"
"auto-qa:user-count:9C27B0:Active user count feature"
"auto-qa:token-counter:795548:Token counter feature"
"auto-qa:local-clusters:3F51B5:Local cluster detection and creation"
"auto-qa:refresh-spin:607D8B:Refresh icon animation consistency"
"auto-qa:dom-errors:D32F2F:DOM nesting and structure violations"
"auto-qa:silent-failures:B71C1C:Errors logged but not shown to users"
"auto-qa:feedback-gaps:FF9800:Missing user feedback on actions"
"auto-qa:loading-gaps:2196F3:Missing loading states on async actions"
"auto-qa:keyboard-nav:9C27B0:Keyboard navigation gaps"
"auto-qa:aria-gaps:A855F7:ARIA label and role gaps"
"auto-qa:modal-safety:F44336:Modal patterns that risk data loss"
"auto-qa:event-parity:607D8B:onClick without keyboard equivalent"
"auto-qa:nil-safety:E53935:Nil pointer safety issues detected by nilaway"
"auto-qa:color-contrast:FF9800:Color contrast accessibility issues"
"auto-qa:bundle-size:3B82F6:Bundle chunk size issues"
"auto-qa:large-files:6B7280:Oversized source files"
"auto-qa:import-cost:F59E0B:Import patterns that hurt tree-shaking"
"auto-qa:adoption:FF1493:Adoption & engagement psychology improvement"
)
for entry in "${LABELS[@]}"; do
IFS=: read -r NAME COLOR DESC <<< "$entry"
gh label create "$NAME" \
--repo "${{ github.repository }}" \
--color "$COLOR" \
--description "$DESC" \
2>/dev/null || true
done
# ── Issue Creation ──────────────────────────────────────────────
- name: Create issues for failures
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
env:
BUILD_OUTCOME: ${{ steps.build_check.outcome }}
LINT_OUTCOME: ${{ steps.lint_check.outcome }}
GO_BUILD_OUTCOME: ${{ steps.go_build_check.outcome }}
BUNDLE_EXCEEDED: ${{ steps.bundle_check.outputs.exceeded }}
BUNDLE_SIZE_KB: ${{ steps.bundle_check.outputs.size_kb }}
HAS_VULNS: ${{ steps.audit_check.outputs.has_vulnerabilities }}
VULN_CRITICAL: ${{ steps.audit_check.outputs.critical }}
VULN_HIGH: ${{ steps.audit_check.outputs.high }}
FOCUS_AREA: ${{ steps.focus.outputs.area }}
# Performance focus
FOCUS_UNUSED_DEPS: ${{ steps.focus_unused_deps.outputs.found }}
FOCUS_LAZY_LOADING: ${{ steps.focus_lazy_loading.outputs.found }}
# Security focus
FOCUS_HARDCODED: ${{ steps.focus_hardcoded.outputs.found }}
FOCUS_DEP_AGE: ${{ steps.focus_dep_age.outputs.found }}
# A11y focus
FOCUS_ARIA: ${{ steps.focus_aria.outputs.found }}
FOCUS_KEYBOARD: ${{ steps.focus_keyboard.outputs.found }}
# Operator focus
FOCUS_MAGIC_NUMBERS: ${{ steps.focus_magic_numbers.outputs.found }}
FOCUS_TOOLTIPS: ${{ steps.focus_tooltips.outputs.found }}
# SRE focus
FOCUS_SINGLE_CLUSTER: ${{ steps.focus_single_cluster.outputs.found }}
FOCUS_HEALTH: ${{ steps.focus_health.outputs.found }}
# Features focus
FOCUS_TODOS: ${{ steps.focus_todos.outputs.found }}
FOCUS_TODO_COUNT: ${{ steps.focus_todos.outputs.count }}
FOCUS_COMPLEXITY: ${{ steps.focus_complexity.outputs.found }}
FOCUS_CONSOLE_LOGS: ${{ steps.focus_console_logs.outputs.found }}
FOCUS_CONSOLE_LOG_COUNT: ${{ steps.focus_console_logs.outputs.count }}
FOCUS_ANY_TYPES: ${{ steps.focus_any_types.outputs.found }}
FOCUS_ANY_TYPE_COUNT: ${{ steps.focus_any_types.outputs.count }}
FOCUS_MEMORY_LEAKS: ${{ steps.focus_memory_leaks.outputs.found }}
# Resilience focus
FOCUS_SWALLOWED: ${{ steps.focus_swallowed.outputs.found }}
FOCUS_LOADING_STATES: ${{ steps.focus_loading_states.outputs.found }}
# Consistency focus
FOCUS_MISSING_FILES: ${{ steps.focus_missing_files.outputs.found }}
FOCUS_CARD_REGISTRY: ${{ steps.focus_card_registry.outputs.found }}
FOCUS_ROUTES: ${{ steps.focus_routes.outputs.found }}
FOCUS_MODAL_CONSISTENCY: ${{ steps.focus_modal_consistency.outputs.found }}
# UI Design
FOCUS_HARDCODED_COLORS: ${{ steps.focus_hardcoded_colors.outputs.found }}
FOCUS_SPACING: ${{ steps.focus_spacing.outputs.found }}
FOCUS_DARK_MODE: ${{ steps.focus_dark_mode.outputs.found }}
FOCUS_TOUCH_TARGETS: ${{ steps.focus_touch_targets.outputs.found }}
FOCUS_COMPONENT_PATTERNS: ${{ steps.focus_component_patterns.outputs.found }}
# NFR Coverage & Self-Improvement
META_PR_ANALYSIS: ${{ steps.meta_pr_analysis.outputs.found }}
FOCUS_TEST_COVERAGE: ${{ steps.focus_test_coverage.outputs.found }}
FOCUS_I18N: ${{ steps.focus_i18n.outputs.found }}
FOCUS_STATE_PATTERNS: ${{ steps.focus_state_patterns.outputs.found }}
FOCUS_NAVIGATION: ${{ steps.focus_navigation.outputs.found }}
FOCUS_EFFICIENCY: ${{ steps.focus_efficiency.outputs.found }}
# Flicker & Centralization
FOCUS_FLICKER: ${{ steps.focus_flicker.outputs.found }}
FOCUS_CENTRALIZATION: ${{ steps.focus_centralization.outputs.found }}
FOCUS_INVENTORY_DEMO: ${{ steps.focus_inventory_demo.outputs.found }}
FOCUS_CONSOLE_ERRORS: ${{ steps.focus_console_errors.outputs.found }}
FOCUS_BUTTON_ACTIONS: ${{ steps.focus_button_actions.outputs.found }}
FOCUS_STALE_DATA: ${{ steps.focus_stale_data.outputs.found }}
FOCUS_COLOR_CONSISTENCY: ${{ steps.focus_color_consistency.outputs.found }}
FOCUS_USER_COUNT: ${{ steps.focus_user_count.outputs.found }}
FOCUS_TOUR: ${{ steps.focus_tour.outputs.found }}
FOCUS_TOKEN_COUNTER: ${{ steps.focus_token_counter.outputs.found }}
FOCUS_LOCAL_CLUSTERS: ${{ steps.focus_local_clusters.outputs.found }}
FOCUS_REFRESH_SPIN: ${{ steps.focus_refresh_spin.outputs.found }}
FOCUS_DOM_ERRORS: ${{ steps.focus_dom_errors.outputs.found }}
FOCUS_SILENT_FAILURES: ${{ steps.focus_silent_failures.outputs.found }}
FOCUS_FEEDBACK_GAPS: ${{ steps.focus_feedback_gaps.outputs.found }}
FOCUS_LOADING_GAPS: ${{ steps.focus_loading_gaps.outputs.found }}
FOCUS_KEYBOARD_NAV: ${{ steps.focus_keyboard_nav.outputs.found }}
FOCUS_ARIA_GAPS: ${{ steps.focus_aria_gaps.outputs.found }}
FOCUS_MODAL_SAFETY: ${{ steps.focus_modal_safety.outputs.found }}
FOCUS_EVENT_PARITY: ${{ steps.focus_event_parity.outputs.found }}
# Adoption psychology — individual findings
ADOPT_VARIABLE_REWARDS: ${{ steps.focus_adoption_psychology.outputs.adopt_variable_rewards }}
ADOPT_STREAK: ${{ steps.focus_adoption_psychology.outputs.adopt_streak }}
ADOPT_MASTERY: ${{ steps.focus_adoption_psychology.outputs.adopt_mastery }}
ADOPT_COMPLETENESS: ${{ steps.focus_adoption_psychology.outputs.adopt_completeness }}
ADOPT_SOCIAL_PROOF: ${{ steps.focus_adoption_psychology.outputs.adopt_social_proof }}
ADOPT_ACTIVITY_FEED: ${{ steps.focus_adoption_psychology.outputs.adopt_activity_feed }}
ADOPT_FEATURE_TEASERS: ${{ steps.focus_adoption_psychology.outputs.adopt_feature_teasers }}
ADOPT_EASTER_EGGS: ${{ steps.focus_adoption_psychology.outputs.adopt_easter_eggs }}
ADOPT_WELCOME_BACK: ${{ steps.focus_adoption_psychology.outputs.adopt_welcome_back }}
# A11y focus
FOCUS_COLOR_CONTRAST: ${{ steps.focus_color_contrast.outputs.found }}
# Performance focus (additional)
FOCUS_BUNDLE_ANALYSIS: ${{ steps.focus_bundle_analysis.outputs.found }}
FOCUS_LARGE_FILES: ${{ steps.focus_large_files.outputs.found }}
FOCUS_IMPORT_COST: ${{ steps.focus_import_cost.outputs.found }}
# Governance & Roadmap
FOCUS_ROADMAP: ${{ steps.focus_roadmap.outputs.found }}
# Tuning (from auto-qa-tuner feedback loop)
BLOCKED_CATEGORIES: ${{ steps.tuning.outputs.blocked }}
BOOSTED_CATEGORIES: ${{ steps.tuning.outputs.boosted }}
TUNED_MAX_ISSUES: ${{ steps.tuning.outputs.max_issues }}
PR_GUIDANCE: ${{ steps.tuning.outputs.pr_guidance }}
# Control
COMMIT_SHA: ${{ github.sha }}
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
with:
github-token: ${{ secrets.CONSOLE_AUTO }}
script: |
const fs = require('fs');
const prefix = process.env.ISSUE_PREFIX;
const tunedMax = parseInt(process.env.TUNED_MAX_ISSUES || '0');
const maxIssues = tunedMax > 0 ? tunedMax : parseInt(process.env.MAX_ISSUES_PER_RUN);
const prGuidance = process.env.PR_GUIDANCE || '';
if (tunedMax > 0) core.info(`Using tuned max issues: ${tunedMax} (default was ${process.env.MAX_ISSUES_PER_RUN})`);
const sha = process.env.COMMIT_SHA.substring(0, 7);
const fullSha = process.env.COMMIT_SHA;
const runUrl = process.env.RUN_URL;
const focusArea = process.env.FOCUS_AREA;
const now = new Date().toISOString();
function readOutput(path, lines = 80) {
try {
const content = fs.readFileSync(path, 'utf8');
return content.split('\n').slice(-lines).join('\n').trim();
} catch {
return '(output not available)';
}
}
function buildBody(checkName, command, output, fixes) {
return [
`## Auto-QA: ${checkName} Failure`,
'',
`**Detected:** ${now} | **Commit:** \`${sha}\` | **Run:** [View](${runUrl})`,
'',
'### Reproduction',
'```bash',
`git checkout ${fullSha}`,
command,
'```',
'',
'### Error Output',
'```',
output,
'```',
'',
'### How to Fix',
...fixes.map(f => `- ${f}`),
...(prGuidance ? ['', `> **PR Guidance:** ${prGuidance}`] : []),
'',
'---',
`*This issue was automatically created by the [Auto-QA workflow](${runUrl}).*`,
].join('\n');
}
function buildFocusBody(areaName, checkName, output, fixes, isEnhancement) {
return [
`## Auto-QA [${areaName}]: ${checkName}`,
'',
`**Detected:** ${now} | **Focus:** ${areaName} | **Commit:** \`${sha}\` | **Run:** [View](${runUrl})`,
'',
'### Findings',
'```',
output,
'```',
'',
`### ${isEnhancement ? 'Suggested Improvements' : 'How to Fix'}`,
...fixes.map(f => `- ${f}`),
...(prGuidance ? ['', `> **PR Guidance:** ${prGuidance}`] : []),
'',
'---',
`*This issue was automatically created by the [Auto-QA workflow](${runUrl}) during **${areaName}** focus day.*`,
].join('\n');
}
// ── Collect baseline failures ──
const checks = [];
if (process.env.BUILD_OUTCOME === 'failure') {
checks.push({
title: `${prefix} TypeScript build failure on main`,
body: buildBody('TypeScript Build', 'cd web && npm run build',
readOutput('/tmp/build-output.txt'),
['Check the TypeScript errors shown above',
'Fix type errors, missing imports, or syntax issues',
'Run `cd web && npm run build` to verify the fix']),
labels: ['bug', 'help wanted', 'auto-qa', 'triage/accepted'],
});
}
if (process.env.LINT_OUTCOME === 'failure') {
checks.push({
title: `${prefix} ESLint violations on main`,
body: buildBody('ESLint', 'cd web && npm run lint',
readOutput('/tmp/lint-output.txt'),
['Fix the ESLint errors listed above',
'Common fixes: remove unused imports, add missing hook dependencies',
'Run `cd web && npm run lint` to verify the fix']),
labels: ['bug', 'help wanted', 'auto-qa', 'triage/accepted'],
});
}
if (process.env.GO_BUILD_OUTCOME === 'failure') {
checks.push({
title: `${prefix} Go backend build failure on main`,
body: buildBody('Go Build', 'CGO_ENABLED=1 go build -o /dev/null ./cmd/console',
readOutput('/tmp/go-build-output.txt'),
['Fix the Go compilation errors shown above',
'Check for missing dependencies, type errors, or undefined references',
'Run `go build ./cmd/console` to verify the fix']),
labels: ['bug', 'help wanted', 'auto-qa', 'triage/accepted'],
});
}
// Bundle size check disabled — too noisy, bundle grows naturally with features
if (process.env.BUNDLE_EXCEEDED === 'true') {
const sizeKB = process.env.BUNDLE_SIZE_KB;
core.info(`Bundle size ${sizeKB}KB exceeds limit — skipping issue creation (disabled).`);
}
if (process.env.HAS_VULNS === 'true') {
const critical = process.env.VULN_CRITICAL || '0';
const high = process.env.VULN_HIGH || '0';
checks.push({
title: `${prefix} npm audit: ${critical} critical, ${high} high vulnerabilities`,
body: [
`## Auto-QA: Security Vulnerabilities`,
'', `**Detected:** ${now} | **Commit:** \`${sha}\` | **Run:** [View](${runUrl})`,
'', `Found **${critical} critical** and **${high} high** severity vulnerabilities.`,
'', '### Audit Summary', '```',
readOutput('/tmp/audit-summary.txt', 50),
'```', '',
'### How to Fix',
'- Run `cd web && npm audit fix` to auto-fix compatible updates',
'- For breaking changes, run `cd web && npm audit fix --force` (test thoroughly)',
'- Check advisories for manual remediation steps',
'- Run `cd web && npm audit --audit-level=high` to verify the fix',
'', '---',
`*This issue was automatically created by the [Auto-QA workflow](${runUrl}).*`,
].join('\n'),
labels: ['bug', 'help wanted', 'auto-qa', 'triage/accepted'],
});
}
// ── Collect focus-area findings ──
const focusLabels = {
performance: 'auto-qa:performance',
security: 'auto-qa:security',
a11y: 'auto-qa:a11y',
operator: 'auto-qa:operator',
sre: 'auto-qa:sre',
features: 'auto-qa:features',
resilience: 'auto-qa:resilience',
consistency: 'auto-qa:consistency',
};
const focusLabel = focusLabels[focusArea] || 'auto-qa';
// Performance (Monday)
if (focusArea === 'performance') {
if (process.env.FOCUS_UNUSED_DEPS === 'true') {
checks.push({
title: `${prefix} Potentially unused npm dependencies`,
body: buildFocusBody('Performance', 'Unused Dependencies',
readOutput('/tmp/focus-unused-deps.txt'),
['Remove unused dependencies with `npm uninstall <pkg>`',
'Verify each package is truly unused before removing',
'Some packages may be used indirectly (plugins, configs)'], true),
labels: ['enhancement', 'help wanted', 'auto-qa', 'triage/accepted', focusLabel],
});
}
if (process.env.FOCUS_LAZY_LOADING === 'true') {
checks.push({
title: `${prefix} Large components missing lazy loading`,
body: buildFocusBody('Performance', 'Missing Lazy Loading',
readOutput('/tmp/focus-lazy-loading.txt'),
['Wrap large page components with `React.lazy(() => import(...))`',
'Add `<Suspense fallback={<Loading />}>` around lazy components',
'Focus on route-level components first for maximum impact'], true),
labels: ['enhancement', 'help wanted', 'auto-qa', 'triage/accepted', focusLabel],
});
}
if (process.env.FOCUS_BUNDLE_ANALYSIS === 'true') {
checks.push({
title: `${prefix} Bundle has JS chunks over 300KB`,
body: buildFocusBody('Performance', 'Bundle Chunk Size',
readOutput('/tmp/focus-bundle-analysis.txt'),
['Split large chunks using `React.lazy` and dynamic imports at route level',
'Use `vite-bundle-visualizer` or `rollup-plugin-visualizer` to identify what is large',
'Move large third-party libs to a separate manual chunk in vite.config.ts',
'Consider loading non-critical libraries on demand (e.g., chart libs)'], true),
labels: ['enhancement', 'help wanted', 'auto-qa', 'triage/accepted', 'auto-qa:bundle-size'],
});
}
if (process.env.FOCUS_LARGE_FILES === 'true') {
checks.push({
title: `${prefix} Oversized source files detected`,
body: buildFocusBody('Performance', 'Large Source Files',
readOutput('/tmp/focus-large-files.txt'),
['Split files over 500 lines into multiple focused modules',
'Extract reusable hooks into `hooks/` directory',
'Move co-located sub-components into their own files under a component directory',
'Large files slow TypeScript type-checking and IDE responsiveness'], true),
labels: ['enhancement', 'help wanted', 'auto-qa', 'triage/accepted', 'auto-qa:large-files'],
});
}
if (process.env.FOCUS_IMPORT_COST === 'true') {
checks.push({
title: `${prefix} Import patterns preventing tree-shaking`,
body: buildFocusBody('Performance', 'Import Cost',
readOutput('/tmp/focus-import-cost.txt'),
['Replace `import * as X from "lib"` with named imports: `import { fn } from "lib"`',
'Use per-method lodash imports: `import debounce from "lodash/debounce"`',
'Verify bundle size before and after with `npm run build` and `du -sh web/dist`',
'Consider alternatives to lodash for simple utilities (native Array/Object methods)'], true),
labels: ['enhancement', 'help wanted', 'auto-qa', 'triage/accepted', 'auto-qa:import-cost'],
});
}
}
// Security (Tuesday)
if (focusArea === 'security') {
if (process.env.FOCUS_HARDCODED === 'true') {
checks.push({
title: `${prefix} Hardcoded URLs or potential credentials in source`,
body: buildFocusBody('Security', 'Hardcoded Credentials/URLs',
readOutput('/tmp/focus-hardcoded.txt'),
['Move API URLs to environment variables or config files',
'Remove any hardcoded tokens or passwords',
'Use `.env` files for environment-specific values',
'Ensure no real credentials are committed (check git history)'], false),
labels: ['bug', 'help wanted', 'auto-qa', 'triage/accepted', focusLabel],
});
}
if (process.env.FOCUS_DEP_AGE === 'true') {
checks.push({
title: `${prefix} Dependencies with major version updates available`,
body: buildFocusBody('Security', 'Outdated Dependencies',
readOutput('/tmp/focus-dep-age.txt'),
['Review changelogs for breaking changes before updating',
'Update one dependency at a time and test thoroughly',
'Major version bumps may require code changes'], true),
labels: ['enhancement', 'help wanted', 'auto-qa', 'triage/accepted', focusLabel],
});
}
}
// A11y (Wednesday)
if (focusArea === 'a11y') {
if (process.env.FOCUS_ARIA === 'true') {
checks.push({
title: `${prefix} Interactive elements missing ARIA labels`,
body: buildFocusBody('Accessibility', 'Missing ARIA Labels',
readOutput('/tmp/focus-aria.txt'),
['Add `aria-label` to icon-only buttons',
'Add `alt` text to all images',
'Use semantic HTML elements where possible',
'Test with a screen reader to verify'], true),
labels: ['enhancement', 'help wanted', 'auto-qa', 'triage/accepted', focusLabel],
});
}
if (process.env.FOCUS_KEYBOARD === 'true') {
checks.push({
title: `${prefix} Keyboard navigation gaps`,
body: buildFocusBody('Accessibility', 'Keyboard Navigation Gaps',
readOutput('/tmp/focus-keyboard.txt'),
['Replace clickable `<div>` elements with `<button>` or add `role="button"` and `tabIndex={0}`',
'Add `onKeyDown` handlers alongside `onClick` for non-button elements',
'Ensure all interactive elements are focusable via Tab'], true),
labels: ['enhancement', 'help wanted', 'auto-qa', 'triage/accepted', focusLabel],
});
}
if (process.env.FOCUS_COLOR_CONTRAST === 'true') {
checks.push({
title: `${prefix} Color contrast risks detected — WCAG AA compliance`,
body: buildFocusBody('Accessibility', 'Color Contrast',
readOutput('/tmp/focus-color-contrast.txt'),
['Ensure text color / background combinations meet WCAG AA 4.5:1 ratio',
'Replace `text-gray-200/300` on light backgrounds with `text-gray-600` or darker',
'Avoid opacity-reduced text below 60% — it drops effective contrast sharply',
'Use inline color styles only when paired with a verified-contrast background',
'Validate with the WebAIM Contrast Checker: https://webaim.org/resources/contrastchecker/'], true),
labels: ['enhancement', 'help wanted', 'auto-qa', 'triage/accepted', 'auto-qa:color-contrast'],
});
}
}
// Operator (Thursday)
if (focusArea === 'operator') {
if (process.env.FOCUS_MAGIC_NUMBERS === 'true') {
checks.push({
title: `${prefix} Hardcoded thresholds and magic numbers`,
body: buildFocusBody('Operator Usefulness', 'Magic Numbers',
readOutput('/tmp/focus-magic-numbers.txt'),
['Extract magic numbers to named constants at the top of the file',
'Consider making thresholds configurable via environment or props',
'Add comments explaining the reasoning behind threshold values'], true),
labels: ['enhancement', 'help wanted', 'auto-qa', 'triage/accepted', focusLabel],
});
}
if (process.env.FOCUS_TOOLTIPS === 'true') {
checks.push({
title: `${prefix} Technical abbreviations without tooltips`,
body: buildFocusBody('Operator Usefulness', 'Missing Tooltips',
readOutput('/tmp/focus-tooltips.txt'),
['Add tooltips to technical abbreviations (CPU, RBAC, CRD, etc.)',
'Use the existing Tooltip component or HTML `title` attribute',
'Explain what status indicators mean on hover'], true),
labels: ['enhancement', 'help wanted', 'auto-qa', 'triage/accepted', focusLabel],
});
}
}
// SRE (Friday)
if (focusArea === 'sre') {
if (process.env.FOCUS_SINGLE_CLUSTER === 'true') {
checks.push({
title: `${prefix} Code may assume single-cluster deployment`,
body: buildFocusBody('SRE/Multi-Cluster', 'Single-Cluster Assumptions',
readOutput('/tmp/focus-single-cluster.txt'),
['Review flagged code for multi-cluster compatibility',
'Ensure cluster selectors and filters work with multiple clusters',
'Add cluster context to API calls where missing'], true),
labels: ['enhancement', 'help wanted', 'auto-qa', 'triage/accepted', focusLabel],
});
}
if (process.env.FOCUS_HEALTH === 'true') {
checks.push({
title: `${prefix} Dashboard components missing health indicators`,
body: buildFocusBody('SRE/Multi-Cluster', 'Missing Health Indicators',
readOutput('/tmp/focus-health.txt'),
['Add health/status indicators to dashboard components',
'Show cluster connectivity status, degraded/healthy state',
'Add visual alerts for unhealthy resources'], true),
labels: ['enhancement', 'help wanted', 'auto-qa', 'triage/accepted', focusLabel],
});
}
}
// Features (Saturday)
if (focusArea === 'features') {
if (process.env.FOCUS_TODOS === 'true') {
const count = process.env.FOCUS_TODO_COUNT || '?';
checks.push({
title: `${prefix} ${count} TODO/FIXME/HACK comments need attention`,
body: buildFocusBody('Feature Recommendations', 'TODO/FIXME/HACK Comments',
readOutput('/tmp/focus-todos.txt'),
['Review each TODO and either implement or create a tracking issue',
'Remove stale TODOs that are no longer relevant',
'Convert HACKs to proper implementations'], true),
labels: ['enhancement', 'help wanted', 'auto-qa', 'triage/accepted', focusLabel],
});
}
if (process.env.FOCUS_COMPLEXITY === 'true') {
checks.push({
title: `${prefix} High-complexity components could be split`,
body: buildFocusBody('Feature Recommendations', 'Complex Components',
readOutput('/tmp/focus-complexity.txt'),
['Extract sub-components from large files',
'Move hooks into custom hooks for reusability',
'Consider splitting into container + presentational components'], true),
labels: ['enhancement', 'help wanted', 'auto-qa', 'triage/accepted', focusLabel],
});
}
if (process.env.FOCUS_CONSOLE_LOGS === 'true') {
const count = process.env.FOCUS_CONSOLE_LOG_COUNT || '?';
checks.push({
title: `${prefix} ${count} debug console.log statements should be removed`,
body: buildFocusBody('Feature Recommendations', 'Debug Statements',
readOutput('/tmp/focus-console-logs.txt'),
['Remove console.log statements used for debugging',
'Use proper logging framework for production logging',
'Consider using debug package with namespaces for development'], true),
labels: ['enhancement', 'help wanted', 'auto-qa', 'triage/accepted', focusLabel],
});
}
if (process.env.FOCUS_ANY_TYPES === 'true') {
const count = process.env.FOCUS_ANY_TYPE_COUNT || '?';
checks.push({
title: `${prefix} ${count} uses of 'any' type reduce type safety`,
body: buildFocusBody('Feature Recommendations', 'Type Safety',
readOutput('/tmp/focus-any-types.txt'),
['Replace `any` with specific types or `unknown`',
'Create interfaces for API response types',
'Use generics for flexible but type-safe code',
'Add `// eslint-disable-next-line` only when truly necessary'], true),
labels: ['enhancement', 'help wanted', 'auto-qa', 'triage/accepted', focusLabel],
});
}
if (process.env.FOCUS_MEMORY_LEAKS === 'true') {
checks.push({
title: `${prefix} Potential memory leaks in component cleanup`,
body: buildFocusBody('Feature Recommendations', 'Memory Leaks',
readOutput('/tmp/focus-memory-leaks.txt'),
['Add cleanup functions to useEffect that returns a function',
'Clear intervals/timeouts in cleanup: `return () => clearInterval(id)`',
'Remove event listeners in cleanup: `return () => el.removeEventListener(...)`',
'Consider using AbortController for fetch cleanup'], true),
labels: ['bug', 'help wanted', 'auto-qa', 'triage/accepted', focusLabel],
});
}
}
// Resilience (every run)
if (process.env.FOCUS_SWALLOWED === 'true') {
checks.push({
title: `${prefix} Swallowed errors and empty catch blocks`,
body: buildFocusBody('Resilience', 'Swallowed Errors',
readOutput('/tmp/focus-swallowed.txt'),
['Add proper error handling in empty catch blocks',
'Show user-facing error messages instead of just logging',
'Use error boundaries for React component errors',
'At minimum, log errors with `console.error` for debugging'], false),
labels: ['bug', 'help wanted', 'auto-qa', 'triage/accepted', 'auto-qa:resilience'],
});
}
if (process.env.FOCUS_LOADING_STATES === 'true') {
checks.push({
title: `${prefix} Components fetching data without loading/error states`,
body: buildFocusBody('Resilience', 'Missing Loading/Error States',
readOutput('/tmp/focus-loading-states.txt'),
['Add loading indicators (skeleton screens or spinners) while data loads',
'Add error states with retry buttons for failed fetches',
'Consider using a shared data-fetching wrapper or custom hook'], true),
labels: ['enhancement', 'help wanted', 'auto-qa', 'triage/accepted', 'auto-qa:resilience'],
});
}
// Consistency (every run)
if (process.env.FOCUS_MISSING_FILES === 'true') {
checks.push({
title: `${prefix} INVENTORY.md references missing component files`,
body: buildFocusBody('Inventory Consistency', 'Missing Component Files',
readOutput('/tmp/focus-missing-files.txt'),
['Remove stale entries from INVENTORY.md for deleted components',
'Add missing component files if they should exist',
'Run the consistency check locally to verify: `grep -oE "web/src/[^ ]+.tsx" INVENTORY.md`'], false),
labels: ['bug', 'help wanted', 'auto-qa', 'triage/accepted', 'auto-qa:consistency'],
});
}
if (process.env.FOCUS_CARD_REGISTRY === 'true') {
checks.push({
title: `${prefix} Card types in INVENTORY.md not registered in source`,
body: buildFocusBody('Inventory Consistency', 'Unregistered Card Types',
readOutput('/tmp/focus-card-registry.txt'),
['Register missing card types in the card registry',
'Remove obsolete card types from INVENTORY.md',
'Ensure card type strings match between inventory and source'], false),
labels: ['bug', 'help wanted', 'auto-qa', 'triage/accepted', 'auto-qa:consistency'],
});
}
if (process.env.FOCUS_ROUTES === 'true') {
checks.push({
title: `${prefix} Routes in INVENTORY.md not found in router config`,
body: buildFocusBody('Inventory Consistency', 'Missing Routes',
readOutput('/tmp/focus-routes.txt'),
['Add missing routes to the router configuration',
'Remove obsolete routes from INVENTORY.md',
'Verify route paths match between inventory and App.tsx'], false),
labels: ['bug', 'help wanted', 'auto-qa', 'triage/accepted', 'auto-qa:consistency'],
});
}
if (process.env.FOCUS_MODAL_CONSISTENCY === 'true') {
checks.push({
title: `${prefix} Modal/drill-down inventory out of sync with source`,
body: buildFocusBody('Inventory Consistency', 'Modal & Drill-Down Drift',
readOutput('/tmp/focus-modal-consistency.txt'),
['Update INVENTORY.md to reflect current modals and drill-down views',
'Add new drill-down views to the inventory',
'Remove references to deleted modals/dialogs'], true),
labels: ['enhancement', 'help wanted', 'auto-qa', 'triage/accepted', 'auto-qa:consistency'],
});
}
// Governance & Roadmap (every run)
if (process.env.FOCUS_ROADMAP === 'true') {
checks.push({
title: `${prefix} ROADMAP.md governance issues`,
body: buildFocusBody('Governance', 'ROADMAP.md',
readOutput('/tmp/focus-roadmap.txt'),
['Review and update ROADMAP.md with current-year milestones',
'Ensure near-term, mid-term, long-term, and non-goals sections are present',
'CNCF incubation requires a public, up-to-date roadmap'], false),
labels: ['bug', 'help wanted', 'auto-qa', 'triage/accepted', 'auto-qa:governance'],
});
}
// UI Design (every run)
if (process.env.FOCUS_HARDCODED_COLORS === 'true') {
checks.push({
title: `${prefix} Hardcoded colors instead of design tokens`,
body: buildFocusBody('UI Design', 'Hardcoded Colors',
readOutput('/tmp/focus-hardcoded-colors.txt'),
['Replace hex colors with Tailwind color classes (e.g., `text-blue-500`)',
'Use CSS variables for custom colors (e.g., `var(--primary)`)',
'Define custom colors in tailwind.config.js for consistency',
'Check that colors adapt properly to dark mode'], true),
labels: ['enhancement', 'help wanted', 'auto-qa', 'triage/accepted', 'auto-qa:ui-design'],
});
}
if (process.env.FOCUS_SPACING === 'true') {
checks.push({
title: `${prefix} Inconsistent spacing values in styles`,
body: buildFocusBody('UI Design', 'Inconsistent Spacing',
readOutput('/tmp/focus-spacing.txt'),
['Replace inline px values with Tailwind spacing classes (p-2, m-4, gap-6)',
'Use the 4px/8px grid system for consistency',
'Define custom spacing in tailwind.config.js if needed',
'Avoid magic numbers - use semantic spacing tokens'], true),
labels: ['enhancement', 'help wanted', 'auto-qa', 'triage/accepted', 'auto-qa:ui-design'],
});
}
if (process.env.FOCUS_DARK_MODE === 'true') {
checks.push({
title: `${prefix} Components missing dark mode support`,
body: buildFocusBody('UI Design', 'Missing Dark Mode',
readOutput('/tmp/focus-dark-mode.txt'),
['Add `dark:` variants to Tailwind classes (e.g., `bg-white dark:bg-gray-900`)',
'Replace inline color styles with Tailwind or CSS variables',
'Test components in both light and dark modes',
'Use semantic color tokens that auto-switch (e.g., `bg-background`)'], true),
labels: ['enhancement', 'help wanted', 'auto-qa', 'triage/accepted', 'auto-qa:ui-design'],
});
}
if (process.env.FOCUS_TOUCH_TARGETS === 'true') {
checks.push({
title: `${prefix} Touch targets may be too small for mobile`,
body: buildFocusBody('UI Design', 'Small Touch Targets',
readOutput('/tmp/focus-touch-targets.txt'),
['Minimum touch target size should be 44x44px (WCAG 2.5.5)',
'Add padding to small buttons: `p-2` or `p-3` instead of `p-0/p-1`',
'Icon-only buttons need adequate hit area around the icon',
'Consider adding `min-h-11 min-w-11` (44px) to interactive elements'], true),
labels: ['enhancement', 'help wanted', 'auto-qa', 'triage/accepted', 'auto-qa:ui-design'],
});
}
if (process.env.FOCUS_COMPONENT_PATTERNS === 'true') {
checks.push({
title: `${prefix} Inconsistent component patterns detected`,
body: buildFocusBody('UI Design', 'Inconsistent Patterns',
readOutput('/tmp/focus-component-patterns.txt'),
['Use the shared Button component instead of raw <button> elements',
'Standardize modal visibility props (prefer `isOpen` pattern)',
'Replace inline styles with Tailwind classes where possible',
'Follow established patterns in existing components'], true),
labels: ['enhancement', 'help wanted', 'auto-qa', 'triage/accepted', 'auto-qa:ui-design'],
});
}
// NFR Coverage & Self-Improvement (weekly)
if (process.env.META_PR_ANALYSIS === 'true') {
checks.push({
title: `${prefix} Weekly NFR analysis: improvement opportunities identified`,
body: [
`## Auto-QA Self-Improvement Analysis`,
'',
`**Generated:** ${now} | **Commit:** \`${sha}\` | **Run:** [View](${runUrl})`,
'',
readOutput('/tmp/meta-pr-analysis.txt', 150),
'',
'### How to Act on This',
'- Review the NFR coverage gaps identified above',
'- Consider adding new Auto-QA checks for under-covered areas',
'- Update `.github/workflows/auto-qa.yml` with new checks',
'- This issue can be closed after reviewing the recommendations',
'',
'---',
`*This is a meta-analysis issue from the [Auto-QA workflow](${runUrl}).*`,
'*It helps identify gaps in automated quality coverage.*',
].join('\n'),
labels: ['enhancement', 'auto-qa', 'triage/accepted', 'auto-qa:meta'],
});
}
// NFR: Test Coverage (every run)
if (process.env.FOCUS_TEST_COVERAGE === 'true') {
checks.push({
title: `${prefix} Components missing test coverage`,
body: buildFocusBody('Testing', 'Missing Test Files',
readOutput('/tmp/focus-test-coverage.txt'),
['Add `.test.tsx` or `.spec.tsx` files for untested components',
'Focus on components with complex logic or user interactions',
'Consider using React Testing Library for component tests',
'Run `npm test -- --coverage` to see current coverage'], true),
labels: ['enhancement', 'help wanted', 'auto-qa', 'triage/accepted', 'auto-qa:nfr'],
});
}
// NFR: Localization (every run)
if (process.env.FOCUS_I18N === 'true') {
checks.push({
title: `${prefix} Hardcoded user-facing strings need extraction`,
body: buildFocusBody('Localization', 'Hardcoded Strings',
readOutput('/tmp/focus-i18n.txt'),
['Extract strings to a translations file (e.g., `en.json`)',
'Use a translation function like `t("key")` or `<Trans>`',
'Consider using react-i18next or similar library',
'Start with visible UI text: buttons, labels, headings'], true),
labels: ['enhancement', 'help wanted', 'auto-qa', 'triage/accepted', 'auto-qa:nfr'],
});
}
// NFR: State Management (every run)
if (process.env.FOCUS_STATE_PATTERNS === 'true') {
checks.push({
title: `${prefix} State management patterns need improvement`,
body: buildFocusBody('Storage & State', 'State Pattern Issues',
readOutput('/tmp/focus-state-patterns.txt'),
['Wrap localStorage calls in try/catch for private browsing',
'Use Context API or state management for prop drilling',
'Consider useSWR/useQuery for data fetching with caching',
'Extract repeated data fetching logic into custom hooks'], true),
labels: ['enhancement', 'help wanted', 'auto-qa', 'triage/accepted', 'auto-qa:nfr'],
});
}
// NFR: Navigation (every run)
if (process.env.FOCUS_NAVIGATION === 'true') {
checks.push({
title: `${prefix} Navigation and routing improvements needed`,
body: buildFocusBody('Navigation', 'Routing Issues',
readOutput('/tmp/focus-navigation.txt'),
['Define route paths as constants in a routes.ts file',
'Add `target="_blank" rel="noopener noreferrer"` to external links',
'Add back navigation to drill-down/detail views',
'Consider adding breadcrumbs for deep navigation'], true),
labels: ['enhancement', 'help wanted', 'auto-qa', 'triage/accepted', 'auto-qa:nfr'],
});
}
// NFR: Efficiency (every run)
if (process.env.FOCUS_EFFICIENCY === 'true') {
checks.push({
title: `${prefix} React efficiency improvements available`,
body: buildFocusBody('Efficiency', 'Performance Patterns',
readOutput('/tmp/focus-efficiency.txt'),
['Move inline style objects to constants or useMemo',
'Wrap callback functions in useCallback when passed as props',
'Use named imports: `import { debounce } from "lodash/debounce"`',
'Consider React.memo for components receiving object props'], true),
labels: ['enhancement', 'help wanted', 'auto-qa', 'triage/accepted', 'auto-qa:nfr'],
});
}
// Flicker Detection (every run)
if (process.env.FOCUS_FLICKER === 'true') {
checks.push({
title: `${prefix} UI flicker patterns detected`,
body: buildFocusBody('UI Quality', 'Flicker Prevention',
readOutput('/tmp/focus-flicker.txt'),
['Batch multiple setState calls into a single update or use useReducer',
'Add Skeleton components for loading states to prevent layout shifts',
'Use Suspense boundaries around async components',
'Only use useLayoutEffect for DOM measurements, not side effects',
'Consider useDeferredValue or useTransition for non-urgent updates'], true),
labels: ['enhancement', 'help wanted', 'auto-qa', 'triage/accepted', 'auto-qa:flicker'],
});
}
// Code Centralization (every run)
if (process.env.FOCUS_CENTRALIZATION === 'true') {
checks.push({
title: `${prefix} Code centralization opportunities found`,
body: buildFocusBody('Code Quality', 'Centralization',
readOutput('/tmp/focus-centralization.txt'),
['Migrate cards to use standardized useCardData/useCardDemoState hooks',
'Extract repeated layout patterns into shared components (StatGrid, CardLayout)',
'Create a useModal hook to standardize modal open/close logic',
'Use DashboardLayout component for consistent grid patterns',
'Move common card utilities to lib/cards/'], true),
labels: ['enhancement', 'help wanted', 'auto-qa', 'triage/accepted', 'auto-qa:centralization'],
});
}
// Inventory Demo Data (every run)
if (process.env.FOCUS_INVENTORY_DEMO === 'true') {
checks.push({
title: `${prefix} Cards missing demo data support`,
body: buildFocusBody('Demo Mode', 'Demo Data Coverage',
readOutput('/tmp/focus-inventory-demo.txt'),
['Cards using useCached* hooks already have demo data via the demoData: parameter in useCache()',
'Cards using isDemoFallback/isDemoData from hooks are already covered',
'For new cards: use useCached* hooks or pass demoData to useCache()',
'Games and pure UI components (modals, animations) do not need demo data',
'Test with localStorage.setItem("kc-demo-mode", "true")'], true),
labels: ['enhancement', 'help wanted', 'auto-qa', 'triage/accepted', 'auto-qa:demo-data'],
});
}
// Console Error Patterns (every run)
if (process.env.FOCUS_CONSOLE_ERRORS === 'true') {
checks.push({
title: `${prefix} Potential console error patterns detected`,
body: buildFocusBody('Error Handling', 'Console Errors',
readOutput('/tmp/focus-console-errors.txt'),
['Add try/catch blocks around async operations',
'Use .catch() on fetch/axios calls to handle network errors',
'Wrap complex components in ErrorBoundary',
'Use optional chaining (?.) for deep property access',
'Show user-visible error states, not just console.error'], false),
labels: ['bug', 'help wanted', 'auto-qa', 'triage/accepted', 'auto-qa:console-errors'],
});
}
// Button/Action Consistency (every run)
if (process.env.FOCUS_BUTTON_ACTIONS === 'true') {
checks.push({
title: `${prefix} Button and action consistency issues`,
body: buildFocusBody('Navigation', 'Button Actions',
readOutput('/tmp/focus-button-actions.txt'),
['Ensure onClick handlers reference defined functions',
'Verify modal triggers have corresponding Modal components',
'Check navigate() calls point to defined routes',
'Replace empty onClick handlers with proper actions or remove them',
'Use TypeScript to catch undefined handler references'], false),
labels: ['bug', 'help wanted', 'auto-qa', 'triage/accepted', 'auto-qa:actions'],
});
}
// Stale Data Patterns (every run)
if (process.env.FOCUS_STALE_DATA === 'true') {
checks.push({
title: `${prefix} Stale data and freshness indicator issues`,
body: buildFocusBody('Data Freshness', 'Stale Data',
readOutput('/tmp/focus-stale-data.txt'),
['Add "Last updated X ago" timestamps to cached data displays',
'Use useDemoMode/useCardDemoState to respond to demo mode toggle',
'Replace "Loading..." text with Skeleton components',
'Add TTL validation to localStorage reads',
'Show visual indicators when data may be stale'], true),
labels: ['enhancement', 'help wanted', 'auto-qa', 'triage/accepted', 'auto-qa:stale-data'],
});
}
// Color Consistency (every run)
if (process.env.FOCUS_COLOR_CONSISTENCY === 'true') {
checks.push({
title: `${prefix} Color consistency issues across components`,
body: buildFocusBody('Design System', 'Color Consistency',
readOutput('/tmp/focus-color-consistency.txt'),
['Use green for success/healthy states, red for errors/failures',
'Standardize on consistent color shades (e.g., always use -500 for primary)',
'Use consistent opacity values (10, 20, 50 rather than arbitrary values)',
'Pick one color family (purple vs violet) and use it consistently',
'Define semantic color tokens in tailwind.config.js'], true),
labels: ['enhancement', 'help wanted', 'auto-qa', 'triage/accepted', 'auto-qa:color'],
});
}
// User Count Feature (every run)
if (process.env.FOCUS_USER_COUNT === 'true') {
checks.push({
title: `${prefix} Active user count feature needs improvements`,
body: buildFocusBody('Features', 'User Count',
readOutput('/tmp/focus-user-count.txt'),
['Add WebSocket or EventSource for real-time user count updates',
'Handle demo mode with mock user count data',
'Add error handling with fallback display',
'Ensure proper cleanup of subscriptions on component unmount',
'Display user count prominently in header/layout'], true),
labels: ['enhancement', 'help wanted', 'auto-qa', 'triage/accepted', 'auto-qa:user-count'],
});
}
// Token Counter Feature (every run)
if (process.env.FOCUS_TOKEN_COUNTER === 'true') {
checks.push({
title: `${prefix} Token counter feature needs improvements`,
body: buildFocusBody('Features', 'Token Counter',
readOutput('/tmp/focus-token-counter.txt'),
['Add visual progress bar or percentage display for token usage',
'Implement configurable token limits with warnings at thresholds',
'Handle demo mode with mock token data',
'Add reset functionality to clear token count',
'Persist token usage across sessions',
'Consider adding usage predictions/estimates'], true),
labels: ['enhancement', 'help wanted', 'auto-qa', 'triage/accepted', 'auto-qa:token-counter'],
});
}
// Tour and Onboarding (every run)
if (process.env.FOCUS_TOUR === 'true') {
checks.push({
title: `${prefix} Tour and onboarding improvements needed`,
body: buildFocusBody('Onboarding', 'Tour Coverage',
readOutput('/tmp/focus-tour.txt'),
['Ensure tour target elements exist in the DOM with correct IDs',
'Add tour coverage for new pages and features',
'Implement tour reset in Settings for users to replay',
'Persist tour completion state to avoid showing repeatedly',
'Add ARIA attributes to tour modals/tooltips for accessibility'], true),
labels: ['enhancement', 'help wanted', 'auto-qa', 'triage/accepted', 'auto-qa:tour'],
});
}
// Local Cluster Detection and Creation (every run)
if (process.env.FOCUS_LOCAL_CLUSTERS === 'true') {
checks.push({
title: `${prefix} Local cluster detection and creation needs improvements`,
body: buildFocusBody('Features', 'Local Clusters',
readOutput('/tmp/focus-local-clusters.txt'),
['Implement detection for kind, k3d, and minikube tools',
'Add cluster creation UI with tool selection and naming',
'Show cluster status (running/stopped) with visual indicators',
'Add cluster deletion with confirmation dialog',
'Check agent connectivity before showing cluster management',
'Show loading spinners during create/delete operations',
'Display installation instructions when no tools are detected',
'Add manual refresh button to update cluster list',
'Handle demo mode with mock cluster data'], true),
labels: ['enhancement', 'help wanted', 'auto-qa', 'triage/accepted', 'auto-qa:local-clusters'],
});
}
// Refresh Icon Animation Consistency (every run)
if (process.env.FOCUS_REFRESH_SPIN === 'true') {
checks.push({
title: `${prefix} Refresh icon animation consistency issues`,
body: buildFocusBody('UI', 'Refresh Animation',
readOutput('/tmp/focus-refresh-spin.txt'),
['Add animate-spin class to all RefreshCw/RefreshCcw icons',
'Tie spin animation to loading state (isLoading, isFetching)',
'Ensure animation completes a full 360° rotation',
'Use consistent animation duration across all components',
'Add refresh icons to data-fetching inventory cards',
'Use Tailwind animate-spin for consistent behavior'], true),
labels: ['enhancement', 'help wanted', 'auto-qa', 'triage/accepted', 'auto-qa:refresh-spin'],
});
}
// DOM Nesting and Structure Violations (every run)
if (process.env.FOCUS_DOM_ERRORS === 'true') {
checks.push({
title: `${prefix} DOM nesting and structure violations found`,
body: buildFocusBody('UI', 'DOM Structure',
readOutput('/tmp/focus-dom-errors.txt'),
['Fix button-inside-button: use div with role="button" and tabIndex={0}',
'Fix interactive elements inside anchor: restructure or use onClick navigation',
'Fix block elements inside <p>: use <div> or <span> appropriately',
'Fix nested forms: refactor to single form with fieldsets',
'Fix table structure: ensure proper thead/tbody/tr hierarchy',
'Fix list items: ensure <li> is always inside <ul> or <ol>',
'Add role and tabIndex to clickable non-interactive elements for accessibility'], true),
labels: ['bug', 'help wanted', 'auto-qa', 'triage/accepted', 'auto-qa:dom-errors'],
});
}
// Silent Failure Detection (every run)
if (process.env.FOCUS_SILENT_FAILURES === 'true') {
checks.push({
title: `${prefix} Silent failures in error handling detected`,
body: buildFocusBody('UX', 'Error Handling',
readOutput('/tmp/focus-silent-failures.txt'),
['Add toast notifications to catch blocks that only log errors',
'Show user-friendly error messages instead of just console.error',
'Use setError state to display inline error messages',
'Consider using error boundaries for component-level failures',
'Log errors to monitoring service AND show user notification'], true),
labels: ['bug', 'help wanted', 'auto-qa', 'triage/accepted', 'auto-qa:silent-failures'],
});
}
// Missing User Feedback (every run)
if (process.env.FOCUS_FEEDBACK_GAPS === 'true') {
checks.push({
title: `${prefix} Missing user feedback on actions`,
body: buildFocusBody('UX', 'User Feedback',
readOutput('/tmp/focus-feedback-gaps.txt'),
['Add toast notifications for successful form submissions',
'Show confirmation dialogs before destructive actions (delete, remove)',
'Display success messages after save/update operations',
'Add error messages when API calls fail',
'Consider progress indicators for long-running operations'], true),
labels: ['enhancement', 'help wanted', 'auto-qa', 'triage/accepted', 'auto-qa:feedback-gaps'],
});
}
// Loading State Gaps (every run)
if (process.env.FOCUS_LOADING_GAPS === 'true') {
checks.push({
title: `${prefix} Missing loading states on async actions`,
body: buildFocusBody('UX', 'Loading States',
readOutput('/tmp/focus-loading-gaps.txt'),
['Add isLoading state to async onClick handlers',
'Disable buttons while async operations are in progress',
'Show spinner or loading text on action buttons',
'Use skeleton components for content loading',
'Prevent double-submission with disabled state'], true),
labels: ['enhancement', 'help wanted', 'auto-qa', 'triage/accepted', 'auto-qa:loading-gaps'],
});
}
// Keyboard Navigation Gaps (every run)
if (process.env.FOCUS_KEYBOARD_NAV === 'true') {
checks.push({
title: `${prefix} Keyboard navigation gaps detected`,
body: buildFocusBody('A11y', 'Keyboard Navigation',
readOutput('/tmp/focus-keyboard-nav.txt'),
['Add arrow key navigation to dropdown menus',
'Implement Tab key navigation in tab panels',
'Support Enter/Space to activate menu items',
'Add Home/End keys for list navigation',
'Ensure focus is visible on all interactive elements'], true),
labels: ['enhancement', 'help wanted', 'auto-qa', 'triage/accepted', 'auto-qa:keyboard-nav'],
});
}
// ARIA Gaps (every run)
if (process.env.FOCUS_ARIA_GAPS === 'true') {
checks.push({
title: `${prefix} ARIA label and role gaps detected`,
body: buildFocusBody('A11y', 'ARIA Attributes',
readOutput('/tmp/focus-aria-gaps.txt'),
['Add aria-label to elements with role="button"',
'Add aria-label or title to icon-only buttons',
'Add role="dialog" and aria-modal to modal components',
'Use aria-labelledby to reference visible labels',
'Add aria-describedby for additional context'], true),
labels: ['enhancement', 'help wanted', 'auto-qa', 'triage/accepted', 'auto-qa:aria-gaps'],
});
}
// Modal Safety (every run)
if (process.env.FOCUS_MODAL_SAFETY === 'true') {
checks.push({
title: `${prefix} Modal safety issues detected`,
body: buildFocusBody('UX', 'Modal Safety',
readOutput('/tmp/focus-modal-safety.txt'),
['Disable backdrop close for modals with forms (closeOnBackdrop={false})',
'Add unsaved changes warning before closing form modals',
'Ensure Escape key is handled for modal close',
'Trap focus within modals to prevent accidental navigation',
'Add confirmation for modals with pending changes'], true),
labels: ['bug', 'help wanted', 'auto-qa', 'triage/accepted', 'auto-qa:modal-safety'],
});
}
// Event Handler Parity (every run)
if (process.env.FOCUS_EVENT_PARITY === 'true') {
checks.push({
title: `${prefix} Event handler accessibility parity issues`,
body: buildFocusBody('A11y', 'Event Parity',
readOutput('/tmp/focus-event-parity.txt'),
['Add role="button" and tabIndex={0} to clickable divs/spans',
'Add onKeyDown handler alongside onClick for keyboard support',
'Handle Enter and Space keys for button-like elements',
'Consider using actual <button> elements instead of divs',
'Ensure all interactive elements are keyboard accessible'], true),
labels: ['enhancement', 'help wanted', 'auto-qa', 'triage/accepted', 'auto-qa:event-parity'],
});
}
// Adoption & Engagement Psychology — individual issues per finding
const adoptionFindings = [
{ env: 'ADOPT_VARIABLE_REWARDS', file: '/tmp/adopt-variable-rewards.txt', title: 'Add variable rewards (rotating tips/spotlights) to static dashboards' },
{ env: 'ADOPT_STREAK', file: '/tmp/adopt-streak.txt', title: 'Add streak/consistency tracking for daily return engagement' },
{ env: 'ADOPT_MASTERY', file: '/tmp/adopt-mastery.txt', title: 'Add feature mastery/exploration progression system' },
{ env: 'ADOPT_COMPLETENESS', file: '/tmp/adopt-completeness.txt', title: 'Add setup completeness score with progress bar' },
{ env: 'ADOPT_SOCIAL_PROOF', file: '/tmp/adopt-social-proof.txt', title: 'Add social proof signals (popular cards, team usage counts)' },
{ env: 'ADOPT_ACTIVITY_FEED', file: '/tmp/adopt-activity-feed.txt', title: 'Add lightweight activity feed showing recent platform events' },
{ env: 'ADOPT_FEATURE_TEASERS', file: '/tmp/adopt-feature-teasers.txt', title: 'Add feature teasers and discovery prompts for hidden capabilities' },
{ env: 'ADOPT_EASTER_EGGS', file: '/tmp/adopt-easter-eggs.txt', title: 'Add easter eggs and hidden discovery moments' },
{ env: 'ADOPT_WELCOME_BACK', file: '/tmp/adopt-welcome-back.txt', title: 'Add welcome-back experience for returning users' },
];
for (const finding of adoptionFindings) {
if (process.env[finding.env] === 'true') {
checks.push({
title: `${prefix} ${finding.title}`,
body: [
`## Auto-QA [Adoption]: ${finding.title}`,
'',
`**Detected:** ${now} | **Focus:** Adoption Psychology | **Commit:** \`${sha}\` | **Run:** [View](${runUrl})`,
'',
readOutput(finding.file),
'',
'### Implementation Guidance',
'- Implement as a small, self-contained UX improvement (target: 1 PR, <200 lines)',
'- Add `ksc_` GA4 event tracking for the new engagement feature',
'- Prefer lightweight implementations over complex systems',
...(prGuidance ? ['', `> **PR Guidance:** ${prGuidance}`] : []),
'',
'---',
`*This issue was automatically created by the [Auto-QA workflow](${runUrl}) during **Adoption Psychology** analysis.*`,
'*Labels `help wanted` enable contributors to pick this up.*',
].join('\n'),
labels: ['enhancement', 'help wanted', 'auto-qa', 'triage/accepted', 'auto-qa:adoption'],
bonus: true, // bypass maxIssues cap — adoption issues are always created
});
}
}
// ── Apply tuning: filter blocked categories, prioritize boosted ──
const blockedSet = new Set((process.env.BLOCKED_CATEGORIES || '').split(',').filter(Boolean));
const boostedSet = new Set((process.env.BOOSTED_CATEGORIES || '').split(',').filter(Boolean));
const beforeCount = checks.length;
const filteredChecks = checks.filter(check => {
const cat = (check.labels || []).find(l => l.startsWith('auto-qa:') && l !== 'auto-qa');
const catName = cat ? cat.replace('auto-qa:', '') : null;
if (catName && blockedSet.has(catName)) {
core.info(`TUNING BLOCKED: Skipping "${check.title}" (category "${catName}" has <20% acceptance rate)`);
return false;
}
return true;
});
// Sort: boosted categories first
filteredChecks.sort((a, b) => {
const catA = (a.labels || []).find(l => l.startsWith('auto-qa:'))?.replace('auto-qa:', '') || '';
const catB = (b.labels || []).find(l => l.startsWith('auto-qa:'))?.replace('auto-qa:', '') || '';
const aBoost = boostedSet.has(catA) ? 0 : 1;
const bBoost = boostedSet.has(catB) ? 0 : 1;
return aBoost - bBoost;
});
if (beforeCount !== filteredChecks.length) {
core.info(`Tuning filtered out ${beforeCount - filteredChecks.length} check(s) from blocked categories`);
}
// Replace checks with filtered+sorted version
checks.length = 0;
checks.push(...filteredChecks);
// ── Create issues ──
if (checks.length === 0) {
core.info(`All checks passed or blocked by tuning (focus: ${focusArea}). No issues to create.`);
return;
}
core.info(`${checks.length} finding(s) to process (focus: ${focusArea}). Creating issues...`);
// Rate-limit-aware retry helper for GitHub API calls.
// Waits for x-ratelimit-reset on 403/429, with exponential backoff fallback.
const MAX_RETRIES = 3;
const INITIAL_BACKOFF_MS = 5000;
async function withRetry(fn, label = 'API call') {
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
try {
return await fn();
} catch (err) {
const isRateLimit = (err.status === 403 || err.status === 429) &&
(err.message || '').toLowerCase().includes('rate limit');
if (!isRateLimit || attempt === MAX_RETRIES) throw err;
// Calculate wait time from reset header or use exponential backoff
const resetEpoch = err.response?.headers?.['x-ratelimit-reset'];
const backoffMs = INITIAL_BACKOFF_MS * Math.pow(2, attempt);
let waitMs = backoffMs;
if (resetEpoch) {
const resetMs = parseInt(resetEpoch, 10) * 1000 - Date.now();
const RATE_LIMIT_BUFFER_MS = 1000;
waitMs = Math.max(resetMs + RATE_LIMIT_BUFFER_MS, backoffMs);
}
const MAX_WAIT_MS = 120000;
waitMs = Math.min(waitMs, MAX_WAIT_MS);
core.warning(`${label}: rate limited (attempt ${attempt + 1}/${MAX_RETRIES}), waiting ${Math.round(waitMs / 1000)}s...`);
await new Promise(resolve => setTimeout(resolve, waitMs));
}
}
}
// Fetch both open AND recently closed issues to prevent duplicates after PR merges
const [{ data: openIssues }, { data: closedIssues }] = await Promise.all([
withRetry(() => github.rest.issues.listForRepo({
owner: context.repo.owner,
repo: context.repo.repo,
state: 'open',
labels: 'auto-qa',
per_page: 100,
}), 'list open issues'),
withRetry(() => github.rest.issues.listForRepo({
owner: context.repo.owner,
repo: context.repo.repo,
state: 'closed',
labels: 'auto-qa',
per_page: 50,
sort: 'updated',
direction: 'desc',
}), 'list closed issues'),
]);
// Filter closed issues to only those closed in the last 7 days
const sevenDaysAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000);
const recentlyClosedIssues = closedIssues.filter(i => new Date(i.closed_at) > sevenDaysAgo);
const existingIssues = [...openIssues, ...recentlyClosedIssues];
core.info(`Found ${openIssues.length} open + ${recentlyClosedIssues.length} recently closed auto-qa issues`);
// Helper to extract the core issue pattern (removes dynamic counts/numbers)
const extractPattern = (title) => {
return title
.replace(/\[Auto-QA\]\s*/, '') // Remove prefix
.replace(/\d+\s*(critical|high|TODO|FIXME|HACK)/gi, '$1') // Remove counts
.replace(/\d+/g, 'N') // Replace remaining numbers with N
.toLowerCase()
.trim();
};
let created = 0;
let createdTotal = 0;
const rejections = [];
let capReached = false;
for (const check of checks) {
if (created >= maxIssues && !check.bonus) {
if (!capReached) {
core.warning(`Max issues cap reached (${maxIssues} issues). Remaining non-bonus findings skipped.`);
capReached = true;
}
continue;
}
// Check for duplicates using pattern matching (handles varying counts in titles)
const checkPattern = extractPattern(check.title);
const duplicate = existingIssues.find(i => {
const existingPattern = extractPattern(i.title);
return existingPattern === checkPattern;
});
if (duplicate) {
const status = duplicate.state === 'open' ? 'still open' : `closed ${new Date(duplicate.closed_at).toLocaleDateString()}`;
core.info(`Skipping duplicate: "${check.title}" matches #${duplicate.number} (${status})`);
continue;
}
const { data: issue } = await withRetry(() => github.rest.issues.create({
owner: context.repo.owner,
repo: context.repo.repo,
title: check.title,
body: check.body,
labels: check.labels,
}), `create issue "${check.title}"`);
core.info(`Created issue #${issue.number}: ${check.title}`);
createdTotal++;
// Auto-assign Copilot coding agent (gated by ASSIGN_COPILOT env var)
const assignCopilot = process.env.ASSIGN_COPILOT === 'true';
if (!assignCopilot) {
core.info(`Copilot assignment disabled (ASSIGN_COPILOT=${process.env.ASSIGN_COPILOT})`);
}
const repo = `${context.repo.owner}/${context.repo.repo}`;
if (assignCopilot) try {
await github.request('POST /repos/{owner}/{repo}/issues/{issue_number}/assignees', {
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issue.number,
assignees: ['copilot-swe-agent[bot]'],
agent_assignment: {
target_repo: repo,
base_branch: 'main',
},
});
core.info(`Assigned Copilot to #${issue.number}`);
} catch (e) {
const hint = e.status === 401
? ' — CONSOLE_AUTO PAT may be expired (check repo secrets)'
: e.status === 403
? ' — token may lack repo scope or Copilot agent is not enabled'
: e.status === 404
? ' — Copilot coding agent may not be enabled for this repo (check GitHub Copilot settings)'
: '';
core.warning(`Could not assign Copilot to #${issue.number}: ${e.message}${hint}`);
// Only track as rejection if it's a rate-limit or auth issue (429/401/403).
// 404 means the Copilot agent endpoint doesn't exist — not a rate limit problem.
if (e.status !== 404) {
rejections.push({ issueNumber: issue.number, status: e.status || 'unknown', message: e.message });
}
}
if (!check.bonus) {
created++;
// Throttle Copilot assignments to avoid rate limiting (only when enabled).
if (assignCopilot) {
const delaySeconds = parseInt(process.env.COPILOT_ASSIGNMENT_DELAY_S || '120');
if (created < maxIssues && delaySeconds > 0) {
core.info(`Waiting ${delaySeconds}s before next Copilot assignment to avoid rate limits...`);
await new Promise(resolve => setTimeout(resolve, delaySeconds * 1000));
}
}
} else {
core.info(`Bonus issue — does not count against maxIssues cap`);
}
// Auto-triage is handled by including triage/accepted in the issue labels at creation
}
// If any Copilot assignments were rejected, create a diagnostic issue
// recommending rate limit config changes (deduplicated).
if (rejections.length > 0) {
const diagTitle = '[Auto-QA] Copilot assignment rejected — review rate limit config';
try {
const existingDiag = await withRetry(() => github.rest.issues.listForRepo({
owner: context.repo.owner,
repo: context.repo.repo,
state: 'open',
labels: 'auto-qa,auto-qa:rate-limit',
per_page: 10,
}), 'list diagnostic issues');
const hasDiag = existingDiag.data.some(i => i.title.includes('Copilot assignment rejected'));
if (!hasDiag) {
const currentDelay = process.env.COPILOT_ASSIGNMENT_DELAY_S || '120';
const rejectionList = rejections.map(r =>
`- Issue #${r.issueNumber}: HTTP ${r.status} — ${r.message}`
).join('\n');
await withRetry(() => github.rest.issues.create({
owner: context.repo.owner,
repo: context.repo.repo,
title: diagTitle,
body: [
'## Copilot Assignment Failures',
'',
`${rejections.length} assignment(s) were rejected during the auto-qa run:`,
'',
rejectionList,
'',
'## Current Config',
`- \`COPILOT_ASSIGNMENT_DELAY_S\`: ${currentDelay}s`,
`- \`MAX_ISSUES_PER_RUN\`: ${maxIssues}`,
'',
'## Recommendation',
`Increase \`COPILOT_ASSIGNMENT_DELAY_S\` (currently ${currentDelay}s) or decrease \`MAX_ISSUES_PER_RUN\` to reduce Copilot rate limit pressure.`,
'',
'If the error is 401/403, check that the `CONSOLE_AUTO` PAT is valid and Copilot agent is enabled.',
'',
'---',
'*Auto-created by the auto-qa workflow when Copilot assignments fail.*',
].join('\n'),
labels: ['auto-qa', 'auto-qa:rate-limit', 'triage/needed'],
}), 'create diagnostic issue');
core.warning(`Created diagnostic issue: ${rejections.length} Copilot assignment(s) rejected`);
} else {
core.info('Diagnostic issue for Copilot rejections already exists — skipping');
}
} catch (diagErr) {
core.warning(`Failed to create diagnostic issue: ${diagErr.message}`);
}
}
core.info(`Auto-QA complete (focus: ${focusArea}): ${createdTotal} issue(s) created (${created} counted against cap), ${checks.length} finding(s) total.`);