import crypto from "node:crypto";
import { rename, rm, writeFile } from "node:fs/promises";
import { mkdir } from "node:fs/promises";
import * as path from "node:path";
import process from "node:process";
import { assessReadiness, } from "./doctor-readiness-blockers.js";
import { DELIVERY_AUTHORITY_DIMENSION_ID, assessDeliveryAuthorityDimension, } from "./doctor-readiness-delivery.js";
import { CONTEXT_ROUTING_DIMENSION_ID, assessContextRoutingDimension, } from "./doctor-readiness-context.js";
import { DEPENDENCIES_SUPPLY_CHAIN_DIMENSION_ID, assessDependenciesSupplyChainDimension, } from "./doctor-readiness-supply-chain.js";
import { CAPABILITIES_TOOLS_DIMENSION_ID, assessCapabilitiesToolsDimension, } from "./doctor-readiness-capabilities.js";
import { DOMAIN_OWNERSHIP_DIMENSION_ID, assessDomainOwnershipDimension, } from "./doctor-readiness-domain.js";
import { FEEDBACK_GUARDRAILS_DIMENSION_ID, assessFeedbackGuardrailsDimension, } from "./doctor-readiness-guardrails.js";
import { EXECUTION_PROOF_DIMENSION_ID, PROPORTIONALITY_DIMENSION_ID, assessExecutionProofDimension, assessProportionalityDimension, } from "./doctor-readiness-journey.js";
import { sanitizeReadinessReportForPersistence } from "./doctor-readiness-report-redaction.js";
import { getPackageVersion } from "./version.js";
// Re-export the blocker-gate surface (RRR-5, #1857) so `.lisa/readiness.json`
// readers and tests keep one import path even though the gate lives in a
// sibling module for file-size hygiene.
export { CLAIM_EVIDENCE_UNAVAILABLE_REASON, SHIP_BLOCKER_IDS, assessProvabilityBlocker, assessReadiness, computeNarrowedClaim, detectShipBlockers, } from "./doctor-readiness-blockers.js";
/** Doctor check name for the repository-readiness collector. */
const REPOSITORY_READINESS_CHECK_NAME = "Repository readiness";
/** Repo-relative display path for the persisted readiness report. */
const READINESS_REPORT_DISPLAY_PATH = path.join(".lisa", "readiness.json");
/**
 * Schema version stamped into `.lisa/readiness.json`. Bump only alongside a
 * deliberate shape change; readers key off this to stay forward-compatible.
 */
export const READINESS_SCHEMA_VERSION = 1;
/**
 * The eight ownership dimensions, in fixed render order, defined once by the
 * `readiness-rubric` rule (RRR-1, #1853). This collector consumes that rubric —
 * it does not redefine the vocabulary. As of #1896 every one of the eight is
 * wired to a producer in {@link DIMENSION_PRODUCERS}, so no dimension is ever
 * blank: each renders either a producer's result or that producer's own stated
 * reason for not assessing it, per the never-silently-omit contract (#1898).
 */
const READINESS_DIMENSIONS = [
    {
        id: "context-routing",
        question: "Can an agent recover the real job from what is written down (integration-access-layer, wiki-knowledge-source, config-resolution)?",
    },
    {
        id: "capabilities-tools",
        question: "Is every tool the work needs provably reachable, not merely installed (tool-access-gate)?",
    },
    {
        id: "domain-ownership",
        question: "Are the business rules, glossary, and danger zones owned and written down (agent-ready domain phase wiki pages)?",
    },
    {
        id: "execution-proof",
        question: "Can the claimed user-visible outcome be proved by running the system (verification, empirical-inquiry, claim-evidence-mapping)?",
    },
    {
        id: "feedback-guardrails",
        question: "Does a failing loop produce a named outcome and a runbook (automation-runbook-contract, observability-audit)?",
    },
    {
        id: "dependencies-supply-chain",
        question: "Is there a confidence model for what the repo depends on (security-audit-handling)?",
    },
    {
        id: "delivery-authority",
        question: "Does the thing that ships equal the thing that was validated, and does the shipping credential carry only the authority it needs (claim-archaeology, security-audit-handling)?",
    },
    {
        id: "proportionality",
        question: "Is the machinery proportional to the job, or is there scaffolding to subtract (repo-scope-split, #1742 subtraction candidates)?",
    },
];
/** The eight ownership dimension ids, in fixed render order. */
export const READINESS_DIMENSION_IDS = READINESS_DIMENSIONS.map(dimension => dimension.id);
/**
 * Resolve the single location every reader and writer uses for the persisted
 * readiness report. Centralizing the path here is intake decision O2's
 * mitigation: relocating the artifact (for example to
 * `wiki/state/agent-ready/readiness.json`) is a one-line change with no other
 * caller to update.
 * @param root - Project root to resolve the report under
 * @returns Absolute path to `.lisa/readiness.json`
 */
export function resolveReadinessReportPath(root) {
    return path.join(root, ".lisa", "readiness.json");
}
/**
 * Assess repository readiness and persist the report to `.lisa/readiness.json`.
 *
 * This is the flag-gated collector `runDoctor` appends only when
 * `options.readiness` is true, so the default doctor path stays byte-identical.
 * It is warn-only per intake decision O1: it never returns `fail` and never
 * changes doctor's exit-code semantics — a standing blocker flips the readiness
 * verdict to `NOT_READY` but still emits a `warn` check, because the gate is on
 * the *claim*, not the *process*. Every dimension is reported (never silently
 * omitted), and a dimension whose evidence surface has not shipped yet renders
 * `SKIP` carrying the reason it was not assessed (#1898). Persistence is atomic and never throws — a write error degrades
 * the check to `warn` instead of aborting the run.
 * @param targetPath - Project path to assess and persist under
 * @returns Doctor check result
 */
export async function checkRepositoryReadiness(targetPath) {
    const dimensions = await buildReadinessDimensions(targetPath);
    const { verdict, blockers, narrowed_claim } = assessReadiness(dimensions);
    const report = {
        schema_version: READINESS_SCHEMA_VERSION,
        generated_at: new Date().toISOString(),
        lisa_version: resolveLisaVersion(),
        worker_signature: currentWorkerSignature(),
        verdict,
        narrowed_claim,
        blockers,
        blocker_count: blockers.length,
        dimensions,
    };
    const headline = formatReadinessHeadline(verdict, dimensions);
    const blockerPhrase = `${blockers.length} standing ship blocker${blockers.length === 1 ? "" : "s"}`;
    const standingDetail = formatStandingBlockerDetail(blockers, narrowed_claim);
    const reportPath = resolveReadinessReportPath(targetPath);
    try {
        await persistReadinessReport(reportPath, sanitizeReadinessReportForPersistence(report));
    }
    catch (error) {
        return {
            name: REPOSITORY_READINESS_CHECK_NAME,
            status: "warn",
            detail: 
            // A failed write is precisely when the operator cannot go read the
            // report, so this branch must name the standing blockers too.
            `${headline} (${blockerPhrase}).${standingDetail} ` +
                `Could not persist ${READINESS_REPORT_DISPLAY_PATH}: ` +
                `${error instanceof Error ? error.message : String(error)}`,
        };
    }
    return {
        name: REPOSITORY_READINESS_CHECK_NAME,
        status: verdict === "READY" ? "ok" : "warn",
        detail: `${headline} (${blockerPhrase}; see readiness-rubric).${standingDetail} ` +
            `Report written to ${READINESS_REPORT_DISPLAY_PATH} (schema_version ${READINESS_SCHEMA_VERSION}).`,
    };
}
/**
 * Name each standing blocker and state the narrowed claim in the operator-facing
 * detail line. A bare `NOT_READY` is unactionable at the gate: the reader is
 * non-technical (`factory-model`), so they must be told which blocker stands and
 * — the rubric's net-new requirement — what the repository IS still ready for.
 * Returns an empty string when nothing stands, leaving the clean line untouched.
 * @param blockers - Standing ship blockers
 * @param narrowedClaim - The computed narrowed claim, or null
 * @returns A leading-space detail fragment, or `""`
 */
function formatStandingBlockerDetail(blockers, narrowedClaim) {
    if (blockers.length === 0) {
        return "";
    }
    const named = blockers
        .map(blocker => `${blocker.id} — ${blocker.label}`)
        .join("; ");
    return ` Standing: ${named}.${narrowedClaim ? ` ${narrowedClaim}` : ""}`;
}
/**
 * Write the operator-facing headline for a readiness run, conclusion first.
 *
 * The reader at the gate is non-technical (`factory-model`), so the sentence
 * leads with what it means rather than burying it after the mechanics. When any
 * dimension was never assessed the headline says the readiness question is NOT
 * ESTABLISHED and states how much was skipped — silence is reported as silence,
 * never as health (#1897). Once every dimension has been assessed the
 * not-established caveat is dropped entirely, because a fully assessed run must
 * not carry a sentence contradicting its own verdict. The doctor renderer
 * already prefixes the check name, so this text does not repeat it.
 * @param verdict - The computed verdict for this run
 * @param dimensions - The per-dimension records for this run
 * @returns One operator-language clause, conclusion first, with no trailing period
 */
export function formatReadinessHeadline(verdict, dimensions) {
    const unassessed = dimensions.filter(dimension => dimension.status === "SKIP").length;
    if (unassessed === 0) {
        return (`${verdict} — every dimension was assessed ` +
            `across ${dimensions.length} ownership dimensions`);
    }
    return (`NOT ESTABLISHED (${verdict}) — ${unassessed} of ${dimensions.length} ` +
        `dimensions ${unassessed === 1 ? "was" : "were"} never assessed, so this ` +
        `cannot say whether an unattended fleet may operate here`);
}
/**
 * Build the eight readiness dimensions in fixed render order by dispatching each
 * to its producer. Execution/proof and proportionality are wired to #1742's
 * shared journey runner (RRR-6, #1858); delivery/authority is wired to the
 * offline workflow producers assessing ship blockers B2 and B3, context/routing
 * to the documentation cross-check assessing B6, dependencies/supply-chain to
 * the manifest and audit-gate producer assessing B5, domain-ownership to the
 * silent-data-loss producer assessing B1, feedback/guardrails to the
 * gate-and-recovery producer assessing B4, and capabilities/tools to a producer
 * whose only honest answer is a reasoned SKIP, because tool reachability needs a
 * live probe (#1896). A dimension with no producer would fall through to a
 * stated-reason SKIP rather than a blank one (#1898), so the report never
 * presents "never looked" as "nothing to report".
 * @param targetPath - Project path to assess
 * @returns The eight per-dimension records, in fixed order
 */
async function buildReadinessDimensions(targetPath) {
    return Promise.all(READINESS_DIMENSIONS.map(async (dimension) => {
        const produce = DIMENSION_PRODUCERS[dimension.id];
        return produce
            ? await runDimensionProducer(dimension.id, targetPath, produce)
            : reasonedSkip(dimension.skipReason ??
                `No readiness probe is wired for the ${dimension.id} dimension ` +
                    "in this Lisa version, so it was not assessed.", dimension.id);
    }));
}
/**
 * Run one dimension's producer behind an error boundary. A producer that throws
 * degrades its own dimension to a stated-reason SKIP instead of aborting the
 * whole readiness check — losing one dimension is recoverable, losing the report
 * is not, and the operator is told which dimension failed and why.
 * @param id - The dimension id
 * @param targetPath - Project path to assess
 * @param produce - The dimension's producer
 * @returns The produced record, or a stated-reason SKIP on failure
 */
export async function runDimensionProducer(id, targetPath, produce) {
    try {
        return await produce(targetPath);
    }
    catch (error) {
        return reasonedSkip(`The ${id} readiness probe could not complete, so this dimension was not ` +
            `assessed: ${error instanceof Error ? error.message : String(error)}`, id);
    }
}
/**
 * The producer for each wired dimension, keyed by dimension id. Adding a
 * dimension's evidence surface is one entry here; a dimension absent from this
 * map falls through to {@link reasonedSkip} rather than to silence.
 */
const DIMENSION_PRODUCERS = {
    [CONTEXT_ROUTING_DIMENSION_ID]: async (targetPath) => await assessContextRoutingDimension(targetPath),
    [DEPENDENCIES_SUPPLY_CHAIN_DIMENSION_ID]: async (targetPath) => await assessDependenciesSupplyChainDimension(targetPath),
    [EXECUTION_PROOF_DIMENSION_ID]: async (targetPath) => await assessExecutionProofDimension(targetPath),
    [PROPORTIONALITY_DIMENSION_ID]: async (targetPath) => await assessProportionalityDimension(targetPath),
    [DELIVERY_AUTHORITY_DIMENSION_ID]: async (targetPath) => await assessDeliveryAuthorityDimension(targetPath),
    [CAPABILITIES_TOOLS_DIMENSION_ID]: async (targetPath) => await assessCapabilitiesToolsDimension(targetPath),
    [DOMAIN_OWNERSHIP_DIMENSION_ID]: async (targetPath) => await assessDomainOwnershipDimension(targetPath),
    [FEEDBACK_GUARDRAILS_DIMENSION_ID]: async (targetPath) => await assessFeedbackGuardrailsDimension(targetPath),
};
/**
 * Build the stated-reason SKIP a dimension without a wired producer renders
 * (#1898). A blank SKIP reads as "nothing to report" when it actually means
 * "never looked", so the reason the rubric already carries is surfaced into the
 * record instead of being dropped. It names no `blocker`, so it can never stand
 * one up.
 * @param reason - Operator-language reason the dimension was not assessed
 * @param id - The dimension id
 * @returns The SKIP dimension record
 */
function reasonedSkip(reason, id) {
    return { id, status: "SKIP", findings: [{ reason, skip: true }] };
}
/**
 * Atomically write the readiness report: write a sibling temp file, then rename
 * it into place so a reader never observes a partial document.
 *
 * The temp name carries a UUID, and that is the load-bearing part rather than a
 * flourish. It used to be a fixed `${reportPath}.tmp`, which meant two
 * concurrent `lisa doctor` runs in one checkout opened the SAME file: both
 * truncated it, both wrote from offset zero, and the first to finish renamed it
 * out from under the second — whose descriptor then pointed at the published
 * report, so its tail landed inside the document the first run had just
 * published. The rename was always atomic; the CONTENT being renamed was not,
 * which is precisely the property the paragraph above claims to buy.
 *
 * Concurrent doctor runs are the normal operating mode here, not an edge case:
 * six agents ran against this repository at once on 2026-08-24.
 *
 * `crypto.randomUUID()` rather than a pid or a timestamp, matching
 * `src/core/learnings-lock.ts`, which uses it for its owner token and its
 * quarantine path for exactly this reason. A pid collides across containers and
 * is reused after wraparound; a millisecond timestamp collides between two runs
 * started in the same tick, which is the case that matters most.
 * @param reportPath - Destination path
 * @param report - Report to persist
 */
export async function persistReadinessReport(reportPath, report) {
    await mkdir(path.dirname(reportPath), { recursive: true });
    const tempPath = `${reportPath}.${crypto.randomUUID()}.tmp`;
    try {
        await writeFile(tempPath, `${JSON.stringify(report, null, 2)}\n`, "utf8");
        await rename(tempPath, reportPath);
    }
    catch (error) {
        // A unique name is only half the fix. `.lisa/` is a directory a consumer
        // commits, so a temp file orphaned by a failed write is a stray artifact
        // that gets committed — and unlike the old fixed name, a UUID name would
        // accumulate one per failure rather than being overwritten by the next run.
        // Removal is best-effort: the write error is the one worth reporting.
        await rm(tempPath, { force: true }).catch(() => undefined);
        throw error;
    }
}
/**
 * Resolve Lisa's package version for the persisted stamp, degrading to
 * `"unknown"` rather than throwing when package metadata cannot be read.
 * @returns Lisa version string
 */
function resolveLisaVersion() {
    try {
        return getPackageVersion();
    }
    catch {
        return "unknown";
    }
}
/**
 * Build the current runtime worker signature the readiness stamp records. This
 * mirrors the worker-epoch host/model/version identity so #1740 can reuse the
 * same stamp (sibling-boundary decision).
 * @returns `host/model/version` signature string
 */
function currentWorkerSignature() {
    const env = process["env"];
    const host = (env.LISA_WORKER_HOST ??
        env.LISA_REMOTE_AGENT ??
        (env.CODEX_HOME || env.CODEX_SANDBOX
            ? "codex"
            : env.CLAUDECODE
                ? "claude"
                : "unknown")).toLowerCase();
    const model = env.LISA_WORKER_MODEL ?? env.CODEX_MODEL ?? "unknown";
    const version = env.LISA_WORKER_VERSION ?? env.CODEX_VERSION ?? "unknown";
    return `${host}/${model}/${version}`;
}
//# sourceMappingURL=doctor-readiness.js.map