Something went wrong. Try again.
A functional toolkit for authoring resource-leveling algorithms — compose constraint and scoring blocks, enumerate feasible schedules
Something went wrong. Try again.
16 kB · 492 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493// Declarative run configs: fill (defaults expanded, version stamped),// runFromConfig end-to-end, weights, enabled levers, and coded failures.
import { describe, expect, test } from "bun:test";import { z } from "zod";
import { GraphValidationError } from "../../src/level-blocks/compile.ts";import type { BlockDef } from "../../src/level-blocks/registry.ts";import { fillRunConfig, runFromConfig,} from "../../src/level-blocks/runConfig.ts";import { STANDARD_CODES, standardRegistry,} from "../../src/level-blocks/standard.ts";import { RunAnalysisError } from "../../src/level-core/errors.ts";import { LEVELSET_VERSION } from "../../src/version.ts";
import { MON_JAN_5, SAT_JAN_10, makeAssignment, makeProject, makeResource, makeTask,} from "../level-core/fixtures.ts";
function crewProject() { const t1 = makeTask({ uniqueId: 1, start: MON_JAN_5, finish: SAT_JAN_10 }); const t2 = makeTask({ uniqueId: 2, start: MON_JAN_5, finish: SAT_JAN_10 }); return makeProject([t1, t2], { resources: [makeResource(100, "Crew")], assignments: [makeAssignment(1, 100), makeAssignment(2, 100)], });}
interface TestBlock { instanceId: string; code: string; params: Record<string, unknown>; enabled?: boolean;}
function baseConfig(): { formatVersion: 1; pipeline: { formatVersion: 1; blocks: TestBlock[] };} { return { formatVersion: 1, pipeline: { formatVersion: 1, blocks: [ { instanceId: "crew-cap", code: STANDARD_CODES.maxConcurrentResource, params: { resourceUniqueId: 100, max: 1 }, }, { instanceId: "wip-cost", code: STANDARD_CODES.concurrentResourceCost, params: { resourceUniqueId: 100, basePrice: 10, growthBase: 2 }, }, { instanceId: "makespan", code: STANDARD_CODES.makespan, params: {} }, { instanceId: "search", code: STANDARD_CODES.serialSGS, params: {} }, ], }, };}
describe("fillRunConfig", () => { test("expands block-schema defaults and stamps the levelset version", () => { const filled = fillRunConfig(baseConfig()); expect(filled.levelset).toBe(LEVELSET_VERSION); const wipCost = filled.pipeline.blocks.find( (b) => b.instanceId === "wip-cost", )!; // threshold has a schema default of 0 the input config never mentioned. expect(wipCost.params).toEqual({ resourceUniqueId: 100, basePrice: 10, growthBase: 2, threshold: 0, }); // Instance-level defaults fill too. expect(wipCost.provenance).toBe("curated"); });
test("rejects unknown top-level keys (strict framework sections)", () => { expect(() => fillRunConfig({ ...baseConfig(), wieghts: {} })).toThrow(); });
test("rejects weights keyed by non-scorer instances", () => { expect(() => fillRunConfig({ ...baseConfig(), weights: { "crew-cap": 1 } }), ).toThrow(GraphValidationError); });});
describe("runFromConfig", () => { test("builds and runs a full pipeline from one JSON document", async () => { const run = runFromConfig(crewProject(), baseConfig()); expect(run.search.name).toBe("serial-SGS"); // instanceIds double as constraint names for attribution. expect(run.constraints.map((c) => c.name)).toEqual(["crew-cap"]); expect(run.analysis.ok).toBe(true);
const best = await run.best(); expect(best).not.toBeNull(); // crew-cap max 1 forces serialization. const starts = best!.tasks.map((t) => t.startDay).sort((a, b) => a - b); expect(starts[1]).toBeGreaterThan(0);
const scores = run.scores(best!); expect(Object.keys(scores).sort()).toEqual(["makespan", "wip-cost"]); expect(scores["makespan"]).toBe(best!.makespan);
const project = run.materialize(best!); expect(project.tasks).toHaveLength(2); });
test("null weight means reported but not optimized", () => { const run = runFromConfig(crewProject(), { ...baseConfig(), weights: { "wip-cost": null, makespan: 1 }, }); expect( run.scorers.find((s) => s.instanceId === "wip-cost")!.weight, ).toBeNull(); expect(run.objective.name).toBe("objective"); });
test("a disabled constraint block compiles to a disabled constraint", () => { const config = baseConfig(); config.pipeline.blocks[0] = { ...config.pipeline.blocks[0]!, enabled: false, }; const run = runFromConfig(crewProject(), config); expect(run.constraints[0]!.enabled).toBe(false); });
test("a disabled constraint that would fail analysis does not block the run", async () => { // Disabling an infeasible deadline is the ablation workflow: the run // must build and schedule, not throw RunAnalysisError. const config = baseConfig(); config.pipeline.blocks.push({ instanceId: "impossible-gate", code: STANDARD_CODES.deadline, params: { taskUniqueId: 1, latestFinish: 0 }, enabled: false, }); const run = runFromConfig(crewProject(), config); expect(run.analysis.ok).toBe(true); expect(await run.best()).not.toBeNull(); });
test("a PhaseRelease instance expands to per-task releases", async () => { const config = baseConfig(); config.pipeline.blocks.unshift({ instanceId: "phase-release", code: STANDARD_CODES.phaseRelease, params: { taskUniqueIds: [1, 2], earliestStart: 3 }, }); const run = runFromConfig(crewProject(), config); expect( run.constraints.slice(0, 2).map((constraint) => constraint.name), ).toEqual(["phase-release[0]", "phase-release[1]"]); const best = await run.best(); expect(best!.tasks.every((task) => task.startDay >= 3)).toBe(true); });
test("filled config round-trips: running it again reproduces the schedule", async () => { const run1 = runFromConfig(crewProject(), baseConfig()); const best1 = await run1.best(); const run2 = runFromConfig(crewProject(), run1.config); const best2 = await run2.best(); expect(best2!.tasks).toEqual(best1!.tasks); expect(run2.config).toEqual(run1.config); });
test("lexicographic objective drives best() and validates its references", async () => { const run = runFromConfig(crewProject(), { ...baseConfig(), objective: { lexicographic: ["makespan", "wip-cost"] }, }); const best = await run.best(); expect(best).not.toBeNull();
expect(() => runFromConfig(crewProject(), { ...baseConfig(), objective: { lexicographic: ["crew-cap"] }, }), ).toThrow(GraphValidationError);
const config = baseConfig(); config.pipeline.blocks[1] = { ...config.pipeline.blocks[1]!, enabled: false, }; expect(() => runFromConfig(crewProject(), { ...config, objective: { lexicographic: ["wip-cost"] }, }), ).toThrow(/disabled scorer/); });
test("a lexicographic tier may be a weighted sum of scorers", async () => { const run = runFromConfig(crewProject(), { ...baseConfig(), objective: { lexicographic: [{ weightedSum: { makespan: 2, "wip-cost": 0.5 } }], }, }); expect(await run.best()).not.toBeNull();
expect(() => runFromConfig(crewProject(), { ...baseConfig(), objective: { lexicographic: [{ weightedSum: { "crew-cap": 1 } }] }, }), ).toThrow(GraphValidationError);
const disabled = baseConfig(); disabled.pipeline.blocks[1] = { ...disabled.pipeline.blocks[1]!, enabled: false, }; expect(() => runFromConfig(crewProject(), { ...disabled, objective: { lexicographic: [{ weightedSum: { "wip-cost": 1 } }] }, }), ).toThrow(/disabled scorer/); });
test("weighted lexicographic tiers reject empty and all-zero sums", () => { expect(() => fillRunConfig({ ...baseConfig(), objective: { lexicographic: [{ weightedSum: {} }] }, }), ).toThrow(/at least one scorer/); expect(() => fillRunConfig({ ...baseConfig(), objective: { lexicographic: [{ weightedSum: { makespan: 0 } }] }, }), ).toThrow(/non-zero weight/); });
test("collectors run against the search stream, labeled by instanceId", async () => { const config = baseConfig(); config.pipeline.blocks.push( { instanceId: "top3", code: STANDARD_CODES.kBest, params: { k: 3, by: ["makespan", "wip-cost"] }, }, { instanceId: "frontier", code: STANDARD_CODES.pareto, params: { over: ["makespan", "wip-cost"] }, }, ); const run = runFromConfig(crewProject(), config); const results = await run.collectors(); expect(results.map((r) => r.instanceId).sort()).toEqual([ "frontier", "top3", ]);
const kbest = results.find((r) => r.instanceId === "top3")!; const pareto = results.find((r) => r.instanceId === "frontier")!; expect(kbest.kind).toBe("KBest"); expect(pareto.kind).toBe("Pareto"); // serialSGS emits a single schedule, so both collectors select it — // KBest of a 1-schedule stream is that schedule; it is the Pareto set too. const best = await run.best(); expect(kbest.schedules).toHaveLength(1); expect(kbest.schedules[0]!.tasks).toEqual(best!.tasks); expect(pareto.schedules).toHaveLength(1); expect(pareto.schedules[0]!.tasks).toEqual(best!.tasks); });
test("a run with no collectors returns an empty collector set", async () => { const run = runFromConfig(crewProject(), baseConfig()); expect(await run.collectors()).toEqual([]); });
test("collector params round-trip through fillRunConfig untouched", () => { const config = baseConfig(); config.pipeline.blocks.push({ instanceId: "top3", code: STANDARD_CODES.kBest, params: { k: 3, by: ["makespan"] }, }); const filled = fillRunConfig(config); const kbest = filled.pipeline.blocks.find((b) => b.instanceId === "top3")!; expect(kbest.params).toEqual({ k: 3, by: ["makespan"] }); });
test("a disabled collector does not run", async () => { const config = baseConfig(); config.pipeline.blocks.push({ instanceId: "top3", code: STANDARD_CODES.kBest, params: { k: 3, by: ["makespan"] }, enabled: false, }); const run = runFromConfig(crewProject(), config); expect(await run.collectors()).toEqual([]); });
test("a collector referencing a non-scorer instance fails validation", () => { const config = baseConfig(); config.pipeline.blocks.push({ instanceId: "bad", code: STANDARD_CODES.kBest, // "crew-cap" is a constraint instance, not a scorer. params: { k: 2, by: ["crew-cap"] }, }); expect(() => runFromConfig(crewProject(), config)).toThrow( GraphValidationError, ); });
test("a collector referencing a missing instance fails validation", () => { const config = baseConfig(); config.pipeline.blocks.push({ instanceId: "bad", code: STANDARD_CODES.pareto, params: { over: ["nope"] }, }); expect(() => runFromConfig(crewProject(), config)).toThrow( GraphValidationError, ); });
// A custom collector whose input carries scorer refs under a param name // other than by/over: its apply() maps `scorers` into the KBest `by` field. // fillRunConfig must extract the ref off this compiled spec (not the param // shape), so a dangling ref is a GraphValidationError — not a silent pass // (the old duck-typing missed non-by/over params) and not a raw TypeError. const pickByDef: BlockDef = { code: "myapp.PickBy.v1", family: "collector", tier: "engineer", block: { id: "PickBy", schema: { input: z.object({ scorers: z.array(z.string()).min(1) }), output: z.object({ kind: z.literal("KBest"), k: z.number(), by: z.array(z.string()), }), }, apply: ({ scorers }: { scorers: string[] }) => ({ kind: "KBest", k: 1, by: scorers, }), toMiniZinc: () => ({ text: "% pick" }), doc: { nl: "pick", pseudocode: "pick" }, }, defaultShadow: { scorers: ["makespan"] }, };
test("a custom collector's refs are validated off the compiled spec, not the param name", () => { const registry = standardRegistry([pickByDef]);
const dangling = baseConfig(); dangling.pipeline.blocks.push({ instanceId: "pick", code: "myapp.PickBy.v1", params: { scorers: ["ghost"] }, }); expect(() => fillRunConfig(dangling, registry)).toThrow( GraphValidationError, );
const valid = baseConfig(); valid.pipeline.blocks.push({ instanceId: "pick", code: "myapp.PickBy.v1", params: { scorers: ["makespan"] }, }); expect(() => fillRunConfig(valid, registry)).not.toThrow(); });
test("a collector referencing a disabled scorer fails validation", () => { const config = baseConfig(); // Disable the wip-cost scorer, then reference it from a collector. config.pipeline.blocks[1] = { ...config.pipeline.blocks[1]!, enabled: false, }; config.pipeline.blocks.push({ instanceId: "top3", code: STANDARD_CODES.kBest, params: { k: 2, by: ["wip-cost"] }, }); expect(() => runFromConfig(crewProject(), config)).toThrow( /disabled scorer/, ); });
test("a Gate collector filters the stream and honors its fallback", async () => { // serialSGS emits one schedule; learn its makespan to set gate bounds. const m = (await runFromConfig(crewProject(), baseConfig()).best())! .makespan;
const withGate = (max: number, fallback: "leastBad" | "none") => { const config = baseConfig(); config.pipeline.blocks.push({ instanceId: "gate", code: STANDARD_CODES.gate, params: { limits: [{ scorer: "makespan", max }], fallback }, }); return runFromConfig(crewProject(), config).collectors(); };
// makespan <= m admits the one schedule. const pass = await withGate(m, "none"); expect(pass).toHaveLength(1); expect(pass[0]!.kind).toBe("Gate"); expect(pass[0]!.schedules).toHaveLength(1);
// makespan <= m-1 admits nothing: "none" empties, "leastBad" keeps one. expect((await withGate(m - 1, "none"))[0]!.schedules).toEqual([]); expect((await withGate(m - 1, "leastBad"))[0]!.schedules).toHaveLength(1); });
test("a Gate referencing a non-scorer instance fails validation", () => { const config = baseConfig(); config.pipeline.blocks.push({ instanceId: "bad-gate", // "crew-cap" is a constraint instance, not a scorer. code: STANDARD_CODES.gate, params: { limits: [{ scorer: "crew-cap", max: 5 }], fallback: "none" }, }); expect(() => runFromConfig(crewProject(), config)).toThrow( GraphValidationError, ); });
test("analysis errors throw RunAnalysisError with coded issues", () => { const config = baseConfig(); config.pipeline.blocks[0] = { instanceId: "ghost-cap", code: STANDARD_CODES.maxConcurrentResource, params: { resourceUniqueId: 999, max: 1 }, }; let caught: unknown; try { runFromConfig(crewProject(), config); } catch (e) { caught = e; } expect(caught).toBeInstanceOf(RunAnalysisError); const issues = (caught as RunAnalysisError).issues; expect( issues.some( (i) => i.code === "LEVELSET_E101" && i.source === "ghost-cap", ), ).toBe(true); });
test("unknown block codes fail graph validation", () => { const config = baseConfig(); config.pipeline.blocks[0] = { instanceId: "x", code: "levelset.DoesNotExist.v1", params: {}, }; expect(() => runFromConfig(crewProject(), config)).toThrow( GraphValidationError, ); });
test("resolve options flow through (workUnits become resolvable refs)", () => { const config = { ...baseConfig(), resolve: { workUnits: [{ id: 10, taskUniqueIds: [1, 2] }] }, }; const run = runFromConfig(crewProject(), config); expect(run.resolved.workUnits.get(10)).toBeDefined(); });});