import type { Browser } from "playwright";
import {
  classify,
  type ClassifiedRun,
  type CoverageEvent,
  type DetectorOutcome,
  type KernelStores,
  type HarnessEvent,
} from "../../../core/src/classify/classifier.js";
import { cellFailureEvent } from "../cell-harness.js";
import { asReadonlyKernelStores } from "../../../core/src/sdk/detector.js";
import { capabilityProfileRef, type CapabilityProfile } from "../../../core/src/sdk/capability.js";
import type { MatrixCell } from "../../../schema/src/records/context.js";
import type { CaptureOptions, CellEvidence } from "../../../playwright/src/cell-runner.js";
import { cellScreenshotArtifacts } from "../../../playwright/src/cell-original.js";
import type { AuthAdapter } from "../../../playwright/src/auth.js";
import type { RedactionRule } from "../../../playwright/src/redaction.js";
import { sha256Canonical } from "../../../schema/src/canonical.js";
import type { CoverageGap } from "../../../universal/src/oracle/witnesses.js";
import type { Detector } from "../../../core/src/sdk/detector.js";
import type { DetectorContext } from "../../../core/src/sdk/detector.js";
import { runInteractionCell } from "./capture.js";
import { evaluateDiscoveredControlOracle } from "./oracles.js";
import { isMeasuredCoupledNumericMeasurement, stableCoupledNumericCandidates } from "./numeric.js";
import {
  assertInteractionCoverageSerialization,
  buildPreconditionCoverageObligation,
  buildPreconditionInteractionCoverage,
  preconditionEvidence,
} from "./types.js";
import type {
  DiscoveredControl,
  DiscoveredControlCoverageOutcome,
  InteractionArtifactRef,
  InteractionCellEvidence,
  InteractionCoverageSerialization,
  PreconditionCoverageObligation,
  PreconditionObligationEvidence,
  Ui001EvidenceRecord,
} from "./types.js";
import type { TerminalDiscoveredControlCoverageOutcome } from "./oracles.js";
import { pushHarnessWithPartialEvidence } from "../partial-evidence-harness.js";

function compare(left: string, right: string): number {
  return left < right ? -1 : left > right ? 1 : 0;
}

function controlIdentity(control: import("./types.js").DiscoveredControl): Omit<import("./types.js").DiscoveredControl, "accessibleName"> {
  const { accessibleName, ...identity } = control;
  void accessibleName;
  return identity;
}

export type InteractionMatrixPlan = {
  cells: MatrixCell[];
  coverageGaps?: CoverageGap[];
  journeyHandledCellIds?: readonly string[];
};

export type InteractionCoverageGap = {
  gap: CoverageGap;
  cellId?: string;
  control?: import("./types.js").DiscoveredControl;
  artifact?: InteractionArtifactRef;
};

export type RunInteractionMatrixInput = {
  plan: InteractionMatrixPlan;
  baseUrl: string;
  detectors: Detector<InteractionCellEvidence, Ui001EvidenceRecord>[];
  stores: KernelStores;
  runId: string;
  seed: string;
  clockStart: string;
  browser: Browser;
  capture: CaptureOptions;
  timeouts: { navigateMs: number; settleMs: number };
  capabilities: CapabilityProfile;
  adapters?: Record<string, AuthAdapter>;
  artifactRunDir?: string;
  redactionRules?: RedactionRule[];
  onCellEvidence?: (evidence: CellEvidence) => void;
  interactionDiscovery?: {
    enabled: boolean;
    routeOptions?: Record<string, { commandControls?: boolean; searchSubmission?: boolean }>;
  };
};

function browserExecutionContext(cell: MatrixCell) {
  return { kind: "browser" as const, cell: { id: cell.id } };
}

function detectorScope(cell: MatrixCell, detectorId: string) {
  return {
    id: cell.id,
    detectorId,
    surfaceId: cell.id,
  };
}

function coverageGapDescriptor(
  entry: InteractionCoverageGap,
  cells: readonly MatrixCell[],
): { prefix: string; cellId?: string } {
  const { gap } = entry;
  if (gap.reason !== "unproven-precondition") {
    return { prefix: gap.reason };
  }
  const separator = gap.failedPrecondition.indexOf(":");
  if (separator < 1) {
    throw new Error(`invalid interaction coverage gap: ${gap.failedPrecondition}`);
  }
  const gapKind = gap.failedPrecondition.slice(0, separator);
  const cellId = entry.cellId ?? [...cells]
    .sort((left, right) => right.id.length - left.id.length)
    .find((cell) => gap.failedPrecondition === `${gapKind}:${cell.id}` || gap.failedPrecondition.startsWith(`${gapKind}:${cell.id}:`))
    ?.id;
  if (cellId === undefined) throw new Error(`interaction coverage gap lacks matrix cell: ${gap.failedPrecondition}`);
  return { prefix: `${gapKind}:${cellId}`, cellId };
}

function coverageGapScope(gap: CoverageGap, descriptor: { prefix: string; cellId?: string }) {
  return {
    id: `${descriptor.prefix}:${sha256Canonical({
      kind: "interaction-coverage-gap-scope",
      detectorId: "interaction-coverage",
      gap,
    })}`,
    detectorId: "interaction-coverage",
    ...(descriptor.cellId === undefined ? {} : { surfaceId: descriptor.cellId }),
  };
}

function coverageGapContext(gap: CoverageGap, descriptor: { prefix: string; cellId?: string }) {
  return {
    kind: "browser" as const,
    cell: {
      id: descriptor.cellId ?? `${descriptor.prefix}:${sha256Canonical({
        kind: "interaction-coverage-gap-cell",
        gap,
      })}`,
    },
  };
}

function witnessRefForGap(gap: CoverageGap, descriptor: { prefix: string; cellId?: string }): CoverageEvent["witnessRefs"] {
  return [
    {
      id: `${descriptor.prefix}:${sha256Canonical({
        kind: "interaction-coverage-gap-witness",
        gap,
      })}`,
    },
  ];
}

function setupObservation(
  entry: InteractionCoverageGap,
  control: DiscoveredControl | undefined,
): import("./types.js").JsonObservation {
  if (control === undefined) {
    return {
      failedPrecondition: entry.gap.reason === "unproven-precondition"
        ? entry.gap.failedPrecondition
        : entry.gap.reason,
      ...(entry.cellId === undefined ? {} : { cellId: entry.cellId }),
    };
  }
  return { control: controlIdentity(control) };
}

function artifactFromCellEvidence(
  evidence: InteractionCellEvidence | undefined,
): InteractionArtifactRef | undefined {
  return evidence?.interactionArtifacts.fullScreenshot;
}

function optionalGapArtifact(
  entry: InteractionCoverageGap,
): { artifact?: InteractionArtifactRef } {
  return entry.artifact === undefined ? {} : { artifact: entry.artifact };
}

function gapEntryContext(
  cellId: string,
  evidence?: InteractionCellEvidence,
  control?: DiscoveredControl,
): Pick<InteractionCoverageGap, "cellId" | "control" | "artifact"> {
  const artifact = artifactFromCellEvidence(evidence);
  return {
    cellId,
    ...(control === undefined ? {} : { control }),
    ...(artifact === undefined ? {} : { artifact }),
  };
}

function preconditionObligationEvidenceForGap(
  gap: CoverageGap,
  entry: InteractionCoverageGap,
  witness: { id: string },
): PreconditionObligationEvidence {
  if (gap.reason !== "unproven-precondition") {
    throw new Error(`expected unproven-precondition gap, received ${gap.reason}`);
  }
  const control = entry.control;
  const setup = setupObservation(entry, control);
  const locator = control?.path;
  const witnessRef = { id: witness.id };
  const prefix = gap.failedPrecondition.split(":")[0] ?? "";
  const artifactFields = optionalGapArtifact(entry);

  if (prefix === "interaction-probe") {
    if (locator === undefined) {
      return preconditionEvidence.missingSetupAndLocator({
        expected: { probe: "interactionProbe" },
        witness: witnessRef,
        ...artifactFields,
      });
    }
    return preconditionEvidence.missingSetup({
      expected: { probe: "interactionProbe" },
      locator,
      witness: witnessRef,
      setup,
      ...artifactFields,
    });
  }
  if (prefix === "interaction-oracle-evidence") {
    return preconditionEvidence.missingObserved({
      expected: { reaction: "required" },
      locator: locator ?? `cell:${entry.cellId ?? "unknown"}`,
      setup,
      witness: witnessRef,
      ...artifactFields,
    });
  }
  if (prefix === "interaction-oracle-reaction") {
    return preconditionEvidence.missingObserved({
      expected: { reaction: "observable-change" },
      locator: locator ?? `cell:${entry.cellId ?? "unknown"}`,
      observed: { reacted: false },
      setup,
      witness: witnessRef,
      ...artifactFields,
    });
  }
  if (prefix === "interaction-discovery-contract") {
    if (gap.failedPrecondition.endsWith(":none")) {
      if (locator === undefined) {
        return preconditionEvidence.missingExpectedAndObserved({
          locator: `cell:${entry.cellId ?? "unknown"}`,
          setup: { discovery: "empty-control-set" },
          witness: witnessRef,
          ...artifactFields,
        });
      }
      return preconditionEvidence.missingExpected({
        observed: { controls: 0 },
        locator,
        setup,
        witness: witnessRef,
        ...artifactFields,
      });
    }
    return preconditionEvidence.missingWitness({
      expected: { contract: "discoverable-oracle" },
      locator: locator ?? `cell:${entry.cellId ?? "unknown"}`,
      setup,
      ...(control === undefined ? {} : { observed: { role: control.role } }),
      ...artifactFields,
    });
  }
  if (prefix === "interaction-coupled-numeric-measurement") {
    return preconditionEvidence.missingObserved({
      expected: { measurement: "coupled-numeric-phase" },
      locator: locator ?? `cell:${entry.cellId ?? "unknown"}`,
      setup,
      witness: witnessRef,
      ...artifactFields,
    });
  }
  if (prefix === "interaction-coupled-numeric-observation") {
    return preconditionEvidence.missingObserved({
      expected: { numericObservation: "present" },
      locator: `cell:${entry.cellId ?? "unknown"}`,
      setup: { discovery: "controls-without-numeric" },
      witness: witnessRef,
      ...artifactFields,
    });
  }
  return preconditionEvidence.unrecognizedGap({
    failedPrecondition: gap.failedPrecondition,
    observed: { gapKind: prefix },
    setup,
    witness: witnessRef,
    ...(locator === undefined ? {} : { locator }),
    ...artifactFields,
  });
}

function preconditionObligationFromGap(
  gap: CoverageGap,
  entry: InteractionCoverageGap,
  witness: { id: string },
): PreconditionCoverageObligation {
  const witnessRefs: [{ id: string }] = [{ id: witness.id }];
  return buildPreconditionCoverageObligation({
    id: gap.reason === "unproven-precondition"
      ? `precondition:${gap.failedPrecondition}`
      : `precondition:${gap.reason}`,
    witnessRefs,
    unproven: preconditionObligationEvidenceForGap(gap, entry, witness),
  });
}

function interactionCoverageEvent(input: {
  cell: MatrixCell;
  control: DiscoveredControl;
  coverage: DiscoveredControlCoverageOutcome | TerminalDiscoveredControlCoverageOutcome;
}): CoverageEvent {
  const control = controlIdentity(input.control);
  const scopeHash = sha256Canonical({
    kind: "interaction-oracle-coverage",
    cellId: input.cell.id,
    control,
    coverage: input.coverage,
  });
  const controlRef = {
    accessibleName: input.control.accessibleName ?? "",
    locator: input.control.path,
  };
  const interaction: InteractionCoverageSerialization = input.coverage.phase === "terminal"
    ? {
        gapKind: input.coverage.reason,
        phase: "terminal",
        evidence: input.coverage.evidence,
        control: controlRef,
      }
    : {
        gapKind: input.coverage.reason,
        phase: "activation",
        evidence: input.coverage.evidence,
        control: controlRef,
      };
  assertInteractionCoverageSerialization(interaction);
  return {
    scope: {
      id: `interaction-oracle:${input.cell.id}:${scopeHash}`,
      detectorId: "interaction-coverage",
      surfaceId: input.cell.id,
    },
    context: browserExecutionContext(input.cell),
    reason: "unproven-precondition",
    witnessRefs: input.coverage.witnessRefs,
    interaction,
  };
}

function coverageEventsFromGaps(
  gaps: InteractionCoverageGap[],
  cells: readonly MatrixCell[],
  capabilities: CapabilityProfile,
): CoverageEvent[] {
  return gaps.map((entry) => {
    const { gap } = entry;
    const descriptor = coverageGapDescriptor(entry, cells);
    const witnessRefs = witnessRefForGap(gap, descriptor);
    const witness = witnessRefs[0];
    if (witness === undefined) {
      throw new Error("coverage gap requires a witness ref");
    }
    const baseEvent = {
      scope: coverageGapScope(gap, descriptor),
      context: coverageGapContext(gap, descriptor),
      reason: gap.reason,
      witnessRefs,
      capabilityProfileRef: capabilityProfileRef(
        capabilities,
        "profileFeature" in gap ? [gap.profileFeature] : [],
      ),
    };
    if (gap.reason !== "unproven-precondition") {
      return baseEvent;
    }
    const obligation = preconditionObligationFromGap(gap, entry, witness);
    const interaction = buildPreconditionInteractionCoverage({
      obligations: [obligation],
      ...(entry.control === undefined
        ? {}
        : {
            control: {
              accessibleName: entry.control.accessibleName ?? "",
              locator: entry.control.path,
            },
          }),
    });
    assertInteractionCoverageSerialization(interaction);
    return {
      ...baseEvent,
      interaction,
    };
  });
}

export function preconditionCoverageFromGap(
  entry: InteractionCoverageGap,
  cells: readonly MatrixCell[],
  capabilities: CapabilityProfile,
): CoverageEvent {
  const [event] = coverageEventsFromGaps([entry], cells, capabilities);
  if (event === undefined) {
    throw new Error("precondition coverage gap produced no event");
  }
  return event;
}

function discoveredOracleOutcome(input: {
  cell: MatrixCell;
  control: import("./types.js").DiscoveredControl;
  observation: import("./oracles.js").DiscoveredControlOracleObservation;
  result: Extract<ReturnType<typeof evaluateDiscoveredControlOracle>, { kind: "finding" }>;
}): DetectorOutcome {
  const locator = input.control.path;
  const laneEligibility = input.result.band === "advisory"
    ? "advisory"
    : "blocking";
  return {
    detector: { id: "ui-a14-discovered-control-oracle", version: "1.0.0" },
    class: input.result.oracle,
    severity: "high",
    target: { kind: "dom", canonical: locator },
    context: browserExecutionContext(input.cell),
    summary: `Discovered control violated ${input.result.oracle}`,
    evidence: [{ truthSource: "observed", payload: { control: controlIdentity(input.control), observation: input.observation, oracle: input.result } }],
    artifacts: [],
    laneEligibility,
    proofConditionMet: true,
    scope: detectorScope(input.cell, "ui-a14-discovered-control-oracle"),
    violation: { control: controlIdentity(input.control), oracle: input.result },
    contextDimensions: {
      route: input.cell.routeId,
      role: input.cell.role,
      locale: input.cell.locale,
    },
  };
}

function coupledNumericOutcome(input: {
  candidate: import("./numeric.js").CoupledNumericCandidate;
  cell: MatrixCell;
}): DetectorOutcome {
  return {
    detector: { id: "ui-a15-coupled-numeric-display", version: "1.0.0" },
    class: input.candidate.oracle.kind,
    severity: "info",
    target: { kind: "dom", canonical: input.candidate.displayIdentity },
    context: browserExecutionContext(input.cell),
    summary: "Stable behaviorally coupled numeric display requires review before promotion",
    evidence: [{ truthSource: "observed", payload: input.candidate }],
    artifacts: [],
    laneEligibility: "advisory",
    scope: detectorScope(input.cell, "ui-a15-coupled-numeric-display"),
    violation: input.candidate,
    contextDimensions: { route: input.cell.routeId, role: input.cell.role, locale: input.cell.locale },
  };
}

function toClassifierOutcome(
  outcome: Awaited<
    ReturnType<Detector<InteractionCellEvidence, Ui001EvidenceRecord>["evaluate"]>
  >[number],
  cell: MatrixCell,
  cellArtifacts: DetectorOutcome["artifacts"],
): DetectorOutcome {
  return {
    detector: outcome.detector,
    class: outcome.class,
    severity: outcome.severity,
    target: outcome.target,
    context: browserExecutionContext(cell),
    summary: outcome.summary,
    evidence: outcome.evidence,
    artifacts: [...outcome.artifacts, ...cellArtifacts],
    laneEligibility: outcome.laneEligibility,
    scope: detectorScope(cell, outcome.detector.id),
    violation: outcome.violation,
    ...(outcome.proofConditionMet === undefined
      ? {}
      : { proofConditionMet: outcome.proofConditionMet }),
    ...(outcome.contextDimensions === undefined
      ? {}
      : { contextDimensions: outcome.contextDimensions }),
  };
}

export function createInteractionDetectorContext(input: {
  clockStart: string;
  seed: string;
  capabilities: CapabilityProfile;
  stores: KernelStores;
}): DetectorContext {
  let clockTick = 0;
  return {
    clock: (): string => new Date(Date.parse(input.clockStart) + clockTick++).toISOString(),
    seed: input.seed,
    capabilities: input.capabilities,
    readStores: asReadonlyKernelStores(input.stores),
  };
}

export async function evaluateInteractionEvidence(input: {
  evidence: InteractionCellEvidence;
  cell: MatrixCell;
  detectors: readonly Detector<InteractionCellEvidence, Ui001EvidenceRecord>[];
  detectorContext: DetectorContext;
}): Promise<DetectorOutcome[]> {
  const sortedDetectors = [...input.detectors].sort((left, right) =>
    compare(left.id, right.id),
  );
  const cellArtifacts = cellScreenshotArtifacts(input.evidence);
  const detectorOutcomes: DetectorOutcome[] = [];
  for (const detector of sortedDetectors) {
    const outcomes = await detector.evaluate(input.evidence, input.detectorContext);
    for (const outcome of outcomes) {
      detectorOutcomes.push(toClassifierOutcome(outcome, input.cell, cellArtifacts));
    }
  }
  return detectorOutcomes;
}

export async function classifyInteractionEvidence(input: {
  evidence: InteractionCellEvidence;
  cell: MatrixCell;
  detectors: readonly Detector<InteractionCellEvidence, Ui001EvidenceRecord>[];
  stores: KernelStores;
  runId: string;
  seed: string;
  clockStart: string;
  capabilities: CapabilityProfile;
}): Promise<ClassifiedRun> {
  return classify({
    detectorOutcomes: await evaluateInteractionEvidence({
      evidence: input.evidence,
      cell: input.cell,
      detectors: input.detectors,
      detectorContext: createInteractionDetectorContext(input),
    }),
    harnessEvents: [],
    coverageEvents: [],
    stores: input.stores,
    runId: input.runId,
  });
}

export async function runInteractionMatrix(
  input: RunInteractionMatrixInput,
): Promise<ClassifiedRun> {
  const detectorContext = createInteractionDetectorContext(input);
  const journeyHandledCellIds = new Set(input.plan.journeyHandledCellIds ?? []);

  const sortedCells = [...input.plan.cells].sort((left, right) =>
    compare(left.id, right.id),
  );
  const sortedDetectors = [...input.detectors].sort((left, right) =>
    compare(left.id, right.id),
  );

  const detectorOutcomes: DetectorOutcome[] = [];
  const harnessEvents: HarnessEvent[] = [];
  const interactionScenarioGaps: InteractionCoverageGap[] = [];
  const interactionCoverageEvents: CoverageEvent[] = [];
  const numericObservations: Array<import("./numeric.js").IndependentCoupledNumericObservation> = [];
  const numericCells = new Map<string, MatrixCell>();

  for (const cell of sortedCells) {
    if (journeyHandledCellIds.has(cell.id)) {
      continue;
    }
    const adapter = input.adapters?.[cell.role];
    let result: Awaited<ReturnType<typeof runInteractionCell>>;
    try {
      result = await runInteractionCell({
        cell,
        baseUrl: input.baseUrl,
        browser: input.browser,
        seed: input.seed,
        clockStart: input.clockStart,
        capture: input.capture,
        timeouts: input.timeouts,
        ...(adapter === undefined ? {} : { authAdapter: adapter }),
        ...(input.artifactRunDir === undefined ? {} : { artifactRunDir: input.artifactRunDir }),
        sourceRunId: input.runId,
        ...(input.redactionRules === undefined ? {} : { redactionRules: input.redactionRules }),
        ...(input.interactionDiscovery === undefined ? {} : { interactionDiscovery: input.interactionDiscovery }),
      });
    } catch (error) {
      harnessEvents.push(cellFailureEvent(cell, error));
      continue;
    }

    if (result.kind === "coverage") {
      interactionScenarioGaps.push({
        gap: {
          reason: "unproven-precondition",
          witnessKind: "precondition",
          failedPrecondition: result.failedPrecondition,
        },
        cellId: cell.id,
      });
      continue;
    }

    if (result.kind === "discovery") {
      if (result.discovery.controls.length === 0) {
        interactionScenarioGaps.push({
          gap: {
            reason: "unproven-precondition",
            witnessKind: "precondition",
            failedPrecondition: `interaction-discovery-contract:${cell.id}:none`,
          },
          ...gapEntryContext(cell.id, result.evidence),
        });
      }
      let numericObservationObserved = false;
      for (const observed of result.observations) {
        const { control, oracle } = observed;
        if (oracle.kind === "coverage-gap") {
          const oracleResult = evaluateDiscoveredControlOracle(oracle, {});
          if (oracleResult.kind === "unproven" && oracleResult.coverage !== undefined) {
            interactionCoverageEvents.push(interactionCoverageEvent({
              cell,
              control,
              coverage: oracleResult.coverage,
            }));
          } else {
            interactionScenarioGaps.push({
              gap: {
                reason: "unproven-precondition",
                witnessKind: "precondition",
                failedPrecondition: `interaction-discovery-contract:${cell.id}:${sha256Canonical(controlIdentity(control))}`,
              },
              ...gapEntryContext(cell.id, result.evidence, control),
            });
          }
          continue;
        }
        if (observed.observation === undefined) {
          interactionScenarioGaps.push({
            gap: {
              reason: "unproven-precondition",
              witnessKind: "precondition",
              failedPrecondition: `interaction-oracle-evidence:${cell.id}:${sha256Canonical({ control: controlIdentity(control), oracle })}`,
            },
            ...gapEntryContext(cell.id, result.evidence, control),
          });
          continue;
        }
        const measuredNumericObservations = observed.numericObservations?.filter(isMeasuredCoupledNumericMeasurement) ?? [];
        for (const measurement of observed.numericObservations ?? []) {
          if (measurement.kind !== "missing") continue;
          interactionScenarioGaps.push({
            gap: {
              reason: "unproven-precondition",
              witnessKind: "precondition",
              failedPrecondition: `interaction-coupled-numeric-measurement:${cell.id}:${sha256Canonical({ controlIdentity: measurement.controlIdentity, displayIdentity: measurement.displayIdentity, missingPhase: measurement.missingPhase })}`,
            },
            ...gapEntryContext(cell.id, result.evidence, control),
          });
        }
        if (measuredNumericObservations.length > 0) {
          numericObservationObserved = true;
          numericObservations.push(...measuredNumericObservations.map(({ observation }) => ({ ...observation, cellId: cell.id, role: cell.role, runId: input.runId })));
          numericCells.set(cell.id, cell);
        }
        const oracleResult = evaluateDiscoveredControlOracle(oracle, observed.observation);
        if (oracleResult.kind === "unproven") {
          if (oracleResult.coverage !== undefined) {
            interactionCoverageEvents.push(interactionCoverageEvent({
              cell,
              control,
              coverage: oracleResult.coverage,
            }));
          } else {
            interactionScenarioGaps.push({
              gap: {
                reason: "unproven-precondition",
                witnessKind: "precondition",
                failedPrecondition: `interaction-oracle-reaction:${cell.id}:${sha256Canonical({ control: controlIdentity(control), oracle })}`,
              },
              ...gapEntryContext(cell.id, result.evidence, control),
            });
          }
        }
        if (oracleResult.kind === "finding") {
          detectorOutcomes.push(discoveredOracleOutcome({ cell, control, observation: observed.observation, result: oracleResult }));
        }
      }
      if (result.discovery.controls.length > 0 && !numericObservationObserved) {
        interactionScenarioGaps.push({
          gap: {
            reason: "unproven-precondition",
            witnessKind: "precondition",
            failedPrecondition: `interaction-coupled-numeric-observation:${cell.id}:none`,
          },
          ...gapEntryContext(cell.id, result.evidence),
        });
      }
      continue;
    }

    if (result.kind !== "evidence") {
      await pushHarnessWithPartialEvidence(harnessEvents, {
        outcome: result.outcome,
        cell,
        partialEvidence: result.partialEvidence,
        artifactRunDir: input.artifactRunDir,
        sourceRunId: input.runId,
        redactionRules: input.redactionRules,
      });
      continue;
    }

    input.onCellEvidence?.(result.evidence);

    if (result.evidence.interactionProbe === undefined) {
      interactionScenarioGaps.push({
        gap: {
          reason: "unproven-precondition",
          witnessKind: "precondition",
          failedPrecondition: `interaction-probe:${cell.id}`,
        },
        ...gapEntryContext(cell.id, result.evidence),
      });
      continue;
    }

    detectorOutcomes.push(...await evaluateInteractionEvidence({
      evidence: result.evidence,
      cell,
      detectors: sortedDetectors,
      detectorContext,
    }));
  }

  for (const candidate of stableCoupledNumericCandidates(numericObservations)) {
    const source = candidate.observations.at(0);
    if (source === undefined) throw new Error("stable numeric candidate lacks observation");
    const cell = numericCells.get(source.cellId);
    if (cell === undefined) throw new Error("stable numeric candidate lacks source cell");
    detectorOutcomes.push(coupledNumericOutcome({ candidate, cell }));
  }

  return classify({
    detectorOutcomes,
    harnessEvents,
    coverageEvents: [
      ...coverageEventsFromGaps(
        [
          ...(input.plan.coverageGaps ?? []).map((gap) => ({ gap })),
          ...interactionScenarioGaps,
        ],
        input.plan.cells,
        input.capabilities,
      ),
      ...interactionCoverageEvents,
    ],
    stores: input.stores,
    runId: input.runId,
  });
}
