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.
15 kB · 440 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441// Derived block metadata — the registry, projected. Every downstream view// (a Blockly canvas, a Tier-1 panel, an assumptions sheet, an LLM tool spec)// needs the same facts about a block: which params are editable scalars,// which are variadic scalar arrays, which are complex (edited off-canvas),// each field's kind/label/bounds/default, and the block's family/tier/doc.// `deriveBlockMeta` computes that from a `BlockDef` alone — zod only, no UI// dependency — by walking the block's input schema through `z.toJSONSchema`.//// This is the levelset-side port of src/lib/blockly/generate.ts's schema// walk (walkSchema / leafField / variadic detection), minus all Blockly JSON// assembly. Both sides agree on the ISO-date convention by construction: the// pattern below is the same one blocks/define.ts stamps on `isoDateSchema`.
import { z } from "zod";
import { FAMILY_CARDINALITY, FAMILY_FOLLOWS, FAMILY_ORDER, type FamilyCardinality,} from "./composition.ts";import { blockInstanceSchema } from "./graph.ts";import type { BlockDef, BlockFamily, BlockTier, PanelBinding,} from "./registry.ts";import { lexicographicTierSchema } from "./runConfig.ts";import { DEFAULT_MAX_COMBINATIONS, HARD_MAX_COMBINATIONS, SWEEP_ENABLED_TARGET, sweepSchema,} from "./sweep.ts";import type { BlockDoc } from "./types.ts";import { LEVELSET_VERSION } from "../version.ts";
/** The ISO-date shape blocks stamp on a date param's zod schema — exported so * both the block authors and every projection agree by construction. */export const ISO_DATE_PATTERN = "^\\d{4}-\\d{2}-\\d{2}$";
/** z.int() bounds (±Number.MAX_SAFE_INTEGER) — schema noise, not real caps; * stripped from derived min/max. */const INT_SENTINEL = 9007199254740991;
/** The JSON-Schema subset z.toJSONSchema emits for block input schemas. */interface JsonSchemaNode { type?: string; properties?: Record<string, JsonSchemaNode>; enum?: unknown[]; pattern?: string; default?: unknown; minimum?: number; maximum?: number; exclusiveMinimum?: number; items?: JsonSchemaNode; minItems?: number; maxItems?: number;}
/** Editable scalar-field kinds. Variadic array *elements* can also be plain * text ("text"); a top-level scalar string with no field form stays complex, * so a field is never "text". */export type BlockFieldKind = | "date" | "number" | "slider" | "dropdown" | "checkbox";
/** Slider bounds for a param the registry binds to a slider control. */export interface SliderSpec { min?: number; max?: number; step?: number;}
/** One editable scalar param, flattened to its dot-path. */export interface BlockFieldSpec { /** Dot-path into the block instance's params. */ path: string; kind: BlockFieldKind; /** panelBinding label ?? last path segment. */ label: string; /** defaultShadow value at this path ?? the schema default; omitted when * neither is present. */ defaultValue?: unknown; /** Inclusive numeric bounds, sentinel-stripped. */ min?: number; max?: number; /** True for integer-typed numeric fields. */ integer?: boolean; /** Enum option values (as strings), for dropdown fields. */ options?: string[]; /** Slider bounds, when the registry binds this param to a slider. */ slider?: SliderSpec;}
/** A scalar-array param rendered as indexed plus/minus rows, mirroring * VariadicParam verbatim — object arrays (modes, phases, limits) have no * field form and stay complex. */export interface VariadicParamSpec { /** Dot-path of the array param itself. */ path: string; label: string; itemKind: "number" | "text"; integer: boolean; /** Per-element numeric bounds (same derivation as scalar fields). */ min?: number; max?: number; minItems: number; maxItems?: number; /** Value for an element added to an empty array (copy-last wins when the * array is non-empty), chosen inside the element bounds. */ emptySeed: number | string; /** Element values a fresh block carries (defaultShadow, else schema * default). */ defaultValues: readonly (number | string)[];}
/** A block's registry facts plus its flattened param taxonomy. */export interface DerivedBlockMeta { code: string; family: BlockFamily; tier: BlockTier; doc: BlockDoc; defaultShadow: unknown; assumptionTemplate?: string; panelBindings?: PanelBinding[]; projectAnchoredPaths?: string[]; /** The block's input schema as JSON Schema (io: "input"). */ inputSchema: unknown; /** Editable scalar params, in display order. */ fields: BlockFieldSpec[]; /** Scalar-array params surfaced as plus/minus rows. */ variadic: VariadicParamSpec[]; /** Params with no field form (object arrays, bare scalar strings). */ complexParams: string[];}
/** The committed dump: engine version, format version, composition grammar, * and every registered block's derived meta. */export interface BlockMetaDump { levelsetVersion: string; formatVersion: 1; composition: { order: readonly BlockFamily[]; follows: Record<BlockFamily, readonly BlockFamily[]>; cardinality: Record<BlockFamily, FamilyCardinality>; }; /** Framework-owned authoring shapes shared by every canvas. Block-specific * params continue to come from each entry in `blocks`. */ authoring: { instance: { /** Graph-instance keys that cannot also be block param names. */ reservedParamNames: readonly string[]; /** Runtime meaning of an omitted optional `enabled` property. */ enabledWhenOmitted: true; schema: unknown; }; sweep: { /** Special target path that toggles the whole block instance. */ enabledTarget: typeof SWEEP_ENABLED_TARGET; defaultMaxCombinations: number; hardMaxCombinations: number; schema: unknown; }; objective: { /** One entry in objective.lexicographic: a scorer id or weighted tier. */ lexicographicTierSchema: unknown; }; }; blocks: DerivedBlockMeta[];}
function authoringMeta(): BlockMetaDump["authoring"] { const instanceSchema = z.toJSONSchema(blockInstanceSchema, { io: "input", }) as JsonSchemaNode; const reservedParamNames = Object.keys( instanceSchema.properties ?? {}, ).filter((name) => name !== "instanceId" && name !== "params"); return { instance: { reservedParamNames, enabledWhenOmitted: true, schema: instanceSchema, }, sweep: { enabledTarget: SWEEP_ENABLED_TARGET, defaultMaxCombinations: DEFAULT_MAX_COMBINATIONS, hardMaxCombinations: HARD_MAX_COMBINATIONS, schema: z.toJSONSchema(sweepSchema, { io: "input" }), }, objective: { lexicographicTierSchema: z.toJSONSchema(lexicographicTierSchema, { io: "input", }), }, };}
function resolveDotPath(value: unknown, path: string): unknown { let current = value; for (const segment of path.split(".")) { if (current === null || typeof current !== "object") return undefined; current = (current as Record<string, unknown>)[segment]; } return current;}
/** Inclusive lower bound for a numeric field; sentinel and exclusive bounds * normalized the way fieldMin does (integer exclusive → +1). */function fieldMin(node: JsonSchemaNode, integer: boolean): number | undefined { if (node.minimum !== undefined && node.minimum !== -INT_SENTINEL) { return node.minimum; } if (node.exclusiveMinimum !== undefined) { return integer ? node.exclusiveMinimum + 1 : node.exclusiveMinimum; } return undefined;}
function fieldMax(node: JsonSchemaNode): number | undefined { if (node.maximum !== undefined && node.maximum !== INT_SENTINEL) { return node.maximum; } return undefined;}
/** A scalar leaf schema node → BlockFieldSpec, or null when it has no field * form (a bare scalar string, or any type z.toJSONSchema doesn't reduce to * a known field kind). */function leafField( path: string, node: JsonSchemaNode, shadowValue: unknown, binding: PanelBinding | undefined,): BlockFieldSpec | null { const label = binding?.label ?? path.split(".").at(-1) ?? path; const defaultValue = shadowValue ?? node.default; const withDefault = defaultValue !== undefined ? { defaultValue } : {};
if (node.type === "string" && node.pattern === ISO_DATE_PATTERN) { return { path, kind: "date", label, ...withDefault }; } if (node.enum !== undefined) { return { path, kind: "dropdown", label, options: node.enum.map((value) => String(value)), ...withDefault, }; } if (node.type === "integer" || node.type === "number") { const integer = node.type === "integer"; const min = fieldMin(node, integer); const max = fieldMax(node); const bounds = { ...(min !== undefined ? { min } : {}), ...(max !== undefined ? { max } : {}), integer, }; if (binding?.control === "slider") { const options = (binding.options ?? {}) as { min?: number; max?: number; step?: number; }; const sliderMin = options.min ?? min; const sliderMax = options.max ?? max; const slider: SliderSpec = { ...(sliderMin !== undefined ? { min: sliderMin } : {}), ...(sliderMax !== undefined ? { max: sliderMax } : {}), ...(options.step !== undefined ? { step: options.step } : {}), }; return { path, kind: "slider", label, ...bounds, slider, ...withDefault }; } return { path, kind: "number", label, ...bounds, ...withDefault }; } if (node.type === "boolean") { return { path, kind: "checkbox", label, ...withDefault }; } return null;}
/** A scalar-array schema node → VariadicParamSpec, or null when the element * type has no field form (object arrays, enum arrays). */function variadicParam( path: string, node: JsonSchemaNode, shadowValue: unknown, binding: PanelBinding | undefined,): VariadicParamSpec | null { const items = node.items; if (items === undefined || items.enum !== undefined) return null; const label = binding?.label ?? path.split(".").at(-1) ?? path; const minItems = node.minItems ?? 0; const defaults = Array.isArray(shadowValue) ? shadowValue : Array.isArray(node.default) ? node.default : undefined; if (items.type === "integer" || items.type === "number") { const integer = items.type === "integer"; const min = fieldMin(items, integer); const max = fieldMax(items); if (defaults === undefined || defaults.some((v) => typeof v !== "number")) { return null; } return { path, label, itemKind: "number", integer, ...(min !== undefined ? { min } : {}), ...(max !== undefined ? { max } : {}), minItems, ...(node.maxItems !== undefined ? { maxItems: node.maxItems } : {}), emptySeed: min ?? 0, defaultValues: defaults as number[], }; } if (items.type === "string" && items.pattern === undefined) { if (defaults === undefined || defaults.some((v) => typeof v !== "string")) { return null; } return { path, label, itemKind: "text", integer: false, minItems, ...(node.maxItems !== undefined ? { maxItems: node.maxItems } : {}), emptySeed: "", defaultValues: defaults as string[], }; } return null;}
/** Flatten a block input schema into scalar fields, variadic scalar-array * params, and complex (field-less) params. Nested objects flatten into * dot-path fields. */function walkSchema( def: BlockDef, node: JsonSchemaNode, prefix: string, fields: BlockFieldSpec[], variadic: VariadicParamSpec[], complex: string[],): void { for (const [key, child] of Object.entries(node.properties ?? {})) { const path = prefix === "" ? key : `${prefix}.${key}`; if (child.type === "object" && child.properties !== undefined) { walkSchema(def, child, path, fields, variadic, complex); continue; } const binding = def.panelBindings?.find((b) => b.paramPath === path); const shadowValue = resolveDotPath(def.defaultShadow, path); if (child.type === "array") { const param = variadicParam(path, child, shadowValue, binding); if (param !== null) { variadic.push(param); } else { complex.push(path); } continue; } const leaf = leafField(path, child, shadowValue, binding); if (leaf !== null) { fields.push(leaf); } else { complex.push(path); } }}
/** Derive one block's metadata from its `BlockDef`. Pure; zod only. An * unrepresentable input schema degrades to a label-only block (empty * fields/variadic/complex) rather than throwing — the meta stays valid and * the block stays editable off-canvas. */export function deriveBlockMeta(def: BlockDef): DerivedBlockMeta { const fields: BlockFieldSpec[] = []; const variadic: VariadicParamSpec[] = []; const complex: string[] = []; let inputSchema: unknown = { type: "object" }; try { const schema = z.toJSONSchema(def.block.schema.input, { io: "input", }) as JsonSchemaNode; inputSchema = schema; walkSchema(def, schema, "", fields, variadic, complex); } catch { fields.length = 0; variadic.length = 0; complex.length = 0; }
return { code: def.code, family: def.family, tier: def.tier, doc: def.block.doc, defaultShadow: def.defaultShadow, ...(def.assumptionTemplate !== undefined ? { assumptionTemplate: def.assumptionTemplate } : {}), ...(def.panelBindings !== undefined ? { panelBindings: def.panelBindings } : {}), ...(def.projectAnchoredPaths !== undefined ? { projectAnchoredPaths: def.projectAnchoredPaths } : {}), inputSchema, fields, variadic, complexParams: complex, };}
/** Dump the whole registry to a serializable metadata document: engine * version, format version, the composition grammar, and every block's * derived meta in registry order. This is the build-time artifact * scripts/generate-block-meta.ts writes to generated/block-meta.json. */export function dumpBlockMeta(registry: { list(): BlockDef[] }): BlockMetaDump { return { levelsetVersion: LEVELSET_VERSION, formatVersion: 1, composition: { order: FAMILY_ORDER, follows: FAMILY_FOLLOWS, cardinality: FAMILY_CARDINALITY, }, authoring: authoringMeta(), blocks: registry.list().map(deriveBlockMeta), };}