292 lines
20 KiB
Markdown
292 lines
20 KiB
Markdown
# 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 these 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
|
|
|
|
```mermaid
|
|
flowchart LR
|
|
Host["Express production host<br/>static and client assets"] -. serves .-> UI
|
|
|
|
subgraph Browser
|
|
UI["Angular standalone components"]
|
|
State["Signals + localStorage<br/>settings, balances, rules"]
|
|
Duration["Indicative duration<br/>session-local calibration"]
|
|
Facade["Planning calculation context<br/>immutable snapshot + async artifacts"]
|
|
Worker["Leave-planner Web Worker"]
|
|
Core["Resumable planner core"]
|
|
Holidays["date-holidays<br/>French public calendar"]
|
|
|
|
UI <--> State
|
|
State -- "pending inputs" --> Duration
|
|
Duration -- "estimated wait" --> UI
|
|
UI -- "rendered-calendar timing" --> Duration
|
|
UI -- "Calculate / Cancel" --> Facade
|
|
Facade -- "request / cancellation" --> Worker
|
|
Worker --> Core
|
|
Core --> Holidays
|
|
Worker -- "heartbeats, progress, checkpoints, outcome" --> Facade
|
|
Worker -- "after outcome: explanation + trace batches" --> Facade
|
|
Facade -- "trace acknowledgements" --> Worker
|
|
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 cooperatively in a dedicated worker. The resumable core is emitted as a separate worker/lazy chunk and retains a synchronous adapter for direct core callers. Trace delivery uses bounded batches with receiver acknowledgements; large calendars and traces can still cause UI pauses and substantial memory use, as recorded in the benchmark report.
|
|
|
|
### Main execution path
|
|
|
|
1. `PlanningDurationService` reads the pending horizon, balances, and ordered rules to estimate the wait without running the planner.
|
|
2. On startup or an explicit Calculate action, `CalendarYear` submits those inputs through `CalendarService`, which 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, reports remaining balances or errors, and records successful request-to-rendered-calendar timing for duration refinement.
|
|
|
|
Calculations run until all objectives complete or the user cancels; there is no automatic elapsed-time cutoff. During preparation and optimization, Cancel interrupts the worker cooperatively and preserves its best jointly valid candidate as a cancelled, incomplete calculation. A cancellation before a valid candidate reports no plan. The final calendar is delivered before the explanation and complete original trace, whose later arrival or failure cannot change the retained plan. Worker heartbeats distinguish responsive long phases from a worker that stops responding. See [the cancellation mechanism and checks](docs/planner-cancellation.md).
|
|
|
|
Technical callers may explicitly supply `deadlineMs`, a finite non-negative planning-work budget of any size. Omission means no deadline; the calendar and benchmark never supply one. This opt-in budget starts after calendar preparation and excludes trace capture, observer callbacks, and post-outcome artifact work. Expiry returns `PlanningIncomplete` with the latest valid checkpoint, if any, rather than a conflict. It is independent of worker liveness monitoring.
|
|
|
|
## Planning model
|
|
|
|
A request contains:
|
|
|
|
- a user-selected half-open planning horizon, including past dates and multiple years;
|
|
- CP, RTT, or Other leave balances with quantities, availability dates, and expiry dates;
|
|
- ordered `TIME_WINDOW` and `IF_THEN` rules;
|
|
- an optional explicit technical 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` | Cancellation, the deadline, or worker failure 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.
|
|
|
|
Settings save a fixed start date and an inclusive last planning date. The planner's exclusive end is the following calendar date, so matching settings dates cover one day. Without saved dates, the default is today in Europe/Paris through the same date twelve months later, exclusive (February 29 uses February 28 in the following year). Invalid edits prevent calculation and preserve the last valid saved dates. Changes to dates, balances, and rules apply to the displayed calendar on the next explicit Calculate action.
|
|
|
|
An indicative range beside Calculate follows the current dates, balances, and rules. Completed plans refine it locally for the current browser session using request-to-rendered-calendar time; later trace delivery is excluded. The range never stops a calculation. See [the duration model and measurement contract](docs/planning-duration.md).
|
|
|
|
## 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
|
|
|
|
```mermaid
|
|
stateDiagram-v2
|
|
direction LR
|
|
|
|
state "Request snapshot" as Snapshot
|
|
state "Validate and normalize" as Validation
|
|
state "Prepare calendar across the horizon" as Preparation
|
|
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 --> Preparation: valid input
|
|
Preparation --> Obligations: calendar ready
|
|
|
|
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
|
|
Preparation --> PlanningIncomplete: Cancel or worker failure
|
|
Obligations --> PlanningIncomplete: Cancel or interruption
|
|
AtRisk --> PlanningIncomplete: Cancel or interruption
|
|
Preferences --> PlanningIncomplete: Cancel or interruption
|
|
Quality --> PlanningIncomplete: Cancel or interruption
|
|
|
|
InvalidInput --> [*]
|
|
PlanningConflict --> [*]
|
|
PlanningIncomplete --> [*]
|
|
Plan --> [*]
|
|
```
|
|
|
|
`PlanningIncomplete` is reachable from every active calculation state. Cancellation retains the best available jointly valid candidate, even before a stage completes; completed stages also publish checkpoints for other interruption paths. An explicit technical deadline applies only after calendar preparation, while worker failure can interrupt any active phase. `PlanningConflict` is produced only when the obligations are valid but jointly infeasible. Outcome delivery does not wait for explanation or trace completion.
|
|
|
|
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.
|
|
|
|
The calendar keeps plan details and the advanced trace collapsed initially. Plan details navigate final placements chronologically, showing provenance, contributions, the first decisive objective, and the full ordered comparison including obligation validity. **Compare dates** opens an on-demand hypothetical comparison across the submitted horizon, preserving the selected balance, type, and provenance without changing the plan.
|
|
|
|
The trace explorer navigates original phases and atomic decisions, expands candidate evaluations, and shows before/after calendar states. Historical date statuses remain separate from hypothetical results. Incomplete outcomes explain the retained plan and unfinished phases; conflicts identify the reported obligations and bottleneck. Controls support keyboard navigation, labelled dates, live announcements, and text or icons alongside color. Only the latest trace is retained locally in memory.
|
|
|
|
## Technical stack
|
|
|
|
- Angular 22 standalone components with zoneless change detection
|
|
- Angular Material and CDK
|
|
- Signals for UI state and local storage for settings, edited leave balances, 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 planner core into its main entry point.
|
|
|
|
The header opens one Settings dialog for planning dates, System / Light / Dark theme selection, English / French / German / Spanish language selection, and ISO week numbers. Changes apply and save on selection; closing the dialog retains them. System follows device appearance changes.
|
|
|
|
Saved balances, including an empty list, are restored before the initial calculation. When no readable saved list exists, CP, RTT, and Other start with random quantities from 10 through 20. These defaults remain unsaved until the first balance edit; subsequent edits save the entire list with identities, order, quantities, types, and calendar dates. Invalid saved balance data is cleared without removing unrelated settings or rules.
|
|
|
|
## Repository layout
|
|
|
|
```text
|
|
src/app/
|
|
├── benchmark/ # Lazy seven-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
|
|
│ ├── calendar-settings-service.ts # Persisted horizon and week-number settings
|
|
│ ├── planning-duration.service.ts # Indicative wait and session-local refinement
|
|
│ ├── user-input-service.ts # Saved balances and explicit calculation requests
|
|
│ └── 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](https://bun.sh/). Dependency versions are locked in `bun.lock`.
|
|
|
|
```bash
|
|
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, a loaded year, and two- and three-year horizons with dated annual balances. It reports preparation time, planning latency, separate trace-delivery latency, total wall time, UI timer delay, available main-thread heap measurements, outcome, last completed stage, placement count, determinism, and trace size/status.
|
|
|
|
The release protocol, recorded desktop measurements, and hardware limitations are documented in [docs/benchmark/README.md](docs/benchmark/README.md). No reference-phone run or multi-year duration ceiling is established; timing measurements are release evidence, not CI assertions.
|
|
|
|
## Docker
|
|
|
|
```bash
|
|
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;
|
|
- GitFlow, Conventional Commit, and release-version checks;
|
|
- Docker image publication for every successful branch push, including branches with an open MR.
|
|
|
|
Pipelines switch to MR pipelines when a branch has an open MR, avoiding duplicate builds. Docker publication from MR pipelines is limited to detached pipelines within this project. Tag pipelines promote the already-built commit image instead of rebuilding it.
|
|
|
|
The required `test` job runs application tests with coverage, one worker and a 2 GiB Node heap limit. A separate automatic `benchmark` job uses the same environment outside coverage instrumentation, with the unchanged five-minute timeout and protocol assertions.
|
|
|
|
At the user's request, the benchmark job has `allow_failure: true` while #78 is open, so benchmark failure does not block functional delivery. #78 must restore the required benchmark gate after the protocol passes. A green pipeline during this exception does not establish benchmark acceptance.
|
|
|
|
### GitFlow and releases
|
|
|
|
`develop` is the default integration branch. Create `feature/*` branches from it, rebase them onto the current `origin/develop` before opening or merging their MR, and merge into `develop` with a merge commit (`--no-ff`). Existing working-branch names such as `feat/*` follow the same rules. Working branches cannot merge `develop` into themselves instead of rebasing.
|
|
|
|
Create `release/X.Y.Z` from `develop` when the release scope is ready. Set `package.json` to `X.Y.Z` on this branch, stabilize it, then merge it into `main` and back into `develop`. Keep the release branch until both MRs have merged. A `hotfix/X.Y.Z` starts from `main`, changes the package version, and follows the same two-target merge process. Do not rebase `main`, `develop`, or a frozen release onto ongoing development.
|
|
|
|
Use Conventional Commits for new commits and MR titles, for example `feat(calendar): add planning periods`, `fix(planner): handle expired balances`, or `chore(release): prepare 1.6.0`. Scopes are optional; `!` marks a breaking change. GitLab generates merge messages from the validated MR title. Legacy history is not rewritten.
|
|
|
|
| Trigger | Published image tags |
|
|
| --- | --- |
|
|
| Successful push to any branch | `branch-<GitLab ref slug>` and `sha-<full commit SHA>` |
|
|
| Explicit release tag `vX.Y.Z` | `vX.Y.Z` and `latest`, using the existing SHA image |
|
|
|
|
For example, `feature/calendar` publishes `branch-feature-calendar`; integration publishes `branch-develop`. GitLab normalizes ref slugs to lowercase and truncates them to 63 bytes, so choose branch names with distinct slugs. Only valid release tags can update `latest`.
|
|
|
|
After the MR into `main` and its full pipeline have succeeded, tag the published main commit:
|
|
|
|
```bash
|
|
git fetch origin
|
|
git tag -a v1.6.0 origin/main -m "Release 1.6.0"
|
|
git push origin v1.6.0
|
|
```
|
|
|
|
The tag must match the checked-out `package.json` version, use stable `vMAJOR.MINOR.PATCH` syntax, and point to a commit reachable from `main`. Wait for the main pipeline before tagging: the release job requires its `sha-<commit>` image. Tags do not increment the version automatically.
|
|
|
|
GitLab project settings must use `develop` as the default branch, the **Merge commit** method, squashing disabled, and **Pipelines must succeed** with skipped pipelines disallowed. Protect `main` and `develop` with no direct or forced pushes and Maintainer-only merges. Set the merge commit template to `%{title}` followed by the MR reference, preserving Conventional Commit subjects. The CI permits only `release/*` and `hotfix/*` into `main`; working MRs target `develop`. Rebase checks validate the target head at pipeline time: if `develop` advances afterward, rebase and rerun the MR pipeline before merging.
|
|
|
|
Run the policy integration check locally with `node scripts/ci-policy.test.mjs`.
|
|
|
|
## Domain and design references
|
|
|
|
- [CONTEXT.md](CONTEXT.md) defines the project vocabulary and planner semantics.
|
|
- [ADR 0001](docs/adr/0001-ordered-heuristic-optimization.md) records the ordered heuristic decision.
|
|
- [ADR 0002](docs/adr/0002-personal-client-side-planner-for-now.md) records the client-side scope.
|
|
- [ADR 0003](docs/adr/0003-constraint-first-lexicographic-planning.md) records the constraint-first planner architecture.
|
|
- [ADR 0004](docs/adr/0004-stable-explanation-original-trace-counterfactual.md) separates the outcome, stable explanation, original trace, and hypothetical analysis.
|
|
- [ADR 0005](docs/adr/0005-saved-planning-inputs-before-random-defaults.md) records saved-input restoration and random fallback behavior.
|
|
- [ADR 0006](docs/adr/0006-user-selected-horizons-and-cancellable-calculations.md) records fixed saved horizons, cancellation, duration estimation, and the removal of the automatic deadline.
|