import { ancestorJobs as ancestors, reusableCallerValidation, } from "./doctor-readiness-reusable-callers.js";
import { artifactNameMismatch, hasArtifactNameMismatch, hasUnprovenArtifactIntegrity, isPublishAction, isPromotionAction, promotesValidatedArtifact, PROMOTION_ACTION, unresolvedArtifactIntegrity, unresolvedPromotedArtifact, } from "./doctor-readiness-promoted-artifact.js";
export { PROMOTION_ACTION } from "./doctor-readiness-promoted-artifact.js";
const PUBLISH_VERB = /(npm publish|docker push|gh release upload|aws s3 sync|cdk deploy|eas submit)/;
const DOCKER_BUILD_PUSH_ACTION = "docker/build-push-action";
/** Commands that actually run a test suite. */
const VALIDATING_COMMANDS = [
    /\b(npm|yarn|pnpm|bun|npx|bunx)\s+(run\s+)?test\b/,
    /\btest:[\w:-]+/,
    /\b(vitest|jest|pytest|rspec|phpunit|minitest)\b/,
    /\b(go|cargo|mvn|gradle|make|dotnet)\s+test\b/,
];
/**
 * Whether a shell command runs a test suite.
 * @param run - The step's `run` text
 * @returns True when the command matches a known test invocation
 */
function isValidatingCommand(run) {
    return VALIDATING_COMMANDS.some(pattern => pattern.test(run));
}
/** Reusable workflows whose whole purpose is validation. */
const VALIDATING_WORKFLOW = /(quality|quality-rails|test|ci)\.ya?ml/;
/** Commands that produce an artifact locally, inside the shipping job. */
const LOCAL_BUILD_COMMANDS = [
    /\b(npm pack|npm run build|yarn build|bun run build|bun run build:\w+|pnpm build|docker build|tsc)\b/,
];
/**
 * Whether a templated package manager expression runs the standard build script.
 * @param run - The step's `run` text
 * @returns True for commands like `${{ inputs.package_manager }} run build`
 */
function isTemplatedPackageManagerBuild(run) {
    const lower = run.toLowerCase();
    const scan = (cursor) => {
        const start = lower.indexOf("${{", cursor);
        if (start < 0) {
            return false;
        }
        const end = lower.indexOf("}}", start + 3);
        if (end < 0) {
            return false;
        }
        const expression = lower.slice(start + 3, end);
        const tail = lower.slice(end + 2).trimStart();
        if ((expression.includes("package_manager") ||
            expression.includes("package-manager")) &&
            /^run\s+build(?:\s|$)/.test(tail)) {
            return true;
        }
        return scan(end + 2);
    };
    return scan(0);
}
/**
 * Whether a command builds an artifact inside the shipping job.
 * @param run - The step's `run` text
 * @returns True when the command matches a known local build invocation
 */
function isLocalBuildCommand(run) {
    return (LOCAL_BUILD_COMMANDS.some(pattern => pattern.test(run)) ||
        isTemplatedPackageManagerBuild(run));
}
const LOCAL_PATH_ARGUMENT = /\s\.{0,2}\//;
const LOCAL_ARTIFACT_FILE = /\.(tgz|tar\.gz|zip|whl)\b/;
const DEFAULT_BRANCHES = new Set(["main", "master"]);
const MAX_STEP_LABEL = 80;
/**
 * Describe a step for persisted evidence: prefer its `name`, else the first
 * non-empty line of its `run`, truncated. A raw multi-line `run` would dump a
 * shell script into `.lisa/readiness.json` and into an operator's face.
 * @param step - The step to describe
 * @returns A single-line label, at most {@link MAX_STEP_LABEL} characters
 */
export function describeStep(step) {
    const source = step.name.trim() !== ""
        ? step.name.trim()
        : (step.run
            .split("\n")
            .find(line => line.trim() !== "")
            ?.trim() ?? step.uses.trim());
    return source.length > MAX_STEP_LABEL
        ? `${source.slice(0, MAX_STEP_LABEL).trimEnd()}…`
        : source;
}
/** A rehearsal flag: the command goes through the motions and ships nothing. */
const DRY_RUN_FLAG = /--dry[-_]?run\b/;
/**
 * Normalize an action reference to its owner/repo component.
 * @param uses - Raw `uses:` value
 * @returns Lowercase action id without an `@ref`
 */
function actionId(uses) {
    return (uses.split("@")[0] ?? "").trim().toLowerCase();
}
/**
 * Whether a step actually puts an artifact in front of users. A `--dry-run`
 * invocation names a publish verb but ships nothing, so faulting its release
 * path would be a finding about something that cannot reach anyone.
 * @param step - The step to classify
 * @returns True when the step really publishes
 */
function isPublishStep(step) {
    return ((PUBLISH_VERB.test(step.run) && !DRY_RUN_FLAG.test(step.run)) ||
        isPublishAction(step));
}
/**
 * Find every step in a job that ships something.
 * @param job - The parsed job
 * @returns The publishing steps, in execution order
 */
export function findPublishSteps(job) {
    return job.steps.filter(isPublishStep);
}
/**
 * Whether a job proves anything about the artifact.
 * @param job - The parsed job
 * @returns True when the job runs a test suite or calls a validating workflow
 */
function isValidatingJob(job) {
    if (job.uses !== "" && VALIDATING_WORKFLOW.test(job.uses)) {
        return true;
    }
    return job.steps.some(step => isValidatingCommand(step.run) ||
        (step.uses !== "" && VALIDATING_WORKFLOW.test(step.uses)));
}
/**
 * Whether validation provably precedes the publish: either an ancestor job
 * validated, or a step earlier in this same job did.
 * @param workflow - The workflow declaring the job
 * @param job - The publishing job
 * @param publishStep - The step that ships
 * @param allWorkflows - Every parsed workflow in the repository
 * @returns True when something was proved before the artifact left
 */
function validationPrecedesPublish(workflow, job, publishStep, allWorkflows) {
    if (reusableCallerValidation(workflow, allWorkflows, isValidatingJob) ===
        "validated") {
        return true;
    }
    if (ancestors(workflow, job).some(isValidatingJob)) {
        return true;
    }
    return job.steps
        .slice(0, job.steps.indexOf(publishStep))
        .some(step => isValidatingCommand(step.run));
}
/**
 * Whether the job ships an artifact it produced itself rather than promoting the
 * one CI built and validated. Downloading the CI artifact only exempts the job
 * when nothing rebuilds *after* the download — a rebuild past the promotion is
 * the very bypass B2 exists to catch.
 * @param job - The publishing job
 * @param publishStep - The step that ships
 * @returns True when a self-built artifact is shipped
 */
function shipsSelfBuiltArtifact(job, publishStep) {
    if (actionId(publishStep.uses) === DOCKER_BUILD_PUSH_ACTION) {
        return true;
    }
    const publishIndex = job.steps.indexOf(publishStep);
    const stepsThroughPublish = job.steps.slice(0, publishIndex + 1);
    const downloadIndex = job.steps
        .slice(0, publishIndex)
        .reduce((latest, step, index) => (isPromotionAction(step.uses) ? index : latest), -1);
    if (downloadIndex >= 0) {
        const rebuildsAfterDownload = stepsThroughPublish
            .slice(downloadIndex + 1)
            .some(step => isLocalBuildCommand(step.run));
        if (!rebuildsAfterDownload) {
            return false;
        }
    }
    return (LOCAL_PATH_ARGUMENT.test(publishStep.run) ||
        LOCAL_ARTIFACT_FILE.test(publishStep.run) ||
        stepsThroughPublish.some(step => isLocalBuildCommand(step.run)));
}
/**
 * Decide whether a workflow's trigger makes in-file validation the expected
 * place for it — the only case where absent validation proves a bypass.
 * @param workflow - The workflow to classify
 * @param defaultBranches - Project-local default-like branch names
 * @param allWorkflows - Every parsed workflow in the repository
 * @returns The reason validation may live elsewhere, or null when it must be here
 */
function offlineUnresolvableTrigger(workflow, defaultBranches, allWorkflows) {
    const events = workflow.on.events;
    if (events.length > 0 && events.every(event => event === "workflow_call")) {
        if (reusableCallerValidation(workflow, allWorkflows, isValidatingJob) ===
            "unvalidated") {
            return null;
        }
        return ("it is a reusable `workflow_call` workflow, so validation is the calling " +
            "workflow's obligation and callers are not resolved offline");
    }
    if (events.includes("workflow_run")) {
        return ("it is triggered by `workflow_run`, so the validating workflow that fired " +
            "it runs upstream and is not resolved offline");
    }
    if (events.includes("release")) {
        return ("it is triggered by a `release` event, so validation may have run on the " +
            "commit or tag the release was cut from, which is not visible in this file");
    }
    // An unfiltered `on: [push]` is a SUPERSET of a default-branch push, so it
    // cannot be treated more strictly than the spelling that names the branch.
    const unfilteredPush = workflow.on.events.includes("push") &&
        workflow.on.pushBranches.length === 0 &&
        workflow.on.pushTags.length === 0;
    const defaultBranchPush = workflow.on.pushBranches.some(branch => {
        const trimmed = branch.trim();
        return (DEFAULT_BRANCHES.has(trimmed) ||
            (defaultBranches ?? []).some(candidate => candidate.trim() === trimmed));
    });
    if (unfilteredPush || defaultBranchPush) {
        return ("it is triggered by a push to the default branch, where branch protection " +
            "may already require the validating checks — a rule that is not visible " +
            "in this file");
    }
    return null;
}
/**
 * Assess every publishing step in one job against B2.
 * @param workflow - The workflow declaring the job
 * @param job - The job to assess
 * @param defaultBranches - Project-local default-like branch names
 * @param allWorkflows - Every parsed workflow in the repository
 * @returns One outcome per publishing step, or an empty list when it ships nothing
 */
export function assessReleasePaths(workflow, job, defaultBranches, allWorkflows) {
    return findPublishSteps(job).map(publishStep => {
        const where = `${workflow.file} job \`${job.id}\` step \`${describeStep(publishStep)}\``;
        const validatingAncestors = ancestors(workflow, job).filter(isValidatingJob);
        const validated = validationPrecedesPublish(workflow, job, publishStep, allWorkflows);
        if (shipsSelfBuiltArtifact(job, publishStep)) {
            return validated
                ? rebuildPastValidation(where)
                : unvalidatedSelfBuild(where, workflow, defaultBranches, allWorkflows);
        }
        if (validated &&
            hasArtifactNameMismatch(job, publishStep, validatingAncestors)) {
            return artifactNameMismatch(where);
        }
        if (validated &&
            hasUnprovenArtifactIntegrity(job, publishStep, validatingAncestors)) {
            return unresolvedArtifactIntegrity(where);
        }
        if (validated) {
            return { kind: "clean" };
        }
        if (promotesValidatedArtifact(job, publishStep, validatingAncestors))
            return unresolvedPromotedArtifact(where);
        // Neither built here nor promoted from CI, and nothing validating precedes it:
        // the link between what ships and what was validated is simply not observable
        // in this file. That is unestablished, not clean.
        return {
            kind: "unresolved",
            reason: `${where} neither builds its own artifact nor promotes a CI-built one via ` +
                `\`${PROMOTION_ACTION}\`, and no validating job precedes it, so what it ` +
                "ships cannot be tied to anything that was validated",
        };
    });
}
/**
 * The provable bypass that survives every trigger: the pipeline validated the
 * source, then the release job rebuilt and shipped its own artifact. Whatever
 * ran upstream, the bytes that reached users are not the bytes that were
 * checked — so no trigger exempts this.
 * @param where - Evidence location label
 * @returns The violation outcome
 */
function rebuildPastValidation(where) {
    return {
        kind: "violation",
        evidence: `${where} rebuilds and ships its own artifact after validation ran — the ` +
            "pipeline proved things about the source tree, then shipped bytes that " +
            `were produced afterwards and never validated. Promote the CI-built ` +
            `artifact via \`${PROMOTION_ACTION}\` instead of repacking at release time`,
    };
}
/**
 * A self-built, self-shipped artifact with nothing validating in front of it:
 * a violation when the trigger makes in-file validation the expected place, and
 * a stated-reason SKIP when the proof could live somewhere not visible offline.
 * @param where - Evidence location label
 * @param workflow - The workflow declaring the job
 * @param defaultBranches - Project-local default-like branch names
 * @param allWorkflows - Every parsed workflow in the repository
 * @returns The violation or unresolved outcome
 */
function unvalidatedSelfBuild(where, workflow, defaultBranches, allWorkflows) {
    const unresolvable = offlineUnresolvableTrigger(workflow, defaultBranches, allWorkflows);
    if (unresolvable !== null) {
        return {
            kind: "unresolved",
            reason: `${where} builds and ships its own artifact with no validating job in ` +
                `its \`needs:\` closure, but ${unresolvable}. Whether what ships equals ` +
                "what was validated is therefore not established either way",
        };
    }
    return {
        kind: "violation",
        evidence: `${where} builds its own artifact and ships it with no validating job in ` +
            `its \`needs:\` closure and no \`${PROMOTION_ACTION}\` promotion of the ` +
            "CI-built one — nothing was proved about the bytes that reached users",
    };
}
//# sourceMappingURL=doctor-readiness-release-path.js.map