A functional toolkit for authoring resource-leveling algorithms — compose constraint and scoring blocks, enumerate feasible schedules
0

Configure Feed

Select the types of activity you want to include in your feed.

feat(level-blocks): derived block metadata (deriveBlockMeta/dumpBlockMeta)

Port a zod→JSON-Schema walk into src/level-blocks/meta.ts, minus all
Blockly assembly: deriveBlockMeta flattens a block's input schema into
editable scalar fields (date/number/slider/dropdown/checkbox with
labels/bounds/defaults), variadic scalar-array params, and complex
(field-less) params, alongside the block's family/tier/doc/shadow and its
JSON-Schema. dumpBlockMeta wraps the whole registry with the composition
grammar. Exports the ISO-date pattern both sides agree on by construction.

Zod-only, exported from levelset/leveling.

Russ T. Fugal (Jul 16, 2026, 4:21 PM -0600) e93da3f4 ff991b2f

+565
+364
src/level-blocks/meta.ts
··· 1 + // Derived block metadata — the registry, projected. Every downstream view 2 + // (a Blockly canvas, a Tier-1 panel, an assumptions sheet, an LLM tool spec) 3 + // needs the same facts about a block: which params are editable scalars, 4 + // which are variadic scalar arrays, which are complex (edited off-canvas), 5 + // each field's kind/label/bounds/default, and the block's family/tier/doc. 6 + // `deriveBlockMeta` computes that from a `BlockDef` alone — zod only, no UI 7 + // dependency — by walking the block's input schema through `z.toJSONSchema`. 8 + // 9 + // This is the levelset-side port of src/lib/blockly/generate.ts's schema 10 + // walk (walkSchema / leafField / variadic detection), minus all Blockly JSON 11 + // assembly. Both sides agree on the ISO-date convention by construction: the 12 + // pattern below is the same one blocks/define.ts stamps on `isoDateSchema`. 13 + 14 + import { z } from "zod"; 15 + 16 + import { 17 + FAMILY_CARDINALITY, 18 + FAMILY_FOLLOWS, 19 + FAMILY_ORDER, 20 + type FamilyCardinality, 21 + } from "./composition.ts"; 22 + import type { BlockDef, BlockFamily, BlockTier, PanelBinding } from "./registry.ts"; 23 + import type { BlockDoc } from "./types.ts"; 24 + import { LEVELSET_VERSION } from "../version.ts"; 25 + 26 + /** The ISO-date shape blocks stamp on a date param's zod schema — exported so 27 + * both the block authors and every projection agree by construction. */ 28 + export const ISO_DATE_PATTERN = "^\\d{4}-\\d{2}-\\d{2}$"; 29 + 30 + /** z.int() bounds (±Number.MAX_SAFE_INTEGER) — schema noise, not real caps; 31 + * stripped from derived min/max. */ 32 + const INT_SENTINEL = 9007199254740991; 33 + 34 + /** The JSON-Schema subset z.toJSONSchema emits for block input schemas. */ 35 + interface JsonSchemaNode { 36 + type?: string; 37 + properties?: Record<string, JsonSchemaNode>; 38 + enum?: unknown[]; 39 + pattern?: string; 40 + default?: unknown; 41 + minimum?: number; 42 + maximum?: number; 43 + exclusiveMinimum?: number; 44 + items?: JsonSchemaNode; 45 + minItems?: number; 46 + maxItems?: number; 47 + } 48 + 49 + /** Editable scalar-field kinds. Variadic array *elements* can also be plain 50 + * text ("text"); a top-level scalar string with no field form stays complex, 51 + * so a field is never "text". */ 52 + export type BlockFieldKind = "date" | "number" | "slider" | "dropdown" | "checkbox"; 53 + 54 + /** Slider bounds for a param the registry binds to a slider control. */ 55 + export interface SliderSpec { 56 + min?: number; 57 + max?: number; 58 + step?: number; 59 + } 60 + 61 + /** One editable scalar param, flattened to its dot-path. */ 62 + export interface BlockFieldSpec { 63 + /** Dot-path into the block instance's params. */ 64 + path: string; 65 + kind: BlockFieldKind; 66 + /** panelBinding label ?? last path segment. */ 67 + label: string; 68 + /** defaultShadow value at this path ?? the schema default; omitted when 69 + * neither is present. */ 70 + defaultValue?: unknown; 71 + /** Inclusive numeric bounds, sentinel-stripped. */ 72 + min?: number; 73 + max?: number; 74 + /** True for integer-typed numeric fields. */ 75 + integer?: boolean; 76 + /** Enum option values (as strings), for dropdown fields. */ 77 + options?: string[]; 78 + /** Slider bounds, when the registry binds this param to a slider. */ 79 + slider?: SliderSpec; 80 + } 81 + 82 + /** A scalar-array param rendered as indexed plus/minus rows, mirroring 83 + * VariadicParam verbatim — object arrays (modes, phases, limits) have no 84 + * field form and stay complex. */ 85 + export interface VariadicParamSpec { 86 + /** Dot-path of the array param itself. */ 87 + path: string; 88 + label: string; 89 + itemKind: "number" | "text"; 90 + integer: boolean; 91 + /** Per-element numeric bounds (same derivation as scalar fields). */ 92 + min?: number; 93 + max?: number; 94 + minItems: number; 95 + maxItems?: number; 96 + /** Value for an element added to an empty array (copy-last wins when the 97 + * array is non-empty), chosen inside the element bounds. */ 98 + emptySeed: number | string; 99 + /** Element values a fresh block carries (defaultShadow, else schema 100 + * default). */ 101 + defaultValues: readonly (number | string)[]; 102 + } 103 + 104 + /** A block's registry facts plus its flattened param taxonomy. */ 105 + export interface DerivedBlockMeta { 106 + code: string; 107 + family: BlockFamily; 108 + tier: BlockTier; 109 + doc: BlockDoc; 110 + defaultShadow: unknown; 111 + assumptionTemplate?: string; 112 + panelBindings?: PanelBinding[]; 113 + projectAnchoredPaths?: string[]; 114 + /** The block's input schema as JSON Schema (io: "input"). */ 115 + inputSchema: unknown; 116 + /** Editable scalar params, in display order. */ 117 + fields: BlockFieldSpec[]; 118 + /** Scalar-array params surfaced as plus/minus rows. */ 119 + variadic: VariadicParamSpec[]; 120 + /** Params with no field form (object arrays, bare scalar strings). */ 121 + complexParams: string[]; 122 + } 123 + 124 + /** The committed dump: engine version, format version, composition grammar, 125 + * and every registered block's derived meta. */ 126 + export interface BlockMetaDump { 127 + levelsetVersion: string; 128 + formatVersion: 1; 129 + composition: { 130 + order: readonly BlockFamily[]; 131 + follows: Record<BlockFamily, readonly BlockFamily[]>; 132 + cardinality: Record<BlockFamily, FamilyCardinality>; 133 + }; 134 + blocks: DerivedBlockMeta[]; 135 + } 136 + 137 + function resolveDotPath(value: unknown, path: string): unknown { 138 + let current = value; 139 + for (const segment of path.split(".")) { 140 + if (current === null || typeof current !== "object") return undefined; 141 + current = (current as Record<string, unknown>)[segment]; 142 + } 143 + return current; 144 + } 145 + 146 + /** Inclusive lower bound for a numeric field; sentinel and exclusive bounds 147 + * normalized the way fieldMin does (integer exclusive → +1). */ 148 + function fieldMin(node: JsonSchemaNode, integer: boolean): number | undefined { 149 + if (node.minimum !== undefined && node.minimum !== -INT_SENTINEL) { 150 + return node.minimum; 151 + } 152 + if (node.exclusiveMinimum !== undefined) { 153 + return integer ? node.exclusiveMinimum + 1 : node.exclusiveMinimum; 154 + } 155 + return undefined; 156 + } 157 + 158 + function fieldMax(node: JsonSchemaNode): number | undefined { 159 + if (node.maximum !== undefined && node.maximum !== INT_SENTINEL) { 160 + return node.maximum; 161 + } 162 + return undefined; 163 + } 164 + 165 + /** A scalar leaf schema node → BlockFieldSpec, or null when it has no field 166 + * form (a bare scalar string, or any type z.toJSONSchema doesn't reduce to 167 + * a known field kind). */ 168 + function leafField( 169 + path: string, 170 + node: JsonSchemaNode, 171 + shadowValue: unknown, 172 + binding: PanelBinding | undefined, 173 + ): BlockFieldSpec | null { 174 + const label = binding?.label ?? path.split(".").at(-1) ?? path; 175 + const defaultValue = shadowValue ?? node.default; 176 + const withDefault = defaultValue !== undefined ? { defaultValue } : {}; 177 + 178 + if (node.type === "string" && node.pattern === ISO_DATE_PATTERN) { 179 + return { path, kind: "date", label, ...withDefault }; 180 + } 181 + if (node.enum !== undefined) { 182 + return { 183 + path, 184 + kind: "dropdown", 185 + label, 186 + options: node.enum.map((value) => String(value)), 187 + ...withDefault, 188 + }; 189 + } 190 + if (node.type === "integer" || node.type === "number") { 191 + const integer = node.type === "integer"; 192 + const min = fieldMin(node, integer); 193 + const max = fieldMax(node); 194 + const bounds = { 195 + ...(min !== undefined ? { min } : {}), 196 + ...(max !== undefined ? { max } : {}), 197 + integer, 198 + }; 199 + if (binding?.control === "slider") { 200 + const options = (binding.options ?? {}) as { min?: number; max?: number; step?: number }; 201 + const sliderMin = options.min ?? min; 202 + const sliderMax = options.max ?? max; 203 + const slider: SliderSpec = { 204 + ...(sliderMin !== undefined ? { min: sliderMin } : {}), 205 + ...(sliderMax !== undefined ? { max: sliderMax } : {}), 206 + ...(options.step !== undefined ? { step: options.step } : {}), 207 + }; 208 + return { path, kind: "slider", label, ...bounds, slider, ...withDefault }; 209 + } 210 + return { path, kind: "number", label, ...bounds, ...withDefault }; 211 + } 212 + if (node.type === "boolean") { 213 + return { path, kind: "checkbox", label, ...withDefault }; 214 + } 215 + return null; 216 + } 217 + 218 + /** A scalar-array schema node → VariadicParamSpec, or null when the element 219 + * type has no field form (object arrays, enum arrays). */ 220 + function variadicParam( 221 + path: string, 222 + node: JsonSchemaNode, 223 + shadowValue: unknown, 224 + binding: PanelBinding | undefined, 225 + ): VariadicParamSpec | null { 226 + const items = node.items; 227 + if (items === undefined || items.enum !== undefined) return null; 228 + const label = binding?.label ?? path.split(".").at(-1) ?? path; 229 + const minItems = node.minItems ?? 0; 230 + const defaults = Array.isArray(shadowValue) 231 + ? shadowValue 232 + : Array.isArray(node.default) 233 + ? node.default 234 + : undefined; 235 + if (items.type === "integer" || items.type === "number") { 236 + const integer = items.type === "integer"; 237 + const min = fieldMin(items, integer); 238 + const max = fieldMax(items); 239 + if (defaults === undefined || defaults.some((v) => typeof v !== "number")) { 240 + return null; 241 + } 242 + return { 243 + path, 244 + label, 245 + itemKind: "number", 246 + integer, 247 + ...(min !== undefined ? { min } : {}), 248 + ...(max !== undefined ? { max } : {}), 249 + minItems, 250 + ...(node.maxItems !== undefined ? { maxItems: node.maxItems } : {}), 251 + emptySeed: min ?? 0, 252 + defaultValues: defaults as number[], 253 + }; 254 + } 255 + if (items.type === "string" && items.pattern === undefined) { 256 + if (defaults === undefined || defaults.some((v) => typeof v !== "string")) { 257 + return null; 258 + } 259 + return { 260 + path, 261 + label, 262 + itemKind: "text", 263 + integer: false, 264 + minItems, 265 + ...(node.maxItems !== undefined ? { maxItems: node.maxItems } : {}), 266 + emptySeed: "", 267 + defaultValues: defaults as string[], 268 + }; 269 + } 270 + return null; 271 + } 272 + 273 + /** Flatten a block input schema into scalar fields, variadic scalar-array 274 + * params, and complex (field-less) params. Nested objects flatten into 275 + * dot-path fields. */ 276 + function walkSchema( 277 + def: BlockDef, 278 + node: JsonSchemaNode, 279 + prefix: string, 280 + fields: BlockFieldSpec[], 281 + variadic: VariadicParamSpec[], 282 + complex: string[], 283 + ): void { 284 + for (const [key, child] of Object.entries(node.properties ?? {})) { 285 + const path = prefix === "" ? key : `${prefix}.${key}`; 286 + if (child.type === "object" && child.properties !== undefined) { 287 + walkSchema(def, child, path, fields, variadic, complex); 288 + continue; 289 + } 290 + const binding = def.panelBindings?.find((b) => b.paramPath === path); 291 + const shadowValue = resolveDotPath(def.defaultShadow, path); 292 + if (child.type === "array") { 293 + const param = variadicParam(path, child, shadowValue, binding); 294 + if (param !== null) { 295 + variadic.push(param); 296 + } else { 297 + complex.push(path); 298 + } 299 + continue; 300 + } 301 + const leaf = leafField(path, child, shadowValue, binding); 302 + if (leaf !== null) { 303 + fields.push(leaf); 304 + } else { 305 + complex.push(path); 306 + } 307 + } 308 + } 309 + 310 + /** Derive one block's metadata from its `BlockDef`. Pure; zod only. An 311 + * unrepresentable input schema degrades to a label-only block (empty 312 + * fields/variadic/complex) rather than throwing — the meta stays valid and 313 + * the block stays editable off-canvas. */ 314 + export function deriveBlockMeta(def: BlockDef): DerivedBlockMeta { 315 + const fields: BlockFieldSpec[] = []; 316 + const variadic: VariadicParamSpec[] = []; 317 + const complex: string[] = []; 318 + let inputSchema: unknown = { type: "object" }; 319 + try { 320 + const schema = z.toJSONSchema(def.block.schema.input as z.ZodType, { 321 + io: "input", 322 + }) as JsonSchemaNode; 323 + inputSchema = schema; 324 + walkSchema(def, schema, "", fields, variadic, complex); 325 + } catch { 326 + fields.length = 0; 327 + variadic.length = 0; 328 + complex.length = 0; 329 + } 330 + 331 + return { 332 + code: def.code, 333 + family: def.family, 334 + tier: def.tier, 335 + doc: def.block.doc, 336 + defaultShadow: def.defaultShadow, 337 + ...(def.assumptionTemplate !== undefined ? { assumptionTemplate: def.assumptionTemplate } : {}), 338 + ...(def.panelBindings !== undefined ? { panelBindings: def.panelBindings } : {}), 339 + ...(def.projectAnchoredPaths !== undefined 340 + ? { projectAnchoredPaths: def.projectAnchoredPaths } 341 + : {}), 342 + inputSchema, 343 + fields, 344 + variadic, 345 + complexParams: complex, 346 + }; 347 + } 348 + 349 + /** Dump the whole registry to a serializable metadata document: engine 350 + * version, format version, the composition grammar, and every block's 351 + * derived meta in registry order. This is the build-time artifact 352 + * scripts/generate-block-meta.ts writes to generated/block-meta.json. */ 353 + export function dumpBlockMeta(registry: { list(): BlockDef[] }): BlockMetaDump { 354 + return { 355 + levelsetVersion: LEVELSET_VERSION, 356 + formatVersion: 1, 357 + composition: { 358 + order: FAMILY_ORDER, 359 + follows: FAMILY_FOLLOWS, 360 + cardinality: FAMILY_CARDINALITY, 361 + }, 362 + blocks: registry.list().map(deriveBlockMeta), 363 + }; 364 + }
+1
src/leveling.ts
··· 79 79 // ── Pipeline-as-data (registry, graph format, compile traversal) ───── 80 80 export * from "./level-blocks/registry.ts"; 81 81 export * from "./level-blocks/composition.ts"; 82 + export * from "./level-blocks/meta.ts"; 82 83 export * from "./level-blocks/graph.ts"; 83 84 export * from "./level-blocks/compile.ts"; 84 85 export * from "./level-blocks/standard.ts";
+200
test/level-blocks/meta.test.ts
··· 1 + import { test, expect, describe } from "bun:test"; 2 + import { z } from "zod"; 3 + 4 + import { 5 + deriveBlockMeta, 6 + dumpBlockMeta, 7 + ISO_DATE_PATTERN, 8 + type BlockFieldSpec, 9 + } from "../../src/level-blocks/meta.ts"; 10 + import type { BlockDef } from "../../src/level-blocks/registry.ts"; 11 + import { 12 + standardBlockDefs, 13 + standardRegistry, 14 + STANDARD_CODES, 15 + } from "../../src/level-blocks/standard.ts"; 16 + 17 + const defsByCode = new Map(standardBlockDefs().map((d) => [d.code, d])); 18 + function meta(code: string) { 19 + return deriveBlockMeta(defsByCode.get(code)!); 20 + } 21 + function field(fields: BlockFieldSpec[], path: string): BlockFieldSpec | undefined { 22 + return fields.find((f) => f.path === path); 23 + } 24 + 25 + describe("deriveBlockMeta — scalar fields", () => { 26 + test("integer field carries bounds, integer flag, and shadow default", () => { 27 + const m = meta(STANDARD_CODES.maxConcurrentResource); 28 + const max = field(m.fields, "max")!; 29 + expect(max.kind).toBe("number"); 30 + expect(max.integer).toBe(true); 31 + expect(max.min).toBe(1); // exclusiveMinimum 0 → inclusive 1 for integers 32 + expect(max.max).toBeUndefined(); // INT_SENTINEL stripped 33 + expect(max.defaultValue).toBe(1); // from defaultShadow 34 + expect(max.label).toBe("max"); 35 + }); 36 + 37 + test("number field with a schema default surfaces that default", () => { 38 + const m = meta(STANDARD_CODES.concurrentResourceCost); 39 + const threshold = field(m.fields, "threshold")!; 40 + expect(threshold.kind).toBe("number"); 41 + // defaultShadow omits threshold, so the schema default (0) fills in. 42 + expect(threshold.defaultValue).toBe(0); 43 + const growth = field(m.fields, "growthBase")!; 44 + expect(growth.min).toBe(1); // exclusiveMinimum 1, non-integer → stays 1 45 + expect(growth.integer).toBe(false); 46 + }); 47 + 48 + test("boolean field becomes a checkbox", () => { 49 + const m = meta(STANDARD_CODES.unimodalDeviation); 50 + const flag = field(m.fields, "allowSecondPeak")!; 51 + expect(flag.kind).toBe("checkbox"); 52 + expect(flag.defaultValue).toBe(false); 53 + }); 54 + 55 + test("enum field becomes a dropdown with string options", () => { 56 + const m = meta(STANDARD_CODES.gate); 57 + const fallback = field(m.fields, "fallback")!; 58 + expect(fallback.kind).toBe("dropdown"); 59 + expect(fallback.options).toEqual(["leastBad", "none"]); 60 + }); 61 + }); 62 + 63 + describe("deriveBlockMeta — variadic scalar arrays", () => { 64 + test("integer array → number variadic (ConcurrentUnitsLimit.unitIds)", () => { 65 + const m = meta(STANDARD_CODES.concurrentUnitsLimit); 66 + const v = m.variadic.find((p) => p.path === "unitIds")!; 67 + expect(v.itemKind).toBe("number"); 68 + expect(v.integer).toBe(true); 69 + expect(v.minItems).toBe(0); 70 + expect(v.emptySeed).toBe(0); 71 + expect(v.defaultValues).toEqual([]); 72 + // unitIds is a variadic, not a complex param. 73 + expect(m.complexParams).not.toContain("unitIds"); 74 + }); 75 + 76 + test("string array → text variadic (KBest.by)", () => { 77 + const m = meta(STANDARD_CODES.kBest); 78 + const v = m.variadic.find((p) => p.path === "by")!; 79 + expect(v.itemKind).toBe("text"); 80 + expect(v.integer).toBe(false); 81 + expect(v.minItems).toBe(1); 82 + expect(v.emptySeed).toBe(""); 83 + expect(v.defaultValues).toEqual(["makespan"]); 84 + }); 85 + }); 86 + 87 + describe("deriveBlockMeta — complex (object-array) params", () => { 88 + test("ModeSelection.modes is complex, taskUniqueId is a field", () => { 89 + const m = meta(STANDARD_CODES.modeSelection); 90 + expect(m.complexParams).toContain("modes"); 91 + expect(field(m.fields, "taskUniqueId")).toBeDefined(); 92 + // No nested field is emitted for the object-array's inner scalars. 93 + expect(m.fields.some((f) => f.path.startsWith("modes."))).toBe(false); 94 + }); 95 + 96 + test("PhaseLateness.phases and Gate.limits are complex", () => { 97 + expect(meta(STANDARD_CODES.phaseLateness).complexParams).toContain("phases"); 98 + expect(meta(STANDARD_CODES.gate).complexParams).toContain("limits"); 99 + }); 100 + }); 101 + 102 + describe("deriveBlockMeta — nested objects flatten to dotted fields", () => { 103 + test("PeakCap.window flattens to window.fromDay / window.toDay", () => { 104 + const m = meta(STANDARD_CODES.peakCap); 105 + expect(field(m.fields, "window.fromDay")).toBeDefined(); 106 + expect(field(m.fields, "window.toDay")).toBeDefined(); 107 + // The container object itself is neither a field nor complex. 108 + expect(m.fields.some((f) => f.path === "window")).toBe(false); 109 + expect(m.complexParams).not.toContain("window"); 110 + }); 111 + }); 112 + 113 + describe("deriveBlockMeta — empty-schema blocks", () => { 114 + test("Makespan and SerialSGS have no fields, variadic, or complex params", () => { 115 + for (const code of [STANDARD_CODES.makespan, STANDARD_CODES.serialSGS]) { 116 + const m = meta(code); 117 + expect(m.fields).toEqual([]); 118 + expect(m.variadic).toEqual([]); 119 + expect(m.complexParams).toEqual([]); 120 + } 121 + }); 122 + }); 123 + 124 + describe("deriveBlockMeta — registry facts", () => { 125 + test("carries family, tier, doc, defaultShadow, and inputSchema", () => { 126 + const m = meta(STANDARD_CODES.makespan); 127 + expect(m.family).toBe("scorer"); 128 + expect(m.tier).toBe("sales"); 129 + expect(m.doc.nl.length).toBeGreaterThan(0); 130 + expect(m.defaultShadow).toEqual({}); 131 + expect((m.inputSchema as { type?: string }).type).toBe("object"); 132 + }); 133 + 134 + test("assumptionTemplate is included when present, omitted otherwise", () => { 135 + expect(meta(STANDARD_CODES.maxConcurrentResource).assumptionTemplate).toContain("concurrent"); 136 + expect(meta(STANDARD_CODES.makespan).assumptionTemplate).toBeUndefined(); 137 + }); 138 + }); 139 + 140 + describe("deriveBlockMeta — panelBinding-driven kinds (date, slider)", () => { 141 + // The standard registry declares no panelBindings and no date params, so a 142 + // synthetic def exercises the slider and date branches. 143 + const schema = z.object({ 144 + start: z.string().regex(/^\d{4}-\d{2}-\d{2}$/), 145 + level: z.number().min(0).max(10), 146 + }); 147 + const def: BlockDef = { 148 + code: "Synthetic", 149 + family: "constraint", 150 + tier: "engineer", 151 + block: { 152 + id: "Synthetic", 153 + schema: { input: schema, output: z.unknown() }, 154 + apply: (i: unknown) => i, 155 + toMiniZinc: () => ({ text: "" }), 156 + doc: { nl: "synthetic", pseudocode: "" }, 157 + } as never, 158 + defaultShadow: { start: "2026-01-01", level: 5 }, 159 + panelBindings: [ 160 + { 161 + paramPath: "level", 162 + label: "Aggressiveness", 163 + control: "slider", 164 + options: { min: 1, max: 8, step: 0.5 }, 165 + }, 166 + ], 167 + }; 168 + 169 + test("date pattern → date field", () => { 170 + const m = deriveBlockMeta(def); 171 + const start = field(m.fields, "start")!; 172 + expect(start.kind).toBe("date"); 173 + expect(start.defaultValue).toBe("2026-01-01"); 174 + }); 175 + 176 + test("slider binding → slider field with binding bounds and label", () => { 177 + const m = deriveBlockMeta(def); 178 + const level = field(m.fields, "level")!; 179 + expect(level.kind).toBe("slider"); 180 + expect(level.label).toBe("Aggressiveness"); 181 + expect(level.slider).toEqual({ min: 1, max: 8, step: 0.5 }); 182 + expect(level.defaultValue).toBe(5); 183 + }); 184 + 185 + test("ISO_DATE_PATTERN matches the block-author date convention", () => { 186 + expect(new RegExp(ISO_DATE_PATTERN).test("2026-07-16")).toBe(true); 187 + expect(new RegExp(ISO_DATE_PATTERN).test("not-a-date")).toBe(false); 188 + }); 189 + }); 190 + 191 + describe("dumpBlockMeta", () => { 192 + test("dumps every registered block with the composition grammar", () => { 193 + const dump = dumpBlockMeta(standardRegistry()); 194 + expect(dump.formatVersion).toBe(1); 195 + expect(dump.blocks.length).toBe(standardBlockDefs().length); 196 + expect(dump.composition.order).toEqual(["constraint", "scorer", "search", "collector"]); 197 + expect(dump.composition.cardinality.search).toEqual({ min: 1, max: 1 }); 198 + expect(dump.levelsetVersion.length).toBeGreaterThan(0); 199 + }); 200 + });