/**
 * ApprenticeshipProgram — the standing program scaffold for apprenticeship /
 * mentorship instances (Apprenticeship Step 1, APPRENTICESHIP-STEP1-PROGRAM-
 * SCAFFOLD-SPEC.md).
 *
 * Each instance is a project under a standing program that crystallizes by
 * bootstrap. This module is the instance-as-project REGISTRY plus the two
 * lifecycle GATES that make "review before you start / capture before you
 * close" unskippable at the state-mutating transition:
 *
 *   - the RETRO-GATE (pending→active): refused unless the prior instance's
 *     retro-harvest exists at its CANONICAL CONFINED PATH and passes the
 *     Step 0 validator (the first instance is seeded by the Echo→Codey
 *     bootstrap harvest).
 *   - the DOC-AS-REQUIRED-ARTIFACT GATE (active→complete): refused until the
 *     instance's declared-required artifacts are VERIFIED PRESENT FROM LIVE
 *     STATE (never a stored flag).
 *
 * Signal vs Authority / The Body and the Mind: the gates are structural
 * preconditions on OBJECTIVE artifacts ("does a validated harvest exist?",
 * "is there ≥1 instance-scoped ledger entry?"). They are NOT quality
 * judgments — whether the mentor truly internalized the lessons stays with
 * the overseer (the mind), informed by the gate. Every verdict is appended
 * to logs/apprenticeship-decisions.jsonl (the decision audit).
 *
 * Concurrency: file-backed store at `.instar/apprenticeship/instances.json`,
 * atomic tmp-write+rename, with a single in-process serialized mutator and an
 * optimistic `version` CAS (lost-update safe under the single-process-per-agent
 * AgentServer model). A corrupt/unparseable store FAILS CLOSED — gates return
 * `allow:false`, never "no prior instance → open the gate".
 */
import fs from 'node:fs';
import path from 'node:path';
import { validateRetroHarvest, parseArtifact, safeArtifactPath, } from './retroHarvestValidator.js';
/** The typed no-op overseer stub (§2). */
export const noopApprenticeshipOverseer = {
    computeDifferential(_instance) {
        return { implemented: false, reason: 'differential read-channel is implemented in Step 4 (need-001)' };
    },
};
const CHARSET_RE = /^[a-z0-9-]+$/;
/** Legal status transitions (§3.4). `complete` is terminal. */
const TRANSITIONS = {
    pending: ['active'],
    active: ['complete', 'blocked'],
    blocked: ['active'],
    complete: [], // terminal
};
// ── Implementation ───────────────────────────────────────────────────
export class ApprenticeshipProgram {
    storePath;
    decisionLogPath;
    projectDir;
    store;
    /** True when the on-disk store was unparseable → gates fail closed. */
    corrupt = false;
    deps;
    constructor(config) {
        this.storePath = path.join(config.stateDir, 'apprenticeship', 'instances.json');
        this.decisionLogPath =
            config.decisionLogPath ?? path.join(config.stateDir, 'logs', 'apprenticeship-decisions.jsonl');
        this.projectDir = config.projectDir;
        this.deps = {
            readHarvest: config.deps?.readHarvest ?? ((rel) => this.defaultReadHarvest(rel)),
            validate: config.deps?.validate ?? validateRetroHarvest,
            countInstanceLedgerEntries: config.deps?.countInstanceLedgerEntries ?? (() => 0),
            detectorAuditExists: config.deps?.detectorAuditExists ?? (() => false),
        };
        this.store = this.loadStore();
    }
    /** Default harvest reader — resolves the confined relative path under projectDir. */
    defaultReadHarvest(relPath) {
        try {
            const full = path.join(this.projectDir, relPath);
            if (!fs.existsSync(full))
                return null;
            return fs.readFileSync(full, 'utf-8');
        }
        catch {
            return null;
        }
    }
    // ── Persistence ────────────────────────────────────────────────────
    loadStore() {
        try {
            if (fs.existsSync(this.storePath)) {
                const raw = fs.readFileSync(this.storePath, 'utf-8');
                const data = JSON.parse(raw);
                if (data && data.version === 1 && Array.isArray(data.instances)) {
                    this.corrupt = false;
                    return data;
                }
                // Present but unparseable shape → fail closed (do NOT silently reset).
                this.corrupt = true;
                return { version: 1, instances: [], lastModified: new Date().toISOString() };
            }
        }
        catch {
            // A present-but-corrupt file fails closed; a truly-missing file is fresh.
            if (fs.existsSync(this.storePath)) {
                this.corrupt = true;
                return { version: 1, instances: [], lastModified: new Date().toISOString() };
            }
        }
        this.corrupt = false;
        return { version: 1, instances: [], lastModified: new Date().toISOString() };
    }
    saveStore() {
        this.store.lastModified = new Date().toISOString();
        const dir = path.dirname(this.storePath);
        fs.mkdirSync(dir, { recursive: true });
        const tmpPath = `${this.storePath}.${process.pid}.tmp`;
        fs.writeFileSync(tmpPath, JSON.stringify(this.store, null, 2) + '\n');
        fs.renameSync(tmpPath, this.storePath);
    }
    /**
     * The single in-process serialized mutator with optimistic `version` CAS.
     * JS is single-threaded and `fn` is synchronous, so a straight read → apply
     * → version++ → persist cannot interleave with another mutate() under the
     * single-process model; the CAS guards against a same-tick stale read by
     * re-reading the live record before commit. Returns the persisted snapshot.
     */
    mutate(id, fn) {
        const idx = this.store.instances.findIndex((i) => i.id === id);
        if (idx === -1)
            throw new Error(`ApprenticeshipProgram.mutate: unknown instance id ${id}`);
        const current = this.store.instances[idx];
        const observedVersion = current.version ?? 0;
        const next = fn({ ...current });
        // CAS re-read: under single-process this is belt-and-braces.
        const latestIdx = this.store.instances.findIndex((i) => i.id === id);
        if (latestIdx === -1)
            throw new Error(`ApprenticeshipProgram.mutate: instance ${id} disappeared mid-apply`);
        if ((this.store.instances[latestIdx].version ?? 0) !== observedVersion) {
            throw new Error(`ApprenticeshipProgram.mutate: version drift on ${id} (CAS abort)`);
        }
        const committed = {
            ...next,
            // requiredArtifacts is immutable after create — restore from the live record.
            requiredArtifacts: current.requiredArtifacts,
            version: observedVersion + 1,
            updatedAt: new Date().toISOString(),
        };
        this.store.instances[latestIdx] = committed;
        this.saveStore();
        return committed;
    }
    // ── Read ───────────────────────────────────────────────────────────
    list() {
        return [...this.store.instances];
    }
    get(id) {
        return this.store.instances.find((i) => i.id === id) ?? null;
    }
    /** True when the on-disk store was present but unparseable (gates fail closed). */
    isCorrupt() {
        return this.corrupt;
    }
    // ── Create ─────────────────────────────────────────────────────────
    /**
     * Create an instance. Charset-clamps id/overseer/mentor/mentee/framework to
     * ^[a-z0-9-]+$, rejects a duplicate id, computes harvestFrom=mentor /
     * harvestTo=mentee. requiredArtifacts defaults to all-required.
     */
    createInstance(input) {
        if (this.corrupt) {
            throw new Error('ApprenticeshipProgram: store is corrupt — refusing to create (fail closed)');
        }
        const overseer = input.overseer ?? '';
        const fields = {
            id: input.id,
            mentor: input.mentor,
            mentee: input.mentee,
            framework: input.framework,
        };
        // overseer is optional (apprenticeship has none); clamp only when present.
        if (overseer)
            fields.overseer = overseer;
        for (const [k, v] of Object.entries(fields)) {
            if (typeof v !== 'string' || !CHARSET_RE.test(v)) {
                throw new Error(`ApprenticeshipProgram.createInstance: ${k}="${v}" must match ^[a-z0-9-]+$`);
            }
        }
        if (input.instanceType !== 'apprenticeship' && input.instanceType !== 'mentorship') {
            throw new Error(`ApprenticeshipProgram.createInstance: invalid instanceType "${input.instanceType}"`);
        }
        if (this.store.instances.some((i) => i.id === input.id)) {
            throw new Error(`ApprenticeshipProgram.createInstance: duplicate id "${input.id}"`);
        }
        const now = new Date().toISOString();
        const harvestFrom = input.mentor; // a harvest is named for the mentoring edge (§3.1)
        const harvestTo = input.mentee;
        // Record the canonical path for humans only — NEVER the resolution source.
        let harvestRef = null;
        try {
            harvestRef = safeArtifactPath(harvestFrom, harvestTo, input.instanceType);
        }
        catch {
            harvestRef = null;
        }
        const instance = {
            id: input.id,
            instanceType: input.instanceType,
            overseer,
            mentor: input.mentor,
            mentee: input.mentee,
            framework: input.framework,
            status: 'pending',
            priorInstanceId: input.priorInstanceId ?? null,
            requiredArtifacts: {
                retroHarvest: input.requiredArtifacts?.retroHarvest ?? true,
                ledgerEntries: input.requiredArtifacts?.ledgerEntries ?? true,
                detectorAudit: input.requiredArtifacts?.detectorAudit ?? true,
            },
            programNeeds: input.programNeeds ?? [],
            harvestFrom,
            harvestTo,
            harvestRef,
            version: 0,
            createdAt: now,
            updatedAt: now,
        };
        this.store.instances.push(instance);
        this.saveStore();
        return instance;
    }
    // ── Gates (pure; truth re-derived from injected deps) ───────────────
    /**
     * The retro-gate (§3.3). priorInstanceId === null → the bootstrap seed (the
     * Echo→Codey artifact at its canonical path) must exist + validate. Else the
     * prior instance must be status:'complete' AND its canonical harvest must
     * exist + validate. partial-accepted passes ONLY with acceptance metadata
     * (acceptedBy + acceptedAt); complete always passes.
     *
     * The path is recomputed via safeArtifactPath(harvestFrom, harvestTo,
     * instanceType) — never a stored harvestRef.
     */
    evaluateStartGate(instance) {
        if (this.corrupt) {
            return { allow: false, reason: 'instance store is corrupt — fail closed' };
        }
        let priorInstance = null;
        let from;
        let to;
        let type;
        let priorHarvestExists;
        if (instance.priorInstanceId === null) {
            // Bootstrap: validate THIS instance's own canonical harvest (the seed).
            from = instance.harvestFrom;
            to = instance.harvestTo;
            type = instance.instanceType;
            priorHarvestExists = false;
        }
        else {
            priorInstance = this.get(instance.priorInstanceId);
            if (!priorInstance) {
                return { allow: false, reason: `prior instance "${instance.priorInstanceId}" not found` };
            }
            if (priorInstance.status !== 'complete') {
                return {
                    allow: false,
                    reason: `prior instance "${priorInstance.id}" is "${priorInstance.status}", must be "complete"`,
                };
            }
            from = priorInstance.harvestFrom;
            to = priorInstance.harvestTo;
            type = priorInstance.instanceType;
            priorHarvestExists = true;
        }
        // Recompute the confined path (NEVER the stored harvestRef).
        let relPath;
        try {
            relPath = safeArtifactPath(from, to, type);
        }
        catch (e) {
            return { allow: false, reason: `cannot resolve canonical harvest path: ${e.message}` };
        }
        const text = this.deps.readHarvest(relPath);
        if (text === null) {
            return { allow: false, reason: `retro-harvest missing at canonical path ${relPath}` };
        }
        const result = this.deps.validate(text, { priorHarvestExists });
        if (!result.valid) {
            return { allow: false, reason: `retro-harvest at ${relPath} is invalid: ${result.errors.join('; ')}` };
        }
        // partial-accepted requires acceptance metadata (acceptedBy + acceptedAt).
        let fm;
        try {
            fm = parseArtifact(text).frontmatter;
        }
        catch (e) {
            return { allow: false, reason: `cannot parse harvest frontmatter: ${e.message}` };
        }
        const completeness = fm.completeness;
        if (completeness === 'partial-accepted') {
            const acceptedBy = fm.acceptedBy;
            const acceptedAt = fm.acceptedAt;
            if (!acceptedBy || !acceptedAt) {
                return {
                    allow: false,
                    reason: `partial harvest at ${relPath} awaiting acceptance (acceptedBy + acceptedAt required)`,
                };
            }
        }
        return {
            allow: true,
            reason: instance.priorInstanceId === null
                ? `bootstrap seed ${relPath} present + valid (completeness=${String(completeness)})`
                : `prior instance "${priorInstance.id}" complete; harvest ${relPath} present + valid`,
        };
    }
    /**
     * The doc-as-required-artifact gate (§3.3). Each requiredArtifacts flag the
     * instance DECLARES required is checked against LIVE state via injected deps —
     * harvestExists+validates, an instance-scoped ledger count ≥1, detectorAudit
     * present. A stored requiredArtifacts:true is NEVER treated as evidence of
     * presence.
     */
    evaluateCompletionGate(instance) {
        if (this.corrupt) {
            return { allow: false, reason: 'instance store is corrupt — fail closed', missing: ['store-corrupt'] };
        }
        const missing = [];
        if (instance.requiredArtifacts.retroHarvest) {
            // This instance's OWN harvest (mentor→mentee edge), at its canonical path.
            let relPath = null;
            try {
                relPath = safeArtifactPath(instance.harvestFrom, instance.harvestTo, instance.instanceType);
            }
            catch {
                relPath = null;
            }
            const text = relPath ? this.deps.readHarvest(relPath) : null;
            if (text === null) {
                missing.push('retroHarvest:absent');
            }
            else {
                const result = this.deps.validate(text, { priorHarvestExists: instance.priorInstanceId !== null });
                if (!result.valid)
                    missing.push('retroHarvest:invalid');
            }
        }
        if (instance.requiredArtifacts.ledgerEntries) {
            const n = this.deps.countInstanceLedgerEntries(instance);
            if (!(n >= 1))
                missing.push('ledgerEntries:none');
        }
        if (instance.requiredArtifacts.detectorAudit) {
            if (!this.deps.detectorAuditExists(instance))
                missing.push('detectorAudit:absent');
        }
        if (missing.length > 0) {
            return {
                allow: false,
                reason: `completion blocked — missing live artifacts: ${missing.join(', ')}`,
                missing,
            };
        }
        return { allow: true, reason: 'all declared-required artifacts verified present from live state', missing: [] };
    }
    // ── Status transitions (the gates are not advisory) ─────────────────
    /**
     * The ONLY way status changes (§3.4). pending→active runs evaluateStartGate
     * and refuses on !allow; active→complete runs evaluateCompletionGate;
     * active→blocked / blocked→active (re-gate) allowed; complete is terminal.
     * Any transition not in the table is rejected with a reason. Every gate
     * verdict is appended to the decision audit.
     */
    transition(id, to) {
        if (this.corrupt) {
            return { ok: false, reason: 'instance store is corrupt — fail closed' };
        }
        const instance = this.get(id);
        if (!instance)
            return { ok: false, reason: `instance "${id}" not found` };
        const allowed = TRANSITIONS[instance.status] ?? [];
        if (!allowed.includes(to)) {
            return {
                ok: false,
                reason: `illegal transition ${instance.status}→${to} (allowed: ${allowed.join(', ') || 'none — terminal'})`,
            };
        }
        // Gate the guarded transitions.
        if (instance.status === 'pending' && to === 'active') {
            const verdict = this.evaluateStartGate(instance);
            this.recordDecision({ gate: 'start', instanceId: id, allow: verdict.allow, reason: verdict.reason });
            if (!verdict.allow)
                return { ok: false, reason: `start gate refused: ${verdict.reason}` };
        }
        if (instance.status === 'blocked' && to === 'active') {
            // Re-gate on resume from blocked.
            const verdict = this.evaluateStartGate(instance);
            this.recordDecision({ gate: 'start', instanceId: id, allow: verdict.allow, reason: verdict.reason });
            if (!verdict.allow)
                return { ok: false, reason: `start gate refused (re-gate): ${verdict.reason}` };
        }
        if (instance.status === 'active' && to === 'complete') {
            const verdict = this.evaluateCompletionGate(instance);
            this.recordDecision({
                gate: 'completion',
                instanceId: id,
                allow: verdict.allow,
                reason: verdict.reason,
                missing: verdict.missing,
            });
            if (!verdict.allow)
                return { ok: false, reason: `completion gate refused: ${verdict.reason}` };
        }
        const updated = this.mutate(id, (i) => ({ ...i, status: to }));
        return { ok: true, reason: `transitioned ${instance.status}→${to}`, instance: updated };
    }
    // ── Decision audit (§3.6) ───────────────────────────────────────────
    recordDecision(entry) {
        try {
            const dir = path.dirname(this.decisionLogPath);
            fs.mkdirSync(dir, { recursive: true });
            const line = JSON.stringify({
                ts: new Date().toISOString(),
                gate: entry.gate,
                instanceId: entry.instanceId,
                allow: entry.allow,
                reason: entry.reason,
                ...(entry.missing && entry.missing.length ? { missing: entry.missing } : {}),
            }) + '\n';
            fs.appendFileSync(this.decisionLogPath, line);
        }
        catch {
            // @silent-fallback-ok — the audit is observability; a write failure must
            // not block the (already-decided) transition.
        }
    }
}
//# sourceMappingURL=ApprenticeshipProgram.js.map