Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
43a3a477d6 | ||
|
|
3b75bdbe3e | ||
|
|
0d68dfeaf8 | ||
|
|
8414c02e10 | ||
|
|
abfbca1d45 | ||
|
|
b37101435a | ||
|
|
7fc3457b63 |
No files matched your search
@@ -46,3 +46,7 @@ Thumbs.db
|
|||||||
|
|
||||||
# Auto-generated i18n files
|
# Auto-generated i18n files
|
||||||
/src/app/i18n-registry.ts
|
/src/app/i18n-registry.ts
|
||||||
|
|
||||||
|
# Claude Code local files (.claude is a symlink to .agents/)
|
||||||
|
.agents/settings.local.json
|
||||||
|
.agents/worktrees/
|
||||||
@@ -24,6 +24,10 @@ Before the protocol, in the same process, the spec times the calibration engine
|
|||||||
a871f8f) on the `two years` scenario, best of three, and prints the result. This
|
a871f8f) on the `two years` scenario, best of three, and prints the result. This
|
||||||
bundle never changes, so ratios stay comparable with the recorded history; the
|
bundle never changes, so ratios stay comparable with the recorded history; the
|
||||||
differential check's frozen bundle can be rebuilt independently.
|
differential check's frozen bundle can be rebuilt independently.
|
||||||
|
The spec repeats the calibration after the protocol and uses the slower of the two
|
||||||
|
values, so load that starts during the protocol slows the yardstick too; the second
|
||||||
|
value can only loosen the budgets. When it does, the log prints it and the summary
|
||||||
|
table again with the budgets actually applied.
|
||||||
Each scenario's budget is its multiplier times that calibration; the mean
|
Each scenario's budget is its multiplier times that calibration; the mean
|
||||||
`planningMs` of its two measured repetitions must not exceed it. A scenario
|
`planningMs` of its two measured repetitions must not exceed it. A scenario
|
||||||
without a multiplier fails too. #286 tightened the multipliers once #302 met
|
without a multiplier fails too. #286 tightened the multipliers once #302 met
|
||||||
|
|||||||
@@ -17,11 +17,11 @@ publishing images. There is no schedule or deployment job in the preserved
|
|||||||
configuration; the Compose files remain manual deployment options.
|
configuration; the Compose files remain manual deployment options.
|
||||||
|
|
||||||
| Job | Behavior |
|
| Job | Behavior |
|
||||||
| ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
| ----------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| `policy` | Tests the policy and publisher with Bun, then validates the checked-out commit, GitFlow, PR title, commits, and stable release version. |
|
| `policy` | Tests the policy and publisher with Bun, then validates the checked-out commit, GitFlow, PR title, commits, and stable release version. |
|
||||||
| `validate` | Runs ESLint, Prettier (application, `scripts/ci-*.mjs` and workflow files) and `bun check` type checks of application and test code, all on Bun with the frozen lockfile. |
|
| `validate` | Runs ESLint, Prettier (application, `scripts/ci-*.mjs` and workflow files) and `bun check` type checks of application and test code, all on Bun with the frozen lockfile. |
|
||||||
| `test` | Runs functional coverage with `bun test --isolate` (line coverage only), converts it to HTML and Cobertura, and uploads coverage for seven days even on failure. |
|
| `test` | Runs functional coverage with `bun test --isolate` (line coverage only), converts it to HTML and Cobertura, and uploads coverage for seven days even on failure. |
|
||||||
| `benchmark` | Required seven-scenario protocol (one warm-up, two measured repetitions) outside coverage in a container limited to 3 GiB. It first times the frozen engine on `two years` (best of three) and fails any scenario whose mean planning time exceeds its multiplier times that calibration; the 240-second timeout is only a hang guard. |
|
| `benchmark` | Required seven-scenario protocol (one warm-up, two measured repetitions) outside coverage in a container limited to 3 GiB. It times the frozen engine on `two years` (best of three) before and after the protocol, and fails any scenario whose mean planning time exceeds its multiplier times that calibration; the 240-second timeout is only a hang guard. |
|
||||||
| `build` | Builds the client-only Angular production bundle (the only check of Angular templates; `bun check` does not replace it) and the Bun static-server bundle, and retains `dist` for seven days. |
|
| `build` | Builds the client-only Angular production bundle (the only check of Angular templates; `bun check` does not replace it) and the Bun static-server bundle, and retains `dist` for seven days. |
|
||||||
| `security` | Runs Semgrep JavaScript/TypeScript SAST and Trivy dependency/secret scans; retains SARIF reports for seven days. Scanner execution must succeed; findings remain reports rather than a new severity gate. The scanners are not project JavaScript, so this job installs no Bun. |
|
| `security` | Runs Semgrep JavaScript/TypeScript SAST and Trivy dependency/secret scans; retains SARIF reports for seven days. Scanner execution must succeed; findings remain reports rather than a new severity gate. The scanners are not project JavaScript, so this job installs no Bun. |
|
||||||
| `images` | Publishes branch/SHA images after required validation, including the benchmark, or promotes an existing SHA image for a stable tag, running the publisher with Bun. Runs only for pushes and same-repository PRs. |
|
| `images` | Publishes branch/SHA images after required validation, including the benchmark, or promotes an existing SHA image for a stable tag, running the publisher with Bun. Runs only for pushes and same-repository PRs. |
|
||||||
@@ -30,10 +30,11 @@ configuration; the Compose files remain manual deployment options.
|
|||||||
The benchmark runs on both push and pull_request events: the push run gates image
|
The benchmark runs on both push and pull_request events: the push run gates image
|
||||||
publication (`images.needs` includes `benchmark`) and the `required` aggregate.
|
publication (`images.needs` includes `benchmark`) and the `required` aggregate.
|
||||||
The relative gate absorbs a slower runner and load that lasts through the whole
|
The relative gate absorbs a slower runner and load that lasts through the whole
|
||||||
job, because calibration runs in the same job. It does not absorb load that
|
job, because calibration runs in the same job. It calibrates before and after the
|
||||||
starts or stops during the protocol, such as an overlapping push and
|
protocol and judges against the slower value, so it also absorbs load that starts
|
||||||
pull_request run of the same commit; the 240-second hang guard still bounds
|
during the protocol, such as an overlapping push and pull_request run of the same
|
||||||
those runs.
|
commit. It does not absorb load that ends before the second calibration; the
|
||||||
|
240-second hang guard still bounds those runs.
|
||||||
|
|
||||||
Push and PR runs are separate so PR title and rebase checks run on the PR event.
|
Push and PR runs are separate so PR title and rebase checks run on the PR event.
|
||||||
New runs cancel older runs for the same event/ref. Image jobs serialize per
|
New runs cancel older runs for the same event/ref. Image jobs serialize per
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { calculatePlanningAsyncInternal } from '../services/frozen-engine/leave-
|
|||||||
import { createPlanningCalculation } from '../services/leave-planner.service';
|
import { createPlanningCalculation } from '../services/leave-planner.service';
|
||||||
import {
|
import {
|
||||||
BENCHMARK_BUDGET_MULTIPLIERS,
|
BENCHMARK_BUDGET_MULTIPLIERS,
|
||||||
|
type BenchmarkMeasurement,
|
||||||
benchmarkReport,
|
benchmarkReport,
|
||||||
benchmarkScenarios,
|
benchmarkScenarios,
|
||||||
benchmarkSummaryHeader,
|
benchmarkSummaryHeader,
|
||||||
@@ -71,23 +72,43 @@ describe('leave planner benchmark protocol', () => {
|
|||||||
}
|
}
|
||||||
// Calibration engine (frozen at a871f8f) on 'two years', best of three: the runner-speed yardstick.
|
// Calibration engine (frozen at a871f8f) on 'two years', best of three: the runner-speed yardstick.
|
||||||
const calibrationRequest = scenarios.find((item) => item.name === 'two years')!.request;
|
const calibrationRequest = scenarios.find((item) => item.name === 'two years')!.request;
|
||||||
let calibrationMs = Infinity;
|
const calibrate = async () => {
|
||||||
|
let best = Infinity;
|
||||||
for (let run = 0; run < 3; run++) {
|
for (let run = 0; run < 3; run++) {
|
||||||
const request = structuredClone(calibrationRequest);
|
const request = structuredClone(calibrationRequest);
|
||||||
const start = performance.now();
|
const start = performance.now();
|
||||||
await calculatePlanningAsyncInternal({ ...request, calculationDate: '2024-01-01' });
|
await calculatePlanningAsyncInternal({
|
||||||
calibrationMs = Math.min(calibrationMs, performance.now() - start);
|
...request,
|
||||||
|
calculationDate: '2024-01-01',
|
||||||
|
});
|
||||||
|
best = Math.min(best, performance.now() - start);
|
||||||
}
|
}
|
||||||
|
return best;
|
||||||
|
};
|
||||||
|
const calibrationBefore = await calibrate();
|
||||||
// Direct stdout writes show each row as soon as its scenario finishes.
|
// Direct stdout writes show each row as soon as its scenario finishes.
|
||||||
const print = (text: string) => process.stdout.write(`${text}\n`);
|
const print = (text: string) => process.stdout.write(`${text}\n`);
|
||||||
print(
|
print(
|
||||||
`\nCalibration: frozen engine on "two years", best of 3 = ${calibrationMs.toFixed(1)} ms` +
|
`\nCalibration: frozen engine on "two years", best of 3 = ${calibrationBefore.toFixed(1)} ms` +
|
||||||
'\nBudget = limit x calibration; ratio = mean / calibration.\n',
|
'\nBudget = limit x calibration; ratio = mean / calibration.\n',
|
||||||
);
|
);
|
||||||
print(benchmarkSummaryHeader());
|
print(benchmarkSummaryHeader());
|
||||||
const measurements = await runLeavePlannerBenchmark(2, (rows) =>
|
const scenarioRows: BenchmarkMeasurement[][] = [];
|
||||||
print(benchmarkSummaryRow(rows, calibrationMs)),
|
const measurements = await runLeavePlannerBenchmark(2, (rows) => {
|
||||||
|
scenarioRows.push(rows);
|
||||||
|
print(benchmarkSummaryRow(rows, calibrationBefore));
|
||||||
|
});
|
||||||
|
// Load that starts after the first calibration slows the scenarios but not the yardstick;
|
||||||
|
// calibrating again and judging against the slower value absorbs it. Never tighter than before.
|
||||||
|
const calibrationAfter = await calibrate();
|
||||||
|
const calibrationMs = Math.max(calibrationBefore, calibrationAfter);
|
||||||
|
if (calibrationMs !== calibrationBefore) {
|
||||||
|
print(
|
||||||
|
`\nCalibration after the protocol = ${calibrationAfter.toFixed(1)} ms; budgets use the slower: ${calibrationMs.toFixed(1)} ms\n`,
|
||||||
);
|
);
|
||||||
|
print(benchmarkSummaryHeader());
|
||||||
|
for (const rows of scenarioRows) print(benchmarkSummaryRow(rows, calibrationMs));
|
||||||
|
}
|
||||||
for (const [name, placements] of [
|
for (const [name, placements] of [
|
||||||
['two years', 60],
|
['two years', 60],
|
||||||
['three years', 90],
|
['three years', 90],
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
SelectedDates,
|
SelectedDates,
|
||||||
} from './calendar-service';
|
} from './calendar-service';
|
||||||
import { DateRange } from '@angular/material/datepicker';
|
import { DateRange } from '@angular/material/datepicker';
|
||||||
|
import { vi } from '../../testing/vitest-compat';
|
||||||
|
|
||||||
describe('CalendarService', () => {
|
describe('CalendarService', () => {
|
||||||
let service: CalendarService;
|
let service: CalendarService;
|
||||||
@@ -197,6 +198,8 @@ describe('CalendarService', () => {
|
|||||||
service.project({ placements: [], horizon });
|
service.project({ placements: [], horizon });
|
||||||
const day = (key: string) => new Date(`${key}T00:00:00`);
|
const day = (key: string) => new Date(`${key}T00:00:00`);
|
||||||
|
|
||||||
|
afterEach(() => vi.useRealTimers());
|
||||||
|
|
||||||
it('projects weekends, holidays and today for the default planning horizon', () => {
|
it('projects weekends, holidays and today for the default planning horizon', () => {
|
||||||
const { selections } = project({ start: '2026-01-01', end: '2027-01-01' });
|
const { selections } = project({ start: '2026-01-01', end: '2027-01-01' });
|
||||||
expect(selections.dayTypes(day('2026-05-09'))).toContain(DayType.WEEKEND);
|
expect(selections.dayTypes(day('2026-05-09'))).toContain(DayType.WEEKEND);
|
||||||
@@ -207,6 +210,15 @@ describe('CalendarService', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('marks today on the Europe/Paris date when the runtime date is still the day before', () => {
|
||||||
|
vi.setSystemTime(new Date('2026-10-17T22:16:00Z'));
|
||||||
|
const horizon = service.getDefaultPlanningHorizon();
|
||||||
|
expect(horizon.start).toBe('2026-10-18');
|
||||||
|
const { selections } = project(horizon);
|
||||||
|
expect(selections.getClasses(day('2026-10-18'))).toContain(DayType.TODAY);
|
||||||
|
expect(selections.getClasses(day('2026-10-17'))).not.toContain(DayType.TODAY);
|
||||||
|
});
|
||||||
|
|
||||||
it('lists the covered months, including partial boundary months and year changes', () => {
|
it('lists the covered months, including partial boundary months and year changes', () => {
|
||||||
expect(project({ start: '2024-11-20', end: '2025-02-02' }).months).toEqual([
|
expect(project({ start: '2024-11-20', end: '2025-02-02' }).months).toEqual([
|
||||||
{ monthIndex: 10, year: 2024 },
|
{ monthIndex: 10, year: 2024 },
|
||||||
|
|||||||
@@ -85,11 +85,9 @@ export class CalendarService {
|
|||||||
const holidays = new Set(
|
const holidays = new Set(
|
||||||
this.getHolidays(first.getFullYear(), last.getFullYear()).map((h) => h.getTime()),
|
this.getHolidays(first.getFullYear(), last.getFullYear()).map((h) => h.getTime()),
|
||||||
);
|
);
|
||||||
const {
|
const [todayYear, todayMonth, todayDate] = planningDateInParis(new Date())
|
||||||
date: todayDate,
|
.split('-')
|
||||||
month: todayMonth,
|
.map(Number);
|
||||||
year: todayYear,
|
|
||||||
} = this.dateCache.getTodayComponents();
|
|
||||||
const months: CalendarProjection['months'] = [];
|
const months: CalendarProjection['months'] = [];
|
||||||
const ranges: SelectedDate[] = [];
|
const ranges: SelectedDate[] = [];
|
||||||
const cursor = new Date(first);
|
const cursor = new Date(first);
|
||||||
@@ -124,7 +122,7 @@ export class CalendarService {
|
|||||||
);
|
);
|
||||||
if (
|
if (
|
||||||
day.getDate() === todayDate &&
|
day.getDate() === todayDate &&
|
||||||
day.getMonth() === todayMonth &&
|
day.getMonth() + 1 === todayMonth &&
|
||||||
day.getFullYear() === todayYear
|
day.getFullYear() === todayYear
|
||||||
)
|
)
|
||||||
ranges.push(new SelectedDate(DayType.TODAY, new DateRange<Date>(day, day)));
|
ranges.push(new SelectedDate(DayType.TODAY, new DateRange<Date>(day, day)));
|
||||||
|
|||||||
@@ -587,8 +587,8 @@ parentPort.on('message', (data) => globalThis.self.onmessage({ data }));
|
|||||||
);
|
);
|
||||||
|
|
||||||
it('stops during calendar preparation without a plan or an obligation conflict', async () => {
|
it('stops during calendar preparation without a plan or an obligation conflict', async () => {
|
||||||
|
holdForCancelAtStage = 'preparation';
|
||||||
const stages: string[] = [];
|
const stages: string[] = [];
|
||||||
let preparationUpdates = 0;
|
|
||||||
const calculation = createPlanningCalculation({
|
const calculation = createPlanningCalculation({
|
||||||
horizon: { start: '2024-01-01', end: '2027-01-01' },
|
horizon: { start: '2024-01-01', end: '2027-01-01' },
|
||||||
balances: [],
|
balances: [],
|
||||||
@@ -596,7 +596,7 @@ parentPort.on('message', (data) => globalThis.self.onmessage({ data }));
|
|||||||
declaredDays: [{ date: '2024-01-02', type: 'other' }],
|
declaredDays: [{ date: '2024-01-02', type: 'other' }],
|
||||||
progress: (stage) => {
|
progress: (stage) => {
|
||||||
stages.push(stage);
|
stages.push(stage);
|
||||||
if (stage === 'preparation' && ++preparationUpdates === 2) calculation.cancel();
|
if (stage === 'preparation') calculation.cancel();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const outcome = await calculation.outcome;
|
const outcome = await calculation.outcome;
|
||||||
@@ -606,7 +606,7 @@ parentPort.on('message', (data) => globalThis.self.onmessage({ data }));
|
|||||||
});
|
});
|
||||||
if (outcome.kind !== 'PlanningIncomplete') return;
|
if (outcome.kind !== 'PlanningIncomplete') return;
|
||||||
expect(outcome.incumbent).toBeUndefined();
|
expect(outcome.incumbent).toBeUndefined();
|
||||||
expect(preparationUpdates).toBeGreaterThanOrEqual(2);
|
expect(stages).toContain('preparation');
|
||||||
expect(stages).not.toContain('obligations');
|
expect(stages).not.toContain('obligations');
|
||||||
expect(await calculation.explanationError).toBeUndefined();
|
expect(await calculation.explanationError).toBeUndefined();
|
||||||
expect(heartbeats).toBeGreaterThan(0);
|
expect(heartbeats).toBeGreaterThan(0);
|
||||||
|
|||||||
Reference in new issue
Block a user