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.

fix(level-blocks): validate metadata dot-paths at registration

panelBindings.paramPath and projectAnchoredPaths are consumed app-side
(panel generation, template save), so a typo'd dot-path previously
failed silently at first use. register() now resolves every declared
path against the parsed defaultShadow — curated to be complete — and
throws when one doesn't resolve, matching the fail-at-registration
principle already applied to defaultShadow itself.

Russ T. Fugal (Jun 12, 2026, 3:25 AM -0600) ec007847 414eee12

+53
+22
src/level-blocks/registry.ts
··· 47 47 list(filter?: { family?: BlockFamily; tier?: BlockTier }): BlockDef[]; 48 48 } 49 49 50 + function resolveDotPath(value: unknown, path: string): unknown { 51 + let current = value; 52 + for (const segment of path.split(".")) { 53 + if (current === null || typeof current !== "object") return undefined; 54 + current = (current as Record<string, unknown>)[segment]; 55 + } 56 + return current; 57 + } 58 + 50 59 export function createRegistry(defs?: BlockDef[]): BlockRegistry { 51 60 const byCode = new Map<string, BlockDef>(); 52 61 ··· 58 67 const parsed = def.block.schema.input.safeParse(def.defaultShadow); 59 68 if (!parsed.success) { 60 69 throw new Error(`Invalid defaultShadow for block "${def.code}": ${parsed.error.message}`); 70 + } 71 + // Dot-paths are consumed app-side (panel generation, template save); 72 + // a typo there would otherwise fail silently at first use. Check them 73 + // against the parsed shadow so they fail here instead — defaultShadow 74 + // is curated to be complete, so every declared path must resolve. 75 + const paths = [ 76 + ...(def.panelBindings ?? []).map((binding) => binding.paramPath), 77 + ...(def.projectAnchoredPaths ?? []), 78 + ]; 79 + for (const path of paths) { 80 + if (resolveDotPath(parsed.data, path) === undefined) { 81 + throw new Error(`Block "${def.code}": path "${path}" does not resolve in defaultShadow`); 82 + } 61 83 } 62 84 byCode.set(def.code, def); 63 85 },
+31
test/level-blocks/registry.test.ts
··· 74 74 expect(registry.get("Bad")).toBeUndefined(); 75 75 expect(registry.list()).toHaveLength(0); 76 76 }); 77 + 78 + test("accepts panelBindings and projectAnchoredPaths that resolve in defaultShadow", () => { 79 + const registry = createRegistry(); 80 + const def = makeDef("ToyCap", "constraint", ToyCapBlock as never, { 81 + resource: "crane", 82 + max: 2, 83 + }); 84 + def.panelBindings = [{ paramPath: "max", label: "Max crews", control: "number" }]; 85 + def.projectAnchoredPaths = ["resource"]; 86 + expect(() => registry.register(def)).not.toThrow(); 87 + }); 88 + 89 + test("throws when a panelBindings paramPath does not resolve in defaultShadow", () => { 90 + const registry = createRegistry(); 91 + const def = makeDef("ToyCap", "constraint", ToyCapBlock as never, { 92 + resource: "crane", 93 + max: 2, 94 + }); 95 + def.panelBindings = [{ paramPath: "maximum", label: "Max crews", control: "number" }]; 96 + expect(() => registry.register(def)).toThrow(/"maximum" does not resolve/); 97 + }); 98 + 99 + test("throws when a projectAnchoredPath does not resolve in defaultShadow", () => { 100 + const registry = createRegistry(); 101 + const def = makeDef("ToyCap", "constraint", ToyCapBlock as never, { 102 + resource: "crane", 103 + max: 2, 104 + }); 105 + def.projectAnchoredPaths = ["window.start"]; 106 + expect(() => registry.register(def)).toThrow(/"window.start" does not resolve/); 107 + }); 77 108 });