"""
boilerplate.py — Embedded Helix-QA framework template files.

BOILERPLATE maps framework-relative paths to file content.
All files come from boilerplate/framework/ — embedded here so the
package has no runtime dependency on the boilerplate directory.

INFRA_FILES is the subset written by write_files_to_helix scaffold_and_tests mode.
"""
from __future__ import annotations

# fmt: off
BOILERPLATE: dict[str, str] = {
    ".env.example": "# ── Application ──────────────────────────────────────────────────────────────\nBASE_URL=https://your-app-url.com\nENVIRONMENT=local\n\n# ── Authentication ────────────────────────────────────────────────────────────\nAUTH_USERNAME=your-test-user\nAUTH_PASSWORD=your-test-password\nSTORAGE_STATE_PATH=./storage-state/auth.json\nAUTH_TIMEOUT=30000\n\n# ── AI / Self-Healing ─────────────────────────────────────────────────────────\n# Provider: openai | azure | anthropic | local\nAI_PROVIDER=openai\nAI_API_KEY=sk-your-openai-api-key\nAI_MODEL=gpt-4o\nAI_TEMPERATURE=0.3\nAI_MAX_TOKENS=2000\nENABLE_SELF_HEALING=true\nENABLE_AI_TEST_GENERATION=false\n\n# Azure OpenAI (only when AI_PROVIDER=azure)\nAZURE_OPENAI_ENDPOINT=https://your-resource.openai.azure.com/\nAZURE_OPENAI_API_KEY=your-azure-key\nAZURE_OPENAI_DEPLOYMENT=gpt-4o\n\n# Anthropic (only when AI_PROVIDER=anthropic)\nANTHROPIC_API_KEY=sk-ant-your-anthropic-key\n\n# Local LLM (only when AI_PROVIDER=local)\nLOCAL_LLM_ENDPOINT=http://localhost:11434/api\n\n# ── Browser ───────────────────────────────────────────────────────────────────\nHEADLESS=true\nBROWSER=chromium\nSLOW_MO=0\nRECORD_VIDEO=false\n\n# ── Execution ─────────────────────────────────────────────────────────────────\nPARALLEL_WORKERS=2\nRETRY_ATTEMPTS=1\nTIMEOUT=30000\nNAVIGATION_TIMEOUT=30000\nACTION_TIMEOUT=10000\n\n# ── Reporting ─────────────────────────────────────────────────────────────────\nSCREENSHOT_ON_FAILURE=true\nVIDEO_ON_FAILURE=false\nTRACE_ON_FAILURE=true\nREPORT_OUTPUT_DIR=./test-results/reports\nFULL_PAGE_SCREENSHOTS=false\n\n# ── Locator Healing ───────────────────────────────────────────────────────────\nLOCATOR_HEAL_ATTEMPTS=3\nLOCATOR_TIMEOUT=10000\nENABLE_LOCATOR_VERSIONING=false\nHEAL_STORE_PATH=./self-heal/healed-locators.json\nHEALING_DASHBOARD_PORT=7890\nHEALIX_REVIEW_PORT=7891\n\n# ── Logging ───────────────────────────────────────────────────────────────────\nLOG_LEVEL=info\nLOG_TO_FILE=false\nLOG_FILE_PATH=./logs/test-execution.log\n\n# ── CI/CD ─────────────────────────────────────────────────────────────────────\nCI=false\nSHARD_INDEX=1\nSHARD_TOTAL=1\n\n# ── Performance ───────────────────────────────────────────────────────────────\nPERFORMANCE_MONITORING=false\nCOLLECT_METRICS=false\n\n# ── Security ─────────────────────────────────────────────────────────────────\nMASK_SENSITIVE_DATA=true\n",
    ".gitignore": "node_modules/\ndist/\n.env\n*.log\ntest-results/screenshots/\ntest-results/videos/\ntest-results/traces/\ntest-results/visual-baselines/\ntest-results/reports/\nlogs/\nstorage-state/auth.json\nself-heal/healed-locators.json\n.DS_Store\n*.js.map\n",
    "Dockerfile": "FROM mcr.microsoft.com/playwright:v1.57.0-jammy\n\nWORKDIR /app\n\nCOPY package*.json ./\nRUN npm ci\n\n# Install Playwright browsers\nRUN npx playwright install --with-deps chromium\n\nCOPY . .\n\nENV CI=true\nENV HEADLESS=true\nENV BROWSER=chromium\nENV PARALLEL_WORKERS=4\n\nCMD [\"npm\", \"run\", \"test:ci\"]\n",
    "README.md": "# QA Automation Framework Boilerplate\n\nA production-ready, zero-config **Playwright + Cucumber + TypeScript** test automation framework with **three-layer AI self-healing** for locators. Copy once, customise to your app.\n\n---\n\n## Features\n\n| Capability | Detail |\n|---|---|\n| **BDD / Gherkin** | Cucumber 12 with tagged scenario filtering |\n| **Parallel execution** | 2 workers (local), 4 workers (CI) |\n| **Three-layer self-healing** | LocatorHealer → TimingHealer → VisualIntentChecker |\n| **5 AI providers** | OpenAI, Anthropic/Claude, Grok, Ollama, LM Studio |\n| **Healing dashboard** | Live browser UI at port 7890 during test runs |\n| **Review server** | Post-run review + PR creation at port 7891 |\n| **CI/CD integration** | Azure Pipelines + GitHub Actions; `healix:apply-ci` script |\n| **Docker** | `docker-compose.yml` for reproducible runs |\n| **Auth caching** | Browser storage-state login once, reuse across all tests |\n| **TypeScript** | Strict mode, ES2022, full path aliases |\n\n---\n\n## Quick Start\n\n```bash\n# 1. Install dependencies\nnpm install\nnpx playwright install chromium\n\n# 2. Configure your environment\ncp .env.example .env\n# Edit .env — set BASE_URL and AI_API_KEY at minimum\n\n# 3. Run the example test\nnpm test\n\n# 4. View the HTML report\nopen test-results/reports/cucumber-report.html\n```\n\n---\n\n## Directory Structure\n\n```\nsrc/\n  config/          # Environment, global setup/teardown\n  hooks/           # Cucumber BeforeAll/Before/After/AfterAll\n  pages/           # BasePage (all healing APIs wired here)\n  locators/        # base.locators.ts — common selectors\n  test/\n    features/      # Your .feature files (Gherkin scenarios)\n    steps/         # Cucumber step definitions\n  templates/       # Copy these to create new pages rapidly\n  utils/\n    helpers/       # Logger, RetryHandler, WaitHelper\n    locators/      # Full self-healing infrastructure\n    ai-assistant/  # AILocatorGenerator, AISelfHealing, AITestGenerator\n    storage-state/ # AuthManager, AuthSetup\nstorage-state/     # Saved browser auth state (git-ignored)\ntest-results/      # Reports, screenshots, traces, videos\nlogs/              # winston log files\n```\n\n---\n\n## Creating a New Page\n\n```bash\n# 1. Copy templates\ncp src/templates/_template.feature        src/test/features/my-page.feature\ncp src/templates/_template.locators.ts    src/locators/my-page.locators.ts\ncp src/templates/_template.steps.ts       src/test/steps/my-page.steps.ts\ncp src/templates/_TemplatePage.ts         src/pages/MyPage.ts\n\n# 2. Rename \"Template\" → your page name in each file\n# 3. Update locators to match your page elements\n# 4. Write your Gherkin scenarios\n# 5. npm test -- --tags @my-tag\n```\n\n---\n\n## Self-Healing Architecture\n\n### Layer 1 — LocatorHealer\nWhen a locator fails, automatically tries:\n1. Primary selector (from locators file)\n2. `getByRole` inferred from `intent` string\n3. `getByLabel` / `getByPlaceholder`\n4. `getByText`\n5. AI Vision (page screenshot → AI identifies element)\n6. CDPSession Accessibility Tree fallback\n\n### Layer 2 — TimingHealer\nAdaptive EMA-based network-idle timeouts per labelled action. Learns expected network patterns per step over time, adjusting automatically as your app's performance changes.\n\n### Layer 3 — VisualIntentChecker\nScreenshot-based element identity verification. For locators with `visualIntent: true`, captures element screenshots and diffs against baselines to detect visual regressions.\n\n---\n\n## AI Provider Configuration\n\nSet `AI_PROVIDER` in `.env`:\n\n| Provider | `AI_PROVIDER` value | Notes |\n|---|---|---|\n| OpenAI (gpt-4o) | `openai` | `AI_API_KEY` = OpenAI key |\n| Anthropic Claude | `anthropic` or `claude` | `AI_API_KEY` = Anthropic key |\n| Grok | `grok` | `AI_API_KEY` = xAI key |\n| Ollama (local) | `ollama` | `AI_BASE_URL=http://localhost:11434/v1` |\n| LM Studio | `local` | `AI_BASE_URL=http://localhost:1234/v1` |\n\nSelf-healing still works without an AI key — Layers 2 and 3 are purely algorithmic.\n\n---\n\n## Healing Dashboard\n\nStart a test run. The dashboard auto-opens at **http://localhost:7890** (or set `HEALING_DASHBOARD_PORT`).\n\n- **Live view**: watch heals in real time\n- **Summary**: counts by strategy\n- **Registry**: all healed locators + approval status\n\n### Post-run Review\n\n```bash\nnpm run healix:review\n# Opens review server at http://localhost:7891\n# Approve / reject individual heals, then click \"Apply + Create PR\"\n```\n\n### CI Auto-Apply\n\n```bash\nHEALIX_CI_AUTO_APPROVE=true npm run healix:apply-ci\n```\n\nApplies all heals to source files, pushes a branch, and opens a PR automatically (GitHub Actions or Azure Pipelines).\n\n---\n\n## Authentication Setup\n\nIf your app requires login:\n\n```bash\nnpm run auth:setup\n```\n\nThen set `USE_STORAGE_STATE=true` in `.env`. All subsequent scenarios reuse the saved session.\n\n---\n\n## Scripts Reference\n\n| Script | Purpose |\n|---|---|\n| `npm test` | Run all tests (parallel) |\n| `npm run test:smoke` | `@smoke` tag only |\n| `npm run test:regression` | `@regression` tag only |\n| `npm run test:headed` | Headed browser (debug) |\n| `npm run test:ci` | CI profile (4 workers, no retry) |\n| `npm run auth:setup` | Save browser auth state |\n| `npm run healix:review` | Start post-run review server |\n| `npm run healix:apply-ci` | CI headless heal applicator |\n| `npm run typecheck` | TypeScript type-check only |\n| `npm run lint` | ESLint check |\n| `npm run docker:up` | Run tests in Docker |\n\n---\n\n## Environment Variables\n\nSee [.env.example](.env.example) for the full reference.\n\n| Variable | Default | Description |\n|---|---|---|\n| `BASE_URL` | `https://example.com` | Application under test |\n| `BROWSER` | `chromium` | `chromium` / `firefox` / `webkit` |\n| `HEADLESS` | `true` | Headless mode |\n| `AI_PROVIDER` | `openai` | AI provider for self-healing |\n| `AI_API_KEY` | — | API key for chosen provider |\n| `HEALING_DASHBOARD_PORT` | `7890` | Live healing dashboard |\n| `HEALIX_REVIEW_PORT` | `7891` | Post-run review server |\n\n---\n\n## Locator Stability Guide\n\n| Stability | Selector type | Use when |\n|---|---|---|\n| 100 | `[data-test=\"x\"]` or `[data-testid=\"x\"]` | Ask devs to add test IDs |\n| 90 | `getByRole` with accessible name | Element has ARIA role |\n| 80 | `#id` | Stable, unique ID exists |\n| 70 | `aria-label` or `placeholder` | Form inputs |\n| 60 | CSS class | Last resort — breaks on redesign |\n\n---\n\n## Contributing\n\n1. Fork this repo\n2. Create a feature branch\n3. Add a failing test + implementation\n4. Submit a PR — Healix will self-heal any locator drift automatically\n",
    "azure-pipelines.yml": "trigger:\n  branches:\n    include:\n      - main\n      - develop\n      - 'release/*'\n  paths:\n    exclude:\n      - 'docs/*'\n      - '*.md'\n\npr:\n  branches:\n    include:\n      - main\n      - develop\n\nvariables:\n  NODE_VERSION: '20.x'\n  PARALLEL_WORKERS: 4\n\nstages:\n  # ── Stage 1: Quality Gates ────────────────────────────────────────────────\n  - stage: Quality\n    displayName: 'Quality Gates'\n    jobs:\n      - job: Lint\n        displayName: 'Lint & Type Check'\n        pool:\n          vmImage: 'ubuntu-latest'\n        steps:\n          - task: NodeTool@0\n            inputs:\n              versionSpec: $(NODE_VERSION)\n            displayName: 'Install Node.js'\n          - script: npm ci\n            displayName: 'Install dependencies'\n          - script: npm run lint\n            displayName: 'ESLint'\n          - script: npm run typecheck\n            displayName: 'TypeScript check'\n\n  # ── Stage 2: Smoke Tests ──────────────────────────────────────────────────\n  - stage: Smoke\n    displayName: 'Smoke Tests'\n    dependsOn: Quality\n    condition: succeeded()\n    jobs:\n      - job: SmokeTests\n        displayName: 'Run @smoke tests'\n        pool:\n          vmImage: 'ubuntu-latest'\n        timeoutInMinutes: \"30\"\n        steps:\n          - task: NodeTool@0\n            inputs:\n              versionSpec: $(NODE_VERSION)\n          - script: npm ci\n            displayName: 'Install dependencies'\n          - script: npx playwright install --with-deps chromium\n            displayName: 'Install Playwright browsers'\n          - script: cp .env.example .env\n            displayName: 'Configure environment'\n          - script: npm run test:smoke\n            displayName: 'Run smoke tests'\n            env:\n              BASE_URL: $(BASE_URL)\n              AUTH_USERNAME: $(AUTH_USERNAME)\n              AUTH_PASSWORD: $(AUTH_PASSWORD)\n              AI_API_KEY: $(AI_API_KEY)\n              ANTHROPIC_API_KEY: $(ANTHROPIC_API_KEY)\n              CI: 'true'\n              HEADLESS: 'true'\n          - task: PublishTestResults@2\n            condition: always()\n            inputs:\n              testResultsFormat: 'JUnit'\n              testResultsFiles: 'test-results/reports/*.xml'\n          - task: PublishPipelineArtifact@1\n            condition: always()\n            inputs:\n              targetPath: 'test-results'\n              artifact: 'smoke-test-results'\n\n  # ── Stage 3: Regression Tests ─────────────────────────────────────────────\n  - stage: Regression\n    displayName: 'Regression Tests'\n    dependsOn: Smoke\n    condition: succeeded()\n    jobs:\n      - job: RegressionTests\n        displayName: 'Run @regression tests'\n        pool:\n          vmImage: 'ubuntu-latest'\n        timeoutInMinutes: \"60\"\n        steps:\n          - task: NodeTool@0\n            inputs:\n              versionSpec: $(NODE_VERSION)\n          - script: npm ci\n          - script: npx playwright install --with-deps chromium\n          - script: cp .env.example .env\n          - script: npm run test:ci\n            displayName: 'Run regression tests'\n            env:\n              BASE_URL: $(BASE_URL)\n              AUTH_USERNAME: $(AUTH_USERNAME)\n              AUTH_PASSWORD: $(AUTH_PASSWORD)\n              AI_API_KEY: $(AI_API_KEY)\n              ANTHROPIC_API_KEY: $(ANTHROPIC_API_KEY)\n              CI: 'true'\n              HEADLESS: 'true'\n              PARALLEL_WORKERS: $(PARALLEL_WORKERS)\n          - script: npm run healix:apply-ci\n            displayName: 'Apply healed locators (CI)'\n            condition: always()\n            env:\n              GH_TOKEN: $(GH_TOKEN)\n              HEAL_SEARCH_ROOTS: 'src/'\n          - task: PublishPipelineArtifact@1\n            condition: always()\n            inputs:\n              targetPath: 'test-results'\n              artifact: 'regression-test-results'\n          - task: PublishPipelineArtifact@1\n            condition: always()\n            inputs:\n              targetPath: 'self-heal/healed-locators.json'\n              artifact: 'healed-locators'\n",
    "cucumber.js": "const common = {\n  requireModule: ['ts-node/register', 'tsconfig-paths/register'],\n  require: ['src/test/steps/**/*.ts'],\n  paths: ['src/test/features/**/*.feature'],\n  timeout: 60000,\n  format: [\n    'progress-bar',\n    'html:test-results/reports/cucumber-report.html',\n    'json:test-results/reports/cucumber-report.json',\n    '@cucumber/pretty-formatter',\n  ],\n  formatOptions: {\n    snippetInterface: 'async-await',\n    colorsEnabled: true,\n  },\n  publishQuiet: true,\n};\n\nmodule.exports = {\n  default: {\n    ...common,\n    parallel: 2,\n    retry: 1,\n  },\n  ci: {\n    ...common,\n    parallel: 4,\n    retry: 2,\n    retryTagFilter: '@flaky',\n  },\n  docker: {\n    ...common,\n    parallel: 4,\n    retry: 2,\n  },\n  debug: {\n    ...common,\n    parallel: 1,\n    retry: 0,\n  },\n};\n",
    "docker-compose.yml": "version: '3.8'\n\nservices:\n  qa-tests:\n    build:\n      context: .\n      dockerfile: Dockerfile\n    environment:\n      - BASE_URL=${BASE_URL:-https://your-app-url.com}\n      - AI_PROVIDER=${AI_PROVIDER:-openai}\n      - AI_API_KEY=${AI_API_KEY}\n      - ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}\n      - AZURE_OPENAI_ENDPOINT=${AZURE_OPENAI_ENDPOINT}\n      - AZURE_OPENAI_API_KEY=${AZURE_OPENAI_API_KEY}\n      - AZURE_OPENAI_DEPLOYMENT=${AZURE_OPENAI_DEPLOYMENT}\n      - AUTH_USERNAME=${AUTH_USERNAME}\n      - AUTH_PASSWORD=${AUTH_PASSWORD}\n      - ENABLE_SELF_HEALING=${ENABLE_SELF_HEALING:-true}\n      - CI=true\n      - HEADLESS=true\n      - BROWSER=chromium\n      - PARALLEL_WORKERS=4\n    volumes:\n      - ./test-results:/app/test-results\n      - ./storage-state:/app/storage-state\n      - ./logs:/app/logs\n",
    "package.json": "{\n  \"name\": \"{{PROJECT_NAME}}\",\n  \"version\": \"1.0.0\",\n  \"description\": \"QA automation framework — Playwright + TypeScript + Cucumber BDD with AI-powered self-healing locators\",\n  \"main\": \"index.js\",\n  \"scripts\": {\n    \"test\": \"cucumber-js\",\n    \"test:smoke\": \"cucumber-js --tags \\\"@smoke\\\"\",\n    \"test:regression\": \"cucumber-js --tags \\\"@regression\\\"\",\n    \"test:headed\": \"HEADLESS=false cucumber-js\",\n    \"test:headed:smoke\": \"HEADLESS=false cucumber-js --tags \\\"@smoke\\\"\",\n    \"test:docker\": \"cucumber-js --profile docker\",\n    \"test:ci\": \"cucumber-js --profile ci\",\n    \"test:debug\": \"PWDEBUG=1 cucumber-js\",\n    \"test:parallel\": \"cucumber-js --parallel 4 --tags \\\"@regression\\\"\",\n    \"test:chrome\": \"BROWSER=chromium cucumber-js\",\n    \"test:firefox\": \"BROWSER=firefox cucumber-js\",\n    \"test:webkit\": \"BROWSER=webkit cucumber-js\",\n    \"auth:setup\": \"ts-node src/utils/storage-state/AuthSetup.ts\",\n    \"report:open\": \"open test-results/reports/cucumber-report.html\",\n    \"docker:build\": \"docker build -f Dockerfile -t qa-framework .\",\n    \"docker:run\": \"docker-compose up --abort-on-container-exit\",\n    \"docker:down\": \"docker-compose down\",\n    \"lint\": \"eslint src --ext .ts\",\n    \"lint:fix\": \"eslint src --ext .ts --fix\",\n    \"format\": \"prettier --write \\\"src/**/*.ts\\\"\",\n    \"format:check\": \"prettier --check \\\"src/**/*.ts\\\"\",\n    \"typecheck\": \"tsc --noEmit\",\n    \"build\": \"tsc\",\n    \"healix:review\": \"ts-node -r tsconfig-paths/register src/utils/locators/review-server.ts\",\n    \"healix:apply-ci\": \"ts-node -r tsconfig-paths/register src/utils/locators/healix-ci-apply.ts\",\n    \"healix:telemetry-preview\": \"ts-node -r tsconfig-paths/register src/utils/locators/TelemetryClient.ts preview\",\n    \"clean\": \"rimraf dist test-results\",\n    \"prepare\": \"husky install\",\n    \"pre-commit\": \"lint-staged\"\n  },\n  \"keywords\": [\n    \"playwright\",\n    \"typescript\",\n    \"cucumber\",\n    \"bdd\",\n    \"self-healing\",\n    \"ai\",\n    \"test-automation\",\n    \"qa\"\n  ],\n  \"author\": \"\",\n  \"license\": \"MIT\",\n  \"dependencies\": {\n    \"@anthropic-ai/sdk\": \"^0.71.2\",\n    \"@azure/openai\": \"^2.0.0\",\n    \"@cucumber/cucumber\": \"^12.5.0\",\n    \"@playwright/test\": \"^1.57.0\",\n    \"axios\": \"^1.6.2\",\n    \"dotenv\": \"^16.3.1\",\n    \"openai\": \"^4.20.1\",\n    \"sharp\": \"^0.33.0\",\n    \"winston\": \"^3.11.0\",\n    \"zod\": \"^3.22.4\"\n  },\n  \"devDependencies\": {\n    \"@cucumber/html-formatter\": \"^21.0.0\",\n    \"@cucumber/pretty-formatter\": \"^1.0.0\",\n    \"@types/node\": \"^20.10.5\",\n    \"@typescript-eslint/eslint-plugin\": \"^6.15.0\",\n    \"@typescript-eslint/parser\": \"^6.15.0\",\n    \"eslint\": \"^8.56.0\",\n    \"eslint-config-prettier\": \"^9.1.0\",\n    \"eslint-plugin-playwright\": \"^0.20.0\",\n    \"husky\": \"^8.0.3\",\n    \"lint-staged\": \"^15.2.0\",\n    \"prettier\": \"^3.1.1\",\n    \"rimraf\": \"^5.0.5\",\n    \"ts-node\": \"^10.9.2\",\n    \"tsconfig-paths\": \"^4.2.0\",\n    \"typescript\": \"^5.9.3\"\n  },\n  \"lint-staged\": {\n    \"*.ts\": [\"eslint --fix\", \"prettier --write\"]\n  },\n  \"engines\": {\n    \"node\": \">=18.0.0\",\n    \"npm\": \">=9.0.0\"\n  }\n}\n",
    "setup.sh": "#!/usr/bin/env bash\n# setup.sh — one-shot project bootstrap\n# Usage: bash setup.sh\n\nset -euo pipefail\n\nCYAN='\\033[0;36m'\nGREEN='\\033[0;32m'\nYELLOW='\\033[1;33m'\nRED='\\033[0;31m'\nNC='\\033[0m'\n\ninfo()  { echo -e \"${CYAN}→${NC}  $*\"; }\nok()    { echo -e \"${GREEN}✓${NC}  $*\"; }\nwarn()  { echo -e \"${YELLOW}⚠${NC}  $*\"; }\ndie()   { echo -e \"${RED}✗${NC}  $*\"; exit 1; }\n\necho \"\"\necho \"  QA Framework — Project Setup\"\necho \"  ─────────────────────────────\"\necho \"\"\n\n# ── Node.js ──────────────────────────────────────────────────────────────────\ninfo \"Checking Node.js...\"\nnode_version=$(node -v 2>/dev/null || echo \"none\")\nif [[ \"$node_version\" == \"none\" ]]; then\n  die \"Node.js not found. Install v18+ from https://nodejs.org\"\nfi\nok \"Node.js $node_version\"\n\n# ── npm install ───────────────────────────────────────────────────────────────\ninfo \"Installing npm dependencies...\"\nnpm ci || npm install\nok \"Dependencies installed\"\n\n# ── Playwright browsers ───────────────────────────────────────────────────────\ninfo \"Installing Playwright browsers...\"\nnpx playwright install chromium --with-deps\nok \"Playwright browsers installed\"\n\n# ── .env ─────────────────────────────────────────────────────────────────────\nif [[ ! -f .env ]]; then\n  info \"Creating .env from template...\"\n  cp .env.example .env\n  ok \".env created — open it and set BASE_URL, AUTH_USERNAME, AUTH_PASSWORD, AI_API_KEY\"\nelse\n  warn \".env already exists — skipping\"\nfi\n\n# ── Output directories ────────────────────────────────────────────────────────\ninfo \"Creating output directories...\"\nmkdir -p test-results/reports test-results/screenshots test-results/videos \\\n         test-results/traces test-results/visual-baselines storage-state logs\nok \"Output directories ready\"\n\necho \"\"\necho \"${GREEN}Setup complete!${NC}\"\necho \"\"\necho \"  Next steps:\"\necho \"  1. Edit .env — set BASE_URL, AUTH_USERNAME, AUTH_PASSWORD, AI_API_KEY\"\necho \"  2. Run smoke tests:     npm run test:smoke\"\necho \"  3. Run all tests:       npm test\"\necho \"  4. Open HTML report:    npm run report:open\"\necho \"\"\n",
    "src/config/baseUrl.ts": "/**\n * Resolves the application base URL.\n *\n * Resolution order (highest priority first):\n *   1. process.env.BASE_URL              — explicit override\n *   2. process.env[`BASE_URL_<ENV>`]     — per-environment URL (e.g. BASE_URL_QA4)\n *   3. http://localhost:3000             — local fallback\n *\n * To add a hardcoded environment→URL map, replace the function body or wrap\n * the call in environment.ts.\n */\nexport function getBaseURL(env: string | undefined): string {\n  if (process.env.BASE_URL) return process.env.BASE_URL;\n  const key = (env ?? \"\").toUpperCase().trim();\n  if (key) {\n    const perEnv = process.env[`BASE_URL_${key}`];\n    if (perEnv) return perEnv;\n  }\n  return \"http://localhost:3000\";\n}\n",
    "src/config/environment.ts": "import { config as dotenvConfig } from \"dotenv\";\nimport { getBaseURL } from \"./baseUrl\";\n\ndotenvConfig();\n\nexport interface EnvironmentConfig {\n  baseUrl: string;\n  environment: string;\n  authUsername: string;\n  authPassword: string;\n  storageStatePath: string;\n  authTimeout: number;\n  aiProvider: \"openai\" | \"azure\" | \"anthropic\" | \"local\";\n  aiApiKey: string;\n  aiModel: string;\n  aiTemperature: number;\n  aiMaxTokens: number;\n  enableSelfHealing: boolean;\n  enableAITestGeneration: boolean;\n  azureOpenAIEndpoint?: string;\n  azureOpenAIApiKey?: string;\n  azureOpenAIDeployment?: string;\n  anthropicApiKey?: string;\n  localLLMEndpoint?: string;\n  headless: boolean;\n  browser: \"chromium\" | \"firefox\" | \"webkit\";\n  parallelWorkers: number;\n  retryAttempts: number;\n  timeout: number;\n  navigationTimeout: number;\n  actionTimeout: number;\n  slowMo: number;\n  screenshotOnFailure: boolean;\n  videoOnFailure: boolean;\n  traceOnFailure: boolean;\n  reportOutputDir: string;\n  fullPageScreenshots: boolean;\n  locatorHealAttempts: number;\n  locatorTimeout: number;\n  enableLocatorVersioning: boolean;\n  logLevel: \"error\" | \"warn\" | \"info\" | \"debug\";\n  logToFile: boolean;\n  logFilePath: string;\n  isCI: boolean;\n  shardIndex: number;\n  shardTotal: number;\n  performanceMonitoring: boolean;\n  collectMetrics: boolean;\n  maskSensitiveData: boolean;\n}\n\nclass Environment {\n  private static instance: Environment;\n  private config: EnvironmentConfig;\n\n  private constructor() {\n    this.config = this.loadConfig();\n    this.validateConfig();\n  }\n\n  public static getInstance(): Environment {\n    if (!Environment.instance) {\n      Environment.instance = new Environment();\n    }\n    return Environment.instance;\n  }\n\n  private loadConfig(): EnvironmentConfig {\n    return {\n      baseUrl: process.env.BASE_URL || getBaseURL(process.env.ENV ?? process.env.ENVIRONMENT),\n      environment: process.env.ENVIRONMENT || \"local\",\n      authUsername: process.env.AUTH_USERNAME || \"\",\n      authPassword: process.env.AUTH_PASSWORD || \"\",\n      storageStatePath: process.env.STORAGE_STATE_PATH || \"./storage-state/auth.json\",\n      authTimeout: parseInt(process.env.AUTH_TIMEOUT || \"30000\"),\n      aiProvider: (process.env.AI_PROVIDER as EnvironmentConfig[\"aiProvider\"]) || \"openai\",\n      aiApiKey: process.env.AI_API_KEY || \"\",\n      aiModel: process.env.AI_MODEL || \"gpt-4o\",\n      aiTemperature: parseFloat(process.env.AI_TEMPERATURE || \"0.3\"),\n      aiMaxTokens: parseInt(process.env.AI_MAX_TOKENS || \"2000\"),\n      enableSelfHealing: process.env.ENABLE_SELF_HEALING !== \"false\",\n      enableAITestGeneration: process.env.ENABLE_AI_TEST_GENERATION === \"true\",\n      azureOpenAIEndpoint: process.env.AZURE_OPENAI_ENDPOINT,\n      azureOpenAIApiKey: process.env.AZURE_OPENAI_API_KEY,\n      azureOpenAIDeployment: process.env.AZURE_OPENAI_DEPLOYMENT,\n      anthropicApiKey: process.env.ANTHROPIC_API_KEY,\n      localLLMEndpoint: process.env.LOCAL_LLM_ENDPOINT,\n      headless: process.env.HEADLESS !== \"false\",\n      browser: (process.env.BROWSER as EnvironmentConfig[\"browser\"]) || \"chromium\",\n      parallelWorkers: parseInt(process.env.PARALLEL_WORKERS || \"2\"),\n      retryAttempts: parseInt(process.env.RETRY_ATTEMPTS || \"1\"),\n      timeout: parseInt(process.env.TIMEOUT || \"30000\"),\n      navigationTimeout: parseInt(process.env.NAVIGATION_TIMEOUT || \"30000\"),\n      actionTimeout: parseInt(process.env.ACTION_TIMEOUT || \"10000\"),\n      slowMo: parseInt(process.env.SLOW_MO || \"0\"),\n      screenshotOnFailure: process.env.SCREENSHOT_ON_FAILURE === \"true\",\n      videoOnFailure: process.env.VIDEO_ON_FAILURE === \"true\",\n      traceOnFailure: process.env.TRACE_ON_FAILURE === \"true\",\n      reportOutputDir: process.env.REPORT_OUTPUT_DIR || \"./test-results/reports\",\n      fullPageScreenshots: process.env.FULL_PAGE_SCREENSHOTS === \"true\",\n      locatorHealAttempts: parseInt(process.env.LOCATOR_HEAL_ATTEMPTS || \"3\"),\n      locatorTimeout: parseInt(process.env.LOCATOR_TIMEOUT || \"10000\"),\n      enableLocatorVersioning: process.env.ENABLE_LOCATOR_VERSIONING === \"true\",\n      logLevel: (process.env.LOG_LEVEL as EnvironmentConfig[\"logLevel\"]) || \"info\",\n      logToFile: process.env.LOG_TO_FILE === \"true\",\n      logFilePath: process.env.LOG_FILE_PATH || \"./logs/test-execution.log\",\n      isCI: process.env.CI === \"true\",\n      shardIndex: parseInt(process.env.SHARD_INDEX || \"1\"),\n      shardTotal: parseInt(process.env.SHARD_TOTAL || \"1\"),\n      performanceMonitoring: process.env.PERFORMANCE_MONITORING === \"true\",\n      collectMetrics: process.env.COLLECT_METRICS === \"true\",\n      maskSensitiveData: process.env.MASK_SENSITIVE_DATA === \"true\",\n    };\n  }\n\n  private validateConfig(): void {\n    const errors: string[] = [];\n    if (!this.config.baseUrl) {\n      errors.push(\"BASE_URL is required\");\n    }\n    if (this.config.enableSelfHealing && !this.config.aiApiKey && !this.config.anthropicApiKey) {\n      console.warn(\"[Environment] Warning: ENABLE_SELF_HEALING=true but no AI API key provided. Self-healing will fall back to rule-based strategies.\");\n    }\n    if (errors.length > 0) {\n      throw new Error(`[Environment] Configuration errors:\\n${errors.join(\"\\n\")}`);\n    }\n  }\n\n  public getConfig(): EnvironmentConfig {\n    return this.config;\n  }\n}\n\nexport const environment = Environment.getInstance();\n",
    "src/config/global-setup.ts": "import { FullConfig } from \"@playwright/test\";\nimport { logger } from \"@utils/helpers/Logger\";\nimport { environment } from \"./environment\";\nimport * as fs from \"fs\";\nimport * as path from \"path\";\n\nasync function globalSetup(_config: FullConfig) {\n  logger.info(\"Starting global setup...\");\n\n  const directories = [\n    \"test-results\",\n    \"test-results/reports\",\n    \"test-results/screenshots\",\n    \"test-results/videos\",\n    \"test-results/traces\",\n    \"test-results/visual-baselines\",\n    \"storage-state\",\n    \"logs\",\n  ];\n\n  for (const dir of directories) {\n    const dirPath = path.join(process.cwd(), dir);\n    if (!fs.existsSync(dirPath)) {\n      fs.mkdirSync(dirPath, { recursive: true });\n      logger.info(`Created directory: ${dir}`);\n    }\n  }\n\n  const env = environment.getConfig();\n  logger.info(\"Environment configuration:\", {\n    baseUrl: env.baseUrl,\n    environment: env.environment,\n    browser: env.browser,\n    headless: env.headless,\n    aiProvider: env.aiProvider,\n    selfHealingEnabled: env.enableSelfHealing,\n  });\n\n  logger.info(\"Global setup completed\");\n}\n\nexport default globalSetup;\n",
    "src/config/global-teardown.ts": "import { FullConfig } from \"@playwright/test\";\nimport { logger } from \"@utils/helpers/Logger\";\n\nasync function globalTeardown(_config: FullConfig) {\n  logger.info(\"Starting global teardown...\");\n  logger.info(\"Global teardown completed\");\n}\n\nexport default globalTeardown;\n",
    "src/hooks/hooks.ts": "import { Before, After, BeforeAll, AfterAll, setDefaultTimeout, Status } from '@cucumber/cucumber';\nimport { chromium, Browser, BrowserContext, Page } from '@playwright/test';\nimport { PageFixture, setFixture } from './pageFixture';\nimport { HealingDashboard } from '@utils/locators/HealingDashboard';\nimport * as path from 'path';\nimport * as fs from 'fs';\n\nlet browser: Browser;\n\nsetDefaultTimeout(60 * 1_000);\n\nBeforeAll(async function () {\n  browser = await chromium.launch({\n    headless: process.env.HEADLESS !== 'false',\n    slowMo: process.env.SLOW_MO ? parseInt(process.env.SLOW_MO) : 0,\n  });\n\n  for (const dir of [\n    path.resolve('test-results', 'screenshots'),\n    path.resolve('test-results', 'traces'),\n    path.resolve('test-results', 'videos'),\n    path.resolve('test-results', 'reports'),\n    path.resolve('storage-state'),\n  ]) {\n    fs.mkdirSync(dir, { recursive: true });\n  }\n\n  await HealingDashboard.getInstance().start();\n});\n\nAfterAll(async function () {\n  await browser?.close();\n  await HealingDashboard.getInstance().stop();\n\n  const store = path.resolve('storage-state', 'healed-locators.json');\n  if (fs.existsSync(store)) {\n    const heals = Object.keys(JSON.parse(fs.readFileSync(store, 'utf8') || '{}'));\n    if (heals.length > 0) {\n      console.log('');\n      console.log('  ╔══════════════════════════════════════════════════════════════╗');\n      console.log('  ║  🩺  SELF-HEALING SUMMARY                                    ║');\n      console.log(`  ║  ${heals.length} locator(s) were healed during this run`.padEnd(66) + '║');\n      console.log('  ║                                                              ║');\n      console.log('  ║  Review & apply heals:  npm run healix:review               ║');\n      console.log('  ║  Heal store: self-heal/healed-locators.json             ║');\n      console.log('  ╚══════════════════════════════════════════════════════════════╝');\n      console.log('');\n    }\n  }\n});\n\nBefore(async function (this: PageFixture, scenario) {\n  const context: BrowserContext = await browser.newContext({\n    viewport: { width: 1440, height: 900 },\n    recordVideo: process.env.RECORD_VIDEO === 'true'\n      ? { dir: path.resolve('test-results', 'videos') }\n      : undefined,\n    ignoreHTTPSErrors: true,\n  });\n\n  await context.tracing.start({ screenshots: true, snapshots: true, sources: true });\n\n  const page: Page = await context.newPage();\n\n  const logger = {\n    info:  (msg: string) => { console.log(`  ℹ  ${msg}`);  },\n    warn:  (msg: string) => { console.warn(` ⚠  ${msg}`);  },\n    error: (msg: string) => { console.error(` ✖  ${msg}`); },\n  };\n\n  this.browser = browser;\n  this.context = context;\n  this.page    = page;\n  this.logger  = logger;\n\n  this.initializeLocatorServices(scenario.pickle.name);\n  this.initializePages();\n  setFixture(this);\n\n  logger.info(`[hooks] Before: \"${scenario.pickle.name}\"`);\n});\n\nAfter(async function (this: PageFixture, scenario) {\n  const failed = scenario.result?.status === Status.FAILED;\n  const scenarioName = scenario.pickle.name.replace(/[^a-z0-9]/gi, '_').toLowerCase();\n  const timestamp = Date.now();\n\n  if (failed) {\n    try {\n      const screenshotPath = path.resolve(\n        'test-results', 'screenshots', `${scenarioName}-${timestamp}.png`,\n      );\n      const screenshot = await this.page.screenshot({ fullPage: true, path: screenshotPath });\n      await this.attach(screenshot, 'image/png');\n    } catch (err) {\n      this.logger.warn(`[hooks] Could not capture screenshot: ${String(err)}`);\n    }\n\n    try {\n      const tracePath = path.resolve(\n        'test-results', 'traces', `${scenarioName}-${timestamp}.zip`,\n      );\n      await this.context.tracing.stop({ path: tracePath });\n    } catch (err) {\n      this.logger.warn(`[hooks] Could not save trace: ${String(err)}`);\n    }\n\n    try {\n      const html = await this.page.content();\n      await this.attach(`<!-- ${scenario.pickle.name} -->\\n${html}`, 'text/html');\n    } catch { /* non-fatal */ }\n  } else {\n    try {\n      await this.context.tracing.stop();\n    } catch { /* non-fatal */ }\n  }\n\n  if (this.locatorRepository) {\n    const summary = this.locatorRepository.summary();\n    this.logger.info(\n      `[hooks] LocatorRepository: ${summary.total} total, ${summary.healed} healed (${summary.healRate})`,\n    );\n  }\n\n  await this.context?.close();\n\n  this.logger.info(`[hooks] After: \"${scenario.pickle.name}\" — ${scenario.result?.status}`);\n});\n",
    "src/hooks/pageFixture.ts": "import { Page, Browser, BrowserContext } from '@playwright/test';\nimport { IWorldOptions, World, setWorldConstructor } from '@cucumber/cucumber';\nimport { LocatorRepository } from '@utils/locators/LocatorRepository';\nimport { LocatorHealer } from '@utils/locators/LocatorHealer';\nimport { TimingHealer } from '@utils/locators/TimingHealer';\nimport { VisualIntentChecker } from '@utils/locators/VisualIntentChecker';\n\nexport interface HealixLogger {\n  info(msg: string): void;\n  warn(msg: string): void;\n  error(msg: string): void;\n}\n\n/**\n * Page-object factory registry — the single source of truth for which\n * page objects this fixture exposes and how they're constructed. Each\n * factory receives the fixture so pages that need more than `this.page`\n * (e.g. an API page that wants `baseURL`) can be wired here too.\n *\n * The `Pages` type below is DERIVED from this map via TypeScript mapped\n * types — adding a new page object is a one-line edit here, no separate\n * field declaration or constructor wiring needed.\n *\n * Empty by default; the generator and migration agents append entries\n * when they add page-object files. Example after adding LoginPage:\n *\n *   const PAGE_FACTORIES = {\n *     loginPage: (f: PageFixture) => new P.LoginPage(f.page),\n *   };\n *\n * The barrel import `import * as P from \"@pages/<app>\"` is added at\n * the top of this file when the first page is registered.\n */\nconst PAGE_FACTORIES = {} as const;\n\n/** Public `pages.X` type derived from the factory registry. */\nexport type Pages = {\n  [K in keyof typeof PAGE_FACTORIES]: ReturnType<typeof PAGE_FACTORIES[K]>;\n};\n\nexport interface FixtureContext {\n  page: Page;\n  browser: Browser;\n  context: BrowserContext;\n  logger: HealixLogger;\n  pages: Pages;\n  locatorRepository: LocatorRepository;\n  locatorHealer: LocatorHealer;\n  locatorTiming: TimingHealer;\n  locatorVisual: VisualIntentChecker;\n}\n\nclass PageFixture extends World {\n  page!: Page;\n  browser!: Browser;\n  context!: BrowserContext;\n  pages!: Pages;\n  locatorRepository!: LocatorRepository;\n  locatorHealer!: LocatorHealer;\n  locatorTiming!: TimingHealer;\n  locatorVisual!: VisualIntentChecker;\n  logger!: HealixLogger;\n\n  constructor(options: IWorldOptions) {\n    super(options);\n  }\n\n  initializeLocatorServices(scenarioName?: string): void {\n    if (!this.page) throw new Error('Page must be set before initialising locator services');\n    if (!this.logger) throw new Error('Logger must be set before initialising locator services');\n\n    const locatorRepository = new LocatorRepository();\n    const locatorHealer = new LocatorHealer(this.page, this.logger, locatorRepository);\n    locatorHealer.currentScenario = scenarioName;\n\n    this.locatorRepository = locatorRepository;\n    this.locatorHealer = locatorHealer;\n    this.locatorTiming = new TimingHealer(this.page, this.logger);\n    this.locatorVisual = new VisualIntentChecker(this.page, this.logger);\n  }\n\n  /** Initialise all page objects from PAGE_FACTORIES (one factory call each). */\n  initializePages(): void {\n    if (!this.page) throw new Error('Page must be set before initialising page objects');\n    this.pages = Object.fromEntries(\n      Object.entries(PAGE_FACTORIES).map(([k, factory]) => [k, (factory as (f: PageFixture) => unknown)(this)]),\n    ) as Pages;\n  }\n}\n\nlet _current: PageFixture | null = null;\n\nexport function setFixture(world: PageFixture): void {\n  _current = world;\n}\n\nexport function fixture(): PageFixture {\n  if (!_current) {\n    throw new Error(\n      '[PageFixture] fixture() called before Before hook initialised it. ' +\n      'Ensure hooks.ts is included in your cucumber require paths.',\n    );\n  }\n  return _current;\n}\n\nsetWorldConstructor(PageFixture);\nexport { PageFixture };\n",
    "src/locators/base.locators.ts": "/**\n * Base locators — shared across all pages.\n * Import specific locators from the locator file for each page.\n */\nexport const BASE_LOCATORS = {\n  HAMBURGER_MENU: '[data-test=\"menu-button\"]',\n  LOGOUT_LINK: '[data-test=\"logout-sidebar-link\"]',\n  ERROR_MESSAGE: '[data-test=\"error-message\"]',\n  SUCCESS_MESSAGE: '[data-test=\"success-message\"]',\n  LOADING_SPINNER: '[data-test=\"loading-spinner\"]',\n  SUBMIT_BUTTON: 'button[type=\"submit\"]',\n  CANCEL_BUTTON: 'button:has-text(\"Cancel\")',\n  INPUT_FIELD: 'input[type=\"text\"]',\n  PASSWORD_INPUT: 'input[type=\"password\"]',\n  EMAIL_INPUT: 'input[type=\"email\"]',\n} as const;\n\nexport type BaseLocatorKey = keyof typeof BASE_LOCATORS;\n",
    "src/pages/BasePage.ts": "import { Page } from \"@playwright/test\";\nimport { WaitHelper } from \"@utils/helpers/WaitHelper\";\nimport { LocatorStrategy } from \"@utils/locators/LocatorStrategy\";\nimport { environment } from \"../config/environment\";\nimport { fixture } from \"@hooks/pageFixture\";\n\n/**\n * BasePage — navigation + page-scoped helpers.\n *\n * Shared healing/timing/visual services live on the cucumber World\n * (`PageFixture`) — access them through `fixture().locatorHealer`,\n * `fixture().locatorTiming`, `fixture().locatorVisual`. Don't construct\n * per-page healer instances: that fragments the heal-store cache.\n */\nexport abstract class BasePage {\n  protected waitHelper: WaitHelper;\n  protected locatorStrategy: LocatorStrategy;\n  protected env = environment.getConfig();\n\n  constructor(protected page: Page) {\n    this.waitHelper = new WaitHelper(page);\n    this.locatorStrategy = new LocatorStrategy(page);\n  }\n\n  async goto(url: string): Promise<void> {\n    await this.page.goto(url, { timeout: this.env.navigationTimeout, waitUntil: \"domcontentloaded\" });\n    await fixture().locatorTiming.waitForNetworkIdle(\"po_nav1\");\n    await this.waitForPageLoad();\n  }\n  async waitForPageLoad(): Promise<void> { await this.waitHelper.waitForPageLoad(); }\n  async getTitle(): Promise<string> { return await this.page.title(); }\n  getUrl(): string { return this.page.url(); }\n  async reload(): Promise<void> {\n    await this.page.reload({ timeout: this.env.navigationTimeout, waitUntil: \"domcontentloaded\" });\n    await this.waitForPageLoad();\n  }\n  async goBack(): Promise<void> {\n    await this.page.goBack({ timeout: this.env.navigationTimeout, waitUntil: \"domcontentloaded\" });\n  }\n  async goForward(): Promise<void> {\n    await this.page.goForward({ timeout: this.env.navigationTimeout, waitUntil: \"domcontentloaded\" });\n  }\n  async takeScreenshot(name: string): Promise<Buffer> {\n    return await this.page.screenshot({\n      path: `test-results/screenshots/${name}-${Date.now()}.png`,\n      fullPage: this.env.fullPageScreenshots,\n    });\n  }\n  async scrollToBottom(): Promise<void> {\n    await this.page.evaluate(() => window.scrollTo(0, document.body.scrollHeight));\n  }\n  async scrollToTop(): Promise<void> {\n    await this.page.evaluate(() => window.scrollTo(0, 0));\n  }\n  async waitForURL(urlPattern: string | RegExp, timeout?: number): Promise<void> {\n    await this.page.waitForURL(urlPattern, { timeout: timeout ?? this.env.navigationTimeout });\n  }\n}\n",
    "src/templates/_TemplatePage.ts": "/**\n * Copy, rename, and keep page logic behind the shared fixture services.\n * Use fixture().locatorHealer for interactions, fixture().locatorTiming after navigation or submit, and fixture().locatorVisual only for visual assertions.\n */\n\nimport { Page, expect } from '@playwright/test';\nimport { fixture } from '@hooks/pageFixture';\nimport { TemplateLocators } from '../templates/_template.locators';\nimport { BasePage } from './BasePage';\n\nexport default class TemplatePage extends BasePage {\n  private readonly loc = TemplateLocators;\n\n  constructor(page?: Page) {\n    super(page ?? fixture().page);\n    Object.entries(this.loc).forEach(([key, val]) =>\n      fixture().locatorRepository.register(key, val.selector, val.intent));\n  }\n\n  async navigate(url: string): Promise<void> {\n    fixture().logger.info(`Navigating to ${url}`);\n    await this.page.goto(url, { waitUntil: 'networkidle' });\n    await fixture().locatorTiming.waitForNetworkIdle('navigate');\n  }\n  async clickPrimaryButton(): Promise<void> {\n    await fixture().locatorHealer.clickWithHealing(\n      'primaryButton', this.loc.primaryButton.selector, this.loc.primaryButton.intent);\n    await fixture().locatorTiming.waitForNetworkIdle('clickPrimaryButton');\n  }\n  async submitForm(): Promise<void> {\n    await fixture().locatorHealer.clickWithHealing(\n      'submitButton', this.loc.submitButton.selector, this.loc.submitButton.intent);\n    await fixture().locatorTiming.waitForNetworkIdle('submitForm');\n  }\n  async fillPrimaryInput(value: string): Promise<void> {\n    await fixture().locatorHealer.fillWithHealing(\n      'primaryInput', this.loc.primaryInput.selector, value, this.loc.primaryInput.intent);\n  }\n  async verifySuccessMessage(): Promise<void> {\n    await fixture().locatorHealer.assertVisibleWithHealing(\n      'successMessage', this.loc.successMessage.selector, this.loc.successMessage.intent);\n    await fixture().locatorVisual.check(\n      'successMessage', this.loc.successMessage.selector, this.loc.successMessage.intent);\n  }\n  async verifyErrorMessageContains(text: string): Promise<void> {\n    const actual = await fixture().locatorHealer.getTextWithHealing(\n      'errorMessage', this.loc.errorMessage.selector, this.loc.errorMessage.intent);\n    expect(actual).toContain(text);\n  }\n}\n",
    "src/templates/_template.feature": "# Copy to src/test/features/my-feature.feature and replace the sample scenarios.\n# Keep one feature per file and quote values passed to {string} steps.\n\nFeature: Template Feature\n  As a user\n  I want to interact with the application\n  So that I can complete my goals\n\n  Background:\n    Given I navigate to \"https://your-app-url.com\"\n\n  @smoke @regression\n  Scenario: Happy path — primary action succeeds\n    When I fill in the primary input with \"valid input\"\n    And I click the primary button\n    Then I should see a success message\n\n  @regression\n  Scenario: Validation error on empty input\n    When I submit the form\n    Then I should see an error message\n\n  @regression\n  Scenario: Validation error shows correct message\n    When I submit the form\n    Then I should see error message \"This field is required\"\n\n  @regression\n  Scenario: Cancel navigates back\n    When I click cancel\n    Then the current URL should contain \"/home\"\n",
    "src/templates/_template.locators.ts": "/**\n * Copy, rename, and prefer stable selectors in this order: data-testid, role and name, id, then aria-label or placeholder.\n * Each locator should define selector, intent, stability, and optional visualIntent.\n */\n\nexport const TemplateLocators = {\n  // ── Page container ──────────────────────────────────────────────────────\n  pageContainer: {\n    selector:  '[data-test=\"page-container\"]',\n    intent:    'Main page container',\n    stability: 100,\n  },\n\n  // ── Primary actions ─────────────────────────────────────────────────────\n  primaryButton: {\n    selector:      '[data-test=\"primary-button\"]',\n    intent:        'Primary action button',\n    stability:     100,\n    visualIntent:  true,\n  },\n\n  submitButton: {\n    selector:      '[data-test=\"submit-button\"]',\n    intent:        'Submit form button',\n    stability:     100,\n    visualIntent:  true,\n  },\n\n  cancelButton: {\n    selector:  '[data-test=\"cancel-button\"]',\n    intent:    'Cancel or close button',\n    stability: 100,\n  },\n\n  // ── Form inputs ─────────────────────────────────────────────────────────\n  primaryInput: {\n    selector:  '[data-test=\"primary-input\"]',\n    intent:    'Primary text input field',\n    stability: 100,\n  },\n\n  // ── Feedback ──────────────────────────────────────────────────────────────\n  successMessage: {\n    selector:      '[data-test=\"success-message\"]',\n    intent:        'Success confirmation message',\n    stability:     100,\n    visualIntent:  true,\n  },\n\n  errorMessage: {\n    selector:      '[data-test=\"error-message\"]',\n    intent:        'Error or validation message',\n    stability:     100,\n    visualIntent:  true,\n  },\n\n  loadingSpinner: {\n    selector:  '[data-test=\"loading-spinner\"]',\n    intent:    'Loading spinner indicator',\n    stability: 100,\n  },\n} as const;\n\nexport type TemplateLocatorKey = keyof typeof TemplateLocators;\n",
    "src/templates/_template.steps.ts": "/**\n * Copy, rename, and keep one step file per feature.\n * Create page objects inside steps and use fixture().logger for scenario-scoped logs.\n */\n\nimport { Given, When, Then } from '@cucumber/cucumber';\nimport { expect } from '@playwright/test';\nimport { fixture } from '@hooks/pageFixture';\nimport TemplatePage from '@pages/_TemplatePage';\n\nlet templatePage: TemplatePage;\n\n// ── Navigation ───────────────────────────────────────────────────────────────\n\nGiven('I navigate to {string}', async function (url: string): Promise<void> {\n  templatePage = new TemplatePage();\n  await templatePage.navigate(url);\n  fixture().logger.info(`Navigated to ${url}`);\n});\n\n// ── Actions ──────────────────────────────────────────────────────────────────\n\nWhen('I fill in the primary input with {string}', async function (value: string): Promise<void> {\n  await templatePage.fillPrimaryInput(value);\n  fixture().logger.info(`Filled in: \"${value}\"`);\n});\n\nWhen('I click the primary button', async function (): Promise<void> {\n  await templatePage.clickPrimaryButton();\n  fixture().logger.info('Clicked primary button');\n});\n\nWhen('I submit the form', async function (): Promise<void> {\n  await templatePage.submitForm();\n  fixture().logger.info('Submitted form');\n});\n\nWhen('I click cancel', async function (): Promise<void> {\n  await templatePage.clickCancel();\n  fixture().logger.info('Clicked cancel');\n});\n\n// ── Assertions ───────────────────────────────────────────────────────────────\n\nThen('I should see the page loaded', async function (): Promise<void> {\n  await templatePage.verifyPageLoaded();\n});\n\nThen('I should see a success message', async function (): Promise<void> {\n  await templatePage.verifySuccessMessage();\n});\n\nThen('I should see an error message', async function (): Promise<void> {\n  await templatePage.verifyErrorMessage();\n});\n\nThen('I should see error message {string}', async function (expectedText: string): Promise<void> {\n  await templatePage.verifyErrorMessageContains(expectedText);\n});\n\nThen('the current URL should contain {string}', async function (urlPattern: string): Promise<void> {\n  const currentUrl = fixture().page.url();\n  expect(currentUrl).toContain(urlPattern);\n  fixture().logger.info(`URL \"${currentUrl}\" contains \"${urlPattern}\"`);\n});\n",
    "src/test/features/example.feature": "@example\nFeature: Example — Hello World\n  As a QA engineer\n  I want to verify the framework is working\n  So that I can start writing tests\n\n  @smoke\n  Scenario: Framework smoke test\n    Given I navigate to \"https://example.com\"\n    Then the page title should contain \"Example\"\n    And the current URL should contain \"example.com\"\n",
    "src/test/steps/common.steps.ts": "import { Given, Then } from '@cucumber/cucumber';\nimport { expect } from '@playwright/test';\nimport { fixture } from '@hooks/pageFixture';\n\nGiven('I navigate to {string}', async function (url: string): Promise<void> {\n  await fixture().page.goto(url, { waitUntil: 'domcontentloaded' });\n  fixture().logger.info(`Navigated to ${url}`);\n});\n\nThen('the page title should contain {string}', async function (expected: string): Promise<void> {\n  const title = await fixture().page.title();\n  expect(title).toContain(expected);\n  fixture().logger.info(`Page title \"${title}\" contains \"${expected}\"`);\n});\n\nThen('the current URL should contain {string}', async function (urlPattern: string): Promise<void> {\n  const currentUrl = fixture().page.url();\n  expect(currentUrl).toContain(urlPattern);\n  fixture().logger.info(`URL \"${currentUrl}\" contains \"${urlPattern}\"`);\n});\n\nThen('I should see an error message', async function (): Promise<void> {\n  const errorLoc = fixture().page.locator('[data-test=\"error-message\"], [data-testid=\"error\"], .error-message');\n  await errorLoc.waitFor({ state: 'visible', timeout: 5000 });\n  fixture().logger.info('Error message is visible');\n});\n\nThen('I should see error message {string}', async function (expectedMessage: string): Promise<void> {\n  const errorLoc = fixture().page.locator('[data-test=\"error-message\"], [data-testid=\"error\"], .error-message');\n  const actual = await errorLoc.textContent();\n  expect(actual).toContain(expectedMessage);\n  fixture().logger.info(`Error message verified: \"${actual}\"`);\n});\n",
    "src/test/steps/hooks.ts": "import '@hooks/hooks';\n",
    "src/test/steps/world.ts": "export { PageFixture } from '../../hooks/pageFixture';\n",
    "src/utils/ai-assistant/AILocatorGenerator.ts": "import { Page } from '@playwright/test';\nimport { logger } from '@utils/helpers/Logger';\nimport { environment } from '@config/environment';\n\nexport interface LocatorGenerationOptions {\n  elementDescription: string;\n  screenshot?:        Buffer;\n  context?:           string;\n}\n\nexport interface GeneratedLocator {\n  selector:     string;\n  strategy:     'testid' | 'role' | 'label' | 'text' | 'css';\n  confidence:   number;\n  alternatives: string[];\n}\n\n/**\n * AILocatorGenerator\n *\n * Generates candidate Playwright locators from a natural-language element description.\n * Falls back to heuristic pattern matching when self-healing is disabled or API key absent.\n *\n * In production, extend `callAIService()` with your preferred provider.\n * The `LocatorHealer` class (Layer 1 healing) provides a fully-featured multi-provider\n * AI Vision implementation using screenshot-based analysis.\n */\nexport class AILocatorGenerator {\n  private readonly env = environment.getConfig();\n\n  constructor(private readonly page: Page) {}\n\n  async generateLocator(options: LocatorGenerationOptions): Promise<GeneratedLocator> {\n    logger.info(`Generating locator for: \"${options.elementDescription}\"`);\n\n    if (!this.env.enableSelfHealing || !this.env.aiApiKey) {\n      logger.warn('AI self-healing not enabled — using heuristic fallback');\n      return this.generateFallbackLocator(options);\n    }\n\n    try {\n      return await this.callAIService(options);\n    } catch (err) {\n      logger.error(`AI locator generation failed: ${String(err)}`);\n      return this.generateFallbackLocator(options);\n    }\n  }\n\n  /**\n   * Override this method to integrate with your chosen AI provider.\n   * See LocatorHealer.healByAiVision() for a full multi-provider implementation.\n   */\n  private async callAIService(options: LocatorGenerationOptions): Promise<GeneratedLocator> {\n    logger.debug('AI service integration placeholder — falling back to heuristic');\n    return this.generateFallbackLocator(options);\n  }\n\n  private generateFallbackLocator(options: LocatorGenerationOptions): GeneratedLocator {\n    const desc = options.elementDescription.toLowerCase();\n    const alternatives: string[] = [];\n\n    if (desc.includes('button')) {\n      const name = options.elementDescription;\n      alternatives.push(`[role=\"button\"]:has-text(\"${name}\")`);\n      alternatives.push(`[data-testid*=\"button\"]`);\n      return { selector: `button:has-text(\"${name}\")`, strategy: 'role', confidence: 0.7, alternatives };\n    }\n    if (desc.includes('input') || desc.includes('field')) {\n      const fieldName = desc.replace(/input|field/gi, '').trim();\n      alternatives.push(`[data-testid=\"${fieldName}\"]`);\n      alternatives.push(`[placeholder*=\"${fieldName}\"]`);\n      return { selector: `input[name=\"${fieldName}\"]`, strategy: 'label', confidence: 0.7, alternatives };\n    }\n    if (desc.includes('link')) {\n      const linkText = desc.replace(/link/gi, '').trim();\n      alternatives.push(`[role=\"link\"]:has-text(\"${linkText}\")`);\n      return { selector: `a:has-text(\"${linkText}\")`, strategy: 'role', confidence: 0.6, alternatives };\n    }\n\n    return { selector: `:has-text(\"${options.elementDescription}\")`, strategy: 'text', confidence: 0.5, alternatives: [] };\n  }\n\n  async validateLocator(selector: string): Promise<boolean> {\n    try {\n      await this.page.locator(selector).first().waitFor({ state: 'attached', timeout: 2_000 });\n      return true;\n    } catch { return false; }\n  }\n\n  async getBestLocator(alternatives: string[]): Promise<string | null> {\n    for (const selector of alternatives) {\n      if (await this.validateLocator(selector)) {\n        logger.info(`Found working alternative: ${selector}`);\n        return selector;\n      }\n    }\n    return null;\n  }\n}\n",
    "src/utils/ai-assistant/AISelfHealing.ts": "import { Page } from '@playwright/test';\nimport { logger } from '@utils/helpers/Logger';\nimport { AILocatorGenerator } from './AILocatorGenerator';\nimport { environment } from '@config/environment';\nimport { getAILocatorRules, getContextRules } from '@utils/locators/LocatorRules';\n\nexport interface SelfHealingOptions {\n  maxAttempts?:  number;\n  useAI?:        boolean;\n  saveHealing?:  boolean;\n  elementType?:  string;\n  context?:      string;\n}\n\nexport interface HealingReport {\n  success:          boolean;\n  originalSelector: string;\n  healedSelector?:  string;\n  method:           'ai' | 'heuristic' | 'rule-based' | 'fallback';\n  attempts:         number;\n  timestamp:        Date;\n  rulesUsed?:       string[];\n}\n\n/**\n * AISelfHealing\n *\n * Orchestrates multiple healing strategies:\n *   1. Rule-based  (LocatorRules engine + getContextRules)\n *   2. Heuristic   (selector variation generation)\n *   3. AI Vision   (AILocatorGenerator — delegates to LocatorHealer providers)\n *   4. Text-based  (fallback :text() selectors)\n *\n * This is used by LocatorManager as the deep-healing fallback.\n * For per-action healing, see LocatorHealer (Layer 1).\n */\nexport class AISelfHealing {\n  private readonly locatorGenerator: AILocatorGenerator;\n  private readonly env = environment.getConfig();\n  private readonly healingHistory: HealingReport[] = [];\n\n  constructor(private readonly page: Page) {\n    this.locatorGenerator = new AILocatorGenerator(page);\n  }\n\n  async heal(brokenSelector: string, elementDescription: string, options: SelfHealingOptions = {}): Promise<HealingReport> {\n    const { maxAttempts = 3, useAI = this.env.enableSelfHealing, saveHealing = true, elementType = 'button', context } = options;\n\n    logger.info(`Self-healing: \"${brokenSelector}\" for \"${elementDescription}\"`);\n\n    const report: HealingReport = { success: false, originalSelector: brokenSelector, method: 'heuristic', attempts: 0, timestamp: new Date(), rulesUsed: [] };\n\n    // Strategy 1: Rule-based\n    try {\n      report.attempts++;\n      const r = await this.tryRuleBasedHealing(elementDescription, elementType, context);\n      if (r.success && r.selector) {\n        return this.finalize(report, r.selector, 'rule-based', r.rulesUsed, saveHealing);\n      }\n    } catch (err) { logger.warn(`Rule-based healing error: ${String(err)}`); }\n\n    // Strategy 2: Heuristic\n    try {\n      report.attempts++;\n      const s = await this.tryHeuristicHealing(brokenSelector, elementDescription);\n      if (s) return this.finalize(report, s, 'heuristic', [], saveHealing);\n    } catch (err) { logger.warn(`Heuristic healing error: ${String(err)}`); }\n\n    // Strategy 3: AI Vision\n    if (useAI && report.attempts < maxAttempts) {\n      try {\n        report.attempts++;\n        const s = await this.tryAIHealing(elementDescription);\n        if (s) return this.finalize(report, s, 'ai', [], saveHealing);\n      } catch (err) { logger.warn(`AI healing error: ${String(err)}`); }\n    }\n\n    // Strategy 4: Text fallback\n    if (report.attempts < maxAttempts) {\n      try {\n        report.attempts++;\n        const s = await this.tryFallbackHealing(elementDescription);\n        if (s) return this.finalize(report, s, 'fallback', [], saveHealing);\n      } catch (err) { logger.warn(`Fallback healing error: ${String(err)}`); }\n    }\n\n    logger.error(`All healing attempts failed for \"${brokenSelector}\"`);\n    return report;\n  }\n\n  private finalize(report: HealingReport, selector: string, method: HealingReport['method'], rulesUsed: string[], save: boolean): HealingReport {\n    report.success        = true;\n    report.healedSelector = selector;\n    report.method         = method;\n    report.rulesUsed      = rulesUsed;\n    logger.info(`Healed via ${method}: ${selector}`);\n    if (save) this.healingHistory.push(report);\n    return report;\n  }\n\n  private async tryRuleBasedHealing(description: string, elementType: string, context?: string): Promise<{ success: boolean; selector?: string; rulesUsed: string[] }> {\n    const rules = getAILocatorRules()\n      .filter(r => r.elementTypes.includes(elementType) || r.elementTypes.includes('*'))\n      .sort((a, b) => a.priority - b.priority);\n\n    const contextPrefs = context ? (getContextRules() as Record<string, { preferredStrategies: string[] }>)[context] : null;\n    let rulesToTry = rules;\n    if (contextPrefs?.preferredStrategies) {\n      rulesToTry = rules.sort((a, b) => {\n        const ai = contextPrefs.preferredStrategies.indexOf(a.pattern);\n        const bi = contextPrefs.preferredStrategies.indexOf(b.pattern);\n        return (ai === -1 ? 99 : ai) - (bi === -1 ? 99 : bi);\n      });\n    }\n\n    const rulesUsed: string[] = [];\n    for (const rule of rulesToTry) {\n      let selector = '';\n      switch (rule.pattern) {\n        case 'testid':      selector = `[data-testid=\"${description.toLowerCase().replace(/\\s+/g, '-')}\"]`; break;\n        case 'label':       selector = `label:text-is(\"${description}\")`; break;\n        case 'placeholder': selector = `[placeholder*=\"${description}\"]`; break;\n        case 'text':        selector = `:text(\"${description}\")`; break;\n        case 'role':        selector = `[role=\"button\"]:has-text(\"${description}\")`; break;\n        case 'css':         selector = `[class*=\"${description.toLowerCase()}\"]`; break;\n      }\n      if (selector) {\n        try {\n          await this.page.locator(selector).first().waitFor({ state: 'attached', timeout: 2_000 });\n          rulesUsed.push(rule.id);\n          return { success: true, selector, rulesUsed };\n        } catch { rulesUsed.push(`${rule.id} (failed)`); }\n      }\n    }\n    return { success: false, rulesUsed };\n  }\n\n  private async tryHeuristicHealing(selector: string, description: string): Promise<string | null> {\n    const idMatch    = selector.match(/#([\\w-]+)/);\n    const classMatch = selector.match(/\\.([\\w-]+)/);\n    const variations = [\n      ...(idMatch    ? [`[id=\"${idMatch[1]}\"]`, `[data-testid=\"${idMatch[1]}\"]`]   : []),\n      ...(classMatch ? [`[class*=\"${classMatch[1]}\"]`]                              : []),\n      `[role=\"button\"]:has-text(\"${description}\")`,\n      `button:has-text(\"${description}\")`,\n      `a:has-text(\"${description}\")`,\n    ];\n    for (const v of variations) {\n      try {\n        await this.page.locator(v).first().waitFor({ state: 'attached', timeout: 2_000 });\n        return v;\n      } catch { /* try next */ }\n    }\n    return null;\n  }\n\n  private async tryAIHealing(description: string): Promise<string | null> {\n    const screenshot = await this.page.screenshot();\n    const result     = await this.locatorGenerator.generateLocator({ elementDescription: description, screenshot });\n    if (await this.locatorGenerator.validateLocator(result.selector)) return result.selector;\n    return this.locatorGenerator.getBestLocator(result.alternatives);\n  }\n\n  private async tryFallbackHealing(description: string): Promise<string | null> {\n    for (const s of [`:text(\"${description}\")`, `:text-is(\"${description}\")`, `*:has-text(\"${description}\")`]) {\n      try {\n        await this.page.locator(s).first().waitFor({ state: 'attached', timeout: 2_000 });\n        return s;\n      } catch { /* try next */ }\n    }\n    return null;\n  }\n\n  getHealingHistory(): HealingReport[] { return [...this.healingHistory]; }\n\n  getHealingStats() {\n    const total      = this.healingHistory.length;\n    const successful = this.healingHistory.filter(r => r.success).length;\n    const methodBreakdown: Record<string, number> = {};\n    this.healingHistory.filter(r => r.success).forEach(r => { methodBreakdown[r.method] = (methodBreakdown[r.method] ?? 0) + 1; });\n    return { totalAttempts: total, successful, failed: total - successful, successRate: total > 0 ? (successful / total) * 100 : 0, methodBreakdown };\n  }\n}\n",
    "src/utils/ai-assistant/AITestGenerator.ts": "import { logger } from '@utils/helpers/Logger';\nimport { environment } from '@config/environment';\nimport OpenAI from 'openai';\n\nexport interface AITestGenerationOptions {\n  gherkinScenario:    string;\n  targetFramework:    'playwright' | 'cypress';\n  includePageObjects: boolean;\n  includeAssertions:  boolean;\n}\n\nexport interface GeneratedTest {\n  code:          string;\n  filename:      string;\n  pageObjects?:  string[];\n  dependencies?: string[];\n}\n\n/**\n * AITestGenerator\n *\n * Converts BDD Gherkin scenarios into executable Playwright TypeScript tests.\n * Requires AI_PROVIDER=openai and AI_API_KEY to be set.\n *\n * Usage:\n *   const gen = new AITestGenerator();\n *   const test = await gen.generateTest({ gherkinScenario, targetFramework: 'playwright', ... });\n *   await gen.saveGeneratedTest(test, 'tests/generated/');\n */\nexport class AITestGenerator {\n  private client: OpenAI | null = null;\n  private readonly env = environment.getConfig();\n\n  constructor() {\n    if (this.env.enableAITestGeneration && this.env.aiApiKey) {\n      this.initAIClient();\n    }\n  }\n\n  private initAIClient(): void {\n    try {\n      switch (this.env.aiProvider) {\n        case 'openai':\n          this.client = new OpenAI({ apiKey: this.env.aiApiKey });\n          break;\n        case 'anthropic':\n          logger.info('AITestGenerator: Anthropic provider configured (extend initAIClient())');\n          break;\n        case 'local':\n          logger.info('AITestGenerator: Local LLM provider configured (extend initAIClient())');\n          break;\n        default:\n          logger.warn(`AITestGenerator: unknown AI provider \"${this.env.aiProvider}\"`);\n      }\n    } catch (err) {\n      logger.error(`Failed to initialise AI client: ${String(err)}`);\n    }\n  }\n\n  async generateTest(options: AITestGenerationOptions): Promise<GeneratedTest> {\n    logger.info(`Generating Playwright test from Gherkin scenario`);\n    if (!this.client) throw new Error('AI client not initialised. Set AI_PROVIDER and AI_API_KEY.');\n\n    const prompt = this.buildPrompt(options);\n    const response = await this.client.chat.completions.create({\n      model:       this.env.aiModel,\n      messages:    [\n        { role: 'system',  content: 'You are an expert Playwright + TypeScript test automation engineer.' },\n        { role: 'user',    content: prompt },\n      ],\n      temperature: this.env.aiTemperature,\n      max_tokens:  this.env.aiMaxTokens,\n    });\n\n    const rawCode = response.choices[0]?.message?.content ?? '';\n    return {\n      code:         this.cleanCode(rawCode),\n      filename:     this.buildFilename(options.gherkinScenario),\n      pageObjects:  this.extractPageObjects(rawCode),\n      dependencies: this.extractDependencies(rawCode),\n    };\n  }\n\n  private buildPrompt(options: AITestGenerationOptions): string {\n    return `\nGenerate a production-ready Playwright test in TypeScript from the following Gherkin scenario:\n\n\\`\\`\\`gherkin\n${options.gherkinScenario}\n\\`\\`\\`\n\nRequirements:\n- Use Playwright's modern locator strategies (getByRole, getByLabel, getByTestId)\n- Include proper TypeScript type annotations\n- Add meaningful comments\n- Use async/await properly\n${options.includePageObjects ? '- Create page object methods if needed' : ''}\n${options.includeAssertions ? '- Include comprehensive assertions using expect()' : ''}\n- Follow maintainability best practices\n- Use proper waits and timeouts\n\nGenerate ONLY the TypeScript code, no markdown or explanations.\n`.trim();\n  }\n\n  private cleanCode(code: string): string {\n    return code.replace(/```typescript\\n?/g, '').replace(/```\\n?/g, '').trim();\n  }\n\n  private buildFilename(scenario: string): string {\n    const match = scenario.match(/Feature:\\s*(.+)/);\n    const name  = match ? match[1] : 'generated';\n    return name.toLowerCase().replace(/[^a-z0-9]+/g, '-') + '.spec.ts';\n  }\n\n  private extractPageObjects(code: string): string[] {\n    const found: string[] = [];\n    const re = /new\\s+(\\w+Page)\\(/g;\n    let m: RegExpExecArray | null;\n    while ((m = re.exec(code)) !== null) if (!found.includes(m[1])) found.push(m[1]);\n    return found;\n  }\n\n  private extractDependencies(code: string): string[] {\n    const found: string[] = [];\n    const re = /import\\s+.+\\s+from\\s+['\"](.+)['\"]/g;\n    let m: RegExpExecArray | null;\n    while ((m = re.exec(code)) !== null) found.push(m[1]);\n    return found;\n  }\n\n  async saveGeneratedTest(test: GeneratedTest, outputDir: string): Promise<void> {\n    const fs   = await import('fs');\n    const path = await import('path');\n    const filePath = path.join(outputDir, test.filename);\n    const dir = path.dirname(filePath);\n    if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });\n    fs.writeFileSync(filePath, test.code, 'utf-8');\n    logger.info(`Generated test saved: ${filePath}`);\n  }\n}\n",
    "src/utils/helpers/Logger.ts": "import winston from \"winston\";\nimport * as path from \"path\";\nimport * as fs from \"fs\";\nimport { environment } from \"../../config/environment\";\n\nclass Logger {\n  private logger: winston.Logger;\n\n  constructor() {\n    const env = environment.getConfig();\n\n    const logsDir = path.dirname(env.logFilePath);\n    if (!fs.existsSync(logsDir)) {\n      fs.mkdirSync(logsDir, { recursive: true });\n    }\n\n    const transports: winston.transport[] = [\n      new winston.transports.Console({\n        format: winston.format.combine(\n          winston.format.colorize(),\n          winston.format.timestamp({ format: \"YYYY-MM-DD HH:mm:ss\" }),\n          winston.format.printf(({ timestamp, level, message, ...meta }) => {\n            const metaStr = Object.keys(meta).length\n              ? JSON.stringify(meta, null, 2)\n              : \"\";\n            return `${timestamp} [${level}]: ${message} ${metaStr}`;\n          }),\n        ),\n      }),\n    ];\n\n    if (env.logToFile) {\n      transports.push(\n        new winston.transports.File({\n          filename: env.logFilePath,\n          format: winston.format.combine(\n            winston.format.timestamp({ format: \"YYYY-MM-DD HH:mm:ss\" }),\n            winston.format.json(),\n          ),\n          maxsize: 10485760, // 10MB\n          maxFiles: 5,\n        }),\n      );\n    }\n\n    this.logger = winston.createLogger({\n      level: env.logLevel,\n      transports,\n    });\n  }\n\n  public info(message: string, meta?: Record<string, unknown>): void {\n    this.logger.info(message, meta);\n  }\n\n  public error(message: string, meta?: Record<string, unknown>): void {\n    this.logger.error(message, meta);\n  }\n\n  public warn(message: string, meta?: Record<string, unknown>): void {\n    this.logger.warn(message, meta);\n  }\n\n  public debug(message: string, meta?: Record<string, unknown>): void {\n    this.logger.debug(message, meta);\n  }\n\n  public startTest(testName: string): void {\n    this.logger.info(`Starting test: ${testName}`, {\n      type: \"test-start\",\n      timestamp: new Date().toISOString(),\n    });\n  }\n\n  public endTest(\n    testName: string,\n    status: \"passed\" | \"failed\" | \"skipped\",\n    duration?: number,\n  ): void {\n    this.logger.info(`Test ${status}: ${testName}`, {\n      type: \"test-end\",\n      status,\n      duration,\n      timestamp: new Date().toISOString(),\n    });\n  }\n}\n\nexport { Logger };\nexport const logger = new Logger();\n",
    "src/utils/helpers/RetryHandler.ts": "import { logger } from \"./Logger\";\n\nexport interface RetryOptions {\n  maxAttempts?: number;\n  delay?: number;\n  backoffMultiplier?: number;\n  timeout?: number;\n  retryableErrors?: string[];\n}\n\nexport class RetryHandler {\n  private defaultOptions: Required<RetryOptions> = {\n    maxAttempts: 3,\n    delay: 1000,\n    backoffMultiplier: 2,\n    timeout: 30000,\n    retryableErrors: [\"timeout\", \"detached\", \"disconnected\", \"navigation\"],\n  };\n\n  async retry<T>(\n    fn: () => Promise<T>,\n    options: RetryOptions = {},\n    context?: string,\n  ): Promise<T> {\n    const opts = { ...this.defaultOptions, ...options };\n    let lastError: Error | undefined;\n    let currentDelay = opts.delay;\n\n    for (let attempt = 1; attempt <= opts.maxAttempts; attempt++) {\n      try {\n        logger.debug(\n          `Attempt ${attempt}/${opts.maxAttempts}${context ? ` - ${context}` : \"\"}`,\n        );\n\n        const result = await Promise.race([\n          fn(),\n          this.createTimeout(opts.timeout, `Operation timed out after ${opts.timeout}ms`),\n        ]);\n\n        if (attempt > 1) {\n          logger.info(`Retry succeeded on attempt ${attempt}${context ? ` - ${context}` : \"\"}`);\n        }\n\n        return result as T;\n      } catch (error: unknown) {\n        const err = error as Error;\n        lastError = err;\n\n        const isRetryable = this.isRetryableError(err, opts.retryableErrors);\n        const isLastAttempt = attempt === opts.maxAttempts;\n\n        logger.warn(`Attempt ${attempt}/${opts.maxAttempts} failed${context ? ` - ${context}` : \"\"}`, {\n          error: err.message,\n          retryable: isRetryable,\n        });\n\n        if (!isRetryable || isLastAttempt) {\n          throw err;\n        }\n\n        await this.sleep(currentDelay);\n        currentDelay *= opts.backoffMultiplier;\n      }\n    }\n\n    throw lastError || new Error(\"Retry failed without error\");\n  }\n\n  private isRetryableError(error: Error, retryableErrors: string[]): boolean {\n    return retryableErrors.some((keyword) =>\n      error.message.toLowerCase().includes(keyword.toLowerCase()),\n    );\n  }\n\n  private createTimeout(ms: number, message: string): Promise<never> {\n    return new Promise((_, reject) =>\n      setTimeout(() => reject(new Error(message)), ms),\n    );\n  }\n\n  private sleep(ms: number): Promise<void> {\n    return new Promise((resolve) => setTimeout(resolve, ms));\n  }\n}\n\nexport const retryHandler = new RetryHandler();\n",
    "src/utils/helpers/WaitHelper.ts": "import { Page, Locator } from \"@playwright/test\";\nimport { logger } from \"./Logger\";\n\nexport interface WaitOptions {\n  timeout?: number;\n  state?: \"attached\" | \"detached\" | \"visible\" | \"hidden\";\n}\n\nexport class WaitHelper {\n  constructor(private page: Page) {}\n\n  async waitForElement(locator: Locator, options: WaitOptions = {}): Promise<void> {\n    const { timeout = 10000, state = \"visible\" } = options;\n    try {\n      await locator.waitFor({ state, timeout });\n      logger.debug(`Element ready in ${state} state`);\n    } catch (error: unknown) {\n      const err = error as Error;\n      logger.error(`Failed to wait for element in ${state} state`, {\n        error: err.message,\n        timeout,\n      });\n      throw error;\n    }\n  }\n\n  async waitForPageLoad(timeout = 30000): Promise<void> {\n    try {\n      await this.page.waitForLoadState(\"networkidle\", { timeout });\n      logger.debug(\"Page fully loaded (networkidle)\");\n    } catch {\n      logger.warn(\"Timeout waiting for networkidle, falling back to domcontentloaded\");\n      await this.page.waitForLoadState(\"domcontentloaded\", { timeout: 5000 });\n    }\n  }\n\n  async waitForNavigation(timeout = 30000): Promise<void> {\n    await this.page.waitForLoadState(\"domcontentloaded\", { timeout });\n    logger.debug(\"Navigation completed\");\n  }\n\n  async waitForURL(urlPattern: string | RegExp, timeout = 30000): Promise<void> {\n    try {\n      await this.page.waitForURL(urlPattern, { timeout });\n      logger.debug(`URL matched pattern: ${urlPattern}`);\n    } catch (error: unknown) {\n      const err = error as Error;\n      logger.error(`URL did not match pattern: ${urlPattern}`, {\n        currentUrl: this.page.url(),\n        error: err.message,\n      });\n      throw error;\n    }\n  }\n\n  async waitForSelector(selector: string, options: WaitOptions = {}): Promise<Locator> {\n    const { timeout = 10000, state = \"visible\" } = options;\n    const locator = this.page.locator(selector);\n    await locator.waitFor({ state, timeout });\n    return locator;\n  }\n\n  async waitForText(text: string | RegExp, timeout = 10000): Promise<void> {\n    await this.page.getByText(text).waitFor({ state: \"visible\", timeout });\n    logger.debug(`Text found: ${text}`);\n  }\n\n  async waitForCount(selector: string, count: number, timeout = 10000): Promise<void> {\n    await this.page.locator(selector).nth(count - 1).waitFor({ state: \"visible\", timeout });\n    logger.debug(`Found ${count} element(s) matching: ${selector}`);\n  }\n\n  async sleep(ms: number): Promise<void> {\n    await this.page.waitForTimeout(ms);\n  }\n}\n",
    "src/utils/locators/ElementContextHelper.ts": "import { Page } from '@playwright/test';\n\nexport interface ElementContext {\n  id:       string;\n  name:     string;\n  page:     string;\n  metadata: {\n    nearText?:   string;\n    role?:       string;\n    ariaLabel?:  string;\n    placeholder?: string;\n    text?:       string;\n  };\n}\n\n/**\n * ElementContextHelper\n *\n * Gathers contextual metadata about a named element on the current page\n * to provide richer hints to AI self-healing and locator generation.\n *\n * Used by LocatorManager when building a heal context payload.\n */\nexport class ElementContextHelper {\n  constructor(private readonly page: Page) {}\n\n  async buildContext(name: string, overrides?: Partial<ElementContext['metadata']>): Promise<ElementContext> {\n    const url       = this.page.url();\n    const nearText  = await this.findNearbyText(name);\n    const role      = overrides?.role       ?? await this.inferRole(name);\n    const ariaLabel = overrides?.ariaLabel  ?? await this.inferAriaLabel(name);\n    const placeholder = overrides?.placeholder ?? await this.inferPlaceholder(name);\n\n    return {\n      id: name, name, page: url,\n      metadata: {\n        nearText:    overrides?.nearText ?? (nearText || undefined),\n        role:        role    || undefined,\n        ariaLabel:   ariaLabel   || undefined,\n        placeholder: placeholder || undefined,\n        text:        name,\n      },\n    };\n  }\n\n  private async inferRole(name: string): Promise<string | null> {\n    const candidates = ['button', 'link', 'textbox', 'heading'];\n    for (const role of candidates) {\n      try {\n        const count = await this.page.getByRole(role as Parameters<Page['getByRole']>[0], { name: new RegExp(name, 'i') }).count();\n        if (count > 0) return role;\n      } catch { /* continue */ }\n    }\n    return null;\n  }\n\n  private async inferAriaLabel(name: string): Promise<string | null> {\n    const loc   = this.page.locator('[aria-label]');\n    const count = await loc.count();\n    for (let i = 0; i < count; i++) {\n      const label = await loc.nth(i).getAttribute('aria-label');\n      if (label?.toLowerCase().includes(name.toLowerCase())) return label;\n    }\n    return null;\n  }\n\n  private async inferPlaceholder(name: string): Promise<string | null> {\n    const loc   = this.page.locator('[placeholder]');\n    const count = await loc.count();\n    for (let i = 0; i < count; i++) {\n      const ph = await loc.nth(i).getAttribute('placeholder');\n      if (ph?.toLowerCase().includes(name.toLowerCase())) return ph;\n    }\n    return null;\n  }\n\n  private async findNearbyText(name: string): Promise<string | null> {\n    try {\n      const count = await this.page.getByText(new RegExp(name, 'i')).count();\n      return count > 0 ? name : null;\n    } catch { return null; }\n  }\n}\n",
    "src/utils/locators/HealApplicator.ts": "import * as fs   from 'fs';\nimport * as path from 'path';\nimport { execSync } from 'child_process';\n\n/**\n * HealApplicator\n *\n * Applies approved heals back to the source TypeScript files so the next run\n * uses the fixed selector natively (zero re-healing overhead).\n *\n * Workflow:\n *   1. Read healed-locators.json — find entries where `approved === true`\n *   2. Search `searchRoots` (.ts files) for the originalSelector string literal\n *   3. Replace the first occurrence with healedSelector\n *   4. Optionally create a Git branch + commit + PR via the `gh` CLI\n *\n * Configuration (env vars):\n *   HEAL_SEARCH_ROOTS   Comma-separated dirs to search  (default: src/)\n *   HEAL_TARGET_REPO    Repo root for git operations    (default: cwd)\n *   HEAL_PR_TITLE       PR title prefix\n *   GH_TOKEN / GITHUB_TOKEN  Required by `gh pr create` in CI\n */\n\nexport interface HealRecord {\n  healedSelector:   string;\n  originalSelector: string;\n  strategy:         string;\n  provider?:        string;\n  intent:           string;\n  healCount:        number;\n  lastHealedAt:     string;\n  approved?:        boolean;\n}\n\nexport interface AppliedHeal {\n  key:              string;\n  originalSelector: string;\n  healedSelector:   string;\n  file:             string;\n  line:             number;\n}\n\nexport interface ApplyResult {\n  applied:      AppliedHeal[];\n  skipped:      string[];\n  errors:       Array<{ key: string; error: string }>;\n  changedFiles: string[];\n  prUrl?:       string;\n}\n\nexport class HealApplicator {\n  private readonly searchRoots: string[];\n  private readonly targetRepo:  string;\n\n  constructor(options?: { searchRoots?: string[]; targetRepo?: string }) {\n    const envRoots = process.env.HEAL_SEARCH_ROOTS?.split(',').map(r => r.trim()) ?? [];\n    this.searchRoots = options?.searchRoots?.length\n      ? options.searchRoots\n      : envRoots.length ? envRoots : [path.resolve(process.cwd(), 'src')];\n    this.targetRepo = options?.targetRepo ?? process.env.HEAL_TARGET_REPO ?? process.cwd();\n  }\n\n  apply(store: Record<string, HealRecord>): ApplyResult {\n    const result: ApplyResult = { applied: [], skipped: [], errors: [], changedFiles: [] };\n    const changed = new Set<string>();\n\n    for (const [key, record] of Object.entries(store)) {\n      if (!record.approved) continue;\n      if (!record.originalSelector || !record.healedSelector || record.originalSelector === record.healedSelector) {\n        result.skipped.push(key); continue;\n      }\n      try {\n        const hit = this.replaceInFiles(record.originalSelector, record.healedSelector);\n        if (hit) { result.applied.push({ key, ...hit }); changed.add(hit.file); }\n        else      { result.skipped.push(key); }\n      } catch (err) {\n        result.errors.push({ key, error: String(err) });\n      }\n    }\n\n    result.changedFiles = [...changed];\n    return result;\n  }\n\n  createPR(changedFiles: string[], summary: AppliedHeal[]): string {\n    if (!changedFiles.length) return '';\n    const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);\n    const branch    = `heal/locator-fixes-${timestamp}`;\n    const prTitle   = process.env.HEAL_PR_TITLE ?? 'fix: apply AI-healed locator fixes';\n    const run = (cmd: string) => execSync(cmd, { cwd: this.targetRepo, stdio: 'pipe' }).toString().trim();\n\n    run(`git checkout -b ${branch}`);\n    for (const file of changedFiles) { run(`git add \"${path.relative(this.targetRepo, file)}\"`); }\n    const body = [\n      '## AI-Healed Locator Fixes', '',\n      '| Key | Original → Healed |', '|-----|-------------------|',\n      ...summary.map(h => `| \\`${h.key}\\` | \\`${h.originalSelector}\\` → \\`${h.healedSelector}\\` |`),\n      '', '_Applied by Healix self-healing dashboard_',\n    ].join('\\n');\n    run(`git commit -m \"${prTitle}\" --message \"${body.replace(/\"/g, '\\\\\"')}\"`);\n    run(`git push origin ${branch}`);\n    return run(`gh pr create --title \"${prTitle}\" --body \"${body.replace(/\"/g, '\\\\\"')}\" --head ${branch} --base main 2>/dev/null || echo \"\"`);\n  }\n\n  private replaceInFiles(original: string, healed: string): (Omit<AppliedHeal, 'key'>) | null {\n    for (const root of this.searchRoots) {\n      if (!fs.existsSync(root)) continue;\n      for (const file of this.collectTsFiles(root)) {\n        const result = this.replaceInFile(file, original, healed);\n        if (result) return { file, line: result.lineNumber, originalSelector: original, healedSelector: result.normHealed };\n      }\n    }\n    return null;\n  }\n\n  private replaceInFile(file: string, original: string, healed: string): { lineNumber: number; normHealed: string } | null {\n    let content: string;\n    try { content = fs.readFileSync(file, 'utf8'); } catch { return null; }\n\n    const escaped  = original.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n    const pattern  = new RegExp(`(['\"\\`])${escaped}\\\\1`);\n    const match    = content.match(pattern);\n    if (!match || match.index === undefined) return null;\n\n    const normHealed = healed.replace(/\\[([^\\]='\"\\s]+)='([^']*)'\\]/g, '[$1=\"$2\"]');\n    const quote      = match[1];\n    const newContent = content.replace(pattern, `${quote}${normHealed}${quote}`);\n    const lineNumber = (content.slice(0, match.index).match(/\\n/g)?.length ?? 0) + 1;\n\n    fs.writeFileSync(file, newContent, 'utf8');\n    return { lineNumber, normHealed };\n  }\n\n  private collectTsFiles(dir: string): string[] {\n    const results: string[] = [];\n    let entries: fs.Dirent[];\n    try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return results; }\n    for (const entry of entries) {\n      const full = path.join(dir, entry.name);\n      if (entry.isDirectory()) {\n        if (!['node_modules', 'dist', '.git'].includes(entry.name)) {\n          results.push(...this.collectTsFiles(full));\n        }\n      } else if (entry.isFile() && entry.name.endsWith('.ts')) {\n        results.push(full);\n      }\n    }\n    return results;\n  }\n}\n",
    "src/utils/locators/HealingDashboard.ts": "/**\n * HealingDashboard — real-time self-healing observability server\n *\n * Starts a lightweight HTTP server (default port 7890) that:\n *   • Accepts healing events pushed by LocatorHealer during test runs\n *   • Serves a live HTML dashboard at http://localhost:<port>\n *   • Exposes JSON APIs at /api/events, /api/summary, /api/registry\n *   • Auto-refreshes via Server-Sent Events (SSE)\n *\n * Usage:\n *   BeforeAll: await HealingDashboard.getInstance().start();\n *   AfterAll:  await HealingDashboard.getInstance().stop();\n */\n\nimport * as http from 'http';\nimport * as fs   from 'fs';\nimport * as path from 'path';\n\nconst HEAL_STORE_PATH = path.resolve(\n  process.env.HEAL_STORE_PATH ?? 'self-heal/healed-locators.json',\n);\n\nfunction readHealStore(): Record<string, unknown> {\n  try {\n    if (!fs.existsSync(HEAL_STORE_PATH)) return {};\n    return JSON.parse(fs.readFileSync(HEAL_STORE_PATH, 'utf8'));\n  } catch { return {}; }\n}\n\nexport interface HealEvent {\n  key:              string;\n  originalSelector: string;\n  strategy:         string;\n  provider?:        string;\n  healedSelector?:  string;\n  intent:           string;\n  scenario?:        string;\n  timestamp:        string;\n}\n\nconst cors = { 'Access-Control-Allow-Origin': '*' };\n\nfunction esc(str: string): string {\n  return str\n    .replace(/&/g, '&amp;').replace(/</g, '&lt;')\n    .replace(/>/g, '&gt;').replace(/\"/g, '&quot;').replace(/'/g, '&#39;');\n}\n\nexport class HealingDashboard {\n  private static _instance: HealingDashboard | null = null;\n\n  static getInstance(): HealingDashboard {\n    if (!HealingDashboard._instance) {\n      HealingDashboard._instance = new HealingDashboard();\n    }\n    return HealingDashboard._instance;\n  }\n\n  static reset(): void { HealingDashboard._instance = null; }\n\n  private readonly port: number;\n  private server: http.Server | null = null;\n  private events: HealEvent[] = [];\n  private sseClients: http.ServerResponse[] = [];\n\n  private constructor() {\n    this.port = parseInt(process.env.HEALING_DASHBOARD_PORT ?? '7890', 10);\n  }\n\n  async start(): Promise<void> {\n    if (this.server) return;\n    this.server = http.createServer((req, res) => this.handleRequest(req, res));\n    await new Promise<void>((resolve) => {\n      this.server!.listen(this.port, '127.0.0.1', () => {\n        console.log(`  🩺  HealingDashboard → http://localhost:${this.port}`);\n        resolve();\n      });\n      this.server!.on('error', (err: NodeJS.ErrnoException) => {\n        if (err.code === 'EADDRINUSE') {\n          console.log(`  🩺  HealingDashboard already running on port ${this.port}`);\n          this.server = null;\n        } else {\n          console.warn(`  ⚠  HealingDashboard failed to start: ${err.message}`);\n          this.server = null;\n        }\n        resolve();\n      });\n    });\n  }\n\n  async stop(): Promise<void> {\n    for (const client of this.sseClients) { try { client.end(); } catch { /* ignore */ } }\n    this.sseClients = [];\n    await new Promise<void>((resolve) => {\n      if (!this.server) { resolve(); return; }\n      this.server.close(() => { this.server = null; resolve(); });\n    });\n  }\n\n  record(event: HealEvent): void {\n    this.events.push(event);\n    this.pushSse(event);\n  }\n\n  private handleRequest(req: http.IncomingMessage, res: http.ServerResponse): void {\n    const url = req.url ?? '/';\n    if (url === '/events' && req.headers.accept?.includes('text/event-stream')) {\n      this.handleSse(res); return;\n    }\n    if (url === '/api/events') {\n      res.writeHead(200, { 'Content-Type': 'application/json', ...cors });\n      res.end(JSON.stringify(this.events, null, 2)); return;\n    }\n    if (url === '/api/summary') {\n      res.writeHead(200, { 'Content-Type': 'application/json', ...cors });\n      res.end(JSON.stringify(this.buildSummary(), null, 2)); return;\n    }\n    if (url === '/api/registry') {\n      res.writeHead(200, { 'Content-Type': 'application/json', ...cors });\n      res.end(JSON.stringify(readHealStore(), null, 2)); return;\n    }\n    if (url === '/api/registry/clear' && req.method === 'POST') {\n      try { fs.mkdirSync(path.dirname(HEAL_STORE_PATH), { recursive: true }); fs.writeFileSync(HEAL_STORE_PATH, '{}', 'utf8'); } catch { /* ignore */ }\n      res.writeHead(200, { 'Content-Type': 'application/json', ...cors });\n      res.end(JSON.stringify({ ok: true })); return;\n    }\n    res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });\n    res.end(this.buildHtml());\n  }\n\n  private handleSse(res: http.ServerResponse): void {\n    res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive', ...cors });\n    res.write(':ok\\n\\n');\n    this.sseClients.push(res);\n    res.on('close', () => { this.sseClients = this.sseClients.filter(c => c !== res); });\n  }\n\n  private pushSse(event: HealEvent): void {\n    const data = `data: ${JSON.stringify(event)}\\n\\n`;\n    for (const client of this.sseClients) { try { client.write(data); } catch { /* client disconnected */ } }\n  }\n\n  private buildSummary() {\n    const total = this.events.length;\n    const byStrategy: Record<string, number> = {};\n    const byKey: Record<string, number> = {};\n    const byProvider: Record<string, number> = {};\n    for (const e of this.events) {\n      byStrategy[e.strategy] = (byStrategy[e.strategy] ?? 0) + 1;\n      byKey[e.key]           = (byKey[e.key]           ?? 0) + 1;\n      if (e.provider) byProvider[e.provider] = (byProvider[e.provider] ?? 0) + 1;\n    }\n    return { total, uniqueKeys: Object.keys(byKey).length, aiHeals: byStrategy['ai-vision'] ?? 0, byStrategy, byKey, byProvider };\n  }\n\n  private buildHtml(): string {\n    const summary = this.buildSummary();\n    const store   = readHealStore();\n    return `<!DOCTYPE html>\n<html lang=\"en\">\n<head><meta charset=\"UTF-8\"><title>Healix — Self-Healing Dashboard</title>\n<style>\n*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}\n:root{--bg:#0d1117;--surface:#161b22;--border:#30363d;--accent:#58a6ff;--green:#3fb950;--yellow:#d29922;--red:#f85149;--purple:#bc8cff;--text:#e6edf3;--muted:#8b949e;--radius:8px}\nbody{background:var(--bg);color:var(--text);font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;font-size:14px;line-height:1.5;padding:24px}\nheader{display:flex;align-items:center;gap:12px;margin-bottom:28px}header h1{font-size:20px;font-weight:600}\n.badge{background:var(--green);color:#000;font-size:11px;font-weight:700;padding:2px 8px;border-radius:20px}\n.provider-pill{margin-left:auto;background:var(--surface);border:1px solid var(--border);border-radius:20px;padding:4px 12px;font-size:12px;color:var(--accent)}\n.cards{display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:16px;margin-bottom:28px}\n.card{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:16px 20px}\n.card .label{font-size:11px;color:var(--muted);text-transform:uppercase;letter-spacing:.6px}\n.card .value{font-size:28px;font-weight:700;margin-top:4px}\n.card.green .value{color:var(--green)}.card.blue .value{color:var(--accent)}.card.purple .value{color:var(--purple)}.card.yellow .value{color:var(--yellow)}\n.breakdown{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:16px 20px;margin-bottom:28px}\n.breakdown h2{font-size:13px;font-weight:600;margin-bottom:12px;color:var(--muted);text-transform:uppercase}\n.bars{display:flex;flex-direction:column;gap:8px}.bar-row{display:flex;align-items:center;gap:10px}\n.bar-label{width:100px;font-size:12px;color:var(--muted);text-align:right}\n.bar-track{flex:1;background:var(--border);border-radius:4px;height:10px;overflow:hidden}\n.bar-fill{height:100%;border-radius:4px;background:var(--accent)}.bar-fill.ai-vision{background:var(--purple)}.bar-fill.ax-tree{background:var(--yellow)}.bar-fill.role{background:var(--green)}\n.bar-count{width:28px;font-size:12px;text-align:right}\n.section-title{font-size:13px;font-weight:600;color:var(--muted);text-transform:uppercase;letter-spacing:.5px;margin-bottom:12px;display:flex;align-items:center;gap:8px}\n.section-title .count{background:var(--accent);color:#000;font-size:11px;font-weight:700;padding:1px 7px;border-radius:20px}\ntable{width:100%;border-collapse:collapse;background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);overflow:hidden}\nth{background:#21262d;text-align:left;padding:10px 14px;font-size:11px;color:var(--muted);text-transform:uppercase;border-bottom:1px solid var(--border)}\ntd{padding:10px 14px;border-bottom:1px solid var(--border);vertical-align:top;font-size:13px}\ntr:last-child td{border-bottom:none}tr:hover td{background:rgba(88,166,255,.04)}\n.key-chip{background:rgba(88,166,255,.12);color:var(--accent);border-radius:4px;padding:2px 7px;font-size:12px;font-family:monospace}\n.selector{font-family:monospace;font-size:12px;color:var(--muted);word-break:break-all}\n.new-selector{font-family:monospace;font-size:12px;color:var(--green);word-break:break-all}\n.badge-strategy{display:inline-block;border-radius:4px;padding:2px 8px;font-size:11px;font-weight:600}\n.badge-strategy.ai-vision{background:rgba(188,140,255,.15);color:var(--purple)}\n.badge-strategy.ax-tree{background:rgba(210,153,34,.15);color:var(--yellow)}\n.badge-strategy.role,.badge-strategy.label{background:rgba(63,185,80,.15);color:var(--green)}\n.badge-strategy.text{background:rgba(88,166,255,.1);color:var(--accent)}\n.badge-strategy.cached{background:rgba(139,148,158,.15);color:var(--muted)}\n.ts{font-size:11px;color:var(--muted);white-space:nowrap}.scenario{font-size:11px;color:var(--muted);max-width:180px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n.empty{text-align:center;padding:48px;color:var(--muted);font-size:13px}\n.pulse{display:inline-block;width:8px;height:8px;background:var(--green);border-radius:50%;margin-right:6px;animation:pulse 2s infinite}\n@keyframes pulse{0%,100%{opacity:1}50%{opacity:.3}}\nfooter{margin-top:32px;text-align:center;font-size:11px;color:var(--muted)}\n</style>\n</head>\n<body>\n<header>\n  <span>🩺</span><h1>Healix — Self-Healing Dashboard</h1><span class=\"badge\">LIVE</span>\n  <span class=\"provider-pill\">AI · ${process.env.AI_PROVIDER ?? 'openai'} / ${process.env.AI_MODEL ?? 'gpt-4o'}</span>\n</header>\n<div class=\"cards\" id=\"cards\">${this.renderCards(summary)}</div>\n<div class=\"breakdown\"><h2>Strategy breakdown</h2><div class=\"bars\" id=\"bars\">${this.renderBars(summary)}</div></div>\n<div class=\"section-title\">Healing events <span class=\"count\" id=\"total-count\">${summary.total}</span>\n  <span style=\"margin-left:auto;font-size:12px;color:var(--muted)\"><span class=\"pulse\"></span>live</span>\n</div>\n<table><thead><tr><th>Time</th><th>Key</th><th>Strategy</th><th>Original selector</th><th>Healed selector</th><th>Intent</th><th>Scenario</th></tr></thead>\n<tbody id=\"events-body\">${this.renderRows(this.events)}</tbody></table>\n<div class=\"section-title\" style=\"margin-top:32px\">Stored heals <span class=\"count\" id=\"registry-count\">${Object.keys(store).length}</span>\n  <span style=\"margin-left:auto;font-size:12px;color:var(--muted)\">\n    <code style=\"color:var(--accent);font-size:11px\">self-heal/healed-locators.json</code>\n    &nbsp;·&nbsp;<button onclick=\"clearRegistry()\" style=\"background:rgba(248,81,73,.1);color:var(--red);border:1px solid var(--red);border-radius:4px;padding:2px 10px;font-size:11px;cursor:pointer\">Clear all</button>\n  </span>\n</div>\n<table><thead><tr><th>Key</th><th>Strategy</th><th>Original selector</th><th>Healed selector</th><th>Heals</th><th>Last healed</th></tr></thead>\n<tbody id=\"registry-body\">${this.renderRegistry(store)}</tbody></table>\n<footer>QA Framework Self-Healing Dashboard · port ${this.port}</footer>\n<script>\nconst es=new EventSource('/events');\nes.onmessage=e=>{const ev=JSON.parse(e.data);prependRow(ev);refreshSummary();refreshRegistry();};\nfunction esc(s){return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/\"/g,'&quot;').replace(/'/g,'&#39;');}\nfunction stratCls(s){return['ai-vision','ax-tree','role','label','text','cached'].includes(s)?s:'';}\nfunction prependRow(ev){const tb=document.getElementById('events-body');const nd=tb.querySelector('.no-data');if(nd)nd.remove();const ts=new Date(ev.timestamp).toLocaleTimeString();const tr=document.createElement('tr');tr.innerHTML=\\`<td class=\"ts\">\\${esc(ts)}</td><td><span class=\"key-chip\">\\${esc(ev.key)}</span></td><td><span class=\"badge-strategy \\${stratCls(ev.strategy)}\">\\${esc(ev.strategy)}\\${ev.provider?' · '+esc(ev.provider):''}</span></td><td class=\"selector\">\\${esc(ev.originalSelector)}</td><td class=\"new-selector\">\\${esc(ev.healedSelector??'—')}</td><td>\\${esc(ev.intent)}</td><td class=\"scenario\">\\${esc(ev.scenario??'—')}</td>\\`;tb.insertBefore(tr,tb.firstChild);}\nasync function refreshSummary(){try{const d=await(await fetch('/api/summary')).json();document.getElementById('total-count').textContent=d.total;document.getElementById('cards').innerHTML=renderCards(d);document.getElementById('bars').innerHTML=renderBars(d);}catch{}}\nasync function refreshRegistry(){try{const d=await(await fetch('/api/registry')).json();const keys=Object.keys(d);document.getElementById('registry-count').textContent=keys.length;document.getElementById('registry-body').innerHTML=keys.length===0?'<tr><td colspan=\"6\" style=\"color:var(--muted);text-align:center;padding:16px\">No heals stored yet</td></tr>':keys.map(k=>{const r=d[k];return\\`<tr><td><code style=\"color:var(--accent)\">\\${esc(k)}</code></td><td><span class=\"badge-strategy \\${stratCls(r.strategy??'')}\">\\${esc(r.strategy??'?')}</span></td><td><code style=\"color:var(--muted);font-size:11px\">\\${esc(r.originalSelector??'')}</code></td><td><code style=\"color:var(--green);font-size:12px\">\\${esc(r.healedSelector??'')}</code></td><td style=\"text-align:center\">\\${r.healCount??0}</td><td style=\"font-size:11px;color:var(--muted)\">\\${r.lastHealedAt?new Date(r.lastHealedAt).toLocaleTimeString():''}</td></tr>\\`;}).join('');}catch{}}\nasync function clearRegistry(){if(!confirm('Clear all stored heals?'))return;await fetch('/api/registry/clear',{method:'POST'});await refreshRegistry();}\nfunction renderCards(s){return\\`<div class=\"card green\"><div class=\"label\">Total heals</div><div class=\"value\">\\${s.total}</div></div><div class=\"card blue\"><div class=\"label\">Unique keys</div><div class=\"value\">\\${s.uniqueKeys}</div></div><div class=\"card purple\"><div class=\"label\">AI Vision heals</div><div class=\"value\">\\${s.aiHeals}</div></div><div class=\"card yellow\"><div class=\"label\">Strategies</div><div class=\"value\">\\${Object.keys(s.byStrategy).length}</div></div>\\`;}\nfunction renderBars(s){const t=s.total||1;const e=Object.entries(s.byStrategy);return e.length===0?'<div style=\"color:var(--muted);font-size:12px\">No events yet</div>':e.map(([k,v])=>{const p=Math.round(v/t*100);return\\`<div class=\"bar-row\"><div class=\"bar-label\">\\${esc(k)}</div><div class=\"bar-track\"><div class=\"bar-fill \\${stratCls(k)}\" style=\"width:\\${p}%\"></div></div><div class=\"bar-count\">\\${v}</div></div>\\`;}).join('');}\nrefreshRegistry();setInterval(refreshRegistry,5000);\n</script>\n</body></html>`;\n  }\n\n  private renderCards(s: ReturnType<HealingDashboard['buildSummary']>): string {\n    return `<div class=\"card green\"><div class=\"label\">Total heals</div><div class=\"value\">${s.total}</div></div>\n      <div class=\"card blue\"><div class=\"label\">Unique keys</div><div class=\"value\">${s.uniqueKeys}</div></div>\n      <div class=\"card purple\"><div class=\"label\">AI Vision heals</div><div class=\"value\">${s.aiHeals}</div></div>\n      <div class=\"card yellow\"><div class=\"label\">Strategies used</div><div class=\"value\">${Object.keys(s.byStrategy).length}</div></div>`;\n  }\n\n  private renderBars(s: ReturnType<HealingDashboard['buildSummary']>): string {\n    const total = s.total || 1;\n    const entries = Object.entries(s.byStrategy);\n    if (!entries.length) return '<div style=\"color:var(--muted);font-size:12px\">No events yet</div>';\n    return entries.map(([k, v]) => {\n      const pct = Math.round((v / total) * 100);\n      const cls = ['ai-vision','ax-tree','role'].includes(k) ? k : '';\n      return `<div class=\"bar-row\"><div class=\"bar-label\">${esc(k)}</div><div class=\"bar-track\"><div class=\"bar-fill ${cls}\" style=\"width:${pct}%\"></div></div><div class=\"bar-count\">${v}</div></div>`;\n    }).join('');\n  }\n\n  private renderRows(events: HealEvent[]): string {\n    if (!events.length) return `<tr class=\"no-data\"><td colspan=\"7\" class=\"empty\">No healing events yet.</td></tr>`;\n    return [...events].reverse().map(e => {\n      const ts  = new Date(e.timestamp).toLocaleTimeString();\n      const cls = ['ai-vision','ax-tree','role','label','text','cached'].includes(e.strategy) ? e.strategy : '';\n      return `<tr><td class=\"ts\">${esc(ts)}</td><td><span class=\"key-chip\">${esc(e.key)}</span></td><td><span class=\"badge-strategy ${cls}\">${esc(e.strategy)}${e.provider ? ` · ${esc(e.provider)}` : ''}</span></td><td class=\"selector\">${esc(e.originalSelector)}</td><td class=\"new-selector\">${esc(e.healedSelector ?? '—')}</td><td>${esc(e.intent)}</td><td class=\"scenario\" title=\"${esc(e.scenario ?? '')}\">${esc(e.scenario ?? '—')}</td></tr>`;\n    }).join('');\n  }\n\n  private renderRegistry(store: Record<string, unknown>): string {\n    const keys = Object.keys(store);\n    if (!keys.length) return `<tr><td colspan=\"6\" style=\"color:var(--muted);text-align:center;padding:16px\">No heals stored yet</td></tr>`;\n    return keys.map(k => {\n      const r = store[k] as Record<string, unknown>;\n      const strat = String(r['strategy'] ?? 'unknown');\n      const cls = ['ai-vision','ax-tree','role','label','text','cached'].includes(strat) ? strat : '';\n      return `<tr><td><code style=\"color:var(--accent)\">${esc(k)}</code></td><td><span class=\"badge-strategy ${cls}\">${esc(strat)}</span></td><td><code style=\"color:var(--muted);font-size:11px\">${esc(String(r['originalSelector'] ?? ''))}</code></td><td><code style=\"color:var(--green);font-size:12px\">${esc(String(r['healedSelector'] ?? ''))}</code></td><td style=\"text-align:center\">${String(r['healCount'] ?? 0)}</td><td style=\"font-size:11px;color:var(--muted)\">${r['lastHealedAt'] ? new Date(String(r['lastHealedAt'])).toLocaleString() : ''}</td></tr>`;\n    }).join('');\n  }\n}\n",
    "src/utils/locators/LocatorHealer.ts": "import { Page, Locator, expect } from '@playwright/test';\nimport { LocatorRepository } from './LocatorRepository';\nimport { HealingDashboard } from './HealingDashboard';\n\nexport interface HealerLogger {\n  info(msg: string): void;\n  warn(msg: string): void;\n  error(msg: string): void;\n}\n\n/**\n * LocatorHealer — Layer 1 Self-Healing\n *\n * Healing chain per action:\n *   1. Original selector (from LocatorRepository.getBestSelector)\n *   2. Role-based: getByRole inferred from intent\n *   3. Label-based: getByLabel / getByPlaceholder\n *   4. Text-based: getByText\n *   5. AI Vision: provider-agnostic vision API (openai | claude | copilot | grok | ollama | local)\n *   6. CDPSession AX tree walk (last resort)\n *\n * Provider is controlled by AI_PROVIDER in .env:\n *   openai   — OpenAI GPT-4o or gpt-4-vision-preview\n *   claude   — Anthropic Claude (claude-opus-4-6 or newer)\n *   grok     — xAI Grok vision API\n *   ollama   — Local Ollama with a vision model (e.g. llava, moondream2)\n *   local    — LM Studio or any OpenAI-compatible local server\n *\n * Healed selectors are persisted in LocatorRepository — zero overhead on repeat runs.\n */\nexport class LocatorHealer {\n  private readonly TIMEOUT = 8_000;\n  currentScenario?: string;\n\n  constructor(\n    private readonly page: Page,\n    private readonly logger: HealerLogger,\n    private readonly repo: LocatorRepository,\n  ) {}\n  // ── Extended action verbs ──────────────────────────────────────────────\n  // Every action method follows the same shape: resolve through the heal\n  // chain, drive the underlying Playwright API, then record success + the\n  // opportunistic data-testid upgrade. The healer-emitted call sites in\n  // migrated code call exactly these signatures.\n\n  async checkWithHealing(\n    key: string,\n    selector: string,\n    intent: string,\n  ): Promise<void> {\n    const loc = await this.resolve(key, selector, intent);\n    await loc.check({ timeout: LocatorHealer.TIMEOUT });\n    this.repo.incrementSuccess(key);\n    await this._maybeUpgradeToTestId(key, selector, intent, loc);\n  }\n\n  async uncheckWithHealing(\n    key: string,\n    selector: string,\n    intent: string,\n  ): Promise<void> {\n    const loc = await this.resolve(key, selector, intent);\n    await loc.uncheck({ timeout: LocatorHealer.TIMEOUT });\n    this.repo.incrementSuccess(key);\n    await this._maybeUpgradeToTestId(key, selector, intent, loc);\n  }\n\n  async hoverWithHealing(\n    key: string,\n    selector: string,\n    intent: string,\n  ): Promise<void> {\n    const loc = await this.resolve(key, selector, intent);\n    await loc.hover({ timeout: LocatorHealer.TIMEOUT });\n    this.repo.incrementSuccess(key);\n    await this._maybeUpgradeToTestId(key, selector, intent, loc);\n  }\n\n  async dblclickWithHealing(\n    key: string,\n    selector: string,\n    intent: string,\n  ): Promise<void> {\n    const loc = await this.resolve(key, selector, intent);\n    await loc.dblclick({ timeout: LocatorHealer.TIMEOUT });\n    this.repo.incrementSuccess(key);\n    await this._maybeUpgradeToTestId(key, selector, intent, loc);\n  }\n\n  async focusWithHealing(\n    key: string,\n    selector: string,\n    intent: string,\n  ): Promise<void> {\n    const loc = await this.resolve(key, selector, intent);\n    await loc.focus({ timeout: LocatorHealer.TIMEOUT });\n    this.repo.incrementSuccess(key);\n    await this._maybeUpgradeToTestId(key, selector, intent, loc);\n  }\n\n  async blurWithHealing(\n    key: string,\n    selector: string,\n    intent: string,\n  ): Promise<void> {\n    const loc = await this.resolve(key, selector, intent);\n    await loc.blur({ timeout: LocatorHealer.TIMEOUT });\n    this.repo.incrementSuccess(key);\n    await this._maybeUpgradeToTestId(key, selector, intent, loc);\n  }\n\n  async tapWithHealing(\n    key: string,\n    selector: string,\n    intent: string,\n  ): Promise<void> {\n    const loc = await this.resolve(key, selector, intent);\n    await loc.tap({ timeout: LocatorHealer.TIMEOUT });\n    this.repo.incrementSuccess(key);\n    await this._maybeUpgradeToTestId(key, selector, intent, loc);\n  }\n\n  async selectOptionWithHealing(\n    key: string,\n    selector: string,\n    value:\n      | string\n      | string[]\n      | { value?: string; label?: string; index?: number },\n    intent: string,\n  ): Promise<void> {\n    const loc = await this.resolve(key, selector, intent);\n    await loc.selectOption(value as any, { timeout: LocatorHealer.TIMEOUT });\n    this.repo.incrementSuccess(key);\n    await this._maybeUpgradeToTestId(key, selector, intent, loc);\n  }\n\n  async pressWithHealing(\n    key: string,\n    selector: string,\n    keyToPress: string,\n    intent: string,\n  ): Promise<void> {\n    const loc = await this.resolve(key, selector, intent);\n    await loc.press(keyToPress, { timeout: LocatorHealer.TIMEOUT });\n    this.repo.incrementSuccess(key);\n    await this._maybeUpgradeToTestId(key, selector, intent, loc);\n  }\n\n  async typeWithHealing(\n    key: string,\n    selector: string,\n    text: string,\n    intent: string,\n  ): Promise<void> {\n    const loc = await this.resolve(key, selector, intent);\n    // `Locator.type` is deprecated by Playwright in favour of pressSequentially;\n    // we call pressSequentially so consumers get the supported behaviour even\n    // though the migrated source called `.type(...)`.\n    await loc.pressSequentially(text, { timeout: LocatorHealer.TIMEOUT });\n    this.repo.incrementSuccess(key);\n    await this._maybeUpgradeToTestId(key, selector, intent, loc);\n  }\n\n  async setInputFilesWithHealing(\n    key: string,\n    selector: string,\n    files:\n      | string\n      | string[]\n      | { name: string; mimeType: string; buffer: Buffer },\n    intent: string,\n  ): Promise<void> {\n    const loc = await this.resolve(key, selector, intent);\n    await loc.setInputFiles(files as any, { timeout: LocatorHealer.TIMEOUT });\n    this.repo.incrementSuccess(key);\n    await this._maybeUpgradeToTestId(key, selector, intent, loc);\n  }\n\n  // ── Assertion variants ─────────────────────────────────────────────────\n  // Each `assert*WithHealing` resolves through the heal chain and then uses\n  // Playwright's `expect(locator)` matcher so the assertion gets the framework's\n  // built-in auto-retry/wait behaviour. `assertNot*` variants use `.not.`.\n\n  async assertHiddenWithHealing(\n    key: string,\n    selector: string,\n    intent: string,\n  ): Promise<void> {\n    const loc = await this.resolve(key, selector, intent);\n    await expect(loc).toBeHidden({ timeout: LocatorHealer.HEAL_TO });\n    await this._maybeUpgradeToTestId(key, selector, intent, loc);\n  }\n\n  async assertEnabledWithHealing(\n    key: string,\n    selector: string,\n    intent: string,\n  ): Promise<void> {\n    const loc = await this.resolve(key, selector, intent);\n    await expect(loc).toBeEnabled({ timeout: LocatorHealer.HEAL_TO });\n    await this._maybeUpgradeToTestId(key, selector, intent, loc);\n  }\n\n  async assertDisabledWithHealing(\n    key: string,\n    selector: string,\n    intent: string,\n  ): Promise<void> {\n    const loc = await this.resolve(key, selector, intent);\n    await expect(loc).toBeDisabled({ timeout: LocatorHealer.HEAL_TO });\n    await this._maybeUpgradeToTestId(key, selector, intent, loc);\n  }\n\n  async assertCheckedWithHealing(\n    key: string,\n    selector: string,\n    intent: string,\n  ): Promise<void> {\n    const loc = await this.resolve(key, selector, intent);\n    await expect(loc).toBeChecked({ timeout: LocatorHealer.HEAL_TO });\n    await this._maybeUpgradeToTestId(key, selector, intent, loc);\n  }\n\n  async assertNotCheckedWithHealing(\n    key: string,\n    selector: string,\n    intent: string,\n  ): Promise<void> {\n    const loc = await this.resolve(key, selector, intent);\n    await expect(loc).not.toBeChecked({ timeout: LocatorHealer.HEAL_TO });\n    await this._maybeUpgradeToTestId(key, selector, intent, loc);\n  }\n\n  async assertFocusedWithHealing(\n    key: string,\n    selector: string,\n    intent: string,\n  ): Promise<void> {\n    const loc = await this.resolve(key, selector, intent);\n    await expect(loc).toBeFocused({ timeout: LocatorHealer.HEAL_TO });\n    await this._maybeUpgradeToTestId(key, selector, intent, loc);\n  }\n\n  async assertNotFocusedWithHealing(\n    key: string,\n    selector: string,\n    intent: string,\n  ): Promise<void> {\n    const loc = await this.resolve(key, selector, intent);\n    await expect(loc).not.toBeFocused({ timeout: LocatorHealer.HEAL_TO });\n    await this._maybeUpgradeToTestId(key, selector, intent, loc);\n  }\n\n  async assertEditableWithHealing(\n    key: string,\n    selector: string,\n    intent: string,\n  ): Promise<void> {\n    const loc = await this.resolve(key, selector, intent);\n    await expect(loc).toBeEditable({ timeout: LocatorHealer.HEAL_TO });\n    await this._maybeUpgradeToTestId(key, selector, intent, loc);\n  }\n\n  async assertNotEditableWithHealing(\n    key: string,\n    selector: string,\n    intent: string,\n  ): Promise<void> {\n    const loc = await this.resolve(key, selector, intent);\n    await expect(loc).not.toBeEditable({ timeout: LocatorHealer.HEAL_TO });\n    await this._maybeUpgradeToTestId(key, selector, intent, loc);\n  }\n\n  async assertAttachedWithHealing(\n    key: string,\n    selector: string,\n    intent: string,\n  ): Promise<void> {\n    const loc = await this.resolve(key, selector, intent);\n    await expect(loc).toBeAttached({ timeout: LocatorHealer.HEAL_TO });\n    await this._maybeUpgradeToTestId(key, selector, intent, loc);\n  }\n\n  async assertNotAttachedWithHealing(\n    key: string,\n    selector: string,\n    intent: string,\n  ): Promise<void> {\n    const loc = await this.resolve(key, selector, intent);\n    await expect(loc).not.toBeAttached({ timeout: LocatorHealer.HEAL_TO });\n    await this._maybeUpgradeToTestId(key, selector, intent, loc);\n  }\n\n  async assertInViewportWithHealing(\n    key: string,\n    selector: string,\n    intent: string,\n  ): Promise<void> {\n    const loc = await this.resolve(key, selector, intent);\n    await expect(loc).toBeInViewport({ timeout: LocatorHealer.HEAL_TO });\n    await this._maybeUpgradeToTestId(key, selector, intent, loc);\n  }\n\n  async assertNotInViewportWithHealing(\n    key: string,\n    selector: string,\n    intent: string,\n  ): Promise<void> {\n    const loc = await this.resolve(key, selector, intent);\n    await expect(loc).not.toBeInViewport({ timeout: LocatorHealer.HEAL_TO });\n    await this._maybeUpgradeToTestId(key, selector, intent, loc);\n  }\n\n  async assertValueWithHealing(\n    key: string,\n    selector: string,\n    value: string | RegExp,\n    intent: string,\n  ): Promise<void> {\n    const loc = await this.resolve(key, selector, intent);\n    await expect(loc).toHaveValue(value, { timeout: LocatorHealer.HEAL_TO });\n    await this._maybeUpgradeToTestId(key, selector, intent, loc);\n  }\n\n  async assertNotValueWithHealing(\n    key: string,\n    selector: string,\n    value: string | RegExp,\n    intent: string,\n  ): Promise<void> {\n    const loc = await this.resolve(key, selector, intent);\n    await expect(loc).not.toHaveValue(value, {\n      timeout: LocatorHealer.HEAL_TO,\n    });\n    await this._maybeUpgradeToTestId(key, selector, intent, loc);\n  }\n\n  async assertTextWithHealing(\n    key: string,\n    selector: string,\n    value: string | RegExp | (string | RegExp)[],\n    intent: string,\n  ): Promise<void> {\n    const loc = await this.resolve(key, selector, intent);\n    await expect(loc).toHaveText(value as any, {\n      timeout: LocatorHealer.HEAL_TO,\n    });\n    await this._maybeUpgradeToTestId(key, selector, intent, loc);\n  }\n\n  async assertNotTextWithHealing(\n    key: string,\n    selector: string,\n    value: string | RegExp | (string | RegExp)[],\n    intent: string,\n  ): Promise<void> {\n    const loc = await this.resolve(key, selector, intent);\n    await expect(loc).not.toHaveText(value as any, {\n      timeout: LocatorHealer.HEAL_TO,\n    });\n    await this._maybeUpgradeToTestId(key, selector, intent, loc);\n  }\n\n  async assertContainsTextWithHealing(\n    key: string,\n    selector: string,\n    value: string | RegExp | (string | RegExp)[],\n    intent: string,\n  ): Promise<void> {\n    const loc = await this.resolve(key, selector, intent);\n    await expect(loc).toContainText(value as any, {\n      timeout: LocatorHealer.HEAL_TO,\n    });\n    await this._maybeUpgradeToTestId(key, selector, intent, loc);\n  }\n\n  async assertNotContainsTextWithHealing(\n    key: string,\n    selector: string,\n    value: string | RegExp | (string | RegExp)[],\n    intent: string,\n  ): Promise<void> {\n    const loc = await this.resolve(key, selector, intent);\n    await expect(loc).not.toContainText(value as any, {\n      timeout: LocatorHealer.HEAL_TO,\n    });\n    await this._maybeUpgradeToTestId(key, selector, intent, loc);\n  }\n\n  async assertCountWithHealing(\n    key: string,\n    selector: string,\n    count: number,\n    intent: string,\n  ): Promise<void> {\n    const loc = await this.resolve(key, selector, intent);\n    await expect(loc).toHaveCount(count, { timeout: LocatorHealer.HEAL_TO });\n    await this._maybeUpgradeToTestId(key, selector, intent, loc);\n  }\n\n  async assertNotCountWithHealing(\n    key: string,\n    selector: string,\n    count: number,\n    intent: string,\n  ): Promise<void> {\n    const loc = await this.resolve(key, selector, intent);\n    await expect(loc).not.toHaveCount(count, {\n      timeout: LocatorHealer.HEAL_TO,\n    });\n    await this._maybeUpgradeToTestId(key, selector, intent, loc);\n  }\n\n  async assertClassWithHealing(\n    key: string,\n    selector: string,\n    value: string | RegExp | (string | RegExp)[],\n    intent: string,\n  ): Promise<void> {\n    const loc = await this.resolve(key, selector, intent);\n    await expect(loc).toHaveClass(value as any, {\n      timeout: LocatorHealer.HEAL_TO,\n    });\n    await this._maybeUpgradeToTestId(key, selector, intent, loc);\n  }\n\n  async assertNotClassWithHealing(\n    key: string,\n    selector: string,\n    value: string | RegExp | (string | RegExp)[],\n    intent: string,\n  ): Promise<void> {\n    const loc = await this.resolve(key, selector, intent);\n    await expect(loc).not.toHaveClass(value as any, {\n      timeout: LocatorHealer.HEAL_TO,\n    });\n    await this._maybeUpgradeToTestId(key, selector, intent, loc);\n  }\n\n  async assertIdWithHealing(\n    key: string,\n    selector: string,\n    value: string | RegExp,\n    intent: string,\n  ): Promise<void> {\n    const loc = await this.resolve(key, selector, intent);\n    await expect(loc).toHaveId(value, { timeout: LocatorHealer.HEAL_TO });\n    await this._maybeUpgradeToTestId(key, selector, intent, loc);\n  }\n\n  async assertNotIdWithHealing(\n    key: string,\n    selector: string,\n    value: string | RegExp,\n    intent: string,\n  ): Promise<void> {\n    const loc = await this.resolve(key, selector, intent);\n    await expect(loc).not.toHaveId(value, { timeout: LocatorHealer.HEAL_TO });\n    await this._maybeUpgradeToTestId(key, selector, intent, loc);\n  }\n\n  async assertAttributeWithHealing(\n    key: string,\n    selector: string,\n    name: string,\n    value: string | RegExp,\n    intent: string,\n  ): Promise<void> {\n    const loc = await this.resolve(key, selector, intent);\n    await expect(loc).toHaveAttribute(name, value, {\n      timeout: LocatorHealer.HEAL_TO,\n    });\n    await this._maybeUpgradeToTestId(key, selector, intent, loc);\n  }\n\n  async assertNotAttributeWithHealing(\n    key: string,\n    selector: string,\n    name: string,\n    value: string | RegExp,\n    intent: string,\n  ): Promise<void> {\n    const loc = await this.resolve(key, selector, intent);\n    await expect(loc).not.toHaveAttribute(name, value, {\n      timeout: LocatorHealer.HEAL_TO,\n    });\n    await this._maybeUpgradeToTestId(key, selector, intent, loc);\n  }\n\n  async assertCSSWithHealing(\n    key: string,\n    selector: string,\n    name: string,\n    value: string | RegExp,\n    intent: string,\n  ): Promise<void> {\n    const loc = await this.resolve(key, selector, intent);\n    await expect(loc).toHaveCSS(name, value, {\n      timeout: LocatorHealer.HEAL_TO,\n    });\n    await this._maybeUpgradeToTestId(key, selector, intent, loc);\n  }\n\n  async assertNotCSSWithHealing(\n    key: string,\n    selector: string,\n    name: string,\n    value: string | RegExp,\n    intent: string,\n  ): Promise<void> {\n    const loc = await this.resolve(key, selector, intent);\n    await expect(loc).not.toHaveCSS(name, value, {\n      timeout: LocatorHealer.HEAL_TO,\n    });\n    await this._maybeUpgradeToTestId(key, selector, intent, loc);\n  }\n\n\n  async resolve(key: string, primarySelector: string, intent: string): Promise<Locator> {\n    const cached = this.repo.getHealed(key);\n    if (cached) {\n      const loc = this.page.locator(cached);\n      if (await this.isVisible(loc)) return loc;\n      this.logger.warn(`[LocatorHealer] Cached heal stale for \"${key}\", re-healing`);\n    }\n\n    {\n      const loc = this.page.locator(primarySelector);\n      if (await this.isVisible(loc)) return loc;\n      this.logger.warn(`[LocatorHealer] Primary selector failed for \"${key}\": ${primarySelector}`);\n    }\n\n    const aiLocator = await this.healByAiVision(key, intent, primarySelector);\n    if (aiLocator) return aiLocator;\n\n    const axLocator = await this.healByAxTree(key, intent, primarySelector);\n    if (axLocator) return axLocator;\n\n    throw new Error(\n      `[LocatorHealer] All healing strategies exhausted for \"${key}\" (intent: \"${intent}\").\\n` +\n      `Check HealingDashboard at http://localhost:${process.env.HEALING_DASHBOARD_PORT ?? 7890}`,\n    );\n  }\n\n  async clickWithHealing(key: string, selector: string, intent: string): Promise<void> {\n    const loc = await this.resolve(key, selector, intent);\n    await loc.waitFor({ state: 'visible', timeout: this.TIMEOUT });\n    await loc.click();\n    this.logger.info(`[LocatorHealer] click \"${key}\"`);\n  }\n\n  async fillWithHealing(key: string, selector: string, value: string, intent: string): Promise<void> {\n    const loc = await this.resolve(key, selector, intent);\n    await loc.waitFor({ state: 'visible', timeout: this.TIMEOUT });\n\n    const inputType = await loc.evaluate((el) => {\n      const tag = (el as HTMLElement).tagName.toLowerCase();\n      if (tag !== 'input' && tag !== 'textarea') return 'non-input';\n      return (el as HTMLInputElement).type ?? 'text';\n    });\n\n    const nonFillableTypes = ['submit', 'button', 'reset', 'image', 'checkbox', 'radio', 'file'];\n    if (nonFillableTypes.includes(inputType) || inputType === 'non-input') {\n      this.logger.warn(`[LocatorHealer] Healed locator for \"${key}\" resolved to non-fillable element — re-healing`);\n      this.repo.evict(key);\n      const keyLabel = key.replace(/[_-]?(textbox|input|field|box)$/i, '').replace(/_/g, ' ').trim();\n      const roleLoc = await (async () => {\n        try {\n          const l = this.page.getByRole('textbox', { name: keyLabel, exact: false });\n          if (await this.isVisible(l)) return l;\n        } catch { /* continue */ }\n        try {\n          const l = this.page.getByPlaceholder(keyLabel, { exact: false });\n          if (await this.isVisible(l)) return l;\n        } catch { /* continue */ }\n        try {\n          const l = this.page.getByLabel(keyLabel, { exact: false });\n          if (await this.isVisible(l)) return l;\n        } catch { /* continue */ }\n        return null;\n      })();\n      if (!roleLoc) throw new Error(`[LocatorHealer] Cannot find fillable element for \"${key}\" (intent: \"${intent}\")`);\n      await roleLoc.fill(value);\n      this.persist(key, roleLoc, 'role');\n      return;\n    }\n\n    await loc.fill(value);\n    this.logger.info(`[LocatorHealer] fill \"${key}\" = \"${value}\"`);\n  }\n\n  // ── Role-aware variants ──────────────────────────────────────────────\n  // Migrated `getByRole(role, { name })` chains route here. Playwright has NO\n  // `role=` selector engine, so the primary locator MUST go through the real\n  // `page.getByRole(role, { name })` API. We try that first and only fall back\n  // to the shared heal chain (`resolve(key, '', intent)`) if it does not\n  // resolve. Logging + heal persistence stay identical to the sibling methods.\n\n  private async resolveByRole(key: string, role: string, name: string, intent: string): Promise<Locator> {\n    try {\n      const primary = name ? this.page.getByRole(role as any, { name }) : this.page.getByRole(role as any);\n      if (await this.isVisible(primary)) return primary;\n      this.logger.warn(`[LocatorHealer] Primary role locator failed for \"${key}\": getByRole(${role}` + (name ? `, { name: \"${name}\" }` : '') + `)`);\n    } catch (err) {\n      this.logger.warn(`[LocatorHealer] Primary role locator threw for \"${key}\": ${String(err)}`);\n    }\n    return this.resolve(key, '', intent);\n  }\n\n  async clickByRoleWithHealing(key: string, role: string, name: string, intent: string): Promise<void> {\n    const loc = await this.resolveByRole(key, role, name, intent);\n    await loc.waitFor({ state: 'visible', timeout: this.TIMEOUT });\n    await loc.click();\n    this.logger.info(`[LocatorHealer] click \"${key}\" (role=${role}` + (name ? `, name=\"${name}\"` : '') + `)`);\n  }\n\n  async fillByRoleWithHealing(key: string, role: string, name: string, value: string, intent: string): Promise<void> {\n    const loc = await this.resolveByRole(key, role, name, intent);\n    await loc.waitFor({ state: 'visible', timeout: this.TIMEOUT });\n    await loc.fill(value);\n    this.logger.info(`[LocatorHealer] fill \"${key}\" = \"${value}\" (role=${role}` + (name ? `, name=\"${name}\"` : '') + `)`);\n  }\n\n  async assertVisibleWithHealing(key: string, selector: string, intent: string): Promise<void> {\n    const loc = await this.resolve(key, selector, intent);\n    await loc.waitFor({ state: 'visible', timeout: this.TIMEOUT });\n    this.logger.info(`[LocatorHealer] assertVisible \"${key}\" ✓`);\n  }\n\n  async assertHiddenWithHealing(key: string, selector: string, _intent: string): Promise<void> {\n    const loc = this.page.locator(selector);\n    await loc.waitFor({ state: 'hidden', timeout: this.TIMEOUT });\n    this.logger.info(`[LocatorHealer] assertHidden \"${key}\" ✓`);\n  }\n\n  async getLocator(key: string, selector: string, intent: string): Promise<Locator> {\n    return this.resolve(key, selector, intent);\n  }\n\n  /**\n   * AI Vision healing — supports openai | claude | grok | ollama | local\n   *\n   * Configured via .env:\n   *   AI_PROVIDER         = openai | claude | copilot | grok | ollama | local\n   *   AI_API_KEY          = your API key (not needed for ollama/local/copilot)\n   *   GITHUB_TOKEN        = Copilot Chat OAuth token (used when AI_PROVIDER=copilot)\n   *   AI_MODEL            = model name override\n   *   LOCAL_LLM_ENDPOINT  = base URL for ollama or LM Studio\n   */\n  private async healByAiVision(key: string, intent: string, originalSelector = ''): Promise<Locator | null> {\n    const provider = (process.env.AI_PROVIDER ?? 'openai').toLowerCase();\n    // Copilot uses GITHUB_TOKEN (an OAuth Copilot Chat token, NOT a PAT —\n    // see scripts/get-copilot-token.ts for extraction from VS Code keychain).\n    // Every other remote provider uses AI_API_KEY (or ANTHROPIC_API_KEY).\n    const apiKey   = provider === 'copilot'\n      ? process.env.GITHUB_TOKEN\n      : (process.env.AI_API_KEY || process.env.ANTHROPIC_API_KEY);\n    const needsKey = !['ollama', 'local'].includes(provider);\n    if (needsKey && !apiKey) {\n      const envName = provider === 'copilot' ? 'GITHUB_TOKEN' : 'AI_API_KEY';\n      this.logger.warn(`[LocatorHealer] AI Vision skipped for \"${key}\" — ${envName} not set`);\n      return null;\n    }\n\n    const prompt =\n      `You are helping a Playwright test bind to an element on a real web page.\\n` +\n      `The selector you return will be cached and reused across many test runs,\\n` +\n      `so it must work for ANY instance of the intent — not just the one in this screenshot.\\n\\n` +\n      `Find the element matching this intent: \"${intent}\"\\n\\n` +\n      `Rules:\\n` +\n      `- Return ONE Playwright-compatible CSS selector. Nothing else. No prose, no markdown, no backticks.\\n` +\n      `- For text matching use ':has-text(\"...\")'. NEVER use jQuery's ':contains(...)' — invalid CSS.\\n` +\n      `- Prefer stable attributes in this order: data-testid > id > aria-label > role+name > href shape > class.\\n` +\n      `- GENERALISE dynamic attribute values so the selector survives across runs:\\n` +\n      `    * numeric IDs in URLs: use 'a[href*=\"/edit/\"]' NOT 'a[href=\"/edit/189332\"]'\\n` +\n      `    * generated ids ('mui-1234'): use '[id^=\"mui-\"]' NOT '[id=\"mui-1234\"]'\\n` +\n      `    * hash / token suffixes: prefer '^=' / '*=' over '='\\n` +\n      `- If multiple elements would match, scope with a unique ANCESTOR so it picks ONE.\\n` +\n      `- The selector must target the element a USER would click / interact with for that intent.`;\n\n    try {\n      const screenshotBuffer = await this.page.screenshot({ fullPage: false });\n      const base64Screenshot = screenshotBuffer.toString('base64');\n      let selector: string | undefined;\n\n      if (provider === 'openai') {\n        const res = await fetch('https://api.openai.com/v1/chat/completions', {\n          method: 'POST',\n          headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}` },\n          body: JSON.stringify({\n            model: process.env.AI_MODEL ?? 'gpt-4o',\n            max_tokens: 256,\n            messages: [{ role: 'user', content: [\n              { type: 'image_url', image_url: { url: `data:image/png;base64,${base64Screenshot}` } },\n              { type: 'text', text: prompt },\n            ]}],\n          }),\n        });\n        if (!res.ok) { this.logger.warn(`[LocatorHealer] OpenAI error ${res.status}`); return null; }\n        const data = await res.json() as { choices: Array<{ message: { content: string } }> };\n        selector = data.choices[0]?.message?.content?.trim();\n      }\n\n      else if (provider === 'claude' || provider === 'anthropic') {\n        const res = await fetch('https://api.anthropic.com/v1/messages', {\n          method: 'POST',\n          headers: { 'Content-Type': 'application/json', 'x-api-key': apiKey!, 'anthropic-version': '2023-06-01' },\n          body: JSON.stringify({\n            model: process.env.AI_MODEL ?? 'claude-opus-4-6',\n            max_tokens: 256,\n            messages: [{ role: 'user', content: [\n              { type: 'image', source: { type: 'base64', media_type: 'image/png', data: base64Screenshot } },\n              { type: 'text', text: prompt },\n            ]}],\n          }),\n        });\n        if (!res.ok) { this.logger.warn(`[LocatorHealer] Claude error ${res.status}`); return null; }\n        const data = await res.json() as { content: Array<{ type: string; text: string }> };\n        selector = data.content.find(b => b.type === 'text')?.text?.trim();\n      }\n\n      else if (provider === 'copilot') {\n        // GitHub Copilot Chat — requires an OAuth Copilot Chat token (PATs are\n        // rejected with HTTP 400) AND IDE-identification headers on every call.\n        const res = await fetch('https://api.githubcopilot.com/chat/completions', {\n          method: 'POST',\n          headers: {\n            'Content-Type': 'application/json',\n            'Authorization': `Bearer ${apiKey}`,\n            'Editor-Version': 'vscode/1.95.0',\n            'Editor-Plugin-Version': 'copilot-chat/0.20.0',\n            'Copilot-Integration-Id': 'vscode-chat',\n            'User-Agent': 'GitHubCopilotChat/0.20.0',\n            'OpenAI-Intent': 'conversation-panel',\n          },\n          body: JSON.stringify({\n            model: process.env.AI_MODEL ?? 'gpt-4o',\n            max_tokens: 256,\n            messages: [{ role: 'user', content: [\n              { type: 'image_url', image_url: { url: `data:image/png;base64,${base64Screenshot}` } },\n              { type: 'text', text: prompt },\n            ]}],\n          }),\n        });\n        if (!res.ok) { this.logger.warn(`[LocatorHealer] Copilot error ${res.status}`); return null; }\n        const data = await res.json() as { choices: Array<{ message: { content: string } }> };\n        selector = data.choices[0]?.message?.content?.trim();\n      }\n\n      else if (provider === 'grok') {\n        const res = await fetch('https://api.x.ai/v1/chat/completions', {\n          method: 'POST',\n          headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}` },\n          body: JSON.stringify({\n            model: process.env.AI_MODEL ?? 'grok-2-vision-latest',\n            max_tokens: 256,\n            messages: [{ role: 'user', content: [\n              { type: 'image_url', image_url: { url: `data:image/png;base64,${base64Screenshot}` } },\n              { type: 'text', text: prompt },\n            ]}],\n          }),\n        });\n        if (!res.ok) { this.logger.warn(`[LocatorHealer] Grok error ${res.status}`); return null; }\n        const data = await res.json() as { choices: Array<{ message: { content: string } }> };\n        selector = data.choices[0]?.message?.content?.trim();\n      }\n\n      else if (provider === 'ollama') {\n        const baseUrl = (process.env.OLLAMA_ENDPOINT || process.env.LOCAL_LLM_ENDPOINT || 'http://localhost:11434').replace(/\\/$/, '');\n        const model = process.env.OLLAMA_MODEL || process.env.AI_MODEL || 'llava';\n        const res = await fetch(`${baseUrl}/api/chat`, {\n          method: 'POST',\n          headers: { 'Content-Type': 'application/json' },\n          body: JSON.stringify({ model, stream: false, messages: [{ role: 'user', content: prompt, images: [base64Screenshot] }] }),\n        });\n        if (!res.ok) { this.logger.warn(`[LocatorHealer] Ollama error ${res.status}`); return null; }\n        const data = await res.json() as { message?: { content: string } };\n        selector = data.message?.content?.trim();\n      }\n\n      else if (provider === 'local') {\n        const baseUrl = (process.env.LM_STUDIO_ENDPOINT || process.env.LOCAL_LLM_ENDPOINT || 'http://localhost:1234').replace(/\\/$/, '');\n        const model = process.env.LM_STUDIO_MODEL || process.env.AI_MODEL || 'local-model';\n        const res = await fetch(`${baseUrl}/v1/chat/completions`, {\n          method: 'POST',\n          headers: { 'Content-Type': 'application/json' },\n          body: JSON.stringify({ model, max_tokens: 256, messages: [{ role: 'user', content: [\n            { type: 'image_url', image_url: { url: `data:image/png;base64,${base64Screenshot}` } },\n            { type: 'text', text: prompt },\n          ]}] }),\n        });\n        if (!res.ok) { this.logger.warn(`[LocatorHealer] LM Studio error ${res.status}`); return null; }\n        const data = await res.json() as { choices: Array<{ message: { content: string } }> };\n        selector = data.choices[0]?.message?.content?.trim();\n      }\n\n      else {\n        this.logger.warn(`[LocatorHealer] Unknown AI_PROVIDER \"${provider}\"`);\n        return null;\n      }\n\n      if (!selector) return null;\n      // ── Sanitise model output ─────────────────────────────────────────\n      // LLMs frequently dress selectors in markdown or jQuery syntax that\n      // Playwright's locator parser rejects. Strip wrappers and translate.\n      selector = selector.trim();\n      const fence = selector.match(/^```(?:[a-zA-Z]+)?\\s*\\n([\\s\\S]*?)\\n```$/);\n      if (fence) selector = fence[1].trim();\n      if (selector.startsWith('`') && selector.endsWith('`')) selector = selector.slice(1, -1).trim();\n      if ((selector.startsWith('\"') && selector.endsWith('\"')) || (selector.startsWith(\"'\") && selector.endsWith(\"'\"))) {\n        const inner = selector.slice(1, -1);\n        if (!inner.includes(selector[0])) selector = inner.trim();\n      }\n      selector = selector.replace(/^(?:selector|css)\\s*[:=]\\s*/i, '').trim();\n      // jQuery `:contains(...)` → Playwright `:has-text(...)`; empty is dropped.\n      selector = selector.replace(/:contains\\(\\s*([\"'])\\s*\\1\\s*\\)/g, '');\n      selector = selector.replace(/:contains\\(/g, ':has-text(');\n      // Normalise single-quoted attribute selectors to double quotes.\n      selector = selector.replace(/\\[([^\\]='\"\\s]+)='([^']*)'\\]/g, '[$1=\"$2\"]');\n      this.logger.warn(`[LocatorHealer] AI Vision (${provider}) suggested: ${selector}`);\n      // Prefer the first VISIBLE match — AI selectors are often correct but\n      // ambiguous (e.g. same anchor in mobile+desktop layouts). Strict-mode\n      // violations would otherwise throw away a valid heal.\n      const loc = this.page.locator(selector).locator('visible=true').first();\n      if (await this.isVisible(loc)) {\n        this.persist(key, loc, 'ai-vision', intent, originalSelector, selector);\n        return loc;\n      }\n    } catch (err) {\n      this.logger.warn(`[LocatorHealer] AI Vision failed for \"${key}\": ${String(err)}`);\n    }\n    return null;\n  }\n\n  private async healByAxTree(key: string, intent: string, originalSelector = ''): Promise<Locator | null> {\n    const INTERACTIVE_ROLES = new Set([\n      'textbox', 'searchbox', 'spinbutton', 'combobox', 'listbox',\n      'button', 'link', 'checkbox', 'radio', 'menuitem', 'tab', 'switch',\n      'slider', 'option', 'treeitem', 'gridcell',\n    ]);\n\n    try {\n      const client = await (this.page.context() as unknown as { newCDPSession(page: Page): Promise<{ send(cmd: string): Promise<{ nodes: unknown[] }>; detach(): Promise<void> }> }).newCDPSession(this.page);\n      const { nodes } = await client.send('Accessibility.getFullAXTree');\n      await client.detach();\n\n      const intentWords = intent.toLowerCase().split(/\\s+/).filter(w => w.length > 2);\n      let best: { node: Record<string, unknown>; score: number } | null = null;\n\n      for (const n of (nodes as Record<string, Record<string, string>>[]) ) {\n        const role = (n['role']?.['value'] ?? '').toLowerCase();\n        const name = (n['name']?.['value'] ?? '').toLowerCase();\n        if (!INTERACTIVE_ROLES.has(role) || !name) continue;\n        const hasWordMatch = intentWords.some(word => new RegExp(`\\\\b${word}\\\\b`).test(name));\n        if (!hasWordMatch) continue;\n        const score = intentWords.filter(word => new RegExp(`\\\\b${word}\\\\b`).test(name)).length;\n        if (!best || score > best.score) best = { node: n as unknown as Record<string, unknown>, score };\n      }\n\n      if (best?.node) {\n        const role = (best.node['role'] as Record<string, string>)?.['value'];\n        const name = (best.node['name'] as Record<string, string>)?.['value'];\n        if (role && name) {\n          const axLoc = this.page.getByRole(role as Parameters<Page['getByRole']>[0], { name, exact: false });\n          if (await this.isVisible(axLoc)) {\n            this.persist(key, axLoc, 'ax-tree', intent, originalSelector, `[role=\"${role}\"][name=\"${name}\"]`);\n            return axLoc;\n          }\n        }\n      }\n    } catch (err) {\n      this.logger.warn(`[LocatorHealer] AX tree healing failed: ${String(err)}`);\n    }\n    return null;\n  }\n\n  private async isVisible(loc: Locator): Promise<boolean> {\n    try {\n      return await loc.isVisible({ timeout: 2_000 });\n    } catch {\n      return false;\n    }\n  }\n\n  private persist(key: string, _loc: Locator, strategy: string, intent = '', originalSelector = '', healedSelector?: string): void {\n    const provider = strategy === 'ai-vision' ? (process.env.AI_PROVIDER ?? 'openai') : undefined;\n    if (healedSelector) {\n      try {\n        if (!this.repo.getHealed(key) && originalSelector) {\n          this.repo.register(key, originalSelector, intent);\n        }\n        this.repo.setHealed(key, healedSelector, strategy, provider);\n        this.logger.info(`[LocatorHealer] 💾 Healed selector saved for \"${key}\" → ${healedSelector}`);\n      } catch (err) {\n        this.logger.warn(`[LocatorHealer] Could not persist heal for \"${key}\": ${String(err)}`);\n      }\n    }\n    try {\n      HealingDashboard.getInstance().record({\n        key,\n        originalSelector,\n        strategy,\n        provider,\n        healedSelector,\n        intent,\n        scenario: this.currentScenario,\n        timestamp: new Date().toISOString(),\n      });\n    } catch { /* dashboard may not be running */ }\n  }\n}\n",
    "src/utils/locators/LocatorManager.ts": "import { Page } from '@playwright/test';\nimport { logger } from '@utils/helpers/Logger';\nimport { LocatorStrategy, LocatorConfig } from './LocatorStrategy';\nimport { AISelfHealing } from '@utils/ai-assistant/AISelfHealing';\nimport { environment } from '@config/environment';\nimport * as fs from 'fs';\n\nexport type { LocatorConfig };\n\nexport interface LocatorDefinition {\n  primary:    LocatorConfig;\n  fallbacks?: LocatorConfig[];\n}\n\nexport interface LocatorMap {\n  [key: string]: LocatorDefinition;\n}\n\n/**\n * LocatorManager\n *\n * Centralised registry for page/component locator maps.\n * On lookup, tries primary → fallbacks → AISelfHealing.\n *\n * Usage (page object constructor):\n *   this.locatorManager.registerLocators('LoginPage', LOGIN_LOCATORS, loginLocatorsFilePath);\n *\n * Usage (page method):\n *   const loc = await this.locatorManager.getLocator('LoginPage', 'USERNAME_INPUT');\n */\nexport class LocatorManager {\n  private readonly locatorStrategy: LocatorStrategy;\n  private readonly locatorMaps     = new Map<string, LocatorMap>();\n  private readonly fileCache       = new Map<string, string>();\n  private readonly selfHealing:    AISelfHealing;\n  private readonly env = environment.getConfig();\n\n  constructor(private readonly page: Page) {\n    this.locatorStrategy = new LocatorStrategy(page);\n    this.selfHealing     = new AISelfHealing(page);\n  }\n\n  registerLocators(name: string, locators: LocatorMap, filePath?: string): void {\n    this.locatorMaps.set(name, locators);\n    if (filePath) this.fileCache.set(name, filePath);\n    logger.debug(`Locators registered for: ${name} (${Object.keys(locators).length} keys)`);\n  }\n\n  async getLocator(pageName: string, locatorKey: string): Promise<import('@playwright/test').Locator | null> {\n    const locatorMap = this.locatorMaps.get(pageName);\n    if (!locatorMap) { logger.warn(`Locator map not found for page: ${pageName}`); return null; }\n    const definition = locatorMap[locatorKey];\n    if (!definition) { logger.warn(`Locator key not found: ${locatorKey} in ${pageName}`); return null; }\n\n    const loc = await this.tryStrategies(definition, pageName, locatorKey);\n    if (loc) return loc;\n\n    if (this.env.enableSelfHealing) {\n      return this.attemptSelfHealing(pageName, locatorKey, definition);\n    }\n\n    logger.warn(`All strategies failed for ${locatorKey} on ${pageName}`);\n    return null;\n  }\n\n  private async tryStrategies(def: LocatorDefinition, pageName: string, key: string): Promise<import('@playwright/test').Locator | null> {\n    for (const strategy of [def.primary, ...(def.fallbacks ?? [])]) {\n      try {\n        const loc = this.locatorStrategy.getLocator(strategy);\n        await loc.waitFor({ timeout: 2_000 });\n        logger.debug(`Found via ${strategy.strategy}`, { page: pageName, key });\n        return loc;\n      } catch { /* try next */ }\n    }\n    return null;\n  }\n\n  private async attemptSelfHealing(pageName: string, key: string, def: LocatorDefinition): Promise<import('@playwright/test').Locator | null> {\n    logger.info(`Self-healing \"${key}\" on ${pageName}`);\n    try {\n      const report = await this.selfHealing.heal(\n        def.primary.value,\n        key.replace(/_/g, ' ').toLowerCase(),\n        { elementType: this.inferElementType(key), context: pageName, maxAttempts: this.env.locatorHealAttempts ?? 3 },\n      );\n      if (report.success && report.healedSelector) {\n        logger.info(`Healed via ${report.method}`, { key, selector: report.healedSelector });\n        this.updateDefinition(pageName, key, report.healedSelector);\n        return this.page.locator(report.healedSelector);\n      }\n    } catch (err) {\n      logger.error(`Healing error for \"${key}\": ${String(err)}`);\n    }\n    return null;\n  }\n\n  private updateDefinition(pageName: string, key: string, healedSelector: string): void {\n    const def = this.locatorMaps.get(pageName)?.[key];\n    if (!def) return;\n    def.primary = { strategy: 'css', value: healedSelector };\n    try {\n      const filePath = this.fileCache.get(pageName);\n      if (filePath && fs.existsSync(filePath)) {\n        const content = fs.readFileSync(filePath, 'utf-8');\n        const updated = content.replace(\n          new RegExp(`(\\\\[?${key}[\\\\]:]?)([^}]*?)value:\\\\s*['\"][^'\"]*['\"]`, 'g'),\n          `$1$2value: '${healedSelector.replace(/'/g, \"\\\\'\")}'`,\n        );\n        fs.writeFileSync(filePath, updated);\n      }\n    } catch (err) {\n      logger.warn(`Could not persist healed locator for \"${key}\": ${String(err)}`);\n    }\n  }\n\n  getStrategies(pageName: string, locatorKey: string): LocatorConfig[] {\n    const def = this.locatorMaps.get(pageName)?.[locatorKey];\n    if (!def) return [];\n    return [def.primary, ...(def.fallbacks ?? [])];\n  }\n\n  getDynamicLocator(pageName: string, locatorKey: string, params: Record<string, string>): LocatorConfig | null {\n    const def = this.locatorMaps.get(pageName)?.[locatorKey];\n    if (!def) return null;\n    let value = def.primary.value;\n    for (const [k, v] of Object.entries(params)) value = value.replace(`{${k}}`, v);\n    return { ...def.primary, value };\n  }\n\n  getRegisteredPages(): string[] { return [...this.locatorMaps.keys()]; }\n  getPageLocators(pageName: string): string[] {\n    const map = this.locatorMaps.get(pageName);\n    return map ? Object.keys(map) : [];\n  }\n\n  private inferElementType(key: string): string {\n    const k = key.toUpperCase();\n    const map: Record<string, string> = { BUTTON: 'button', CLICK: 'button', INPUT: 'input', FIELD: 'input', LINK: 'link', ANCHOR: 'link', CHECKBOX: 'checkbox', RADIO: 'radio', SELECT: 'select', DROPDOWN: 'select', LABEL: 'label', HEADING: 'heading', TITLE: 'heading', IMAGE: 'img', ICON: 'img' };\n    for (const [pattern, type] of Object.entries(map)) { if (k.includes(pattern)) return type; }\n    return 'button';\n  }\n}\n",
    "src/utils/locators/LocatorRepository.ts": "import * as fs   from 'fs';\nimport * as path from 'path';\n\nconst HEAL_STORE_PATH = path.resolve(\n  process.env.HEAL_STORE_PATH ?? 'self-heal/healed-locators.json',\n);\n\ninterface HealRecord {\n  healedSelector:  string;\n  originalSelector: string;\n  strategy:         string;\n  provider?:        string;\n  intent:           string;\n  healCount:        number;\n  lastHealedAt:     string;\n  approved?:        boolean;\n}\n\nfunction loadHealStore(): Record<string, HealRecord> {\n  try {\n    if (!fs.existsSync(HEAL_STORE_PATH)) return {};\n    const raw = fs.readFileSync(HEAL_STORE_PATH, 'utf8');\n    return JSON.parse(raw) as Record<string, HealRecord>;\n  } catch {\n    return {};\n  }\n}\n\nfunction persistHeal(key: string, record: HealRecord): void {\n  try {\n    const dir = path.dirname(HEAL_STORE_PATH);\n    if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });\n    const store = loadHealStore();\n    store[key] = record;\n    fs.writeFileSync(HEAL_STORE_PATH, JSON.stringify(store, null, 2), 'utf8');\n  } catch (err) {\n    console.warn(`[LocatorRepository] Could not write heal store: ${String(err)}`);\n  }\n}\n\nfunction evictHeal(key: string): void {\n  try {\n    if (!fs.existsSync(HEAL_STORE_PATH)) return;\n    const store = loadHealStore();\n    if (key in store) {\n      delete store[key];\n      fs.writeFileSync(HEAL_STORE_PATH, JSON.stringify(store, null, 2), 'utf8');\n    }\n  } catch { /* non-fatal */ }\n}\n\nexport interface LocatorEntry {\n  key:             string;\n  selector:        string;\n  intent:          string;\n  healedSelector?: string;\n  healStrategy?:   string;\n  healProvider?:   string;\n  healCount:       number;\n  lastHealedAt?:   Date;\n  id?:             string;\n  name?:           string;\n  page?:           string;\n  locator?:        string;\n  strategy?:       string;\n  value?:          string;\n  options?:        Record<string, unknown>;\n  metadata?:       Record<string, unknown>;\n}\n\nexport class LocatorRepository {\n  private static _instance: LocatorRepository | null = null;\n\n  static getInstance(): LocatorRepository {\n    if (!LocatorRepository._instance) {\n      LocatorRepository._instance = new LocatorRepository();\n    }\n    return LocatorRepository._instance;\n  }\n\n  static resetInstance(): void {\n    LocatorRepository._instance = null;\n  }\n\n  private readonly entries = new Map<string, LocatorEntry>();\n\n  register(key: string, selector: string, intent: string): void {\n    if (!this.entries.has(key)) {\n      const entry: LocatorEntry = { key, selector, intent, healCount: 0 };\n      const store = loadHealStore();\n      if (store[key]) {\n        entry.healedSelector = store[key].healedSelector;\n        entry.healStrategy   = store[key].strategy;\n        entry.healProvider   = store[key].provider;\n        entry.healCount      = store[key].healCount;\n        entry.lastHealedAt   = new Date(store[key].lastHealedAt);\n      }\n      this.entries.set(key, entry);\n    }\n  }\n\n  getBestSelector(key: string): string {\n    const entry = this.entries.get(key);\n    if (!entry) throw new Error(`LocatorRepository: key \"${key}\" not registered`);\n    return entry.healedSelector ?? entry.selector;\n  }\n\n  getHealed(key: string): string | null {\n    return this.entries.get(key)?.healedSelector ?? null;\n  }\n\n  setHealed(key: string, healedSelector: string, strategy = 'unknown', provider?: string): void {\n    const entry = this.entries.get(key);\n    if (!entry) {\n      this.entries.set(key, { key, selector: '', intent: key, healCount: 0 });\n    }\n    const e = this.entries.get(key)!;\n    e.healedSelector = healedSelector;\n    e.healStrategy   = strategy;\n    e.healProvider   = provider;\n    e.healCount++;\n    e.lastHealedAt = new Date();\n    persistHeal(key, {\n      healedSelector,\n      originalSelector: e.selector,\n      strategy,\n      provider,\n      intent:      e.intent,\n      healCount:   e.healCount,\n      lastHealedAt: e.lastHealedAt.toISOString(),\n    });\n  }\n\n  evict(key: string): void {\n    const entry = this.entries.get(key);\n    if (entry) {\n      entry.healedSelector = undefined;\n      entry.healStrategy   = undefined;\n      entry.healProvider   = undefined;\n    }\n    evictHeal(key);\n  }\n\n  getIntent(key: string): string {\n    const entry = this.entries.get(key);\n    if (!entry) throw new Error(`LocatorRepository: key \"${key}\" not registered`);\n    return entry.intent;\n  }\n\n  getByName(name: string): LocatorEntry | undefined {\n    for (const entry of this.entries.values()) {\n      if (entry.name === name || entry.key === name) return entry;\n    }\n    const lower = name.toLowerCase();\n    for (const entry of this.entries.values()) {\n      if (\n        (entry.name ?? '').toLowerCase() === lower ||\n        entry.key.toLowerCase() === lower\n      ) return entry;\n    }\n    return undefined;\n  }\n\n  update(partial: Partial<LocatorEntry> & { id: string }): void {\n    const key = partial.id;\n    const existing = this.entries.get(key);\n    if (existing) {\n      Object.assign(existing, partial);\n      if (partial.locator) {\n        existing.healedSelector = partial.locator;\n        existing.healCount      = (existing.healCount ?? 0) + 1;\n        existing.lastHealedAt   = new Date();\n      }\n    } else {\n      this.entries.set(key, {\n        key,\n        selector: partial.selector ?? partial.locator ?? '',\n        intent:   partial.name ?? key,\n        healCount: 0,\n        ...partial,\n        ...(partial.locator ? { healedSelector: partial.locator } : {}),\n      });\n    }\n  }\n\n  getAll(): LocatorEntry[] {\n    return Array.from(this.entries.values());\n  }\n\n  clearHealed(): void {\n    for (const entry of this.entries.values()) {\n      delete entry.healedSelector;\n      entry.healCount = 0;\n      delete entry.lastHealedAt;\n    }\n  }\n\n  summary(): { total: number; healed: number; healRate: string } {\n    const total  = this.entries.size;\n    const healed = Array.from(this.entries.values()).filter(e => e.healedSelector).length;\n    return {\n      total,\n      healed,\n      healRate: total ? `${Math.round((healed / total) * 100)}%` : '0%',\n    };\n  }\n}\n",
    "src/utils/locators/LocatorRules.ts": "import * as fs   from 'fs';\nimport * as path from 'path';\nimport { logger } from '@utils/helpers/Logger';\n\nexport type RulePriorityKey = 'dataTestId' | 'role' | 'ariaLabel' | 'name' | 'id' | 'text' | 'placeholder' | 'css' | 'xpath';\nexport type LocatorPattern   = 'testid' | 'role' | 'label' | 'placeholder' | 'text' | 'css' | 'xpath';\n\nexport interface OptimizationRules {\n  priorities:  RulePriorityKey[];\n  uniqueCheck: boolean;\n}\n\nexport interface RuleCondition {\n  attribute: string;\n  operator:  'exists' | 'equals' | 'contains' | 'startsWith' | 'endsWith' | 'matches';\n  value?:    string | RegExp;\n  required:  boolean;\n}\n\nexport interface RuleExample {\n  html:         string;\n  locatorValue: string;\n  description:  string;\n  success:      boolean;\n}\n\nexport interface LocatorRule {\n  id:           string;\n  name:         string;\n  pattern:      LocatorPattern;\n  elementTypes: string[];\n  priority:     number;\n  conditions:   RuleCondition[];\n  examples:     RuleExample[];\n  confidence:   number;\n}\n\nconst CONTEXT_RULES = {\n  formInputs:      { preferredStrategies: ['testid', 'label', 'placeholder'], description: 'Form input fields' },\n  buttons:         { preferredStrategies: ['testid', 'role', 'text'],         description: 'Clickable buttons' },\n  navigationLinks: { preferredStrategies: ['text', 'role', 'testid'],         description: 'Navigation links' },\n  dropdowns:       { preferredStrategies: ['testid', 'label', 'css'],         description: 'Select/dropdown elements' },\n};\n\nexport function getContextRules() { return CONTEXT_RULES; }\n\nexport function getAILocatorRules(): LocatorRule[] {\n  return [\n    {\n      id: 'rule-testid-primary', name: 'Test ID — Primary Strategy', pattern: 'testid',\n      elementTypes: ['button', 'input', 'select', 'link', 'div', 'span'], priority: 1,\n      conditions: [{ attribute: 'data-testid', operator: 'exists', required: true }],\n      examples: [{ html: '<button data-testid=\"login-button\">Login</button>', locatorValue: 'login-button', description: 'Button with data-testid', success: true }],\n      confidence: 0.95,\n    },\n    {\n      id: 'rule-aria-label', name: 'ARIA Label Strategy', pattern: 'label',\n      elementTypes: ['button', 'input', 'link'], priority: 2,\n      conditions: [{ attribute: 'aria-label', operator: 'exists', required: true }],\n      examples: [{ html: '<button aria-label=\"Close Menu\">×</button>', locatorValue: 'Close Menu', description: 'Button with aria-label', success: true }],\n      confidence: 0.9,\n    },\n    {\n      id: 'rule-placeholder', name: 'Placeholder Strategy', pattern: 'placeholder',\n      elementTypes: ['input', 'textarea'], priority: 3,\n      conditions: [{ attribute: 'placeholder', operator: 'exists', required: true }],\n      examples: [{ html: '<input placeholder=\"Enter username\">', locatorValue: 'Enter username', description: 'Input with placeholder', success: true }],\n      confidence: 0.85,\n    },\n    {\n      id: 'rule-visible-text', name: 'Visible Text Strategy', pattern: 'text',\n      elementTypes: ['button', 'link', 'span', 'div'], priority: 4,\n      conditions: [{ attribute: 'textContent', operator: 'exists', required: true }],\n      examples: [{ html: '<button>Login</button>', locatorValue: 'Login', description: 'Button with visible text', success: true }],\n      confidence: 0.75,\n    },\n    {\n      id: 'rule-css-selector', name: 'CSS Selector Strategy', pattern: 'css',\n      elementTypes: ['*'], priority: 5,\n      conditions: [{ attribute: 'class', operator: 'exists', required: true }],\n      examples: [{ html: '<button class=\"btn btn-primary\">Login</button>', locatorValue: '.btn.btn-primary', description: 'Button with class', success: true }],\n      confidence: 0.7,\n    },\n  ];\n}\n\n/**\n * LocatorRules\n *\n * Singleton that loads selector strategy priorities.\n * Optionally reads a `.vscode/copilot-instructions.md` file to auto-tune priorities.\n * Falls back to sensible defaults if the file is absent.\n */\nexport class LocatorRules {\n  private static instance: LocatorRules;\n  private rules: OptimizationRules = {\n    priorities:  ['dataTestId', 'role', 'ariaLabel', 'name', 'id', 'text', 'placeholder', 'css', 'xpath'],\n    uniqueCheck: true,\n  };\n\n  private constructor() { this.loadRules(); }\n\n  static getInstance(): LocatorRules {\n    if (!LocatorRules.instance) LocatorRules.instance = new LocatorRules();\n    return LocatorRules.instance;\n  }\n\n  private loadRules(): void {\n    try {\n      const rulesPath = path.join(process.cwd(), '.vscode', 'copilot-instructions.md');\n      if (!fs.existsSync(rulesPath)) return;\n      const content = fs.readFileSync(rulesPath, 'utf-8');\n      const found: RulePriorityKey[] = [];\n      const add = (key: RulePriorityKey, patterns: RegExp[]) => {\n        if (patterns.some(re => re.test(content))) found.push(key);\n      };\n      add('dataTestId', [/data-?test(id)?/i, /testid/i]);\n      add('role',       [/role-?based/i, /\\brole\\b/i]);\n      add('ariaLabel',  [/aria-?label/i]);\n      add('name',       [/\\bname\\b/i]);\n      add('id',         [/\\bid\\b/i]);\n      add('text',       [/text-?based/i, /visible text/i]);\n      add('placeholder',[/placeholder/i]);\n      add('css',        [/css selector/i]);\n      add('xpath',      [/xpath/i]);\n      if (found.length) {\n        this.rules.priorities = [...found, ...this.rules.priorities.filter(d => !found.includes(d))];\n      }\n      logger.info(`LocatorRules loaded: [${this.rules.priorities.join(', ')}]`);\n    } catch (err) {\n      logger.warn(`LocatorRules: using defaults (${String(err)})`);\n    }\n  }\n\n  getPriorities(): RulePriorityKey[] { return this.rules.priorities; }\n\n  orderStrategies(hints: Record<string, unknown>): RulePriorityKey[] {\n    const available = this.rules.priorities.filter(p => hints[p] !== undefined);\n    return available.length ? available : this.rules.priorities;\n  }\n\n  getRulesForElementType(elementType: string): LocatorRule[] {\n    return getAILocatorRules().filter(r => r.elementTypes.includes(elementType) || r.elementTypes.includes('*')).sort((a, b) => a.priority - b.priority);\n  }\n\n  getContextPreferences(contextType: string): unknown {\n    return CONTEXT_RULES[contextType as keyof typeof CONTEXT_RULES];\n  }\n}\n",
    "src/utils/locators/LocatorStrategy.ts": "import { Page, Locator } from '@playwright/test';\nimport { logger } from '@utils/helpers/Logger';\n\nexport type LocatorStrategyType = 'testid' | 'role' | 'label' | 'placeholder' | 'text' | 'css' | 'xpath';\n\nexport interface LocatorConfig {\n  strategy: LocatorStrategyType;\n  value:    string;\n  options?: Record<string, unknown>;\n}\n\n/**\n * LocatorStrategy\n *\n * Encapsulates all Playwright locator factory calls.\n * Keeps page objects free of strategy switch logic.\n *\n * Use via LocatorManager.getLocator(), or directly from a BasePage subclass:\n *   const loc = this.locatorStrategy.getLocator({ strategy: 'testid', value: 'submit-btn' });\n */\nexport class LocatorStrategy {\n  constructor(private readonly page: Page) {}\n\n  getLocator(config: LocatorConfig): Locator {\n    switch (config.strategy) {\n      case 'testid':      return this.page.getByTestId(config.value);\n      case 'role':        return this.page.getByRole(config.value as Parameters<Page['getByRole']>[0], config.options);\n      case 'label':       return this.page.getByLabel(config.value, config.options);\n      case 'placeholder': return this.page.getByPlaceholder(config.value, config.options);\n      case 'text':        return this.page.getByText(config.value, config.options);\n      case 'css':\n      case 'xpath':       return this.page.locator(config.value);\n      default: throw new Error(`Unknown locator strategy: ${(config as LocatorConfig).strategy}`);\n    }\n  }\n\n  async tryMultipleStrategies(configs: LocatorConfig[]): Promise<{ locator: Locator; config: LocatorConfig } | null> {\n    for (const config of configs) {\n      try {\n        const locator = this.getLocator(config);\n        await locator.waitFor({ timeout: 2_000 });\n        logger.debug(`Locator found via ${config.strategy}: ${config.value}`);\n        return { locator, config };\n      } catch { /* try next */ }\n    }\n    return null;\n  }\n\n  async generateFallbackLocators(primary: LocatorConfig): Promise<LocatorConfig[]> {\n    const fallbacks: LocatorConfig[] = [primary];\n    if (primary.strategy === 'testid') {\n      try {\n        const el = this.page.getByTestId(primary.value).first();\n        await el.waitFor({ state: 'attached', timeout: 1_000 });\n        const role      = await el.getAttribute('role');\n        const ariaLabel = await el.getAttribute('aria-label');\n        if (role)      fallbacks.push({ strategy: 'role',  value: role });\n        if (ariaLabel) fallbacks.push({ strategy: 'label', value: ariaLabel });\n      } catch { /* element not ready */ }\n    }\n    return fallbacks;\n  }\n\n  getByTextFuzzy(text: string, exact = false): Locator {\n    return exact ? this.page.getByText(text, { exact: true }) : this.page.getByText(new RegExp(text, 'i'));\n  }\n\n  getButton(name: string | RegExp): Locator { return this.page.getByRole('button', { name }); }\n  getLink(name: string | RegExp): Locator   { return this.page.getByRole('link',   { name }); }\n  getTextbox(name: string | RegExp): Locator { return this.page.getByRole('textbox', { name }); }\n  getHeading(name: string | RegExp, level?: 1|2|3|4|5|6): Locator { return this.page.getByRole('heading', { name, level }); }\n\n  getCombinedLocator(strategies: string[]): Locator { return this.page.locator(strategies.join(', ')); }\n  getNth(locator: Locator, index: number): Locator  { return locator.nth(index); }\n  getFirstVisible(locator: Locator): Locator         { return locator.first(); }\n  filterByText(locator: Locator, text: string | RegExp): Locator { return locator.filter({ hasText: text }); }\n  filterByLocator(locator: Locator, inner: Locator): Locator     { return locator.filter({ has: inner }); }\n  getParent(locator: Locator): Locator { return locator.locator('..'); }\n\n  buildRobustLocator(info: { testId?: string; role?: string; label?: string }): Locator {\n    const strategies: string[] = [];\n    if (info.testId) strategies.push(`[data-testid=\"${info.testId}\"]`);\n    if (info.role)   strategies.push(`[role=\"${info.role}\"]`);\n    if (info.label)  strategies.push(`[aria-label=\"${info.label}\"]`);\n    if (!strategies.length) throw new Error('No valid locator strategies provided');\n    return this.getCombinedLocator(strategies);\n  }\n\n  async isInteractable(locator: Locator): Promise<boolean> {\n    try {\n      await locator.waitFor({ state: 'visible', timeout: 2_000 });\n      return await locator.isEnabled();\n    } catch { return false; }\n  }\n\n  async getCount(locator: Locator): Promise<number> { return locator.count(); }\n}\n",
    "src/utils/locators/PlaywrightHealerLogger.ts": "import { HealerLogger } from './LocatorHealer';\nimport { logger } from '@utils/helpers/Logger';\n\n/**\n * PlaywrightHealerLogger bridges the generic HealerLogger interface\n * used by LocatorHealer, TimingHealer, and VisualIntentChecker with\n * the project's Winston-backed Logger singleton.\n */\nexport class PlaywrightHealerLogger implements HealerLogger {\n  info(msg: string): void  { logger.info(msg);  }\n  warn(msg: string): void  { logger.warn(msg);  }\n  error(msg: string): void { logger.error(msg); }\n}\n",
    "src/utils/locators/TimingHealer.ts": "import { Page } from '@playwright/test';\nimport { HealerLogger } from './LocatorHealer';\n\ninterface TimingRecord {\n  label:      string;\n  durationMs: number;\n  timestamp:  Date;\n}\n\n/**\n * TimingHealer — Layer 2 Self-Healing\n *\n * Measures network-idle wait time per labelled action and auto-adjusts\n * future waits using an Exponential Moving Average (EMA).\n * Flags drift to HealingDashboard when actual > baseline × threshold.\n *\n * Usage inside a page method:\n *   await fixture().locatorTiming.waitForNetworkIdle('clickSubmit');\n */\nexport class TimingHealer {\n  private static readonly DEFAULT_TIMEOUT  = 10_000;\n  private static readonly EMA_ALPHA        = 0.3;\n  private static readonly DRIFT_THRESHOLD  = 1.5;\n\n  private readonly history = new Map<string, TimingRecord[]>();\n  private readonly emaMap  = new Map<string, number>();\n\n  constructor(\n    private readonly page: Page,\n    private readonly logger: HealerLogger,\n  ) {}\n\n  async waitForNetworkIdle(label: string, timeoutMs?: number): Promise<void> {\n    const timeout = timeoutMs ?? this.getAdaptiveTimeout(label);\n    const start   = Date.now();\n\n    try {\n      await this.page.waitForLoadState('networkidle', { timeout });\n    } catch {\n      this.logger.warn(`[TimingHealer] networkidle timeout (${timeout}ms) for \"${label}\" — continuing`);\n    }\n\n    const elapsed = Date.now() - start;\n    this.record(label, elapsed);\n    this.updateEma(label, elapsed);\n    this.checkDrift(label, elapsed, timeout);\n    this.logger.info(`[TimingHealer] \"${label}\" settled in ${elapsed}ms (adaptive: ${timeout}ms)`);\n  }\n\n  getAdaptiveTimeout(label: string): number {\n    const ema = this.emaMap.get(label);\n    if (!ema) return TimingHealer.DEFAULT_TIMEOUT;\n    return Math.ceil(ema * 1.5);\n  }\n\n  getHistory(label: string): TimingRecord[] {\n    return this.history.get(label) ?? [];\n  }\n\n  getSummary(): Array<{ label: string; emaMs: number; adaptiveTimeoutMs: number }> {\n    return Array.from(this.emaMap.entries()).map(([label, ema]) => ({\n      label,\n      emaMs: Math.round(ema),\n      adaptiveTimeoutMs: this.getAdaptiveTimeout(label),\n    }));\n  }\n\n  private record(label: string, durationMs: number): void {\n    if (!this.history.has(label)) this.history.set(label, []);\n    this.history.get(label)!.push({ label, durationMs, timestamp: new Date() });\n  }\n\n  private updateEma(label: string, durationMs: number): void {\n    const prev = this.emaMap.get(label) ?? durationMs;\n    this.emaMap.set(label, TimingHealer.EMA_ALPHA * durationMs + (1 - TimingHealer.EMA_ALPHA) * prev);\n  }\n\n  private checkDrift(label: string, elapsed: number, timeout: number): void {\n    const baseline = timeout / 1.5;\n    if (elapsed > baseline * TimingHealer.DRIFT_THRESHOLD) {\n      this.logger.warn(\n        `[TimingHealer] DRIFT detected for \"${label}\": actual=${elapsed}ms, baseline=${Math.round(baseline)}ms ` +\n        `(${Math.round((elapsed / baseline) * 100)}%). ` +\n        `Review at http://localhost:${process.env.HEALING_DASHBOARD_PORT ?? 7890}`,\n      );\n    }\n  }\n}\n",
    "src/utils/locators/VisualIntentChecker.ts": "import { Page } from '@playwright/test';\nimport { HealerLogger } from './LocatorHealer';\nimport * as fs   from 'fs';\nimport * as path from 'path';\n\n/**\n * VisualIntentChecker — Layer 3 Self-Healing\n *\n * At every assertion on an element flagged with `visualIntent: true` in locators.ts,\n * VisualIntentChecker:\n *   1. Takes an element-level screenshot\n *   2. Compares it against a stored baseline (size-based diff)\n *   3. If diff exceeds threshold → logs a warning for manual review\n *\n * First run: baseline is saved.\n * Subsequent runs: screenshot is diffed against baseline.\n *\n * Baselines are stored in: test-results/visual-baselines/{key}.png\n *\n * Usage inside a page object assertion:\n *   await fixture().locatorVisual.check('submitButton', selector, intent);\n */\nexport class VisualIntentChecker {\n  private static readonly BASELINE_DIR = path.resolve(\n    process.cwd(), 'test-results', 'visual-baselines',\n  );\n  private static readonly DIFF_THRESHOLD_PCT = 2;\n\n  constructor(\n    private readonly page: Page,\n    private readonly logger: HealerLogger,\n  ) {\n    fs.mkdirSync(VisualIntentChecker.BASELINE_DIR, { recursive: true });\n  }\n\n  async check(key: string, selector: string, _intent: string): Promise<void> {\n    const baselinePath = path.join(VisualIntentChecker.BASELINE_DIR, `${key}.png`);\n\n    try {\n      const element   = this.page.locator(selector).first();\n      const isVisible = await element.isVisible({ timeout: 3_000 }).catch(() => false);\n\n      if (!isVisible) {\n        this.logger.warn(`[VisualIntentChecker] Element \"${key}\" not visible — skipping`);\n        return;\n      }\n\n      const screenshot = await element.screenshot({ timeout: 5_000 });\n\n      if (!fs.existsSync(baselinePath)) {\n        fs.writeFileSync(baselinePath, screenshot);\n        this.logger.info(`[VisualIntentChecker] Baseline saved for \"${key}\"`);\n        return;\n      }\n\n      const baseline = fs.readFileSync(baselinePath);\n\n      if (screenshot.length === baseline.length && screenshot.equals(baseline)) {\n        this.logger.info(`[VisualIntentChecker] \"${key}\" visual match ✓`);\n        return;\n      }\n\n      const diffPath = path.join(VisualIntentChecker.BASELINE_DIR, `${key}.diff.png`);\n      fs.writeFileSync(diffPath, screenshot);\n\n      const sizeDiffPct = Math.abs(screenshot.length - baseline.length) / baseline.length * 100;\n\n      if (sizeDiffPct > VisualIntentChecker.DIFF_THRESHOLD_PCT) {\n        this.logger.warn(\n          `[VisualIntentChecker] VISUAL CHANGE detected for \"${key}\" ` +\n          `(size diff: ${sizeDiffPct.toFixed(1)}%). Diff: ${diffPath}. ` +\n          `Review at http://localhost:${process.env.HEALING_DASHBOARD_PORT ?? 7890}`,\n        );\n      } else {\n        fs.writeFileSync(baselinePath, screenshot);\n        this.logger.info(`[VisualIntentChecker] \"${key}\" minor diff (${sizeDiffPct.toFixed(1)}%) — baseline updated ✓`);\n      }\n    } catch (err) {\n      this.logger.warn(`[VisualIntentChecker] Check failed for \"${key}\": ${String(err)}`);\n    }\n  }\n\n  clearBaseline(key: string): void {\n    const baselinePath = path.join(VisualIntentChecker.BASELINE_DIR, `${key}.png`);\n    if (fs.existsSync(baselinePath)) {\n      fs.unlinkSync(baselinePath);\n      this.logger.info(`[VisualIntentChecker] Baseline cleared for \"${key}\"`);\n    }\n  }\n\n  clearAllBaselines(): void {\n    const files = fs.readdirSync(VisualIntentChecker.BASELINE_DIR)\n      .filter((f: string) => f.endsWith('.png') && !f.endsWith('.diff.png'));\n    files.forEach((f: string) => fs.unlinkSync(path.join(VisualIntentChecker.BASELINE_DIR, f)));\n    this.logger.info(`[VisualIntentChecker] Cleared ${files.length} baselines`);\n  }\n\n  listBaselines(): string[] {\n    return fs.readdirSync(VisualIntentChecker.BASELINE_DIR)\n      .filter((f: string) => f.endsWith('.png') && !f.endsWith('.diff.png'))\n      .map((f: string) => path.basename(f, '.png'));\n  }\n}\n",
    "src/utils/locators/healix-ci-apply.ts": "/**\n * healix-ci-apply.ts — headless heal applicator for CI/CD pipelines\n *\n * Run after tests complete in CI:\n *   npm run healix:apply-ci\n *\n * Behaviour:\n *   HEALIX_CI_AUTO_APPROVE=true  → auto-approves all heals, applies to source, creates PR\n *   HEALIX_CI_AUTO_APPROVE=false → prints summary only; human reviews via `npm run healix:review`\n *\n * Always exits 0 so the pipeline does not fail on healing activity.\n *\n * Environment variables:\n *   HEALIX_CI_AUTO_APPROVE   \"true\" to auto-approve (default: false)\n *   HEALIX_CI_DRAFT_PR       \"true\" for draft PR (default: true)\n *   HEAL_STORE_PATH          Path to healed-locators.json\n *   HEAL_SEARCH_ROOTS        Comma-separated dirs to search for selectors\n *   HEAL_TARGET_REPO         Git repo root\n *   HEAL_PR_BASE_BRANCH      Base branch (default: main)\n *   ADO_PAT                  Azure DevOps PAT (ADO pipeline only)\n *   GH_TOKEN / GITHUB_TOKEN  GitHub token for `gh pr create`\n */\n\nimport * as fs   from 'fs';\nimport * as path from 'path';\nimport { HealApplicator, HealRecord } from './HealApplicator';\n\nconst HEAL_STORE_PATH = path.resolve(process.env.HEAL_STORE_PATH ?? 'self-heal/healed-locators.json');\nconst AUTO_APPROVE    = process.env.HEALIX_CI_AUTO_APPROVE === 'true';\nconst DRAFT_PR        = process.env.HEALIX_CI_DRAFT_PR !== 'false';\nconst BASE_BRANCH     = process.env.HEAL_PR_BASE_BRANCH ?? 'main';\n\nfunction readStore(): Record<string, HealRecord> {\n  try { if (!fs.existsSync(HEAL_STORE_PATH)) return {}; return JSON.parse(fs.readFileSync(HEAL_STORE_PATH, 'utf8')); }\n  catch { return {}; }\n}\n\nfunction writeStore(store: Record<string, HealRecord>): void {\n  const dir = path.dirname(HEAL_STORE_PATH);\n  if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });\n  fs.writeFileSync(HEAL_STORE_PATH, JSON.stringify(store, null, 2), 'utf8');\n}\n\nfunction printSummaryTable(store: Record<string, HealRecord>): void {\n  const keys = Object.keys(store);\n  if (!keys.length) { console.log('  ℹ  No heals in store — nothing to apply.'); return; }\n  console.log('');\n  console.log('  ┌──────────────────────────────────────────────────────────────────┐');\n  console.log('  │  🩺  HEALED LOCATORS SUMMARY                                     │');\n  console.log('  ├────────────────────┬─────────────┬──────────────────────────────');\n  console.log('  │ Key                │ Strategy    │ Original → Healed');\n  console.log('  ├────────────────────┼─────────────┼──────────────────────────────');\n  for (const key of keys) {\n    const r = store[key];\n    console.log(`  │ ${key.padEnd(18).slice(0,18)} │ ${(r.strategy ?? '?').padEnd(11).slice(0,11)} │ ${(r.originalSelector ?? '').slice(0,24)} → ${(r.healedSelector ?? '').slice(0,24)}`);\n  }\n  console.log('  └────────────────────┴─────────────┴──────────────────────────────');\n  console.log('');\n}\n\nfunction emitCiAnnotations(\n  applied: import('./HealApplicator').AppliedHeal[],\n  store:   Record<string, HealRecord>,\n  prUrl:   string,\n): void {\n  const isAdo    = !!process.env.SYSTEM_TEAMFOUNDATIONCOLLECTIONURI;\n  const isGitHub = !!process.env.GITHUB_ACTIONS;\n\n  if (isAdo) {\n    console.log('##vso[build.addbuildtag]healix-healed');\n    for (const a of applied) {\n      console.log(`##vso[task.logissue type=warning]Healed \"${a.key}\": ${a.originalSelector} → ${a.healedSelector}`);\n    }\n    if (prUrl) console.log(`##vso[task.logissue type=warning]Heal PR: ${prUrl}`);\n    console.log(`##vso[artifact.upload containerfolder=healix;artifactname=healed-locators]${HEAL_STORE_PATH}`);\n  }\n\n  if (isGitHub && process.env.GITHUB_STEP_SUMMARY) {\n    const lines = [\n      '## 🩺 Healix — Self-Healing Locator Report', '',\n      `**${applied.length} locator(s) healed** during this test run.`,\n      prUrl ? `\\n**PR:** ${prUrl}` : '', '',\n      '| Key | Strategy | Original → Healed |', '|-----|----------|-------------------|',\n      ...applied.map(a => `| \\`${a.key}\\` | ${store[a.key]?.strategy ?? '?'} | \\`${a.originalSelector}\\` → \\`${a.healedSelector}\\` |`),\n    ];\n    fs.appendFileSync(process.env.GITHUB_STEP_SUMMARY, lines.join('\\n') + '\\n');\n  }\n}\n\nasync function main(): Promise<void> {\n  console.log('');\n  console.log('  🩺  Healix CI — post-run heal processor');\n  console.log(`      Store: ${HEAL_STORE_PATH}`);\n  console.log(`      Auto-approve: ${AUTO_APPROVE}`);\n  console.log('');\n\n  const store = readStore();\n  const total = Object.keys(store).length;\n  if (total === 0) { console.log('  ✅  No heals recorded — all locators resolved natively.'); process.exit(0); }\n\n  printSummaryTable(store);\n\n  if (!AUTO_APPROVE) {\n    console.log('  ℹ  HEALIX_CI_AUTO_APPROVE not set — heals saved as pipeline artifact.');\n    console.log('     To review locally:');\n    console.log('       1. Download healed-locators.json artifact');\n    console.log('       2. Place at self-heal/healed-locators.json');\n    console.log('       3. Run: npm run healix:review');\n    console.log('');\n    process.exit(0);\n  }\n\n  console.log('  🔄  Auto-approving all heals…');\n  for (const key of Object.keys(store)) store[key].approved = true;\n  writeStore(store);\n\n  const applicator = new HealApplicator();\n  const result     = applicator.apply(store);\n\n  if (result.applied.length === 0) {\n    console.log('');\n    console.log('  ⚠  No selectors replaced in source files.');\n    if (result.skipped.length) console.log(`     Skipped: ${result.skipped.join(', ')}`);\n    process.exit(0);\n  }\n\n  console.log(`  ✅  Applied ${result.applied.length} heal(s):`);\n  for (const a of result.applied) {\n    console.log(`     • ${a.key}: ${a.originalSelector} → ${a.healedSelector}`);\n    console.log(`       in ${path.relative(process.cwd(), a.file)}:${a.line}`);\n  }\n\n  let prUrl = '';\n  try {\n    const { execSync } = await import('child_process');\n    const repo         = process.env.HEAL_TARGET_REPO ?? process.cwd();\n    const timestamp    = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);\n    const branch       = `heal/locator-fixes-${timestamp}`;\n    const prTitle      = process.env.HEAL_PR_TITLE ?? 'fix: apply AI-healed locator fixes';\n    const run          = (cmd: string) => execSync(cmd, { cwd: repo, stdio: 'pipe' }).toString().trim();\n\n    try { run('git config user.email'); } catch {\n      run('git config user.email \"healix-ci@pipeline\"');\n      run('git config user.name \"Healix CI\"');\n    }\n\n    run(`git checkout -b ${branch}`);\n    for (const f of result.changedFiles) { run(`git add \"${path.relative(repo, f)}\"`); }\n\n    const prBody = [\n      '## 🩺 AI-Healed Locator Fixes', '',\n      '> Automatically generated by Healix. Review carefully before merging.', '',\n      '| Key | Strategy | Original → Healed |', '|-----|----------|-------------------|',\n      ...result.applied.map(a => `| \\`${a.key}\\` | ${store[a.key]?.strategy ?? '?'} | \\`${a.originalSelector}\\` → \\`${a.healedSelector}\\` |`),\n      '', `**Files changed:** ${result.changedFiles.map(f => path.relative(repo, f)).join(', ')}`,\n    ].join('\\n');\n\n    run(`git commit -m \"${prTitle.replace(/\"/g, \"'\")}\"`);\n    run(`git push origin ${branch}`);\n\n    const isAdo    = !!process.env.SYSTEM_TEAMFOUNDATIONCOLLECTIONURI;\n    const isGitHub = !!process.env.GITHUB_ACTIONS;\n\n    if (isAdo) {\n      const orgUrl  = (process.env.SYSTEM_TEAMFOUNDATIONCOLLECTIONURI ?? '').replace(/\\/$/, '');\n      const project = process.env.SYSTEM_TEAMPROJECT ?? '';\n      const repoId  = process.env.BUILD_REPOSITORY_NAME ?? '';\n      const pat     = process.env.ADO_PAT ?? '';\n      if (orgUrl && project && repoId && pat) {\n        try {\n          run(`az devops configure --defaults organization=\"${orgUrl}\" project=\"${project}\"`);\n          const isDraftFlag = DRAFT_PR ? '--draft true' : '';\n          prUrl = run(`az repos pr create --repository \"${repoId}\" --source-branch \"${branch}\" --target-branch \"${BASE_BRANCH}\" --title \"${prTitle.replace(/\"/g,\"'\")}\" --description \"${prBody.replace(/\"/g,\"'\").replace(/\\n/g,' ')}\" ${isDraftFlag} --query \"url\" --output tsv 2>&1`);\n          if (!prUrl.includes('https://')) prUrl = '';\n        } catch { /* skip PR creation */ }\n      }\n    } else if (isGitHub) {\n      const isDraft = DRAFT_PR ? '--draft' : '';\n      const esc = (s: string) => s.replace(/\"/g, '\\\\\"');\n      prUrl = run(`gh pr create --title \"${esc(prTitle)}\" --body \"${esc(prBody).replace(/\\n/g,'\\\\n')}\" --base ${BASE_BRANCH} ${isDraft} 2>&1 || echo \"PR_FAILED\"`);\n      if (prUrl.includes('PR_FAILED')) prUrl = '';\n    }\n\n    if (prUrl) console.log(`  🚀  PR created: ${prUrl}`);\n    else { console.log(`  ℹ  Branch pushed: ${branch}`); console.log('     Create a PR manually.'); }\n\n  } catch (err) {\n    console.log(`  ⚠  Files updated but PR creation failed: ${String(err)}`);\n  }\n\n  emitCiAnnotations(result.applied, store, prUrl);\n  process.exit(0);\n}\n\nmain().catch(err => { console.error('Healix CI apply failed:', err); process.exit(1); });\n",
    "src/utils/locators/index.ts": "export { LocatorRepository }            from './LocatorRepository';\nexport type { LocatorEntry }            from './LocatorRepository';\nexport { LocatorHealer }               from './LocatorHealer';\nexport type { HealerLogger }           from './LocatorHealer';\nexport { HealingDashboard }            from './HealingDashboard';\nexport type { HealEvent }              from './HealingDashboard';\nexport { HealApplicator }              from './HealApplicator';\nexport type { HealRecord, AppliedHeal, ApplyResult } from './HealApplicator';\nexport { TimingHealer }                from './TimingHealer';\nexport { VisualIntentChecker }         from './VisualIntentChecker';\nexport { LocatorManager }              from './LocatorManager';\nexport { ElementContextHelper }        from './ElementContextHelper';\nexport { LocatorRules }                from './LocatorRules';\nexport { LocatorStrategy }             from './LocatorStrategy';\nexport { PlaywrightHealerLogger }      from './PlaywrightHealerLogger';\n",
    "src/utils/locators/review-server.ts": "/**\n * Healix Review Server — standalone post-run heal review tool\n *\n * Run after tests complete:\n *   npm run healix:review\n *\n * Open http://localhost:7891 to:\n *   • Review AI-healed selectors\n *   • Approve / reject each one\n *   • Apply approved heals to source files and create a GitHub / ADO PR\n *\n * Environment:\n *   HEALIX_REVIEW_PORT   Port (default: 7891)\n *   HEAL_STORE_PATH      Path to healed-locators.json\n *   HEAL_SEARCH_ROOTS    Comma-separated dirs to search for selectors\n *   HEAL_TARGET_REPO     Git repo root\n *   HEAL_PR_TITLE        PR title\n */\n\nimport * as http from 'http';\nimport * as fs   from 'fs';\nimport * as path from 'path';\nimport { HealApplicator, HealRecord } from './HealApplicator';\n\nconst PORT            = parseInt(process.env.HEALIX_REVIEW_PORT ?? '7891', 10);\nconst HEAL_STORE_PATH = path.resolve(process.env.HEAL_STORE_PATH ?? 'self-heal/healed-locators.json');\n\nconst cors = {\n  'Access-Control-Allow-Origin':  '*',\n  'Access-Control-Allow-Methods': 'GET,POST,PATCH,OPTIONS',\n  'Access-Control-Allow-Headers': 'Content-Type',\n};\n\nfunction readStore():  Record<string, HealRecord> {\n  try { if (!fs.existsSync(HEAL_STORE_PATH)) return {}; return JSON.parse(fs.readFileSync(HEAL_STORE_PATH, 'utf8')); }\n  catch { return {}; }\n}\n\nfunction writeStore(store: Record<string, HealRecord>): void {\n  const dir = path.dirname(HEAL_STORE_PATH);\n  if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });\n  fs.writeFileSync(HEAL_STORE_PATH, JSON.stringify(store, null, 2), 'utf8');\n}\n\nfunction readBody(req: http.IncomingMessage): Promise<string> {\n  return new Promise(resolve => {\n    const chunks: Buffer[] = [];\n    req.on('data', c => chunks.push(c));\n    req.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));\n  });\n}\n\nasync function handleRequest(req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {\n  const url    = req.url ?? '/';\n  const method = req.method ?? 'GET';\n\n  if (method === 'OPTIONS') { res.writeHead(204, cors); res.end(); return; }\n\n  if (url === '/api/store' && method === 'GET') {\n    res.writeHead(200, { 'Content-Type': 'application/json', ...cors });\n    res.end(JSON.stringify(readStore(), null, 2)); return;\n  }\n\n  if (url === '/api/approve' && method === 'PATCH') {\n    const { key, approved } = JSON.parse(await readBody(req)) as { key: string; approved: boolean };\n    const store = readStore();\n    if (store[key]) { store[key].approved = approved; writeStore(store); res.writeHead(200, { 'Content-Type': 'application/json', ...cors }); res.end(JSON.stringify({ ok: true, key, approved })); }\n    else { res.writeHead(404, { 'Content-Type': 'application/json', ...cors }); res.end(JSON.stringify({ ok: false, error: `Key \"${key}\" not found` })); }\n    return;\n  }\n\n  if (url === '/api/approve-all' && method === 'POST') {\n    const { approved } = JSON.parse(await readBody(req)) as { approved: boolean };\n    const store = readStore();\n    for (const k of Object.keys(store)) store[k].approved = approved;\n    writeStore(store);\n    res.writeHead(200, { 'Content-Type': 'application/json', ...cors });\n    res.end(JSON.stringify({ ok: true, count: Object.keys(store).length })); return;\n  }\n\n  if (url === '/api/apply' && method === 'POST') {\n    const store    = readStore();\n    const approved = Object.values(store).filter(r => r.approved);\n    if (approved.length === 0) {\n      res.writeHead(200, { 'Content-Type': 'application/json', ...cors });\n      res.end(JSON.stringify({ ok: false, message: 'No approved heals to apply' })); return;\n    }\n    const applicator = new HealApplicator();\n    const result     = applicator.apply(store);\n    let prUrl = '', prError = '';\n    if (result.changedFiles.length > 0) {\n      try { prUrl = applicator.createPR(result.changedFiles, result.applied); }\n      catch (err) { prError = String(err); }\n      for (const a of result.applied) { if (store[a.key]) delete store[a.key].approved; }\n      writeStore(store);\n    }\n    res.writeHead(200, { 'Content-Type': 'application/json', ...cors });\n    res.end(JSON.stringify({\n      ok: result.errors.length === 0, applied: result.applied, skipped: result.skipped,\n      skippedDetails: Object.fromEntries(result.skipped.filter(k => store[k]).map(k => [k, { originalSelector: store[k].originalSelector, healedSelector: store[k].healedSelector }])),\n      errors: result.errors, prUrl, prError, changedFiles: result.changedFiles,\n    }, null, 2)); return;\n  }\n\n  if (url === '/api/clear' && method === 'POST') {\n    writeStore({});\n    res.writeHead(200, { 'Content-Type': 'application/json', ...cors });\n    res.end(JSON.stringify({ ok: true })); return;\n  }\n\n  res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });\n  res.end(buildReviewHtml());\n}\n\nfunction escHtml(s: string): string {\n  return String(s ?? '').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/\"/g,'&quot;').replace(/'/g,'&#39;');\n}\n\nfunction buildRow(record: HealRecord, key: string): string {\n  const stratClass = ['ai-vision','ax-tree','role','label','text','cached'].includes(record.strategy) ? record.strategy : '';\n  const rowClass   = record.approved === true ? 'approved' : record.approved === false ? 'rejected' : '';\n  const dotClass   = record.approved === true ? 'approved' : record.approved === false ? 'rejected' : 'pending';\n  const dateShort  = record.lastHealedAt ? new Date(record.lastHealedAt).toLocaleString() : '';\n  return `<tr class=\"${rowClass}\" data-key=\"${escHtml(key)}\" data-approved=\"${String(!!record.approved)}\">\n    <td><span class=\"status-dot ${dotClass}\"></span></td>\n    <td><code>${escHtml(key)}</code></td>\n    <td><span class=\"badge ${stratClass}\">${escHtml(record.strategy)}</span></td>\n    <td><code style=\"color:var(--muted)\">${escHtml(record.originalSelector)}</code></td>\n    <td><code style=\"color:var(--green);font-weight:600\">${escHtml(record.healedSelector)}</code></td>\n    <td style=\"font-size:12px;color:var(--muted);max-width:220px\">${escHtml(record.intent)}<br><span style=\"font-size:10px\">${dateShort}</span></td>\n    <td style=\"text-align:center\">${record.healCount}</td>\n    <td style=\"white-space:nowrap\">\n      <button class=\"approve-btn\" onclick=\"setApproved('${escHtml(key)}', true)\">✓ Approve</button>\n      &nbsp;<button class=\"reject-btn\" onclick=\"setApproved('${escHtml(key)}', false)\">✗ Reject</button>\n    </td>\n  </tr>`;\n}\n\nfunction buildReviewHtml(): string {\n  const store = readStore();\n  const keys  = Object.keys(store);\n  return `<!DOCTYPE html>\n<html lang=\"en\">\n<head><meta charset=\"UTF-8\"><title>Healix — Heal Review</title>\n<style>\n:root{--bg:#0d1117;--surface:#161b22;--border:#30363d;--accent:#58a6ff;--green:#3fb950;--red:#f85149;--yellow:#d2994a;--purple:#bc8cff;--muted:#8b949e;--text:#e6edf3}\n*{box-sizing:border-box;margin:0;padding:0}\nbody{background:var(--bg);color:var(--text);font:14px/1.5 'Segoe UI',system-ui,sans-serif;padding:24px}\nheader{display:flex;align-items:center;gap:16px;margin-bottom:32px;border-bottom:1px solid var(--border);padding-bottom:16px}\nheader h1{font-size:20px;font-weight:600}.subtitle{color:var(--muted);font-size:13px}\n.toolbar{display:flex;gap:12px;align-items:center;margin-bottom:20px;flex-wrap:wrap}\nbutton{cursor:pointer;border:none;border-radius:6px;padding:7px 16px;font:inherit;font-weight:600;font-size:13px;transition:opacity .15s}\nbutton:hover{opacity:.85}\n.btn-approve-all{background:rgba(63,185,80,.15);color:var(--green);border:1px solid var(--green)}\n.btn-reject-all{background:rgba(248,81,73,.1);color:var(--red);border:1px solid var(--red)}\n.btn-apply{background:var(--accent);color:#0d1117;font-size:14px;padding:9px 22px}\n.btn-apply:disabled{opacity:.4;cursor:not-allowed}\n.btn-clear{background:rgba(248,81,73,.1);color:var(--red);border:1px solid var(--red);margin-left:auto}\n.count-badge{background:var(--surface);border:1px solid var(--border);border-radius:12px;padding:2px 10px;font-size:12px;color:var(--muted)}\ntable{width:100%;border-collapse:collapse;font-size:13px}\nth{text-align:left;padding:10px 14px;border-bottom:2px solid var(--border);color:var(--muted);font-weight:600;font-size:12px;text-transform:uppercase;letter-spacing:.05em;white-space:nowrap}\ntd{padding:10px 14px;border-bottom:1px solid var(--border);vertical-align:top}\ntr:last-child td{border-bottom:none}tr.approved td{background:rgba(63,185,80,.04)}tr.rejected td{background:rgba(248,81,73,.04);opacity:.6}\ncode{font-family:monospace;font-size:11px;background:rgba(255,255,255,.05);border-radius:4px;padding:2px 6px;word-break:break-all}\n.badge{display:inline-block;border-radius:4px;padding:2px 8px;font-size:11px;font-weight:600}\n.badge.ai-vision{background:rgba(188,140,255,.15);color:var(--purple)}.badge.ax-tree{background:rgba(210,153,34,.15);color:var(--yellow)}\n.badge.role,.badge.label,.badge.text{background:rgba(63,185,80,.1);color:var(--green)}\n.approve-btn{background:rgba(63,185,80,.12);color:var(--green);border:1px solid rgba(63,185,80,.3);padding:4px 12px;font-size:12px;border-radius:4px}\n.reject-btn{background:rgba(248,81,73,.1);color:var(--red);border:1px solid rgba(248,81,73,.3);padding:4px 12px;font-size:12px;border-radius:4px}\n.status-dot{display:inline-block;width:9px;height:9px;border-radius:50%;margin-right:6px}\n.status-dot.approved{background:var(--green)}.status-dot.rejected{background:var(--red)}.status-dot.pending{background:var(--muted)}\n#pr-result{margin-top:24px;padding:16px;border-radius:8px;border:1px solid var(--border);font-size:13px;display:none}\n#pr-result.success{border-color:var(--green);background:rgba(63,185,80,.06)}#pr-result.error{border-color:var(--red);background:rgba(248,81,73,.06)}\n.empty{text-align:center;padding:48px;color:var(--muted)}\n</style>\n</head>\n<body>\n<header>\n  <span style=\"font-size:28px\">🩺</span>\n  <div><h1>Healix — Heal Review</h1><div class=\"subtitle\">Review AI-healed selectors, approve what's valid, then apply to the repo and create a PR</div></div>\n  <span class=\"count-badge\" id=\"approved-count\">0 approved</span>\n</header>\n<div class=\"toolbar\">\n  <button class=\"btn-approve-all\" onclick=\"approveAll(true)\">✓ Approve all</button>\n  <button class=\"btn-reject-all\" onclick=\"approveAll(false)\">✗ Reject all</button>\n  <button class=\"btn-apply\" id=\"apply-btn\" onclick=\"applyHeals()\" disabled>🚀 Apply approved &amp; create PR</button>\n  <button class=\"btn-clear\" onclick=\"clearAll()\">🗑 Clear all heals</button>\n</div>\n<table>\n  <thead><tr><th>Status</th><th>Key</th><th>Strategy</th><th>Original selector</th><th>Healed selector ✓</th><th>Intent</th><th>Heals</th><th>Actions</th></tr></thead>\n  <tbody id=\"heal-body\">\n    ${keys.length === 0 ? '<tr><td colspan=\"8\" class=\"empty\">No healed selectors in store — run your tests first</td></tr>' : keys.map(k => buildRow(store[k], k)).join('')}\n  </tbody>\n</table>\n<div id=\"pr-result\"></div>\n<script>\nfunction escHtml(s){return String(s??'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/\"/g,'&quot;').replace(/'/g,'&#39;');}\nfunction updateCount(){const rows=document.querySelectorAll('#heal-body tr[data-key]');let c=0;rows.forEach(r=>{if(r.dataset.approved==='true')c++;});document.getElementById('approved-count').textContent=c+' approved';document.getElementById('apply-btn').disabled=c===0;}\nasync function setApproved(key,approved){await fetch('/api/approve',{method:'PATCH',headers:{'Content-Type':'application/json'},body:JSON.stringify({key,approved})});const row=document.querySelector('tr[data-key=\"'+CSS.escape(key)+'\"]');if(row){row.dataset.approved=approved;row.className=approved?'approved':'rejected';const dot=row.querySelector('.status-dot');dot.className='status-dot '+(approved?'approved':'rejected');}updateCount();}\nasync function approveAll(approved){await fetch('/api/approve-all',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({approved})});location.reload();}\nasync function applyHeals(){const btn=document.getElementById('apply-btn');btn.disabled=true;btn.textContent='⏳ Applying…';const panel=document.getElementById('pr-result');panel.style.display='none';try{const d=await(await fetch('/api/apply',{method:'POST'})).json();if(d.prUrl){panel.className='success';panel.style.display='block';panel.innerHTML='✅ PR created: <a href=\"'+escHtml(d.prUrl)+'\" target=\"_blank\" style=\"color:var(--accent)\">'+escHtml(d.prUrl)+'</a>';setTimeout(()=>location.reload(),3000);}else if(d.changedFiles&&d.changedFiles.length>0){panel.className='success';panel.style.display='block';panel.innerHTML='✅ '+d.applied.length+' file(s) updated locally.'+(d.prError?'<br>⚠ PR failed: <code>'+escHtml(d.prError)+'</code>':'');setTimeout(()=>location.reload(),3000);}else if(d.skipped&&d.skipped.length>0){panel.className='error';panel.style.display='block';panel.innerHTML='⚠ '+d.skipped.length+' heal(s) skipped — original selector not found in source files:<br>'+d.skipped.map(k=>'<code>'+escHtml(k)+'</code>').join('<br>');btn.disabled=false;btn.textContent='🚀 Apply approved & create PR';}else{panel.className='error';panel.style.display='block';panel.innerHTML='⚠ '+(d.message??'Nothing was applied.');btn.disabled=false;btn.textContent='🚀 Apply approved & create PR';}}catch(err){panel.className='error';panel.style.display='block';panel.innerHTML='✖ '+escHtml(String(err));btn.disabled=false;btn.textContent='🚀 Apply approved & create PR';}}\nasync function clearAll(){if(!confirm('Clear all stored heals?'))return;await fetch('/api/clear',{method:'POST'});location.reload();}\nupdateCount();\n</script>\n</body></html>`;\n}\n\nconst server = http.createServer((req, res) => {\n  handleRequest(req, res).catch(err => { res.writeHead(500, { 'Content-Type': 'text/plain' }); res.end(String(err)); });\n});\n\nserver.listen(PORT, '127.0.0.1', () => {\n  console.log('');\n  console.log('  ╔══════════════════════════════════════════════════════════════╗');\n  console.log(`  ║  🩺  Healix Review Server  →  http://localhost:${PORT}         ║`);\n  console.log('  ║  Review AI-healed selectors, approve what is valid,         ║');\n  console.log('  ║  then click \"Apply approved & create PR\".                   ║');\n  console.log('  ║  Ctrl+C to exit after reviewing.                            ║');\n  console.log('  ╚══════════════════════════════════════════════════════════════╝');\n  console.log('');\n  const total = Object.keys(readStore()).length;\n  if (total === 0) console.log('  ⚠  No heals in store yet — run your tests first.');\n  else console.log(`  📋  ${total} healed selector(s) ready for review`);\n  console.log('');\n});\n\nserver.on('error', (err: NodeJS.ErrnoException) => {\n  if (err.code === 'EADDRINUSE') console.error(`  ✖  Port ${PORT} in use. Set HEALIX_REVIEW_PORT.`);\n  else console.error(`  ✖  Server error: ${err.message}`);\n  process.exit(1);\n});\n",
    "src/utils/storage-state/AuthManager.ts": "import { Browser, BrowserContext, Page } from '@playwright/test';\nimport * as fs   from 'fs';\nimport * as path from 'path';\nimport { logger } from '@utils/helpers/Logger';\nimport { environment } from '@config/environment';\n\nexport interface AuthCredentials {\n  username: string;\n  password: string;\n  role?:    string;\n}\n\n/**\n * AuthManager — manages browser storage-state for authenticated sessions.\n *\n * Usage:\n *   const auth = new AuthManager(browser);\n *   await auth.login();                          // saves storage-state to disk\n *   const ctx = await auth.createAuthenticatedContext();\n *\n * Customize `performLoginActions()` and `waitForLoginSuccess()` to match your app's\n * login flow and post-login indicators.\n */\nexport class AuthManager {\n  private storageStatePath: string;\n\n  constructor(private readonly browser: Browser) {\n    const env = environment.getConfig();\n    this.storageStatePath = env.storageStatePath;\n    const dir = path.dirname(this.storageStatePath);\n    if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });\n  }\n\n  async login(credentials?: AuthCredentials): Promise<void> {\n    const env   = environment.getConfig();\n    const creds = credentials ?? { username: env.authUsername, password: env.authPassword };\n    logger.info(`Performing login for user: ${creds.username}`);\n\n    const context = await this.browser.newContext();\n    const page    = await context.newPage();\n    try {\n      await page.goto(`${env.baseUrl}/login`, { timeout: env.navigationTimeout, waitUntil: 'domcontentloaded' });\n      await this.performLoginActions(page, creds);\n      await this.waitForLoginSuccess(page);\n      await this.saveStorageState(context);\n      logger.info('Login successful — storage state saved');\n    } catch (err) {\n      logger.error(`Login failed: ${String(err)}`);\n      throw err;\n    } finally {\n      await context.close();\n    }\n  }\n\n  /**\n   * Customize this method for your application's login form.\n   */\n  private async performLoginActions(page: Page, credentials: AuthCredentials): Promise<void> {\n    const env = environment.getConfig();\n    await page.fill('input[name=\"username\"], input[type=\"email\"], #username', credentials.username, { timeout: env.actionTimeout });\n    await page.fill('input[name=\"password\"], input[type=\"password\"], #password', credentials.password, { timeout: env.actionTimeout });\n    await page.click('button[type=\"submit\"], button:has-text(\"Login\"), button:has-text(\"Sign in\")', { timeout: env.actionTimeout });\n    logger.debug('Login form submitted');\n  }\n\n  /**\n   * Customize this method to detect successful login in your application.\n   */\n  private async waitForLoginSuccess(page: Page): Promise<void> {\n    const env = environment.getConfig();\n    try {\n      await Promise.race([\n        page.waitForURL(/dashboard|home|app/, { timeout: env.authTimeout }),\n        page.waitForSelector('[data-testid=\"user-menu\"], .user-profile, #dashboard', { timeout: env.authTimeout }),\n      ]);\n    } catch {\n      if (page.url().includes('login')) throw new Error('Login failed — still on login page');\n      throw new Error('Login success detection timed out');\n    }\n  }\n\n  async saveStorageState(context: BrowserContext, customPath?: string): Promise<void> {\n    const target = customPath ?? this.storageStatePath;\n    await context.storageState({ path: target });\n    logger.info(`Storage state saved: ${target}`);\n  }\n\n  async loadStorageState(customPath?: string): Promise<any> {\n    const target = customPath ?? this.storageStatePath;\n    if (!fs.existsSync(target)) { logger.warn(`Storage state not found: ${target}`); return null; }\n    try {\n      const state = JSON.parse(fs.readFileSync(target, 'utf-8'));\n      logger.info(`Storage state loaded: ${target}`);\n      return state;\n    } catch (err) { logger.error(`Failed to load storage state: ${String(err)}`); return null; }\n  }\n\n  async isStorageStateValid(customPath?: string): Promise<boolean> {\n    const target = customPath ?? this.storageStatePath;\n    if (!fs.existsSync(target)) return false;\n    try {\n      const state = JSON.parse(fs.readFileSync(target, 'utf-8'));\n      if (!state?.cookies?.length) return false;\n      const now = Date.now();\n      return state.cookies.some((c: any) => !c.expires || c.expires * 1_000 > now);\n    } catch { return false; }\n  }\n\n  async createAuthenticatedContext(forceLogin = false): Promise<BrowserContext> {\n    if (forceLogin || !(await this.isStorageStateValid())) {\n      logger.info('Creating fresh login session');\n      await this.login();\n    }\n    const state   = await this.loadStorageState();\n    const context = await this.browser.newContext({ storageState: state, viewport: { width: 1920, height: 1080 } });\n    logger.info('Authenticated context created');\n    return context;\n  }\n\n  async createAuthenticatedPage(forceLogin = false): Promise<Page> {\n    const ctx = await this.createAuthenticatedContext(forceLogin);\n    return ctx.newPage();\n  }\n\n  async verifyAuthentication(page: Page): Promise<boolean> {\n    const indicators = [\n      page.locator('[data-testid=\"user-menu\"]'),\n      page.locator('.user-profile'),\n      page.locator('#logout-button'),\n      page.locator('button:has-text(\"Logout\")'),\n    ];\n    for (const loc of indicators) {\n      try { await loc.waitFor({ timeout: 3_000 }); return true; } catch { /* try next */ }\n    }\n    if (page.url().includes('login')) { logger.warn('Not authenticated — on login page'); return false; }\n    return false;\n  }\n\n  async logout(page: Page): Promise<void> {\n    await page.click('button:has-text(\"Logout\"), #logout, [data-testid=\"logout\"]', { timeout: 5_000 });\n    await page.waitForURL(/login/, { timeout: 10_000 });\n    if (fs.existsSync(this.storageStatePath)) { fs.unlinkSync(this.storageStatePath); logger.info('Storage state cleared'); }\n  }\n\n  getStorageStatePath(): string { return this.storageStatePath; }\n  setStorageStatePath(p: string): void { this.storageStatePath = p; }\n}\n",
    "src/utils/storage-state/AuthSetup.ts": "import { chromium } from '@playwright/test';\nimport { AuthManager } from './AuthManager';\nimport { logger } from '@utils/helpers/Logger';\nimport { environment } from '@config/environment';\n\n/**\n * AuthSetup — CLI script to pre-authenticate and save browser storage state.\n *\n * Run once before your test suite to avoid logging in on every scenario:\n *   npm run auth:setup\n *\n * The saved storage-state file is loaded automatically by hooks.ts via the\n * `STORAGE_STATE_PATH` environment variable.\n */\nasync function setupAuth(): Promise<void> {\n  const env = environment.getConfig();\n  logger.info('Starting authentication setup');\n  logger.info(`Environment : ${env.environment}`);\n  logger.info(`Base URL    : ${env.baseUrl}`);\n\n  const browser = await chromium.launch({ headless: false });\n  try {\n    const authManager = new AuthManager(browser);\n    await authManager.login();\n    logger.info('✅ Authentication setup completed');\n    logger.info(`   Storage state: ${authManager.getStorageStatePath()}`);\n\n    const isValid = await authManager.isStorageStateValid();\n    if (isValid) {\n      logger.info('✅ Storage state validated');\n    } else {\n      logger.error('❌ Storage state validation failed');\n      process.exit(1);\n    }\n  } catch (err) {\n    logger.error(`❌ Authentication setup failed: ${String(err)}`);\n    process.exit(1);\n  } finally {\n    await browser.close();\n  }\n}\n\nif (require.main === module) {\n  setupAuth()\n    .then(() => process.exit(0))\n    .catch(err => { logger.error(`Unhandled error: ${String(err)}`); process.exit(1); });\n}\n\nexport { setupAuth };\n",
    "tsconfig.json": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2022\",\n    \"module\": \"commonjs\",\n    \"lib\": [\"ES2022\", \"dom\"],\n    \"outDir\": \"./dist\",\n    \"strict\": true,\n    \"esModuleInterop\": true,\n    \"skipLibCheck\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"resolveJsonModule\": true,\n    \"moduleResolution\": \"node\",\n    \"declaration\": true,\n    \"declarationMap\": true,\n    \"sourceMap\": true,\n    \"noUnusedLocals\": true,\n    \"noUnusedParameters\": true,\n    \"noImplicitReturns\": true,\n    \"noFallthroughCasesInSwitch\": true,\n    \"allowSyntheticDefaultImports\": true,\n    \"experimentalDecorators\": true,\n    \"emitDecoratorMetadata\": true,\n    \"types\": [\"node\", \"@types/node\"],\n    \"rootDir\": \"src\",\n    \"baseUrl\": \".\",\n    \"paths\": {\n      \"@config/*\": [\"src/config/*\"],\n      \"@hooks/*\": [\"src/hooks/*\"],\n      \"@pages/*\": [\"src/pages/*\"],\n      \"@utils/*\": [\"src/utils/*\"],\n      \"@test/*\": [\"src/test/*\"],\n      \"@features/*\": [\"src/test/features/*\"],\n      \"@steps/*\": [\"src/test/steps/*\"],\n      \"@locators/*\": [\"src/locators/*\"],\n      \"@fixtures/*\": [\"src/fixtures/*\"]\n    }\n  },\n  \"include\": [\n    \"src/**/*\",\n    \"src/test/features/**/*\",\n    \"src/test/steps/**/*.ts\"\n  ],\n  \"exclude\": [\"node_modules\", \"dist\", \"test-results\", \"src/templates\"],\n  \"ts-node\": {\n    \"require\": [\"tsconfig-paths/register\"],\n    \"transpileOnly\": true,\n    \"files\": true,\n    \"compilerOptions\": {\n      \"module\": \"commonjs\"\n    }\n  }\n}\n",
    "src/pages/index.ts": "// Page-objects barrel — re-exports every page class so callers can do\n// `import * as P from \"@pages\"` and reference `P.XxxPage`. The generator\n// and migration agents append entries here when they add pages.\nexport {};\n",
}

# fmt: on


# ──────────────────────────────────────────────────────────────────────────
# Single source of truth for LocatorHealer.ts
#
# The runtime LocatorHealer is GENERATED by
# `agent_playwright_generator.server._gen_locator_healer_cls`, which bakes in
# heal_intel intent-shape rules and AI-vision config. The migration pipeline
# and the playwright generator both scaffold from that generator. Historically
# this module kept its own frozen copy of LocatorHealer.ts, which drifted out
# of sync with the generator — the injector emitted calls to methods the frozen
# copy lacked (and vice versa), producing tsc errors in migrated projects.
#
# To prevent that drift, we OVERWRITE the frozen entry below with the live
# generator output at import time. The frozen string above is kept only as a
# fallback for environments where the generator's optional deps (mcp,
# python-dotenv) are unavailable AND the shim loader cannot stand them in.
def _canonical_locator_healer_ts() -> str | None:
    try:
        from stlc_agents.agent_playwright_generator.server import (
            _gen_locator_healer_cls,
        )
        return _gen_locator_healer_cls(True)
    except Exception:
        # Optional runtime deps (mcp / python-dotenv) may be missing when this
        # module is imported in a lightweight context. Reuse the migration's
        # dependency shim, which stubs those modules so the generator imports.
        try:
            from stlc_agents.agent_migration._migrate import (
                _load_scaffold_locator_repository_fn,
            )
            scaffold = _load_scaffold_locator_repository_fn()
            if scaffold is None:
                return None
            result = scaffold(
                output_dir="src/utils/locators",
                enable_ai_vision=True,
                repository_path="./self-heals/healed-locators.json",
                dashboard_port=7890,
                enable_timing_healing=True,
                enable_visual_regression=True,
            )
            return result.get("files", {}).get(
                "src/utils/locators/LocatorHealer.ts"
            )
        except Exception:
            return None


_generated_healer = _canonical_locator_healer_ts()
if _generated_healer:
    BOILERPLATE["src/utils/locators/LocatorHealer.ts"] = _generated_healer


# Subset used for infra scaffolding (src/utils/locators/)
INFRA_FILES = {
    k.removeprefix("src/utils/locators/"): v
    for k, v in BOILERPLATE.items()
    if k.startswith("src/utils/locators/")
}
