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.
4.2 kB · 100 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101// Family composition rules as registry data — the grammar that says how the// four block families stack into a pipeline. The formatVersion-1 graph is a// flat SET (validateGraph is order-insensitive), so these rules encode a// canonical *reading* order — constraints, then scorers, then the search// strategy, then collectors — plus per-family cardinality, rather than// data-flow wiring.//// This is the levelset-side source of truth that a canvas connection checker// (src/lib/blockly/connections.ts) mirrors visually: a canvas must not be able// to author a stack these rules reject, and `validateGraph` enforces the// cardinality half. Pure data + pure predicates, no Blockly import — the meta// dump ships `{ order, follows, cardinality }` straight from here.
import type { BlockFamily } from "./registry.ts";
/** Canonical band order — the reading order a stack is laid out in. */export const FAMILY_ORDER: readonly BlockFamily[] = [ "constraint", "scorer", "search", "collector",];
/** Families allowed to sit directly below each family in a stack. The * strict-checker reading (null never matches non-null, otherwise * intersection) buys the invariants: a scorer can't sit above a constraint, * only collectors follow the search, and collectors attach below the search * or another collector. */export const FAMILY_FOLLOWS: Record<BlockFamily, readonly BlockFamily[]> = { constraint: ["constraint", "scorer", "search"], scorer: ["scorer", "search"], search: ["collector"], collector: ["collector"],};
/** How many instances of each family a valid graph may hold. `max: null` is * unbounded. Today only `search` is bounded (exactly one); the rest are * open. `validateGraph` loops over this rather than hard-coding the * one-search rule, so a future bound (e.g. exactly-one-objective) is a data * edit here, not a control-flow change there. */export interface FamilyCardinality { readonly min: number; readonly max: number | null;}export const FAMILY_CARDINALITY: Record<BlockFamily, FamilyCardinality> = { constraint: { min: 0, max: null }, scorer: { min: 0, max: null }, search: { min: 1, max: 1 }, collector: { min: 0, max: null },};
/** The pure grammar predicate the family bands encode: may a block of family * `below` connect directly beneath a block of family `above`? */export function canFollow(above: BlockFamily, below: BlockFamily): boolean { return FAMILY_FOLLOWS[above].includes(below);}
/** * Stable family sort into canonical band order. `validateGraph` treats the * block list as a set, so stored graphs may carry any order; sorting before * stacking guarantees every adjacent pair satisfies `canFollow` whenever the * graph holds at most one search block — i.e. every valid stored graph reads * as one connected stack. Unknown families sink to the end, first-seen order * preserved within a band. */export function sortIntoBandOrder<T>( items: readonly T[], familyOf: (item: T) => BlockFamily | undefined,): T[] { const rank = (item: T): number => { const family = familyOf(item); return family === undefined ? FAMILY_ORDER.length : FAMILY_ORDER.indexOf(family); }; return [...items] .map((item, index) => ({ item, index })) .sort((a, b) => rank(a.item) - rank(b.item) || a.index - b.index) .map(({ item }) => item);}
/** A cardinality violation phrased for a graph issue — kept next to the data * so the search wording ("Expected exactly one search-family block, found N") * stays stable as the rule generalizes. */export function cardinalityMessage( family: BlockFamily, card: FamilyCardinality, count: number,): string { let expectation: string; if (card.max === card.min) { expectation = `exactly ${card.min === 1 ? "one" : String(card.min)}`; } else if (card.max === null) { expectation = `at least ${String(card.min)}`; } else { expectation = `between ${String(card.min)} and ${String(card.max)}`; } const plural = card.min === 1 && card.max === 1 ? "" : "s"; return `Expected ${expectation} ${family}-family block${plural}, found ${String(count)}`;}