// KBest — Collector block. // // Keeps the best `k` schedules out of a candidate stream, ranked // lexicographically over the referenced scorer instances: the second scorer // only breaks ties on the first, and so on (order matters). Unlike a // Scorer, which ranks a single schedule, a Collector selects a *set* — the // run consumption path (`LevelingRun.collectors()`) executes it. // // The `by` entries are scorer instanceIds from the same pipeline, not Scorer // closures: they resolve to compiled scorers only at run-wiring time (as // `objective.lexicographic` does), so the emitted spec stays plain JSON. // // Collectors are a selection layer above the solver, so `toMiniZinc` emits // an explanatory comment rather than a constraint fragment. import { z } from "zod"; import type { KBestCollector } from "../level-core/types.ts"; import type { CollectorBlock, MiniZincFragment } from "./types.ts"; export const KBestInputSchema = z.object({ k: z.number().int().positive(), /** Scorer instanceIds in tie-break priority order; at least one. */ by: z.array(z.string()).min(1), }); export type KBestInput = z.infer; const KBestOutputSchema: z.ZodType = z.object({ kind: z.literal("KBest"), k: z.number().int().positive(), by: z.array(z.string()).min(1), }); export const KBestBlock: CollectorBlock = { id: "KBest", schema: { input: KBestInputSchema, output: KBestOutputSchema }, apply: ({ k, by }): KBestCollector => ({ kind: "KBest", k, by }), toMiniZinc: ({ k }): MiniZincFragment => ({ text: `% KBest(k=${String(k)}) — selection layer; keeps the best k schedules, emits no solver constraint.`, }), doc: { nl: "Keeps the best k schedules, ranked lexicographically over the referenced scorers (order matters).", pseudocode: "sort(candidates, lex(by)).slice(0, k)", }, };