/**
 * @internal
 * constitution/parse.mjs — AGENTS.md principle extractor (C14 subtask 1).
 *
 * Pure function: parseAgentsMd(markdown) -> Principle[]
 *
 * A Principle is:
 *   {
 *     id:       string,      // stable slug
 *     title:    string,      // human-readable first line / heading
 *     text:     string,      // the matched chunk (keyword seed)
 *     source:   { section: string, line: number },
 *     keywords: string[],    // derived, deduped, stopword-stripped
 *     severity: 'must' | 'should' | 'never' | 'principle' | 'obligation'
 *   }
 *
 * Extraction rules (applied in order, dedup by normalized title):
 *   1. Numbered lists under a heading matching /principles?|constraints?|rules?/i
 *      → severity 'principle', id `p{n}-{slug(title)}`.
 *   2. Bullets (any depth) containing a standalone MUST/SHOULD/NEVER token
 *      (word-boundary, case-sensitive) → severity from the token,
 *      id `{severity}-{slug(first-5-words)}`.
 *   3. Rows of the "Deployment Modes" + "Obligations" table →
 *      one principle per row + an umbrella principle
 *      `deployment-modes-all-modes` whose keywords include every mode name.
 *
 * Returns `[]` (not throws) on empty string or markdown with no matches.
 */
const STOPWORDS = new Set([
    'the',
    'a',
    'an',
    'and',
    'or',
    'of',
    'for',
    'to',
    'in',
    'on',
    'must',
    'should',
    'never',
    'with',
    'without',
    'is',
    'are',
    'this',
    'that',
    'these',
    'those',
    'be',
    'been',
    'being',
    'have',
    'has',
    'had',
    'not',
    'but',
    'from',
    'into',
    'over',
    'under',
    'its',
]);
/**
 * Lowercase + replace non-alphanumerics with hyphens + trim.
 * Used for building stable slug ids.
 */
function slugify(text) {
    return String(text)
        .toLowerCase()
        .replace(/[^a-z0-9]+/g, '-')
        .replace(/^-+|-+$/g, '')
        .slice(0, 60);
}
/**
 * Derive keywords from a text chunk:
 *   lowercase → strip non-[a-z0-9] → drop stopwords → keep len >= 4 → dedup.
 */
function deriveKeywords(text) {
    const seen = new Set();
    const out = [];
    const tokens = String(text)
        .toLowerCase()
        .split(/[^a-z0-9]+/g)
        .filter(Boolean);
    for (const tok of tokens) {
        if (tok.length < 4)
            continue;
        if (STOPWORDS.has(tok))
            continue;
        if (seen.has(tok))
            continue;
        seen.add(tok);
        out.push(tok);
    }
    return out;
}
/**
 * Normalize a title for dedup: lowercase + collapse whitespace + strip
 * punctuation.
 */
function normalizeTitle(title) {
    return String(title)
        .toLowerCase()
        .replace(/[^a-z0-9\s]+/g, ' ')
        .replace(/\s+/g, ' ')
        .trim();
}
/**
 * Detect a `## Heading` or `### Heading` line. Returns {level, title} or null.
 */
function parseHeading(line) {
    const m = /^(#{1,6})\s+(.+?)\s*$/.exec(line);
    if (!m)
        return null;
    return { level: m[1].length, title: m[2] };
}
/**
 * Strip markdown bullet/numbering prefix and any bold wrappers so the caller
 * sees the raw text content of a bullet line.
 */
function stripBulletPrefix(line) {
    return line
        .replace(/^\s*(?:[-*+]|\d+\.)\s+/, '')
        .replace(/^\*\*(.+?)\*\*/, '$1')
        .trim();
}
/**
 * Return true when the line starts a bullet (any depth) or a numbered item.
 */
function isBullet(line) {
    return /^\s*(?:[-*+]|\d+\.)\s+/.test(line);
}
/**
 * Return true when the line looks like a Principles/Constraints/Rules heading.
 */
function isPrinciplesHeading(title) {
    return /principles?|constraints?|rules?/i.test(title);
}
/**
 * Return true when the line looks like the Deployment-Modes obligation heading.
 */
function isDeploymentModesHeading(title) {
    return /deployment\s*modes/i.test(title) && /obligation/i.test(title);
}
/**
 * Pull the first N words out of a text chunk (used for slug seeds).
 */
function firstWords(text, n) {
    return String(text).split(/\s+/).slice(0, n).join(' ');
}
/**
 * Extract principles from an AGENTS.md markdown string.
 *
 * @param {string} markdown
 * @returns {Array<{
 *   id: string,
 *   title: string,
 *   text: string,
 *   source: { section: string, line: number },
 *   keywords: string[],
 *   severity: 'must'|'should'|'never'|'principle'|'obligation'
 * }>}
 */
export function parseAgentsMd(markdown) {
    if (typeof markdown !== 'string' || markdown.length === 0)
        return [];
    const lines = markdown.split(/\r?\n/);
    const principles = [];
    const seenTitles = new Set();
    function push(p) {
        const key = normalizeTitle(p.title);
        if (!key)
            return;
        if (seenTitles.has(key))
            return;
        seenTitles.add(key);
        principles.push(p);
    }
    // Track state across lines.
    let section = '(root)';
    let inPrinciples = false;
    let principlesCounter = 0;
    let tableHeadingSection = null;
    let tableHeaderLine = null;
    let tableHeaderCols = null;
    let tableRowsSeen = 0;
    const deploymentModeNames = [];
    for (let i = 0; i < lines.length; i++) {
        const rawLine = lines[i];
        const line = rawLine;
        const heading = parseHeading(line);
        if (heading) {
            // If we were mid-way through a deployment-modes table and a new
            // heading appears, finalize the umbrella principle for the rows
            // we already collected before we lose the state.
            if (tableHeadingSection && tableRowsSeen > 0) {
                const umbrellaText = `Deployment modes: ${deploymentModeNames.join(', ')}`;
                push({
                    id: 'deployment-modes-all-modes',
                    title: 'Deployment modes — all modes covered',
                    text: umbrellaText,
                    source: { section: tableHeadingSection, line: tableHeaderLine ?? i + 1 },
                    keywords: [
                        'deployment',
                        'modes',
                        ...deploymentModeNames.map((m) => slugify(m).replace(/-/g, '')),
                        ...deriveKeywords(umbrellaText),
                    ].filter(Boolean),
                    severity: 'obligation',
                });
            }
            section = heading.title;
            inPrinciples = isPrinciplesHeading(heading.title);
            if (inPrinciples)
                principlesCounter = 0;
            // Deployment-modes detection — next non-blank bullet block below
            // triggers table parsing.
            if (isDeploymentModesHeading(heading.title)) {
                tableHeadingSection = heading.title;
                tableHeaderLine = null;
                tableHeaderCols = null;
                tableRowsSeen = 0;
            }
            else {
                // A non-deployment heading clears any partially-collected table
                // state (we only read the table directly below its heading).
                if (tableHeadingSection && i > 0) {
                    tableHeadingSection = null;
                    tableHeaderCols = null;
                    tableRowsSeen = 0;
                }
            }
            continue;
        }
        // Rule 3 — Deployment Modes table rows.
        if (tableHeadingSection) {
            // Table detection: a line starting with `|` is a table row; header
            // is followed by a `|---|---|` separator.
            if (/^\s*\|/.test(line)) {
                const cols = line
                    .trim()
                    .replace(/^\|/, '')
                    .replace(/\|\s*$/, '')
                    .split('|')
                    .map((c) => c.trim());
                if (!tableHeaderCols) {
                    tableHeaderCols = cols;
                    tableHeaderLine = i + 1;
                    continue;
                }
                // Separator row — skip.
                if (cols.every((c) => /^:?-+:?$/.test(c)))
                    continue;
                // Data row → first column is the mode name.
                const modeName = cols[0].replace(/\*\*/g, '').trim();
                if (!modeName)
                    continue;
                const rowText = cols.join(' ');
                tableRowsSeen++;
                deploymentModeNames.push(modeName);
                push({
                    id: `deployment-mode-${slugify(modeName)}`,
                    title: `Deployment mode: ${modeName}`,
                    text: rowText,
                    source: { section: tableHeadingSection, line: i + 1 },
                    keywords: deriveKeywords(`${modeName} ${rowText}`),
                    severity: 'obligation',
                });
                continue;
            }
            // A non-table, non-blank line after we started collecting rows
            // means the table has ended. Finalize the umbrella principle.
            if (tableRowsSeen > 0 && line.trim().length > 0) {
                const umbrellaText = `Deployment modes: ${deploymentModeNames.join(', ')}`;
                push({
                    id: 'deployment-modes-all-modes',
                    title: 'Deployment modes — all modes covered',
                    text: umbrellaText,
                    source: { section: tableHeadingSection, line: tableHeaderLine ?? i + 1 },
                    keywords: [
                        'deployment',
                        'modes',
                        ...deploymentModeNames.map((m) => slugify(m).replace(/-/g, '')),
                        ...deriveKeywords(umbrellaText),
                    ].filter(Boolean),
                    severity: 'obligation',
                });
                tableHeadingSection = null;
                tableHeaderCols = null;
                tableRowsSeen = 0;
            }
        }
        // Rule 1 — numbered list under a Principles/Constraints/Rules heading.
        if (inPrinciples && /^\s*\d+\.\s+/.test(line)) {
            principlesCounter++;
            const body = stripBulletPrefix(line);
            const titleSeed = firstWords(body.replace(/[.:]/g, ' '), 5);
            push({
                id: `p${principlesCounter}-${slugify(titleSeed)}`,
                title: body,
                text: body,
                source: { section, line: i + 1 },
                keywords: deriveKeywords(body),
                severity: 'principle',
            });
            continue;
        }
        // Rule 2 — bullets with a standalone MUST/SHOULD/NEVER token.
        if (isBullet(line)) {
            const body = stripBulletPrefix(line);
            // MUST NOT should still count as 'must' (rule hits MUST first).
            const tokenMatch = /\b(MUST|SHOULD|NEVER)\b/.exec(body);
            if (tokenMatch) {
                const severity = tokenMatch[1].toLowerCase();
                const titleSeed = firstWords(body.replace(/[.:]/g, ' '), 5);
                push({
                    id: `${severity}-${slugify(titleSeed)}`,
                    title: body,
                    text: body,
                    source: { section, line: i + 1 },
                    keywords: deriveKeywords(body),
                    severity,
                });
            }
        }
    }
    // If the file ends while a deployment-modes table was still open, emit
    // the umbrella principle now.
    if (tableHeadingSection && tableRowsSeen > 0) {
        const umbrellaText = `Deployment modes: ${deploymentModeNames.join(', ')}`;
        push({
            id: 'deployment-modes-all-modes',
            title: 'Deployment modes — all modes covered',
            text: umbrellaText,
            source: { section: tableHeadingSection, line: tableHeaderLine ?? lines.length },
            keywords: [
                'deployment',
                'modes',
                ...deploymentModeNames.map((m) => slugify(m).replace(/-/g, '')),
                ...deriveKeywords(umbrellaText),
            ].filter(Boolean),
            severity: 'obligation',
        });
    }
    return principles;
}
// Re-export helpers so match.mjs / tests can reuse the same stopword policy.
export { deriveKeywords, normalizeTitle, slugify };
//# sourceMappingURL=parse.mjs.map