/**
 * @internal
 * policy.mjs — docs-coherence policy model (C33 docs-coherence guarantee).
 *
 * A policy is a flat map of `glob-or-path → 'auto-update' | 'flag-drift' | 'skip'`.
 * Loaded from `.aperant/config.json.docs_policy`, falling back to the baked-in
 * defaults shipped in `packages/framework/templates/docs-policy-defaults.json`.
 *
 * Consumers:
 *   - `apt-tools docs-audit` (runtime policy resolution per surface)
 *   - `apt-planner` §3.9 (docs-impact enumeration — which surfaces does a diff
 *     touch, and what do we do with each?)
 *   - `apt-reviewer` Pass 6 (docs-drift categorization)
 *   - `apt-setup` Batch 7 (user-visible picker that writes the override map)
 *
 * Matching contract (specificity ordering):
 *
 *   1. Literal path match wins over any glob.
 *   2. Among matching globs, the longest pattern wins (deeper > shallower).
 *   3. Ties are resolved by definition order (Map iteration).
 *
 * User overrides win over defaults when the same key is present in both.
 * A surface that matches nothing returns `null` — callers treat that as "no
 * registered policy; skip" (NOT the default for CLAUDE.md / AGENTS.md etc,
 * which are keyed explicitly in the defaults).
 */
import { existsSync, readFileSync } from 'node:fs';
import { join } from 'node:path';
/**
 * Canonical defaults. Mirrored byte-for-byte at
 * `packages/framework/templates/docs-policy-defaults.json`.
 * The drift-prevention test in `docs-audit-policy.test.ts` fails loudly if
 * the JSON template and this constant diverge.
 *
 * Surfaces are grouped for readability; each group's entries carry the same
 * default policy. Order is significant for the "longest match wins" rule when
 * patterns overlap — e.g. `.aperant/features/REGISTRY.md` (literal) must beat
 * `.aperant/features/*.json` (glob) if we ever extend to cover that fan-out.
 */
export const DEFAULT_DOCS_POLICY = Object.freeze({
    // External prose — emits findings only; never auto-edited.
    'CLAUDE.md': 'flag-drift',
    'AGENTS.md': 'flag-drift',
    'packages/*/README.md': 'flag-drift',
    'README.md': 'flag-drift',
    'docs/**': 'flag-drift',
    // Internal state — auto-patched by the reviewer / executor when the policy
    // says so. These files are already framework-managed, so auto-update is the
    // right default.
    '.aperant/roadmap/**/milestones.json': 'auto-update',
    '.aperant/features/REGISTRY.md': 'auto-update',
    '.aperant/features/*.json': 'auto-update',
    '.aperant/project_index.json': 'auto-update',
    '.aperant/tasks/**': 'auto-update',
});
/**
 * Match a filesystem path to the most specific registered surface in `policy`.
 *
 * Returns the policy string for the winning surface, or `null` when nothing
 * in `policy` matches. Matching semantics:
 *
 *   - `**` matches any path segment(s), including empty
 *   - `*` matches within a single path segment (no `/`)
 *   - A literal path with no glob meta is an exact match only.
 *
 * Specificity ordering: literal > glob; within globs, longer pattern wins.
 */
export function matchSurfaceToPolicy(path, policy) {
    if (!path || !policy || typeof policy !== 'object')
        return null;
    const entries = Object.entries(policy);
    let best = null; // { pattern, value, score }
    for (const [pattern, value] of entries) {
        if (!pattern || typeof value !== 'string')
            continue;
        if (!pathMatches(path, pattern))
            continue;
        const score = specificityScore(pattern);
        if (!best || score > best.score) {
            best = { pattern, value, score };
        }
    }
    return best ? best.value : null;
}
/**
 * Test whether `path` matches `pattern`. Supports `**` (any segments) and `*`
 * (non-slash within one segment). Path separators are normalized to `/`.
 */
function pathMatches(path, pattern) {
    const normPath = String(path).replace(/\\/g, '/').replace(/^\.\//, '');
    const normPat = String(pattern).replace(/\\/g, '/').replace(/^\.\//, '');
    if (!normPat.includes('*')) {
        return normPath === normPat;
    }
    const regex = globToRegex(normPat);
    return regex.test(normPath);
}
function globToRegex(pattern) {
    // Transform glob → anchored regex:
    //   - `**` → `.*`          (cross-segment wildcard)
    //   - `*`  → `[^/]*`       (single-segment wildcard)
    //   - other meta chars escape to literals.
    let src = '^';
    for (let i = 0; i < pattern.length; i++) {
        const c = pattern[i];
        if (c === '*') {
            if (pattern[i + 1] === '*') {
                src += '.*';
                i++; // consume second *
            }
            else {
                src += '[^/]*';
            }
        }
        else if ('.^$+?()[]{}|\\'.includes(c)) {
            src += '\\' + c;
        }
        else {
            src += c;
        }
    }
    src += '$';
    return new RegExp(src);
}
/**
 * Specificity score — literal paths beat globs; longer patterns beat shorter
 * ones; `**` is less specific than `*` (doublestar matches across segments,
 * so we subtract for each occurrence). Score is total length minus 10 per
 * `**` and 1 per `*` (single-star is a mild penalty; doublestar is a big one).
 */
function specificityScore(pattern) {
    const hasGlob = pattern.includes('*');
    if (!hasGlob) {
        // Literal — always beats any glob. Bias heavily so even short literals
        // (e.g. `CLAUDE.md`) win over long globs (e.g. `packages/*/README.md`).
        return 100000 + pattern.length;
    }
    let base = pattern.length;
    const doubleStars = (pattern.match(/\*\*/g) || []).length;
    // Count single stars that are NOT part of a **.
    const singleStars = (pattern.match(/\*/g) || []).length - doubleStars * 2;
    base -= doubleStars * 10;
    base -= singleStars * 1;
    return base;
}
/**
 * Load the effective docs policy for a project. Merges user overrides from
 * `.aperant/config.json.docs_policy` on top of `DEFAULT_DOCS_POLICY`. User
 * overrides win on key conflict; user-only keys are additive.
 *
 * Missing `.aperant/config.json` or missing `docs_policy` block → pure defaults.
 * Malformed overrides (not an object) → pure defaults (best-effort, no throw).
 */
export function loadDocsPolicy(projectDir) {
    const defaults = { ...DEFAULT_DOCS_POLICY };
    if (!projectDir)
        return defaults;
    const configPath = join(projectDir, '.aperant', 'config.json');
    if (!existsSync(configPath))
        return defaults;
    let overrides;
    try {
        const raw = JSON.parse(readFileSync(configPath, 'utf-8'));
        overrides = raw?.docs_policy;
    }
    catch {
        return defaults;
    }
    if (!overrides || typeof overrides !== 'object' || Array.isArray(overrides)) {
        return defaults;
    }
    const merged = { ...defaults };
    for (const [k, v] of Object.entries(overrides)) {
        if (typeof v === 'string' && ['auto-update', 'flag-drift', 'skip'].includes(v)) {
            merged[k] = v;
        }
    }
    return merged;
}
//# sourceMappingURL=policy.mjs.map