/**
 * design/template-extract.mjs — pure typeui-sh SKILL.md → DESIGN.md extractor (C42).
 *
 * Given the raw text of a typeui-sh SKILL.md plus a slug, produces:
 *   - design_md:   a schema-valid DESIGN.md string (YAML frontmatter + minimal
 *                  Stitch-compatible body). Round-trips through
 *                  `validateDesignMarkdown` with zero errors.
 *   - provenance:  a plain JS object destined for `provenance.yaml`. Carries
 *                  source attribution + a `warnings[]` list documenting every
 *                  default fallback the extractor had to apply.
 *   - warnings:    a copy of `provenance.warnings` for callers that want to
 *                  surface the warnings separately (e.g. the build script).
 *
 * Pure function. No fs/io/globals/process. Call sites pass in the SKILL.md
 * text + slug + extractedAt date. See spec R2 for the line-by-line rules.
 *
 * Design rationale (spec R2):
 *   - Grep over a small set of literal line shapes. The typeui-sh corpus is
 *     consistent enough that a parser works; no LLM or ambiguity.
 *   - Every lossy decision (prose-scale fallback, prose-spacing fallback)
 *     emits a warning so `provenance.yaml` shows exactly what an editor
 *     should improve.
 *   - Anti-patterns: every extracted entry carries the 15 universal rules
 *     from `packages/framework/templates/DESIGN.md` so a project adopting
 *     this library inherits the lint catalogue for free (spec R3).
 */
import { parse as parseYaml, stringify as stringifyYaml } from 'yaml';
const FRONTMATTER_RE = /^---\n([\s\S]*?)\n---\n/;
/** @typedef {{ id: string, category: string, pattern: string, why: string, rewrite: string }} AntiPattern */
/**
 * The 15 universal antiPatterns rules — byte-compatible with the ones in
 * `packages/framework/templates/DESIGN.md`. Every library entry gets these
 * so projects adopting an entry inherit the lint catalogue.
 *
 * Kept here (not imported) because the scaffold is itself authored in YAML
 * frontmatter + prose, and parsing it back at extraction time would add a
 * circular file-read dependency to a pure function. The scaffold and this
 * constant are kept in sync by design-validate.test.ts (scaffold passes) +
 * design-template.test.ts (every library entry passes).
 *
 * @type {AntiPattern[]}
 */
export const UNIVERSAL_ANTI_PATTERNS = [
    {
        id: 'no-side-stripe-borders',
        category: 'layout',
        pattern: 'border-left:\\s*(?:[3-9]|\\d{2,})px',
        why: 'Side-stripe callouts (>= 3px left borders) are an AI tell — they read as generic CMS snippets, not designed surfaces.',
        rewrite: 'Use a subtle background tint, a top-aligned icon, or a semantic text color instead of a left stripe.',
    },
    {
        id: 'no-nested-cards',
        category: 'layout',
        pattern: 'class="[^"]*rounded-(?:xl|2xl|3xl)[^"]*shadow[^"]*"[^>]*>\\s*<[^>]*class="[^"]*rounded-(?:xl|2xl|3xl)[^"]*shadow',
        why: "A rounded-shadowed card containing another rounded-shadowed card reads as 'I didn't know how to group these'. Flatten.",
        rewrite: 'Use section-level whitespace and a single card, or swap the outer card for a plain div.',
    },
    {
        id: 'no-gradient-text',
        category: 'typography',
        pattern: 'background-clip:\\s*text',
        why: 'Gradient text (background-clip: text) is the single strongest AI aesthetic. It reads as try-hard landing-page filler.',
        rewrite: 'Use a solid color from tokens.colors.text or tokens.colors.accent. Reserve gradients for non-type surfaces.',
    },
    {
        id: 'no-inter-only',
        category: 'typography',
        pattern: 'font-family:\\s*[\'"]?Inter[\'"]?\\s*[,;]?\\s*$',
        why: 'Inter as the sole declared font is the AI-default and signals zero type choice was made.',
        rewrite: 'Declare a primary family with intentional fallbacks (see tokens.typography.families.primary) or pick a distinctive display face.',
    },
    {
        id: 'no-all-bold-headings',
        category: 'typography',
        pattern: '<h[1-3][^>]*class="[^"]*font-black\\b',
        why: "`font-black` on every heading collapses type hierarchy into shouting. Weight differentiates; shouting doesn't.",
        rewrite: 'Reserve font-black for one hero moment per page; use semibold/bold for most headings.',
    },
    {
        id: 'no-uppercase-body',
        category: 'typography',
        pattern: '<p[^>]*class="[^"]*uppercase\\b',
        why: 'ALL-CAPS body text kills readability. Uppercase belongs on labels and eyebrow text only.',
        rewrite: 'Use tracking-widest or small-caps on short labels; keep body at normal case.',
    },
    {
        id: 'no-purple-saas-gradient-hero',
        category: 'color',
        pattern: 'linear-gradient\\([^)]*#[68][0-9a-f]{2}[a-f0-9]f[a-f0-9]',
        why: 'Purple→pink hero gradients are the generic SaaS tell.',
        rewrite: 'Use a single tokens.colors.accent flood or a neutral photograph.',
    },
    {
        id: 'no-cyan-on-dark',
        category: 'color',
        pattern: 'color:\\s*#(?:0[0-9a-f]f[0-9a-f]ff|00ffff|22d3ee|06b6d4|0891b2)\\b',
        why: 'Bright cyan/electric-teal text on dark backgrounds is the 2015 futuristic-dashboard trope — high chroma, low information density.',
        rewrite: 'Use a desaturated accent from tokens.colors.accent or a neutral color at reduced alpha.',
    },
    {
        id: 'no-rainbow-accents',
        category: 'color',
        pattern: '(?:text|bg|border)-(?:red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-500[^"]*"[^>]*(?:text|bg|border)-(?:red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-500',
        why: "Multiple saturated Tailwind 500-series accents on the same surface reads as 'I couldn't decide'. Commit to one accent.",
        rewrite: 'Use tokens.colors.accent for primary actions; reserve status colors for semantic contexts only.',
    },
    {
        id: 'no-bounce-easing',
        category: 'motion',
        pattern: 'cubic-bezier\\(0\\.68,\\s*-0\\.55',
        why: 'Overshoot/bounce easings feel juvenile on UI surfaces.',
        rewrite: 'Use tokens.motion.easing.standard for UI, emphatic only for hero-level moments.',
    },
    {
        id: 'no-slow-default',
        category: 'motion',
        pattern: 'transition(?:-[a-z]+)?:\\s*(?:all|[a-z-]+)\\s+(?:[5-9]\\d{2}|\\d{4,})ms',
        why: 'Transitions >= 500ms on UI interactions feel sluggish; users interpret them as jank.',
        rewrite: 'Keep UI transitions under 280ms (tokens.motion.duration.slow). Reserve 500ms+ for hero entrances only.',
    },
    {
        id: 'no-permanent-spinner',
        category: 'motion',
        pattern: 'animation:\\s*spin\\s+\\d+(?:\\.\\d+)?s\\s+linear\\s+infinite',
        why: "A perpetual spinner (`animation: spin … infinite`) left in a mockup for 'loading state' always ships to prod.",
        rewrite: 'Use discrete loading/empty/error variants side-by-side, or replace with a skeleton.',
    },
    {
        id: 'no-missing-focus',
        category: 'interaction',
        pattern: '<button[^>]*class="[^"]*(?:focus:outline-none|outline-none)[^"]*"(?![^>]*focus-visible:)',
        why: "Removing the focus outline without replacing it breaks keyboard accessibility. This is the AI's laziest shortcut.",
        rewrite: 'Either keep the default outline or replace with `focus-visible:outline-2 focus-visible:outline-offset-2`.',
    },
    {
        id: 'no-placeholder-names',
        category: 'content',
        pattern: '\\b(John|Jane)\\s+Doe\\b',
        why: 'John/Jane Doe placeholder strings leak into shipped UI when the AI skips real sample data.',
        rewrite: 'Use sample data that reflects the actual product (real-looking names, real tasks, real copy).',
    },
    {
        id: 'no-lorem-ipsum',
        category: 'content',
        pattern: '\\bLorem\\s+ipsum\\s+dolor\\s+sit\\s+amet\\b',
        why: 'Lorem ipsum in a shipped mockup means no one thought about what the copy should actually say.',
        rewrite: 'Write real copy that reflects the feature. If the feature is too early for copy, render a single-word label instead.',
    },
];
/** Default scale/spacing fallbacks from the spec R2 steps 4 + 7. */
const DEFAULT_SCALE = ['12px', '14px', '16px', '20px', '24px', '32px'];
const DEFAULT_SPACING = ['4px', '8px', '12px', '16px', '24px', '32px'];
// -----------------------------------------------------------------------------
// Helpers
// -----------------------------------------------------------------------------
/**
 * Extract the leading YAML frontmatter block as a parsed object.
 * Returns `null` when missing or unparseable.
 * @param {string} text
 * @returns {Record<string, unknown> | null}
 */
function parseFrontmatter(text) {
    if (typeof text !== 'string')
        return null;
    const m = text.match(FRONTMATTER_RE);
    if (!m)
        return null;
    try {
        const parsed = parseYaml(m[1]);
        if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
            return /** @type {Record<string, unknown>} */ (parsed);
        }
    }
    catch {
        return null;
    }
    return null;
}
/**
 * Grab a single line from the `## Style Foundations` block that starts with
 * the given literal prefix (after the leading `- `). Returns the trimmed
 * remainder after the prefix + `:` delimiter, or null.
 *
 * @param {string} body
 * @param {string} prefix e.g. "Typography scale"
 * @returns {string | null}
 */
function extractFoundationLine(body, prefix) {
    // Anchor to `^\\s*-\\s+<prefix>\\s*:` at the start of a line; the rest
    // of the line is the value. `m` flag makes `^` match line starts.
    const re = new RegExp(`^\\s*-\\s+${escapeRegex(prefix)}\\s*:\\s*(.*)$`, 'm');
    const m = body.match(re);
    return m ? m[1].trim() : null;
}
function escapeRegex(s) {
    return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
/**
 * Parse the `primary=<hex>, secondary=<hex>, …` comma-list into a key→value
 * map. Values are returned verbatim (no case normalization).
 *
 * @param {string} str
 * @returns {Record<string, string>}
 */
function parseKvList(str) {
    const out = /** @type {Record<string, string>} */ ({});
    if (!str)
        return out;
    for (const raw of str.split(/\s*,\s*/)) {
        const eq = raw.indexOf('=');
        if (eq === -1)
            continue;
        const key = raw.slice(0, eq).trim();
        const val = raw.slice(eq + 1).trim();
        if (key)
            out[key] = val;
    }
    return out;
}
/**
 * sRGB luminance of a #RRGGBB / #RGB hex color in [0, 1]. Used to choose
 * light-mode vs dark-mode fallback for `border`/`surface` when the SKILL.md
 * doesn't declare one.
 *
 * @param {string} hex
 * @returns {number}
 */
function luminance(hex) {
    if (typeof hex !== 'string')
        return 1;
    let s = hex.trim();
    if (s.startsWith('#'))
        s = s.slice(1);
    if (s.length === 3)
        s = s
            .split('')
            .map((c) => c + c)
            .join('');
    if (s.length !== 6 && s.length !== 8)
        return 1;
    const r = Number.parseInt(s.slice(0, 2), 16) / 255;
    const g = Number.parseInt(s.slice(2, 4), 16) / 255;
    const b = Number.parseInt(s.slice(4, 6), 16) / 255;
    // Rec. 709 luminance (perceptual approximation is fine here).
    return 0.2126 * r + 0.7152 * g + 0.0722 * b;
}
/**
 * Detect "isDark" from a surface hex. Threshold 0.5 matches spec R2.3.
 * Malformed hex falls back to light (returns false) so we don't flip random
 * entries to the dark fallback set.
 */
function isDarkSurface(surfaceHex) {
    return luminance(surfaceHex) < 0.5;
}
/**
 * Parse a `12/14/16/20/24/32` style scale into `['12px', '14px', …]`. Returns
 * `null` when the line doesn't match that literal shape.
 *
 * @param {string} line
 * @returns {string[] | null}
 */
function parseNumericSlashScale(line) {
    if (!line)
        return null;
    const m = line.match(/^(\d+(?:\/\d+)+)\s*$/);
    if (!m)
        return null;
    return m[1].split('/').map((n) => `${n}px`);
}
/**
 * Parse `weights=100, 200, 300, …` into a sorted positive-int array.
 *
 * @param {string} str
 * @returns {number[]}
 */
function parseWeights(str) {
    if (!str)
        return [];
    const out = [];
    for (const tok of str.split(/\s*,\s*/)) {
        const n = Number.parseInt(tok.trim(), 10);
        if (Number.isFinite(n) && n > 0)
            out.push(n);
    }
    return out;
}
/**
 * Title-case a slug for `meta.name`. "neobrutalism" → "Neobrutalism",
 * "paper" → "Paper". Keeps subsequent characters lowercase (slugs are
 * already lowercase by convention in typeui-sh).
 *
 * @param {string} slug
 */
function titleCaseSlug(slug) {
    if (!slug)
        return '';
    return slug.charAt(0).toUpperCase() + slug.slice(1);
}
// -----------------------------------------------------------------------------
// Main
// -----------------------------------------------------------------------------
/**
 * Extract a schema-valid DESIGN.md + provenance sidecar from a typeui-sh
 * SKILL.md. Pure function — no I/O.
 *
 * @param {string} skillText — raw SKILL.md text
 * @param {string} slug      — the directory name under typeui-sh/
 * @param {string} extractedAt — ISO date (YYYY-MM-DD) for provenance.extracted_at
 * @returns {{ design_md: string, provenance: Record<string, unknown>, warnings: string[] }}
 */
export function extractTypeuiSkill(skillText, slug, extractedAt) {
    if (typeof skillText !== 'string' || skillText.length === 0) {
        throw new Error('extractTypeuiSkill: skillText must be a non-empty string');
    }
    if (typeof slug !== 'string' || slug.length === 0) {
        throw new Error('extractTypeuiSkill: slug must be a non-empty string');
    }
    if (typeof extractedAt !== 'string' || extractedAt.length === 0) {
        throw new Error('extractTypeuiSkill: extractedAt must be a non-empty ISO date string');
    }
    const warnings = [];
    const fm = parseFrontmatter(skillText) || {};
    // meta fields pulled from the SKILL.md frontmatter
    const skillName = typeof fm.name === 'string' && fm.name.length > 0 ? fm.name : slug;
    const description = typeof fm.description === 'string' && fm.description.length > 0 ? fm.description : '';
    const license = typeof fm.license === 'string' && fm.license.length > 0 ? fm.license : 'MIT';
    const sourceAuthor = fm.metadata && typeof fm.metadata === 'object' && !Array.isArray(fm.metadata)
        ? /** @type {Record<string, unknown>} */ (fm.metadata).author
        : null;
    // Body grep. The `## Style Foundations` block is the machine-parseable
    // data; everything else is prose we ignore.
    const typoLine = extractFoundationLine(skillText, 'Typography scale') || '';
    const colorLine = extractFoundationLine(skillText, 'Color palette') || '';
    const spacingLine = extractFoundationLine(skillText, 'Spacing scale') || '';
    // ── Color derivation ────────────────────────────────────────────────────
    // The `Color palette` line has two segments split by ` | `:
    //   "primary, neutral, … | Tokens: primary=#111, secondary=#8B5, … text=#111"
    // Split on the literal `Tokens:` substring to be lenient with whitespace.
    const tokensIdx = colorLine.indexOf('Tokens:');
    const tokensPart = tokensIdx === -1 ? colorLine : colorLine.slice(tokensIdx + 'Tokens:'.length);
    const colorKv = parseKvList(tokensPart);
    const primary = colorKv.primary || '#111111';
    const surface = colorKv.surface || '#FFFFFF';
    const text = colorKv.text || '#111827';
    const dark = isDarkSurface(surface);
    // bg ← surface, surface stays as surface for now. If the design happens
    // to carry a lighter tint we have no signal for it in the SKILL.md; the
    // spec says "use as-is if distinct, else fall back to white/near-black".
    // We use surface as bg and keep the same value for surface; callers can
    // refine later.
    const bg = surface;
    const surfaceOut = dark ? '#1A1A20' : '#F8F8F5';
    const border = dark ? '#2A2A32' : '#E5E7EB';
    const colors = /** @type {Record<string, unknown>} */ ({
        bg,
        surface: surfaceOut,
        accent: primary,
        text,
        border,
    });
    const status = /** @type {Record<string, string>} */ ({});
    if (colorKv.success)
        status.success = colorKv.success;
    if (colorKv.warning)
        status.warning = colorKv.warning;
    if (colorKv.danger)
        status.danger = colorKv.danger;
    if (colorKv.info)
        status.info = colorKv.info;
    if (Object.keys(status).length > 0)
        colors.status = status;
    // ── Typography derivation ───────────────────────────────────────────────
    // Line shape:
    //   "12/14/16/20/24/32 | Fonts: primary=Inter, display=Inter, mono=JBM | weights=…"
    //   OR prose scale: "desktop-first expressive scale | Fonts: … | weights=…"
    const typoParts = typoLine.split('|').map((s) => s.trim());
    const scaleStr = typoParts[0] || '';
    let fontsStr = '';
    let weightsStr = '';
    for (const part of typoParts.slice(1)) {
        if (part.startsWith('Fonts:'))
            fontsStr = part.slice('Fonts:'.length).trim();
        else if (part.startsWith('weights='))
            weightsStr = part.slice('weights='.length).trim();
    }
    const scaleParsed = parseNumericSlashScale(scaleStr);
    let scale;
    if (scaleParsed && scaleParsed.length >= 3) {
        scale = scaleParsed;
    }
    else {
        scale = [...DEFAULT_SCALE];
        warnings.push(`typography scale: prose fallback applied (SKILL.md said "${scaleStr || '<empty>'}"). Replaced with default ${DEFAULT_SCALE.join('/').replace(/px/g, '')}.`);
    }
    const fontsKv = parseKvList(fontsStr);
    const families = /** @type {Record<string, string>} */ ({
        primary: fontsKv.primary && fontsKv.primary.length > 0 ? fontsKv.primary : 'Inter',
    });
    if (fontsKv.display)
        families.display = fontsKv.display;
    if (fontsKv.mono)
        families.mono = fontsKv.mono;
    const weights = parseWeights(weightsStr);
    const typography = /** @type {Record<string, unknown>} */ ({
        families,
        scale,
    });
    if (weights.length > 0)
        typography.weights = weights;
    // ── Spacing derivation ──────────────────────────────────────────────────
    const spacingParsed = parseNumericSlashScale(spacingLine);
    let spacing;
    if (spacingParsed && spacingParsed.length >= 2) {
        spacing = spacingParsed;
    }
    else {
        spacing = [...DEFAULT_SPACING];
        warnings.push(`spacing scale: prose fallback applied (SKILL.md said "${spacingLine || '<empty>'}"). Replaced with default ${DEFAULT_SPACING.join('/').replace(/px/g, '')}.`);
    }
    // ── Frontmatter assembly ────────────────────────────────────────────────
    const frontmatter = {
        meta: {
            name: titleCaseSlug(skillName || slug),
            version: '1.0.0',
            updated: extractedAt,
            description: description || `Extracted from typeui-sh/${slug}.`,
        },
        tokens: {
            colors,
            typography,
            spacing,
        },
        antiPatterns: UNIVERSAL_ANTI_PATTERNS.map((p) => ({ ...p })),
        inspiration: [slug],
    };
    // YAML stringify with 2-space indent matches the scaffold's default.
    // Note: yaml's default `lineWidth` is 80 which can wrap long patterns.
    // We pass `{ lineWidth: 0 }` to disable wrapping so regex strings stay
    // on single lines — critical for `apt-tools design-lint` to match them.
    const yamlBlock = stringifyYaml(frontmatter, { lineWidth: 0 });
    const design_md = `---\n${yamlBlock}---\n\n# ${frontmatter.meta.name} — design contract\n\n> Source: typeui-sh/${slug}. See \`provenance.yaml\` for extraction notes + any default fallbacks.\n\n## 1. Visual Theme\n\nExtracted from the typeui-sh ${slug} skill. The machine-readable tokens live in the YAML frontmatter above; narrative sections point at them so \`apt-tools design-lint\` and \`apt-tools design-validate\` can cite specific fields.\n\n## 2. Color Palette\n\nSee \`tokens.colors\` in the frontmatter.\n\n## 3. Typography\n\nSee \`tokens.typography\` in the frontmatter.\n`;
    const provenance = {
        origin: 'extracted',
        source: 'typeui-sh',
        source_path: `docs/frameworks/Design-system/examples/by-source/typeui-sh/${slug}/SKILL.md`,
        source_license: license,
        source_author: typeof sourceAuthor === 'string' ? sourceAuthor : 'typeui.sh',
        extracted_at: extractedAt,
        extractor_version: '1.0.0',
        warnings,
    };
    return { design_md, provenance, warnings };
}
//# sourceMappingURL=template-extract.mjs.map