Auto-QA Agent #1206
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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.`); |