/**
 * FeatureRolloutReconciler — makes the InitiativeTracker self-populating
 * (GRADUATED-FEATURE-ROLLOUT-SPEC §4.1). Runs IN-PROCESS against the tracker
 * (the HTTP create surface drops kind/pipelineStage/parentProjectId), upserting
 * a kind:'task' initiative per spec from artifacts that already exist — approved
 * spec frontmatter, instar-dev trace (specPath/prNumber), and git merge state —
 * and attaching a rollout track for ships-staged features whose stage is DERIVED
 * from observing the config flag.
 *
 * The fs/git scanning is injected (`SpecArtifact[]`) so this reconciliation logic
 * is fully unit-testable without a repo. Every tracker write passes `ifMatch`
 * (OCC). Idempotent: re-running upserts, never duplicates. Bounded backfill:
 * historical merged specs register terminal (provenance); only recently-merged
 * or ships-staged specs become `active` tracks (anti-flood).
 */
import { deriveRolloutStage, rolloutPhaseStatuses, shouldArchiveAtStage, isRegression, ROLLOUT_PHASE_IDS, } from './featureRollout.js';
/** Map artifact state → the dev pipelineStage (no rollout). */
export function derivePipelineStage(a) {
    if (a.abandoned)
        return 'skipped';
    if (a.merged)
        return 'merged';
    if (a.traceExists || a.prNumber != null)
        return 'building';
    if (a.approved)
        return 'approved';
    if (a.reviewConverged)
        return 'spec-converged';
    return 'spec-drafted';
}
export class FeatureRolloutReconciler {
    deps;
    now;
    constructor(deps) {
        this.deps = deps;
        this.now = deps.now ?? (() => Date.now());
    }
    /** One reconciliation pass over all discovered specs. Idempotent. */
    async reconcile() {
        const summary = { created: [], advanced: [], archived: [], regressed: [], skipped: [], unchanged: [] };
        // Index existing tasks by specPath for rename detection.
        const bySpecPath = new Map();
        for (const init of this.deps.tracker.list()) {
            if (init.kind === 'project')
                continue;
            if (init.specPath)
                bySpecPath.set(init.specPath, init);
        }
        for (const art of this.deps.listSpecArtifacts()) {
            try {
                await this.reconcileOne(art, bySpecPath, summary);
            }
            catch {
                // A single bad spec must not abort the whole pass.
                summary.skipped.push(art.id);
            }
        }
        return summary;
    }
    async reconcileOne(art, bySpecPath, summary) {
        const stage = derivePipelineStage(art);
        // Rename detection: an existing record with this specPath wins over id match.
        const existing = bySpecPath.get(art.specPath) ?? this.deps.tracker.get(art.id);
        // Rollout observation (ships-staged + merged only).
        const rolloutEligible = Boolean(art.shipsStaged && art.merged && art.flagPath);
        const observedStage = rolloutEligible ? deriveRolloutStage(this.deps.observeFlag(art.flagPath)) : undefined;
        if (!existing) {
            // ── CREATE ──
            // Bounded backfill: a historical (not-recent) merged spec without a live
            // rollout registers TERMINAL (archived, provenance) — not an active card.
            const isActive = !art.merged || art.mergedRecently || rolloutEligible;
            const phaseStatuses = observedStage ? rolloutPhaseStatuses(observedStage) : undefined;
            const phases = rolloutEligible && phaseStatuses
                ? ROLLOUT_PHASE_IDS.map(id => ({ id, name: id, status: phaseStatuses[id] }))
                : [{ id: 'lifecycle', name: stage }];
            await this.deps.tracker.create({
                id: art.id, title: art.title, description: `Auto-registered from ${art.specPath}`,
                phases, kind: 'task', pipelineStage: stage, specPath: art.specPath,
                prNumber: art.prNumber,
                rollout: rolloutEligible
                    ? { flagPath: art.flagPath, stage: observedStage, evidenceSource: art.evidenceSource, promotionCriteria: art.promotionCriteria }
                    : undefined,
            });
            // default-on parks the track as 'paused' (NON-terminal → reopenable on a
            // later regression; 'archived' maps to TaskFlow's terminal `cancelled`
            // which would seal it). Historical non-rollout backfill uses 'archived'
            // (genuinely terminal provenance, never reopened).
            if (observedStage && shouldArchiveAtStage(observedStage)) {
                await this.deps.tracker.update(art.id, { status: 'paused', ifMatch: this.deps.tracker.get(art.id)?.version });
            }
            else if (!isActive) {
                await this.deps.tracker.update(art.id, { status: 'archived', ifMatch: this.deps.tracker.get(art.id)?.version });
            }
            summary.created.push(art.id);
            return;
        }
        // ── UPDATE (idempotent + OCC) ──
        const id = existing.id;
        let touched = false;
        // Advance dev pipelineStage when artifacts moved it forward.
        if (existing.pipelineStage !== stage) {
            await this.deps.tracker.update(id, { pipelineStage: stage, prNumber: art.prNumber, ifMatch: this.deps.tracker.get(id)?.version });
            touched = true;
        }
        if (rolloutEligible && observedStage) {
            const prevStage = existing.rollout?.stage;
            if (prevStage && isRegression(prevStage, observedStage)) {
                await this.deps.tracker.update(id, { pipelineStage: 'regressed', status: 'active', ifMatch: this.deps.tracker.get(id)?.version });
                await this.applyRolloutStage(id, observedStage);
                summary.regressed.push(id);
                return;
            }
            if (prevStage !== observedStage) {
                await this.applyRolloutStage(id, observedStage);
                if (shouldArchiveAtStage(observedStage))
                    summary.archived.push(id);
                else
                    summary.advanced.push(id);
                return;
            }
        }
        if (touched)
            summary.advanced.push(id);
        else
            summary.unchanged.push(id);
    }
    /** Apply a rollout stage: sync phase statuses + the rollout block; archive
     *  (reopenable) at default-on rather than completing. */
    async applyRolloutStage(id, stage) {
        const cur = this.deps.tracker.get(id);
        if (!cur)
            return;
        const statuses = rolloutPhaseStatuses(stage);
        for (const pid of ROLLOUT_PHASE_IDS) {
            const phase = cur.phases.find(p => p.id === pid);
            if (phase && phase.status !== statuses[pid]) {
                await this.deps.tracker.setPhaseStatus(id, pid, statuses[pid]);
            }
        }
        const fresh = this.deps.tracker.get(id);
        await this.deps.tracker.update(id, {
            rollout: { flagPath: fresh?.rollout?.flagPath ?? '', stage: stage, evidenceSource: fresh?.rollout?.evidenceSource, promotionCriteria: fresh?.rollout?.promotionCriteria, lastDigestNotifiedAt: fresh?.rollout?.lastDigestNotifiedAt },
            // 'paused' (non-terminal, reopenable) at default-on — NOT 'archived'
            // (which maps to TaskFlow's terminal `cancelled` and would seal the
            // record against a later regression). 'active' for live/dry-run.
            status: shouldArchiveAtStage(stage) ? 'paused' : 'active',
            ifMatch: fresh?.version,
        });
    }
}
//# sourceMappingURL=FeatureRolloutReconciler.js.map