/**
 * repo-schemas.ts — Canonical Zod schemas for task and config validation.
 *
 * Task schemas validate .ailf/tasks/*.yaml and .task.ts files against the
 * canonical GeneralizedTaskDefinition shape. Field names match the internal
 * domain model: `area` (not featureArea), `assertions` (not assert),
 * `context.docs` (not canonicalDocs), `prompt.text` (not vars.task).
 *
 * Previously this file re-exported from @sanity/ailf-tasks. That package
 * has been eliminated — all schema logic now lives here.
 *
 * Config schemas (RepoConfigSchema, trigger config) are eval-pipeline-
 * specific and remain here unchanged.
 *
 * @see packages/core/src/types/generalized-task.ts — canonical TypeScript types
 * @see docs/archive/exec-plans/tasks-as-content/phase-4-repo-based-tasks.md
 */
import { z } from "zod";
// ---------------------------------------------------------------------------
// Constants — curated assertion types and rubric template names
// ---------------------------------------------------------------------------
/**
 * The set of assertion types allowed in task files.
 *
 * Combines a curated subset of Promptfoo assertion types (stable, well-
 * documented, useful for external authors) with the agent-harness-specific
 * types mapped by `mode-handlers/agent-harness/assertions.ts`.
 */
export const CURATED_ASSERTION_TYPES = [
    "llm-rubric",
    "contains",
    "contains-any",
    "contains-all",
    "not-contains",
    "icontains",
    "icontains-any",
    "regex",
    "javascript",
    "similar",
    "cost",
    "latency",
    // Agent-harness assertions — verify sandbox state after the agent runs.
    // See src/pipeline/compiler/mode-handlers/agent-harness/assertions.ts
    "file-exists",
    "file-contains",
    "command-succeeds",
    "diff-matches",
];
/**
 * Valid rubric template names — must match template keys in
 * `packages/eval/config/rubrics.ts`.
 */
export const RUBRIC_TEMPLATE_NAMES = [
    // Core literacy dimensions
    "task-completion",
    "code-correctness",
    "doc-coverage",
    // MCP server dimensions
    "mcp-input-validation",
    "mcp-output-correctness",
    "mcp-error-handling",
    "mcp-security",
    // Knowledge probe dimensions
    "factual-correctness",
    "completeness",
    "currency",
    // Agent harness dimensions
    "process-quality",
    "agent-output",
    "agent-tool-usage",
];
// ---------------------------------------------------------------------------
// Doc ref schemas — polymorphic canonical doc references
// ---------------------------------------------------------------------------
/**
 * Polymorphic canonical doc reference — discriminated by key presence.
 * Exactly one resolution key (slug, path, id, or perspective) must be present.
 *
 * @see docs/design-docs/canonical-doc-resolution.md
 */
const SlugDocRefSchema = z.object({
    slug: z.string().min(1),
    reason: z.string().optional().default(""),
});
const PathDocRefSchema = z.object({
    path: z.string().min(1),
    reason: z.string().optional().default(""),
});
const IdDocRefSchema = z.object({
    id: z.string().min(1),
    reason: z.string().optional().default(""),
    /** Human-readable slug annotation (not used for resolution) */
    slug: z.string().optional(),
    /** Human-readable path annotation (not used for resolution) */
    path: z.string().optional(),
});
const PerspectiveDocRefSchema = z.object({
    perspective: z.string().min(1),
    reason: z.string().optional().default(""),
});
// Order matters: IdDocRefSchema first because it may also carry `slug`
// and `path` as optional annotations. Zod tries schemas in order, so
// entries like `{ id: "...", slug: "..." }` must match IdDocRefSchema
// (not SlugDocRefSchema).
const CanonicalDocRefSchema = z.union([
    IdDocRefSchema,
    SlugDocRefSchema,
    PathDocRefSchema,
    PerspectiveDocRefSchema,
]);
// ---------------------------------------------------------------------------
// Assertion schemas
// ---------------------------------------------------------------------------
/**
 * A single criterion within an llm-rubric assertion. Stable id-text pair.
 */
export const CriterionRefSchema = z.object({
    id: z
        .string()
        .min(1)
        .regex(/^[a-z0-9][a-z0-9-]*$/, {
        message: "criterion id must be lowercase alphanumeric with hyphens",
    }),
    text: z.string().min(1),
});
/**
 * A templated LLM-rubric assertion — uses one of the predefined rubric
 * templates with author-supplied criteria.
 */
export const TemplatedAssertionSchema = z.object({
    type: z.literal("llm-rubric"),
    template: z.enum(RUBRIC_TEMPLATE_NAMES),
    criteria: z.array(CriterionRefSchema).min(1),
    weight: z.number().optional(),
});
/**
 * A value-based assertion (contains, regex, cost, etc.). Uses .passthrough()
 * to allow extra fields for future extension without schema breakage.
 */
const ValueAssertionSchema = z
    .object({
    type: z.enum(CURATED_ASSERTION_TYPES),
    value: z.unknown().optional(),
    threshold: z.number().optional(),
    weight: z.number().optional(),
})
    .passthrough();
/** Union of all supported assertion shapes. */
const AssertionSchema = z.union([
    TemplatedAssertionSchema,
    ValueAssertionSchema,
]);
// ---------------------------------------------------------------------------
// Shared field schemas — building blocks reused across mode variants
// ---------------------------------------------------------------------------
/**
 * Variable keys reserved by the AILF compilers — populated automatically
 * from canonical task fields (`prompt.text`, `context.docs`, `area`).
 * Mirrors `ReservedPromptVarKey` in `@sanity/ailf-core`; the `satisfies`
 * clause makes drift a build error.
 */
const RESERVED_PROMPT_VAR_KEYS = [
    "task",
    "docs",
    "__featureArea",
];
const TaskPromptSchema = z.object({
    template: z.string().optional(),
    text: z.string().optional(),
    systemMessage: z.string().optional(),
    vars: z
        .record(z.string(), z.unknown())
        .refine((vars) => !RESERVED_PROMPT_VAR_KEYS.some((key) => key in vars), {
        message: `prompt.vars contains a reserved key. Reserved keys: ` +
            RESERVED_PROMPT_VAR_KEYS.join(", ") +
            `. Use prompt.text for the prompt body and context.docs for ` +
            `documentation references.`,
    })
        .optional(),
});
const RubricRefSchema = z.union([
    z.object({ ref: z.string().min(1) }),
    z.object({
        inline: z.string().min(1),
        dimensions: z
            .array(z.object({ key: z.string().min(1), weight: z.number() }))
            .optional(),
    }),
]);
const TaskProviderConfigSchema = z.object({
    id: z.string().min(1),
    config: z.record(z.string(), z.unknown()).optional(),
});
const TaskOptionsSchema = z.object({
    timeout: z.number().optional(),
    cache: z.boolean().optional(),
    transformOutput: z.string().optional(),
    promptfooOverrides: z.record(z.string(), z.unknown()).optional(),
});
const BaselineConfigSchema = z.object({
    enabled: z.boolean().optional(),
    rubric: z.enum(["abbreviated", "full", "none"]).optional(),
});
const MultiTurnSchema = z.object({
    turns: z.array(z.object({
        role: z.enum(["user", "assistant"]),
        content: z.string(),
    })),
});
// Context variants — shape differs slightly per mode. Keeping them separate
// is what enforces variant-specific strictness (e.g. only mcp-server carries
// `context.tools`).
const ContextLiteracyLikeSchema = z.object({
    docs: z.array(CanonicalDocRefSchema).optional(),
    fixtures: z.array(z.string()).optional(),
});
const ContextMcpSchema = z.object({
    docs: z.array(CanonicalDocRefSchema).optional(),
    fixtures: z.array(z.string()).optional(),
    tools: z.array(z.string()).optional(),
});
// Mode-specific nested shapes
const McpAuthSchema = z.object({
    type: z.enum(["bearer", "basic", "api_key", "oauth"]),
    token: z.string().optional(),
    username: z.string().optional(),
    password: z.string().optional(),
    value: z.string().optional(),
    keyName: z.string().optional(),
    placement: z.enum(["header", "query"]).optional(),
    grantType: z.enum(["client_credentials", "password"]).optional(),
    tokenUrl: z.string().optional(),
    clientId: z.string().optional(),
    clientSecret: z.string().optional(),
    scopes: z.array(z.string()).optional(),
});
const McpServerConfigSchema = z.object({
    transport: z.enum(["stdio", "sse", "streamable-http"]),
    command: z.string().optional(),
    url: z.string().optional(),
    env: z.record(z.string(), z.string()).optional(),
    headers: z.record(z.string(), z.string()).optional(),
    startupTimeoutMs: z.number().optional(),
    auth: McpAuthSchema.optional(),
});
const SandboxSchema = z.object({
    type: z.enum(["docker", "git-worktree", "none", "nsjail", "tempdir"]),
    image: z.string().optional(),
    limits: z
        .object({
        cpus: z.number().optional(),
        memoryBytes: z.number().optional(),
        diskBytes: z.number().optional(),
        networkAccess: z.boolean().optional(),
    })
        .optional(),
});
const KnowledgeBaseSchema = z.object({
    type: z.enum(["sanity-dataset", "embeddings-index", "file-corpus"]),
    name: z.string().min(1),
    config: z.record(z.string(), z.unknown()).optional(),
});
// ---------------------------------------------------------------------------
// CanonicalTaskSchema — mode-discriminated union
//
// Mirrors `GeneralizedTaskDefinition` from
// packages/core/src/types/generalized-task.ts. Each variant declares only
// the fields that belong to its mode — cross-variant field mixing (e.g.
// `{ mode: "mcp-server", sandbox: {...} }`) is rejected because strict
// objects do not permit unknown keys.
//
// YAML tasks may omit `mode` (defaults to "literacy"). The default is
// applied in `parseCanonicalTaskFile` before the discriminated union runs,
// since Zod cannot default a discriminator inside a discriminated union.
// ---------------------------------------------------------------------------
// Common fields shared by every variant — used as a base for each per-mode
// schema via `.extend()`. Kept internal to avoid consumers building tasks
// from the base alone.
const TaskCommonFieldsSchema = z.object({
    id: z
        .string()
        .min(1)
        .regex(/^[a-z0-9][a-z0-9-]*$/, "Task ID must be lowercase alphanumeric with hyphens"),
    title: z.string().min(1),
    description: z.string().optional(),
    area: z.string().optional(),
    difficulty: z.enum(["basic", "intermediate", "advanced"]).optional(),
    tags: z.array(z.string()).optional(),
    status: z
        .enum(["active", "draft", "paused", "archived"])
        .optional()
        .default("active"),
    assertions: z.array(AssertionSchema).optional(),
    rubric: RubricRefSchema.optional(),
    providers: z.array(TaskProviderConfigSchema).optional(),
    options: TaskOptionsSchema.optional(),
    prompt: TaskPromptSchema.optional(),
    metadata: z.record(z.string(), z.unknown()).optional(),
});
// `.strict()` on each variant rejects unknown keys — this is what enforces
// cross-variant strictness (e.g. a literacy task cannot carry `sandbox`).
const LiteracyTaskSchema = TaskCommonFieldsSchema.extend({
    mode: z.literal("literacy"),
    context: ContextLiteracyLikeSchema.optional(),
    referenceSolution: z.string().optional(),
    docCoverage: z.boolean().optional().default(false),
    baseline: BaselineConfigSchema.optional(),
}).strict();
const MCPServerTaskSchema = TaskCommonFieldsSchema.extend({
    mode: z.literal("mcp-server"),
    serverConfig: McpServerConfigSchema.optional(),
    capabilities: z.array(z.string()).optional(),
    context: ContextMcpSchema.optional(),
    multiTurn: MultiTurnSchema.optional(),
    models: z.array(z.string()).optional(),
    maxToolRounds: z.number().optional(),
}).strict();
const AgentHarnessTaskSchema = TaskCommonFieldsSchema.extend({
    mode: z.literal("agent-harness"),
    sandbox: SandboxSchema.optional(),
    tools: z.array(z.string()).optional(),
    fixtures: z.array(z.string()).optional(),
    context: ContextLiteracyLikeSchema.optional(),
    multiTurn: MultiTurnSchema.optional(),
}).strict();
const KnowledgeProbeTaskSchema = TaskCommonFieldsSchema.extend({
    mode: z.literal("knowledge-probe"),
    probeStrategy: z
        .enum(["breadth-first", "depth-first", "random-sample", "coverage-guided"])
        .optional(),
    knowledgeBase: KnowledgeBaseSchema.optional(),
    context: ContextLiteracyLikeSchema.optional(),
}).strict();
const CustomTaskSchema = TaskCommonFieldsSchema.extend({
    mode: z.literal("custom"),
    handler: z.string().min(1),
    schema: z.record(z.string(), z.unknown()).optional(),
    context: ContextLiteracyLikeSchema.optional(),
}).strict();
/**
 * Zod schema for a single task definition — a mode-discriminated union
 * mirroring `GeneralizedTaskDefinition`.
 *
 * Unknown keys are rejected on each variant. Invalid modes are rejected
 * by the discriminator itself.
 */
export const CanonicalTaskSchema = z.discriminatedUnion("mode", [
    LiteracyTaskSchema,
    MCPServerTaskSchema,
    AgentHarnessTaskSchema,
    KnowledgeProbeTaskSchema,
    CustomTaskSchema,
]);
// ---------------------------------------------------------------------------
// Content Lake authorable slice (D0038 / W0073)
//
// Runtime validator for Content Lake-authored tasks. Mirrors the
// `ContentLakeAuthorableTask` type from @sanity/ailf-core: today exactly
// the literacy variant. Expanding `ContentLakeAuthorableMode` requires
// extending this schema in the same pass (use `z.discriminatedUnion("mode", [...])`
// once the authorable set has more than one member).
// ---------------------------------------------------------------------------
export const ContentLakeAuthorableTaskSchema = LiteracyTaskSchema;
/**
 * Schema for an array of canonical tasks — what a single .ailf/tasks/*.yaml
 * file contains. Each file must define at least one task.
 */
export const CanonicalTaskFileSchema = z
    .array(CanonicalTaskSchema)
    .min(1);
/**
 * Pre-process raw task entries before discriminated-union parsing: when
 * `mode` is missing, default it to `"literacy"`. Zod cannot default a
 * discriminator in-place, so we normalise here.
 */
function applyModeDefault(raw) {
    if (!Array.isArray(raw))
        return raw;
    return raw.map((entry) => {
        if (typeof entry !== "object" || entry === null)
            return entry;
        const obj = entry;
        if (obj.mode === undefined)
            return { ...obj, mode: "literacy" };
        return obj;
    });
}
/**
 * Parse and validate a task file's content against the canonical schema.
 * Returns typed tasks or throws with a user-friendly Zod error message.
 *
 * Accepts pre-parsed YAML data (unknown), not a raw string.
 */
export function parseCanonicalTaskFile(raw, filename) {
    const prepared = applyModeDefault(raw);
    const result = CanonicalTaskFileSchema.safeParse(prepared);
    if (!result.success) {
        const messages = result.error.issues
            .map((i) => `  [${i.path.join(".")}]: ${i.message}`)
            .join("\n");
        throw new Error(`Invalid task file "${filename}":\n${messages}`);
    }
    return result.data;
}
// ---------------------------------------------------------------------------
// Legacy field name detection
//
// When authors accidentally use the old @sanity/ailf-tasks field names
// (featureArea, canonicalDocs, assert, vars), surface a helpful error
// message telling them what the canonical names are.
// ---------------------------------------------------------------------------
// Phrasing avoids literal `{` and `}` characters on purpose. GitHub Actions
// registers each line of a multi-line secret as a mask, so a pretty-printed
// JSON secret introduces standalone `{` / `}` masks that then redact every
// `{` / `}` in subsequent log output. See W0144.
/** Old field names from @sanity/ailf-tasks → canonical equivalents */
const LEGACY_FIELD_MAP = {
    featureArea: "area",
    canonicalDocs: "context.docs (move the docs array under a context parent)",
    assert: "assertions",
    vars: "prompt (move the text string under a prompt parent)",
};
/**
 * Detect legacy field names in raw task data and return helpful messages.
 *
 * Runs BEFORE Zod parsing to catch the most common migration mistake —
 * using old field names from @sanity/ailf-tasks instead of the canonical
 * GeneralizedTaskDefinition shape.
 */
export function detectLegacyFieldNames(raw, filename) {
    const warnings = [];
    if (!Array.isArray(raw))
        return warnings;
    for (let i = 0; i < raw.length; i++) {
        const entry = raw[i];
        if (typeof entry !== "object" || entry === null)
            continue;
        const obj = entry;
        const taskId = typeof obj.id === "string" ? obj.id : `task[${i}]`;
        for (const [legacy, canonical] of Object.entries(LEGACY_FIELD_MAP)) {
            if (legacy in obj) {
                warnings.push(`[${filename}] ${taskId}: Found legacy field "${legacy}" — ` +
                    `use "${canonical}" instead. ` +
                    "See contributing-tasks.md for the canonical task format.");
            }
        }
    }
    return warnings;
}
/**
 * Pre-process legacy `prompt.vars.{task,docs,__featureArea}` into the
 * canonical shape. Backwards-compatible: legacy-shape tasks continue to
 * load, but a deprecation warning is emitted per affected task.
 *
 *   Legacy:    prompt: { vars: { task: "...", docs: "file://..." } }
 *   Canonical: prompt: { text: "..." }
 *
 * Applies to every task regardless of mode. Per-task dedup: at most one
 * warning per task per call, listing every reserved key that was present.
 */
export function migratePromptShape(raw, filename) {
    if (!Array.isArray(raw))
        return { migrated: raw, warnings: [] };
    const warnings = [];
    const migrated = raw.map((entry, i) => {
        if (typeof entry !== "object" || entry === null)
            return entry;
        const obj = entry;
        const prompt = obj.prompt;
        if (typeof prompt !== "object" || prompt === null)
            return entry;
        const promptObj = prompt;
        const vars = promptObj.vars;
        if (typeof vars !== "object" || vars === null)
            return entry;
        const varsObj = vars;
        // Detect which reserved keys are present
        const presentReserved = RESERVED_PROMPT_VAR_KEYS.filter((key) => key in varsObj);
        if (presentReserved.length === 0)
            return entry;
        const taskId = typeof obj.id === "string" ? obj.id : `task[${i}]`;
        // Build migrated prompt + vars
        const newPrompt = { ...promptObj };
        const newVars = { ...varsObj };
        for (const key of presentReserved) {
            if (key === "task" && newPrompt.text === undefined) {
                // Move the prompt body to prompt.text only if the canonical slot
                // is unset; an explicit prompt.text always wins.
                newPrompt.text = newVars.task;
            }
            delete newVars[key];
        }
        // Drop empty vars to keep the migrated shape minimal
        if (Object.keys(newVars).length === 0) {
            delete newPrompt.vars;
        }
        else {
            newPrompt.vars = newVars;
        }
        warnings.push(`[${filename}] ${taskId}: deprecated prompt.vars keys ` +
            `(${presentReserved.join(", ")}) — migrated to canonical shape ` +
            `(prompt.text + context.docs). Update the task source to silence ` +
            `this warning.`);
        return { ...obj, prompt: newPrompt };
    });
    return { migrated, warnings };
}
// ---------------------------------------------------------------------------
// Config schemas — specific to the eval pipeline
// ---------------------------------------------------------------------------
const TriggerModeSchema = z.enum(["validate-only", "eval"]);
const TriggerConfigSchema = z.object({
    mode: TriggerModeSchema.default("eval"),
    paths: z.array(z.string()).optional(),
    blocking: z.boolean().optional(),
    notify: z.boolean().optional(),
    compare: z.boolean().optional(),
});
const ScheduleTriggerSchema = TriggerConfigSchema.extend({
    cron: z.string().min(1),
});
/**
 * Documentation source configuration.
 * Defines which Sanity project holds the documentation being evaluated.
 *
 * `studioOrigin` (W0077 Phase 6d) replaces the retired
 * `--sanity-studio-origin` CLI flag on `ailf run`. The `SANITY_STUDIO_ORIGIN`
 * env var still wins over this value at resolution time.
 */
const SourceConfigSchema = z
    .object({
    projectId: z.string().min(1).optional(),
    dataset: z.string().min(1).optional(),
    baseUrl: z.string().url().optional(),
    studioOrigin: z.string().url().optional(),
})
    .optional();
/**
 * Report store configuration.
 * Defines which Sanity project receives `ailf.report` documents.
 */
const ReportStoreConfigSchema = z
    .object({
    projectId: z.string().min(1),
    dataset: z.string().min(1),
})
    .optional();
/**
 * Publish policy. Controls when `ailf run` writes a report to the Content
 * Lake without an explicit `--publish` / `--no-publish` flag.
 *
 * - `auto: "always"`    — publish any run with a configured report store
 * - `auto: "full-runs"` — publish non-debug runs (default)
 * - `auto: "never"`     — never auto-publish; users must pass --publish
 *
 * `tag` is a default value for `--publish-tag` when not passed at the CLI.
 *
 * @see docs/design-docs/pipeline-command-surface.md §5.3
 */
const PublishConfigSchema = z
    .object({
    auto: z.enum(["always", "full-runs", "never"]).optional(),
    tag: z.string().optional(),
})
    .optional();
/**
 * Execution-tier configuration. Per-environment values that the four
 * retired CLI flags used to set: concurrency, grader replications, gap
 * analysis toggle, and the AILF API URL.
 *
 * @see docs/design-docs/pipeline-command-surface.md §5.7 (W0077 Phase 6b)
 */
const ExecutionConfigSchema = z
    .object({
    concurrency: z.number().int().positive().optional(),
    graderReplications: z.number().int().positive().optional(),
    /**
     * Plan 03-04 GRAD-04 — replications per borderline judgment.
     * Default 3 (composition-root). Positive integer.
     */
    borderlineReplications: z.number().int().positive().optional(),
    gapAnalysis: z.boolean().optional(),
    apiUrl: z.string().url().optional(),
})
    .optional();
/**
 * Grader configuration.
 *
 * - `context: "rubric-only"` — grader sees only the rubric template +
 *   criteria + candidate response.
 * - `context: "with-docs"` — canonical reference content is injected into
 *   the assertion's `rubricPrompt` so the grader has authoritative ground
 *   truth.
 */
const GraderConfigSchema = z
    .object({
    context: z.enum(["rubric-only", "with-docs"]).optional(),
})
    .optional();
/**
 * Task-source configuration (W0077 Phase 6h). Replaces the retired
 * `--task-source` and `--repo-tasks-path` CLI flags on `ailf run`.
 *
 * - `type`           — `content-lake` (default) or `repo`. When `repo`,
 *                      tasks load from `repoTasksPath` (or
 *                      `<cwd>/.ailf/tasks/` if unset).
 * - `repoTasksPath`  — optional explicit path. Resolved relative to the
 *                      caller's cwd. Required to exist on disk.
 *
 * No env-var fallback today; cascade is config-file → built-in default.
 */
const TaskSourceConfigSchema = z
    .object({
    type: z.enum(["content-lake", "repo"]).optional(),
    repoTasksPath: z.string().min(1).optional(),
})
    .optional();
/**
 * Artifact-writer configuration (W0077 Phase 6g). Replaces the retired
 * `--no-artifacts`, `--artifacts-dir`, and `--artifacts-exclude` CLI flags
 * on `ailf run`. The `AILF_ARTIFACTS_DIR` env var still wins over
 * `artifacts.dir` at resolution time. `artifacts.enabled` defaults to
 * `true` (writers attached); set `false` to disable all writers (mirrors
 * the legacy `--no-artifacts` semantics).
 *
 * Other commands (`ailf runs export`, etc.) keep their `--artifacts-dir`
 * flag — that's a "read from this directory" override, distinct from the
 * pipeline's write-side `artifacts.dir`.
 */
const ArtifactsConfigSchema = z
    .object({
    enabled: z.boolean().optional(),
    dir: z.string().min(1).optional(),
    exclude: z.array(z.string().min(1)).optional(),
})
    .optional();
/**
 * Owner attribution (W0077 Phase 6f). Replaces the retired `--owner-team`
 * and `--owner-individual` CLI flags. Both feed the D0037 caller envelope
 * that surfaces in remote-mode runs. Env vars `AILF_OWNER_TEAM` and
 * `AILF_OWNER_INDIVIDUAL` still win over these values at resolution time.
 */
const OwnerConfigSchema = z
    .object({
    team: z.string().min(1).optional(),
    individual: z.string().min(1).optional(),
})
    .optional();
/**
 * Post-run diagnosis summary policy (Phase 6 / DIAG-06).
 * Sits in the W0077 Phase-6a auto-load pathway.
 */
const SummaryConfigSchema = z
    .object({
    onRun: z.enum(["auto", "always", "never"]).optional(),
})
    .optional();
/**
 * Agentic-mode configuration (W0077 Phase 6f). Replaces the retired
 * `--header` and `--allowed-origin` CLI flags. `headers` is a key/value
 * object (mirrors `DOC_HEADERS` env-var JSON shape); `allowedOrigins` is a
 * list of origin globs. The `DOC_HEADERS` and `DOC_ALLOWED_ORIGIN(S)` env
 * vars still apply downstream as additive merges.
 */
const AgenticConfigSchema = z
    .object({
    headers: z.record(z.string(), z.string()).optional(),
    allowedOrigins: z.array(z.string().min(1)).optional(),
})
    .optional();
/**
 * Output-directory configuration. Replaces the retired `--output-dir`
 * CLI flag on `ailf run`. Resolution order:
 *
 *   .ailf/config.yaml `output.dir` > built-in default
 *
 * Path is resolved relative to the caller's cwd. The built-in default is
 * `<cwd>/.ailf/results/latest/` (see resolve-output-dir.ts). Other commands
 * (`ailf publish`, `ailf pr-comment`, etc.) keep their `--output-dir`
 * flag — that's a "read from this directory" override, distinct from the
 * pipeline's write-side `output.dir`.
 *
 * @see docs/design-docs/pipeline-command-surface.md §5.7 (W0077 Phase 6c)
 * @see docs/design-docs/output-dir-routing.md
 */
const OutputConfigSchema = z
    .object({
    dir: z.string().min(1).optional(),
})
    .optional();
/**
 * Zod schema for .ailf/config.yaml — controls documentation source,
 * report destination, and trigger behavior for evaluations from an
 * external repository.
 */
export const RepoConfigSchema = z.object({
    source: SourceConfigSchema,
    reportStore: ReportStoreConfigSchema,
    publish: PublishConfigSchema,
    execution: ExecutionConfigSchema,
    grader: GraderConfigSchema,
    output: OutputConfigSchema,
    owner: OwnerConfigSchema,
    agentic: AgenticConfigSchema,
    artifacts: ArtifactsConfigSchema,
    summary: SummaryConfigSchema,
    taskSource: TaskSourceConfigSchema,
    triggers: z
        .object({
        pr: TriggerConfigSchema.optional(),
        "pr-task-change": TriggerConfigSchema.optional(),
        main: TriggerConfigSchema.optional(),
        schedule: ScheduleTriggerSchema.optional(),
    })
        .optional(),
});
/**
 * Parse and validate .ailf/config.yaml content. Returns typed config or throws.
 */
export function parseRepoConfig(raw, filename = ".ailf/config.yaml") {
    const result = RepoConfigSchema.safeParse(raw);
    if (!result.success) {
        const messages = result.error.issues
            .map((i) => `  [${i.path.join(".")}]: ${i.message}`)
            .join("\n");
        throw new Error(`Invalid repo config "${filename}":\n${messages}`);
    }
    return result.data;
}
// ---------------------------------------------------------------------------
// Workflow contract — structural shape of `.github/workflows/ailf-eval.yml`
//
// Used by the Tier 1 init contract test (W0139 M2) and the Tier 4 drift
// check to assert that the template `init` writes still matches what a
// downstream consumer's PR comment + report-publish path depends on.
//
// Loose by design: this is a contract on the consumer-visible parts (job
// runs `npx @sanity/ailf ... run --remote`, posts a PR comment, names the
// workflow `AI Literacy Eval`), not the literal byte content of the YAML.
// ---------------------------------------------------------------------------
const WorkflowStepSchema = z
    .object({
    name: z.string().optional(),
    id: z.string().optional(),
    if: z.string().optional(),
    uses: z.string().optional(),
    run: z.string().optional(),
    env: z.record(z.string(), z.unknown()).optional(),
    with: z.record(z.string(), z.unknown()).optional(),
})
    .passthrough();
const WorkflowJobSchema = z
    .object({
    name: z.string().optional(),
    "runs-on": z.union([z.string(), z.array(z.string())]),
    permissions: z.record(z.string(), z.unknown()).optional(),
    steps: z.array(WorkflowStepSchema).min(1),
})
    .passthrough();
/**
 * Structural schema for the `ailf-eval.yml` workflow template emitted by
 * `ailf init`. Validates the consumer-visible contract: a workflow named
 * "AI Literacy Eval" with at least one job containing checkout + eval
 * steps. The literal YAML body is intentionally not pinned — comments,
 * step ordering, and option flags can shift without breaking consumers.
 */
export const AilfEvalWorkflowSchema = z
    .object({
    name: z.string().min(1),
    on: z.unknown(),
    jobs: z.record(z.string(), WorkflowJobSchema),
})
    .passthrough();
/**
 * Parse and validate a `.github/workflows/ailf-eval.yml` payload (already
 * loaded from YAML). Throws with a Zod-formatted message on failure.
 */
export function parseAilfEvalWorkflow(raw, filename = ".github/workflows/ailf-eval.yml") {
    const result = AilfEvalWorkflowSchema.safeParse(raw);
    if (!result.success) {
        const messages = result.error.issues
            .map((i) => `  [${i.path.join(".")}]: ${i.message}`)
            .join("\n");
        throw new Error(`Invalid workflow "${filename}":\n${messages}`);
    }
    return result.data;
}
