/**
 * Pipeline action — resolves CLI options and orchestrates pipeline steps.
 *
 * This file is the thin CLI-to-orchestrator bridge. The bulk of execution
 * logic lives in packages/eval/src/orchestration/.
 *
 * Responsibilities:
 * - Resolve CLI flags into typed ResolvedOptions
 * - Delegate to the PipelineOrchestrator for step execution
 *
 * @see packages/eval/src/orchestration/ for the step-based pipeline
 */
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync, } from "fs";
import { dirname, resolve } from "path";
import { fileURLToPath } from "url";
import { classifyUrls } from "../pipeline/classify-url.js";
import { LiteracyVariant, normalizeMode } from "../pipeline/normalize-mode.js";
import { assessImpact, buildReverseMapping, } from "../pipeline/reverse-mapping.js";
import { buildAppContext, parseArtifactUploadEnv, } from "../orchestration/build-app-context.js";
import { buildStepSequence } from "../orchestration/build-step-sequence.js";
import { orchestratePipeline } from "../orchestration/pipeline-orchestrator.js";
import { resolveReportUrl } from "../sinks/slack/dashboard-links.js";
import { load } from "js-yaml";
import { ZodError } from "zod";
import { PLACEHOLDER_OWNER_TEAM, } from "../_vendor/ailf-core/index.js";
import { parseRepoConfig, } from "../adapters/task-sources/repo-schemas.js";
import { loadTsConfig } from "../adapters/config-sources/ts-config-loader.js";
import { loadUserConfig, } from "../adapters/config-sources/user-config.js";
import { injectDocIdsToRefs, mergeInjectContextDocs, } from "./shared/inject-doc-flag.js";
import { getCallerCwd, resolveOutputDir } from "./shared/resolve-output-dir.js";
// Phase 6 / DIAG-06 — single formatter, single visual contract (D6-04).
// Import statically so bundlers and type-checkers can verify the export
// exists at build time rather than deferring to runtime dynamic import.
import { formatCardSummaryLine } from "./interpret.js";
import { defaultVersionsFromReport } from "./shared/versions-from-report.js";
const __dirname = dirname(fileURLToPath(import.meta.url));
const ROOT = resolve(__dirname, "..", "..");
// ---------------------------------------------------------------------------
// Valid search modes
// ---------------------------------------------------------------------------
const VALID_SEARCH_MODES = ["open", "origin-only", "off"];
/**
 * Pure option resolution — computes ResolvedOptions from CLI flags without
 * any side effects. Safe to call from --explain without mutating process.env.
 *
 * Exported so the plan builder can call it independently.
 */
export function computeResolvedOptions(opts, repoConfig = null, userConfig = null) {
    // Resolve paths relative to the caller's cwd, not the eval package root
    const callerCwd = getCallerCwd();
    // The repo config (`.ailf/ailf.config.ts` or `.ailf/config.yaml`, etc.) is
    // loaded asynchronously by `loadRepoConfig` and injected here so this
    // function stays pure + synchronous (safe for `--explain`). Downstream
    // cascades (source, agentic, owner, output, etc.) read from it; env vars
    // still win at each cascade below.
    // Validate + normalize mode via the single boundary function.
    // normalizeMode() maps legacy variant names (baseline, agentic, etc.)
    // to canonical mode "literacy" + variant, and throws on invalid input.
    let mode;
    let variant;
    try {
        const normalized = normalizeMode(opts.mode);
        mode = normalized.mode;
        // Explicit --variant flag takes precedence over what normalizeMode inferred
        variant = opts.variant ?? normalized.variant;
        // Canonical mode "literacy" with no variant defaults to the full variant
        // (standard + agentic). This preserves the pre-canonical CLI behavior
        // where `--mode full` was the default, without emitting the legacy alias
        // deprecation warning for users who pass no flags at all.
        if (mode === "literacy" && !variant) {
            variant = LiteracyVariant.FULL;
        }
    }
    catch (err) {
        console.error(`❌ ${err instanceof Error ? err.message : String(err)}`);
        process.exit(1);
    }
    // Debug + filter options — any filter flag (--filter-first-n,
    // --filter-pattern, --filter-sample) implies --debug, so users don't
    // need to pass both. When DEBUG_EVAL is explicitly "0", ignore the env
    // vars. CLI flags always win over env vars.
    const debugEnvDisabled = process.env.DEBUG_EVAL === "0";
    const filterFirstN = opts.filterFirstN ??
        (process.env.DEBUG_EVAL_N && !debugEnvDisabled
            ? parseInt(process.env.DEBUG_EVAL_N, 10)
            : undefined);
    const filterPattern = opts.filterPattern ??
        (process.env.DEBUG_EVAL_PATTERN && !debugEnvDisabled
            ? process.env.DEBUG_EVAL_PATTERN
            : undefined);
    const filterSample = opts.filterSample ??
        (process.env.DEBUG_EVAL_SAMPLE && !debugEnvDisabled
            ? parseInt(process.env.DEBUG_EVAL_SAMPLE, 10)
            : undefined);
    const debugEnabled = opts.debug ||
        process.env.DEBUG_EVAL === "1" ||
        filterFirstN !== undefined ||
        filterPattern !== undefined ||
        filterSample !== undefined;
    const debug = debugEnabled
        ? {
            enabled: true,
            firstN: filterFirstN,
            pattern: filterPattern,
            sample: filterSample,
        }
        : undefined;
    // Search mode validation
    const searchMode = opts.search ?? process.env.EVAL_SEARCH_MODE ?? "open";
    if (!VALID_SEARCH_MODES.includes(searchMode)) {
        console.error(`❌ Invalid --search mode "${searchMode}". Must be one of: ${VALID_SEARCH_MODES.join(", ")}`);
        process.exit(1);
    }
    // Merge repeatable args (singular + plural aliases). `headerArgs` and
    // `allowedOriginArgs` are populated from `.ailf/config.yaml`'s `agentic`
    // block (W0077 Phase 6f); the CLI flags `--header` and `--allowed-origin`
    // were retired. The URL-classification block below may still append a
    // host to `allowedOriginArgs` when neither config nor CLI provided one.
    // The `DOC_HEADERS` and `DOC_ALLOWED_ORIGIN(S)` env vars still merge in
    // sources.ts at the doc-fetch boundary (additive, unchanged).
    const urlArgs = opts.url;
    const headerArgs = [];
    const allowedOriginArgs = [];
    const sanityDocumentArgs = opts.sanityDocument;
    if (repoConfig?.agentic?.headers) {
        for (const [key, value] of Object.entries(repoConfig.agentic.headers)) {
            headerArgs.push(`${key}: ${value}`);
        }
    }
    if (repoConfig?.agentic?.allowedOrigins) {
        allowedOriginArgs.push(...repoConfig.agentic.allowedOrigins);
    }
    // Source overrides — perspective stays per-run (CLI flag), the dataset /
    // project / studio-origin trio moved into `.ailf/config.yaml`'s `source`
    // block in W0077 Phase 6d. Env vars still win over the config-file value.
    const perspectiveOverride = opts.sanityPerspective;
    // URL classification (pure computation — results captured, not applied to env)
    if (urlArgs.length > 0) {
        const classification = classifyUrls(urlArgs);
        if (classification.documentIds.length > 0) {
            const existing = sanityDocumentArgs.length > 0 ? sanityDocumentArgs : [];
            const merged = [...new Set([...existing, ...classification.documentIds])];
            sanityDocumentArgs.length = 0;
            sanityDocumentArgs.push(...merged);
        }
    }
    // Auto-infer allowed origin from --url
    if (urlArgs.length > 0 && allowedOriginArgs.length === 0) {
        try {
            const hostname = new URL(urlArgs[0]).hostname.replace(/^www\./, "");
            allowedOriginArgs.push(hostname);
        }
        catch {
            // Invalid URL — will be caught later in validation
        }
    }
    // Scoping
    const areaOption = opts.area ?? process.env.EVAL_FILTER_AREAS ?? undefined;
    const taskOption = opts.task ?? process.env.EVAL_FILTER_TASKS ?? undefined;
    const tagOption = opts.tag?.length
        ? opts.tag
        : process.env.EVAL_FILTER_TAGS
            ? process.env.EVAL_FILTER_TAGS.split(",")
                .map((s) => s.trim())
                .filter(Boolean)
            : undefined;
    const changedDocsOption = opts.changedDocs ?? process.env.EVAL_CHANGED_DOCS ?? undefined;
    // Document-driven scoping (pure — computes impactSummary without env writes)
    let impactSummary;
    if (changedDocsOption) {
        const changedSlugs = changedDocsOption
            .split(",")
            .map((s) => s.trim())
            .filter(Boolean);
        if (changedSlugs.length > 0) {
            const reverseMapping = buildReverseMapping(ROOT);
            impactSummary = assessImpact(changedSlugs, reverseMapping);
            if (impactSummary.areas.length === 0) {
                console.warn(`\n⚠️  No evaluation tasks reference any of the changed documents:`);
                for (const slug of changedSlugs) {
                    console.warn(`    - ${slug}`);
                }
                console.warn(`\n    Score impact cannot be measured for these documents.\n`);
            }
        }
    }
    // Comparison: --before-source auto-enables --compare. The `--compare` flag
    // is a Commander optional-argument: undefined when not passed, `true` for
    // the bare flag (compare against latest), and a string path when the user
    // pinned a specific baseline (`--compare path/to/baseline.json`).
    const beforeOption = opts.beforeSource;
    const compareEnabled = (opts.compare !== undefined && opts.compare !== false) ||
        beforeOption !== undefined;
    const compareBaseline = typeof opts.compare === "string" ? opts.compare : undefined;
    // Whether this is a remote (API-dispatched) run. Needed for the D0064
    // auto-promotion below (local-only) and to preserve an explicit
    // `content-lake` task source on `--remote`, which forwards it to the API
    // instead of inlining `.ailf/tasks/` (SDK-1815).
    const remote = opts.remote || process.env.AILF_REMOTE === "1";
    // Task-source resolution (W0077 Phase 6h) — `--task-source` and
    // `--repo-tasks-path` retired. Both move under `taskSource: {...}` in
    // `.ailf/config.yaml`. Cascade: config → built-in default.
    // When type is `repo` and no path is set, fall back to `<cwd>/.ailf/tasks/`
    // (the location `ailf init` scaffolds).
    //
    // D0064 — when the user has NOT set `taskSource.type` and a populated
    // `<cwd>/.ailf/tasks/` exists on a local run, evaluate those repo tasks
    // (repo-only), matching what `ailf run --remote` already does, instead of
    // the bundled `tasks/${mode}/` corpus. This keys off the RAW config value,
    // not `resolveTaskSourceType` — that function collapses both an unset value
    // and the literal `"content-lake"` to `undefined`, but only a truly-unset
    // value should auto-promote. Explicit `"content-lake"` is the documented
    // opt-out that keeps the bundled default.
    const configuredTaskSourceType = repoConfig?.taskSource?.type;
    const autoPromoteRepoTasks = shouldAutoPromoteRepoTasks({
        configuredType: configuredTaskSourceType,
        callerCwd,
        remote,
    });
    const resolvedTaskSourceType = autoPromoteRepoTasks
        ? "repo"
        : resolveTaskSourceType(configuredTaskSourceType, remote);
    const resolvedRepoTasksPath = resolveRepoTasksPath(callerCwd, repoConfig?.taskSource?.repoTasksPath, resolvedTaskSourceType);
    if (autoPromoteRepoTasks) {
        process.stderr.write(repoTasksAutoPromotionNotice(resolvedRepoTasksPath));
    }
    // Source union (D0060 mitigation #2) — `source` in the repo config is
    // either the NAME of a registered source (string) or the inline-location
    // object. A name resolves through `loadSource(name)` with no overrides,
    // so provenance records the registered `source.name` (e.g. "production")
    // instead of "env-override" and the run stays inside the D0059 trust
    // gate. The inline object is the escape hatch for custom doc sites and
    // keeps routing into the `*Override` fields below.
    const namedSource = typeof repoConfig?.source === "string"
        ? // Env still wins over the config-file value, matching the
            // override cascade below. DOC_SOURCE is only consulted when the
            // config names a source — otherwise `loadSource` keeps reading it
            // directly, preserving the pre-union behavior (including remote
            // runs not forwarding DOC_SOURCE).
            // oxlint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- empty string env var should fall back
            process.env.DOC_SOURCE || repoConfig.source
        : undefined;
    const inlineSource = typeof repoConfig?.source === "object" ? repoConfig.source : undefined;
    // Source overrides (W0077 Phase 6d) — `--sanity-dataset`, `--sanity-project`,
    // and `--sanity-studio-origin` were retired from `ailf run`. Cascade is now:
    //   env var > .ailf/config.yaml `source.*` > built-in default (in sources.ts).
    const datasetOverride = process.env.SANITY_DATASET ?? inlineSource?.dataset;
    const projectIdOverride = process.env.SANITY_PROJECT_ID ?? inlineSource?.projectId;
    const studioOriginOverride = process.env.SANITY_STUDIO_ORIGIN ?? inlineSource?.studioOrigin;
    // `source.baseUrl` was parsed by the repo-config schema but never mapped
    // into the source overrides — it took effect only via the `DOC_BASE_URL`
    // env var or a named `config/sources.ts` entry (D0022). Map it here with
    // the same env-wins cascade as the trio above; `configToSourceOverrides`
    // keeps the explicit `--url` flag (`urls[0]`) ahead of it.
    const baseUrlOverride = process.env.DOC_BASE_URL ?? inlineSource?.baseUrl;
    // `--inject-doc <id>` (D0090) — bare ids injected into every in-scope task's
    // context. The flag never encodes the source in argv: the run's active
    // `--source` interprets each id at fetch time — a content-lake source reads
    // it as a Sanity `_id`, a Google Drive source as a `fileId`. Each produced
    // ref is a bare `IdDocRef` validated through the shared
    // `GeneralizedDocRefSchema` the `--config` file and the API body also use, so
    // the three surfaces cannot drift (D0045). Complex / multi-source /
    // full-locator refs go through the JSON `--config` payload instead.
    const injectDocArgs = opts.injectDoc ?? [];
    const injectContextDocsOption = injectDocArgs.length > 0 ? injectDocIdsToRefs(injectDocArgs) : undefined;
    // `--sanity-document` soft-deprecation notice (D0090). It keeps its
    // replace/append fetch-override semantics and its `--remote` forwarding, but
    // emits a one-time stderr notice steering users at `--inject-doc` / the
    // config. The notice is precise about the inject-vs-replace difference:
    // `--inject-doc` ADDS a document's context to every in-scope task, whereas
    // `--sanity-document` REPLACES the canonical doc (or appends) in the fetched
    // set. The notice does not block the run.
    if (sanityDocumentArgs.length > 0) {
        warnSanityDocumentDeprecated();
    }
    // Report store overrides (W0077 Phase 6e — `--report-dataset` and
    // `--report-project` retired). Resolution order:
    //   1. Environment variables (AILF_REPORT_DATASET, AILF_REPORT_PROJECT_ID)
    //   2. .ailf/config.yaml reportStore block
    //   3. Eval dataset override (so perspective evals publish to the same dataset)
    const reportDataset = process.env.AILF_REPORT_DATASET ??
        repoConfig?.reportStore?.dataset ??
        datasetOverride ??
        undefined;
    const reportProjectId = process.env.AILF_REPORT_PROJECT_ID ??
        repoConfig?.reportStore?.projectId ??
        undefined;
    // Publish polarity (W0077 Phase 4) — auto policy lives in
    // .ailf/config.yaml's `publish.auto` (or env / default). CLI flags and
    // AILF_PUBLISH still override the policy.
    const reportStoreToken = process.env.AILF_REPORT_SANITY_API_TOKEN ?? process.env.SANITY_API_TOKEN;
    const reportStoreConfigured = Boolean(reportStoreToken);
    const publishExplicit = opts.publish !== undefined || process.env.AILF_PUBLISH !== undefined;
    const publishAuto = resolvePublishAuto(repoConfig?.publish?.auto);
    let publishEnabled;
    if (opts.publish !== undefined) {
        publishEnabled = opts.publish;
    }
    else if (process.env.AILF_PUBLISH === "1") {
        publishEnabled = true;
    }
    else if (process.env.AILF_PUBLISH === "0") {
        publishEnabled = false;
    }
    else {
        // Apply the auto policy. The report store still has to be configured
        // for `auto: "always"` and `"full-runs"` — without a token, publishing
        // is impossible regardless of policy.
        switch (publishAuto) {
            case "always":
                publishEnabled = reportStoreConfigured;
                break;
            case "never":
                publishEnabled = false;
                break;
            case "full-runs":
            default:
                publishEnabled = reportStoreConfigured && !debugEnabled;
                break;
        }
    }
    // Tag default cascade: --publish-tag > AILF_PUBLISH_TAG > .ailf/config.yaml
    const publishTag = opts.publishTag ?? process.env.AILF_PUBLISH_TAG ?? repoConfig?.publish?.tag;
    // Execution-tier resolution (W0077 Phase 6b) — concurrency, grader
    // replications, gap analysis, and api URL all moved from CLI flags to
    // `.ailf/config.yaml`'s `execution` block. Cascade for each:
    //   env var (where one exists) > .ailf/config.yaml > built-in default
    const concurrency = repoConfig?.execution?.concurrency;
    const graderReplications = repoConfig?.execution?.graderReplications;
    const borderlineReplications = repoConfig?.execution?.borderlineReplications;
    const gapAnalysisEnabled = repoConfig?.execution?.gapAnalysis ?? true;
    // Phase 6 / DIAG-06 — post-run diagnosis summary policy from .ailf/config.yaml.
    // Precedence resolution (CLI flag > env > config > auto) lives in
    // shouldRunPostSummary(); this only carries the config-file signal.
    const summaryOnRun = repoConfig?.summary?.onRun;
    // Grader context policy. Cascade: env var > .ailf/config.yaml > unset
    // (defaults to rubric-only at the EvalConfig boundary). The env var is the
    // operational lever for one-shot comparison runs without editing the config file.
    const rawGraderContext = process.env.AILF_GRADER_CONTEXT ?? repoConfig?.grader?.context;
    const graderContext = rawGraderContext === "with-docs" || rawGraderContext === "rubric-only"
        ? rawGraderContext
        : undefined;
    if (rawGraderContext && graderContext === undefined) {
        console.error(`❌ Invalid grader.context "${rawGraderContext}". Must be "rubric-only" or "with-docs".`);
        process.exit(1);
    }
    // Remote mode flag (`remote`) is resolved earlier — see the D0064
    // auto-promotion block above.
    const apiUrl = process.env.AILF_API_URL ??
        repoConfig?.execution?.apiUrl ??
        userConfig?.execution?.apiUrl ??
        "https://ailf-api.sanity.build";
    // W0202 — `--api-key` flag wins over the env var. The CLI's dotenv
    // bootstrap (`cli.ts`) loads the project `.env` with `override: true`,
    // matching the repo convention that the file is the source of truth
    // (see `.claude/rules/config.md`). That convention silently squashes
    // shell-set `AILF_API_KEY`, so an explicit per-run flag is the only
    // ergonomic way to pick a non-default key for one invocation.
    const apiKey = opts.apiKey ?? process.env.AILF_API_KEY ?? undefined;
    // Output directory (W0077 Phase 6c) — `output.dir` from .ailf/config.yaml
    // when set, otherwise <cwd>/.ailf/results/latest/.
    const outputDir = resolveOutputDir(repoConfig?.output?.dir);
    return {
        allowedOriginArgs,
        apiKey,
        apiUrl,
        areaOption,
        beforeOption,
        changedDocsOption,
        compareBaseline,
        compareEnabled,
        compareThreshold: opts.threshold,
        concurrency,
        baseUrlOverride,
        datasetOverride,
        debug,
        dryRun: opts.dryRun,
        gapAnalysisEnabled,
        graderReplications,
        borderlineReplications,
        graderContext,
        headerArgs,
        impactSummary,
        mode,
        variant,
        noAutoScope: opts.autoScope === false,
        noCache: !opts.cache,
        noRemoteCache: opts.remoteCache === false,
        outputDir,
        outputPath: opts.output,
        perspectiveOverride,
        projectIdOverride,
        promptfooUrl: opts.promptfooUrl,
        publishEnabled,
        reportStoreConfigured,
        publishExplicit,
        publishTag,
        remote,
        reportDataset,
        reportProjectId,
        sanityDocumentArgs,
        injectContextDocsOption,
        searchMode,
        skipEval: opts.eval === false,
        skipFetch: opts.fetch === false,
        // `--source` flag > DOC_SOURCE env (folded into namedSource) > repo
        // config named source. Flows to `ResolvedConfig.source` for local
        // `loadSource(name)` resolution and to `PipelineRequest.source` for
        // `--remote` (build-request.ts).
        source: opts.source ?? namedSource,
        studioOriginOverride,
        repoTasksPath: resolvedRepoTasksPath,
        taskOption,
        tagOption,
        taskSourceType: resolvedTaskSourceType,
        urlArgs,
        // Artifact-writer settings (W0077 Phase 6g) — `--no-artifacts`,
        // `--artifacts-dir`, and `--artifacts-exclude` retired. Cascade:
        //   AILF_ARTIFACTS_DIR > .ailf/config.yaml `artifacts.dir` > default
        //   .ailf/config.yaml `artifacts.enabled: false` > writers attached
        //   .ailf/config.yaml `artifacts.exclude` > no exclusions
        // `--no-artifacts-write` (artifactsDryRun) stays per-run.
        artifactsDisabled: repoConfig?.artifacts?.enabled === false,
        artifactsDir: process.env.AILF_ARTIFACTS_DIR ?? repoConfig?.artifacts?.dir,
        artifactsDryRun: opts.artifactsWrite === false,
        artifactsExclude: repoConfig?.artifacts?.exclude,
        classificationOption: opts.classification?.trim() || undefined,
        // Owner attribution (W0077 Phase 6f) — `--owner-team` and
        // `--owner-individual` retired. Cascade: AILF_OWNER_TEAM /
        // AILF_OWNER_INDIVIDUAL env vars > .ailf/config.yaml `owner.*` > undefined.
        // Downstream resolution in build-request.ts already honors the env var as a
        // fallback when this option is unset, but threading it through here keeps
        // the cascade order explicit.
        ownerTeamOption: process.env.AILF_OWNER_TEAM?.trim() ||
            repoConfig?.owner?.team ||
            userConfig?.owner?.team ||
            undefined,
        ownerIndividualOption: process.env.AILF_OWNER_INDIVIDUAL?.trim() ||
            repoConfig?.owner?.individual ||
            userConfig?.owner?.individual ||
            undefined,
        purposeOption: opts.purpose?.trim() || undefined,
        labelOptions: opts.label ?? [],
        // Project provenance policy (strengthen-only) from .ailf/config.yaml. The
        // T2 gate merges it onto SERVER_FLOOR; absent = floor only.
        provenanceOption: repoConfig?.provenance,
        summaryOnRun,
    };
}
const PUBLISH_AUTO_VALUES = ["always", "full-runs", "never"];
/**
 * Resolve the publish auto policy. Precedence:
 *   1. .ailf/config.yaml `publish.auto`
 *   2. AILF_PUBLISH_AUTO env var
 *   3. Default: "full-runs" (preserves the historical smart default)
 *
 * Unrecognized env-var values fall through to the default with a warning;
 * the schema validates the config-file value at parse time.
 */
function resolvePublishAuto(repoValue) {
    if (repoValue)
        return repoValue;
    const envValue = process.env.AILF_PUBLISH_AUTO?.trim();
    if (envValue &&
        PUBLISH_AUTO_VALUES.includes(envValue)) {
        return envValue;
    }
    if (envValue) {
        console.warn(`⚠️  AILF_PUBLISH_AUTO="${envValue}" is not recognized; valid values are ${PUBLISH_AUTO_VALUES.join(", ")}. Falling back to "full-runs".`);
    }
    return "full-runs";
}
// ---------------------------------------------------------------------------
// Phase 6 / DIAG-06 — post-run diagnosis summary helpers
// ---------------------------------------------------------------------------
/**
 * Determine whether the post-run diagnosis summary hook should fire.
 *
 * 4-level precedence chain (D0054):
 *   Level 1 — CLI flag (absolute): if `cliOpts.summary` is boolean, use it.
 *   Level 2 — AILF_INTERPRET_ON_RUN env var (absolute): strict "1"/"0" parse;
 *             anything else falls through (T-06-11 spoofing mitigation).
 *   Level 3 — config `summary.onRun` (absolute): "always" → true; "never" → false;
 *             "auto" or absent falls through to level 4.
 *   Level 4 — default always-on: diagnosis is a first-class output produced
 *             for every pipeline run unless explicitly opted out at levels 1–3.
 */
export function shouldRunPostSummary(cliOpts, resolvedOnRun) {
    // Level 1: CLI flag wins absolutely
    if (cliOpts.summary === true)
        return true;
    if (cliOpts.summary === false)
        return false;
    // Level 2: AILF_INTERPRET_ON_RUN env var (strict parse)
    const envVal = process.env.AILF_INTERPRET_ON_RUN;
    if (envVal === "1")
        return true;
    if (envVal === "0")
        return false;
    // Anything else (garbage, unset) falls through
    // Level 3: config summary.onRun
    if (resolvedOnRun === "always")
        return true;
    if (resolvedOnRun === "never")
        return false;
    // "auto" or undefined falls through
    // Level 4: diagnosis is on by default — emit a diagnosis artifact for every
    // pipeline run unless an upstream level explicitly opted out (D0054).
    return true;
}
/**
 * Build a SynthesisCostTelemetry payload from a completed Diagnosis.
 *
 * Aggregates:
 *   - cost: sum of meta.cost across all cards (undefined treated as 0)
 *   - parseFailureCount: cards where status==="degraded" (parse failures)
 *   - parseFailureRate: parseFailureCount / total-cards (max 8 per D6-09)
 *   - perCard: per-card row with safe-extracted structured metadata
 *
 * Deliberately does NOT read card.body — only structured meta fields are
 * persisted (T-06-14 PII guard per threat model).
 */
// D6-09: denominator is always the fixed card-registry size, not cards.length.
// Using cards.length would allow parseFailureRate > 1.0 when the registry is
// a subset (e.g. test registries), violating the SynthesisCostTelemetrySchema
// min(0).max(1) constraint. Single edit point if the registry ever grows.
const CARD_REGISTRY_SIZE = 8;
export function buildSynthesisTelemetry(diagnosis) {
    const cards = diagnosis.cards;
    let totalCost = 0;
    let parseFailureCount = 0;
    const perCard = cards.map((card) => {
        // "missing" cards have no `meta` — narrow with status guard
        const meta = card.status !== "missing" ? card.meta : undefined;
        const cost = meta?.cost ?? 0;
        totalCost += cost;
        // Parse failures produce status="degraded" (not "missing") in the current
        // runner (diagnosis-runner.ts). A "missing" card is absence, not failure.
        // If a future code path can produce status="missing" from a parse failure,
        // this line must be updated and the parseFailed contract re-evaluated.
        const parseFailed = card.status === "degraded";
        if (parseFailed)
            parseFailureCount++;
        const row = {
            cardType: card.cardType,
            parseFailed,
            cardVersion: meta?.cardVersion ?? "unknown",
            generatedAt: meta?.generatedAt ?? new Date().toISOString(),
        };
        if (cost > 0)
            row.cost = cost;
        if (meta?.latencyMs !== undefined)
            row.latencyMs = meta.latencyMs;
        if (meta?.tokenUsage?.input !== undefined)
            row.tokenInput = meta.tokenUsage.input;
        if (meta?.tokenUsage?.output !== undefined)
            row.tokenOutput = meta.tokenUsage.output;
        return row;
    });
    return {
        cost: totalCost,
        parseFailureCount,
        parseFailureRate: parseFailureCount / CARD_REGISTRY_SIZE,
        perCard,
    };
}
/**
 * Run post-pipeline hooks after the pipeline completes.
 *
 * Fires after orchestratePipeline() + writePipelineResult() (D6-02).
 * Hook failure prints to stderr but does NOT change exit code (D6-03).
 * Fires whenever shouldRunPostSummary returns true (D0054 — default on).
 *
 * @param ctx - App context (composition root wiring)
 * @param result - Pipeline result (includes reportId when published)
 * @param args - Hook options (cliOpts, summaryOnRun from config, optional runnerFactory for tests)
 */
export async function runPostPipelineHooks(ctx, result, args) {
    if (!shouldRunPostSummary(args.cliOpts, args.summaryOnRun))
        return;
    if (!result.reportId) {
        process.stderr.write("ℹ️ No report published — skipping post-summary.\n");
        return;
    }
    const reportId = result.reportId;
    try {
        // Build the runner — use injected factory (tests) or composition root (production)
        let runner;
        if (args.runnerFactory) {
            runner = args.runnerFactory(ctx);
        }
        else {
            const { getDiagnosisRunner } = await import("../composition-root.js");
            // eslint-disable-next-line @typescript-eslint/no-explicit-any
            runner = getDiagnosisRunner(ctx);
        }
        // Read the stored report — needed by the runner for version metadata
        const report = await ctx.reportStore?.read(reportId);
        if (!report) {
            process.stderr.write(`ℹ️ Report not found: ${reportId} — skipping post-summary.\n`);
            return;
        }
        // Derive version metadata from the stored report. Shares the single
        // `defaultVersionsFromReport` helper with `ailf interpret` so the
        // path-relevant axes fall back to the canonical engine versions instead
        // of the literal "unknown" that produced `diagnosis-unknown-…` paths
        // (W0286).
        const versionedInputs = defaultVersionsFromReport(report);
        // Run the diagnosis
        // eslint-disable-next-line @typescript-eslint/no-explicit-any
        const diagnosis = await runner.run({
            report: report,
            versions: versionedInputs,
            refresh: false,
        });
        // Print per-card summary lines to stdout (D6-04 single formatter)
        for (const card of diagnosis.cards) {
            process.stdout.write(`${formatCardSummaryLine(card)}\n`);
        }
        // Build and write synthesis telemetry back to the report doc (D6-08)
        // patchSynthesis is now part of ReportStorePort (CR-01) — guard on store
        // presence only; absent store means no report store is configured (expected).
        if (ctx.reportStore) {
            const telemetry = buildSynthesisTelemetry(diagnosis);
            await ctx.reportStore.patchSynthesis(reportId, telemetry);
        }
        else {
            process.stderr.write("ℹ️ No reportStore configured — synthesis telemetry not written to Sanity.\n");
        }
    }
    catch (err) {
        const msg = err instanceof Error ? err.message : String(err);
        process.stderr.write(`⚠️ Diagnosis failed: ${msg}. Run \`ailf interpret ${reportId}\` to retry.\n`);
    }
}
/** Resolve and validate the --task-source flag value. */
function resolveTaskSourceType(raw, remote) {
    if (!raw)
        return undefined; // unset — bundled default (local) / server default
    if (raw === "repo")
        return "repo";
    if (raw === "content-lake") {
        // If the user specifies a content-lake source, respect that.
        // If this isn't a remote run, default to the bundled corpus.
        return remote ? "content-lake" : undefined;
    }
    console.error(`❌ Invalid --task-source "${raw}". Must be "repo" or "content-lake".`);
    process.exit(1);
}
/**
 * Resolve the repo tasks path.
 *
 * - Explicit `--repo-tasks-path` wins (resolved relative to callerCwd).
 * - When `--task-source=repo` is set without a path, defaults to
 *   `./.ailf/tasks/` in callerCwd — the location created by `ailf init`.
 * - Otherwise returns undefined (Content Lake source).
 *
 * Exits with a helpful error when an explicit path doesn't exist, or when
 * the repo source was requested but no tasks directory can be found.
 */
function resolveRepoTasksPath(callerCwd, explicitPath, taskSourceType) {
    if (explicitPath) {
        const abs = resolve(callerCwd, explicitPath);
        if (!existsSync(abs)) {
            console.error(`❌ Repo tasks directory not found: ${abs}\n` +
                "   Provide a valid --repo-tasks-path, or run 'ailf init' to scaffold .ailf/tasks/.");
            process.exit(1);
        }
        return abs;
    }
    if (taskSourceType === "repo") {
        const defaultPath = resolve(callerCwd, ".ailf", "tasks");
        if (!existsSync(defaultPath)) {
            console.error(`❌ --task-source=repo was set but no tasks directory was found.\n` +
                `   Looked for: ${defaultPath}\n` +
                "   Run 'ailf init' to scaffold .ailf/tasks/, or pass --repo-tasks-path <path>.");
            process.exit(1);
        }
        return defaultPath;
    }
    return undefined;
}
/**
 * True when `<callerCwd>/.ailf/tasks/` exists and holds at least one runnable
 * task file — the same file shapes `RepoTaskSource` loads (`*.task.ts`,
 * `*.task.js`, `*.yaml`, `*.yml`; dotfiles ignored). Mirrors the source
 * adapter's discovery so "present" means "would actually evaluate something".
 */
export function hasRunnableRepoTasks(callerCwd) {
    const tasksDir = resolve(callerCwd, ".ailf", "tasks");
    if (!existsSync(tasksDir))
        return false;
    try {
        return readdirSync(tasksDir).some((f) => !f.startsWith(".") &&
            (f.endsWith(".task.ts") ||
                f.endsWith(".task.js") ||
                f.endsWith(".yaml") ||
                f.endsWith(".yml")));
    }
    catch {
        return false;
    }
}
/**
 * D0064 — decide whether a local `ailf run` should auto-promote to the repo's
 * own `.ailf/tasks/` (repo-only) when the user has not chosen a task source.
 *
 * Promotes only when ALL hold:
 * - `configuredType` is unset (the RAW `taskSource.type`, NOT the
 *   `resolveTaskSourceType` output — that collapses `"content-lake"` to
 *   `undefined`, and an explicit `"content-lake"` must stay the opt-out).
 * - the run is local (`--remote` already inlines `.ailf/tasks/`).
 * - a populated `<callerCwd>/.ailf/tasks/` exists.
 */
export function shouldAutoPromoteRepoTasks(args) {
    if (args.configuredType !== undefined)
        return false;
    if (args.remote)
        return false;
    return hasRunnableRepoTasks(args.callerCwd);
}
/**
 * One-time stderr notice when a local run auto-promotes to `.ailf/tasks/`
 * (D0064). The pre-D0064 default silently evaluated the bundled corpus while
 * the user's tasks went unscored — the footgun this replaces — so the new
 * default announces itself and names the opt-out.
 */
function repoTasksAutoPromotionNotice(tasksPath) {
    const where = tasksPath ?? ".ailf/tasks/";
    return (`ℹ️  Evaluating your repo tasks in ${where} ` +
        `(no taskSource set — D0064 default).\n` +
        `   To evaluate the framework's bundled example corpus instead, set ` +
        `\`taskSource: { type: "content-lake" }\` in .ailf/ailf.config.ts.\n`);
}
// ---------------------------------------------------------------------------
// --config file error formatting
// ---------------------------------------------------------------------------
/**
 * Fields that exist on `RepoConfig` (`.ailf/ailf.config.ts`) but NOT on the
 * `EvalConfig` accepted by `--config`. Their presence is a strong signal the
 * user pasted their auto-loaded repo config into a `--config` file.
 * Everything else (`execution`, `grader`, `publish`, `reportStore`,
 * `artifacts`, `agentic`, `summary`, `taskSource`, `output`) overlaps
 * between the two shapes, and `source` accepts the inline object in both
 * (D0060 mitigation #2), so it is no longer a repo-config tell.
 */
const REPO_ONLY_CONFIG_KEYS = ["triggers", "owner"];
/**
 * Detect whether a `--config` validation error looks like a `RepoConfig`
 * (`.ailf/ailf.config.ts`) pasted into the wrong place. The only remaining
 * tell is an `unrecognized_keys` issue naming `triggers` or `owner` —
 * an object-shaped `source` is valid `EvalConfig` since the source union
 * (D0060 mitigation #2) and must not trigger the hint on its own.
 */
function looksLikeRepoConfig(error) {
    return error.issues.some((issue) => {
        if (issue.code !== "unrecognized_keys")
            return false;
        const keys = issue.keys ?? [];
        return keys.some((k) => REPO_ONLY_CONFIG_KEYS.includes(k));
    });
}
/**
 * Render a failed `--config` load as a clean CLI diagnostic instead of an
 * uncaught ZodError stack trace. Mirrors the first-5-issues style of the
 * Content Lake gates, and appends a cross-schema hint when the file smells
 * like a `.ailf/ailf.config.ts` (`RepoConfig`) rather than an `EvalConfig`.
 */
export function formatConfigFileError(err, filePath) {
    if (!(err instanceof ZodError)) {
        const msg = err instanceof Error ? err.message : String(err);
        return `❌ Failed to load --config file: ${filePath}\n   ${msg}`;
    }
    const issues = err.issues
        .slice(0, 5)
        .map((i) => `  [${i.path.join(".")}]: ${i.message}`)
        .join("\n");
    const more = err.issues.length > 5
        ? `\n  …and ${err.issues.length - 5} more issue(s)`
        : "";
    const lines = [`❌ Invalid --config file: ${filePath}`, `${issues}${more}`];
    if (looksLikeRepoConfig(err)) {
        lines.push("", "💡 This looks like a .ailf/ailf.config.ts (RepoConfig), which is a", "   different shape from the EvalConfig that --config expects:", "     • --config (EvalConfig): per-run fields like `areas`, `tasks`,", "       `mode`, and `compare`. `source` is the NAME of a source declared", "       in config/sources.ts (a string) or an inline object", "       ({ projectId, dataset, baseUrl }).", "     • .ailf/ailf.config.ts (RepoConfig): repo-only `triggers` and", "       `owner` blocks. It is auto-loaded by every `ailf run` — you", "       don't pass it via --config.", "   If you meant to set repo defaults, place this file at", "   .ailf/ailf.config.ts and drop the --config flag.");
    }
    return lines.join("\n");
}
// ---------------------------------------------------------------------------
// Pipeline entry point
// ---------------------------------------------------------------------------
/**
 * Map a `--remote` dispatch failure to a clean CLI message + exit code.
 *
 * Shared by the flag-based remote path and the `--config <file> --remote`
 * path (D0090) so both surface owner-team and gateway errors identically.
 * Anything unrecognized is re-thrown for the top-level handler.
 */
async function reportRemoteCliErrorAndExit(err) {
    const { RemoteOwnerTeamRequiredError } = await import("./interactive/resolve-missing-inputs.js");
    const { ApiError } = await import("../adapters/api-client/index.js");
    if (err instanceof RemoteOwnerTeamRequiredError) {
        console.error("❌ A remote run requires an owner team.");
        console.error("   Set one with `npx @sanity/ailf config set owner.team <slug>`, the " +
            "AILF_OWNER_TEAM env var, or `owner.team` in .ailf/ailf.config.ts.");
        process.exit(2);
    }
    if (err instanceof ApiError && err.code === "provenance_invalid") {
        console.error(`❌ Run cannot be dispatched — ${err.message}`);
        console.error("   Set an owner team with `npx @sanity/ailf config set owner.team <slug>`, " +
            "the AILF_OWNER_TEAM env var, or `owner.team` in .ailf/ailf.config.ts.");
        process.exit(1);
    }
    if (err instanceof ApiError) {
        console.error(`❌ ${err.message}`);
        process.exit(1);
    }
    throw err;
}
/**
 * Execute the evaluation pipeline.
 *
 * 1. Resolve CLI options into typed ResolvedOptions
 * 2. Build AppContext (composition root wires adapters)
 * 3. Build step sequence from context
 * 4. Delegate to the PipelineOrchestrator
 */
export async function executePipeline(cliOpts) {
    warnIfPlaceholderOwnerTeam();
    // When --config is provided, resolve config from file instead of CLI flags
    if (cliOpts.config) {
        const { existsSync } = await import("fs");
        const { FileConfigAdapter, isInlineJson } = await import("../adapters/config-sources/file-config-adapter.js");
        // A `--config` value that is inline JSON (D0090) has no file to stat;
        // only enforce the path-exists check for the file form.
        if (!isInlineJson(cliOpts.config) && !existsSync(cliOpts.config)) {
            console.error(`❌ Config file not found: ${cliOpts.config}`);
            process.exit(1);
        }
        const { createAppContext } = await import("../composition-root.js");
        const callerCwd = getCallerCwd();
        const adapter = new FileConfigAdapter(cliOpts.config, ROOT);
        let config;
        try {
            config = await adapter.resolve();
        }
        catch (err) {
            const configLabel = isInlineJson(cliOpts.config)
                ? "inline JSON"
                : cliOpts.config;
            console.error(formatConfigFileError(err, configLabel));
            process.exit(1);
        }
        // When `taskSource.type` is `repo` and no `repoTasksPath` was set in
        // the config file, fall back to `<callerCwd>/.ailf/tasks/` (the
        // location `ailf init` scaffolds). Silent fallback — composition-root
        // surfaces a helpful error if the directory is missing.
        if (config.taskSourceType === "repo" && !config.repoTasksPath) {
            const defaultPath = resolve(callerCwd, ".ailf", "tasks");
            if (existsSync(defaultPath)) {
                config.repoTasksPath = defaultPath;
            }
        }
        if (cliOpts.output) {
            config.outputPath = resolve(callerCwd, cliOpts.output);
        }
        // `--inject-doc` flag-plus-config MERGE (D0090). `injectContextDocs` is an
        // additive list, so flag refs and the config-file's `injectContextDocs`
        // CONCATENATE (config first, then flag) and dedupe per resolution key
        // rather than the flag overriding the config. This is the only path where
        // both a config-file list and `--inject-doc` flag refs can coexist; the
        // non-config flow has no config-file source for the list.
        const configInjectDocArgs = cliOpts.injectDoc ?? [];
        if (configInjectDocArgs.length > 0) {
            const flagRefs = injectDocIdsToRefs(configInjectDocArgs);
            config.injectContextDocs = mergeInjectContextDocs(config.injectContextDocs, flagRefs);
        }
        // `--sanity-document` still emits its soft-deprecation notice on the
        // `--config` path — the flag flows through `documentIds` in a JSON/API
        // config and is honored alongside the file's fields.
        if ((cliOpts.sanityDocument ?? []).length > 0) {
            warnSanityDocumentDeprecated();
        }
        // `--config <file|json> --remote` (D0090): dispatch the file-resolved
        // config to the API instead of running locally — the local-only artifact
        // env fallbacks and AppContext path below are skipped. The document scope
        // (widened `changedDocs` + `injectContextDocs`) and run selectors ride on
        // `config`; the caller envelope, auth, and owner-team resolution come from
        // `resolveOptions` (flags/env), the same way the flag-based `--remote` path
        // resolves them.
        if (cliOpts.remote || process.env.AILF_REMOTE === "1") {
            const o = await resolveOptions(cliOpts);
            const { runRemotePipelineFromConfig } = await import("./remote-pipeline.js");
            const { resolveRemoteOwnerTeam } = await import("./interactive/resolve-missing-inputs.js");
            const isTty = Boolean(process.stdin.isTTY && process.stdout.isTTY);
            try {
                await resolveRemoteOwnerTeam(o, { isTty, yes: cliOpts.yes ?? false });
                await runRemotePipelineFromConfig(config, o, callerCwd);
            }
            catch (err) {
                await reportRemoteCliErrorAndExit(err);
            }
            return;
        }
        // An explicit `taskSource: { type: "content-lake" }` in the `--config`
        // payload is HONORED on this local run — `createAppContext` wires
        // `ContentLakeTaskSource`. The AILF executor evaluates every dispatched
        // `--remote` job through this exact local `--config` path (it runs the
        // pipeline in-process without re-passing `--remote`), so collapsing
        // content-lake to the bundled corpus here silently dropped the caller's
        // Studio-authored tasks and evaluated the framework's bundled examples
        // instead — a hard "No literacy tasks match" for Content-Lake-only areas
        // (e.g. app-sdk) and, worse, a silently-wrong corpus for areas that also
        // ship a bundled task. A truly-unset `taskSource` still falls back to the
        // bundled corpus (`createTaskSource`); only an explicit content-lake source
        // reaches the Content Lake (W-honor-content-lake-tasksource-local-config).
        // Artifact-writer env-var fallbacks. The adapter populates the bulk of
        // the artifact settings from `EvalConfigSchema.artifacts.*` (W0077
        // Phase 6g); we layer the env-var fallbacks here for fields the schema
        // doesn't cover (GCS bucket, upload mode), and the AILF_ARTIFACTS_DIR
        // override that wins over both schema and CLI.
        config.artifactsDir = process.env.AILF_ARTIFACTS_DIR ?? config.artifactsDir;
        config.artifactsDryRun ??= cliOpts.artifactsWrite === false;
        config.artifactGcsBucket ??= process.env.AILF_GCS_ARTIFACT_BUCKET;
        config.artifactUpload ??= parseArtifactUploadEnv(process.env.AILF_ARTIFACT_UPLOAD);
        // Create AppContext directly from the merged config so adapters
        // (especially taskSource) are wired from the file config's
        // taskSourceType — not from CLI defaults.
        console.log(`  📂 Output directory: ${config.outputDir}`);
        const ctx = createAppContext(config);
        const pipelineStart = Date.now();
        const steps = buildStepSequence(ctx, pipelineStart);
        const result = await orchestratePipeline(ctx, steps);
        writePipelineResult(result, config.outputDir);
        // Phase 6 / DIAG-06: post-run hook fires after artifacts are written (D6-02)
        await runPostPipelineHooks(ctx, result, {
            cliOpts,
            summaryOnRun: config.summaryOnRun,
        });
        process.exit(result.success ? 0 : 1);
    }
    const o = await resolveOptions(cliOpts);
    console.log(`  📂 Output directory: ${o.outputDir}`);
    // Remote mode — submit to AILF API instead of running locally.
    // Use the caller's working directory (not the package root) because
    // remote mode reads .ailf/tasks/ from the user's repo, not from
    // packages/eval/. When installed via npx, ROOT points into the npm
    // cache which never contains .ailf/.
    if (o.remote) {
        const { runRemotePipeline } = await import("./remote-pipeline.js");
        const { resolveRemoteOwnerTeam } = await import("./interactive/resolve-missing-inputs.js");
        const isTty = Boolean(process.stdin.isTTY && process.stdout.isTTY);
        try {
            // A remote run always needs an owner team (the API gateway rejects a
            // teamless dispatch). Prompt for it when interactive; otherwise the
            // gateway's rejection is surfaced cleanly below instead of as a stack.
            await resolveRemoteOwnerTeam(o, { isTty, yes: cliOpts.yes ?? false });
            await runRemotePipeline(o, getCallerCwd());
        }
        catch (err) {
            await reportRemoteCliErrorAndExit(err);
        }
        return;
    }
    // Dry-run: validate only, don't execute steps
    if (o.dryRun) {
        const { validateConfiguration } = await import("../pipeline/validate.js");
        const { checkEnvironment } = await import("../pipeline/checks.js");
        const validation = validateConfiguration(ROOT);
        const envIssues = checkEnvironment(ROOT);
        validation.issues.push(...envIssues);
        const errors = validation.issues.filter((i) => i.severity === "error");
        if (errors.length > 0) {
            console.error("❌ Configuration validation failed:\n");
            for (const e of errors) {
                console.error(`  ERROR [${e.source}] ${e.message}`);
            }
            process.exit(1);
        }
        console.log("\n  ✅ Configuration is valid");
        console.log("  Pipeline configuration is valid. No steps were executed.");
        console.log("  Remove --dry-run to execute the full pipeline.\n");
        process.exit(0);
    }
    // Interactive resolution of required-but-missing inputs (v1: owner.team).
    // Local run path only — the --remote and --dry-run branches returned above.
    // Lazy import keeps @inquirer/prompts off the hot path for scripted runs.
    const { resolveMissingInputs } = await import("./interactive/resolve-missing-inputs.js");
    const isTty = Boolean(process.stdin.isTTY && process.stdout.isTTY);
    await resolveMissingInputs(o, { isTty, yes: cliOpts.yes ?? false });
    const ctx = buildAppContext(o, ROOT);
    const pipelineStart = Date.now();
    const steps = buildStepSequence(ctx, pipelineStart);
    const result = await orchestratePipeline(ctx, steps);
    writePipelineResult(result, o.outputDir);
    // Phase 6 / DIAG-06: post-run hook fires after artifacts are written (D6-02)
    await runPostPipelineHooks(ctx, result, {
        cliOpts,
        summaryOnRun: o.summaryOnRun,
    });
    // Print a dashboard deep-link to the published report (null when publish
    // was skipped, so a --no-publish run prints nothing). Last line so it's
    // the first thing the user sees on completion.
    const reportLink = resolveReportUrl(result.reportId);
    if (reportLink) {
        console.log(`  🔗 Dashboard: ${reportLink}`);
    }
    process.exit(result.success ? 0 : 1);
}
// ---------------------------------------------------------------------------
// Internal helpers
// ---------------------------------------------------------------------------
/**
 * D0090: one-time stderr notice that `--sanity-document` is soft-deprecated.
 *
 * The flag keeps working (its replace/append fetch-override semantics and its
 * `--remote` forwarding are both intact) for at least one minor cycle. The
 * notice steers users at `--inject-doc` / the `--config` payload and is
 * precise about the semantic difference the two mechanisms have:
 *
 * - `--inject-doc` (and config `injectContextDocs`) ADD a document's context to
 *   every in-scope task — additive, never replaces anything.
 * - `--sanity-document` REPLACES the canonical doc when slugs match (otherwise
 *   appends) in the fetched doc set — a fetch-time source-set override.
 *
 * Stderr (not stdout) so it never contaminates piped report output. Exported
 * for the deprecation-notice contract test.
 */
export const SANITY_DOCUMENT_DEPRECATION_NOTICE = "  ⚠️  --sanity-document is deprecated and will be removed in a future " +
    "release.\n" +
    "      To ADD a document's context to every in-scope task, use " +
    "`--inject-doc <id>`\n" +
    "      (or `injectContextDocs` in a --config payload). --inject-doc is " +
    "additive.\n" +
    "      --sanity-document REPLACES the canonical doc (or appends) in the " +
    "fetched\n" +
    "      doc set — that replace semantic has no --inject-doc equivalent yet, " +
    "so the\n" +
    "      flag keeps working for now.\n";
export function warnSanityDocumentDeprecated() {
    process.stderr.write(SANITY_DOCUMENT_DEPRECATION_NOTICE);
}
/**
 * W0143: warn once on stderr when the consumer is still running with the
 * unedited init-template placeholder for AILF_OWNER_TEAM. The run
 * continues — provenance just gets attributed as unset rather than the
 * literal placeholder.
 */
function warnIfPlaceholderOwnerTeam() {
    if (process.env.AILF_OWNER_TEAM?.trim() !== PLACEHOLDER_OWNER_TEAM)
        return;
    console.warn(`  ⚠️  AILF_OWNER_TEAM is still set to the init-template placeholder ` +
        `"${PLACEHOLDER_OWNER_TEAM}". Provenance will be logged as unset. ` +
        `Set a real team slug in .github/workflows/ailf-eval.yml (or unset ` +
        `AILF_OWNER_TEAM) to attribute this run.`);
}
/**
 * Resolve CLI options into typed ResolvedOptions, loading the repo config
 * from `<cwd>/.ailf/` first. This is the single async entry point; the pure
 * `computeResolvedOptions` does the option mapping once the config is loaded.
 */
export async function resolveOptions(opts) {
    const [repoConfig, userConfig] = await Promise.all([
        loadRepoConfig(),
        loadUserConfig(),
    ]);
    return computeResolvedOptions(opts, repoConfig, userConfig);
}
function writePipelineResult(result, outputDir) {
    mkdirSync(outputDir, { recursive: true });
    const resultFile = resolve(outputDir, "pipeline-result.json");
    writeFileSync(resultFile, JSON.stringify(result, null, 2));
    console.log(`  📄 Pipeline result: ${resultFile}\n`);
}
/**
 * Repo-config filenames probed under `<cwd>/.ailf/`, in resolution
 * precedence order (highest first). `ailf init` writes `ailf.config.ts`
 * (default `--format ts`), `config.yaml`, or `config.json` depending on
 * `--format`; all are honored here.
 *
 * TypeScript/JavaScript files load via the same jiti mechanism `.task.ts`
 * files use (`loadTsConfig`) — there is no second TS-loading path. YAML and
 * JSON load via `js-yaml` (which also parses JSON). When more than one file
 * is present the first match wins and the rest are ignored with a warning.
 */
const REPO_CONFIG_CANDIDATES = [
    "ailf.config.ts",
    "ailf.config.js",
    "config.ts",
    "config.js",
    "config.yaml",
    "config.yml",
    "config.json",
];
function isTsConfigFile(filename) {
    return filename.endsWith(".ts") || filename.endsWith(".js");
}
/**
 * Load the repo config from `<cwd>/.ailf/`. Probes TS/JS/YAML/JSON in a
 * fixed precedence order (see `REPO_CONFIG_CANDIDATES`) and returns the
 * first match, validated against `RepoConfigSchema`. Returns null when no
 * config file is present, or when the matched file fails to load/parse (a
 * warning is emitted and the run falls back to defaults + env vars).
 *
 * Auto-loads regardless of `--task-source`: the same config file is the
 * per-environment configuration home for every run (W0077 Phase 6a).
 * Environment variables still win over file values — that cascade lives in
 * `computeResolvedOptions`, which receives the parsed config from here.
 */
export async function loadRepoConfig(cwd = getCallerCwd()) {
    const ailfDir = resolve(cwd, ".ailf");
    const present = REPO_CONFIG_CANDIDATES.filter((name) => existsSync(resolve(ailfDir, name)));
    if (present.length === 0)
        return null;
    const [chosen, ...shadowed] = present;
    if (shadowed.length > 0) {
        console.warn(`  ⚠️  Multiple .ailf config files found; using ${chosen}, ignoring ` +
            `${shadowed.join(", ")}.`);
    }
    const configPath = resolve(ailfDir, chosen);
    const relPath = `.ailf/${chosen}`;
    try {
        let raw;
        if (isTsConfigFile(chosen)) {
            const result = await loadTsConfig(configPath);
            if (!result.ok) {
                console.warn(`  ⚠️  Failed to load ${relPath}: ${result.error}`);
                return null;
            }
            raw = result.value;
        }
        else {
            raw = load(readFileSync(configPath, "utf-8"));
        }
        return parseRepoConfig(raw, relPath);
    }
    catch (err) {
        console.warn(`  ⚠️  Failed to parse ${relPath}: ${err instanceof Error ? err.message : String(err)}`);
        return null;
    }
}
