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.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143# levelset> A TypeScript resource-leveling and project-scheduling toolkit. Author constraints and scorers as composable blocks, run a schedule-generation search over any `ProjectFile`, and materialize a re-leveled schedule. Bundled readers/writers move real plans in and out of Microsoft Project (`.mpp`, `.mspdi`), JSON, CSV, and XLSX.The library has two halves that can be used independently:- **Leveling** (`levelset/leveling`) — the scheduling engine: resolve calendars, search for feasible schedules under a set of constraints, score and pick among them, then write the result back. Pulls only `zod` (a peer dependency), so the file-I/O dependencies stay out of your bundle.- **File I/O** (`levelset`, `levelset/advanced`) — read a `.mpp`/`.mspdi`/JSON plan into a structured `ProjectFile`, or write a `ProjectFile` out as MSPDI, JSON, CSV, or XLSX. The MPP reader extracts tasks, resources, assignments, calendars, and relations.The unifying data type is `ProjectFile`: a plain object holding `tasks`, `resources`, `assignments`, `calendars`, `relations`, and `properties`. Readers produce it, writers consume it, and the leveling engine round-trips through it.## Core model- `ProjectFile` — the interchange document (tasks, resources, assignments, calendars, relations, properties).- `Task` — a unit of work with a `uniqueId`, `duration` (a `Duration` instance), dates, and `predecessors` (precedence edges).- `Resource` — a person, crew, equipment, or cost item, referenced by `uniqueId`.- `Assignment` — links a task to a resource with `units` (how many of that resource the task demands while active).- A **`WorkUnit`** is the largest independent work package — the minimum complete increment that can be handed over, grouping a set of `taskUniqueIds`. Work units are what the WIP-limiting constraints and scorers operate on. They are authored once via `resolveCalendar`'s `workUnits` option, which resolves them into a by-id registry on `ResolvedProject` (`workUnits: Map<number, WorkUnit>`, duplicate ids rejected); constraints and scorers then reference them by id.## Leveling pipelineThe flow is a sequence of small composable steps:```ProjectFile ──resolveCalendar──▶ ResolvedProject ──serialSGS.run(constraints)──▶ ScheduleStream ──bestBy(scorer)──▶ Schedule ──materialize──▶ ProjectFile```- `resolveCalendar(project)` turns calendars into day-indexed working time (a bitmap + prefix sum) so "working days between A and B" is O(1).- `serialSGS` is a greedy serial Schedule Generation Scheme: topo-sort by precedence, then place each task on the earliest day that satisfies every constraint. It yields one feasible schedule per run via an async generator.- `ScheduleStream` is a lazy iterable of feasible schedules with `filter` / `map` / `take` / `branch` and the materializers `bestBy` / `paretoFrontier` / `collect`.- `materialize(schedule)` writes a chosen `Schedule` back into a `ProjectFile`, ready to hand to any writer.Minimal end-to-end example:```tsimport { resolveCalendar, serialSGS, streamFromFactory, materialize } from "levelset/leveling";import type { Constraint, Scorer } from "levelset/leveling";const resolved = resolveCalendar(project, { // Work units are defined once here; constraints/scorers reference them by id. workUnits: [ { id: 10, taskUniqueIds: [1, 2, 3] }, { id: 20, taskUniqueIds: [4, 5, 6] }, ],});const constraints: Constraint[] = [ { kind: "MaxConcurrentResource", resourceUniqueId: 100, max: 3 }, { kind: "ConcurrentUnitsLimit", max: 2, // at most 2 work units open at once (a hard WIP cap) unitIds: [10, 20], // references the units resolved above },];const stream = streamFromFactory(() => serialSGS.run(resolved, constraints));const makespan: Scorer = { name: "makespan", direction: "min", score: (s) => s.makespan };const best = await stream.bestBy(makespan);if (best) { const leveled = materialize(best); // a ProjectFile you can write out}```## Constraints`Constraint` is a discriminated union (the `kind` field) — the interchange format between blocks and the search. `serialSGS` enforces a subset directly; the rest are recorded as `unsupportedConstraints` annotations on the emitted schedule (intended for a future CP-SAT / MiniZinc backend).Enforced by `serialSGS`:- `Precedence` — FS / SS / FF / SF edges with lag (FF/SF are approximated).- `MaxConcurrentResource` — at most N tasks may demand a given resource on any day (a crew/throughput cap).- `PeakCap` — sum of fractional units on a resource ≤ a cap (optionally windowed).- `ConcurrentUnitsLimit` — at most N work units have active work on any day; optional `discipline` (a `resourceUniqueId`) scopes the cap per-discipline. This is the hard WIP cap that prioritizes completion.- `Release` / `Deadline` — earliest-start / latest-finish bounds per task. (Pin a task to a fixed window by pairing a `Release` and a `Deadline` on it.)- `Calendars` — baked into `resolveCalendar` upstream.Recorded as annotations only (not yet enforced): `UnitPrecedence`, `UnimodalProfile`, `ModeSelection`, `CrewFlowContinuity`.## Scorers and blocksA `Scorer` ranks a `Schedule` (`direction: "min" | "max"`); `stream.bestBy(scorer)` picks the best. Scoring **blocks** (`level-blocks`) build configured scorers and also emit a MiniZinc fragment for the compiled backend.- `ConcurrentResourceCostBlock` — prices each marginal concurrent worker on an exponential curve (a soft `MaxConcurrentResource`).- `OpenUnitPenaltyBlock` — prices each open-unit-day beyond a `softMax` (a soft WIP limit).- `HiringLagPenaltyBlock` — prices week-over-week headcount increases (training/ramp cost).- `UnimodalDeviationBlock` — prices how far a resource histogram departs from a single-peak shape.Constraint blocks (`MaxConcurrentResourceBlock`, `ConcurrentUnitsLimitBlock`) build the corresponding hard `Constraint` plus a MiniZinc fragment.Note: because `serialSGS` emits a single schedule, a scorer currently *ranks* output rather than *steers* it. Scorers become an optimization lever once a multi-candidate search (restart / LDS / LNS) consumes them. Hard constraints are what shape `serialSGS` output today.## File I/O```tsimport { readMpp, readMspdi, readJson, writeMspdi, writeJson, writeCsv, writeXlsx } from "levelset";const project = readMpp(mppBytes); // Uint8Array | ArrayBuffer (MPP14+ / Project 2010+)const project2 = readMspdi(xmlString); // MSPDI XMLconst json = writeJson(project); // also writeMspdi, writeCsvconst xlsx = await writeXlsx(project); // returns a Uint8Array```| Format | Read | Write || ------ | ---- | ----- || MPP (binary, MPP14+) | Yes | No || MSPDI (XML) | Yes | Yes || JSON | Yes | Yes || CSV | No | Yes || XLSX | No | Yes |Validate untrusted JSON with `levelset/schema` (Zod schemas with transforms that rebuild `Duration` and `Date` instances): `ProjectFileSchema.safeParse(data)`.## Subpath exports- `levelset` — convenience functions (`readMpp`, `writeJson`, `writeCsv`, `writeXlsx`, …) and the core model types.- `levelset/leveling` — the resource-leveling toolkit (`level-core` + `level-blocks`); no file-I/O dependencies.- `levelset/advanced` — reader/writer classes and container utilities (`MppReader`, `MspdiWriter`, `parseMppBuffer`, `detectMppVariant`, …).- `levelset/schema` — Zod validation schemas for all model types.## Docs- [README](README.md): full usage guide — install, file I/O, the leveling toolkit, supported formats, and scripts.- [Literature review](docs/literature-review.md): the published research behind the engine's design (compositional constraint programming, lazy enumeration, hyper-heuristics, LLM-assisted optimization modeling).- [CHANGELOG](CHANGELOG.md): release history.## Source entry points- [src/index.ts](src/index.ts): the `levelset` convenience API (readers/writers + model types).- [src/leveling.ts](src/leveling.ts): the `levelset/leveling` surface (core types, pipeline functions, blocks).- [src/level-core](src/level-core): the leveling engine — `resolveCalendar`, `serialSGS`, `scheduleStream`, `materialize`, `types.ts` (the `Constraint`/`Schedule`/`Scorer` definitions).- [src/level-blocks](src/level-blocks): configured constraint and scorer blocks.- [src/model](src/model): the `ProjectFile` model (`Task`, `Resource`, `Assignment`, `Calendar`, `Relation`, `Duration`).## Optional- [src/advanced.ts](src/advanced.ts): lower-level reader/writer classes and MPP container inspection.- [src/schema](src/schema): Zod schemas exported under `levelset/schema`.- [src/mpp](src/mpp), [src/mspdi](src/mspdi), [src/json](src/json), [src/csv](src/csv), [src/xlsx](src/xlsx): per-format reader/writer implementations.- [test](test): the test suite, a source of worked usage examples.