// storage/project-resolver.ts — SPEC-1011 Bug F: Resilient project resolution with disk fingerprint
// Resolves a project from its data directory OR from disk fingerprints (planu/ + specs/).
// Used by planu_status, challenge_spec, and snapshot_spec_hashes to avoid "Project not found"
// when the knowledge entry is missing but the project clearly exists on disk.
import { access } from 'node:fs/promises';
import { join } from 'node:path';
import { getKnowledge, saveKnowledge } from './knowledge-store/knowledge.js';
import { hashProjectPath, projectDataDir } from './base-store.js';
/**
 * Check whether a file/directory path exists on disk (no-throw helper).
 */
async function pathExists(p) {
    try {
        await access(p);
        return true;
    }
    catch {
        return false;
    }
}
/**
 * Resolve a project from its projectPath.
 *
 * Strategy:
 * 1. Compute projectId from hashProjectPath(projectPath).
 * 2. Try getKnowledge(projectId) — standard fast path.
 * 3. If null, check disk fingerprints:
 *    a. data/projects/{hash}/ exists
 *    b. planu/specs/ exists AND (planu/status.json exists OR planu/specs/ exists)
 * 4. If any fingerprint matches, create a minimal knowledge entry via saveKnowledge,
 *    log the recovery event, and return { recovered: true }.
 * 5. If neither knowledge nor fingerprint exists, return null.
 */
export async function resolveProjectFromPath(projectPath) {
    const projectId = hashProjectPath(projectPath);
    // Fast path: knowledge.json present
    const existing = await getKnowledge(projectId);
    if (existing !== null) {
        return { projectId, knowledge: existing, recovered: false };
    }
    // Disk fingerprint checks
    const dataDir = projectDataDir(projectId);
    const planuDir = join(projectPath, 'planu');
    const specsDir = join(projectPath, 'planu', 'specs');
    const statusJson = join(projectPath, 'planu', 'status.json');
    const [dataDirExists, planuDirExists, statusJsonExists, specsDirExists] = await Promise.all([
        pathExists(dataDir),
        pathExists(planuDir),
        pathExists(statusJson),
        pathExists(specsDir),
    ]);
    let matchedFingerprint = null;
    if (dataDirExists) {
        matchedFingerprint = `data/projects/${projectId}/ exists`;
    }
    else if (planuDirExists && (statusJsonExists || specsDirExists)) {
        matchedFingerprint = statusJsonExists
            ? 'planu/ + planu/status.json exist'
            : 'planu/ + planu/specs/ exist';
    }
    if (matchedFingerprint === null) {
        return null;
    }
    // Build a minimal knowledge entry so subsequent tool calls succeed
    const minimalKnowledge = buildMinimalKnowledge(projectId, projectPath);
    try {
        await saveKnowledge(projectId, minimalKnowledge);
    }
    catch {
        // If save fails, still return the minimal knowledge so the caller can proceed this call
    }
    // Log the silent recovery for observability (best-effort, fire-and-forget)
    void logSilentRecovery(projectId, projectPath, matchedFingerprint);
    return { projectId, knowledge: minimalKnowledge, recovered: true };
}
/** Write a recovery log entry to autopilot-log.jsonl (best-effort). */
async function logSilentRecovery(projectId, projectPath, reason) {
    try {
        const { appendAutopilotLogEntry } = await import('./autopilot-log-store.js');
        await appendAutopilotLogEntry(projectId, {
            specId: 'system',
            hookName: 'silent_project_recovery',
            result: 'pass',
            timestamp: new Date().toISOString(),
            durationMs: 0,
            // Extra metadata beyond AutopilotLogEntry shape — stored for observability
            ...{ event: 'silent_project_recovery', projectPath, reason },
        });
    }
    catch {
        // best-effort — log failure must never block project resolution
    }
}
/** Build a minimal but valid ProjectKnowledge entry for a recovered project. */
function buildMinimalKnowledge(projectId, projectPath) {
    const now = new Date().toISOString();
    return {
        projectId,
        projectPath,
        locale: 'en',
        experienceLevel: 'intermediate',
        language: 'unknown',
        languageVersion: null,
        framework: null,
        frameworkVersion: null,
        packageManager: null,
        buildCommand: null,
        testCommand: null,
        lintCommand: null,
        stack: [],
        database: 'unknown', // recovered without DB knowledge — init_project re-detection will set the real value
        orm: null,
        architecture: {
            primary: 'monolith',
            secondary: [],
            layers: [],
            boundaries: [],
            communicationPatterns: [],
            deploymentUnits: [],
        },
        apps: [],
        conventions: {},
        layers: [],
        environments: [],
        apiContracts: [],
        monorepo: false,
        externalServices: [],
        tooling: {},
        specLocation: 'planu/specs',
        envSetup: {
            projectId,
            variables: [],
            missing: [],
            security: {
                gitignoreHasEnv: true,
                noSecretsInCode: true,
                noEnvInGit: true,
                noSecretsInLogs: true,
                usesSecretManager: false,
                issues: [],
            },
            setupSteps: [],
        },
        linting: {
            projectId,
            language: 'unknown',
            detectedLinters: [],
            missingRecommended: [],
            rulesConflicts: [],
            customRulesFromProject: [],
            maturityScore: 0,
        },
        availableMcps: [],
        docsRegistry: {},
        qualityProfile: {
            enabledCategories: ['solid', 'clean-code', 'architecture', 'security', 'error-handling'],
            customRules: [],
            principles: ['SOLID', 'Clean Code', 'Fail Fast', 'DRY'],
            strictness: 'standard',
        },
        lastAnalyzed: now,
    };
}
//# sourceMappingURL=project-resolver.js.map