import {
  coderTaskToPrompt
} from "./chunk-5IBQAPVB.js";
import {
  UI_FINDING_SEVERITIES,
  UI_LENSES
} from "./chunk-DPEUKJRO.js";
import "./chunk-DGUM43GV.js";

// src/audit/issue-writer.ts
import { promises as fs } from "fs";
import path from "path";

// src/profiles/ui-auditor/slugify.ts
function slugify(value, fieldName) {
  const slug = value.toLowerCase().normalize("NFKD").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80);
  if (slug.length === 0) {
    throw new Error(
      `ui-auditor: ${fieldName} slugified to empty string. Provide a non-empty value (got ${JSON.stringify(value)}).`
    );
  }
  return slug;
}

// src/audit/issue-writer.ts
var SEVERITY_ORDER = {
  critical: 0,
  high: 1,
  med: 2,
  low: 3
};
function assertValidWorkspaceDir(dir) {
  if (typeof dir !== "string" || dir.length === 0) {
    throw new Error(
      `audit-writer: workspaceDir must be a non-empty string (got ${JSON.stringify(dir)})`
    );
  }
  if (!path.isAbsolute(dir)) {
    throw new Error(
      `audit-writer: workspaceDir must be an absolute path (got ${JSON.stringify(dir)})`
    );
  }
  if (dir.split(path.sep).includes("..")) {
    throw new Error(
      `audit-writer: workspaceDir must not contain '..' segments (got ${JSON.stringify(dir)})`
    );
  }
}
async function initAuditWorkspace(workspaceDir) {
  assertValidWorkspaceDir(workspaceDir);
  await fs.mkdir(path.join(workspaceDir, "issues"), { recursive: true });
  await fs.mkdir(path.join(workspaceDir, "screenshots"), { recursive: true });
  const regPath = path.join(workspaceDir, "registry.json");
  try {
    await fs.access(regPath);
  } catch {
    const fresh = { schemaVersion: 1, findings: [], routes: {} };
    await fs.writeFile(regPath, JSON.stringify(fresh, null, 2));
  }
}
async function readAuditRegistry(workspaceDir) {
  assertValidWorkspaceDir(workspaceDir);
  const regPath = path.join(workspaceDir, "registry.json");
  const text = await fs.readFile(regPath, "utf8");
  const parsed = JSON.parse(text);
  if (!parsed || typeof parsed !== "object") {
    throw new Error(`audit-writer: registry.json at ${regPath} is not a JSON object`);
  }
  const reg = parsed;
  if (reg.schemaVersion !== 1 || !Array.isArray(reg.findings) || typeof reg.routes !== "object") {
    throw new Error(`audit-writer: registry.json at ${regPath} has an unrecognized shape`);
  }
  return reg;
}
async function writeAuditRegistry(workspaceDir, reg) {
  const regPath = path.join(workspaceDir, "registry.json");
  const tmpPath = `${regPath}.tmp-${process.pid}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
  await fs.writeFile(tmpPath, JSON.stringify(reg, null, 2));
  await fs.rename(tmpPath, regPath);
}
var workspaceLocks = /* @__PURE__ */ new Map();
async function withWorkspaceLock(workspaceDir, fn) {
  const key = path.resolve(workspaceDir);
  const prev = workspaceLocks.get(key) ?? Promise.resolve();
  let signalDone;
  const done = new Promise((resolve) => {
    signalDone = resolve;
  });
  workspaceLocks.set(key, done);
  try {
    await prev;
    return await fn();
  } finally {
    signalDone();
    if (workspaceLocks.get(key) === done) workspaceLocks.delete(key);
  }
}
function assertFindingShape(f, index) {
  const where = `audit-writer: findings[${index}]`;
  if (!UI_LENSES.includes(f.lens)) {
    throw new Error(
      `${where}: invalid lens ${JSON.stringify(f.lens)}; one of ${UI_LENSES.join("|")}`
    );
  }
  if (!UI_FINDING_SEVERITIES.includes(f.severity)) {
    throw new Error(
      `${where}: invalid severity ${JSON.stringify(f.severity)}; one of ${UI_FINDING_SEVERITIES.join("|")}`
    );
  }
  for (const field of ["title", "route", "observation", "impact", "suggestedFix"]) {
    const v = f[field];
    if (typeof v !== "string" || v.trim().length === 0) {
      throw new Error(`${where}: ${field} must be a non-empty string`);
    }
  }
  if (!Array.isArray(f.screenshots) || f.screenshots.length === 0) {
    throw new Error(`${where}: screenshots must be a non-empty array`);
  }
  for (let i = 0; i < f.screenshots.length; i += 1) {
    const s = f.screenshots[i];
    if (!s || typeof s.path !== "string" || s.path.length === 0) {
      throw new Error(`${where}.screenshots[${i}].path must be a non-empty string`);
    }
    if (s.path.split(/[/\\]/).includes("..")) {
      throw new Error(`${where}.screenshots[${i}].path must not contain '..' segments`);
    }
  }
}
function nextFindingId(reg) {
  let max = 0;
  for (const f of reg.findings) {
    if (typeof f.id === "number" && f.id > max) max = f.id;
  }
  return max + 1;
}
function slugifyTitle(title) {
  return slugify(title, "title");
}
function renderFinding(finding) {
  if (!finding.id) {
    throw new Error("audit-writer: cannot render a finding without an assigned id");
  }
  if (finding.screenshots.length === 0) {
    throw new Error(`audit-writer: finding #${finding.id} has no screenshots`);
  }
  const id = finding.id;
  const numStr = String(id).padStart(3, "0");
  const metaLines = [
    `> **Issue #${numStr}** \xB7 **Severity:** ${finding.severity} \xB7 **Lens:** ${finding.lens}`,
    `> **Route:** \`${finding.route}\`${finding.url ? ` \xB7 **URL:** ${finding.url}` : ""}`
  ];
  if (finding.viewport) metaLines.push(`> **Viewport:** ${finding.viewport}`);
  if (finding.selector) metaLines.push(`> **Selector:** \`${finding.selector}\``);
  if (finding.tags && finding.tags.length > 0) {
    metaLines.push(`> **Tags:** ${finding.tags.map((t) => `\`${t}\``).join(", ")}`);
  }
  if (finding.similarTo && finding.similarTo.length > 0) {
    metaLines.push(
      `> **Possible duplicates:** ${finding.similarTo.map((n) => `#${String(n).padStart(3, "0")}`).join(", ")}`
    );
  }
  metaLines.push(`> _Generated: ${finding.createdAt ?? (/* @__PURE__ */ new Date()).toISOString()}_`);
  const reproSteps = finding.reproSteps ?? `1. Open \`${finding.route}\`${finding.url ? ` (${finding.url})` : ""}
2. Observe the area shown in the screenshot${finding.selector ? ` (selector: \`${finding.selector}\`)` : ""}.`;
  const evidence = finding.screenshots.map((s) => `![${path.basename(s.path)}](../${s.path})`).join("\n\n");
  return [
    `# [UI] ${finding.title}`,
    "",
    metaLines.join("\n"),
    "",
    "## Observation",
    finding.observation.trim(),
    "",
    "## Why it matters",
    finding.impact.trim(),
    "",
    "## Steps to reproduce",
    reproSteps,
    "",
    "## Suggested fix",
    finding.suggestedFix.trim(),
    "",
    "## Evidence",
    evidence,
    "",
    "---",
    `<sub>Generated by agent-runtime/ui-auditor \xB7 lens=\`${finding.lens}\` \xB7 severity=\`${finding.severity}\`</sub>`,
    ""
  ].join("\n");
}
async function appendFindings(workspaceDir, findings) {
  assertValidWorkspaceDir(workspaceDir);
  for (let i = 0; i < findings.length; i += 1) {
    const f = findings[i];
    if (!f) throw new Error(`audit-writer: findings[${i}] is undefined`);
    assertFindingShape(f, i);
  }
  return withWorkspaceLock(workspaceDir, async () => {
    await initAuditWorkspace(workspaceDir);
    const reg = await readAuditRegistry(workspaceDir);
    const usedIds = /* @__PURE__ */ new Set();
    for (const f of reg.findings) {
      if (typeof f.id === "number") usedIds.add(f.id);
    }
    const written = [];
    const files = [];
    let nextId = nextFindingId(reg);
    for (const incoming of findings) {
      let id = incoming.id;
      if (id !== void 0) {
        if (usedIds.has(id)) {
          throw new Error(
            `audit-writer: incoming finding id ${id} (title=${JSON.stringify(incoming.title)}) collides with an existing registry entry`
          );
        }
      } else {
        id = nextId;
        while (usedIds.has(id)) id += 1;
        nextId = id + 1;
      }
      usedIds.add(id);
      const createdAt = incoming.createdAt ?? (/* @__PURE__ */ new Date()).toISOString();
      const persisted = { ...incoming, id, createdAt };
      const slug = slugifyTitle(persisted.title);
      const fileName = `${String(id).padStart(3, "0")}--${persisted.lens}--${slug}.md`;
      const filePathAbs = path.join(workspaceDir, "issues", fileName);
      const filePathRel = `issues/${fileName}`;
      await fs.writeFile(filePathAbs, renderFinding(persisted));
      reg.findings.push(persisted);
      written.push(persisted);
      files.push(filePathRel);
    }
    await writeAuditRegistry(workspaceDir, reg);
    return { written, files };
  });
}
async function registerCaptures(workspaceDir, options) {
  assertValidWorkspaceDir(workspaceDir);
  if (typeof options.route !== "string" || options.route.trim().length === 0) {
    throw new Error("audit-writer: registerCaptures: route must be a non-empty string");
  }
  return withWorkspaceLock(workspaceDir, async () => {
    await initAuditWorkspace(workspaceDir);
    const reg = await readAuditRegistry(workspaceDir);
    const slot = reg.routes[options.route] ?? { captures: [] };
    if (options.url) slot.url = options.url;
    slot.captures = [...slot.captures, ...options.captures];
    reg.routes[options.route] = slot;
    await writeAuditRegistry(workspaceDir, reg);
  });
}
function summarizeRegistry(reg) {
  const bySeverity = {
    critical: 0,
    high: 0,
    med: 0,
    low: 0
  };
  const byLens = {};
  const byRoute = {};
  for (const f of reg.findings) {
    bySeverity[f.severity] += 1;
    byLens[f.lens] = (byLens[f.lens] ?? 0) + 1;
    byRoute[f.route] = (byRoute[f.route] ?? 0) + 1;
  }
  return { total: reg.findings.length, bySeverity, byLens, byRoute };
}
async function writeAuditIndex(workspaceDir) {
  assertValidWorkspaceDir(workspaceDir);
  const reg = await readAuditRegistry(workspaceDir);
  const summary = summarizeRegistry(reg);
  const sorted = [...reg.findings].sort((a, b) => {
    const sa = SEVERITY_ORDER[a.severity];
    const sb = SEVERITY_ORDER[b.severity];
    if (sa !== sb) return sa - sb;
    return (a.id ?? 0) - (b.id ?? 0);
  });
  const lines = [];
  lines.push("# UI audit index", "");
  lines.push(`_Generated: ${(/* @__PURE__ */ new Date()).toISOString()}_`, "");
  lines.push(`**Total findings:** ${summary.total}`, "");
  lines.push("## By severity");
  for (const s of ["critical", "high", "med", "low"]) {
    const n = summary.bySeverity[s] ?? 0;
    if (n > 0) lines.push(`- **${s}** \u2014 ${n}`);
  }
  lines.push("", "## By lens");
  const lensEntries = Object.entries(summary.byLens).map(([k, v]) => [
    k,
    v ?? 0
  ]);
  for (const [lens, n] of lensEntries.sort((a, b) => b[1] - a[1])) {
    lines.push(`- **${lens}** \u2014 ${n}`);
  }
  lines.push("", "## By route");
  const routeEntries = Object.entries(summary.byRoute).map(([k, v]) => [
    k,
    v ?? 0
  ]);
  for (const [route, n] of routeEntries.sort((a, b) => b[1] - a[1])) {
    lines.push(`- \`${route}\` \u2014 ${n}`);
  }
  lines.push("", "## Findings", "");
  for (const f of sorted) {
    if (typeof f.id !== "number") continue;
    const num = String(f.id).padStart(3, "0");
    const slug = slugifyTitle(f.title);
    const file = `issues/${num}--${f.lens}--${slug}.md`;
    lines.push(`- [#${num} \`${f.severity}\` \`${f.lens}\` \`${f.route}\` \u2014 ${f.title}](${file})`);
  }
  lines.push("");
  const out = path.join(workspaceDir, "index.md");
  const body = lines.join("\n");
  await fs.writeFile(out, body);
  return out;
}

// src/profiles/ui-auditor/prompt.ts
var ENVELOPE_BEGIN = "<<UI_AUDIT_TASK>>";
var ENVELOPE_END = "<<UI_AUDIT_TASK_END>>";
function encodeAuditTaskEnvelope(task) {
  return `${ENVELOPE_BEGIN}${JSON.stringify(task)}${ENVELOPE_END}`;
}
function decodeAuditTaskEnvelope(prompt) {
  const start = prompt.indexOf(ENVELOPE_BEGIN);
  if (start === -1) return void 0;
  const payloadStart = start + ENVELOPE_BEGIN.length;
  const end = prompt.indexOf(ENVELOPE_END, payloadStart);
  if (end === -1) return void 0;
  const payload = prompt.slice(payloadStart, end);
  try {
    const parsed = JSON.parse(payload);
    if (!parsed || typeof parsed !== "object") return void 0;
    const t = parsed;
    if (typeof t.lens !== "string" || !Array.isArray(t.captures)) return void 0;
    return t;
  } catch {
    return void 0;
  }
}
function formatAuditorPrompt(task) {
  const lines = [];
  lines.push(`# UI audit iteration \u2014 lens: ${task.lens}`);
  lines.push("");
  if (task.productContext && task.productContext.trim().length > 0) {
    lines.push("## Product context");
    lines.push(task.productContext.trim());
    lines.push("");
  }
  lines.push("## Captures to take");
  task.captures.forEach((cap, i) => {
    const vp = cap.viewport ? `${cap.viewport.width}x${cap.viewport.height}` : "1280x800 (default)";
    const detail = [
      `viewport=${vp}`,
      cap.fullPage ? "fullPage=true" : null,
      cap.elementSelector ? `selector=\`${cap.elementSelector}\`` : null,
      cap.waitFor ? `waitFor=\`${cap.waitFor}\`` : null,
      cap.waitMs !== void 0 ? `waitMs=${cap.waitMs}` : null,
      cap.label ? `label=${cap.label}` : null
    ].filter((s) => s !== null).join(" \xB7 ");
    lines.push(`${i + 1}. route=\`${cap.route}\` url=${cap.url} ${detail ? `(${detail})` : ""}`);
  });
  lines.push("");
  if (task.knownFindingIds && task.knownFindingIds.length > 0) {
    lines.push("## Known findings (link via similarTo, do not refile)");
    lines.push(task.knownFindingIds.map((n) => `#${String(n).padStart(3, "0")}`).join(", "));
    lines.push("");
  }
  lines.push("## Output format");
  lines.push(
    "Emit a single JSON object with the shape `{ findings: UiFinding[], notes?: string }` where every finding has the fields enumerated in your system prompt. The screenshots field on each finding must reference the captures above by path. Do not emit findings outside the lens."
  );
  return lines.join("\n");
}

// src/profiles/ui-auditor/in-process-client.ts
function asSandboxEvent(type, data) {
  return { type, data };
}
var DEFAULT_VIEWPORT = { width: 1280, height: 800 };
var NAV_TIMEOUT_MS = 3e4;
async function defaultLaunch() {
  const mod = await import("playwright");
  if (!mod?.chromium || typeof mod.chromium.launch !== "function") {
    throw new Error(
      "ui-auditor: playwright is not installed. Install `playwright` (and run `playwright install chromium`) or pass a custom `launchBrowser` to createInProcessUiAuditClient."
    );
  }
  return mod.chromium.launch({ headless: true });
}
function nowStamp() {
  const d = /* @__PURE__ */ new Date();
  const pad = (n) => String(n).padStart(2, "0");
  return `${d.getUTCFullYear()}${pad(d.getUTCMonth() + 1)}${pad(d.getUTCDate())}-${pad(d.getUTCHours())}${pad(d.getUTCMinutes())}${pad(d.getUTCSeconds())}-${String(d.getUTCMilliseconds()).padStart(3, "0")}`;
}
function viewportOf(req) {
  return req.viewport ?? DEFAULT_VIEWPORT;
}
function captureFilename(req) {
  const vp = viewportOf(req);
  const labelPart = req.label ? `--${slugify(req.label, "label")}` : "";
  return `${slugify(req.route, "route")}--${vp.width}x${vp.height}${labelPart}--${nowStamp()}.png`;
}
function assertHttpUrl(url) {
  let parsed;
  try {
    parsed = new URL(url);
  } catch {
    throw new Error(`ui-auditor: capture url is not parseable (got ${JSON.stringify(url)})`);
  }
  if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
    throw new Error(
      `ui-auditor: capture url must use http or https (got ${parsed.protocol} in ${JSON.stringify(url)})`
    );
  }
}
async function captureOne(page, req, outAbsPath, signal, navPolicy) {
  signal.throwIfAborted();
  assertHttpUrl(req.url);
  await page.setViewportSize(viewportOf(req));
  const waitUntil = navPolicy === "spa" ? "domcontentloaded" : "networkidle";
  await page.goto(req.url, { waitUntil, timeout: NAV_TIMEOUT_MS });
  if (req.waitFor) {
    await page.waitForSelector(req.waitFor, { timeout: 15e3 });
  }
  const extra = req.waitMs ?? 500;
  if (extra > 0) await page.waitForTimeout(extra);
  signal.throwIfAborted();
  if (req.elementSelector) {
    await page.locator(req.elementSelector).first().screenshot({ path: outAbsPath });
  } else {
    await page.screenshot({ path: outAbsPath, fullPage: req.fullPage === true });
  }
}
function makeSandboxId() {
  const rand = () => Math.random().toString(16).slice(2, 10);
  return `ui-audit-${rand()}${rand()}`;
}
function createInProcessUiAuditClient(options) {
  const launch = options.launchBrowser ?? defaultLaunch;
  const navPolicy = options.navPolicy ?? "strict";
  let browserPromise;
  let closed = false;
  async function getBrowser() {
    if (closed) {
      throw new Error("ui-auditor: client is closed; create a new client to run another iteration");
    }
    if (!browserPromise) browserPromise = launch();
    return browserPromise;
  }
  async function* runIteration(promptText, signal) {
    const task = decodeAuditTaskEnvelope(promptText);
    if (!task) {
      throw new Error(
        "ui-auditor: prompt is missing a UI_AUDIT_TASK envelope. Use uiAuditorProfile().taskToPrompt to format prompts, or pass an envelope-prefixed prompt manually."
      );
    }
    if (task.captures.length === 0) {
      throw new Error("ui-auditor: task has zero captures; nothing to audit.");
    }
    yield asSandboxEvent("audit.lens", { lens: task.lens });
    const browser = await getBrowser();
    const context = await browser.newContext({ viewport: DEFAULT_VIEWPORT });
    let primaryError;
    let closeError;
    try {
      const page = await context.newPage();
      const captures = [];
      const fs2 = await import("fs/promises");
      const path2 = await import("path");
      const shotsDir = path2.join(options.workspaceDir, "screenshots");
      await fs2.mkdir(shotsDir, { recursive: true });
      for (const req of task.captures) {
        signal.throwIfAborted();
        const filename = captureFilename(req);
        const absPath = path2.join(shotsDir, filename);
        const relPath = `screenshots/${filename}`;
        await captureOne(page, req, absPath, signal, navPolicy);
        const vp = viewportOf(req);
        const cap = {
          path: relPath,
          viewport: `${vp.width}x${vp.height}`,
          fullPage: req.fullPage === true,
          route: req.route,
          url: req.url,
          capturedAt: (/* @__PURE__ */ new Date()).toISOString()
        };
        if (req.elementSelector) cap.elementSelector = req.elementSelector;
        if (req.label) cap.label = req.label;
        captures.push(cap);
        yield asSandboxEvent("audit.capture", cap);
      }
      const judgeOut = await options.judge({
        lens: task.lens,
        captures,
        productContext: task.productContext,
        knownFindingIds: task.knownFindingIds,
        promptText,
        signal
      });
      for (const finding of judgeOut.findings) {
        yield asSandboxEvent("audit.finding", finding);
      }
      if (judgeOut.notes && judgeOut.notes.trim().length > 0) {
        yield asSandboxEvent("audit.notes", { notes: judgeOut.notes });
      }
      const usage = judgeOut.tokenUsage ?? { input: 0, output: 0 };
      yield asSandboxEvent("done", {
        tokenUsage: {
          inputTokens: usage.input,
          outputTokens: usage.output
        },
        totalCostUsd: judgeOut.costUsd ?? 0
      });
    } catch (err) {
      primaryError = err;
    } finally {
      try {
        await context.close();
      } catch (err) {
        closeError = err;
      }
    }
    if (primaryError !== void 0 && closeError !== void 0) {
      throw new AggregateError(
        [primaryError, closeError],
        "ui-auditor: iteration failed AND context.close() failed; both errors attached."
      );
    }
    if (primaryError !== void 0) throw primaryError;
    if (closeError !== void 0) throw closeError;
  }
  function makeSyntheticSandbox() {
    const id = makeSandboxId();
    const instance = {
      id,
      streamPrompt(message, opts) {
        const signal = opts?.signal ?? new AbortController().signal;
        return runIteration(message, signal);
      },
      async delete() {
      }
    };
    return instance;
  }
  return {
    async create(_options) {
      return makeSyntheticSandbox();
    },
    describePlacement(box) {
      const id = box.id;
      return { kind: "sibling", sandboxId: typeof id === "string" ? id : void 0 };
    },
    async close() {
      closed = true;
      const pending = browserPromise;
      browserPromise = void 0;
      if (pending) {
        const browser = await pending;
        await browser.close();
      }
    }
  };
}

// src/profiles/ui-auditor/lens-prompts.ts
var SHARED_AUDITOR_RULES = `
You are auditing a UI for a specific class of problems. Stay strictly in your assigned lens \u2014 do not file issues that belong to another lens (a separate iteration will catch those).

A finding is only valid if a thoughtful product designer would agree the screenshot shows something that should change. Avoid:
- Personal taste ("I'd prefer brand blue").
- Hallucinated text or controls you cannot actually see in the screenshot.
- Suggestions that depend on requirements you don't have access to.
- Pile-on findings about the same root cause \u2014 file ONE finding and use \`similarTo\` to link the rest.

Required for every finding:
- title: concrete, names the offending element AND what's wrong (NOT "improve UX").
- severity: critical=blocks a core task or accessibility blocker; high=noticeable friction; med=visible polish issue; low=nitpick.
- observation: 1\u20133 sentences describing exactly what you see that is wrong.
- impact: who is affected and how (concrete).
- suggestedFix: a specific change a developer could apply without asking you back.
- screenshots: refer to the captures attached to this iteration by path.
- selector: when you can pin the offending element with a CSS selector.

Most findings are med or low. Reserve high/critical for genuine blockers.
`.trim();
var LENS_BRIEFS = {
  consistency: `
LENS: consistency
Look for inconsistencies in the design system \u2014 things that look like they came from different products glued together.
Signals: multiple font families, inconsistent weights/sizes for the same role, two shades of "primary", arbitrary paddings/margins that don't snap to a scale (4/8/12/16/24), same control with different border-radius or shadow on different pages, mixed icon styles (filled vs outlined), inconsistent button heights/padding for the same variant, inconsistent capitalization (Title Case vs sentence case) for the same role.
NOT this lens: layout misalignment (use \`layout\`), confusing user flow (use \`ux-flow\`), contrast/keyboard issues (use \`accessibility\`).
Title format: \`Inconsistent <thing> between <A> and <B>\`.
`.trim(),
  hierarchy: `
LENS: hierarchy
Look for broken visual hierarchy \u2014 places where the eye does not land on what matters most.
Signals: primary CTA same weight as secondary/tertiary controls, headings (H1/H2/H3) nearly the same size, important data buried (headline number smaller than its label), decoration outshining content, too many emphases competing, wrong scan order, missing or overly heavy section dividers.
NOT this lens: same-role styled differently (\`consistency\`), grid/alignment (\`layout\`), contrast-failing text (\`accessibility\`).
Title format: \`Weak hierarchy: <element> does not read as the <intended-role>\`.
`.trim(),
  layout: `
LENS: layout
Look for layout and organization problems \u2014 alignment, grouping, whitespace, structural choices that hurt scannability.
Signals: misalignment within rows, inconsistent gutters in grids, orphan whitespace next to crammed regions, poor grouping (related fields separated, unrelated fields adjacent), no visual sections (long wall of content), container overflow (text/content punching out of card boundaries), cramped or oversized hit targets, sidebars/headers sized wrong relative to main content.
NOT this lens: same-role styled differently (\`consistency\`), click-distance/friction (\`ux-flow\`), overflow specifically at small viewports (\`responsive\`).
Title format: \`<Region> alignment/spacing problem\` or \`<Region> grouping unclear\`.
`.trim(),
  "ux-flow": `
LENS: ux-flow
Look for interaction-flow friction \u2014 action sequences that are slower, more annoying, or more error-prone than necessary.
Signals: sequential clicks far apart (e.g. Next top-right while user is bottom-left), destructive action adjacent to primary with same weight, confirmations that don't say what's being confirmed, primary CTA below the fold or hidden in a kebab menu, silent state changes (toggle gives no feedback), form ordering that fights real-world order, dead-end states after submit, lost inputs on back-navigation, hidden pre-selected options.
NOT this lens: visual style only (\`consistency\`), component arrangement without a flow problem (\`layout\`), microcopy clarity (\`content\`).
Title format: \`<Action A> \u2192 <Action B> friction: <root cause>\`.
`.trim(),
  duplication: `
LENS: duplication
Look for redundancy \u2014 the same control, link, or piece of content appearing more than once with no good reason.
Signals: two ways to do the same action on the same screen with no difference, repeated nav (same links in sidebar AND top nav), drifted duplicates (two copies that have diverged), content repeated verbatim, icon + label saying the same thing twice in one row, per-row + bulk actions that overlap confusingly, multiple status indicators conveying the same status.
NOT this lens: inconsistent styling of duplicates (\`consistency\`) \u2014 this lens is about the existence of duplicates.
Title format: \`Duplicate <thing> in <location A> and <location B>\`.
`.trim(),
  accessibility: `
LENS: accessibility
Look for accessibility blockers and degradations. Be conservative \u2014 do not assume violations you cannot see.
Signals: insufficient contrast on body text or controls, missing/invisible focus styles, tiny tap targets (<24px on mobile), color as sole signal (red border with no message), form labels missing or not associated (placeholders standing in for labels), broken heading order (H1 \u2192 H4), modals that don't trap focus, decorative elements that take focus, errors not announced, important text rendered inside images.
NOT this lens: generic "looks confusing" (\`hierarchy\` or \`content\`), layout overflow at small viewports (\`responsive\`).
Title format: \`Accessibility: <specific blocker> in <element>\`.
`.trim(),
  responsive: `
LENS: responsive
Look for layout breakage across viewport sizes \u2014 content that works at one width but degrades at another. This iteration's captures should include the same surface at >=2 viewports; compare across them.
Signals: horizontal scroll where content should reflow, overlapping elements (header overlaps content, fixed footer covers inputs), desktop nav crammed into mobile without collapsing, table columns that don't truncate, tap targets too close at touch sizes, controls vanishing at certain widths, layout flips that break grouping order, modals exceeding viewport height (confirm button unreachable).
NOT this lens: issues present at every viewport (\`consistency\` / \`hierarchy\` instead).
Title format: \`<Element/Region> breaks at <viewport>\`.
`.trim(),
  states: `
LENS: states
Look for missing or broken UI states \u2014 the not-happy-paths that make a product feel finished or unfinished. The iteration's captures should depict at least one non-default state.
Signals: empty lists with no guidance, skeletons that don't match final layout (CLS on settle), error states with no message or recovery action, disabled buttons with no explanation, toasts that disappear before being read, success states that don't confirm, missing hover/focus/active/disabled variants on primary controls, no long-content view, no-permission state broken.
NOT this lens: generic polish on the happy path (other lenses), missing focus rings specifically (\`accessibility\`).
Title format: \`Missing/broken <state> state on <surface>\`.
`.trim(),
  content: `
LENS: content
Look for microcopy and content problems \u2014 text that is unclear, inconsistent, condescending, jargon-heavy, or wrong.
Signals: jargon/internal language leaking ("Provisioning a Tenant" instead of "Setting up your account"), inconsistent terminology (workspace vs team), verbose button labels, empty-state copy that's just "No results", error messages blaming the user, tone inconsistency, truncation without affordance, mixed date/number formats on one page, placeholder used as a label, "Saved!" toast appearing before save completes, typos and grammar errors.
NOT this lens: visual treatment of text (\`hierarchy\` / \`consistency\`), missing labels for a11y (\`accessibility\`).
Title format: \`Copy: "<actual text>" in <location>\` or \`Inconsistent term: "<A>" vs "<B>"\`.
`.trim(),
  interaction: `
LENS: interaction
Look for interaction quality problems \u2014 affordances, feedback, and micro-interactions.
Signals: no affordance (clickable areas not looking clickable, non-clickable areas looking clickable), missing feedback (>100ms click with no progress), hover surprises (whole row highlights but only title clickable), cursor inconsistency, animations that block input, missing transitions where they're needed (accordion snaps open), drag-and-drop without indicators, scroll-jacking, click-through bugs (card click handler firing alongside button), hover-only revelations on touch.
NOT this lens: position of controls (\`layout\` / \`ux-flow\`), missing focus styles (\`accessibility\`).
Title format: \`<Action> on <element>: <missing/wrong> feedback\`.
`.trim(),
  "performance-perceived": `
LENS: performance-perceived
Look for perceived-performance problems \u2014 visible jank a real user would notice, not benchmark numbers. This iteration's captures should include >=2 frames during load to show shift.
Signals: layout shift (CLS) when late-arriving images/fonts/banners settle, FOUC (flash of unstyled content), font swap jumps, late-loading hero images that shift everything, skeletons that don't match final shape, spinners on instant local actions, loading state reappearing after content paints (refetch on focus), modal open animation longer than the operation it precedes.
NOT this lens: slow API calls (file separately), stale data after navigation (\`states\`).
Title format: \`Layout shift / late paint on <route>: <root cause>\`.
`.trim(),
  other: `
LENS: other
Use ONLY when a finding is clearly a UI quality issue but does not fit any other lens. Strongly prefer a specific lens \u2014 \`other\` should be rare. Title must still be concrete.
`.trim()
};
function buildAuditorSystemPrompt(lens) {
  const brief = LENS_BRIEFS[lens];
  return `${SHARED_AUDITOR_RULES}

${brief}`;
}

// src/profiles/ui-auditor/output-adapter.ts
var KNOWN_LENS_VALUES = new Set(UI_LENSES);
function isUiLens(v) {
  return typeof v === "string" && KNOWN_LENS_VALUES.has(v);
}
function parseAuditorEvents(events) {
  const findings = [];
  const captures = [];
  let lens;
  let notes;
  for (const evt of events) {
    if (!evt || typeof evt !== "object") continue;
    const type = String(evt.type ?? "");
    const data = evt.data && typeof evt.data === "object" ? evt.data : void 0;
    if (!data) continue;
    switch (type) {
      case "audit.lens": {
        const v = data.lens;
        if (isUiLens(v)) lens = v;
        break;
      }
      case "audit.capture": {
        const cap = data;
        if (typeof cap.path === "string" && typeof cap.viewport === "string" && typeof cap.fullPage === "boolean" && typeof cap.route === "string" && typeof cap.url === "string" && typeof cap.capturedAt === "string") {
          const out2 = {
            path: cap.path,
            viewport: cap.viewport,
            fullPage: cap.fullPage,
            route: cap.route,
            url: cap.url,
            capturedAt: cap.capturedAt
          };
          if (cap.elementSelector) out2.elementSelector = cap.elementSelector;
          if (cap.label) out2.label = cap.label;
          captures.push(out2);
        }
        break;
      }
      case "audit.finding": {
        const f = data;
        if (typeof f.title === "string" && f.title.trim().length > 0 && isUiLens(f.lens) && typeof f.severity === "string" && ["low", "med", "high", "critical"].includes(f.severity) && typeof f.route === "string" && typeof f.observation === "string" && typeof f.impact === "string" && typeof f.suggestedFix === "string" && Array.isArray(f.screenshots)) {
          findings.push(f);
        }
        break;
      }
      case "audit.notes": {
        const n = data.notes;
        if (typeof n === "string" && n.trim().length > 0) notes = n;
        break;
      }
      default:
        break;
    }
  }
  const out = { lens: lens ?? "other", findings, captures };
  if (notes) out.notes = notes;
  return out;
}

// src/profiles/ui-auditor/validator.ts
var GENERIC_TITLE_PATTERNS = [
  /^improve\s/i,
  /^fix\s/i,
  /^update\s/i,
  /^better\s/i,
  /^bad\s/i,
  /^make\s.+\sbetter/i,
  /\bUX\b\s*$/i,
  /\bUI\b\s*$/i
];
function isGenericTitle(title) {
  const t = title.trim();
  if (t.length < 16) return true;
  return GENERIC_TITLE_PATTERNS.some((re) => re.test(t));
}
function createUiAuditorValidator(task) {
  return {
    async validate(output) {
      const findings = output.findings;
      const captures = output.captures;
      const capturePaths = new Set(captures.map((c) => c.path));
      const offLens = findings.filter((f) => f.lens !== task.lens);
      if (offLens.length > 0) {
        const verdict2 = {
          valid: false,
          score: 0,
          notes: `${offLens.length} finding(s) filed under wrong lens (expected ${task.lens}; got ${offLens.map((f) => f.lens).join(", ")})`,
          scores: { offLens: 0 }
        };
        return verdict2;
      }
      const missingEvidence = findings.filter(
        (f) => !Array.isArray(f.screenshots) || f.screenshots.length === 0
      );
      if (missingEvidence.length > 0) {
        const verdict2 = {
          valid: false,
          score: 0,
          notes: `${missingEvidence.length} finding(s) have no screenshot evidence`,
          scores: { evidence: 0 }
        };
        return verdict2;
      }
      const unresolvedShot = findings.filter(
        (f) => f.screenshots.some((s) => !capturePaths.has(s.path))
      );
      if (unresolvedShot.length > 0) {
        const verdict2 = {
          valid: false,
          score: 0,
          notes: `${unresolvedShot.length} finding(s) reference screenshot paths not captured this iteration`,
          scores: { evidence: 0 }
        };
        return verdict2;
      }
      if (findings.length === 0) {
        const verdict2 = {
          valid: true,
          score: 0.5,
          notes: "No findings reported. Neither a confident pass nor a failure.",
          scores: { specificity: 0, evidence: 1, titles: 1 }
        };
        return verdict2;
      }
      const withSelector = findings.filter((f) => typeof f.selector === "string").length;
      const specificity = withSelector / findings.length;
      const generic = findings.filter((f) => isGenericTitle(f.title)).length;
      const titles = 1 - generic / findings.length;
      const withFullEvidence = findings.filter(
        (f) => Array.isArray(f.screenshots) && f.screenshots.length > 0 && f.screenshots.every((s) => capturePaths.has(s.path))
      ).length;
      const evidence = withFullEvidence / findings.length;
      const score = Number((0.4 * specificity + 0.4 * evidence + 0.2 * titles).toFixed(4));
      const verdict = {
        valid: true,
        score,
        notes: `${findings.length} finding(s) \u2014 specificity=${specificity.toFixed(2)} evidence=${evidence.toFixed(2)} titles=${titles.toFixed(2)}`,
        scores: { specificity, evidence, titles }
      };
      return verdict;
    }
  };
}

// src/profiles/ui-auditor/profile.ts
function uiAuditorProfile(options = {}) {
  const name = options.name ?? "ui-auditor";
  const profile = {
    name,
    description: "Vision-driven UI auditor. One lens per iteration.",
    prompt: { systemPrompt: "" },
    model: options.model ? { default: options.model } : void 0,
    tools: { browser: true, vision: true },
    metadata: { role: "ui-auditor" }
  };
  const output = { parse: parseAuditorEvents };
  const validator = options.task ? createUiAuditorValidator(options.task) : createUiAuditorValidator({ lens: "other", captures: [] });
  const taskToPrompt = (task) => `${encodeAuditTaskEnvelope(task)}
${buildAuditorSystemPrompt(task.lens)}

${formatAuditorPrompt(task)}`;
  const agentRunSpec = {
    name,
    profile,
    taskToPrompt
  };
  return { profile, taskToPrompt, output, validator, agentRunSpec };
}
export {
  LENS_BRIEFS,
  SHARED_AUDITOR_RULES,
  UI_FINDING_SEVERITIES,
  UI_LENSES,
  appendFindings,
  buildAuditorSystemPrompt,
  coderTaskToPrompt,
  createInProcessUiAuditClient,
  createUiAuditorValidator,
  decodeAuditTaskEnvelope,
  encodeAuditTaskEnvelope,
  formatAuditorPrompt,
  initAuditWorkspace,
  parseAuditorEvents,
  readAuditRegistry,
  registerCaptures,
  summarizeRegistry,
  uiAuditorProfile,
  writeAuditIndex
};
//# sourceMappingURL=profiles.js.map