// engine/handoff-artifacts/io.ts — SPEC-725: Atomic I/O for handoff artifacts
import { createHash } from 'node:crypto';
import { readFile } from 'node:fs/promises';
import { join } from 'node:path';
import { atomicWriteFile } from '../safety/atomic-write-file.js';
import { acquireLock, releaseLock } from '../safety/cross-process-lock.js';
import { appendTransitionEvent } from '../../storage/transition-log.js';
import { validateArtifact } from './schemas.js';
import { resolveCompat } from './version-policy.js';
import { projectDataDir } from '../../storage/base-store.js';
// Current schema version for version-policy checks
const CURRENT_SCHEMA_VERSION = '1.0.0';
// ---------------------------------------------------------------------------
// Path resolution
// ---------------------------------------------------------------------------
/**
 * Returns the file path for a handoff artifact.
 * Layout: ~/.planu/data/projects/<projectId>/handoffs/<specId>/<kind>
 *
 * Note: kind values like 'spec.lock', 'review_feedback', etc. are used directly as filenames.
 */
function artifactPath(projectId, specId, kind) {
    // review_feedback is a .md file, rest are .json
    const filename = kind === 'review_feedback' ? 'review_feedback.md' : `${kind}.json`;
    return join(projectDataDir(projectId), 'handoffs', specId, filename);
}
/** Map artifact kind to the transition-log eventType */
function eventTypeForKind(kind) {
    const map = {
        intake: 'handoff.intake',
        'spec.lock': 'handoff.spec.lock',
        review_feedback: 'handoff.review_feedback',
        'implementation-report': 'handoff.implementation-report',
        'validation-report': 'handoff.validation-report',
    };
    return map[kind];
}
/**
 * Validate and atomically write a handoff artifact under a cross-process lock.
 * Also appends a transition-log event with the SHA-256 of the payload.
 *
 * Steps:
 *  1. Acquire lock keyed on `${projectId}:${specId}:${kind}`
 *  2. Validate payload via validateArtifact
 *  3. Write via atomicWriteFile
 *  4. Compute SHA-256 of payload JSON
 *  5. Call appendTransitionEvent
 *  6. Release lock
 */
export async function appendArtifact(input) {
    const { projectId, specId, kind, payload } = input;
    const lockKey = `${projectId}:${specId}:${kind}`;
    // We use projectId as the 'projectPath' for the lock — the lock dir is relative
    // to cwd, so we use a combined specId that encodes the artifact key
    const lockHandle = await acquireLock('.', lockKey, { reason: `handoff.${kind}` });
    if (lockHandle === null) {
        throw new Error(`[handoff-artifacts] Could not acquire lock for ${lockKey}`);
    }
    try {
        // Validate payload
        const validation = validateArtifact(kind, payload);
        if (!validation.ok) {
            const firstError = validation.errors[0];
            throw new Error(`[handoff-artifacts] Validation failed for ${kind}: ${firstError?.code ?? 'UNKNOWN'} at ${firstError?.path ?? '?'} — ${firstError?.message ?? ''}`);
        }
        // Serialize to JSON (or for review_feedback, use body if present)
        const serialized = JSON.stringify(payload, null, 2);
        const targetPath = artifactPath(projectId, specId, kind);
        // Compute SHA-256 of payload
        const sha = createHash('sha256').update(serialized, 'utf8').digest('hex');
        // Write atomically
        await atomicWriteFile(targetPath, serialized);
        // Emit transition-log event
        await appendTransitionEvent({
            projectId,
            specId,
            eventType: eventTypeForKind(kind),
            frontmatterSha: sha,
        });
        return { path: targetPath, sha };
    }
    finally {
        await releaseLock(lockHandle);
    }
}
/**
 * Read a handoff artifact from disk, validate it, and apply version-policy checks.
 * Returns ValidationOk with warnings on forward-minor compatibility, ValidationErr on failure.
 * Returns ValidationErr if the file does not exist.
 */
export async function readArtifact(args) {
    const { projectId, specId, kind } = args;
    const targetPath = artifactPath(projectId, specId, kind);
    let raw;
    try {
        raw = await readFile(targetPath, 'utf-8');
    }
    catch (err) {
        const code = err instanceof Error && 'code' in err ? err.code : undefined;
        if (code === 'ENOENT') {
            return {
                ok: false,
                errors: [
                    {
                        code: 'ARTIFACT_NOT_FOUND',
                        path: targetPath,
                        message: `Artifact not found: ${targetPath}`,
                    },
                ],
            };
        }
        throw err;
    }
    let parsed;
    try {
        parsed = JSON.parse(raw);
    }
    catch {
        return {
            ok: false,
            errors: [
                {
                    code: 'INVALID_JSON',
                    path: targetPath,
                    message: 'Artifact file contains invalid JSON',
                },
            ],
        };
    }
    // Run schema validation
    const validation = validateArtifact(kind, parsed);
    if (!validation.ok) {
        return validation;
    }
    // Apply version-policy checks
    const warnings = [...validation.warnings];
    const payloadVersion = parsed.schema_version;
    if (payloadVersion) {
        const compat = resolveCompat(payloadVersion, CURRENT_SCHEMA_VERSION);
        if (!compat.ok) {
            return {
                ok: false,
                errors: [
                    {
                        code: 'INCOMPATIBLE_MAJOR',
                        path: 'schema_version',
                        message: compat.warning ??
                            `Major version mismatch: ${payloadVersion} vs ${CURRENT_SCHEMA_VERSION}`,
                    },
                ],
            };
        }
        if (compat.level === 'forward-minor' && compat.warning) {
            warnings.push({ code: 'FORWARD_COMPAT_MINOR', message: compat.warning });
        }
    }
    return {
        ok: true,
        payload: validation.payload,
        warnings,
    };
}
//# sourceMappingURL=io.js.map