/**
 * @sanity/ailf-core — Report schema (W0191)
 *
 * Runtime Zod gate for `ailf.report` documents at the ReportStore
 * read/write boundary. Mirrors the W0073 pattern that
 * `ContentLakeAuthorableTaskSchema` applies to `ailf.task` — turn silent
 * shape drift into a loud parse failure at the Content Lake boundary.
 *
 * **Scope A** (this file): top-level shape is `passthrough()` so that
 * adding a new top-level Report field on the TS side does not silently
 * fail validation before the schema author updates this mirror.
 * `provenance` (and nested `provenance.lineage`) are `strict()` because
 * they are fully mirrored — unknown keys there signal real drift.
 * `summary`, `comparison`, and `artifactManifest` are modeled as
 * non-empty objects with passthrough; their deep contents
 * (`ScoreSummary`, `FeatureScore`, the W0051 slim shapes, etc.) are
 * intentionally out of scope and may grow into a Scope B follow-up.
 *
 * @see packages/eval/src/adapters/task-sources/content-lake-task-source.ts
 *      — sibling W0073 gate
 * @see packages/core/src/types/index.ts — `Report`, `ReportProvenance`,
 *      `ReportLineage`
 * @see packages/shared/src/run-context.ts — `RunContext`
 * @see docs/work-items/W0191-report-store-schema-gate.json
 */
import { z } from "zod";
import { LITERACY_VARIANTS } from "../../ailf-shared/index.js";
import { ERROR_CLASSIFICATIONS } from "../services/error-classification.js";
import { DEGRADED_ENRICHMENT_FIELDS, REPORT_VALIDITY_STATUSES, } from "../types/index.js";
// ---------------------------------------------------------------------------
// RunContext building blocks (mirrors packages/shared/src/run-context.ts)
// ---------------------------------------------------------------------------
const RunOwnerSchema = z
    .object({
    team: z.string().min(1),
    individual: z.string().optional(),
})
    .strict();
const RunExecutorUserSchema = z
    .object({
    type: z.literal("user"),
    name: z.string().optional(),
    // `email` carries PII and is gated by AILF_CAPTURE_EMAIL on capture; it
    // may legitimately appear here on stored documents that captured it.
    email: z.string().optional(),
    surface: z.enum(["cli", "studio", "api"]),
    githubActor: z.string().optional(),
})
    .strict();
const RunExecutorSystemSchema = z
    .object({
    type: z.literal("system"),
    name: z.string().min(1),
    workflow: z.string().optional(),
    runId: z.string().optional(),
})
    .strict();
const RunExecutorSchema = z.discriminatedUnion("type", [
    RunExecutorUserSchema,
    RunExecutorSystemSchema,
]);
const RunHostSchema = z
    .object({
    platform: z.string().min(1),
    arch: z.string().min(1),
    ci: z.string().optional(),
})
    .strict();
const RunToolSchema = z
    .object({
    ailfVersion: z.string().min(1),
    nodeVersion: z.string().min(1),
})
    .strict();
const RunTriggerSchema = z.discriminatedUnion("type", [
    z
        .object({
        type: z.literal("ci"),
        runId: z.string().min(1),
        workflow: z.string().min(1),
    })
        .strict(),
    z
        .object({
        type: z.literal("cross-repo"),
        callerRef: z.string().optional(),
        callerRepo: z.string().min(1),
    })
        .strict(),
    z.object({ type: z.literal("manual") }).strict(),
    z
        .object({
        type: z.literal("scheduled"),
        schedule: z.string().min(1),
    })
        .strict(),
    z
        .object({
        type: z.literal("webhook"),
        documentId: z.string().optional(),
        source: z.string().min(1),
    })
        .strict(),
]);
const RunGitSchema = z
    .object({
    branch: z.string().min(1),
    prNumber: z.number().optional(),
    repo: z.string().min(1),
    sha: z.string().min(1),
})
    .strict();
const RunSourceSchema = z
    .object({
    baseUrl: z.string().min(1),
    dataset: z.string().optional(),
    name: z.string().min(1),
    perspective: z.string().optional(),
    projectId: z.string().optional(),
})
    .strict();
const RunModelEntrySchema = z
    .object({
    id: z.string().min(1),
    label: z.string().min(1),
})
    .strict();
// ---------------------------------------------------------------------------
// ReportLineage (mirrors `ReportLineage` in core types)
//
// The TS type narrows `RunContext.lineage` (which is `RunLineage` with
// `parentJobId`) down to `{ rerunOf?, comparedAgainst? }`. In practice
// `publish-report-step.ts` spreads existing lineage, so a stored
// `provenance.lineage` may legitimately carry `parentJobId` inherited
// from the run side. We model the runtime contract — strict, but
// `parentJobId` allowed — so that legitimate data does not trip the gate.
// ---------------------------------------------------------------------------
export const ReportLineageSchema = z
    .object({
    rerunOf: z.string().optional(),
    comparedAgainst: z.string().optional(),
    parentJobId: z.string().optional(),
})
    .strict();
// ---------------------------------------------------------------------------
// ReportAutoScope (mirrors `ReportAutoScope` in core types)
// ---------------------------------------------------------------------------
const ReportAutoScopeSchema = z
    .object({
    enabled: z.boolean(),
    affectedTaskIds: z.array(z.string()),
    skippedTaskIds: z.array(z.string()),
    perspective: z.string().min(1),
    impactSummary: z
        .object({
        added: z.number(),
        modified: z.number(),
        removed: z.number(),
    })
        .strict(),
    // Release-modified docs outside the evaluated canonical set (the D0066
    // visibility net). Optional so historical immutable reports — published
    // before the field existed — still validate.
    uncovered: z.array(z.string()).optional(),
})
    .strict();
// ---------------------------------------------------------------------------
// PromptfooUrlEntry (mirrors `PromptfooUrlEntry` in core types)
// ---------------------------------------------------------------------------
const PromptfooUrlEntrySchema = z
    .object({
    mode: z.string().min(1),
    url: z.string().min(1),
})
    .strict();
// ---------------------------------------------------------------------------
// ReportProvenance — full mirror of `ReportProvenance extends RunContext`
// ---------------------------------------------------------------------------
export const ReportProvenanceSchema = z
    .object({
    // RunContext fields
    areas: z.array(z.string()),
    classification: z.enum([
        "official",
        "adhoc",
        "experimental",
        "test",
        "external",
    ]),
    evalFingerprint: z.string().optional(),
    executor: RunExecutorSchema,
    git: RunGitSchema.optional(),
    graderModel: z.string().min(1),
    host: RunHostSchema.optional(),
    labels: z.array(z.string()).optional(),
    lineage: ReportLineageSchema.optional(),
    mode: z.string().min(1),
    models: z.array(RunModelEntrySchema),
    owner: RunOwnerSchema,
    purpose: z.string().optional(),
    source: RunSourceSchema,
    taskIds: z.array(z.string()).optional(),
    tool: RunToolSchema.optional(),
    trigger: RunTriggerSchema,
    variant: z.enum(LITERACY_VARIANTS).optional(),
    // ReportProvenance additions
    autoScope: ReportAutoScopeSchema.optional(),
    contextHash: z.string().optional(),
    // All eval sub-run fingerprints (a FULL literacy run has one per
    // sub-run). Optional so historical immutable reports — published before
    // the field existed — still validate; `.strict()` above means an unknown
    // key here would otherwise reject the whole report on read/write.
    evalFingerprints: z.array(z.string()).optional(),
    promptfooUrl: z.string().optional(),
    promptfooUrls: z.array(PromptfooUrlEntrySchema).optional(),
    runId: z.string().min(1),
    targetDocuments: z.array(z.string()).optional(),
    injectedContextDocs: z
        .array(z.union([
        z.object({
            id: z.string().min(1),
            slug: z.string().optional(),
            path: z.string().optional(),
            reason: z.string().optional(),
        }),
        z.object({ path: z.string().min(1), reason: z.string().optional() }),
        z.object({ slug: z.string().min(1), reason: z.string().optional() }),
        z.object({
            perspective: z.string().min(1),
            reason: z.string().optional(),
        }),
    ]))
        .optional(),
})
    .strict();
// ---------------------------------------------------------------------------
// ReportSchema — top-level Report shape
//
// Top-level uses `.passthrough()`: adding a new top-level Report field on
// the TS side should not break stored documents at the read boundary
// before the schema author updates this mirror. `summary`, `comparison`,
// and `artifactManifest` are also passthrough — their deep shapes
// (ScoreSummary, FeatureScore, the W0051 slim types) are out of Scope A.
// ---------------------------------------------------------------------------
const RecordPassthroughSchema = z.record(z.string(), z.unknown());
// ---------------------------------------------------------------------------
// ReportValidity — top-level data-health axis (D0059), orthogonal to
// `provenance.classification`. `satisfies z.ZodType<ReportValidity>` (D0045):
// the domain type is authored independently in ../types/report-validity.ts,
// so schema/type drift is a build error. Strict — unknown keys signal drift.
// ---------------------------------------------------------------------------
export const ReportValiditySchema = z
    .object({
    status: z.enum(REPORT_VALIDITY_STATUSES),
    reasons: z.array(z.string()),
    method: z.enum(["auto", "manual"]),
    rulesetVersion: z.string().min(1),
    assessedAt: z.iso.datetime({ offset: true }),
})
    .strict();
// ---------------------------------------------------------------------------
// ErrorSummary — typed error signal on `summary` (eval failure observability,
// slice B1). `satisfies z.ZodType<ErrorSummary>` (D0045): the domain type is
// authored independently in ../types/storage-schema.ts, so schema/type drift is
// a build error. Strict — unknown keys signal drift.
// ---------------------------------------------------------------------------
const FullyErroredTaskSchema = z
    .object({
    taskId: z.string().min(1),
    area: z.string().min(1),
    classification: z.enum(ERROR_CLASSIFICATIONS),
    sampleError: z.string(),
})
    .strict();
export const ErrorSummarySchema = z
    .object({
    totalTestCount: z.number().int().nonnegative(),
    erroredTestCount: z.number().int().nonnegative(),
    errorRate: z.number().min(0).max(1),
    fullyErroredTasks: z.array(FullyErroredTaskSchema),
    byClassification: z.record(z.enum(ERROR_CLASSIFICATIONS), z.number()),
})
    .strict();
export const ReportSchema = z
    .object({
    id: z.string().min(1),
    completedAt: z.iso.datetime({ offset: true }),
    durationMs: z.number().nonnegative(),
    summary: RecordPassthroughSchema.refine((s) => Object.keys(s).length > 0, {
        message: "summary must be a non-empty object",
    }),
    provenance: ReportProvenanceSchema,
    // The eval write path persists `comparison: null` when the slim copy
    // is empty, so the schema accepts null at the wire boundary.
    comparison: RecordPassthroughSchema.nullable().optional(),
    artifactManifest: RecordPassthroughSchema.optional(),
    // The eval write path stores `tag: report.tag ?? null` and
    // `title: report.title ?? null`, so the schema accepts null on both.
    tag: z.string().nullable().optional(),
    title: z.string().nullable().optional(),
    // Degraded marker (mirrors `ReportDegradation`): present when a scored run
    // is degraded. Discriminated on `reason` — `enrichment-missing` (a full
    // eval scored tests but gap-analysis enrichment never landed) or
    // `artifacts-not-persisted` (a remote artifact backend was configured but
    // nothing reached it). Each member is strict — unknown keys signal real
    // drift. Subsumed by `validity` (D0059); retained for back-compat reads.
    degraded: z
        .discriminatedUnion("reason", [
        z
            .object({
            reason: z.literal("enrichment-missing"),
            // Enum derived from the canonical DegradedEnrichmentField tuple so
            // the schema cannot drift from the core type.
            missing: z.array(z.enum(DEGRADED_ENRICHMENT_FIELDS)),
            detail: z.string().min(1),
        })
            .strict(),
        z
            .object({
            reason: z.literal("artifacts-not-persisted"),
            // Artifact section names (ArtifactType). Kept as strings rather
            // than importing the registry tuple here — the values are produced
            // by our own writer and the reason literal already gates the shape.
            missing: z.array(z.string().min(1)),
            detail: z.string().min(1),
        })
            .strict(),
    ])
        .optional(),
    // Data-health axis (D0059). Additive + nullable: pre-stamp reads have no
    // `validity` and are treated as trustworthy until backfilled.
    validity: ReportValiditySchema.optional(),
})
    .passthrough();
