Files
vac-optimizer/README.md
T

12 KiB

VacOptimizer

VacOptimizer is a personal leave planner for turning dated leave balances into a deterministic plan around weekends and French public holidays. It handles mandatory rules, ordered preferences, earned leave, expiring balances, and interrupted calculations without relying on a backend solver.

The planner answers four distinct questions:

  • Is the request valid?
  • Can all mandatory rules be satisfied together?
  • What is the best plan found by the ordered heuristic?
  • If calculation stops early, what valid incumbent is available?
  • Why did each displayed placement survive the objective order?
  • Which alternatives were considered, and what would another date change?

Architecture

flowchart LR
    Host["Express production host<br/>static and client assets"] -. serves .-> UI

    subgraph Browser
        UI["Angular standalone components"]
        State["Signals and localStorage-backed settings"]
        Facade["Planning calculation context<br/>immutable snapshot + async artifacts"]
        Worker["Leave-planner Web Worker"]
        Core["Synchronous planner core"]
        Holidays["date-holidays<br/>French public calendar"]

        UI <--> State
        UI --> Facade
        Facade --> Worker
        Worker --> Core
        Core --> Holidays
        Worker -- "progress, checkpoints, outcome, then artifacts" --> Facade
        Facade --> UI
    end

Leave calculation stays in the browser. The production Express process serves the generated Angular application; it does not participate in planning. Browser calculations run in a dedicated worker so the calendar remains responsive. The synchronous core is emitted as a separate worker/lazy chunk; the facade imports it only for validation or execution outside the normal browser-worker path.

Main execution path

  1. CalendarYear takes a snapshot of leave balances and ordered rules.
  2. CalendarService supplies the Europe/Paris planning horizon and opens one immutable planning calculation context.
  3. leave-planner.service.ts copies the request into a canonical transport shape and starts the worker.
  4. leave-planner.worker.ts runs the framework-free core, sends progress and valid checkpoints, publishes the outcome first, then delivers its stable explanation and original trace.
  5. CalendarYear ignores stale calculations, renders the selected dates, and reports remaining balances or errors.

Planning work has a default and hard maximum wall-clock limit of ten seconds. If it expires after a valid checkpoint, the result is PlanningIncomplete with that incumbent; it is not reported as a constraint conflict. Explanation and trace delivery happen after the outcome and do not consume that planning budget.

Planning model

A request contains:

  • a half-open planning horizon of at most 12 months;
  • CP, RTT, or Other leave balances with quantities, availability dates, and expiry dates;
  • ordered TIME_WINDOW and IF_THEN rules;
  • an optional deadline and progress callback.

Rules are either:

  • obligations, which must all be feasible in the same plan; or
  • preferences, optimized lexicographically in user order.

TIME_WINDOW places a quantity inside an inclusive date window. IF_THEN creates before, after, around, or automatic patterns for each matching French public holiday. A completed IF_THEN occurrence may earn a new dated leave entitlement after its final time-off period ends.

The outcome is one of:

Outcome Meaning
InvalidInput The request is malformed.
PlanningConflict Valid obligations cannot all be satisfied together.
Plan All stages completed with placements, periods, metrics, provenance, and remaining balances.
PlanningIncomplete The deadline or worker stopped calculation; a valid incumbent may be present.

All planning dates are normalized as YYYY-MM-DD calendar values in the Europe/Paris domain. The planner treats weekdays excluding French national public holidays as working days.

Optimization pipeline

The core is a deterministic, constraint-first heuristic rather than a mathematical solver. It applies these objectives in order, never improving a lower objective by degrading a higher one; complete public-holiday weeks are selected before broader calendar-gain and work-sequence optimization:

Planner state machine

stateDiagram-v2
    direction LR

    state "Request snapshot" as Snapshot
    state "Validate and normalize" as Validation
    state "Solve obligations jointly" as Obligations
    state "Place at-risk balances" as AtRisk
    state "Resolve ordered preferences and earned chains" as Preferences
    state "Run lexicographic quality passes<br/>(complete holiday weeks, then non-working-day gain)" as Quality

    [*] --> Snapshot
    Snapshot --> Validation: canonical request

    Validation --> InvalidInput: malformed input
    Validation --> Obligations: valid input

    Obligations --> PlanningConflict: no joint solution
    Obligations --> AtRisk: feasible solution

    AtRisk --> Preferences: at-risk leave placed
    Preferences --> Quality: ordered preferences resolved
    Quality --> Plan: all quality passes complete

    Snapshot --> PlanningIncomplete: worker unavailable
    Validation --> PlanningIncomplete: interrupted
    Obligations --> PlanningIncomplete: deadline
    AtRisk --> PlanningIncomplete: deadline
    Preferences --> PlanningIncomplete: deadline
    Quality --> PlanningIncomplete: deadline

    InvalidInput --> [*]
    PlanningConflict --> [*]
    PlanningIncomplete --> [*]
    Plan --> [*]

PlanningIncomplete is reachable from every active calculation state. Once obligations are feasible, each completed stage publishes a valid checkpoint; an interruption returns the latest checkpoint as the incumbent. PlanningConflict is produced only when the obligations are valid but jointly infeasible.

  1. validate and normalize the request;
  2. find joint obligation assignments;
  3. minimize unplaced at-risk leave;
  4. satisfy each preference in user order, including shared placements and earned-entitlement chains;
  5. maximize completed public-holiday week value (Wednesday holidays outrank Tuesday/Thursday, which outrank Monday/Friday);
  6. maximize adjacent weekend and public-holiday gain;
  7. minimize the longest work sequence;
  8. minimize deficits caused by internal work sequences shorter than five days;
  9. minimize supporting placements;
  10. prefer earlier-expiring balances;
  11. prefer earlier placement dates.

Every placement records its source balance and all rules it satisfies. Remaining balances are classified as unavoidable expiry, at risk, or deferred.

Technical stack

  • Angular 21 standalone components with zoneless change detection
  • Angular Material and CDK
  • Signals for UI state and local storage for persisted settings and rules
  • TypeScript 6
  • Web Workers for planner isolation
  • date-holidays for the French public calendar
  • Bun for dependency management and project scripts
  • Vitest through the Angular test builder
  • ESLint and Prettier
  • Angular server build hosted by Express
  • GitLab CI and a multi-stage Docker image

The /benchmark route and planner core are lazy-loaded, so the normal application does not execute the benchmark or bundle the synchronous planner into its main entry point.

Repository layout

src/app/
├── benchmark/                    # Lazy five-scenario performance harness
├── components/                   # Calendar, inputs, rules, and settings UI
├── services/
│   ├── leave-planner.service.ts  # Canonical async planning boundary
│   ├── leave-planner.worker.ts   # Browser worker adapter
│   ├── leave-planner.core.ts     # Framework-free planning algorithm
│   ├── calendar-service.ts       # Calendar projection and UI integration
│   └── planning-date.ts          # Shared Europe/Paris date helpers
└── shared/                       # Small shared UI utilities

src/i18n/                         # English, French, German, and Spanish text
docs/adr/                         # Architecture decisions
docs/benchmark/                   # Reference-phone benchmark protocol
CONTEXT.md                        # Canonical domain language and rules

Local development

Prerequisite: Bun. Dependency versions are locked in bun.lock.

bun install --frozen-lockfile
bun start

Open http://localhost:4200. The development server reloads when source files change.

Useful commands

Command Purpose
bun start Generate the i18n registry and start the development server.
bun run test --watch=false Run the unit and integration suite once.
bun run lint Run ESLint.
bun run format:check Check Prettier formatting.
bunx tsc --noEmit -p tsconfig.app.json Type-check application code.
bunx tsc --noEmit -p tsconfig.spec.json Type-check tests.
bun run build Create the optimized browser and server bundles in dist/.
bun run serve:ssr:vac-optimizer Serve a completed production build on port 4000.

Performance benchmark

Build or start the application, then open /benchmark. The isolated harness covers a representative year, typed obligations, chained earned entitlements, a deliberate conflict, and a loaded year. It reports preparation time, planning latency, separate trace-delivery latency, total wall time, outcome, last completed stage, placement count, determinism, and trace size/status.

The release protocol and reference-phone thresholds are documented in docs/benchmark/README.md. Phone timings are release evidence, not CI assertions.

Docker

docker build -t vac-optimizer .
docker run --rm -p 4000:4000 vac-optimizer

The image builds the Angular application with Bun and Node, then runs the generated Express server with Bun. Set PORT to override the default port 4000.

Continuous integration

GitLab CI runs independent validation, test, build, security, and image stages:

  • ESLint, Prettier, and TypeScript checks;
  • Vitest with coverage artifacts;
  • an optimized production build;
  • GitLab SAST and Trivy filesystem scanning;
  • Docker image publication from the default branch.

Domain and design references

  • CONTEXT.md defines the project vocabulary and planner semantics.
  • ADR 0001 records the ordered heuristic decision.
  • ADR 0002 records the client-side scope.
  • ADR 0003 records the constraint-first planner architecture.
  • ADR 0004 separates the outcome, stable explanation, original trace, and hypothetical analysis.