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(analyzeRun): exempt milestones from the E105 duration bound

serialSGS places milestones with zero span regardless of their nominal
durationDays, but analyzeRun applied the (deadline - release) < duration
bound to them. A milestone carrying an explicit duration (as MPP/JSON
files can) with a same-day release + deadline gate was reported as
LEVELSET_E105 'no feasible placement exists' — and runFromConfig threw —
for a run serialSGS places successfully.

Russ T. Fugal (Jul 16, 2026, 2:12 AM -0600) ee56a65b a4981456

+25 -2
+5 -1
src/level-core/analyzeRun.ts
··· 199 199 }); 200 200 continue; 201 201 } 202 - const duration = taskById.get(taskUniqueId)?.durationDays; 202 + // Milestones are placed with zero span whatever their nominal duration 203 + // (serialSGS's `task.milestone || durationDays === 0` branch), so the 204 + // duration bound only applies to real tasks. 205 + const task = taskById.get(taskUniqueId); 206 + const duration = task === undefined || task.milestone ? undefined : task.durationDays; 203 207 if (duration !== undefined && deadline.day - releaseDay < duration) { 204 208 issues.push({ 205 209 code: "LEVELSET_E105",
+20 -1
test/level-core/analyzeRun.test.ts
··· 6 6 import { resolveCalendar } from "../../src/level-core/resolveCalendar.ts"; 7 7 import { serialSGS } from "../../src/level-core/search/serialSGS.ts"; 8 8 import type { Constraint, ResolvedProject, Scorer } from "../../src/level-core/types.ts"; 9 - import { RelationType } from "../../src/model/types.ts"; 9 + import { Duration } from "../../src/model/Duration.ts"; 10 + import { RelationType, TimeUnit } from "../../src/model/types.ts"; 10 11 11 12 import { 12 13 MON_JAN_5, ··· 124 125 }, 125 126 ], 126 127 }, 128 + ]); 129 + expect(analysis.ok).toBe(true); 130 + expect(analysis.issues).toHaveLength(0); 131 + }); 132 + 133 + test("milestones are exempt from the E105 duration bound", () => { 134 + // A milestone is placed with zero span whatever its nominal duration 135 + // carries (serialSGS's milestone branch), so a same-day release + 136 + // deadline pair is feasible and must not be reported as E105. 137 + const gate = { 138 + ...makeTask({ uniqueId: 3, start: MON_JAN_5, finish: MON_JAN_5, milestone: true }), 139 + duration: Duration.from(5, TimeUnit.Days), 140 + }; 141 + const resolved = resolveCalendar(makeProject([gate]), {}); 142 + expect(resolved.tasks[0]!.durationDays).toBeGreaterThan(0); 143 + const analysis = analyzeRun(resolved, [ 144 + { kind: "Release", taskUniqueId: 3, earliestStart: 2 }, 145 + { kind: "Deadline", taskUniqueId: 3, latestFinish: 2 }, 127 146 ]); 128 147 expect(analysis.ok).toBe(true); 129 148 expect(analysis.issues).toHaveLength(0);