/**
 * config/share-policy.mjs — artifact-sharing policy resolver (C35).
 *
 * Rationale: `.aperant/.gitignore` used to be a hardcoded template with the
 * `solo` profile baked in. C35 replaces that with a policy-driven model so
 * users can deliberately choose what leaves their machine. Every file under
 * `.aperant/` belongs to one of five taxonomy classes (ephemeral / derived /
 * shared-source / durable-source / local-private — see spec.md §4). Only
 * `shared-source` items are toggleable; `ephemeral` and `derived` are always
 * ignored, `durable-source` is always allowlisted.
 *
 * This module is a PURE helper: no IO, no git, no side effects beyond the
 * stderr warnings we emit for user mistakes. The effective policy returned by
 * `resolveSharePolicy` is consumed by `buildAperantGitignoreTemplate(policy)`
 * in upgrade-gitignore.mjs and by `cmdAudit('privacy', …)` in
 * commands/audit.mjs — any divergence between the emitter and the matcher is
 * a bug in one of those callers, not in this module.
 *
 * Policy shape (flat — no `share.tasks.index` key; INDEX.md is derived and
 * its visibility follows `share.tasks` automatically):
 *
 *   {
 *     visibility: 'solo' | 'team' | 'oss',   // preset (unused after resolution
 *                                            //  except for diagnostics)
 *     roadmap:   boolean,                    // .aperant/roadmap/** *.json
 *     tasks:     boolean,                    // .aperant/tasks/ *\/ **
 *     team:      { roster: boolean },        // .aperant/team.json
 *     decisions: boolean,                    // .aperant/decisions/ *.md
 *     digests:   boolean                     // .aperant/digests/ *.md (resolved
 *                                            //  from tri-state input)
 *   }
 *
 * Spec cross-refs:
 *   - spec.md §3: config schema diff + merge rule
 *   - spec.md §4: artifact taxonomy
 *   - spec.md §5: buildAperantGitignoreTemplate(policy) signature change
 *
 * C36 reshaped the .aperant folder tree: `contexts/` was renamed to
 * `context/installed/` (durable-source) and runtime ledgers live under
 * `context/notes/` (local-private, deny-by-default). The `plans/` directory
 * was retired outright. The durable-source branch below names only
 * `context/installed/` — legacy `contexts/` paths now fall through to
 * local-private and are not allowlisted.
 *
 * @internal
 */
/**
 * Default share policy. Matches the `solo` preset — nothing trackable except
 * the durable-source scaffolding that already shipped pre-C35.
 *
 * @type {Readonly<{
 *   visibility: 'solo',
 *   roadmap: false,
 *   tasks: false,
 *   team: { roster: false },
 *   decisions: false,
 *   digests: false,
 * }>}
 *
 * @internal
 */
export const DEFAULT_SHARE_POLICY = Object.freeze({
    visibility: 'solo',
    roadmap: false,
    tasks: false,
    team: Object.freeze({ roster: false }),
    decisions: false,
    digests: false,
});
/**
 * Expand a visibility preset into a base share-toggle set. `team` and `oss`
 * produce the same toggle shape today (they differ only in setup-flow copy
 * and the public-remote gate's wording). Unknown presets degrade to `solo`
 * with a stderr warning — callers must still treat the return value as
 * authoritative.
 *
 * Note: `digests` is returned as a tri-state (null = inherit). Callers should
 * pass the result through `resolveSharePolicy` to collapse it to a boolean.
 *
 * @param {string} preset
 * @returns {{
 *   visibility: 'solo' | 'team' | 'oss',
 *   roadmap: boolean,
 *   tasks: boolean,
 *   team: { roster: boolean },
 *   decisions: boolean,
 *   digests: null | boolean,
 * }}
 *
 * @internal
 */
export function expandVisibilityPreset(preset) {
    switch (preset) {
        case 'solo':
            return {
                visibility: 'solo',
                roadmap: false,
                tasks: false,
                team: { roster: false },
                decisions: false,
                digests: null,
            };
        case 'team':
            return {
                visibility: 'team',
                roadmap: true,
                tasks: false,
                team: { roster: true },
                decisions: true,
                digests: null,
            };
        case 'oss':
            return {
                visibility: 'oss',
                roadmap: true,
                tasks: false,
                team: { roster: true },
                decisions: true,
                digests: null,
            };
        default:
            process.stderr.write(`[apt-tools] warning: unknown visibility preset '${String(preset)}' — falling back to 'solo'.\n`);
            return {
                visibility: 'solo',
                roadmap: false,
                tasks: false,
                team: { roster: false },
                decisions: false,
                digests: null,
            };
    }
}
/**
 * Resolve the effective share policy from a loaded config object. Applies the
 * merge rule from spec.md §3:
 *
 *   1. Start from DEFAULT_SHARE_POLICY (solo).
 *   2. Apply preset expansion from `config.share.visibility`.
 *   3. Overlay explicit `config.share.*` keys on top of the preset.
 *   4. Collapse `share.digests === null` to `config.digests?.enabled === true`.
 *   5. If `share.digests === true` while `config.digests?.enabled !== true`,
 *      warn on stderr (user is opting into sharing a directory nothing writes
 *      to today — the allow rule still lands, but we want to flag the
 *      mismatch).
 *
 * Missing `share` block → defaults to `solo`. Unknown `visibility` string →
 * warn + fall back to `solo`.
 *
 * @param {object} [config]
 * @returns {{
 *   visibility: 'solo' | 'team' | 'oss',
 *   roadmap: boolean,
 *   tasks: boolean,
 *   team: { roster: boolean },
 *   decisions: boolean,
 *   digests: boolean,
 * }}
 *
 * @internal
 */
export function resolveSharePolicy(config) {
    const share = config?.share ?? null;
    const presetName = share?.visibility ?? 'solo';
    const preset = expandVisibilityPreset(presetName);
    // Overlay explicit share.* keys on top of the preset expansion. Each key is
    // checked independently so partial overrides are supported (e.g. preset =
    // team with share.roadmap: false).
    const merged = {
        visibility: preset.visibility,
        roadmap: share && typeof share.roadmap === 'boolean' ? share.roadmap : preset.roadmap,
        tasks: share && typeof share.tasks === 'boolean' ? share.tasks : preset.tasks,
        team: {
            roster: share?.team && typeof share.team.roster === 'boolean'
                ? share.team.roster
                : preset.team.roster,
        },
        decisions: share && typeof share.decisions === 'boolean' ? share.decisions : preset.decisions,
        digests: share && (share.digests === true || share.digests === false) ? share.digests : preset.digests, // null — to be collapsed below
    };
    // Digests inheritance: null → config.digests.enabled.
    const digestsEnabled = config?.digests?.enabled === true;
    if (merged.digests === null || merged.digests === undefined) {
        merged.digests = digestsEnabled;
    }
    else if (merged.digests === true && !digestsEnabled) {
        process.stderr.write('[apt-tools] warning: share.digests=true while config.digests.enabled is not true — the allow rule will land, but no digests will be written until digests are enabled.\n');
    }
    return merged;
}
/**
 * Build a deterministic matcher answering "does this relative path belong to
 * a class that the effective policy denies?". Single source of truth used by
 * `apt-tools audit privacy` so emitter and matcher can never drift.
 *
 * Paths are expected relative to the project root (e.g. `.aperant/team.json`,
 * `.aperant/tasks/42/spec.md`). Paths outside `.aperant/` are reported as
 * `ignored: false` — this matcher is not a full gitignore implementation, it
 * only reasons about the policy-driven slice of `.aperant/`.
 *
 * @param {ReturnType<typeof resolveSharePolicy>} policy
 * @returns {(relPath: string) => { ignored: boolean, class: string|null, reason: string|null }}
 *
 * @internal
 */
export function buildPolicyIgnoreMatcher(policy) {
    return function match(relPath) {
        if (typeof relPath !== 'string' || relPath.length === 0) {
            return { ignored: false, class: null, reason: null };
        }
        // Normalize to posix separators — git ls-files always emits forward
        // slashes but callers may pass through a Windows-style path.
        const p = relPath.replace(/\\/g, '/');
        if (!p.startsWith('.aperant/')) {
            return { ignored: false, class: null, reason: null };
        }
        const rest = p.slice('.aperant/'.length);
        // Allow-always: config.json, .gitignore, axes.json, and the durable-
        // source scaffolding directories. These ship regardless of policy.
        if (rest === 'config.json' || rest === '.gitignore' || rest === 'axes.json') {
            return { ignored: false, class: 'durable-source', reason: null };
        }
        if (rest.startsWith('features/') ||
            rest.startsWith('prompts/') ||
            rest.startsWith('templates/') ||
            rest.startsWith('workflows/') ||
            rest.startsWith('context/installed/') ||
            rest.startsWith('standards/')) {
            return { ignored: false, class: 'durable-source', reason: null };
        }
        // Ephemeral — always ignored, regardless of any toggle.
        if (rest === 'state.json' ||
            rest === 'active.json' ||
            rest === 'pending-effects.jsonl' ||
            rest === 'pending-effects.dead.jsonl' ||
            rest.startsWith('events/') ||
            rest.startsWith('locks/') ||
            rest.startsWith('team-status/') ||
            rest.startsWith('paused/') ||
            rest.startsWith('rationalizations/') ||
            rest === 'digests/.last-run.json' ||
            rest === 'digests/errors.log') {
            return { ignored: true, class: 'ephemeral', reason: 'ephemeral' };
        }
        // Derived — always ignored (two-way write = drift).
        if (rest === 'tasks/INDEX.md' || rest === 'decisions/INDEX.md') {
            return { ignored: true, class: 'derived', reason: 'derived' };
        }
        if (/^roadmap\/.*ROADMAP\.md$/.test(rest)) {
            return { ignored: true, class: 'derived', reason: 'derived' };
        }
        // Shared-source — toggleable. Committed iff the matching share.* is on.
        if (rest === 'team.json') {
            return policy.team.roster
                ? { ignored: false, class: 'shared-source', reason: null }
                : {
                    ignored: true,
                    class: 'shared-source',
                    reason: 'share.team.roster=false',
                };
        }
        // Roadmap: the template emits `!roadmap/**`, so any file under roadmap/
        // (JSON source or incidental docs) follows share.roadmap — EXCEPT the
        // derived ROADMAP.md views, which are matched above and stay ignored.
        // Keeping the matcher in lockstep with the allow-rule prevents audit
        // false-positives on tracked `.aperant/roadmap/foo.md`-style files when
        // share.roadmap is on.
        if (/^roadmap\//.test(rest)) {
            return policy.roadmap
                ? { ignored: false, class: 'shared-source', reason: null }
                : { ignored: true, class: 'shared-source', reason: 'share.roadmap=false' };
        }
        if (/^decisions\/.+\.md$/.test(rest)) {
            return policy.decisions
                ? { ignored: false, class: 'shared-source', reason: null }
                : {
                    ignored: true,
                    class: 'shared-source',
                    reason: 'share.decisions=false',
                };
        }
        if (/^digests\/.+\.md$/.test(rest)) {
            return policy.digests
                ? { ignored: false, class: 'shared-source', reason: null }
                : { ignored: true, class: 'shared-source', reason: 'share.digests=false' };
        }
        if (/^tasks\/[^/]+\/.+/.test(rest)) {
            return policy.tasks
                ? { ignored: false, class: 'shared-source', reason: null }
                : { ignored: true, class: 'shared-source', reason: 'share.tasks=false' };
        }
        // Everything else under .aperant/ falls through to ignored (deny by
        // default) — matches the template's leading `*` rule. Classed as
        // `local-private` for reporting purposes.
        return { ignored: true, class: 'local-private', reason: 'deny-by-default' };
    };
}
//# sourceMappingURL=share-policy.mjs.map