From fe011b963b6d6e4027eb8552953adcf1c72c4fb1 Mon Sep 17 00:00:00 2001 From: keyobs Date: Sun, 9 Aug 2026 14:29:05 +0200 Subject: [PATCH 01/68] chore(tooling): wire up dx-flow commit workflow with oxlint Adopt @keyobs/dx-flow for husky hooks, commitlint, and release scripts (branch protection on main, .env guard, tsc pre-commit, tests before push to develop). Biome is removed right after setup; oxlint stays the linter/formatter. --- .gitignore | 6 +- .husky/commit-msg | 14 + .husky/pre-commit | 50 ++++ .husky/pre-push | 50 ++++ .npmrc | 1 + commitlint.config.mjs | 70 +++++ package.json | 21 +- pnpm-lock.yaml | 641 +++++++++++++++++++++++++++++++++++++++++- 8 files changed, 846 insertions(+), 7 deletions(-) create mode 100755 .husky/commit-msg create mode 100755 .husky/pre-commit create mode 100755 .husky/pre-push create mode 100644 .npmrc create mode 100644 commitlint.config.mjs diff --git a/.gitignore b/.gitignore index e051a40..53af622 100644 --- a/.gitignore +++ b/.gitignore @@ -27,4 +27,8 @@ devdx # Skills installed via skills-lock.json (regenerated from the lockfile, like node_modules) .agents/skills -.claude/skills \ No newline at end of file +.claude/skills +.claude + +# Husky internal hook runner (regenerated by the "prepare" script) +.husky/_ \ No newline at end of file diff --git a/.husky/commit-msg b/.husky/commit-msg new file mode 100755 index 0000000..5b40392 --- /dev/null +++ b/.husky/commit-msg @@ -0,0 +1,14 @@ +#!/usr/bin/env sh + +echo "📝 Validating commit message..." + +npx --no -- commitlint --edit "$1" +status=$? + +if [ "$status" -eq 0 ]; then + echo "✅ Commit message follows conventional standards." +else + echo "❌ Invalid commit format!" + echo "💡 Use: (): tell why you changed that" + exit "$status" +fi diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100755 index 0000000..2041060 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,50 @@ +#!/usr/bin/env sh + +run_exec() { + if [ -f pnpm-lock.yaml ]; then + pnpm exec "$@" + elif [ -f yarn.lock ]; then + yarn "$@" + elif [ -f bun.lockb ] || [ -f bun.lock ]; then + bunx "$@" + else + npx "$@" + fi +} + +echo "🚦 Pre-commit sanity check..." + +branch="$(git rev-parse --abbrev-ref HEAD)" + +if [ "$branch" = "main" ] || [ "$branch" = "master" ]; then + echo "🔒 Don't commit to $branch." + exit 1 +fi + +staged_files="$(git diff --cached --name-only)" + +if printf "%s\n" "$staged_files" | grep -E '^\.env(\.|$)' >/dev/null; then + echo "❌ Error: You are attempting to commit a .env file!" + exit 1 +fi + +echo "🧹 Linting & formatting staged files..." +run_exec lint-staged +status=$? +if [ "$status" -ne 0 ]; then + exit "$status" +fi + +staged_ts="$(printf "%s\n" "$staged_files" | grep -E '\.(ts|tsx)$' || true)" +if [ -n "$staged_ts" ]; then + echo "🎓 Type checking..." + run_exec tsc --noEmit --pretty false + status=$? + if [ "$status" -ne 0 ]; then + exit "$status" + fi +else + echo "ℹ️ No TS files staged for type checking." +fi + +echo "🎉 Pre-commit checks passed." diff --git a/.husky/pre-push b/.husky/pre-push new file mode 100755 index 0000000..a1159a4 --- /dev/null +++ b/.husky/pre-push @@ -0,0 +1,50 @@ +#!/usr/bin/env sh + +run_script() { + if [ -f pnpm-lock.yaml ]; then + pnpm run "$1" + elif [ -f yarn.lock ]; then + yarn "$1" + elif [ -f bun.lockb ] || [ -f bun.lock ]; then + bun run "$1" + else + npm run "$1" + fi +} + +has_script() { + node -e "const pkg = require('./package.json'); process.exit(pkg.scripts && pkg.scripts['$1'] ? 0 : 1)" +} + +branch="$(git rev-parse --abbrev-ref HEAD)" + +if [ "$branch" = "main" ] || [ "$branch" = "master" ]; then + echo "🔒 Don't push to $branch." + exit 1 +fi + +echo "🧹 DX-FLOW: Running lint fix..." +if has_script lint:fix; then + run_script lint:fix + status=$? + if [ "$status" -ne 0 ]; then + exit "$status" + fi +fi + +echo "🧐 DX-FLOW: Running tests..." +if ! has_script test:run; then + exit 0 +fi + +run_script test:run +status=$? + +if [ "$branch" = "develop" ] && [ "$status" -ne 0 ]; then + echo "☠️ Tests failed. Push to develop aborted." + exit 1 +fi + +if [ "$status" -ne 0 ]; then + echo "⚠️ Tests failed, but allowing push on $branch." +fi diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..dad9f99 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +@keyobs:registry=https://npm.pkg.github.com diff --git a/commitlint.config.mjs b/commitlint.config.mjs new file mode 100644 index 0000000..a48896a --- /dev/null +++ b/commitlint.config.mjs @@ -0,0 +1,70 @@ +const headerPattern = /^(?:([a-zA-Z0-9-]+):\s)?(\w+)\(([^)]+)\):\s(.+)$/; + +export default { + extends: ["@commitlint/config-conventional"], + parserPreset: { + parserOpts: { + headerPattern, + headerCorrespondence: ["ticket", "type", "scope", "subject"], + }, + }, + plugins: [ + { + rules: { + "type-enum-insensitive": ({ type }, when, value) => { + if (!type) return [true]; + const isFound = value.some((val) => val.toLowerCase() === type.toLowerCase()); + return [ + when === "never" ? !isFound : isFound, + `type must be one of [${value.join(", ")}]`, + ]; + }, + }, + }, + ], + rules: { + "scope-empty": [2, "never"], + "type-case": [0], + "type-enum": [0], + "subject-case": [0], + "type-enum-insensitive": [ + 2, + "always", + [ + "feat", + "fix", + "doc", + "style", + "refactor", + "perf", + "test", + "build", + "ci", + "chore", + "core", + "revert", + "merge", + "config", + "clean" + ], + ], + }, +}; + +/* +- build — Build system or dependencies +- chore — Maintenance or tooling tasks +- ci — Continuous integration changes +- config — Configuration changes +- core — Application logic or behavior +- doc — Documentation changes +- feat — New feature +- fix — Bug fix +- merge — Merge branches +- perf — Performance improvements +- refactor — Code restructure, same behavior +- revert — Revert previous commit +- style — Formatting, no logic change +- test — Add or update tests +- clean — Code cleanup, no logic change +*/ diff --git a/package.json b/package.json index 4a6b9c1..c7387e4 100644 --- a/package.json +++ b/package.json @@ -7,19 +7,38 @@ "dev": "vite", "build": "tsc -b && vite build", "lint": "oxlint", - "preview": "vite preview" + "preview": "vite preview", + "prepare": "husky", + "lint:fix": "oxlint --fix", + "check": "tsc --noEmit", + "release:patch": "dx-flow release patch", + "release:minor": "dx-flow release minor", + "release:major": "dx-flow release major", + "qa:new": "dx-flow qa:new", + "qa:validate": "dx-flow qa:validate", + "qa:reset": "dx-flow qa:reset" }, "dependencies": { "react": "^19.2.8", "react-dom": "^19.2.8" }, "devDependencies": { + "@commitlint/cli": "^21.2.1", + "@commitlint/config-conventional": "^21.2.0", + "@keyobs/dx-flow": "3.1.3", "@types/node": "^24.13.3", "@types/react": "^19.2.17", "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^6.0.4", + "husky": "^9.1.7", + "lint-staged": "^17.3.0", "oxlint": "^1.75.0", "typescript": "~6.0.2", "vite": "^8.2.0" + }, + "lint-staged": { + "*.{js,jsx,ts,tsx}": [ + "oxlint --fix" + ] } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a76ffd6..0d3ed03 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -15,6 +15,15 @@ importers: specifier: ^19.2.8 version: 19.2.8(react@19.2.8) devDependencies: + '@commitlint/cli': + specifier: ^21.2.1 + version: 21.2.1(@types/node@24.13.3)(conventional-commits-parser@7.1.2)(typescript@6.0.3) + '@commitlint/config-conventional': + specifier: ^21.2.0 + version: 21.2.0 + '@keyobs/dx-flow': + specifier: 3.1.3 + version: 3.1.3 '@types/node': specifier: ^24.13.3 version: 24.13.3 @@ -26,7 +35,13 @@ importers: version: 19.2.4(@types/react@19.2.18) '@vitejs/plugin-react': specifier: ^6.0.4 - version: 6.0.5(vite@8.2.0(@types/node@24.13.3)) + version: 6.0.5(vite@8.2.0(@types/node@24.13.3)(jiti@2.6.1)(yaml@2.9.0)) + husky: + specifier: ^9.1.7 + version: 9.1.7 + lint-staged: + specifier: ^17.3.0 + version: 17.3.0 oxlint: specifier: ^1.75.0 version: 1.77.0 @@ -35,10 +50,108 @@ importers: version: 6.0.3 vite: specifier: ^8.2.0 - version: 8.2.0(@types/node@24.13.3) + version: 8.2.0(@types/node@24.13.3)(jiti@2.6.1)(yaml@2.9.0) packages: + '@babel/code-frame@7.29.7': + resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.29.7': + resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==} + engines: {node: '>=6.9.0'} + + '@commitlint/cli@21.2.1': + resolution: {integrity: sha512-blsZGe29hJ72VGEFVl72IVYX+1vsfINpjA9yWQA6i7OKD/McGEOXg08sKIRKjFk4JvzhV/9n0l3i6NooPLTNfg==} + engines: {node: '>=22.12.0'} + hasBin: true + + '@commitlint/config-conventional@21.2.0': + resolution: {integrity: sha512-Qf8WRDVcyVd14if6VTWenebxFbKnVnbzPUJjlzjkyJGeHK2xCGd63Dr1XZzj0plXKQb9P0BfOxoc1HVeCo2BWQ==} + engines: {node: '>=22.12.0'} + + '@commitlint/config-validator@21.2.0': + resolution: {integrity: sha512-t7AzNHAKeIdo/3NRGwzpufKHsKkPHmFs/56N2Fnsh0/r0rGtnQzTxk6vnFgjaGr4hdSQKNB50/KAhR9Yk4LJKA==} + engines: {node: '>=22.12.0'} + + '@commitlint/ensure@21.2.0': + resolution: {integrity: sha512-76IF9vDNS13lAzEEik9eKwzt8f9hYhWiwVXZ2AnyLCz5/f511FsEQ3pw1X3/zSQpdRLQU7i5qDMVKyXi1GWjSg==} + engines: {node: '>=22.12.0'} + + '@commitlint/execute-rule@21.0.1': + resolution: {integrity: sha512-RifH+FmImozKBE6mozhF4K3r2RRKP7SMi/Q/zLCmExtp5e05lhHOUYqGBlFBAGNHaZxU/WYw1XuugYK9jQzqnA==} + engines: {node: '>=22.12.0'} + + '@commitlint/format@21.2.0': + resolution: {integrity: sha512-c4q64xaav2U83t7k7RyzJerBZurPer7FxUOY0RL5L/6CZijZ7K+s6HIBGIghj0ey1P2+seRX0J9XQYtDued6tg==} + engines: {node: '>=22.12.0'} + + '@commitlint/is-ignored@21.2.0': + resolution: {integrity: sha512-4/eB0vBN7L88O/oC4ajAEqi7j2ZfNgxl/+11RfAV9YosejZgDXhY2C9VcHnHJhOzPLoSy5P3Mg/46kqeyJfXKw==} + engines: {node: '>=22.12.0'} + + '@commitlint/lint@21.2.0': + resolution: {integrity: sha512-ceO5dp9pLjEZ6y6qbq/uXWXDPykqqlTsyzoQ0NzecpisSJhK3kTy9qzQoPeJuWG/IMNdV1lO0RgmzqoAlSi1uw==} + engines: {node: '>=22.12.0'} + + '@commitlint/load@21.2.0': + resolution: {integrity: sha512-RjlzWQqruRwIenJEfZtq7kG97co97nKoHpflE5YnF61tDLXxHPrdWImgzw6VL6MlFyaOcVlk74eBV8ZQmc3oIA==} + engines: {node: '>=22.12.0'} + + '@commitlint/message@21.2.0': + resolution: {integrity: sha512-YxGoiXD/HXNXLJPrQwE5poXa+XH0CBEm+mdvbHQP0g6MV/dmJyUFCzPNzZbxL93GvZ70TmtTK0Z0/IBpAqHv8g==} + engines: {node: '>=22.12.0'} + + '@commitlint/parse@21.2.0': + resolution: {integrity: sha512-QHWxG4d0PLTF634/AdyZ0MQS+CLn5YOuJlCFhMMlSGKFxzYGUetkHBj18xgBD+6fVzUrA2lrCdi/vlS2f/oYXg==} + engines: {node: '>=22.12.0'} + + '@commitlint/read@21.2.1': + resolution: {integrity: sha512-hUW7EJQnNTL0vPOmVMNK4CrnrNBN0nN+JJHReFkdHO5y4iyHeEmTBwuC15OCqUTjxWo7idnH1LftfpWVIaPWIA==} + engines: {node: '>=22.12.0'} + + '@commitlint/resolve-extends@21.2.0': + resolution: {integrity: sha512-4O/1j51+79Wth9s/MGxt/5gs0XYLDgNlYpltQfhAvLE0itusLKs9zruxbiNg1oOkmkb9L9L4USYGjEj7n87NxA==} + engines: {node: '>=22.12.0'} + + '@commitlint/rules@21.2.0': + resolution: {integrity: sha512-C2yXMNpiB8ETZKfx5JD8+ExgF8vTU1VQMKPSUUYwqKpw9oJWQBrlXBpdU038mj2WPjof7o9UzFpmTyBeGMZwZg==} + engines: {node: '>=22.12.0'} + + '@commitlint/to-lines@21.0.1': + resolution: {integrity: sha512-bd1BFII7p1EQZre9Kaj+kKaMFP3cFCdt21K7DItVux9XP5WjLgJ0/Uy1pJJh9aPwVJ6SKg62PxqlZaHI8hQAXw==} + engines: {node: '>=22.12.0'} + + '@commitlint/top-level@21.2.0': + resolution: {integrity: sha512-Y5gmQ+KxzqCrBFJfLvFEPvvwD3LDiNZoTT2yeFBm96M8qhmqSzQc5DvX3rheAaAMjyIvMXOCLS/mWfdpONsjyQ==} + engines: {node: '>=22.12.0'} + + '@commitlint/types@21.2.0': + resolution: {integrity: sha512-7zVFCDB2reMvJH5dmbKnOQPjZEvjdJTH8jc0U/PIPU1r3/+vf5pD1HlfitV2MWsWXrvu7u39iY1lyLUPOaN0Gw==} + engines: {node: '>=22.12.0'} + + '@conventional-changelog/git-client@3.1.1': + resolution: {integrity: sha512-w/q+UIVdWQMgXlziPIYfPlyDud+H8kcvSCzDQQoc/gid7yZzb6eNfAkNN4UlEcS2cVVh924jevsOIb36d0In2g==} + engines: {node: '>=22'} + peerDependencies: + conventional-commits-filter: ^6.0.1 + conventional-commits-parser: ^7.1.2 + peerDependenciesMeta: + conventional-commits-filter: + optional: true + conventional-commits-parser: + optional: true + + '@conventional-changelog/template@1.2.1': + resolution: {integrity: sha512-TzlTVpKPjaqW6qOYjQcYUDuGsLCNsvFHVBXkYGTAnf5V37jCWrE5haKNXzz0WZUtVHjrpV76L1buANjwXMfT8w==} + engines: {node: '>=22'} + + '@keyobs/dx-flow@3.1.3': + resolution: {integrity: sha512-e0wMZ3cv3dpHHvq2UAfDDxQezV7zdV2aFBSM/iG/i73q7EMdAJkxjPP/scrIhla7bSKc3qVFOVTMhvKB3vyRyw==, tarball: https://npm.pkg.github.com/download/@keyobs/dx-flow/3.1.3/a9d82ad83e1ceea8319c51e7f6e618929f3a1698} + engines: {node: '>=22.21.1'} + hasBin: true + '@oxc-project/types@0.143.0': resolution: {integrity: sha512-u6JZdLBTLotrNC9Vd6vPssINdzcCzleKAH6EJKImQb7GtYvX5keN2dxkoK44stCc4tffE6QQRtZTXVSzsLUlWA==} @@ -257,6 +370,14 @@ packages: '@rolldown/pluginutils@1.0.1': resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} + '@simple-libs/child-process-utils@2.0.0': + resolution: {integrity: sha512-dvNoRKLijXnD0XoJAz94pbNuB5GQgDr55UhpSPhffDkTT0Cmcqh9jSCOtwfT2d4H6MI9E7c4SgtMuJXZ6F3c6A==} + engines: {node: '>=22'} + + '@simple-libs/stream-utils@2.0.0': + resolution: {integrity: sha512-fCTuZK4QBa+39Oz9l4OGfJfz+GpwCp3AqO7Zch3to99xHPgstVsRFpeQ8LNd2o1Gv8raL2mCFwiaHh7bFSp5DQ==} + engines: {node: '>=22'} + '@types/node@24.13.3': resolution: {integrity: sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==} @@ -281,6 +402,62 @@ packages: babel-plugin-react-compiler: optional: true + ajv@8.20.0: + resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==} + + ansi-regex@6.2.2: + resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} + engines: {node: '>=12'} + + ansi-styles@6.2.3: + resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} + engines: {node: '>=12'} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + argue-cli@3.1.0: + resolution: {integrity: sha512-DhBpBfXL4SS2uC0N922MMajKR3CdrTG0u2or1PNYgXMsrSzViJrbtvT0nCLlLGUI0plam/ZZCs7aAauHtW9thw==} + engines: {node: '>=22'} + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + cliui@9.0.1: + resolution: {integrity: sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==} + engines: {node: '>=20'} + + conventional-changelog-angular@9.2.1: + resolution: {integrity: sha512-oWSL6ZhnXbYraOFTK3PgRAQJ8fADDAEv5K6AdeyQPLvjFmhG8+ejL0jZZp/R7vTmGJaBvZEE+sE7dB4bCv7sAw==} + engines: {node: '>=22'} + + conventional-changelog-conventionalcommits@10.2.1: + resolution: {integrity: sha512-n4Kr1HFMTf3iMbES0TMxKIcYtUUv4rKqyQQp2JwfOEfFCOfGT3Tq4mCyJ8S9/YPyWhydjfKrrvnyl+gCjA+mJQ==} + engines: {node: '>=22'} + + conventional-commits-parser@7.1.2: + resolution: {integrity: sha512-O+x4N2yH+ijvqWlIyTHsXTAP+algNWgGbjY2duCe8w2vUMvUB95cLRslCPfTMQyLAKlet3bhZTdu6ozn4M+QJQ==} + engines: {node: '>=22'} + hasBin: true + + cosmiconfig-typescript-loader@6.3.0: + resolution: {integrity: sha512-Akr82WH1Wfqatyiqpj8HDkO2o2KmJRu1FhKfSNJP3K4IdXwHfEyL7MOb62i1AGQVLtIQM+iCE9CGOtrfhR+mmA==} + engines: {node: '>=v18'} + peerDependencies: + '@types/node': '*' + cosmiconfig: '>=9' + typescript: '>=5' + + cosmiconfig@9.0.2: + resolution: {integrity: sha512-gtTZxTDau1wL7Y7zifc2dd8jHSK/k6BTx/2Xp/BpdlAdnlYWFVt7qhJqgwi7637yRwRQ3qL4ZidbB4I8tA5VOg==} + engines: {node: '>=14'} + peerDependencies: + typescript: '>=4.9.5' + peerDependenciesMeta: + typescript: + optional: true + csstype@3.2.3: resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} @@ -288,6 +465,29 @@ packages: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} + emoji-regex@10.6.0: + resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==} + + env-paths@2.2.1: + resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} + engines: {node: '>=6'} + + error-ex@1.3.4: + resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==} + + es-toolkit@1.50.0: + resolution: {integrity: sha512-OyZKhUVvEep9ITEiwHn8GKnMRQIVqoSIX7WnRbkWgJkllCujilqP2rD0u979tkl8wqyc8ICwlc1UBVv/Sl1G6w==} + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-uri@3.1.5: + resolution: {integrity: sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw==} + fdir@6.5.0: resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} engines: {node: '>=12.0.0'} @@ -302,6 +502,55 @@ packages: engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + get-east-asian-width@1.6.0: + resolution: {integrity: sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==} + engines: {node: '>=18'} + + global-directory@5.0.0: + resolution: {integrity: sha512-1pgFdhK3J2LeM+dVf2Pd424yHx2ou338lC0ErNP2hPx4j8eW1Sp0XqSjNxtk6Tc4Kr5wlWtSvz8cn2yb7/SG/w==} + engines: {node: '>=20'} + + husky@9.1.7: + resolution: {integrity: sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==} + engines: {node: '>=18'} + hasBin: true + + import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} + engines: {node: '>=6'} + + ini@6.0.0: + resolution: {integrity: sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ==} + engines: {node: ^20.17.0 || >=22.9.0} + + is-arrayish@0.2.1: + resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + + is-plain-obj@4.1.0: + resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} + engines: {node: '>=12'} + + jiti@2.6.1: + resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} + hasBin: true + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-yaml@4.3.1: + resolution: {integrity: sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==} + hasBin: true + + json-parse-even-better-errors@2.3.1: + resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + lightningcss-android-arm64@1.33.0: resolution: {integrity: sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==} engines: {node: '>= 12.0.0'} @@ -376,6 +625,14 @@ packages: resolution: {integrity: sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==} engines: {node: '>= 12.0.0'} + lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + + lint-staged@17.3.0: + resolution: {integrity: sha512-woZS3vNe3UKqBaLPvbLOtKRY4tLANpWQhom12MGWqC8Mh1lCOO+WgSwmX2amjJAqTY9BkXYW87fCUH5H9Ph6xw==} + engines: {node: '>=22.22.1'} + hasBin: true + nanoid@3.3.17: resolution: {integrity: sha512-xQLf0A3HOMlgHq0n247/LRuAOYmB7dXJ/DvAxGvsSBij45XtBSmQycu+F8ODbHwns/XyFZagyL1+J0Offw1E0g==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} @@ -394,6 +651,14 @@ packages: vite-plus: optional: true + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + parse-json@5.2.0: + resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} + engines: {node: '>=8'} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -414,6 +679,18 @@ packages: resolution: {integrity: sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==} engines: {node: '>=0.10.0'} + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + resolve-from@5.0.0: + resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} + engines: {node: '>=8'} + rolldown@1.2.3: resolution: {integrity: sha512-rn9wpmxplLf7NLNyCk9FyWh3FM43DbY8jOzCdEPzH7uflhTftRbCEpqi6Ly2osgoU8OwObtmavMbWLaWy4LX7A==} engines: {node: ^20.19.0 || >=22.12.0} @@ -422,10 +699,35 @@ packages: scheduler@0.27.0: resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} + semver@7.8.5: + resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} + engines: {node: '>=10'} + hasBin: true + source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} + string-argv@0.3.2: + resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==} + engines: {node: '>=0.6.19'} + + string-width@7.2.0: + resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} + engines: {node: '>=18'} + + string-width@8.2.2: + resolution: {integrity: sha512-GaPUh5gfdrYzqeVNZvUfT23vYYxXzKYidUcnMtJg/3rxRV63EFZy3k6xfKlmfeJD0176lnUV/Usr3XcwSvFzpg==} + engines: {node: '>=20'} + + strip-ansi@7.2.0: + resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==} + engines: {node: '>=12'} + + tinyexec@1.3.0: + resolution: {integrity: sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ==} + engines: {node: '>=18'} + tinyglobby@0.2.17: resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} engines: {node: '>=12.0.0'} @@ -481,8 +783,158 @@ packages: yaml: optional: true + wrap-ansi@9.0.2: + resolution: {integrity: sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==} + engines: {node: '>=18'} + + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + yaml@2.9.0: + resolution: {integrity: sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==} + engines: {node: '>= 14.6'} + hasBin: true + + yargs-parser@22.0.0: + resolution: {integrity: sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==} + engines: {node: ^20.19.0 || ^22.12.0 || >=23} + + yargs@18.1.0: + resolution: {integrity: sha512-2rAgRKu54VsHkqI0/tYkmluGXHD4KW7yZoycuqDQ15QOTnc2VVfy0nN/1eMhnQLO00A+dwtK20xuCnc1YGeUyg==} + engines: {node: ^20.19.0 || ^22.12.0 || >=23} + snapshots: + '@babel/code-frame@7.29.7': + dependencies: + '@babel/helper-validator-identifier': 7.29.7 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/helper-validator-identifier@7.29.7': {} + + '@commitlint/cli@21.2.1(@types/node@24.13.3)(conventional-commits-parser@7.1.2)(typescript@6.0.3)': + dependencies: + '@commitlint/config-conventional': 21.2.0 + '@commitlint/format': 21.2.0 + '@commitlint/lint': 21.2.0 + '@commitlint/load': 21.2.0(@types/node@24.13.3)(typescript@6.0.3) + '@commitlint/read': 21.2.1(conventional-commits-parser@7.1.2) + '@commitlint/types': 21.2.0 + tinyexec: 1.3.0 + yargs: 18.1.0 + transitivePeerDependencies: + - '@types/node' + - conventional-commits-filter + - conventional-commits-parser + - typescript + + '@commitlint/config-conventional@21.2.0': + dependencies: + '@commitlint/types': 21.2.0 + conventional-changelog-conventionalcommits: 10.2.1 + + '@commitlint/config-validator@21.2.0': + dependencies: + '@commitlint/types': 21.2.0 + ajv: 8.20.0 + + '@commitlint/ensure@21.2.0': + dependencies: + '@commitlint/types': 21.2.0 + es-toolkit: 1.50.0 + + '@commitlint/execute-rule@21.0.1': {} + + '@commitlint/format@21.2.0': + dependencies: + '@commitlint/types': 21.2.0 + picocolors: 1.1.1 + + '@commitlint/is-ignored@21.2.0': + dependencies: + '@commitlint/types': 21.2.0 + semver: 7.8.5 + + '@commitlint/lint@21.2.0': + dependencies: + '@commitlint/is-ignored': 21.2.0 + '@commitlint/parse': 21.2.0 + '@commitlint/rules': 21.2.0 + '@commitlint/types': 21.2.0 + + '@commitlint/load@21.2.0(@types/node@24.13.3)(typescript@6.0.3)': + dependencies: + '@commitlint/config-validator': 21.2.0 + '@commitlint/execute-rule': 21.0.1 + '@commitlint/resolve-extends': 21.2.0 + '@commitlint/types': 21.2.0 + cosmiconfig: 9.0.2(typescript@6.0.3) + cosmiconfig-typescript-loader: 6.3.0(@types/node@24.13.3)(cosmiconfig@9.0.2(typescript@6.0.3))(typescript@6.0.3) + es-toolkit: 1.50.0 + is-plain-obj: 4.1.0 + picocolors: 1.1.1 + transitivePeerDependencies: + - '@types/node' + - typescript + + '@commitlint/message@21.2.0': {} + + '@commitlint/parse@21.2.0': + dependencies: + '@commitlint/types': 21.2.0 + conventional-changelog-angular: 9.2.1 + conventional-commits-parser: 7.1.2 + + '@commitlint/read@21.2.1(conventional-commits-parser@7.1.2)': + dependencies: + '@commitlint/top-level': 21.2.0 + '@commitlint/types': 21.2.0 + '@conventional-changelog/git-client': 3.1.1(conventional-commits-parser@7.1.2) + tinyexec: 1.3.0 + transitivePeerDependencies: + - conventional-commits-filter + - conventional-commits-parser + + '@commitlint/resolve-extends@21.2.0': + dependencies: + '@commitlint/config-validator': 21.2.0 + '@commitlint/types': 21.2.0 + es-toolkit: 1.50.0 + global-directory: 5.0.0 + resolve-from: 5.0.0 + + '@commitlint/rules@21.2.0': + dependencies: + '@commitlint/ensure': 21.2.0 + '@commitlint/message': 21.2.0 + '@commitlint/to-lines': 21.0.1 + '@commitlint/types': 21.2.0 + + '@commitlint/to-lines@21.0.1': {} + + '@commitlint/top-level@21.2.0': + dependencies: + escalade: 3.2.0 + + '@commitlint/types@21.2.0': + dependencies: + conventional-commits-parser: 7.1.2 + picocolors: 1.1.1 + + '@conventional-changelog/git-client@3.1.1(conventional-commits-parser@7.1.2)': + dependencies: + '@simple-libs/child-process-utils': 2.0.0 + '@simple-libs/stream-utils': 2.0.0 + semver: 7.8.5 + optionalDependencies: + conventional-commits-parser: 7.1.2 + + '@conventional-changelog/template@1.2.1': {} + + '@keyobs/dx-flow@3.1.3': {} + '@oxc-project/types@0.143.0': {} '@oxlint/binding-android-arm-eabi@1.77.0': @@ -586,6 +1038,12 @@ snapshots: '@rolldown/pluginutils@1.0.1': {} + '@simple-libs/child-process-utils@2.0.0': + dependencies: + '@simple-libs/stream-utils': 2.0.0 + + '@simple-libs/stream-utils@2.0.0': {} + '@types/node@24.13.3': dependencies: undici-types: 7.18.2 @@ -598,15 +1056,83 @@ snapshots: dependencies: csstype: 3.2.3 - '@vitejs/plugin-react@6.0.5(vite@8.2.0(@types/node@24.13.3))': + '@vitejs/plugin-react@6.0.5(vite@8.2.0(@types/node@24.13.3)(jiti@2.6.1)(yaml@2.9.0))': dependencies: '@rolldown/pluginutils': 1.0.1 - vite: 8.2.0(@types/node@24.13.3) + vite: 8.2.0(@types/node@24.13.3)(jiti@2.6.1)(yaml@2.9.0) + + ajv@8.20.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.5 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + + ansi-regex@6.2.2: {} + + ansi-styles@6.2.3: {} + + argparse@2.0.1: {} + + argue-cli@3.1.0: {} + + callsites@3.1.0: {} + + cliui@9.0.1: + dependencies: + string-width: 7.2.0 + strip-ansi: 7.2.0 + wrap-ansi: 9.0.2 + + conventional-changelog-angular@9.2.1: + dependencies: + '@conventional-changelog/template': 1.2.1 + + conventional-changelog-conventionalcommits@10.2.1: + dependencies: + '@conventional-changelog/template': 1.2.1 + + conventional-commits-parser@7.1.2: + dependencies: + '@simple-libs/stream-utils': 2.0.0 + argue-cli: 3.1.0 + + cosmiconfig-typescript-loader@6.3.0(@types/node@24.13.3)(cosmiconfig@9.0.2(typescript@6.0.3))(typescript@6.0.3): + dependencies: + '@types/node': 24.13.3 + cosmiconfig: 9.0.2(typescript@6.0.3) + jiti: 2.6.1 + typescript: 6.0.3 + + cosmiconfig@9.0.2(typescript@6.0.3): + dependencies: + env-paths: 2.2.1 + import-fresh: 3.3.1 + js-yaml: 4.3.1 + parse-json: 5.2.0 + optionalDependencies: + typescript: 6.0.3 csstype@3.2.3: {} detect-libc@2.1.2: {} + emoji-regex@10.6.0: {} + + env-paths@2.2.1: {} + + error-ex@1.3.4: + dependencies: + is-arrayish: 0.2.1 + + es-toolkit@1.50.0: {} + + escalade@3.2.0: {} + + fast-deep-equal@3.1.3: {} + + fast-uri@3.1.5: {} + fdir@6.5.0(picomatch@4.0.5): optionalDependencies: picomatch: 4.0.5 @@ -614,6 +1140,39 @@ snapshots: fsevents@2.3.3: optional: true + get-caller-file@2.0.5: {} + + get-east-asian-width@1.6.0: {} + + global-directory@5.0.0: + dependencies: + ini: 6.0.0 + + husky@9.1.7: {} + + import-fresh@3.3.1: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + ini@6.0.0: {} + + is-arrayish@0.2.1: {} + + is-plain-obj@4.1.0: {} + + jiti@2.6.1: {} + + js-tokens@4.0.0: {} + + js-yaml@4.3.1: + dependencies: + argparse: 2.0.1 + + json-parse-even-better-errors@2.3.1: {} + + json-schema-traverse@1.0.0: {} + lightningcss-android-arm64@1.33.0: optional: true @@ -663,6 +1222,16 @@ snapshots: lightningcss-win32-arm64-msvc: 1.33.0 lightningcss-win32-x64-msvc: 1.33.0 + lines-and-columns@1.2.4: {} + + lint-staged@17.3.0: + dependencies: + picomatch: 4.0.5 + string-argv: 0.3.2 + tinyexec: 1.3.0 + optionalDependencies: + yaml: 2.9.0 + nanoid@3.3.17: {} oxlint@1.77.0: @@ -687,6 +1256,17 @@ snapshots: '@oxlint/binding-win32-ia32-msvc': 1.77.0 '@oxlint/binding-win32-x64-msvc': 1.77.0 + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + parse-json@5.2.0: + dependencies: + '@babel/code-frame': 7.29.7 + error-ex: 1.3.4 + json-parse-even-better-errors: 2.3.1 + lines-and-columns: 1.2.4 + picocolors@1.1.1: {} picomatch@4.0.5: {} @@ -704,6 +1284,12 @@ snapshots: react@19.2.8: {} + require-from-string@2.0.2: {} + + resolve-from@4.0.0: {} + + resolve-from@5.0.0: {} + rolldown@1.2.3: dependencies: '@oxc-project/types': 0.143.0 @@ -726,8 +1312,29 @@ snapshots: scheduler@0.27.0: {} + semver@7.8.5: {} + source-map-js@1.2.1: {} + string-argv@0.3.2: {} + + string-width@7.2.0: + dependencies: + emoji-regex: 10.6.0 + get-east-asian-width: 1.6.0 + strip-ansi: 7.2.0 + + string-width@8.2.2: + dependencies: + get-east-asian-width: 1.6.0 + strip-ansi: 7.2.0 + + strip-ansi@7.2.0: + dependencies: + ansi-regex: 6.2.2 + + tinyexec@1.3.0: {} + tinyglobby@0.2.17: dependencies: fdir: 6.5.0(picomatch@4.0.5) @@ -737,7 +1344,7 @@ snapshots: undici-types@7.18.2: {} - vite@8.2.0(@types/node@24.13.3): + vite@8.2.0(@types/node@24.13.3)(jiti@2.6.1)(yaml@2.9.0): dependencies: lightningcss: 1.33.0 picomatch: 4.0.5 @@ -747,3 +1354,27 @@ snapshots: optionalDependencies: '@types/node': 24.13.3 fsevents: 2.3.3 + jiti: 2.6.1 + yaml: 2.9.0 + + wrap-ansi@9.0.2: + dependencies: + ansi-styles: 6.2.3 + string-width: 7.2.0 + strip-ansi: 7.2.0 + + y18n@5.0.8: {} + + yaml@2.9.0: + optional: true + + yargs-parser@22.0.0: {} + + yargs@18.1.0: + dependencies: + cliui: 9.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + string-width: 8.2.2 + y18n: 5.0.8 + yargs-parser: 22.0.0 From decfd0bbf5ed5cce8581d31a1feb0f661ba3ec46 Mon Sep 17 00:00:00 2001 From: keyobs Date: Sun, 9 Aug 2026 14:46:15 +0200 Subject: [PATCH 02/68] build(spike): install vertical-spike dependencies and configure vitest Add zod, three/@react-three/fiber/@react-three/drei, ml-pca for the Grid -> FeatureExtractor -> GeometryDescriptor -> PCA -> point cloud spike (ADR-0002). Wire vitest into vite.config.ts and add test/test:run scripts so dx-flow's pre-push hook has something to run. --- package.json | 13 +- pnpm-lock.yaml | 865 +++++++++++++++++++++++++++++++++++++++++++++++++ vite.config.ts | 6 +- 3 files changed, 881 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index c7387e4..8c094bd 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,8 @@ "prepare": "husky", "lint:fix": "oxlint --fix", "check": "tsc --noEmit", + "test": "vitest", + "test:run": "vitest run", "release:patch": "dx-flow release patch", "release:minor": "dx-flow release minor", "release:major": "dx-flow release major", @@ -19,8 +21,13 @@ "qa:reset": "dx-flow qa:reset" }, "dependencies": { + "@react-three/drei": "^10.7.8", + "@react-three/fiber": "^9.7.0", + "ml-pca": "^4.1.1", "react": "^19.2.8", - "react-dom": "^19.2.8" + "react-dom": "^19.2.8", + "three": "^0.185.1", + "zod": "^4.4.3" }, "devDependencies": { "@commitlint/cli": "^21.2.1", @@ -29,12 +36,14 @@ "@types/node": "^24.13.3", "@types/react": "^19.2.17", "@types/react-dom": "^19.2.3", + "@types/three": "^0.185.4", "@vitejs/plugin-react": "^6.0.4", "husky": "^9.1.7", "lint-staged": "^17.3.0", "oxlint": "^1.75.0", "typescript": "~6.0.2", - "vite": "^8.2.0" + "vite": "^8.2.0", + "vitest": "^4.1.10" }, "lint-staged": { "*.{js,jsx,ts,tsx}": [ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0d3ed03..813e6c7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,12 +8,27 @@ importers: .: dependencies: + '@react-three/drei': + specifier: ^10.7.8 + version: 10.7.8(@react-three/fiber@9.7.0(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(three@0.185.1))(@types/react@19.2.18)(@types/three@0.185.4)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(three@0.185.1) + '@react-three/fiber': + specifier: ^9.7.0 + version: 9.7.0(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(three@0.185.1) + ml-pca: + specifier: ^4.1.1 + version: 4.1.1 react: specifier: ^19.2.8 version: 19.2.8 react-dom: specifier: ^19.2.8 version: 19.2.8(react@19.2.8) + three: + specifier: ^0.185.1 + version: 0.185.1 + zod: + specifier: ^4.4.3 + version: 4.4.3 devDependencies: '@commitlint/cli': specifier: ^21.2.1 @@ -33,6 +48,9 @@ importers: '@types/react-dom': specifier: ^19.2.3 version: 19.2.4(@types/react@19.2.18) + '@types/three': + specifier: ^0.185.4 + version: 0.185.4 '@vitejs/plugin-react': specifier: ^6.0.4 version: 6.0.5(vite@8.2.0(@types/node@24.13.3)(jiti@2.6.1)(yaml@2.9.0)) @@ -51,6 +69,9 @@ importers: vite: specifier: ^8.2.0 version: 8.2.0(@types/node@24.13.3)(jiti@2.6.1)(yaml@2.9.0) + vitest: + specifier: ^4.1.10 + version: 4.1.10(@types/node@24.13.3)(vite@8.2.0(@types/node@24.13.3)(jiti@2.6.1)(yaml@2.9.0)) packages: @@ -62,6 +83,10 @@ packages: resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==} engines: {node: '>=6.9.0'} + '@babel/runtime@7.29.7': + resolution: {integrity: sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==} + engines: {node: '>=6.9.0'} + '@commitlint/cli@21.2.1': resolution: {integrity: sha512-blsZGe29hJ72VGEFVl72IVYX+1vsfINpjA9yWQA6i7OKD/McGEOXg08sKIRKjFk4JvzhV/9n0l3i6NooPLTNfg==} engines: {node: '>=22.12.0'} @@ -147,11 +172,25 @@ packages: resolution: {integrity: sha512-TzlTVpKPjaqW6qOYjQcYUDuGsLCNsvFHVBXkYGTAnf5V37jCWrE5haKNXzz0WZUtVHjrpV76L1buANjwXMfT8w==} engines: {node: '>=22'} + '@dimforge/rapier3d-compat@0.12.0': + resolution: {integrity: sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow==} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + '@keyobs/dx-flow@3.1.3': resolution: {integrity: sha512-e0wMZ3cv3dpHHvq2UAfDDxQezV7zdV2aFBSM/iG/i73q7EMdAJkxjPP/scrIhla7bSKc3qVFOVTMhvKB3vyRyw==, tarball: https://npm.pkg.github.com/download/@keyobs/dx-flow/3.1.3/a9d82ad83e1ceea8319c51e7f6e618929f3a1698} engines: {node: '>=22.21.1'} hasBin: true + '@mediapipe/tasks-vision@0.10.17': + resolution: {integrity: sha512-CZWV/q6TTe8ta61cZXjfnnHsfWIdFhms03M9T7Cnd5y2mdpylJM0rF1qRq+wsQVRMLz1OYPVEBU9ph2Bx8cxrg==} + + '@monogrid/gainmap-js@3.4.0': + resolution: {integrity: sha512-2Z0FATFHaoYJ8b+Y4y4Hgfn3FRFwuU5zRrk+9dFWp4uGAdHGqVEdP7HP+gLA3X469KXHmfupJaUbKo1b/aDKIg==} + peerDependencies: + three: '>= 0.159.0' + '@oxc-project/types@0.143.0': resolution: {integrity: sha512-u6JZdLBTLotrNC9Vd6vPssINdzcCzleKAH6EJKImQb7GtYvX5keN2dxkoK44stCc4tffE6QQRtZTXVSzsLUlWA==} @@ -277,6 +316,42 @@ packages: cpu: [x64] os: [win32] + '@react-three/drei@10.7.8': + resolution: {integrity: sha512-rJXyuzLm2Xq0kafHuR47ajDGbOe/pEhzIr4m8E8zwzQs0iNjloFDqBwRhrXmP/w+onLeYyN3EYPFW/cwWK/4yA==} + peerDependencies: + '@react-three/fiber': ^9.0.0 + react: ^19 + react-dom: ^19 + three: '>=0.159' + peerDependenciesMeta: + react-dom: + optional: true + + '@react-three/fiber@9.7.0': + resolution: {integrity: sha512-EWm9FwcaOZQu/ExFW5rggoCMM1NJet5YbxVxKaOE+KSncrjU0Wx7017qSyGFvupviK89nMYGCWU3BIK4dI1clw==} + peerDependencies: + expo: '>=43.0' + expo-asset: '>=8.4' + expo-file-system: '>=11.0' + expo-gl: '>=11.0' + react: '>=19 <19.3' + react-dom: '>=19 <19.3' + react-native: '>=0.78' + three: '>=0.156' + peerDependenciesMeta: + expo: + optional: true + expo-asset: + optional: true + expo-file-system: + optional: true + expo-gl: + optional: true + react-dom: + optional: true + react-native: + optional: true + '@rolldown/binding-android-arm64@1.2.3': resolution: {integrity: sha512-zrJtHDcaZJ1Fp7xf4hNl+7seH9Cn/N5TwLYkhgXREtBwAd/jaqW3uqeHxpDugJLVICWg4eW44kOQEGJ1r6jCGw==} engines: {node: ^20.19.0 || >=22.12.0} @@ -378,17 +453,60 @@ packages: resolution: {integrity: sha512-fCTuZK4QBa+39Oz9l4OGfJfz+GpwCp3AqO7Zch3to99xHPgstVsRFpeQ8LNd2o1Gv8raL2mCFwiaHh7bFSp5DQ==} engines: {node: '>=22'} + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + + '@tweenjs/tween.js@23.1.3': + resolution: {integrity: sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==} + + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + + '@types/draco3d@1.4.10': + resolution: {integrity: sha512-AX22jp8Y7wwaBgAixaSvkoG4M/+PlAcm3Qs4OW8yT9DM4xUpWKeFhLueTAyZF39pviAdcDdeJoACapiAceqNcw==} + + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + '@types/node@24.13.3': resolution: {integrity: sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==} + '@types/offscreencanvas@2019.7.3': + resolution: {integrity: sha512-ieXiYmgSRXUDeOntE1InxjWyvEelZGP63M+cGuquuRLuIKKT1osnkXjxev9B7d1nXSug5vpunx+gNlbVxMlC9A==} + '@types/react-dom@19.2.4': resolution: {integrity: sha512-Bsc+QHgp+P/F02XDzNCY9jnZNCUuLki36KT7VKrTXXLdHf+vHMNZnW1rVu5DNW/rCK+fya3DATySbLM4yhtKUw==} peerDependencies: '@types/react': ^19.2.0 + '@types/react-reconciler@0.28.9': + resolution: {integrity: sha512-HHM3nxyUZ3zAylX8ZEyrDNd2XZOnQ0D5XfunJF5FLQnZbHHYq4UWvW1QfelQNXv1ICNkwYhfxjwfnqivYB6bFg==} + peerDependencies: + '@types/react': '*' + '@types/react@19.2.18': resolution: {integrity: sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==} + '@types/stats.js@0.17.4': + resolution: {integrity: sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA==} + + '@types/three@0.185.4': + resolution: {integrity: sha512-gAsBIC07NIFrxjbf7tH2t71c38uulFfk/RFoC7FNBSjMRAQ8J1x/RBvusX0N5PJouaYFJawXQqfCQ0RKUx/1nA==} + + '@types/webxr@0.5.24': + resolution: {integrity: sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg==} + + '@use-gesture/core@10.3.1': + resolution: {integrity: sha512-WcINiDt8WjqBdUXye25anHiNxPc0VOrlT8F6LLkU6cycrOGUDyY/yyFmsg3k8i5OLvv25llc0QC45GhR/C8llw==} + + '@use-gesture/react@10.3.1': + resolution: {integrity: sha512-Yy19y6O2GJq8f7CHf7L0nxL8bf4PZCPaVOCgJrusOeFHY1LvHgYXnmnXg6N5iwAnbgbZCDjo60SiM6IPJi9C5g==} + peerDependencies: + react: '>= 16.8.0' + '@vitejs/plugin-react@6.0.5': resolution: {integrity: sha512-BOVzne/NL162sMdResB25mUv+vWMF5NoAjNf09TeGlE7ZpszZWSD3winycicLJw72yeVsoCn/2kOhEuCvEShMA==} engines: {node: ^20.19.0 || >=22.12.0} @@ -402,6 +520,35 @@ packages: babel-plugin-react-compiler: optional: true + '@vitest/expect@4.1.10': + resolution: {integrity: sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==} + + '@vitest/mocker@4.1.10': + resolution: {integrity: sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==} + peerDependencies: + msw: ^2.4.9 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@4.1.10': + resolution: {integrity: sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==} + + '@vitest/runner@4.1.10': + resolution: {integrity: sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==} + + '@vitest/snapshot@4.1.10': + resolution: {integrity: sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==} + + '@vitest/spy@4.1.10': + resolution: {integrity: sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==} + + '@vitest/utils@4.1.10': + resolution: {integrity: sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==} + ajv@8.20.0: resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==} @@ -420,10 +567,33 @@ packages: resolution: {integrity: sha512-DhBpBfXL4SS2uC0N922MMajKR3CdrTG0u2or1PNYgXMsrSzViJrbtvT0nCLlLGUI0plam/ZZCs7aAauHtW9thw==} engines: {node: '>=22'} + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + + bidi-js@1.0.3: + resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==} + + buffer@6.0.3: + resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} + callsites@3.1.0: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} + camera-controls@3.1.2: + resolution: {integrity: sha512-xkxfpG2ECZ6Ww5/9+kf4mfg1VEYAoe9aDSY+IwF0UEs7qEzwy0aVRfs2grImIECs/PoBtWFrh7RXsQkwG922JA==} + engines: {node: '>=22.0.0', npm: '>=10.5.1'} + peerDependencies: + three: '>=0.126.1' + + chai@6.2.2: + resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} + engines: {node: '>=18'} + cliui@9.0.1: resolution: {integrity: sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==} engines: {node: '>=20'} @@ -441,6 +611,9 @@ packages: engines: {node: '>=22'} hasBin: true + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + cosmiconfig-typescript-loader@6.3.0: resolution: {integrity: sha512-Akr82WH1Wfqatyiqpj8HDkO2o2KmJRu1FhKfSNJP3K4IdXwHfEyL7MOb62i1AGQVLtIQM+iCE9CGOtrfhR+mmA==} engines: {node: '>=v18'} @@ -458,13 +631,28 @@ packages: typescript: optional: true + cross-env@7.0.3: + resolution: {integrity: sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==} + engines: {node: '>=10.14', npm: '>=6', yarn: '>=1'} + hasBin: true + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + csstype@3.2.3: resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + detect-gpu@5.0.70: + resolution: {integrity: sha512-bqerEP1Ese6nt3rFkwPnGbsUF9a4q+gMmpTVVOEzoCyeCc+y7/RvJnQZJx1JwhgQI5Ntg0Kgat8Uu7XpBqnz1w==} + detect-libc@2.1.2: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} + draco3d@1.5.7: + resolution: {integrity: sha512-m6WCKt/erDXcw+70IJXnG7M3awwQPAsZvJGX5zY7beBqpELw6RDGkYVU0W43AFxye4pDZ5i2Lbyc/NNGqwjUVQ==} + emoji-regex@10.6.0: resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==} @@ -475,6 +663,9 @@ packages: error-ex@1.3.4: resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==} + es-module-lexer@2.3.1: + resolution: {integrity: sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==} + es-toolkit@1.50.0: resolution: {integrity: sha512-OyZKhUVvEep9ITEiwHn8GKnMRQIVqoSIX7WnRbkWgJkllCujilqP2rD0u979tkl8wqyc8ICwlc1UBVv/Sl1G6w==} @@ -482,6 +673,13 @@ packages: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + expect-type@1.4.0: + resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==} + engines: {node: '>=12.0.0'} + fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -497,6 +695,12 @@ packages: picomatch: optional: true + fflate@0.6.11: + resolution: {integrity: sha512-3JyEFWGjFn7zHmoa9+zG1BmW7X2okcmAB+0Cnu9UFbVs/jCBnl2A8o065ZlXiw145K3eBM3uLuzrYXC0RK7eDg==} + + fflate@0.8.3: + resolution: {integrity: sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==} + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -514,11 +718,23 @@ packages: resolution: {integrity: sha512-1pgFdhK3J2LeM+dVf2Pd424yHx2ou338lC0ErNP2hPx4j8eW1Sp0XqSjNxtk6Tc4Kr5wlWtSvz8cn2yb7/SG/w==} engines: {node: '>=20'} + glsl-noise@0.0.0: + resolution: {integrity: sha512-b/ZCF6amfAUb7dJM/MxRs7AetQEahYzJ8PtgfrmEdtw6uyGOr+ZSGtgjFm6mfsBkxJ4d2W7kg+Nlqzqvn3Bc0w==} + + hls.js@1.6.17: + resolution: {integrity: sha512-NUplVGVuc1hSPwdB/9/cbRkUmLrYi75/hqiXKdA+l300pJNxDu96R7jRb2imDzWJqIUF4I5ThmAdp9GvOCXsuQ==} + husky@9.1.7: resolution: {integrity: sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==} engines: {node: '>=18'} hasBin: true + ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + + immediate@3.0.6: + resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==} + import-fresh@3.3.1: resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} engines: {node: '>=6'} @@ -527,6 +743,9 @@ packages: resolution: {integrity: sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ==} engines: {node: ^20.17.0 || >=22.9.0} + is-any-array@3.0.0: + resolution: {integrity: sha512-o4h+tylWykC4BD1vaejp6gDxoM13bwW8FGuNs4yIKpj8xbBJcRxJx8vZpq0dCr7ZDEfeKjmsi/euolKhX6f/ww==} + is-arrayish@0.2.1: resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} @@ -534,6 +753,17 @@ packages: resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} engines: {node: '>=12'} + is-promise@2.2.2: + resolution: {integrity: sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + its-fine@2.0.0: + resolution: {integrity: sha512-KLViCmWx94zOvpLwSlsx6yOCeMhZYaxrJV87Po5k/FoZzcPSahvK5qJ7fYhS61sZi5ikmh2S3Hz55A2l3U69ng==} + peerDependencies: + react: ^19.0.0 + jiti@2.6.1: resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} hasBin: true @@ -551,6 +781,9 @@ packages: json-schema-traverse@1.0.0: resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + lie@3.3.0: + resolution: {integrity: sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==} + lightningcss-android-arm64@1.33.0: resolution: {integrity: sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==} engines: {node: '>= 12.0.0'} @@ -633,11 +866,47 @@ packages: engines: {node: '>=22.22.1'} hasBin: true + maath@0.10.8: + resolution: {integrity: sha512-tRvbDF0Pgqz+9XUa4jjfgAQ8/aPKmQdWXilFu2tMy4GWj4NOsx99HlULO4IeREfbO3a0sA145DZYyvXPkybm0g==} + peerDependencies: + '@types/three': '>=0.134.0' + three: '>=0.134.0' + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + meshline@3.3.1: + resolution: {integrity: sha512-/TQj+JdZkeSUOl5Mk2J7eLcYTLiQm2IDzmlSvYm7ov15anEcDJ92GHqqazxTSreeNgfnYu24kiEvvv0WlbCdFQ==} + peerDependencies: + three: '>=0.137' + + meshoptimizer@1.1.1: + resolution: {integrity: sha512-oRFNWJRDA/WTrVj7NWvqa5HqE1t9MYDj2VaWirQCzCCrAd2GHrqR/sQezCxiWATPNlKTcRaPRHPJwIRoPBAp5g==} + + ml-array-max@2.0.0: + resolution: {integrity: sha512-QQZ4kENwpWmyNb98UXRDFXrmtIXuXtt1+bSbda/2KA85+F+rrJP8hZk6QOkCQXM2Th9mUDYdq/PNByPdT9ID4A==} + + ml-array-min@2.0.0: + resolution: {integrity: sha512-GRj6Ky6sW9vGL6yIjgsHmXZ9YgrdmcQ8nCxPqEGeKc6dkfYg1XDYxGFxADUjNuZyoCd5PUscWAS4N+cFaX6hFg==} + + ml-array-rescale@2.0.0: + resolution: {integrity: sha512-2GGtKfSno94/kIloWGvpp/U5Q5vLvLrza+SAaGsLeo6Xj4mEbA6Gqx+oTfZFkxnd1grT2X007HfJNs3T5BsiVg==} + + ml-matrix@6.15.0: + resolution: {integrity: sha512-wFa1v6KP8bKp+fj0nYmRs1Pb5K4zRkXGKsOvLinvILENFIADncm4XlOI+S1M7yuACMGfI6cfk0IifDgd4j5xmw==} + + ml-pca@4.1.1: + resolution: {integrity: sha512-HQwswMK1dObj+ppk3EPcQMR2djWK0Cri8mAFd2nITtXHkLfO4DBBsEtiCT5KiR+2e3hQjp+lI0UyqZpdf04AlQ==} + nanoid@3.3.17: resolution: {integrity: sha512-xQLf0A3HOMlgHq0n247/LRuAOYmB7dXJ/DvAxGvsSBij45XtBSmQycu+F8ODbHwns/XyFZagyL1+J0Offw1E0g==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + obug@2.1.4: + resolution: {integrity: sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==} + engines: {node: '>=12.20.0'} + oxlint@1.77.0: resolution: {integrity: sha512-qnGh8XJHaQ0dprrDXNQZgS0FgjI6v+V3+X8DwmaV++5Aamy6jGKfDdQ1TUvhUxtmKFAbEf4/WeO5QZX+5WSngg==} engines: {node: ^20.19.0 || >=22.12.0} @@ -659,6 +928,13 @@ packages: resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} engines: {node: '>=8'} + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -670,11 +946,26 @@ packages: resolution: {integrity: sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==} engines: {node: ^10 || ^12 || >=14} + potpack@1.0.2: + resolution: {integrity: sha512-choctRBIV9EMT9WGAZHn3V7t0Z2pMQyl0EZE6pFc/6ml3ssw7Dlf/oAOvFwjm1HVsqfQN8GfeFyJ+d8tRzqueQ==} + + promise-worker-transferable@1.0.4: + resolution: {integrity: sha512-bN+0ehEnrXfxV2ZQvU2PetO0n4gqBD4ulq3MI1WOPLgr7/Mg9yRQkX5+0v1vagr74ZTsl7XtzlaYDo2EuCeYJw==} + react-dom@19.2.8: resolution: {integrity: sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==} peerDependencies: react: ^19.2.8 + react-use-measure@2.1.7: + resolution: {integrity: sha512-KrvcAo13I/60HpwGO5jpW7E9DfusKyLPLvuHlUyP5zqnmAPhNc6qTRjUQrdTADl0lpPpDVU2/Gg51UlOGHXbdg==} + peerDependencies: + react: '>=16.13' + react-dom: '>=16.13' + peerDependenciesMeta: + react-dom: + optional: true + react@19.2.8: resolution: {integrity: sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==} engines: {node: '>=0.10.0'} @@ -704,10 +995,36 @@ packages: engines: {node: '>=10'} hasBin: true + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + stats-gl@2.4.2: + resolution: {integrity: sha512-g5O9B0hm9CvnM36+v7SFl39T7hmAlv541tU81ME8YeSb3i1CIP5/QdDeSB3A0la0bKNHpxpwxOVRo2wFTYEosQ==} + peerDependencies: + '@types/three': '*' + three: '*' + + stats.js@0.17.0: + resolution: {integrity: sha512-hNKz8phvYLPEcRkeG1rsGmV5ChMjKDAWU7/OJJdDErPBNChQXxCo3WZurGpnWc6gZhAzEPFad1aVgyOANH1sMw==} + + std-env@4.2.0: + resolution: {integrity: sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==} + string-argv@0.3.2: resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==} engines: {node: '>=0.6.19'} @@ -724,6 +1041,27 @@ packages: resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==} engines: {node: '>=12'} + suspend-react@0.1.3: + resolution: {integrity: sha512-aqldKgX9aZqpoDp3e8/BZ8Dm7x1pJl+qI3ZKxDN0i/IQTWUwBx/ManmlVJ3wowqbno6c2bmiIfs+Um6LbsjJyQ==} + peerDependencies: + react: '>=17.0' + + three-mesh-bvh@0.8.3: + resolution: {integrity: sha512-4G5lBaF+g2auKX3P0yqx+MJC6oVt6sB5k+CchS6Ob0qvH0YIhuUk1eYr7ktsIpY+albCqE80/FVQGV190PmiAg==} + peerDependencies: + three: '>= 0.159.0' + + three-stdlib@2.36.1: + resolution: {integrity: sha512-XyGQrFmNQ5O/IoKm556ftwKsBg11TIb301MB5dWNicziQBEs2g3gtOYIf7pFiLa0zI2gUwhtCjv9fmjnxKZ1Cg==} + peerDependencies: + three: '>=0.128.0' + + three@0.185.1: + resolution: {integrity: sha512-5aojFCXKwnjBRZvUnt3WFfEcvUJgkN5LlijRFN95hMy8WVkG4I0QNcJE+OuWvuJ0bOdStrbfXn0pkd6/QyiAlg==} + + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + tinyexec@1.3.0: resolution: {integrity: sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ==} engines: {node: '>=18'} @@ -732,6 +1070,26 @@ packages: resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} engines: {node: '>=12.0.0'} + tinyrainbow@3.1.1: + resolution: {integrity: sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==} + engines: {node: '>=14.0.0'} + + troika-three-text@0.52.5: + resolution: {integrity: sha512-Ry3jRhic9pzcY4JduSvRRyDmVOSqEW19gT4vtK+aCiPNVcDlmkxvGG0YbFd36RTDq1wExOupXnvNF/j1oiHHDA==} + peerDependencies: + three: '>=0.125.0' + + troika-three-utils@0.52.5: + resolution: {integrity: sha512-WsePbcX8RtfidRfsxK1eCZCjF81ZDzAKHH/evLs0hdV2wpoCb0vArGZHdzdOJrSS3k4zfdtbKDaBh8+phkrYnw==} + peerDependencies: + three: '>=0.125.0' + + troika-worker-utils@0.52.0: + resolution: {integrity: sha512-W1CpvTHykaPH5brv5VHLfQo9D1OYuo0cSBEUQFFT/nBUzM8iD6Lq2/tgG/f1OelbAS1WtaTPQzE5uM49egnngw==} + + tunnel-rat@0.1.2: + resolution: {integrity: sha512-lR5VHmkPhzdhrM092lI2nACsLO4QubF0/yoOhzX7c+wIpbN1GjHNzCc91QlpxBi+cnx8vVJ+Ur6vL5cEoQPFpQ==} + typescript@6.0.3: resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==} engines: {node: '>=14.17'} @@ -740,6 +1098,15 @@ packages: undici-types@7.18.2: resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==} + use-sync-external-store@1.6.0: + resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + utility-types@3.11.0: + resolution: {integrity: sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw==} + engines: {node: '>= 4'} + vite@8.2.0: resolution: {integrity: sha512-pn+CFpM0lwDeKwmOq1ZaBK/9sjorZcgqxki6MbY/jPEVd9vichIlmlD4HmQ5wdP5EgqQCFRaACBxMC7uEGc6lQ==} engines: {node: ^20.19.0 || >=22.12.0} @@ -783,6 +1150,63 @@ packages: yaml: optional: true + vitest@4.1.10: + resolution: {integrity: sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==} + engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@opentelemetry/api': ^1.9.0 + '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 + '@vitest/browser-playwright': 4.1.10 + '@vitest/browser-preview': 4.1.10 + '@vitest/browser-webdriverio': 4.1.10 + '@vitest/coverage-istanbul': 4.1.10 + '@vitest/coverage-v8': 4.1.10 + '@vitest/ui': 4.1.10 + happy-dom: '*' + jsdom: '*' + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@opentelemetry/api': + optional: true + '@types/node': + optional: true + '@vitest/browser-playwright': + optional: true + '@vitest/browser-preview': + optional: true + '@vitest/browser-webdriverio': + optional: true + '@vitest/coverage-istanbul': + optional: true + '@vitest/coverage-v8': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + webgl-constants@1.1.1: + resolution: {integrity: sha512-LkBXKjU5r9vAW7Gcu3T5u+5cvSvh5WwINdr0C+9jpzVB41cjQAP5ePArDtk/WHYdVj0GefCgM73BA7FlIiNtdg==} + + webgl-sdf-generator@1.1.1: + resolution: {integrity: sha512-9Z0JcMTFxeE+b2x1LJTdnaT8rT8aEp7MVxkNwoycNmJWwPdzoXzMh0BjJSh/AEFP+KPYZUli814h8bJZFIZ2jA==} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + wrap-ansi@9.0.2: resolution: {integrity: sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==} engines: {node: '>=18'} @@ -804,6 +1228,42 @@ packages: resolution: {integrity: sha512-2rAgRKu54VsHkqI0/tYkmluGXHD4KW7yZoycuqDQ15QOTnc2VVfy0nN/1eMhnQLO00A+dwtK20xuCnc1YGeUyg==} engines: {node: ^20.19.0 || ^22.12.0 || >=23} + zod@4.4.3: + resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==} + + zustand@4.5.7: + resolution: {integrity: sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==} + engines: {node: '>=12.7.0'} + peerDependencies: + '@types/react': '>=16.8' + immer: '>=9.0.6' + react: '>=16.8' + peerDependenciesMeta: + '@types/react': + optional: true + immer: + optional: true + react: + optional: true + + zustand@5.0.14: + resolution: {integrity: sha512-/8tAspM5LMPr28b3fwLYrtdj77ECpfZviaP75CMTnwO8ISyaE4GDIG/9rDDYq/cH9D2Xw2A2RXglLInmVBQB/g==} + engines: {node: '>=12.20.0'} + peerDependencies: + '@types/react': '>=18.0.0' + immer: '>=9.0.6' + react: '>=18.0.0' + use-sync-external-store: '>=1.2.0' + peerDependenciesMeta: + '@types/react': + optional: true + immer: + optional: true + react: + optional: true + use-sync-external-store: + optional: true + snapshots: '@babel/code-frame@7.29.7': @@ -814,6 +1274,8 @@ snapshots: '@babel/helper-validator-identifier@7.29.7': {} + '@babel/runtime@7.29.7': {} + '@commitlint/cli@21.2.1(@types/node@24.13.3)(conventional-commits-parser@7.1.2)(typescript@6.0.3)': dependencies: '@commitlint/config-conventional': 21.2.0 @@ -933,8 +1395,19 @@ snapshots: '@conventional-changelog/template@1.2.1': {} + '@dimforge/rapier3d-compat@0.12.0': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + '@keyobs/dx-flow@3.1.3': {} + '@mediapipe/tasks-vision@0.10.17': {} + + '@monogrid/gainmap-js@3.4.0(three@0.185.1)': + dependencies: + promise-worker-transferable: 1.0.4 + three: 0.185.1 + '@oxc-project/types@0.143.0': {} '@oxlint/binding-android-arm-eabi@1.77.0': @@ -994,6 +1467,59 @@ snapshots: '@oxlint/binding-win32-x64-msvc@1.77.0': optional: true + '@react-three/drei@10.7.8(@react-three/fiber@9.7.0(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(three@0.185.1))(@types/react@19.2.18)(@types/three@0.185.4)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(three@0.185.1)': + dependencies: + '@babel/runtime': 7.29.7 + '@mediapipe/tasks-vision': 0.10.17 + '@monogrid/gainmap-js': 3.4.0(three@0.185.1) + '@react-three/fiber': 9.7.0(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(three@0.185.1) + '@use-gesture/react': 10.3.1(react@19.2.8) + camera-controls: 3.1.2(three@0.185.1) + cross-env: 7.0.3 + detect-gpu: 5.0.70 + glsl-noise: 0.0.0 + hls.js: 1.6.17 + maath: 0.10.8(@types/three@0.185.4)(three@0.185.1) + meshline: 3.3.1(three@0.185.1) + react: 19.2.8 + stats-gl: 2.4.2(@types/three@0.185.4)(three@0.185.1) + stats.js: 0.17.0 + suspend-react: 0.1.3(react@19.2.8) + three: 0.185.1 + three-mesh-bvh: 0.8.3(three@0.185.1) + three-stdlib: 2.36.1(three@0.185.1) + troika-three-text: 0.52.5(three@0.185.1) + tunnel-rat: 0.1.2(@types/react@19.2.18)(react@19.2.8) + use-sync-external-store: 1.6.0(react@19.2.8) + utility-types: 3.11.0 + zustand: 5.0.14(@types/react@19.2.18)(react@19.2.8)(use-sync-external-store@1.6.0(react@19.2.8)) + optionalDependencies: + react-dom: 19.2.8(react@19.2.8) + transitivePeerDependencies: + - '@types/react' + - '@types/three' + - immer + + '@react-three/fiber@9.7.0(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(three@0.185.1)': + dependencies: + '@babel/runtime': 7.29.7 + '@types/webxr': 0.5.24 + base64-js: 1.5.1 + buffer: 6.0.3 + its-fine: 2.0.0(@types/react@19.2.18)(react@19.2.8) + react: 19.2.8 + react-use-measure: 2.1.7(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + scheduler: 0.27.0 + suspend-react: 0.1.3(react@19.2.8) + three: 0.185.1 + use-sync-external-store: 1.6.0(react@19.2.8) + zustand: 5.0.14(@types/react@19.2.18)(react@19.2.8)(use-sync-external-store@1.6.0(react@19.2.8)) + optionalDependencies: + react-dom: 19.2.8(react@19.2.8) + transitivePeerDependencies: + - '@types/react' + - immer + '@rolldown/binding-android-arm64@1.2.3': optional: true @@ -1044,23 +1570,105 @@ snapshots: '@simple-libs/stream-utils@2.0.0': {} + '@standard-schema/spec@1.1.0': {} + + '@tweenjs/tween.js@23.1.3': {} + + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + + '@types/deep-eql@4.0.2': {} + + '@types/draco3d@1.4.10': {} + + '@types/estree@1.0.9': {} + '@types/node@24.13.3': dependencies: undici-types: 7.18.2 + '@types/offscreencanvas@2019.7.3': {} + '@types/react-dom@19.2.4(@types/react@19.2.18)': dependencies: '@types/react': 19.2.18 + '@types/react-reconciler@0.28.9(@types/react@19.2.18)': + dependencies: + '@types/react': 19.2.18 + '@types/react@19.2.18': dependencies: csstype: 3.2.3 + '@types/stats.js@0.17.4': {} + + '@types/three@0.185.4': + dependencies: + '@dimforge/rapier3d-compat': 0.12.0 + '@tweenjs/tween.js': 23.1.3 + '@types/stats.js': 0.17.4 + '@types/webxr': 0.5.24 + fflate: 0.8.3 + meshoptimizer: 1.1.1 + + '@types/webxr@0.5.24': {} + + '@use-gesture/core@10.3.1': {} + + '@use-gesture/react@10.3.1(react@19.2.8)': + dependencies: + '@use-gesture/core': 10.3.1 + react: 19.2.8 + '@vitejs/plugin-react@6.0.5(vite@8.2.0(@types/node@24.13.3)(jiti@2.6.1)(yaml@2.9.0))': dependencies: '@rolldown/pluginutils': 1.0.1 vite: 8.2.0(@types/node@24.13.3)(jiti@2.6.1)(yaml@2.9.0) + '@vitest/expect@4.1.10': + dependencies: + '@standard-schema/spec': 1.1.0 + '@types/chai': 5.2.3 + '@vitest/spy': 4.1.10 + '@vitest/utils': 4.1.10 + chai: 6.2.2 + tinyrainbow: 3.1.1 + + '@vitest/mocker@4.1.10(vite@8.2.0(@types/node@24.13.3)(jiti@2.6.1)(yaml@2.9.0))': + dependencies: + '@vitest/spy': 4.1.10 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 8.2.0(@types/node@24.13.3)(jiti@2.6.1)(yaml@2.9.0) + + '@vitest/pretty-format@4.1.10': + dependencies: + tinyrainbow: 3.1.1 + + '@vitest/runner@4.1.10': + dependencies: + '@vitest/utils': 4.1.10 + pathe: 2.0.3 + + '@vitest/snapshot@4.1.10': + dependencies: + '@vitest/pretty-format': 4.1.10 + '@vitest/utils': 4.1.10 + magic-string: 0.30.21 + pathe: 2.0.3 + + '@vitest/spy@4.1.10': {} + + '@vitest/utils@4.1.10': + dependencies: + '@vitest/pretty-format': 4.1.10 + convert-source-map: 2.0.0 + tinyrainbow: 3.1.1 + ajv@8.20.0: dependencies: fast-deep-equal: 3.1.3 @@ -1076,8 +1684,27 @@ snapshots: argue-cli@3.1.0: {} + assertion-error@2.0.1: {} + + base64-js@1.5.1: {} + + bidi-js@1.0.3: + dependencies: + require-from-string: 2.0.2 + + buffer@6.0.3: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + callsites@3.1.0: {} + camera-controls@3.1.2(three@0.185.1): + dependencies: + three: 0.185.1 + + chai@6.2.2: {} + cliui@9.0.1: dependencies: string-width: 7.2.0 @@ -1097,6 +1724,8 @@ snapshots: '@simple-libs/stream-utils': 2.0.0 argue-cli: 3.1.0 + convert-source-map@2.0.0: {} + cosmiconfig-typescript-loader@6.3.0(@types/node@24.13.3)(cosmiconfig@9.0.2(typescript@6.0.3))(typescript@6.0.3): dependencies: '@types/node': 24.13.3 @@ -1113,10 +1742,26 @@ snapshots: optionalDependencies: typescript: 6.0.3 + cross-env@7.0.3: + dependencies: + cross-spawn: 7.0.6 + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + csstype@3.2.3: {} + detect-gpu@5.0.70: + dependencies: + webgl-constants: 1.1.1 + detect-libc@2.1.2: {} + draco3d@1.5.7: {} + emoji-regex@10.6.0: {} env-paths@2.2.1: {} @@ -1125,10 +1770,18 @@ snapshots: dependencies: is-arrayish: 0.2.1 + es-module-lexer@2.3.1: {} + es-toolkit@1.50.0: {} escalade@3.2.0: {} + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.9 + + expect-type@1.4.0: {} + fast-deep-equal@3.1.3: {} fast-uri@3.1.5: {} @@ -1137,6 +1790,10 @@ snapshots: optionalDependencies: picomatch: 4.0.5 + fflate@0.6.11: {} + + fflate@0.8.3: {} + fsevents@2.3.3: optional: true @@ -1148,8 +1805,16 @@ snapshots: dependencies: ini: 6.0.0 + glsl-noise@0.0.0: {} + + hls.js@1.6.17: {} + husky@9.1.7: {} + ieee754@1.2.1: {} + + immediate@3.0.6: {} + import-fresh@3.3.1: dependencies: parent-module: 1.0.1 @@ -1157,10 +1822,23 @@ snapshots: ini@6.0.0: {} + is-any-array@3.0.0: {} + is-arrayish@0.2.1: {} is-plain-obj@4.1.0: {} + is-promise@2.2.2: {} + + isexe@2.0.0: {} + + its-fine@2.0.0(@types/react@19.2.18)(react@19.2.8): + dependencies: + '@types/react-reconciler': 0.28.9(@types/react@19.2.18) + react: 19.2.8 + transitivePeerDependencies: + - '@types/react' + jiti@2.6.1: {} js-tokens@4.0.0: {} @@ -1173,6 +1851,10 @@ snapshots: json-schema-traverse@1.0.0: {} + lie@3.3.0: + dependencies: + immediate: 3.0.6 + lightningcss-android-arm64@1.33.0: optional: true @@ -1232,8 +1914,48 @@ snapshots: optionalDependencies: yaml: 2.9.0 + maath@0.10.8(@types/three@0.185.4)(three@0.185.1): + dependencies: + '@types/three': 0.185.4 + three: 0.185.1 + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + meshline@3.3.1(three@0.185.1): + dependencies: + three: 0.185.1 + + meshoptimizer@1.1.1: {} + + ml-array-max@2.0.0: + dependencies: + is-any-array: 3.0.0 + + ml-array-min@2.0.0: + dependencies: + is-any-array: 3.0.0 + + ml-array-rescale@2.0.0: + dependencies: + is-any-array: 3.0.0 + ml-array-max: 2.0.0 + ml-array-min: 2.0.0 + + ml-matrix@6.15.0: + dependencies: + is-any-array: 3.0.0 + ml-array-rescale: 2.0.0 + + ml-pca@4.1.1: + dependencies: + ml-matrix: 6.15.0 + nanoid@3.3.17: {} + obug@2.1.4: {} + oxlint@1.77.0: optionalDependencies: '@oxlint/binding-android-arm-eabi': 1.77.0 @@ -1267,6 +1989,10 @@ snapshots: json-parse-even-better-errors: 2.3.1 lines-and-columns: 1.2.4 + path-key@3.1.1: {} + + pathe@2.0.3: {} + picocolors@1.1.1: {} picomatch@4.0.5: {} @@ -1277,11 +2003,24 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + potpack@1.0.2: {} + + promise-worker-transferable@1.0.4: + dependencies: + is-promise: 2.2.2 + lie: 3.3.0 + react-dom@19.2.8(react@19.2.8): dependencies: react: 19.2.8 scheduler: 0.27.0 + react-use-measure@2.1.7(react-dom@19.2.8(react@19.2.8))(react@19.2.8): + dependencies: + react: 19.2.8 + optionalDependencies: + react-dom: 19.2.8(react@19.2.8) + react@19.2.8: {} require-from-string@2.0.2: {} @@ -1314,8 +2053,27 @@ snapshots: semver@7.8.5: {} + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + siginfo@2.0.0: {} + source-map-js@1.2.1: {} + stackback@0.0.2: {} + + stats-gl@2.4.2(@types/three@0.185.4)(three@0.185.1): + dependencies: + '@types/three': 0.185.4 + three: 0.185.1 + + stats.js@0.17.0: {} + + std-env@4.2.0: {} + string-argv@0.3.2: {} string-width@7.2.0: @@ -1333,6 +2091,28 @@ snapshots: dependencies: ansi-regex: 6.2.2 + suspend-react@0.1.3(react@19.2.8): + dependencies: + react: 19.2.8 + + three-mesh-bvh@0.8.3(three@0.185.1): + dependencies: + three: 0.185.1 + + three-stdlib@2.36.1(three@0.185.1): + dependencies: + '@types/draco3d': 1.4.10 + '@types/offscreencanvas': 2019.7.3 + '@types/webxr': 0.5.24 + draco3d: 1.5.7 + fflate: 0.6.11 + potpack: 1.0.2 + three: 0.185.1 + + three@0.185.1: {} + + tinybench@2.9.0: {} + tinyexec@1.3.0: {} tinyglobby@0.2.17: @@ -1340,10 +2120,40 @@ snapshots: fdir: 6.5.0(picomatch@4.0.5) picomatch: 4.0.5 + tinyrainbow@3.1.1: {} + + troika-three-text@0.52.5(three@0.185.1): + dependencies: + bidi-js: 1.0.3 + three: 0.185.1 + troika-three-utils: 0.52.5(three@0.185.1) + troika-worker-utils: 0.52.0 + webgl-sdf-generator: 1.1.1 + + troika-three-utils@0.52.5(three@0.185.1): + dependencies: + three: 0.185.1 + + troika-worker-utils@0.52.0: {} + + tunnel-rat@0.1.2(@types/react@19.2.18)(react@19.2.8): + dependencies: + zustand: 4.5.7(@types/react@19.2.18)(react@19.2.8) + transitivePeerDependencies: + - '@types/react' + - immer + - react + typescript@6.0.3: {} undici-types@7.18.2: {} + use-sync-external-store@1.6.0(react@19.2.8): + dependencies: + react: 19.2.8 + + utility-types@3.11.0: {} + vite@8.2.0(@types/node@24.13.3)(jiti@2.6.1)(yaml@2.9.0): dependencies: lightningcss: 1.33.0 @@ -1357,6 +2167,46 @@ snapshots: jiti: 2.6.1 yaml: 2.9.0 + vitest@4.1.10(@types/node@24.13.3)(vite@8.2.0(@types/node@24.13.3)(jiti@2.6.1)(yaml@2.9.0)): + dependencies: + '@vitest/expect': 4.1.10 + '@vitest/mocker': 4.1.10(vite@8.2.0(@types/node@24.13.3)(jiti@2.6.1)(yaml@2.9.0)) + '@vitest/pretty-format': 4.1.10 + '@vitest/runner': 4.1.10 + '@vitest/snapshot': 4.1.10 + '@vitest/spy': 4.1.10 + '@vitest/utils': 4.1.10 + es-module-lexer: 2.3.1 + expect-type: 1.4.0 + magic-string: 0.30.21 + obug: 2.1.4 + pathe: 2.0.3 + picomatch: 4.0.5 + std-env: 4.2.0 + tinybench: 2.9.0 + tinyexec: 1.3.0 + tinyglobby: 0.2.17 + tinyrainbow: 3.1.1 + vite: 8.2.0(@types/node@24.13.3)(jiti@2.6.1)(yaml@2.9.0) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 24.13.3 + transitivePeerDependencies: + - msw + + webgl-constants@1.1.1: {} + + webgl-sdf-generator@1.1.1: {} + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + wrap-ansi@9.0.2: dependencies: ansi-styles: 6.2.3 @@ -1378,3 +2228,18 @@ snapshots: string-width: 8.2.2 y18n: 5.0.8 yargs-parser: 22.0.0 + + zod@4.4.3: {} + + zustand@4.5.7(@types/react@19.2.18)(react@19.2.8): + dependencies: + use-sync-external-store: 1.6.0(react@19.2.8) + optionalDependencies: + '@types/react': 19.2.18 + react: 19.2.8 + + zustand@5.0.14(@types/react@19.2.18)(react@19.2.8)(use-sync-external-store@1.6.0(react@19.2.8)): + optionalDependencies: + '@types/react': 19.2.18 + react: 19.2.8 + use-sync-external-store: 1.6.0(react@19.2.8) diff --git a/vite.config.ts b/vite.config.ts index 8b0f57b..8291117 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,7 +1,11 @@ -import { defineConfig } from 'vite' +import { defineConfig } from 'vitest/config' import react from '@vitejs/plugin-react' // https://vite.dev/config/ export default defineConfig({ plugins: [react()], + test: { + environment: 'node', + include: ['src/**/*.test.ts'], + }, }) From d6ba57a1d53e5e6ad888f7ace6a88f4852a937c3 Mon Sep 17 00:00:00 2001 From: keyobs Date: Sun, 9 Aug 2026 21:06:43 +0200 Subject: [PATCH 03/68] feat(data): add FDJ EuroMillions historical draws CSV Official FDJ export (~680 draws, 2020-02 to 2026-08), served from public/ per ADR-0001 (client-side-only, dataset committed to the repo). Feeds the ADR-0002 vertical spike and, later, /draws. --- public/results/euromillions_202002.csv | 681 +++++++++++++++++++++++++ 1 file changed, 681 insertions(+) create mode 100644 public/results/euromillions_202002.csv diff --git a/public/results/euromillions_202002.csv b/public/results/euromillions_202002.csv new file mode 100644 index 0000000..3ca63f5 --- /dev/null +++ b/public/results/euromillions_202002.csv @@ -0,0 +1,681 @@ +annee_numero_de_tirage;jour_de_tirage;date_de_tirage;numéro_de_tirage_dans_le_cycle;date_de_forclusion;boule_1;boule_2;boule_3;boule_4;boule_5;etoile_1;etoile_2;boules_gagnantes_en_ordre_croissant;etoiles_gagnantes_en_ordre_croissant;nombre_de_gagnant_au_rang1_Euro_Millions_en_france;nombre_de_gagnant_au_rang1_Euro_Millions_en_europe;rapport_du_rang1_Euro_Millions;nombre_de_gagnant_au_rang2_Euro_Millions_en_france;nombre_de_gagnant_au_rang2_Euro_Millions_en_europe;rapport_du_rang2_Euro_Millions;nombre_de_gagnant_au_rang3_Euro_Millions_en_france;nombre_de_gagnant_au_rang3_Euro_Millions_en_europe;rapport_du_rang3_Euro_Millions;nombre_de_gagnant_au_rang4_Euro_Millions_en_france;nombre_de_gagnant_au_rang4_Euro_Millions_en_europe;rapport_du_rang4_Euro_Millions;nombre_de_gagnant_au_rang5_Euro_Millions_en_france;nombre_de_gagnant_au_rang5_Euro_Millions_en_europe;rapport_du_rang5_Euro_Millions;nombre_de_gagnant_au_rang6_Euro_Millions_en_france;nombre_de_gagnant_au_rang6_Euro_Millions_en_europe;rapport_du_rang6_Euro_Millions;nombre_de_gagnant_au_rang7_Euro_Millions_en_france;nombre_de_gagnant_au_rang7_Euro_Millions_en_europe;rapport_du_rang7_Euro_Millions;nombre_de_gagnant_au_rang8_Euro_Millions_en_france;nombre_de_gagnant_au_rang8_Euro_Millions_en_europe;rapport_du_rang8_Euro_Millions;nombre_de_gagnant_au_rang9_Euro_Millions_en_france;nombre_de_gagnant_au_rang9_Euro_Millions_en_europe;rapport_du_rang9_Euro_Millions;nombre_de_gagnant_au_rang10_Euro_Millions_en_france;nombre_de_gagnant_au_rang10_Euro_Millions_en_europe;rapport_du_rang10_Euro_Millions;nombre_de_gagnant_au_rang11_Euro_Millions_en_france;nombre_de_gagnant_au_rang11_Euro_Millions_en_europe;rapport_du_rang11_Euro_Millions;nombre_de_gagnant_au_rang12_Euro_Millions_en_france;nombre_de_gagnant_au_rang12_Euro_Millions_en_europe;rapport_du_rang12_Euro_Millions;nombre_de_gagnant_au_rang13_Euro_Millions_en_france;nombre_de_gagnant_au_rang13_Euro_Millions_en_europe;rapport_du_rang13_Euro_Millions;nombre_de_gagnant_au_rang1_Etoile+;rapport_du_rang1_Etoile+;nombre_de_gagnant_au_rang2_Etoile+;rapport_du_rang2_Etoile+;nombre_de_gagnant_au_rang3_Etoile+;rapport_du_rang3_Etoile+;nombre_de_gagnant_au_rang4_Etoile+;rapport_du_rang4_Etoile+;nombre_de_gagnant_au_rang5_Etoile+;rapport_du_rang5_Etoile+;nombre_de_gagnant_au_rang6_Etoile+;rapport_du_rang6_Etoile+;nombre_de_gagnant_au_rang7_Etoile+;rapport_du_rang7_Etoile+;nombre_de_gagnant_au_rang8_Etoile+;rapport_du_rang8_Etoile+;nombre_de_gagnant_au_rang9_Etoile+;rapport_du_rang9_Etoile+;nombre_de_gagnant_au_rang10_Etoile+;rapport_du_rang10_Etoile+;numero_My_Million;numero_Tirage_Exceptionnel_Euro_Million; +26063;VENDREDI;07/08/2026;10;06/11/2026;35;26;29;47;38;1;2;-26-29-35-38-47-;-1-2-;0;1;111058174,00;1;7;112479,10;2;6;30669,60;2;22;2605,30;142;723;146,00;328;1420;78,60;425;1855;42,20;4431;20313;19,30;7437;33369;13,10;17614;80063;10,10;24716;114617;8,60;113528;490858;6,30;260637;1156077;4,30;1;1841,70;1;1473,30;56;32,80;155;11,80;2223;2,00;3569;3,40;12292;4,70;22541;11,90;54722;2,70;570379;2,30;RZ 296 9256;; +26062;MARDI;04/08/2026;9;03/11/2026;25;50;34;46;30;1;12;-25-30-34-46-50-;-1-12-;0;0;0;0;3;192409,40;2;6;22484,60;3;20;2101,00;100;474;163,30;238;1106;73,90;228;1122;51,20;3645;17126;16,70;4129;20848;15,30;10227;52122;11,40;18391;87386;8,20;63740;316485;7,10;167371;824628;4,40;0;0;2;1148,10;62;20,50;117;10,90;1809;1,70;2101;4,00;9414;4,20;16458;11,30;32224;3,20;338542;2,70;FR 875 0961;; +26061;VENDREDI;31/07/2026;8;30/10/2026;10;25;31;24;45;5;4;-10-24-25-31-45-;-4-5-;0;0;0;0;2;343065,50;1;8;20045,00;7;38;1314,40;178;792;116,10;406;1725;56,30;341;1644;41,50;5067;24176;14,10;8293;37743;10,00;16330;74771;9,40;25693;122160;7,00;115187;529751;5,10;231941;1062178;4,10;0;0;4;682,40;95;15,90;202;7,50;2434;1,50;3937;2,50;12328;3,80;20172;10,90;55001;2,20;453076;2,40;AW 461 1722;; +26060;MARDI;28/07/2026;7;27/10/2026;7;5;49;24;30;11;10;-5-7-24-30-49-;-10-11-;0;0;0;0;1;522575,90;0;2;61067,30;6;29;1311,70;127;596;117,50;190;1010;73,30;365;1506;34,50;2825;14328;18,10;5596;25052;11,50;16263;68880;7,80;13934;73599;8,80;76756;350687;5,80;218080;959974;3,40;0;0;2;1026,10;69;16,50;111;10,20;1442;1,90;2768;2,70;7304;4,90;11446;14,50;38259;2,40;284968;2,90;GA 472 5470;; +26059;VENDREDI;24/07/2026;6;23/10/2026;47;30;10;36;8;1;4;-8-10-30-36-47-;-1-4-;0;0;0;1;1;645724,00;1;5;30183,20;2;19;2474,00;105;539;160,60;245;1267;72,20;261;1331;48,30;4071;19442;16,50;5563;27343;13,10;12952;64240;10,30;22487;104580;7,70;91113;423659;6,00;208660;986232;4,10;1;1416,80;0;0;54;47,20;126;11,20;1958;1,80;2769;3,40;10694;4,10;17708;11,60;44521;2,60;401288;2,50;SD 826 3887;; +26058;MARDI;21/07/2026;5;20/10/2026;39;8;28;2;3;11;2;-2-3-8-28-39-;-2-11-;0;0;0;0;2;250104,30;0;2;58453,50;4;19;1916,50;118;606;110,60;274;1224;57,90;226;1150;43,30;3691;17520;14,20;5621;26369;10,50;12408;58007;8,90;18754;89922;6,90;77647;365439;5,40;181107;836260;3,80;0;0;3;651,00;58;18,70;135;8,00;1841;1,40;2761;2,60;9334;3,60;14710;10,70;38258;2,30;295061;2,60;HF 658 0122;; +26057;VENDREDI;17/07/2026;4;16/10/2026;34;21;40;12;23;10;9;-12-21-23-34-40-;-9-10-;0;0;0;0;0;0;3;7;111131,40;4;21;2185,80;135;726;116,40;236;1149;77,70;329;1679;37,40;3175;16279;19,20;6445;31505;11,10;16764;75769;8,60;16723;86415;9,10;92518;442664;5,60;230113;1065262;3,70;0;0;3;829,30;76;18,10;119;11,60;1627;2,10;3201;2,80;8910;4,80;14841;13,50;46074;2,40;391983;2,50;MA 638 4778;; +26056;MARDI;14/07/2026;3;13/10/2026;42;10;19;37;47;9;12;-10-19-37-42-47-;-9-12-;0;0;0;1;2;238156,40;1;5;22264,40;5;24;1444,70;95;467;136,70;157;866;77,90;266;1168;40,60;2454;12776;18,50;3870;20400;12,90;10182;50012;9,80;12919;66955;8,90;59763;301034;6,20;158494;752440;4,00;0;0;3;607,60;43;23,50;83;12,20;1313;1,90;1942;3,40;6845;4,60;11401;12,90;30116;2,70;280883;2,60;DR 329 0687;; +26055;VENDREDI;10/07/2026;2;09/10/2026;2;28;14;33;48;8;10;-2-14-28-33-48-;-8-10-;0;0;0;0;4;149698,40;1;7;19992,50;6;32;1362,10;120;659;121,80;242;1186;71,50;258;1351;44,10;3729;17535;17,00;5761;27622;12,00;13599;64779;9,50;20234;93242;8,00;85221;400598;5,80;201800;940529;4,00;0;0;3;815,60;61;22,20;142;9,50;1919;1,70;2908;3,10;10733;3,90;18087;10,90;43640;2,50;384121;2,50;UP 040 3568;; +26054;MARDI;07/07/2026;1;06/10/2026;45;5;33;29;47;8;5;-5-29-33-45-47-;-5-8-;0;0;0;0;3;150509,80;2;5;21105,90;5;27;1217,40;100;481;125,80;273;1398;45,70;199;965;46,60;4483;20917;10,70;4356;21114;11,80;9098;43565;10,70;24687;114244;4,90;66941;316806;5,60;138870;646656;4,40;0;0;3;624,50;49;21,20;139;7,40;2115;1,20;2187;3,10;11979;2,70;19339;7,80;33694;2,50;313614;2,40;RC 137 6956;; +26053;VENDREDI;03/07/2026;7;02/10/2026;12;2;25;39;17;2;1;-2-12-17-25-39-;-1-2-;0;1;80220498,00;1;3;233586,20;1;9;18197,60;5;50;1020,20;259;996;94,30;355;1600;62,00;509;2073;33,60;4947;22846;15,20;9783;40821;9,50;20191;88162;8,20;23906;113324;7,70;128003;543566;5,00;270477;1195498;3,70;1;1596,10;4;319,20;117;13,60;172;9,20;2429;1,60;4711;2,20;11714;4,20;17277;13,40;62041;2,10;450660;2,50;DB 464 8307;; +26052;MARDI;30/06/2026;6;29/09/2026;37;48;44;1;8;6;2;-1-8-37-44-48-;-2-6-;0;0;0;1;2;268526,70;0;8;15689,70;6;24;1628,90;86;429;167,80;260;1201;63,30;189;907;58,90;4137;18644;14,30;4656;21803;13,60;10073;48350;11,40;24311;108275;6,20;75337;348128;6,00;161651;768876;4,40;1;1177,30;2;470,90;50;23,50;127;9,20;2032;1,40;2374;3,30;11398;3,20;20610;8,30;37073;2,60;368815;2,30;UY 611 2890;; +26051;VENDREDI;26/06/2026;5;25/09/2026;6;35;16;26;34;11;12;-6-16-26-34-35-;-11-12-;0;0;0;0;3;216058,50;1;12;12624,10;0;24;1966,00;100;542;160,30;201;1067;86,10;329;1726;37,40;2912;15988;20,10;5045;26452;13,60;16304;76248;8,70;15791;85067;9,50;74321;375348;6,80;232527;1065466;3,80;0;0;0;0;56;70,90;106;13,30;1610;2,20;2652;3,50;8752;5,10;14592;14,20;38719;3,00;356323;2,80;TE 244 5417;; +26050;MARDI;23/06/2026;4;22/09/2026;45;36;33;3;46;5;6;-3-33-36-45-46-;-5-6-;0;0;0;1;3;166281,50;1;5;23317,60;5;22;1650,60;77;408;163,90;155;986;71,70;167;894;55,50;2671;14697;16,90;3930;20811;13,30;8528;44050;11,70;15760;82965;7,50;65148;325105;6,00;133285;672893;4,70;1;1103,80;3;294,30;48;22,90;76;14,50;1389;1,90;1959;3,70;7960;4,30;15785;10,20;32809;2,70;372035;2,10;LN 170 7243;; +26049;VENDREDI;19/06/2026;3;18/09/2026;39;41;42;8;34;2;7;-8-34-39-41-42-;-2-7-;0;0;0;0;0;0;1;7;110334,50;3;19;2398,50;103;453;185,30;411;1531;57,90;215;1110;56,10;6030;23965;13,00;5596;26024;13,30;11461;55202;11,70;35902;141949;5,50;89449;404196;6,10;180882;848710;4,60;0;0;2;1316,60;59;24,70;196;7,40;2874;1,20;2866;3,40;17120;2,60;33550;6,30;45110;2,60;498403;2,10;TP 802 5197;; +26048;MARDI;16/06/2026;2;15/09/2026;18;25;37;31;45;4;9;-18-25-31-37-45-;-4-9-;0;0;0;0;0;0;0;7;85884,00;2;22;1612,40;119;496;131,70;302;1410;48,90;242;1095;44,30;4208;19927;12,10;4823;21848;12,30;10843;48177;10,40;23407;108392;5,60;72338;325136;5,90;160349;714942;4,30;0;0;0;0;59;55,00;144;8,00;1998;1,40;2413;3,20;11545;3,10;19812;8,50;35852;2,60;348458;2,40;CK 811 1065;; +26047;VENDREDI;12/06/2026;1;11/09/2026;14;7;22;4;23;7;1;-4-7-14-22-23-;-1-7-;0;0;0;0;3;212362,30;2;13;11453,60;22;77;602,30;418;1533;55,70;756;2696;33,40;685;2686;23,60;8878;32004;9,90;13538;52307;6,70;28848;108670;6,00;36187;136915;5,80;147557;593648;4,20;287961;1209750;3,30;0;0;11;256,30;192;8,10;332;4,70;4220;,90;6508;1,60;17476;2,80;23599;9,60;71515;1,80;434770;2,60;QO 195 2951;; +26046;MARDI;09/06/2026;14;08/09/2026;44;2;23;46;7;5;3;-2-7-23-44-46-;-3-5-;0;1;174984088,00;0;6;148909,60;1;9;23201,70;12;54;1204,40;237;968;123,70;615;2576;49,10;485;2055;43,30;9372;38150;11,60;14040;52650;9,40;25371;100180;9,20;50418;201996;5,50;192517;745660;4,70;362545;1424090;3,90;0;0;3;1330,40;98;22,60;268;8,20;4212;1,30;6436;2,30;22977;3,00;37156;8,70;88909;2,00;670138;2,40;MA 582 0737;; +26045;VENDREDI;05/06/2026;13;04/09/2026;5;6;16;17;49;2;12;-5-6-16-17-49-;-2-12-;0;0;0;0;3;365514,60;2;12;21356,70;3;37;2157,40;313;1096;134,10;562;2181;71,20;715;2475;44,10;9325;33566;16,20;14010;50504;12,00;34358;123783;9,10;47817;175938;7,80;209512;762299;5,60;517606;1873424;3,70;0;0;3;1798,40;152;19,70;304;9,80;4759;1,50;6957;2,80;24424;3,80;38145;11,40;103128;2,30;813539;2,60;PU 143 6071;; +26044;MARDI;02/06/2026;12;01/09/2026;18;9;17;42;6;7;9;-6-9-17-18-42-;-7-9-;0;0;0;1;4;204367,10;2;7;27293,60;17;54;1102,00;316;1132;96,80;890;2764;41,90;582;2032;40,00;11762;39480;10,30;14326;51293;8,80;26856;98632;8,50;54168;186839;5,40;192538;701061;4,60;371190;1381662;3,70;0;0;7;545,10;146;14,50;412;5,10;5438;,90;6650;2,10;25387;2,60;35029;8,80;89187;1,90;623753;2,40;HH 903 3995;; +26043;VENDREDI;29/05/2026;11;28/08/2026;5;14;35;31;18;12;2;-5-14-18-31-35-;-2-12-;0;0;0;3;9;98854,10;3;11;18903,10;13;42;1542,00;229;990;120,50;409;1777;70,90;601;2631;33,60;5790;25593;17,30;9523;41495;11,90;24093;103967;8,80;29784;134217;8,30;136177;595679;5,80;345311;1495645;3,70;0;0;5;729,90;113;17,90;196;10,30;2885;1,70;4568;2,90;15366;4,10;25081;11,80;66195;2,50;558876;2,60;OQ 902 2205;; +26042;MARDI;26/05/2026;10;25/08/2026;23;37;6;35;25;12;6;-6-23-25-35-37-;-6-12-;0;0;0;1;1;624963,40;1;2;73032,10;5;18;2527,50;115;578;144,90;262;1298;68,20;251;1291;48,20;4045;19021;16,30;5388;27104;12,80;13295;66392;9,70;21565;100048;7,80;83455;408086;6,00;204768;1004678;3,90;0;0;2;1148,20;53;24,00;141;9,00;2117;1,50;2551;3,30;10657;3,70;17089;10,90;39857;2,60;348321;2,60;UN 179 1377;; +26041;VENDREDI;22/05/2026;9;21/08/2026;6;31;37;22;26;5;8;-6-22-26-31-37-;-5-8-;0;0;0;1;5;157970,00;2;5;36920,10;12;49;1173,40;161;832;127,30;503;2357;47,50;355;1663;47,30;7731;35864;10,90;8174;38001;11,50;17658;81809;9,90;41160;195050;5,00;120429;568167;5,40;258680;1214421;4,10;1;1670,90;6;222,70;79;21,10;228;7,30;3557;1,10;3861;2,80;18970;2,70;31065;7,80;56868;2,40;495490;2,40;FB 735 2189;; +26040;MARDI;19/05/2026;8;18/08/2026;12;20;45;38;2;5;2;-2-12-20-38-45-;-2-5-;0;0;0;1;3;195442,20;1;9;15226,00;6;23;1855,70;173;640;122,80;354;1623;51,20;367;1365;42,70;5622;24973;11,60;6914;30031;10,80;13151;58287;10,40;27835;128487;5,70;95090;432564;5,30;187448;858994;4,30;1;1246,30;3;332,30;79;15,70;152;8,10;2553;1,20;3297;2,50;12778;3,00;19438;9,30;45251;2,20;377769;2,30;HZ 793 8733;; +26039;VENDREDI;15/05/2026;7;14/08/2026;10;38;43;41;3;2;9;-3-10-38-41-43-;-2-9-;0;0;0;0;1;710654,00;0;2;83045,70;1;27;1916,00;94;568;167,70;316;1577;63,80;262;1324;53,40;4934;25304;13,90;5698;29717;13,20;12529;64558;11,30;29296;143226;6,20;93960;470189;5,90;204094;1015066;4,40;0;0;1;2681,10;48;31,00;153;9,70;2407;1,50;2834;3,50;13781;3,40;24773;8,70;45966;2,60;463094;2,30;NT 565 8161;; +26038;MARDI;12/05/2026;6;11/08/2026;26;35;32;36;4;7;5;-4-26-32-35-36-;-5-7-;0;0;0;0;0;0;0;3;229375,50;3;23;1765,30;77;414;180,60;260;1359;58,10;173;984;56,40;4459;22052;12,50;4246;22838;13,50;8884;47072;12,20;30026;137523;5,00;73784;369066;5,90;147942;748811;4,70;0;0;1;2166,60;43;27,90;139;8,60;2066;1,40;2122;3,80;13801;2,70;26835;6,50;36318;2,70;405239;2,10;VK 398 4678;; +26037;VENDREDI;08/05/2026;5;07/08/2026;19;17;37;34;2;11;8;-2-17-19-34-37-;-8-11-;0;0;0;0;3;233549,00;3;13;12596,30;5;25;2040,10;160;784;119,80;260;1489;66,70;392;1922;36,30;4066;21299;16,30;6330;33335;11,60;16060;80745;8,90;20680;110595;7,90;94407;483556;5,70;236671;1159111;3,80;0;0;2;1298,20;80;18,00;129;11,10;2103;1,70;3164;3,00;10589;4,20;17249;12,20;46867;2,50;396835;2,60;MI 925 5672;; +26036;MARDI;05/05/2026;4;04/08/2026;4;8;3;31;20;6;8;-3-4-8-20-31-;-6-8-;0;0;0;0;4;131785,50;3;6;20533,60;10;43;892,40;214;839;84,20;407;1607;46,50;380;1525;34,40;5274;21603;12,10;7594;31857;9,10;15726;66559;8,10;25290;108130;6,10;95862;421221;4,90;201520;894639;3,70;0;0;6;349,20;94;12,30;202;5,70;2572;1,10;3568;2,10;12334;2,90;18044;9,40;46348;2,00;335557;2,50;TF 464 4375;; +26035;VENDREDI;01/05/2026;3;31/07/2026;42;47;46;3;9;11;1;-3-9-42-46-47-;-1-11-;0;0;0;0;4;171526,90;0;4;40088,60;7;27;1849,80;93;526;174,90;194;1229;79,10;196;1281;53,30;3257;19149;17,80;4369;26222;14,50;10940;62446;11,30;18021;104637;8,20;73933;418568;6,40;192096;1042979;4,10;0;0;3;786,00;53;24,70;97;13,50;1663;1,90;2264;3,80;9348;4,40;15451;12,30;37036;2,90;345262;2,70;UL 104 2892;; +26034;MARDI;28/04/2026;2;28/07/2026;46;47;29;26;41;9;8;-26-29-41-46-47-;-8-9-;0;0;0;0;1;480828,00;1;1;112377,40;1;17;2058,90;66;367;175,60;180;869;78,40;150;845;56,60;2631;13006;18,40;3611;18776;14,20;7689;40046;12,40;15059;73556;8,10;60545;300164;6,30;128435;629888;4,80;0;0;1;1939,10;35;30,70;93;11,50;1367;1,90;1782;4,00;7666;4,40;13904;11,30;30404;2,90;347012;2,20;RB 351 8789;; +26033;VENDREDI;24/04/2026;1;24/07/2026;40;30;45;26;25;5;1;-25-26-30-40-45-;-1-5-;0;0;0;2;3;204255,10;1;8;17901,60;1;17;2623,90;93;570;144,10;275;1329;65,30;231;1242;49,10;3921;19949;15,20;5712;28326;12,00;11172;58532;10,80;22917;108046;7,10;82133;409643;5,90;177462;881728;4,40;0;0;0;0;54;71,00;141;9,70;1855;1,80;2821;3,20;11062;3,90;19350;10,30;41081;2,70;410209;2,40;SG 160 4533;; +26032;MARDI;21/04/2026;12;21/07/2026;29;47;16;13;40;4;3;-13-16-29-40-47-;-3-4-;1;3;48175066,00;2;5;152175,40;2;10;17782,90;7;34;1629,10;177;819;124,50;357;1543;69,90;349;1645;46,00;5339;23601;16,00;8731;37302;11,30;17677;76810;10,20;29541;130852;7,20;130469;555464;5,40;264267;1139139;4,20;1;1766,10;5;282,50;82;21,50;166;10,60;2528;1,70;4037;2,90;13622;4,00;23208;11,10;60207;2,40;557120;2,30;GX 965 7967;; +26031;VENDREDI;17/04/2026;11;17/07/2026;22;23;47;28;41;6;8;-22-23-28-41-47-;-6-8-;0;0;0;0;3;322491,10;4;13;17393,40;10;37;1903,40;158;853;152,00;511;2083;65,80;358;1721;56,00;7900;34393;14,00;8423;41164;13,00;18414;90194;11,00;45977;192949;6,20;142987;663338;5,70;311503;1455435;4,20;0;0;2;1937,60;81;26,50;249;8,60;3636;1,40;4080;3,50;21500;3,10;34715;9,00;68384;2,50;647791;2,40;TE 349 5398;; +26030;MARDI;14/04/2026;10;14/07/2026;1;2;4;44;28;12;5;-1-2-4-28-44-;-5-12-;0;0;0;0;2;344056,40;1;2;80411,60;3;37;1353,80;122;662;139,30;293;1408;69,20;311;1597;42,90;4045;20342;16,80;6474;32379;11,80;17191;80566;8,80;22116;109721;7,80;96235;466267;5,80;240375;1122055;3,80;0;0;2;1276,30;65;21,80;144;9,80;2061;1,70;3176;2,90;11013;4,00;17397;11,90;46853;2,40;389630;2,60;HO 590 8240;; +26029;VENDREDI;10/04/2026;9;10/07/2026;10;13;41;38;14;6;9;-10-13-14-38-41-;-6-9-;0;0;0;0;3;270959,60;1;3;63327,70;10;42;1408,90;192;860;126,70;433;2122;54,30;406;1767;45,80;7531;32793;12,30;8323;37799;11,90;18104;82775;10,10;40001;176245;5,70;136943;587746;5,40;290697;1280340;4,00;0;0;4;832,30;77;24,00;201;9,20;3568;1,20;3979;3,10;18950;3,00;29575;9,10;64633;2,30;543801;2,40;LY 091 0623;; +26028;MARDI;07/04/2026;8;07/07/2026;14;49;11;19;36;7;6;-11-14-19-36-49-;-6-7-;0;0;0;2;5;113717,30;0;3;44296,00;5;33;1254,20;137;617;123,50;277;1342;60,00;249;1242;45,60;4298;20355;13,90;6328;28764;10,90;11637;56719;10,30;23220;107960;6,50;97571;435910;5,10;182568;864402;4,10;0;0;3;720,80;68;17,60;135;8,80;2022;1,40;3023;2,60;10943;3,40;17579;9,90;46008;2,10;390606;2,20;JH 669 2859;; +26027;VENDREDI;03/04/2026;7;03/07/2026;27;46;8;49;29;10;2;-8-27-29-46-49-;-2-10-;0;0;0;1;3;236718,40;1;3;55324,90;5;42;1230,80;138;617;154,30;369;1583;63,50;337;1462;48,30;5087;22828;15,40;6755;30417;12,90;15920;72157;10,10;27744;120579;7,30;103362;459870;6,00;245680;1100234;4,10;0;0;2;1468,40;60;27,10;175;9,30;2685;1,50;3324;3,20;14303;3,50;24117;9,80;51680;2,60;466773;2,50;SU 524 4508;; +26026;MARDI;31/03/2026;6;30/06/2026;8;38;10;5;33;2;7;-5-8-10-33-38-;-2-7-;0;0;0;1;3;189523,00;0;6;22147,30;10;42;985,40;123;573;133,00;451;1818;44,30;271;1335;42,40;6606;27033;10,40;6486;30287;10,40;12934;63580;9,20;33650;140223;5,00;98453;454155;4,90;190417;924494;3,90;0;0;4;537,50;58;20,50;210;5,60;3083;,90;3092;2,50;15546;2,40;23511;7,40;46759;2,10;366046;2,30;CM 690 8200;; +26025;VENDREDI;27/03/2026;5;26/06/2026;44;4;43;10;48;4;2;-4-10-43-44-48-;-2-4-;0;0;0;1;2;353647,20;3;6;27551,00;4;31;1660,90;115;605;156,70;302;1398;71,70;233;1172;60,10;5135;23682;14,80;5965;29474;13,30;12068;60402;12,10;29558;133485;6,60;104059;478401;5,80;208023;990321;4,50;1;1573,70;2;629,40;80;19,60;153;10,20;2512;1,50;2937;3,50;13956;3,50;22753;10,00;51300;2,50;475250;2,40;LL 648 3204;; +26024;MARDI;24/03/2026;4;23/06/2026;18;27;16;12;17;3;1;-12-16-17-18-27-;-1-3-;0;0;0;2;3;172870,70;0;9;13467,50;4;29;1301,80;193;804;86,40;346;1602;45,80;379;1645;31,40;4605;20456;12,60;7733;31703;9,00;16553;70112;7,60;21286;95948;6,70;101412;433402;4,70;218629;950092;3,40;0;0;2;1049,70;91;12,80;157;7,40;2172;1,30;3609;2,10;10107;3,60;14546;11,70;48254;1,90;321215;2,60;NJ 917 9927;; +26023;VENDREDI;20/03/2026;3;19/06/2026;16;12;5;37;46;8;10;-5-12-16-37-46-;-8-10-;0;0;0;1;3;220584,60;1;3;51554,20;5;20;2408,60;126;659;134,60;288;1314;71,30;309;1422;46,30;4404;19313;17,00;6681;29512;12,40;15985;69774;9,80;22855;101943;8,10;98674;436798;5,90;241970;1048414;4,00;1;1535,20;2;614,10;70;21,90;155;9,90;2320;1,60;3413;3,00;12125;3,90;20058;11,10;49809;2,50;430923;2,50;NG 772 7907;; +26022;MARDI;17/03/2026;2;16/06/2026;28;33;41;17;5;9;3;-5-17-28-33-41-;-3-9-;0;0;0;2;12;42063,70;6;16;7373,20;13;52;706,60;174;842;80,30;463;1906;37,50;373;1621;31,00;5569;25081;10,00;6503;30247;9,20;13353;59922;8,70;27951;125629;5,00;87155;395936;5,00;183067;813883;3,90;0;0;7;304,50;75;15,70;219;5,40;2671;1,10;3039;2,60;13681;2,70;22636;7,60;42640;2,20;358302;2,40;JI 632 6165;; +26021;VENDREDI;13/03/2026;1;12/06/2026;26;17;48;41;13;10;4;-13-17-26-41-48-;-4-10-;0;0;0;0;2;415147,60;1;5;38810,70;17;54;1119,30;231;867;128,40;532;1980;59,40;537;2202;37,50;7491;27769;14,80;9815;39875;11,50;24348;97919;8,70;38293;142691;7,20;146302;569073;5,70;358932;1399692;3,70;0;0;9;463,80;119;19,40;270;8,50;3881;1,40;4967;3,10;19992;3,60;32813;10,30;75584;2,50;639512;2,60;IZ 061 2918;; +26020;MARDI;10/03/2026;11;09/06/2026;27;12;50;14;44;4;12;-12-14-27-44-50-;-4-12-;0;1;209527211,00;1;2;516689,70;2;10;24151,70;17;52;1446,60;316;1125;123,10;607;2306;63,50;719;2598;39,60;8513;32452;15,80;13190;48880;11,70;32560;119072;8,90;42716;163108;7,90;188222;704669;5,70;475361;1744382;3,70;0;0;5;967,70;126;21,30;277;9,70;4065;1,60;6267;2,80;20580;4,10;32328;12,10;87815;2,50;719521;2,70;MM 280 8208;; +26019;VENDREDI;06/03/2026;10;05/06/2026;16;19;15;28;37;9;6;-15-16-19-28-37-;-6-9-;0;0;0;2;5;233193,40;3;9;30278,40;16;75;1131,70;386;1355;115,30;932;3475;47,50;818;2990;38,80;12783;48703;11,90;17577;62853;10,30;37062;135577;8,80;66772;251189;5,80;237677;870310;5,20;504535;1887204;3,90;1;3102,30;3;827,20;179;17,30;412;7,50;6007;1,20;8267;2,50;31319;3,10;49110;9,20;111911;2,20;916138;2,40;CZ 541 3784;; +26018;MARDI;03/03/2026;9;02/06/2026;50;24;6;34;7;5;7;-6-7-24-34-50-;-5-7-;0;0;0;0;6;147207,10;0;5;41285,60;5;46;1397,70;213;915;129,40;621;2592;48,30;378;1744;50,40;10337;41288;10,60;10253;43355;11,30;20318;86078;10,60;57813;225691;4,90;166899;677964;5,10;329297;1342534;4,10;0;0;3;1300,70;120;18,00;302;7,10;4768;1,10;4857;2,90;26138;2,60;40316;7,80;78301;2,20;667959;2,30;;; +26017;VENDREDI;27/02/2026;8;29/05/2026;27;24;39;42;14;10;6;-14-24-27-39-42-;-6-10-;0;0;0;5;14;75686,50;9;31;7988,60;23;76;1014,90;464;1610;88,20;683;2710;55,40;914;3615;29,10;8952;36233;14,50;14241;54853;10,70;32230;130052;8,40;45222;185628;7,10;187989;734167;5,60;432187;1755815;3,80;2;1313,40;7;300,20;207;12,60;330;7,90;4404;1,40;6676;2,60;22062;3,70;35829;10,70;89730;2,40;745452;2,50;LQ 197 7609;; +26016;MARDI;24/02/2026;7;26/05/2026;10;43;47;40;27;10;6;-10-27-40-43-47-;-6-10-;0;0;0;1;4;199961,60;3;6;31156,20;10;40;1455,60;167;772;138,90;376;1764;64,20;349;1740;45,70;5786;25330;15,70;7729;33171;13,30;17452;77066;10,70;32497;136052;7,30;119361;505784;6,20;273448;1178407;4,30;0;0;4;845,10;77;24,30;199;9,40;2856;1,60;3731;3,30;15458;3,80;25977;10,50;57203;2,70;550785;2,40;HG 063 7125;; +26015;VENDREDI;20/02/2026;6;22/05/2026;24;33;28;35;13;5;9;-13-24-28-33-35-;-5-9-;0;0;0;1;7;161198,00;4;10;26372,20;12;66;1244,50;337;1412;107,10;939;3804;42,00;728;2881;39,00;12913;52618;10,60;16005;62730;9,90;35179;129858;8,90;65149;261719;5,40;227794;877573;5,00;499846;1834281;3,90;1;3022,60;4;604,50;165;18,30;397;7,60;5958;1,20;7577;2,60;30162;3,10;48440;9,10;107517;2,30;884539;2,40;UR 586 8252;; +26014;MARDI;17/02/2026;5;19/05/2026;6;41;1;10;4;12;5;-1-4-6-10-41-;-5-12-;0;0;0;1;5;109156,50;0;4;31889,60;6;30;1324,30;161;664;110,20;262;1087;71,10;453;1653;32,80;3447;15352;17,70;6377;26733;11,30;16799;67841;8,30;17540;81765;8,30;84799;372429;5,70;216295;905276;3,80;1;1251,80;5;200,20;91;13,70;129;9,70;1841;1,60;3135;2,60;9303;4,20;15081;12,10;42133;2,40;343181;2,60;NR 762 9710;; +26013;VENDREDI;13/02/2026;4;15/05/2026;37;40;13;31;9;6;9;-9-13-31-37-40-;-6-9-;0;0;0;1;3;263509,10;2;7;26394,10;13;55;1046,30;238;837;126,60;642;2259;49,60;468;1771;44,40;9416;33340;11,80;11244;39671;11,00;23625;83918;9,70;48579;171434;5,70;168204;579594;5,30;354968;1231716;4,00;1;2382,60;8;238,20;132;18,00;298;7,90;4549;1,30;5767;2,70;23638;3,10;35923;9,60;82894;2,30;699833;2,40;PR 479 0473;; +26012;MARDI;10/02/2026;3;12/05/2026;1;19;42;17;34;8;5;-1-17-19-34-42-;-5-8-;0;0;0;0;0;0;1;6;102136,10;10;39;927,10;113;551;120,80;343;1631;43,10;285;1144;43,20;5320;23940;10,30;5727;25601;10,70;13856;56574;9,00;27425;127436;4,80;81408;370210;5,20;184475;797590;3,90;0;0;5;409,70;69;16,40;166;6,80;2523;1,10;2828;2,60;12797;2,80;21354;7,70;39825;2,30;332291;2,40;TO 505 2133;; +26011;VENDREDI;06/02/2026;2;08/05/2026;20;10;24;13;23;6;11;-10-13-20-23-24-;-6-11-;0;0;0;1;5;126931,60;3;14;10595,00;9;37;1248,60;269;984;86,40;371;1623;55,40;692;2662;23,70;5357;22579;14,00;9265;38119;9,20;25435;100007;6,50;25494;110407;7,20;118356;499088;5,00;304506;1258739;3,20;1;1497,40;5;239,50;126;11,80;193;7,70;2726;1,30;4514;2,20;12972;3,60;19415;11,20;57537;2,10;395381;2,70;EF 203 4608;; +26010;MARDI;03/02/2026;1;05/05/2026;27;28;37;34;26;4;9;-26-27-28-34-37-;-4-9-;0;0;0;0;1;471469,20;0;7;15741,40;3;18;1906,70;82;388;162,90;232;1151;58,00;202;877;53,50;3970;19584;11,90;4021;18939;13,80;9036;41789;11,60;23526;111331;5,30;67441;312092;5,90;152745;690122;4,30;0;0;1;1981,80;45;24,40;111;9,90;1872;1,40;2004;3,60;11169;3,10;18095;8,80;33271;2,70;320221;2,40;NO 936 5502;; +26009;VENDREDI;30/01/2026;10;01/05/2026;14;18;31;35;46;7;11;-14-18-31-35-46-;-7-11-;0;1;123555827,00;0;3;289086,80;2;7;28956,10;12;50;1262,60;170;854;136,10;430;1892;64,90;430;1874;46,10;6164;28458;15,10;8405;38283;12,50;18299;83970;10,60;33871;155850;6,90;129870;575481;5,90;280172;1257673;4,30;0;0;8;443,40;74;26,60;210;9,30;3005;1,60;3983;3,30;16435;3,70;29091;9,80;61727;2,60;610913;2,30;HV 221 1285;; +26008;MARDI;27/01/2026;9;28/04/2026;47;42;23;43;4;9;3;-4-23-42-43-47-;-3-9-;0;0;0;0;0;0;1;8;95070,10;5;24;1869,90;106;574;144,00;314;1638;53,30;195;1092;56,20;5566;26682;11,50;5244;26850;12,70;11401;56729;11,20;31265;142515;5,40;91858;441567;5,50;184591;882221;4,40;0;0;2;1189,70;52;25,40;146;9,00;2551;1,20;2521;3,50;14399;2,80;25222;7,60;43521;2,50;408859;2,30;CY 698 4086;; +26007;VENDREDI;23/01/2026;8;24/04/2026;4;42;5;13;21;3;10;-4-5-13-21-42-;-3-10-;0;0;0;0;2;398883,80;0;6;31075,20;11;60;967,90;241;1034;103,40;559;2305;49,00;592;2531;31,30;6890;30529;13,00;10497;47096;9,40;23988;107828;7,60;32433;151415;6,60;142333;644533;4,80;308576;1429692;3,50;0;0;2;1475,60;109;15,00;257;6,30;3359;1,20;4835;2,20;15891;3,20;23819;10,00;67817;1,90;451373;2,60;KC 826 3307;; +26006;MARDI;20/01/2026;7;21/04/2026;22;18;19;50;11;1;11;-11-18-19-22-50-;-1-11-;0;0;0;0;2;295401,40;1;3;46026,70;9;40;1075,20;101;615;128,80;260;1406;59,50;310;1468;40,00;3946;19497;15,00;6078;28948;11,30;15513;71902;8,50;19212;95225;7,70;87465;412376;5,60;230267;1058593;3,50;0;0;5;452,40;49;25,60;132;9,50;1975;1,50;2968;2,80;9602;4,10;14240;12,80;42251;2,40;314729;2,90;FR 631 6934;; +26005;VENDREDI;16/01/2026;6;17/04/2026;5;24;17;50;29;10;5;-5-17-24-29-50-;-5-10-;0;0;0;0;3;265077,70;3;16;11616,10;9;52;1113,20;203;1028;103,70;384;2143;52,60;446;2257;35,00;5717;29909;13,20;8613;43718;10,10;19748;97505;8,40;27731;148081;6,70;118961;606740;5,10;270885;1353642;3,70;0;0;2;1416,50;100;15,70;201;7,80;2844;1,30;4054;2,60;14103;3,50;20776;11,00;57765;2,20;432549;2,60;RC 732 6307;; +26004;MARDI;13/01/2026;5;14/04/2026;47;6;44;10;18;10;2;-6-10-18-44-47-;-2-10-;0;0;0;0;3;187049,90;2;6;21858,30;7;22;1856,80;127;573;131,30;333;1292;61,50;310;1350;41,40;4531;19414;14,30;5742;25611;12,10;13094;59191;9,80;21994;99749;7,00;85460;383361;5,70;194658;894698;3,90;0;0;2;1090,40;56;21,60;174;6,90;2316;1,30;2786;2,90;11079;3,40;17457;10,10;42077;2,30;337610;2,60;IB 283 6404;; +26003;VENDREDI;09/01/2026;4;10/04/2026;10;34;26;7;1;2;4;-1-7-10-26-34-;-2-4-;0;0;0;2;6;116285,00;3;9;18118,40;7;45;1128,60;241;928;100,80;445;1945;50,80;515;2090;33,20;6055;26684;13,00;9650;40232;9,60;20474;88601;8,10;28906;133462;6,50;124067;550548;5,00;266193;1194409;3,70;0;0;2;1357,90;96;15,70;185;8,10;2759;1,30;4541;2,20;13447;3,50;20744;10,60;58523;2,10;435263;2,50;GZ 103 4529;; +26002;MARDI;06/01/2026;3;07/04/2026;5;17;18;14;31;12;10;-5-14-17-18-31-;-10-12-;0;0;0;0;0;0;1;3;204841,20;4;22;1648,20;129;540;123,60;204;1007;70,10;418;1693;29,30;2943;13573;18,20;5315;22783;12,10;17053;71070;7,20;14943;68508;9,10;72116;313379;6,20;224029;962787;3,20;0;0;2;1025,60;65;17,50;109;10,40;1579;1,80;2730;2,70;7906;4,50;12428;13,30;37203;2,50;280074;2,90;EB 277 2217;; +26001;VENDREDI;02/01/2026;2;03/04/2026;46;42;27;44;8;10;1;-8-27-42-44-46-;-1-10-;0;0;0;0;4;181127,30;2;9;18814,40;8;37;1425,40;124;619;156,90;262;1344;76,40;283;1563;46,10;3783;19962;18,00;5585;29286;13,70;15260;76932;9,70;20750;107521;8,40;84500;435736;6,50;229238;1161243;3,90;0;0;3;886,50;69;21,40;139;10,60;1988;1,80;2813;3,50;10948;4,20;18224;11,80;42763;2,80;400288;2,60;QI 521 4040;; +25104;MARDI;30/12/2025;1;31/03/2026;29;44;26;11;34;10;1;-11-26-29-34-44-;-1-10-;0;0;0;0;1;539027,50;2;6;20996,60;3;22;1783,60;104;493;146,60;221;962;79,40;283;1405;38,20;3072;14686;18,20;4847;23190;12,90;13138;63325;8,80;16409;77735;8,60;71120;337455;6,30;196775;918795;3,70;0;0;1;2168,00;50;24,00;115;10,40;1692;1,70;2494;3,20;8737;4,30;14471;12,10;36392;2,70;321784;2,70;HR 329 6221;; +25103;VENDREDI;26/12/2025;4;27/03/2026;22;32;48;12;36;3;4;-12-22-32-36-48-;-3-4-;0;1;54026007,00;2;9;73045,20;2;8;19205,80;6;23;2080,70;172;801;110,00;309;1407;66,20;309;1654;39,50;4142;19713;16,60;6897;32786;11,10;14146;66857;10,10;23877;110345;7,40;102259;463404;5,50;205442;939287;4,40;0;0;5;540,70;91;16,50;146;10,20;2013;1,80;3347;3,00;11457;4,10;19308;11,30;48782;2,50;480598;2,20;SF 344 7031;; +25102;MARDI;23/12/2025;3;24/03/2026;26;29;44;8;27;11;12;-8-26-27-29-44-;-11-12-;0;0;0;2;6;99593,30;2;8;17457,40;3;22;1977,20;119;614;130,50;186;1046;80,90;318;1459;40,70;2887;15398;19,30;5403;27189;12,20;16495;75938;8,10;15062;81923;9,10;76310;382289;6,10;232481;1073472;3,50;1;1290,70;2;516,20;60;21,50;100;12,90;1555;2,00;2858;3,00;8208;4,90;13601;13,80;39561;2,60;319282;2,90;QK 002 7273;; +25101;VENDREDI;19/12/2025;2;20/03/2026;39;43;44;21;17;1;11;-17-21-39-43-44-;-1-11-;0;0;0;1;2;338895,20;1;6;26401,80;1;17;2902,40;118;647;140,40;236;1197;80,20;333;1589;42,40;3732;18490;18,20;5580;28182;13,30;14753;72337;9,60;20633;101590;8,30;87025;417169;6,40;235820;1095056;3,90;1;1561,60;0;0;52;54,00;121;12,90;2008;1,90;2906;3,60;10945;4,40;18701;12,10;44627;2,80;419166;2,70;MR 927 8268;; +25100;MARDI;16/12/2025;1;17/03/2026;41;14;16;44;40;2;10;-14-16-40-41-44-;-2-10-;0;0;0;1;1;488880,90;3;7;16322,70;4;16;2224,30;80;418;156,80;206;979;70,70;156;925;52,60;2873;14388;16,90;3666;18645;14,50;8531;44138;11,40;17982;81894;7,40;60081;293965;6,50;140198;693990;4,40;1;1113,90;2;445,50;41;27,10;122;9,10;1534;1,80;1908;3,90;9343;3,70;16350;9,90;31051;2,90;330996;2,40;SB 120 7415;; +25099;VENDREDI;12/12/2025;1;13/03/2026;30;7;25;41;37;11;5;-7-25-30-37-41-;-5-11-;0;1;17000000,00;1;4;164437,20;1;5;30745,30;5;26;1841,60;127;611;144,30;294;1447;64,40;302;1460;44,80;4561;21430;15,20;6471;30341;12,00;15419;68638;9,90;24090;115000;7,10;101530;457820;5,60;242534;1031927;4,00;0;0;4;729,40;67;24,10;154;10,50;2384;1,60;3234;3,30;12531;4,00;20752;11,40;51167;2,60;455579;2,50;QP 012 5788;; +25098;MARDI;09/12/2025;3;10/03/2026;13;49;2;29;8;2;11;-2-8-13-29-49-;-2-11-;0;1;142433804,00;1;7;110381,70;5;11;16416,90;15;46;1222,70;240;971;106,70;507;1982;55,20;558;2143;35,90;7221;28586;13,40;10771;41793;10,20;24447;93471;8,50;35592;143957;6,70;142535;571700;5,30;333400;1301047;3,70;1;1857,40;6;247,60;119;15,60;242;7,60;3424;1,30;5044;2,40;16714;3,50;25742;10,50;66002;2,30;505973;2,60;HV 507 3990;; +25097;VENDREDI;05/12/2025;2;06/03/2026;34;25;15;46;9;8;12;-9-15-25-34-46-;-8-12-;0;0;0;1;2;548975,60;3;14;18329,20;12;48;1665,10;267;1097;134,20;530;2013;77,30;715;2755;39,70;7776;30712;17,80;11887;48247;12,60;29829;119175;9,50;41564;164376;8,30;182315;712359;6,00;460232;1772798;3,90;1;2897,80;6;386,30;144;20,10;264;10,90;3991;1,80;5880;3,30;21240;4,20;35528;11,90;89103;2,60;810422;2,50;NQ 393 9174;; +25096;MARDI;02/12/2025;1;03/03/2026;14;4;41;13;20;6;12;-4-13-14-20-41-;-6-12-;0;0;0;0;2;249653,50;1;9;12966,20;8;23;1580,30;104;467;143,30;228;1004;70,50;286;1141;43,50;3429;14715;16,90;5063;21976;12,60;13223;58195;8,80;18294;78527;7,90;73102;326849;6,00;193521;860109;3,60;0;0;3;696,30;56;20,70;118;9,80;1884;1,50;2582;3,00;9875;3,70;15884;10,60;37243;2,50;311819;2,70;SB 684 6490;; +25095;VENDREDI;28/11/2025;14;27/02/2026;5;33;42;29;39;3;9;-5-29-33-39-42-;-3-9-;1;1;178656733,00;1;5;205348,80;0;6;39994,50;11;60;1245,70;230;1054;130,60;634;2987;48,70;471;2222;46,00;9733;46143;11,00;10045;49711;11,40;21435;102915;10,30;53199;243240;5,20;158225;749530;5,40;322957;1533526;4,20;0;0;3;1304,10;112;19,40;312;6,90;4449;1,20;4760;3,00;24359;2,80;40276;7,80;74702;2,40;659359;2,30;DT 593 0475;; +25094;MARDI;25/11/2025;13;24/02/2026;11;35;44;17;6;7;3;-6-11-17-35-44-;-3-7-;0;0;0;0;0;0;5;13;74451,30;19;73;782,30;198;985;106,80;665;3109;35,70;449;1979;39,40;10226;44655;8,70;9880;44650;9,70;18965;88922;9,10;53432;231707;4,20;137234;624136;4,90;261958;1240535;4,00;0;0;10;289,50;96;16,70;279;5,70;4439;,90;4413;2,40;22908;2,20;36149;6,40;62023;2,10;500048;2,30;LI 694 8496;; +25093;VENDREDI;21/11/2025;12;20/02/2026;35;17;48;29;19;9;5;-17-19-29-35-48-;-5-9-;0;0;0;1;5;187734,70;3;9;24375,90;16;67;1019,80;173;1034;121,70;544;2862;46,40;482;2348;39,80;8427;42501;11,00;10260;49206;10,50;21330;99866;9,70;44930;221576;5,30;149318;703948;5,20;319402;1454386;4,10;0;0;7;514,30;73;27,40;232;8,60;3710;1,30;4733;2,80;20202;3,10;32637;8,90;69569;2,30;597530;2,40;MF 112 7235;; +25092;MARDI;18/11/2025;11;17/02/2026;48;4;2;21;15;12;6;-2-4-15-21-48-;-6-12-;0;0;0;2;6;122146,50;2;8;21410,70;8;35;1524,30;179;744;132,00;309;1488;69,80;457;1940;37,60;4888;22269;16,30;7255;33187;12,20;18614;83955;9,00;26308;115687;7,90;105990;479072;6,00;268909;1219556;3,80;1;1570,60;4;314,10;73;21,50;152;10,30;2487;1,50;3419;3,00;13025;3,70;21964;10,40;50473;2,50;429657;2,60;PI 805 4876;; +25091;VENDREDI;14/11/2025;10;13/02/2026;9;45;26;48;27;9;8;-9-26-27-45-48-;-8-9-;0;0;0;1;3;294047,90;0;4;51542,80;8;41;1566,20;141;852;138,80;381;1949;64,10;335;1706;51,50;5701;27927;15,70;8823;43975;11,10;17521;88029;10,30;29359;145774;7,50;133302;650475;5,30;271490;1317488;4,20;1;1833,90;3;489,00;59;31,00;198;9,20;2872;1,50;4157;2,90;14308;4,00;22368;11,90;63427;2,30;549951;2,40;FF 071 6932;; +25090;MARDI;11/11/2025;9;10/02/2026;47;22;32;36;4;2;10;-4-22-32-36-47-;-2-10-;0;0;0;1;2;322798,60;2;4;37721,60;3;31;1516,00;113;593;145,90;231;1351;67,70;239;1353;47,50;3587;19910;16,10;4600;26686;13,40;10668;62698;10,60;20853;108725;7,40;71688;400105;6,30;169154;944285;4,30;0;0;3;730,30;62;19,60;114;10,60;1674;1,80;2237;3,60;10190;3,70;18141;9,70;34520;2,90;362932;2,40;UL 250 5542;; +25089;VENDREDI;07/11/2025;8;06/02/2026;40;21;43;11;39;8;2;-11-21-39-40-43-;-2-8-;0;0;0;0;4;199745,20;1;7;26676,40;8;30;1938,70;121;695;154,10;368;1983;57,10;271;1459;54,50;6549;31389;12,60;6400;34361;12,90;14117;76186;10,80;39043;181670;5,50;102922;526727;5,90;227806;1164335;4,30;0;0;5;602,90;64;26,10;191;8,70;2956;1,40;3173;3,50;18016;2,90;32150;7,60;50158;2,70;508165;2,30;BU 251 5247;; +25088;MARDI;04/11/2025;7;03/02/2026;25;28;45;9;6;1;4;-6-9-25-28-45-;-1-4-;0;0;0;0;3;197503,10;0;8;17309,90;6;29;1487,30;170;711;111,70;294;1404;59,80;406;1759;33,50;4124;19536;15,10;6546;30878;10,60;16130;74726;8,20;19806;98351;7,50;90743;431470;5,40;214763;1023577;3,60;0;0;2;1087,80;79;15,20;135;8,90;1915;1,50;3033;2,60;9359;4,00;15022;11,70;43357;2,20;331957;2,60;RJ 760 6571;; +25087;VENDREDI;31/10/2025;6;30/01/2026;43;38;5;45;14;7;11;-5-14-38-43-45-;-7-11-;0;0;0;0;2;405711,30;4;6;31607,10;9;41;1440,70;136;704;154,50;309;1646;69,80;285;1545;52,30;4941;26385;15,30;6700;34264;13,10;14429;75210;11,10;27982;148030;6,80;106265;528099;6,00;229681;1165262;4,40;0;0;4;753,50;62;27,00;172;9,70;2510;1,60;3312;3,30;14055;3,70;24453;9,90;52382;2,60;517235;2,30;CU 609 8974;; +25086;MARDI;28/10/2025;5;27/01/2026;35;49;24;7;8;2;12;-7-8-24-35-49-;-2-12-;0;0;0;1;1;540419,60;3;9;14033,80;10;22;1788,20;104;533;135,90;226;1051;72,80;259;1321;40,70;3030;15057;17,80;4905;24554;12,20;12492;61526;9,00;16165;79662;8,40;76124;363956;5,80;192928;917369;3,70;0;0;3;683,90;65;17,50;126;9,00;1563;1,80;2435;3,10;8530;4,20;13104;12,70;37628;2,40;307235;2,60;KT 230 2700;; +25085;VENDREDI;24/10/2025;4;23/01/2026;25;8;45;7;31;12;7;-7-8-25-31-45-;-7-12-;0;0;0;0;2;343025,20;4;12;13361,70;11;32;1560,60;203;854;107,70;350;1430;68,00;404;1784;38,30;4724;21481;15,90;8240;36614;10,40;17432;80170;8,80;23491;109256;7,80;115960;521011;5,10;254605;1166133;3,70;0;0;7;382,60;94;15,80;192;7,70;2490;1,40;3933;2,50;12226;3,80;18133;11,90;56081;2,10;418938;2,50;HE 121 1375;; +25084;MARDI;21/10/2025;3;20/01/2026;5;29;40;24;42;6;12;-5-24-29-40-42-;-6-12-;0;0;0;0;0;0;0;4;155906,70;3;16;2299,80;82;443;153,00;218;977;73,30;225;1097;45,90;3181;14683;17,10;4378;20790;13,50;11129;53264;9,80;16880;77393;8,10;68270;317298;6,20;173392;812936;3,90;0;0;1;1979,80;39;28,20;115;9,50;1656;1,60;2214;3,30;8871;3,90;14405;11,10;34219;2,60;294975;2,70;OA 155 5726;; +25083;VENDREDI;17/10/2025;2;16/01/2026;39;44;35;47;13;3;5;-13-35-39-44-47-;-3-5-;0;0;0;0;1;647213,60;0;6;25210,70;9;37;1273,30;134;635;136,60;317;1585;57,80;227;1242;51,90;5255;25880;12,40;6194;30386;11,80;12530;60552;11,00;30733;146767;5,50;89803;444580;5,70;184580;888164;4,60;0;0;6;437,70;64;22,70;157;9,20;2587;1,40;3053;3,20;14352;3,20;27657;7,70;44593;2,60;476460;2,20;HD 452 5951;; +25082;MARDI;14/10/2025;1;13/01/2026;8;5;18;16;14;10;3;-5-8-14-16-18-;-3-10-;0;0;0;1;2;234121,90;2;8;13679,50;6;26;1311,00;201;708;88,60;338;1318;50,30;474;1702;27,40;4192;17604;13,20;6863;27890;9,30;16711;67762;7,10;20445;87453;6,70;86770;372233;4,90;205398;876026;3,30;0;0;6;323,30;90;11,90;179;6,00;2148;1,20;3384;2,10;10221;3,30;14856;10,50;42620;2,00;296229;2,60;MJ 556 9345;; +25081;VENDREDI;10/10/2025;2;09/01/2026;7;21;20;17;6;10;1;-6-7-17-20-21-;-1-10-;0;1;29671390,00;3;11;60131,50;4;13;11891,60;3;37;1301,30;218;897;98,80;416;1656;56,60;675;2549;25,80;4965;21288;15,40;9782;38649;9,50;25960;107097;6,30;22718;100891;8,20;119180;501601;5,20;319152;1360688;3,00;1;1526,50;1;1221,20;95;16,00;206;7,40;2547;1,40;4609;2,20;11742;4,00;17409;12,80;58031;2,10;380273;2,90;MX 457 8544;; +25080;MARDI;07/10/2025;1;06/01/2026;24;39;48;43;42;5;8;-24-39-42-43-48-;-5-8-;0;0;0;0;2;241122,50;0;3;37569,50;7;22;1595,70;93;373;173,30;232;1213;56,30;178;851;56,40;3839;19086;12,50;3761;18910;14,10;8359;40481;12,30;23479;112362;5,30;60177;294871;6,40;130786;623926;4,90;0;0;5;401,70;57;19,50;126;8,80;1960;1,40;1988;3,70;11587;3,00;22682;7,10;31252;2,90;358754;2,20;GE 840 8288;; +25079;VENDREDI;03/10/2025;2;02/01/2026;25;41;6;12;18;2;6;-6-12-18-25-41-;-2-6-;0;1;29922038,00;0;1;674530,30;3;8;19706,10;24;67;732,80;234;985;91,80;590;2427;39,30;437;1952;34,40;7923;31531;10,60;9781;40937;9,10;19317;81557;8,50;35855;149056;5,60;128145;531787;5,00;256411;1109294;3,80;0;0;9;313,20;111;14,10;273;5,70;3636;1,00;4779;2,10;16762;2,90;24438;9,30;62401;2,00;457442;2,40;NV 550 7879;; +25078;MARDI;30/09/2025;1;30/12/2025;48;15;8;3;17;2;8;-3-8-15-17-48-;-2-8-;0;0;0;1;8;62213,20;0;2;58161,00;7;47;770,80;156;645;103,40;402;1907;36,90;295;1243;39,80;5649;26244;9,40;6684;29414;9,40;13874;59939;8,50;28453;127594;4,80;89521;406247;4,80;186811;848336;3,70;1;1141,40;3;304,30;86;13,20;174;6,50;2762;1,00;3327;2,20;13757;2,60;20107;8,20;44404;2,10;324942;2,50;HX 965 5996;; +25077;VENDREDI;26/09/2025;6;26/12/2025;25;17;4;44;28;11;5;-4-17-25-28-44-;-5-11-;0;1;130000000,00;5;12;95952,40;1;17;15829,80;22;94;891,70;455;1708;90,40;738;3013;54,10;1113;4004;28,60;9885;41525;13,80;17089;66930;9,50;41911;154860;7,60;47901;207067;6,90;215459;869935;5,20;525160;1999731;3,60;4;733,90;10;234,80;209;14,00;377;7,70;4798;1,50;8041;2,40;23607;3,90;37831;11,30;102905;2,30;808839;2,60;IQ 493 4176;; +25076;MARDI;23/09/2025;5;23/12/2025;13;29;33;11;24;5;2;-11-13-24-29-33-;-2-5-;0;0;0;0;1;565440,80;1;4;33038,20;9;64;643,10;186;884;85,70;455;2140;37,40;414;1861;30,20;5964;27386;10,20;8980;40618;7,70;17768;80304;7,20;27530;127952;5,50;109481;496268;4,40;226396;1015359;3,50;0;0;6;374,50;98;12,70;199;6,20;2721;1,10;4138;2,00;12803;3,00;19390;9,40;52043;1,90;361528;2,50;IW 582 7095;; +25075;VENDREDI;19/09/2025;4;19/12/2025;26;32;42;8;10;9;12;-8-10-26-32-42-;-9-12-;0;0;0;0;1;700266,50;2;5;32732,70;1;36;1416,00;121;677;138,70;271;1383;71,70;342;1615;43,10;3727;19071;18,20;6087;30632;12,70;16011;77812;9,30;19766;100763;8,70;93776;461345;5,90;249728;1168223;3,80;0;0;1;2685,20;67;22,20;131;11,30;1898;1,90;3038;3,20;10299;4,50;17161;12,60;46769;2,60;407244;2,60;UP 609 9974;; +25074;MARDI;16/09/2025;3;16/12/2025;35;9;13;1;40;6;5;-1-9-13-35-40-;-5-6-;0;0;0;1;2;265444,10;0;3;41359,10;5;26;1486,40;112;542;131,30;228;1210;62,10;260;1232;42,90;3649;18150;14,50;5921;27066;10,80;12247;55780;9,80;19478;96689;6,80;87812;404814;5,10;178623;824639;4,00;1;1181,80;2;472,70;62;19,00;116;10,10;1834;1,60;2931;2,70;9594;3,80;15314;11,20;42934;2,20;364306;2,30;TW 640 9289;; +25073;VENDREDI;12/09/2025;2;12/12/2025;10;37;31;5;23;3;11;-5-10-23-31-37-;-3-11-;0;0;0;1;2;333007,50;1;5;31131,70;8;49;989,40;164;761;117,30;356;1674;56,40;447;1911;34,70;4940;23276;14,20;8254;36368;10,10;19189;81573;8,40;25431;118433;7,00;112101;502978;5,20;263821;1136356;3,70;0;0;3;917,60;77;19,80;177;8,60;2445;1,50;3948;2,50;12741;3,70;21258;10,50;54564;2,30;426817;2,60;JQ 308 4438;; +25072;MARDI;09/09/2025;1;09/12/2025;3;24;13;39;40;8;2;-3-13-24-39-40-;-2-8-;0;0;0;0;0;0;1;5;123026,70;3;21;1728,40;116;598;111,80;337;1542;45,80;236;1321;37,50;4785;22193;11,10;5216;24829;11,10;11724;56664;9,10;26035;116099;5,30;79931;360618;5,40;177273;807700;3,90;0;0;1;2123,60;55;21,40;169;6,90;2352;1,20;2623;3,00;12648;2,90;22211;7,70;39958;2,40;349664;2,40;HW 329 7662;; +25071;VENDREDI;05/09/2025;5;05/12/2025;30;27;41;31;43;5;8;-27-30-31-41-43-;-5-8-;0;1;65278573,00;0;1;743932,40;0;5;34773,80;6;30;1805,20;146;621;160,60;386;1934;54,50;283;1386;53,40;6433;30838;12,00;6964;32080;12,80;14020;67211;11,40;38510;179491;5,10;104276;493537;5,90;225078;1046162;4,50;0;0;4;768,20;67;25,40;191;8,90;3045;1,40;3419;3,30;18018;2,90;32593;7,60;50980;2,70;527451;2,30;EQ 858 3674;; +25070;MARDI;02/09/2025;4;02/12/2025;31;32;36;13;30;12;1;-13-30-31-32-36-;-1-12-;0;0;0;0;2;284860,20;0;4;33288,20;1;20;2073,60;80;429;178,00;205;982;82,20;185;1064;53,30;3284;15911;17,80;4187;20546;15,40;11003;54062;10,90;19157;86825;8,20;68157;323676;6,90;187995;880244;4,10;0;0;1;2346,20;44;29,60;112;11,60;1734;1,80;2162;4,00;10193;4,00;16837;11,30;35138;3,00;339671;2,70;GX 069 8683;; +25069;VENDREDI;29/08/2025;3;28/11/2025;6;13;37;10;9;1;7;-6-9-10-13-37-;-1-7-;0;0;0;1;5;138979,10;0;2;81204,30;22;65;778,20;238;910;102,40;667;2147;45,80;512;1893;36,50;9002;32086;10,70;10961;41690;9,20;21806;87909;8,10;39308;150987;5,70;142083;574759;4,70;282781;1208430;3,60;1;1580,40;6;210,70;108;14,60;294;5,30;4124;,90;5205;2,00;18396;2,70;25382;9,00;67540;1,90;448752;2,50;IA 474 1654;; +25068;MARDI;26/08/2025;2;25/11/2025;2;42;38;14;45;11;10;-2-14-38-42-45-;-10-11-;0;0;0;0;2;255796,20;1;4;29891,90;4;19;1960,10;93;468;146,50;175;832;87,10;235;1079;47,20;2483;12514;20,30;4320;20246;14,00;11142;50826;10,40;13365;68046;9,40;63919;295080;6,80;171850;770372;4,20;0;0;4;539,40;58;20,60;84;14,20;1326;2,20;2276;3,50;7049;5,30;12155;14,30;33101;2,90;317911;2,70;QW 618 1429;; +25067;VENDREDI;22/08/2025;1;21/11/2025;39;30;9;12;6;10;4;-6-9-12-30-39-;-4-10-;0;0;0;1;2;330801,20;1;11;14057,00;5;36;1337,80;179;823;107,80;389;1602;58,50;519;2100;31,30;5091;21707;15,10;8213;34752;10,50;22643;91081;7,50;24634;109421;7,50;108318;465401;5,60;277193;1170392;3,50;0;0;3;903,30;66;22,80;196;7,60;2507;1,50;4033;2,50;12565;3,70;20171;10,80;53481;2,30;399661;2,70;BO 496 3223;; +25066;MARDI;19/08/2025;18;18/11/2025;24;43;41;34;31;8;6;-24-31-34-41-43-;-6-8-;1;1;250000000,00;4;7;270238,90;2;7;38515,60;26;78;1076,60;313;1150;134,50;622;2489;65,70;583;2286;50,20;9855;38081;15,00;11635;47427;13,50;25966;105829;11,20;56596;214362;6,70;183800;734877;6,10;407128;1622924;4,50;0;0;3;1717,10;144;19,80;291;9,80;4523;1,50;5332;3,50;25750;3,50;47903;8,70;84274;2,80;896145;2,30;VK 747 8211;; +25065;VENDREDI;15/08/2025;17;14/11/2025;36;40;35;30;13;6;2;-13-30-35-36-40-;-2-6-;0;0;0;0;2;592222,90;2;13;21294,10;16;63;1368,60;203;1023;155,20;540;2481;67,60;485;2339;50,40;8978;40120;14,70;10232;48421;13,50;23213;109005;11,20;55801;234111;6,30;168516;759032;6,10;366018;1688844;4,40;0;0;9;525,90;96;27,30;246;10,60;3968;1,60;4796;3,60;25162;3,20;44960;8,50;77998;2,70;824436;2,30;JZ 186 6062;; +25064;MARDI;12/08/2025;16;11/11/2025;46;48;28;18;42;3;9;-18-28-42-46-48-;-3-9-;0;0;0;0;1;970058,30;1;6;37786,40;15;58;1217,50;190;839;155,00;557;2359;58,20;418;1914;50,40;9028;36957;13,00;9876;42492;12,60;21681;92517;10,80;54129;215719;5,60;152183;640893;5,90;325985;1364969;4,50;0;0;4;1024,10;92;24,70;251;9,00;4159;1,30;4624;3,20;24565;2,90;42990;7,70;70473;2,60;715143;2,30;IT 232 9034;; +25063;VENDREDI;08/08/2025;15;07/11/2025;2;34;19;12;44;10;6;-2-12-19-34-44-;-6-10-;0;0;0;2;6;183180,90;8;21;12232,10;26;72;1111,20;510;1464;100,60;576;2636;59,10;1629;4276;25,60;8400;36400;15,00;13746;54367;11,20;35836;136121;8,30;41930;185239;7,40;185421;761556;5,60;448412;1883537;3,70;1;2472,40;11;179,80;231;10,70;272;9,00;3992;1,50;6491;2,50;20175;3,80;32203;11,20;87041;2,30;685511;2,60;FU 501 2143;; +25062;MARDI;05/08/2025;14;04/11/2025;1;3;5;42;47;5;10;-1-3-5-42-47-;-5-10-;0;0;0;0;3;281225,80;1;5;39436,20;5;28;2193,40;132;736;153,70;419;1848;64,70;363;1683;49,90;6465;27758;15,10;8786;38712;12,10;20396;89293;9,70;35589;154890;6,80;135205;579368;5,70;307433;1303797;4,10;0;0;3;1153,20;55;34,90;182;10,50;3067;1,50;4264;3,00;16987;3,50;26539;10,50;64143;2,40;544786;2,50;DL 585 0537;; +25061;VENDREDI;01/08/2025;13;31/10/2025;29;4;25;16;30;10;2;-4-16-25-29-30-;-2-10-;0;0;0;0;3;331615,70;4;14;16608,00;17;68;1065,00;269;1313;101,60;546;2461;57,30;786;3201;30,90;7385;34085;14,50;11864;52194;10,50;28725;126467;8,10;38234;172275;7,20;156572;704224;5,50;375741;1683554;3,70;0;0;9;432,70;137;15,70;280;7,70;3562;1,50;5554;2,60;18443;3,60;30796;10,20;73809;2,40;606018;2,50;AX 511 9972;; +25060;MARDI;29/07/2025;12;28/10/2025;5;46;44;6;42;4;8;-5-6-42-44-46-;-4-8-;0;0;0;0;4;193669,00;1;5;36210,90;6;25;2255,70;114;582;178,40;285;1651;66,50;273;1380;55,90;5594;28236;13,60;5655;30308;14,20;12523;66114;12,10;37906;174670;5,50;99074;500434;6,10;224250;1121436;4,30;0;0;5;553,50;50;30,70;125;12,30;2543;1,50;2760;3,70;16737;2,80;25529;8,70;47523;2,60;449065;2,40;RE 350 7487;; +25059;VENDREDI;25/07/2025;11;24/10/2025;32;36;23;6;7;11;12;-6-7-23-32-36-;-11-12-;0;0;0;1;5;180631,10;2;9;23453,50;6;27;2435,00;153;854;141,80;291;1611;79,40;393;2261;39,70;4232;23762;18,90;7132;39057;12,80;21840;109645;8,50;23064;126862;8,90;107613;568250;6,20;325664;1597566;3,50;0;0;5;663,00;74;24,80;147;12,50;2213;2,00;3622;3,40;11847;4,80;19421;13,80;54056;2,80;458727;2,90;LF 343 4676;; +25058;MARDI;22/07/2025;10;21/10/2025;8;41;33;26;15;9;10;-8-15-26-33-41-;-9-10-;0;0;0;1;5;144005,00;0;13;12944,70;3;20;2620,70;190;955;101,10;264;1355;75,30;416;2090;34,30;3499;19255;18,60;6904;35872;11,10;16129;82421;9,00;19504;102270;8,80;97854;500245;5,60;234721;1172483;3,90;1;1461,20;2;584,50;102;14,30;122;11,90;1722;2,10;3145;3,10;9797;4,60;16140;13,20;46625;2,50;419618;2,50;VM 159 6614;; +25057;VENDREDI;18/07/2025;9;17/10/2025;25;19;42;45;13;9;2;-13-19-25-42-45-;-2-9-;0;0;0;1;8;114872,30;1;8;26847,50;12;52;1286,50;266;1189;103,60;541;2628;49,50;479;2432;37,60;7865;37716;12,10;10504;49680;10,20;22348;106931;8,80;39413;190854;6,00;150549;709562;5,10;316282;1500884;3,80;0;0;5;676,30;117;16,00;238;7,80;3619;1,20;4843;2,50;18497;3,10;28589;9,50;70688;2,10;547619;2,40;TR 056 2866;; +25056;MARDI;15/07/2025;8;14/10/2025;41;45;24;49;38;1;6;-24-38-41-45-49-;-1-6-;0;0;0;0;1;625081,30;3;10;14609,10;3;24;1896,00;94;503;166,60;212;1155;76,70;246;1242;50,10;3360;17486;17,80;4713;24263;14,30;11068;58905;10,90;19528;99931;7,80;69788;362410;6,80;162882;859234;4,60;0;0;1;2297,60;47;27,10;100;12,70;1663;1,90;2293;3,70;9526;4,20;18936;9,80;34606;3,00;395050;2,30;BY 654 8780;; +25055;VENDREDI;11/07/2025;7;10/10/2025;8;24;45;23;49;2;10;-8-23-24-45-49-;-2-10-;0;0;0;0;1;762238,80;1;8;22268,40;4;32;1734,00;124;724;141,10;319;1520;71,00;353;1754;43,20;5273;24375;15,50;6843;33587;12,60;16877;81649;9,60;27884;130005;7,30;108533;510338;5,80;260627;1233108;3,90;0;0;2;1487,70;67;24,60;167;9,80;2715;1,50;3439;3,20;13863;3,70;23011;10,40;53524;2,50;459152;2,60;NG 234 1973;; +25054;MARDI;08/07/2025;6;07/10/2025;50;18;9;1;8;5;1;-1-8-9-18-50-;-1-5-;0;0;0;1;9;66695,40;0;6;23381,70;8;44;993,10;226;974;82,60;398;1898;44,80;366;1736;34,40;5235;23391;12,70;8115;37143;8,90;16254;72418;8,50;24212;112534;6,60;102932;470066;5,00;217983;985419;3,80;0;0;6;388,90;105;12,30;201;6,40;2516;1,20;3873;2,20;11636;3,50;16789;11,20;49661;2,10;360609;2,60;PR 568 2088;; +25053;VENDREDI;04/07/2025;5;03/10/2025;45;19;48;29;42;5;7;-19-29-42-45-48-;-5-7-;0;0;0;0;1;723367,00;4;10;16906,20;5;39;1350,20;137;661;146,70;440;2016;50,80;270;1392;51,70;7536;32284;11,10;6978;32558;12,30;13815;66828;11,10;43109;184953;4,90;110770;506291;5,60;218418;1025052;4,40;0;0;1;2878,10;73;21,90;199;8,00;3516;1,10;3395;3,10;20129;2,50;35093;6,60;53291;2,40;532587;2,10;ND 836 8918;; +25052;MARDI;01/07/2025;4;30/09/2025;34;17;1;28;32;7;8;-1-17-28-32-34-;-7-8-;0;0;0;0;3;186076,90;5;8;16308,40;3;34;1195,20;128;602;124,30;227;1231;64,20;212;1122;49,50;3841;18291;15,20;6491;29582;10,40;11454;55354;10,40;21258;97882;7,10;95831;433847;5,00;171061;825734;4,20;0;0;1;2230,00;57;21,70;100;12,30;1860;1,60;3125;2,60;10338;3,70;17289;10,40;46300;2,20;410470;2,10;JC 350 5018;; +25051;VENDREDI;27/06/2025;3;26/09/2025;27;45;19;36;49;7;10;-19-27-36-45-49-;-7-10-;0;0;0;0;5;139732,90;2;6;27214,90;5;34;1495,90;166;790;118,50;363;1525;64,90;330;1643;42,30;5013;21480;16,20;6971;32660;11,80;14875;72964;9,90;28035;119182;7,30;102363;467164;5,90;216491;1027285;4,30;0;0;1;2812,50;73;21,40;195;8,00;2512;1,50;3458;3,00;14083;3,40;24462;9,30;50584;2,50;486275;2,30;EL 697 7800;; +25050;MARDI;24/06/2025;2;23/09/2025;16;21;39;7;23;11;7;-7-16-21-23-39-;-7-11-;0;0;0;1;4;129694,40;1;2;60623,40;4;31;1218,20;149;718;96,80;315;1708;43,00;338;1602;32,20;4417;22541;11,40;7158;33379;8,60;14871;68902;7,70;21377;102456;6,30;98913;442550;4,60;207372;942225;3,40;0;0;1;2144,60;69;17,20;163;7,30;2203;1,30;3514;2,20;11026;3,40;15818;10,90;48406;2,00;337100;2,50;KD 688 9409;; +25049;VENDREDI;20/06/2025;1;19/09/2025;37;24;47;5;8;9;3;-5-8-24-37-47-;-3-9-;0;0;0;1;4;172608,90;1;4;40341,50;10;41;1225,80;173;814;113,70;506;2032;48,10;357;1596;43,00;7201;30280;11,30;8048;34959;10,90;17178;72626;9,80;38484;160260;5,30;118866;510014;5,30;250880;1064811;4,10;1;1673,40;3;446,20;86;19,40;249;6,70;3497;1,10;4041;2,70;18753;2,80;32578;7,40;58690;2,30;518453;2,30;WN 956 0331;; +25048;MARDI;17/06/2025;21;16/09/2025;22;44;49;23;13;3;5;-13-22-23-44-49-;-3-5-;0;1;250000000,00;0;3;7825661,00;5;16;20064,10;18;77;1298,50;369;1609;114,40;1028;4042;48,10;739;3302;41,40;14300;59720;11,40;18590;76119;10,00;37663;153398;9,20;73453;308585;5,50;276137;1129769;4,70;543331;2222499;3,90;0;0;10;572,80;179;17,70;476;6,60;6464;1,20;8516;2,50;33269;3,00;52235;8,80;125738;2,00;959702;2,40;PG 494 5686;; +25047;VENDREDI;13/06/2025;20;12/09/2025;43;40;45;2;28;3;7;-2-28-40-43-45-;-3-7-;0;0;0;0;7;4493785,70;5;17;25302,20;25;86;1557,80;373;1613;153,00;1112;4505;57,90;852;3546;51,70;19638;73437;12,40;19135;76430;13,30;39417;163963;11,60;122516;450381;5,10;304082;1191575;6,00;623914;2534452;4,60;0;0;14;603,30;188;24,90;484;9,60;9125;1,20;9310;3,30;55163;2,60;107972;6,30;145353;2,60;1570697;2,10;OW 405 0185;; +25046;MARDI;10/06/2025;19;09/09/2025;36;40;45;39;19;5;6;-19-36-39-40-45-;-5-6-;0;0;0;1;4;5914294,60;5;12;26957,40;13;56;1799,20;255;1221;152,00;588;2672;73,40;548;2723;50,60;8658;41884;16,40;13031;58295;13,10;28063;127177;11,20;50281;235400;7,30;203475;900370;6,00;429572;1918167;4,50;1;3157,50;4;631,50;114;27,60;277;11,30;4129;1,90;6091;3,40;23359;4,20;43394;10,60;94230;2,70;1032154;2,20;UF 448 1097;; +25045;VENDREDI;06/06/2025;18;05/09/2025;30;29;21;20;35;2;12;-20-21-29-30-35-;-2-12-;0;0;0;1;7;2737810,60;3;18;23054,20;32;99;1305,60;451;1764;134,90;978;3682;68,30;956;3843;46,00;13560;53529;16,50;20837;82454;11,90;48241;186493;9,80;72845;283271;7,80;301684;1198212;5,80;731493;2871563;3,90;1;4476,70;17;210,60;222;20,10;427;10,40;6497;1,70;9886;3,00;35454;3,90;57627;11,30;143162;2,50;1257341;2,50;KR 483 4676;; +25044;MARDI;03/06/2025;17;02/09/2025;47;12;15;38;48;7;5;-12-15-38-47-48-;-5-7-;0;0;0;1;6;212330,10;3;9;33083,30;10;77;1204,40;257;1180;144,70;862;3348;53,90;569;2562;49,50;13507;52778;12,00;13732;57834;12,20;28362;120208;10,90;76933;294124;5,40;216553;881610;5,70;441467;1826079;4,40;1;3041,70;2;1216,60;143;21,20;399;7,60;6090;1,20;6496;3,10;34336;2,70;58767;7,50;100125;2,50;970006;2,20;DK 401 0261;; +25043;VENDREDI;30/05/2025;16;29/08/2025;36;7;14;4;33;1;5;-4-7-14-33-36-;-1-5-;0;0;0;6;10;126179,90;7;18;16383,50;19;59;1556,80;280;1313;128,80;693;3138;57,00;737;3336;37,60;10383;44407;14,10;15125;66133;10,50;37103;157111;8,30;53120;226299;6,90;214912;932814;5,30;498772;2129110;3,70;0;0;7;727,50;126;22,40;314;9,00;4770;1,40;6864;2,70;23871;3,70;38744;10,60;98241;2,30;804227;2,50;BB 232 0733;; +25042;MARDI;27/05/2025;15;26/08/2025;30;41;38;12;40;4;12;-12-30-38-40-41-;-4-12-;0;0;0;0;2;486735,00;2;10;22751,60;12;42;1687,20;220;891;146,50;397;1815;76,00;442;1987;48,80;6570;27160;17,80;9775;40249;13,40;22920;94907;10,60;37210;147631;8,20;150047;601177;6,30;364581;1451448;4,20;0;0;6;719,50;98;24,40;179;13,30;3100;1,90;4622;3,40;17798;4,20;30184;11,60;70156;2,80;671279;2,50;QR 074 0084;; +25041;VENDREDI;23/05/2025;14;22/08/2025;49;43;29;46;10;12;7;-10-29-43-46-49-;-7-12-;0;0;0;1;4;270867,00;3;10;25322,40;5;43;1834,20;191;882;164,70;401;1925;79,70;441;2151;50,10;6646;29802;18,10;9447;43716;13,70;20899;97583;11,40;36828;167141;8,10;149202;673673;6,30;332536;1527587;4,50;0;0;3;1409,60;101;23,20;193;12,10;3346;1,70;4642;3,30;18133;4,00;31963;10,70;70970;2,70;723896;2,30;RB 444 2267;; +25040;MARDI;20/05/2025;13;19/08/2025;47;1;29;8;13;5;6;-1-8-13-29-47-;-5-6-;0;0;0;0;1;839110,40;1;7;28016,20;13;49;1246,60;262;1034;108,80;456;2060;57,70;526;2154;38,80;6297;28703;14,50;10801;45087;10,30;23214;96677;8,90;31974;148864;7,00;153230;644453;5,10;316727;1352916;3,90;0;0;5;663,90;108;17,00;218;8,40;2868;1,60;4913;2,50;14841;3,90;23375;11,50;70321;2,10;555450;2,40;AW 582 7504;; +25039;VENDREDI;16/05/2025;12;15/08/2025;25;9;37;46;6;6;12;-6-9-25-37-46-;-6-12-;0;0;0;1;5;199795,20;1;7;33353,80;13;50;1454,40;224;1074;124,70;515;2132;66,40;498;2348;42,30;7105;30789;16,10;10181;44698;12,40;24356;106981;9,60;38153;160167;7,80;152661;656758;6,00;367525;1578227;4,00;1;2260,50;6;301,40;112;20,10;258;8,70;3576;1,50;4949;3,00;18948;3,70;30757;10,70;72852;2,50;618414;2,60;CI 695 9575;; +25038;MARDI;13/05/2025;11;12/08/2025;19;9;44;47;50;2;9;-9-19-44-47-50-;-2-9-;0;0;0;1;2;382845,10;0;4;44738,60;7;48;1161,20;149;760;135,10;450;2167;50,00;293;1643;46,40;6504;29645;12,80;9218;46202;9,20;16322;81295;9,70;33889;156013;6,10;121166;558850;5,40;244061;1142108;4,20;0;0;3;986,00;70;23,40;208;7,90;3020;1,30;4321;2,50;15809;3,20;25509;9,40;57291;2,30;497520;2,40;SQ 356 6673;; +25037;VENDREDI;09/05/2025;10;08/08/2025;15;18;25;47;29;9;5;-15-18-25-29-47-;-5-9-;0;0;0;2;6;153430,40;2;6;35859,20;14;69;971,20;195;1070;115,30;637;3254;40,10;445;2190;41,80;8672;43669;10,50;10374;49730;10,20;21264;100159;9,50;44882;219063;5,20;148315;704694;5,10;306617;1430207;4,00;0;0;9;389,60;98;19,80;273;7,10;3876;1,20;4726;2,70;20137;3,00;32223;8,80;68572;2,30;580251;2,40;EI 835 2462;; +25036;MARDI;06/05/2025;9;05/08/2025;23;24;47;48;8;4;9;-8-23-24-47-48-;-4-9-;0;0;0;1;3;226674,40;1;7;22704,60;9;34;1455,90;139;590;154,50;377;1803;53,40;292;1386;48,80;6322;28867;11,70;6957;30711;12,30;14989;68538;10,20;34842;156396;5,40;109081;476415;5,60;240583;1050239;4,10;1;1545,90;3;412,20;57;27,10;176;8,70;2870;1,30;3305;3,10;15707;3,10;26008;8,60;50621;2,50;450313;2,40;LW 054 7366;; +25035;VENDREDI;02/05/2025;8;01/08/2025;21;49;5;3;19;1;10;-3-5-19-21-49-;-1-10-;0;0;0;1;4;209233,30;3;8;24450,60;8;42;1450,60;242;1050;106,80;376;1847;64,20;647;2832;29,40;5126;25353;16,40;9098;43044;10,80;25042;116371;7,40;24254;127471;8,20;121450;591292;5,50;328304;1576485;3,30;0;0;3;967,20;124;13,00;193;8,30;2562;1,50;4334;2,40;12325;4,10;18711;12,50;57625;2,30;412808;2,80;SN 962 2277;; +25034;MARDI;29/04/2025;7;29/07/2025;24;30;41;2;9;11;1;-2-9-24-30-41-;-1-11-;0;0;0;0;0;0;2;11;65420,20;7;32;1326,90;130;605;129,20;229;1156;71,50;264;1502;38,60;3301;16848;17,20;5267;25854;12,50;14499;67339;8,90;17239;89036;8,20;76684;373985;6,10;209656;972679;3,80;0;0;4;551,60;74;16,50;125;9,80;1664;1,80;2476;3,30;8614;4,40;14325;12,40;37303;2,70;321126;2,70;DN 431 1820;; +25033;VENDREDI;25/04/2025;6;25/07/2025;13;32;39;41;22;1;12;-13-22-32-39-41-;-1-12-;0;0;0;0;5;148333,50;1;5;34667,90;11;41;1316,80;140;696;142,80;283;1345;78,10;446;2202;33,50;3890;20366;18,10;5819;29462;13,90;17005;85992;8,90;21838;111027;8,30;82342;426810;6,80;239976;1213128;3,80;0;0;7;396,80;66;23,30;142;10,80;2033;1,80;2954;3,50;11463;4,20;20291;11,10;41059;3,10;403059;2,70;UF 496 9318;; +25032;MARDI;22/04/2025;5;22/07/2025;37;47;33;7;28;9;12;-7-28-33-37-47-;-9-12-;0;0;0;0;0;0;0;4;161263,00;7;28;1359,30;88;498;140,70;197;947;78,20;241;1250;41,60;2724;13963;18,60;4633;24882;11,60;12233;62197;8,60;14302;74011;8,80;68503;342032;6,00;175513;848025;3,90;0;0;2;988,70;37;29,60;120;9,10;1365;2,00;2284;3,20;7415;4,60;12569;12,70;33612;2,60;303179;2,60;VQ 339 8773;; +25031;VENDREDI;18/04/2025;4;18/07/2025;20;48;39;35;27;3;8;-20-27-35-39-48-;-3-8-;0;0;0;1;5;134387,60;1;6;26173,80;9;44;1111,70;150;694;129,80;329;1760;54,10;310;1397;47,90;5549;26824;12,40;6425;30751;12,10;13266;63633;10,90;31846;148487;5,60;97192;452131;5,80;198671;941740;4,50;0;0;7;392,60;73;20,90;152;10,00;2674;1,40;3215;3,10;15219;3,10;27914;7,90;47799;2,60;491736;2,20;RF 024 0852;; +25030;MARDI;15/04/2025;3;15/07/2025;3;31;38;44;33;12;6;-3-31-33-38-44-;-6-12-;0;0;0;0;0;0;0;5;127814,00;6;23;1639,50;57;415;167,30;192;825;89,00;197;1042;49,50;3158;14405;17,90;4223;20484;14,00;11129;52563;10,10;17443;77515;8,30;65802;314277;6,50;168309;807850;4,00;0;0;4;521,70;30;38,60;109;10,60;1639;1,70;2151;3,60;9213;3,90;15796;10,70;33379;2,80;323426;2,60;UE 731 6890;; +25029;VENDREDI;11/04/2025;2;11/07/2025;14;34;46;49;28;3;8;-14-28-34-46-49-;-3-8-;0;0;0;1;4;165122,30;0;6;25727,80;11;34;1414,10;147;621;142,60;349;1706;54,80;292;1336;49,20;5635;25904;12,60;6066;28376;12,90;13021;59927;11,40;32298;145952;5,60;98232;438492;5,90;203478;914696;4,50;1;1567,90;10;125,40;79;19,80;174;9,00;2694;1,40;3078;3,40;15170;3,20;28657;7,90;48326;2,60;505328;2,20;HN 251 6760;; +25028;MARDI;08/04/2025;1;08/07/2025;15;3;48;14;49;1;7;-3-14-15-48-49-;-1-7-;0;0;0;2;4;121704,90;1;4;28444,40;4;26;1363,00;99;468;139,40;275;1091;63,20;175;858;56,50;4439;17230;14,00;4459;21045;12,80;9326;45047;11,10;24329;96020;6,30;76015;336626;5,70;156557;716092;4,30;2;573,00;1;916,90;50;22,90;121;9,40;2174;1,30;2249;3,40;11722;3,00;20045;8,30;37960;2,40;352651;2,30;DX 524 3301;; +25027;VENDREDI;04/04/2025;2;04/07/2025;50;27;39;45;41;7;5;-27-39-41-45-50-;-5-7-;1;1;30117535,00;1;1;684735,20;0;5;32006,70;2;28;1780,20;129;597;153,80;366;1751;55,40;259;1386;49,20;6027;27314;12,40;6342;29436;12,90;12645;60328;11,70;38546;165425;5,10;95497;444405;6,00;190705;886742;4,90;1;1677,90;1;1342,30;62;27,00;170;9,80;2833;1,40;3208;3,50;17866;2,90;37860;6,40;47519;2,90;583594;2,00;CQ 159 4215;; +25026;MARDI;01/04/2025;1;01/07/2025;30;49;46;9;24;4;12;-9-24-30-46-49-;-4-12-;0;0;0;0;2;266728,50;3;9;13853,00;2;9;4314,80;96;393;182,00;218;921;82,10;226;998;53,20;3257;14128;18,80;5218;21031;14,00;12430;51822;10,60;18342;77262;8,60;78746;320432;6,50;199795;810737;4,10;0;0;1;2452,10;51;26,70;124;10,90;1669;2,00;2612;3,40;9489;4,50;16264;12,20;39983;2,80;369561;2,60;OH 642 3223;; +25025;VENDREDI;28/03/2025;8;27/06/2025;10;21;30;45;42;1;9;-10-21-30-42-45-;-1-9-;0;1;250000000,00;5;9;338645,80;9;24;18585,60;36;116;1197,70;675;2040;125,40;1562;4599;58,80;1513;4772;39,80;20673;64252;14,70;28578;88580;11,90;69243;210303;9,30;108134;338662;7,00;420996;1300647;5,70;1051344;3111406;3,80;3;1990,90;10;477,80;308;19,30;692;8,60;9473;1,50;13202;3,00;49866;3,70;83519;10,40;195689;2,50;1696563;2,50;FF 641 9577;; +25024;MARDI;25/03/2025;7;24/06/2025;36;3;18;33;1;6;2;-1-3-18-33-36-;-2-6-;0;0;0;0;2;616553,40;1;8;36024,60;14;69;1300,90;372;1334;123,90;881;3076;56,80;703;2774;44,20;13205;46584;13,10;16027;59429;11,50;34833;132501;9,60;69546;247081;6,20;240339;876893;5,50;521721;1942643;4,00;0;0;6;971,40;183;17,60;407;7,90;5919;1,30;7431;2,90;30788;3,30;52442;9,00;110488;2,40;985309;2,30;DQ 142 2803;; +25023;VENDREDI;21/03/2025;6;20/06/2025;9;17;18;21;50;12;5;-9-17-18-21-50-;-5-12-;0;0;0;1;4;330212,80;5;17;18159,10;18;72;1335,40;392;1407;125,80;803;2983;62,70;913;3328;39,50;11191;41905;15,60;18582;69007;10,60;45023;161655;8,40;57935;214175;7,70;264214;977456;5,30;649044;2319001;3,60;0;0;8;790,90;196;17,90;382;9,20;5340;1,60;8524;2,70;27933;3,90;43278;11,80;122096;2,30;944454;2,70;LI 392 0967;; +25022;MARDI;18/03/2025;5;17/06/2025;46;9;35;4;50;8;11;-4-9-35-46-50-;-8-11-;0;0;0;0;3;332090,30;0;6;38807,40;9;33;2197,70;205;848;157,50;443;1879;75,10;389;1793;55,30;6844;28656;17,30;9377;39406;14,00;21968;91297;11,20;39447;160779;7,70;152825;620751;6,30;370686;1447963;4,30;0;0;4;1138,10;102;24,70;217;11,60;3343;1,80;4459;3,80;18955;4,20;32673;11,30;72159;2,80;736962;2,40;ET 758 3936;; +25021;VENDREDI;14/03/2025;4;13/06/2025;8;33;49;35;10;2;9;-8-10-33-35-49-;-2-9-;0;0;0;2;3;387524,60;2;8;33964,00;11;55;1538,70;257;1126;138,40;645;2642;62,30;621;2540;45,50;10559;41736;13,80;12934;52637;12,20;28353;114586;10,40;59201;232010;6,20;203369;806481;5,60;447185;1755386;4,20;0;0;8;653,90;133;21,80;283;10,20;4724;1,50;6113;3,10;27173;3,30;45994;9,20;94187;2,50;878881;2,40;RO 531 0083;; +25020;MARDI;11/03/2025;3;10/06/2025;36;37;42;47;13;11;7;-13-36-37-42-47-;-7-11-;0;0;0;1;3;302199,80;4;10;21188,70;5;37;1783,70;191;800;151,90;496;2056;62,50;376;1679;53,70;7165;30755;14,60;9028;39147;12,80;19083;82665;11,30;39888;167267;6,70;142894;596277;6,00;301940;1269940;4,50;0;0;3;1303,00;95;22,80;219;9,90;3239;1,60;4077;3,50;18596;3,60;32282;9,80;66164;2,70;675483;2,30;HT 340 0230;; +25019;VENDREDI;07/03/2025;2;06/06/2025;32;6;48;27;10;8;3;-6-10-27-32-48-;-3-8-;0;0;0;2;3;429799,40;3;12;25112,80;26;89;1054,60;418;1507;114,70;983;3784;48,30;874;3081;41,60;14093;53706;11,90;17544;64892;11,00;36382;136202;9,70;74527;286792;5,60;258524;946610;5,30;539716;1986190;4,10;1;3437,90;7;392,90;195;17,60;397;8,60;6320;1,30;8137;2,80;34077;3,10;57595;8,70;120236;2,30;1047190;2,30;NC 896 8360;; +25018;MARDI;04/03/2025;1;03/06/2025;29;42;15;20;35;9;2;-15-20-29-35-42-;-2-9-;0;0;0;2;7;68564,80;1;5;22434,60;6;28;1247,80;106;530;121,40;267;1221;55,70;223;1141;41,90;3909;18059;13,20;4771;22979;11,60;10188;48863;10,10;20874;96708;6,20;73372;344094;5,50;153613;724716;4,20;1;1060,20;3;282,70;48;22,00;136;7,70;1877;1,40;2383;2,90;9962;3,30;16646;9,20;35751;2,40;326024;2,30;HB 180 2007;; +25017;VENDREDI;28/02/2025;4;30/05/2025;43;31;46;16;7;9;5;-7-16-31-43-46-;-5-9-;0;1;52448674,00;0;2;353112,60;0;8;20632,00;4;35;1468,80;162;702;134,90;372;1975;50,60;283;1485;47,30;5656;29005;12,10;6731;33630;11,60;14114;69321;10,50;31772;161746;5,40;102444;490638;5,60;215501;1023433;4,30;0;0;0;0;80;52,00;165;9,00;2565;1,40;3219;3,00;14543;3,20;25403;8,50;48593;2,50;453565;2,30;RD 782 8358;; +25016;MARDI;25/02/2025;3;27/05/2025;23;24;44;17;22;11;6;-17-22-23-24-44-;-6-11-;0;0;0;1;3;167896,00;1;6;19620,00;5;22;1666,60;126;544;124,10;243;1169;61,00;260;1374;36,50;3660;17726;14,10;5632;27761;10,00;13903;69403;7,50;18035;88397;7,10;76685;373133;5,30;193603;929020;3,40;1;1034,70;2;413,80;68;15,20;123;8,40;1785;1,40;2636;2,60;9025;3,60;13102;11,50;36884;2,30;273443;2,70;GT 442 6106;; +25015;VENDREDI;21/02/2025;2;23/05/2025;1;43;3;20;11;11;2;-1-3-11-20-43-;-2-11-;0;0;0;0;3;213502,40;4;10;14969,70;9;32;1457,00;159;745;115,20;367;1584;57,30;391;1752;36,40;4683;21878;14,50;7647;33261;10,60;18450;77813;8,50;23967;113178;7,00;101233;461955;5,40;247414;1088811;3,70;0;0;6;409,70;66;20,60;173;7,80;2257;1,50;3546;2,50;11818;3,60;18456;10,80;48572;2,30;369476;2,60;AC 559 3492;; +25014;MARDI;18/02/2025;1;20/05/2025;26;40;5;25;14;5;7;-5-14-25-26-40-;-5-7-;0;0;0;1;1;469895,30;0;4;27455,50;7;38;900,10;118;596;105,70;432;2036;32,70;224;1171;39,90;6243;28069;8,30;5826;27683;9,40;10513;50884;9,50;29894;133047;4,40;82964;392145;4,70;148346;720876;4,10;0;0;1;1801,00;63;15,80;211;4,70;2862;,80;2716;2,40;13338;2,30;19866;7,30;39096;2,10;314532;2,30;IW 657 0898;; +25013;VENDREDI;14/02/2025;6;16/05/2025;38;14;36;31;4;10;3;-4-14-31-36-38-;-3-10-;0;1;78450739,00;1;2;442087,50;0;5;41329,20;12;45;1430,30;130;770;153,90;322;1735;72,20;278;1803;48,80;5428;28237;15,50;6444;36861;13,30;15140;84809;10,70;31325;158678;6,90;105904;572136;6,00;247653;1322764;4,20;1;1720,00;6;229,30;70;24,50;140;12,20;2619;1,60;3241;3,50;15265;3,50;26628;9,40;51943;2,70;512859;2,40;QN 143 4928;; +25012;MARDI;11/02/2025;5;13/05/2025;21;3;46;36;41;6;1;-3-21-36-41-46-;-1-6-;0;0;0;1;5;113658,30;2;4;33204,70;4;30;1378,90;100;519;146,80;195;1056;76,20;198;1087;52,00;3222;16549;17,10;4129;21756;14,50;9343;51207;11,40;19600;94596;7,50;67784;343915;6,50;153996;805259;4,40;1;1176,30;2;470,50;53;22,10;97;12,10;1497;1,90;2022;3,80;9257;4,00;17228;9,90;32730;2,90;356426;2,30;MT 990 8988;; +25011;VENDREDI;07/02/2025;4;09/05/2025;10;24;45;29;23;8;10;-10-23-24-29-45-;-8-10-;0;0;0;0;0;0;0;6;146483,20;8;48;1080,40;150;673;141,90;342;1634;61,80;342;1570;45,20;5109;22992;15,40;7629;35090;11,20;17560;81279;9,00;25489;115925;7,60;110924;508548;5,50;264731;1199016;3,70;0;0;2;1374,00;63;24,20;162;9,40;2554;1,40;3673;2,70;12872;3,70;18662;11,90;53461;2,30;412347;2,60;BT 538 9173;; +25010;MARDI;04/02/2025;3;06/05/2025;29;43;3;39;4;5;10;-3-4-29-39-43-;-5-10-;0;0;0;0;0;0;0;1;643777,50;6;24;1582,70;89;463;151,10;208;1003;73,70;185;966;53,80;3250;16585;15,60;3983;21383;13,50;9987;49792;10,80;18217;91488;7,10;68671;341234;6,00;164677;792657;4,10;0;0;3;663,00;45;24,50;96;11,50;1618;1,70;2054;3,60;8834;3,90;15429;10,40;33345;2,70;316812;2,50;LY 186 2372;; +25009;VENDREDI;31/01/2025;2;02/05/2025;30;41;49;44;22;7;2;-22-30-41-44-49-;-2-7-;0;0;0;0;1;662510,10;1;3;51613,10;6;28;1722,40;114;529;167,90;364;1514;62,00;225;1165;56,60;6004;25940;12,70;5569;26701;13,70;11462;57187;11,90;38036;154092;5,30;92082;430353;6,00;182640;892502;4,70;0;0;3;864,40;57;25,20;160;9,00;2814;1,20;2725;3,50;17115;2,60;33001;6,30;44586;2,60;483980;2,10;CV 402 0872;; +25008;MARDI;28/01/2025;1;29/04/2025;42;41;37;49;9;9;1;-9-37-41-42-49-;-1-9-;0;0;0;0;1;474163,90;0;1;110819,90;5;20;1725,80;68;334;190,30;189;964;69,70;155;783;60,30;3026;14770;15,90;3400;17824;14,70;7796;40350;12,10;18403;85554;6,90;56553;282713;6,60;127010;631428;4,70;0;0;2;911,30;37;27,30;99;10,20;1421;1,70;1715;3,90;8725;3,60;15230;9,70;28203;2,90;300191;2,40;BE 304 1672;; +25007;VENDREDI;24/01/2025;8;25/04/2025;30;2;19;49;11;3;8;-2-11-19-30-49-;-3-8-;0;1;99397573,00;2;9;92865,40;5;14;13952,60;10;64;950,60;323;1193;93,90;586;2564;46,20;560;2385;34,90;7962;37569;11,00;10765;46224;10,00;22536;97848;8,80;40172;191091;5,40;144174;641405;5,10;296950;1351272;3,90;0;0;4;773,40;138;12,40;260;6,60;3425;1,20;4815;2,30;17756;3,00;28269;8,80;65568;2,10;516928;2,40;PK 962 2825;; +25006;MARDI;21/01/2025;7;22/04/2025;8;6;14;27;41;5;4;-6-8-14-27-41-;-4-5-;0;0;0;2;5;121051,80;1;6;23576,50;7;35;1258,80;204;837;96,90;346;1514;56,60;398;1748;34,40;4368;21547;13,90;7261;35112;9,50;15503;71448;8,70;22380;110536;6,80;99051;482098;4,90;204046;974545;3,90;1;1210,20;3;322,70;95;12,70;163;7,40;2058;1,40;3348;2,40;10475;3,60;16108;10,90;45532;2,10;356098;2,40;KV 091 2370;; +25005;VENDREDI;17/01/2025;6;18/04/2025;24;42;15;35;8;8;6;-8-15-24-35-42-;-6-8-;0;0;0;2;9;85022,70;0;13;13756,90;13;64;870,30;255;1052;97,50;519;2312;46,90;406;2032;37,50;7052;31713;12,00;8980;42987;9,80;17160;84911;9,30;34781;156737;6,10;121067;576078;5,20;242853;1192259;4,00;0;0;6;468,10;109;14,30;239;6,50;3236;1,20;4153;2,50;16145;3,00;24923;9,10;56230;2,20;461750;2,40;VA 724 9628;; +25004;MARDI;14/01/2025;5;15/04/2025;41;48;29;20;18;5;9;-18-20-29-41-48-;-5-9-;0;0;0;1;6;95604,50;0;5;26813,20;2;19;2197,80;92;491;156,60;280;1512;53,70;223;1095;52,10;4387;23134;12,30;5052;25394;12,50;10617;52451;11,30;25287;131241;5,40;78923;391066;5,70;167122;811530;4,40;1;1186,10;2;474,40;44;26,90;127;9,30;2062;1,40;2375;3,30;11520;3,20;19924;8,60;37567;2,60;364877;2,30;PK 626 8865;; +25003;VENDREDI;10/01/2025;4;11/04/2025;36;37;27;42;12;7;6;-12-27-36-37-42-;-6-7-;0;0;0;2;4;179667,00;0;3;55988,10;8;31;1687,60;173;801;120,30;376;1720;59,20;268;1557;45,90;5390;24794;14,40;8257;37007;10,70;13994;69838;10,60;28883;131395;6,80;122042;539324;5,20;215753;1040834;4,30;1;1531,40;1;1225,10;81;18,90;166;9,20;2501;1,50;3833;2,60;13556;3,50;22598;9,80;56673;2,20;507078;2,10;QJ 602 0619;; +25002;MARDI;07/01/2025;3;08/04/2025;41;20;35;33;47;4;12;-20-33-35-41-47-;-4-12-;0;0;0;0;1;521898,30;1;5;24395,20;3;14;2713,70;84;412;169,80;167;924;80,00;218;1026;50,60;2606;13140;19,70;3851;19699;14,70;9645;48369;11,10;14737;71968;9,00;59649;293098;7,00;149467;731851;4,50;0;0;2;1042,40;36;32,10;99;11,60;1399;2,00;1981;3,90;7701;4,70;14201;11,90;30016;3,10;334441;2,50;HE 598 4252;; +25001;VENDREDI;03/01/2025;2;04/04/2025;19;35;37;3;29;1;9;-3-19-29-35-37-;-1-9-;0;0;0;1;2;316853,40;1;6;24684,60;4;29;1590,70;146;608;139,70;388;1591;56,40;322;1418;44,50;5148;22935;13,70;6516;29727;11,80;15093;68729;9,50;27255;119525;6,60;96072;439059;5,60;224141;1008934;3,90;1;1394,60;2;557,80;77;18,10;183;7,60;2373;1,40;3112;3,00;12597;3,40;20128;10,10;46017;2,50;398155;2,50;SI 059 9896;; +24105;MARDI;31/12/2024;1;01/04/2025;33;24;19;26;28;12;8;-19-24-26-28-33-;-8-12-;0;0;0;1;4;188075,10;0;11;15984,00;3;32;1711,40;125;782;129,00;266;1606;66,40;332;1961;38,20;3761;22223;16,80;6214;36088;11,50;16236;90132;8,60;19095;118600;7,90;91289;525874;5,60;238214;1309459;3,60;1;1376,50;2;550,60;54;25,40;159;8,60;2009;1,70;3067;3,00;10221;4,20;16206;12,40;45874;2,40;366456;2,70;OI 368 4788;; +24104;VENDREDI;27/12/2024;3;28/03/2025;45;33;12;27;22;4;8;-12-22-27-33-45-;-4-8-;0;1;41205363,00;1;5;129991,80;1;9;16878,40;10;60;788,50;185;854;102,00;491;2174;42,30;439;1923;33,60;6732;29649;10,90;8229;36455;9,90;18600;80975;8,30;33536;147394;5,50;114649;496697;5,10;253883;1098086;3,70;0;0;7;372,50;81;17,80;228;6,30;2969;1,20;3859;2,50;15260;2,90;23390;9,00;53013;2,20;412119;2,50;KK 662 2144;; +24103;MARDI;24/12/2024;2;25/03/2025;6;24;16;23;10;12;11;-6-10-16-23-24-;-11-12-;0;0;0;1;2;306780,30;3;16;8962,40;7;32;1395,70;182;763;107,80;247;1285;67,60;707;2663;22,90;3456;16824;18,10;6951;30239;11,20;24800;97631;6,50;16984;85694;8,90;91172;409663;5,90;295994;1235083;3,10;0;0;5;501,50;103;13,50;124;11,20;1779;1,90;3427;2,70;9047;4,80;14698;13,80;46207;2,40;341728;2,90;OC 384 9703;; +24102;VENDREDI;20/12/2024;1;21/03/2025;14;10;50;33;21;6;9;-10-14-21-33-50-;-6-9-;0;0;0;0;3;222689,40;2;8;19517,30;12;47;1034,70;158;792;113,10;423;1908;49,60;359;1797;37,00;5729;27621;12,00;7555;35050;10,50;16238;77174;8,90;30960;142307;5,80;109002;496007;5,30;236494;1095328;3,80;0;0;5;549,40;79;19,30;205;7,40;2756;1,30;3693;2,70;14862;3,20;24997;8,90;52653;2,30;457311;2,40;FP 855 3646;; +24101;MARDI;17/12/2024;6;18/03/2025;1;21;4;29;3;7;2;-1-3-4-21-29-;-2-7-;1;1;77557137,00;0;1;603073,30;1;8;17618,50;14;48;914,60;242;923;87,60;554;2094;40,80;431;1749;34,30;7662;30297;9,90;9579;39664;8,40;17216;75380;8,20;37796;152149;4,90;117001;505740;4,70;222671;1002365;3,80;0;0;7;336,70;103;12,70;261;5,00;3438;,90;4415;1,90;16971;2,40;26465;7,20;54578;1,90;397402;2,30;MH 801 5430;; +24100;VENDREDI;13/12/2024;5;14/03/2025;25;1;42;15;50;10;4;-1-15-25-42-50-;-4-10-;0;0;0;0;1;880351,00;5;9;22861,30;13;46;1393,10;202;805;146,60;492;1775;70,30;571;2226;39,30;7843;26101;16,70;10446;37914;12,80;25572;94973;9,50;43352;143323;7,60;157494;556232;6,20;383582;1372521;4,00;0;0;7;670,50;96;27,10;259;10,00;3968;1,60;5264;3,30;21559;3,80;37666;10,10;78548;2,70;746151;2,50;GS 852 9356;; +24099;MARDI;10/12/2024;4;11/03/2025;29;25;5;47;24;9;5;-5-24-25-29-47-;-5-9-;0;0;0;0;1;554372,40;1;3;43188,60;1;30;1345,20;134;596;124,70;398;1816;43,20;248;1118;49,30;5556;27028;10,20;6282;28922;10,60;12658;56637;10,10;28333;136199;5,00;96682;434741;5,00;194905;859369;4,10;0;0;0;0;65;54,00;183;6,80;2586;1,20;2981;2,80;13318;2,90;19349;9,40;46023;2,20;363805;2,50;ES 665 1000;; +24098;VENDREDI;06/12/2024;3;07/03/2025;25;39;47;14;44;6;10;-14-25-39-44-47-;-6-10-;0;0;0;0;0;0;3;11;78150,90;4;39;1300,60;138;608;153,60;235;1415;69,80;335;1479;46,90;4348;20745;16,70;6223;28649;13,50;14702;69227;10,40;24112;112234;7,70;95001;426665;6,40;222339;1025701;4,30;0;0;3;978,10;75;21,70;120;13,50;2247;1,80;3056;3,50;12330;4,10;22032;10,80;47695;2,80;482184;2,40;LT 636 0771;; +24097;MARDI;03/12/2024;2;04/03/2025;46;23;34;25;47;6;8;-23-25-34-46-47-;-6-8-;0;0;0;0;0;0;1;5;129039,70;3;21;1812,80;68;443;158,30;239;1158;64,00;186;967;53,80;3919;17740;14,60;4527;21694;13,30;9470;46965;11,50;22670;100698;6,50;70597;338553;6,00;153855;736296;4,50;0;0;1;2152,20;30;39,80;102;11,70;1911;1,50;2304;3,40;11053;3,40;20097;8,60;35157;2,80;374020;2,30;AE 196 0600;; +24096;VENDREDI;29/11/2024;1;28/02/2025;8;18;43;17;29;5;9;-8-17-18-29-43-;-5-9-;0;0;0;0;4;171105,20;0;6;26660,00;8;46;1083,10;189;936;98,00;479;2459;39,40;465;1883;36,20;6683;32798;10,30;8626;39040;9,70;19660;81499;8,60;33348;164667;5,20;121497;545804;4,90;261622;1123038;3,80;0;0;4;692,50;92;16,70;217;7,00;3090;1,20;4030;2,50;15531;3,10;24221;9,20;57853;2,10;445996;2,50;CJ 789 1845;; +24095;MARDI;26/11/2024;15;25/02/2025;40;25;31;11;7;12;9;-7-11-25-31-40-;-9-12-;0;1;212448937,00;4;15;73659,10;2;14;18445,00;11;55;1462,40;354;1492;99,30;568;2400;65,20;802;3462;31,70;7616;33051;16,60;13598;57385;10,60;35112;144615;7,90;38931;168815;8,20;185250;781193;5,50;474219;1953020;3,50;0;0;5;910,90;150;16,80;267;9,40;3617;1,70;6415;2,60;18620;4,20;30529;12,10;85964;2,40;685309;2,60;WM 610 1340;; +24094;VENDREDI;22/11/2024;14;21/02/2025;12;10;29;11;31;11;8;-10-11-12-29-31-;-8-11-;0;0;0;3;11;151312,40;11;27;14407,60;28;93;1302,80;692;2182;102,20;1192;3916;60,20;1745;5351;30,90;17104;56732;14,60;28994;90710;10,10;71764;217100;7,90;85991;295013;7,00;385625;1246775;5,20;938271;2931777;3,60;2;2366,60;15;252,40;324;14,60;541;8,70;7837;1,50;13013;2,40;39422;3,70;59328;11,60;172961;2,20;1278305;2,60;GV 101 9014;F LWM 35935,F GWD 13213,F PHF 13460,F VZV 83429,F FXV 55986,F GMM 67357,F HSD 57137,F DJF 33286,F CFP 59552,F CGW 16564,F SZF 07542,F KNP 54102,F QLN 57442,F FHZ 79989,F CNP 17928,F PDH 28668,F KZG 04815,F QCD 50969,F VZW 90554,F GRF 26663,F HMR 37863,F QBR 56894,F HFP 80660,F BCR 25193,F FXW 82222,F TXV 15611,F FRK 44523,F QBS 00245,F RQN 82256,N CJK 47213,O LXQ 36780,W NPZ 41244,W DBV 47030,O FTL 28269,H QLJ 06109,T QKP 19167,V QKP 61496,J QKS 64141,M QLB 74455,V QKP 53077,T QLH 36037,J QKN 47246,X QKM 17444,V PJR 45966,Z QLD 76785,T QLB 99719,M QLF 27486,M QKW 06991,M PJQ 65682,J QLG 22716,Z QKX 82893,V QKG 63373,J QLH 15154,X QKZ 16818,V QLD 49200,T QKQ 32242,J QLD 02896,Z QLD 36160,H QKW 78778,R LTH 63481,R LTL 73909,R LTP 84743,R LTL 24947,R LTM 65259,E BCM 03824,E BBX 97858,E BBX 29007,E BCG 03652,E BFC 45227,E BCG 02594,E BBJ 26152,E BCT 01332,E BFJ 65972,E BFS 30806,E BDV 99400,E BFT 45043,E BCR 18727,P JPV 29109,P JVG 27108,P JJN 39681,P JRW 37687,P JPH 07932,P JNJ 24734,P JDZ 32757,P JGJ 36775,P JMM 00998,B WPS 07863,B WQH 59438,B WRB 94521,B WRG 36072,B ZZZ 66811,B WPJ 29140,B WPP 46396,S BCW 88348,S BDN 76665,S BDP 67456,S BDF 83348,L LRV 88951,I RKJ 86198,I RKS 16405; +24093;MARDI;19/11/2024;13;18/02/2025;28;20;49;13;4;7;12;-4-13-20-28-49-;-7-12-;0;0;0;0;2;437600,40;3;11;18595,30;10;54;1179,80;289;1110;105,70;496;2142;57,90;659;2307;37,70;6562;28642;15,20;11600;45762;10,60;25900;105496;8,50;32669;141623;7,70;153207;638935;5,40;342223;1464889;3,70;0;0;4;842,90;130;14,40;243;7,70;3081;1,50;5261;2,30;15419;3,80;24283;11,20;69310;2,20;541899;2,50;HH 153 7574;; +24092;VENDREDI;15/11/2024;12;14/02/2025;7;39;8;44;34;2;6;-7-8-34-39-44-;-2-6-;0;0;0;0;1;1030972,90;2;9;26772,80;15;51;1471,60;158;821;168,30;478;2331;62,60;357;1870;54,90;8960;38936;13,10;9391;45700;12,50;20192;100728;10,50;50428;215347;5,90;160103;723305;5,60;344042;1612654;4,00;0;0;4;977,20;72;30,10;211;10,20;3909;1,30;4306;3,30;21759;3,10;35141;9,00;72861;2,40;642356;2,40;DS 412 3521;; +24091;MARDI;12/11/2024;11;11/02/2025;11;12;50;10;22;10;1;-10-11-12-22-50-;-1-10-;0;0;0;0;3;256306,60;0;12;14975,70;10;41;1365,20;196;910;113,30;397;1827;59,60;533;2292;33,40;5525;24670;15,50;9048;39940;10,60;22955;98242;8,00;26399;119027;8,00;125041;545032;5,50;322931;1394060;3,50;0;0;6;477,20;86;18,40;185;8,50;2509;1,50;3987;2,60;12413;4,00;18180;12,70;56655;2,30;402520;2,80;TN 647 7615;; +24090;VENDREDI;08/11/2024;10;07/02/2025;48;2;35;33;42;1;3;-2-33-35-42-48-;-1-3-;0;0;0;0;3;316791,80;2;12;18509,80;11;39;1773,90;145;811;157,10;385;1816;74,10;375;1838;51,50;5426;27240;17,30;7970;38734;13,60;17317;87064;11,20;32730;157954;7,50;122912;594236;6,30;270606;1340773;4,50;0;0;6;587,90;75;26,10;202;9,70;2593;1,80;3896;3,30;15425;4,00;29558;9,60;57836;2,70;618200;2,30;RS 871 5067;; +24089;MARDI;05/11/2024;9;04/02/2025;50;8;37;1;19;9;6;-1-8-19-37-50-;-6-9-;0;0;0;1;6;119513,00;3;8;20949,10;10;44;1186,30;193;870;110,50;410;1882;54,00;453;1846;38,60;6004;27255;13,10;8113;35363;11,20;18140;80046;9,20;32599;146834;6,10;113765;501357;5,60;254391;1128830;4,00;0;0;6;466,50;84;18,50;170;9,10;2847;1,30;3692;2,80;14785;3,30;25513;8,90;52683;2,40;467139;2,40;HR 461 7160;; +24088;VENDREDI;01/11/2024;8;31/01/2025;44;7;50;33;38;2;7;-7-33-38-44-50-;-2-7-;0;0;0;0;5;152337,60;0;4;44504,70;7;50;1108,90;131;701;145,70;485;2297;47,00;292;1589;47,70;7612;33152;11,40;7349;39237;10,70;14339;77972;10,10;43770;184689;5,10;114310;567133;5,30;213391;1113984;4,30;0;0;5;546,70;61;24,80;226;6,70;3433;1,10;3371;3,00;19101;2,50;31887;6,90;52525;2,30;492047;2,20;AE 224 2371;; +24087;MARDI;29/10/2024;7;28/01/2025;45;30;19;1;2;12;10;-1-2-19-30-45-;-10-12-;0;0;0;1;2;293155,70;0;3;45676,80;4;23;1855,70;84;478;164,40;211;1027;80,90;254;1368;42,60;3152;15437;18,90;4661;22908;14,20;13913;65223;9,20;16212;81020;9,00;70188;342322;6,70;210847;984152;3,70;1;1190,20;3;317,40;38;31,30;91;13,00;1530;1,90;2288;3,40;8093;4,60;12898;13,40;34948;2,80;298332;2,90;MU 135 5274;; +24086;VENDREDI;25/10/2024;6;24/01/2025;4;20;45;25;17;9;8;-4-17-20-25-45-;-8-9-;0;0;0;0;2;402669,50;3;7;26888,70;10;40;1465,60;198;1014;106,50;330;1724;66,20;410;2069;38,70;4927;25930;15,40;9209;45365;9,80;19205;94572;8,80;24866;131896;7,60;124010;630402;5,00;263077;1321965;3,80;0;0;4;692,80;98;15,70;156;9,80;2386;1,60;4253;2,40;11849;4,00;18560;12,10;57058;2,20;454992;2,40;VP 603 1205;; +24085;MARDI;22/10/2024;5;21/01/2025;13;46;3;27;30;1;2;-3-13-27-30-46-;-1-2-;0;0;0;1;2;269163,30;4;9;13979,50;5;24;1632,80;156;705;102,30;226;1155;66,00;363;1695;31,60;3216;16432;16,30;6395;29417;10,10;14859;69726;7,90;16944;85273;7,90;87506;402090;5,20;196822;930745;3,60;1;1121,80;2;448,70;85;13,10;111;10,10;1490;1,80;2955;2,50;8018;4,40;13014;12,50;40539;2,20;324679;2,50;UK 403 4512;; +24084;VENDREDI;18/10/2024;4;17/01/2025;23;39;4;15;30;12;1;-4-15-23-30-39-;-1-12-;0;0;0;0;2;352631,70;0;6;27471,90;5;35;1466,80;140;706;133,90;332;1479;67,50;473;2170;32,30;4399;20021;17,50;6585;30918;12,60;20483;94852;7,60;23330;107220;8,20;92509;432047;6,40;272420;1256893;3,50;0;0;3;932,00;62;24,60;174;8,80;2238;1,70;3197;3,20;12077;4,00;19955;11,20;45500;2,70;391339;2,80;OJ 183 5872;; +24083;MARDI;15/10/2024;3;14/01/2025;15;48;2;36;32;3;9;-2-15-32-36-48-;-3-9-;0;0;0;0;1;506802,60;0;5;23689,60;7;27;1366,40;92;481;141,20;229;1221;58,80;206;1035;48,70;3904;19239;13,10;4079;21401;13,10;8801;45553;11,50;24215;113067;5,60;66251;328382;6,00;142398;692194;4,60;0;0;4;507,10;49;22,50;111;9,90;1906;1,40;2046;3,60;11439;3,00;22592;7,20;32549;2,80;362827;2,20;TF 309 7516;; +24082;VENDREDI;11/10/2024;2;10/01/2025;23;46;32;16;49;5;4;-16-23-32-46-49-;-4-5-;0;0;0;1;3;223088,00;0;6;26069,60;8;30;1624,00;163;738;121,60;320;1369;69,30;303;1437;46,30;4530;20133;16,50;6565;31194;11,90;13896;64209;10,70;25347;112915;7,40;99826;463749;5,60;206566;946037;4,40;0;0;5;567,80;82;19,20;158;9,80;2220;1,70;3113;3,30;12291;3,90;22724;10,00;47820;2,60;500129;2,20;JJ 233 7686;; +24081;MARDI;08/10/2024;1;07/01/2025;7;19;34;14;40;8;6;-7-14-19-34-40-;-6-8-;0;0;0;0;4;122519,20;0;7;16362,70;7;31;1150,80;116;533;123,30;314;1313;52,90;269;1279;38,10;4572;19378;12,50;5650;25616;10,60;12548;56697;8,90;24137;100095;6,10;84914;372862;5,10;186586;821666;3,70;0;0;3;687,50;68;16,80;145;7,90;2122;1,30;2701;2,80;11308;3,10;17947;9,20;40727;2,20;335665;2,40;WM 303 1045;; +24080;VENDREDI;04/10/2024;8;03/01/2025;4;17;8;16;20;6;1;-4-8-16-17-20-;-1-6-;0;1;162256512,00;5;12;94700,00;10;20;13279,70;30;97;852,80;528;1811;84,10;972;3444;46,70;1306;4613;24,50;11722;43110;13,10;18900;68777;9,10;45349;167492;7,00;52545;204256;6,90;230953;870340;5,10;540775;2086207;3,40;3;927,40;16;139,10;221;12,50;434;6,30;5128;1,30;8415;2,10;23718;3,60;36316;11,00;104618;2,10;757309;2,60;NR 671 2640;; +24079;MARDI;01/10/2024;7;31/12/2024;22;41;8;42;29;9;11;-8-22-29-41-42-;-9-11-;0;0;0;1;7;124015,80;2;10;20289,10;8;46;1373,80;185;846;137,60;380;1791;68,70;527;2087;41,40;5804;26227;16,40;9497;40760;11,80;22997;93991;9,50;31735;141713;7,60;139618;587531;5,80;336913;1358844;4,00;1;2060,60;2;824,20;79;25,70;173;11,80;2717;1,80;4307;3,10;14875;4,20;25464;11,60;64203;2,60;572723;2,50;LE 141 0047;; +24078;VENDREDI;27/09/2024;6;27/12/2024;7;21;38;20;27;12;5;-7-20-21-27-38-;-5-12-;0;0;0;1;3;416328,80;7;18;16217,10;17;68;1337,00;415;1478;113,30;795;2773;63,80;1130;3682;33,70;10891;39401;15,70;18595;67092;10,30;48235;164809;7,80;54133;199805;7,80;258915;940455;5,20;649782;2273779;3,40;1;3397,30;7;388,20;187;17,80;378;8,90;5132;1,60;8527;2,60;26564;3,90;40847;11,90;120278;2,30;900632;2,70;AQ 201 0796;; +24077;MARDI;24/09/2024;5;24/12/2024;20;21;49;36;18;5;3;-18-20-21-36-49-;-3-5-;0;0;0;2;4;138661,10;2;2;64814,70;6;25;1615,00;96;535;139,00;324;1429;55,00;255;1096;50,40;4936;22528;12,20;5143;24809;12,40;11230;51717;11,00;28367;129548;5,30;87643;400108;5,40;175531;793593;4,40;1;1219,20;3;325,10;42;29,00;150;8,00;2228;1,30;2439;3,30;12649;3,00;21363;8,20;40884;2,40;377496;2,30;KD 820 4282;; +24076;VENDREDI;20/09/2024;4;20/12/2024;16;37;34;25;29;3;7;-16-25-29-34-37-;-3-7-;0;0;0;1;3;231778,90;1;13;12500,90;12;54;937,30;147;752;123,90;572;2608;37,70;302;1537;45,00;8476;37366;9,20;7628;34527;11,10;14313;68306;10,50;48495;207694;4,10;113129;502378;5,40;208871;994624;4,40;1;1525,20;3;305,00;61;24,20;242;6,20;3766;1,00;3578;2,80;20937;2,20;37192;5,90;52149;2,30;503672;2,10;GK 429 0112;; +24075;MARDI;17/09/2024;3;17/12/2024;30;32;20;41;44;10;1;-20-30-32-41-44-;-1-10-;0;0;0;0;0;0;1;4;159071,50;5;19;1976,00;97;460;150,30;213;917;79,70;215;1158;44,30;3085;13868;18,50;4528;20774;13,70;10445;49650;10,70;16142;73056;8,80;64649;296919;6,80;163134;752833;4,30;0;0;2;1055,90;42;27,90;100;11,60;1536;1,80;2181;3,50;8004;4,50;14555;11,60;31151;3,00;324241;2,60;QG 873 4967;; +24074;VENDREDI;13/09/2024;2;13/12/2024;31;42;10;15;17;4;12;-10-15-17-31-42-;-4-12-;0;0;0;1;3;252503,60;0;5;35408,50;7;23;2397,50;207;749;135,60;438;1516;70,80;638;2144;35,10;6143;21197;17,70;10614;35525;11,80;27791;94256;8,30;31206;111292;8,50;149577;509083;5,80;390054;1330865;3,60;0;0;3;1329,80;97;22,80;229;9,50;3206;1,70;5189;2,80;16316;4,20;26132;12,20;72758;2,40;591003;2,70;TE 497 3637;; +24073;MARDI;10/09/2024;1;10/12/2024;48;46;6;29;47;9;2;-6-29-46-47-48-;-2-9-;0;0;0;0;1;484860,80;2;6;18886,60;3;13;2715,10;77;357;182,10;192;954;72,00;151;811;59,50;3424;15401;15,60;3844;18241;14,70;8378;40170;12,40;20745;93391;6,50;62495;290184;6,50;135782;629021;4,80;0;0;2;998,50;42;26,40;89;12,40;1656;1,60;1964;3,70;9674;3,50;17740;9,00;30927;2,90;351858;2,20;QN 829 8322;; +24072;VENDREDI;06/09/2024;12;06/12/2024;34;47;14;12;41;3;4;-12-14-34-41-47-;-3-4-;0;1;149017789,00;1;4;257427,90;1;7;34380,00;14;55;1362,90;210;1022;135,10;434;2111;69,10;468;2061;49,70;7002;31655;16,20;10530;46435;12,30;21629;96179;11,00;39884;175554;7,30;168968;717046;5,60;337735;1470121;4,40;0;0;10;421,60;90;25,70;184;12,40;3147;1,80;4715;3,20;17890;4,00;30057;11,20;76083;2,50;739666;2,20;GS 272 9213;; +24071;MARDI;03/09/2024;11;03/12/2024;7;9;11;45;16;5;2;-7-9-11-16-45-;-2-5-;0;0;0;0;3;262554,00;2;11;16735,40;16;55;1042,50;238;1014;104,10;714;2788;40,00;600;2309;33,90;8754;37098;10,50;12070;48894;8,90;25955;102946;7,90;41274;178225;5,50;159631;663338;4,60;333091;1378289;3,60;0;0;7;447,10;109;15,80;310;5,50;3640;1,10;5293;2,10;17851;3,00;26491;9,40;70351;2,00;492448;2,50;MS 437 2213;; +24070;VENDREDI;30/08/2024;10;29/11/2024;3;24;42;33;27;6;4;-3-24-27-33-42-;-4-6-;0;0;0;2;5;184395,80;4;14;15391,50;16;48;1398,20;271;1148;107,60;673;2702;48,30;513;2523;36,40;9163;37184;12,30;10823;50222;10,10;23976;111392;8,50;45826;189326;6,10;147363;687522;5,20;330448;1536840;3,80;0;0;6;572,10;114;16,10;292;6,40;4009;1,10;4771;2,60;20136;2,90;31748;8,60;65768;2,30;542203;2,50;IX 551 1819;; +24069;MARDI;27/08/2024;9;26/11/2024;1;47;42;8;11;11;4;-1-8-11-42-47-;-4-11-;0;0;0;0;0;0;1;5;160042,00;6;36;1311,50;134;626;138,90;294;1394;65,90;295;1368;47,20;4534;20589;15,60;6701;29083;12,30;15577;66704;10,00;23087;108715;7,40;100021;444203;5,70;239668;1036127;3,90;0;0;5;500,90;66;21,00;135;10,10;2098;1,60;3053;3,00;10536;4,10;16599;12,00;45861;2,40;372929;2,60;IS 628 8428;; +24068;VENDREDI;23/08/2024;8;22/11/2024;50;47;24;9;15;8;9;-9-15-24-47-50-;-8-9-;0;0;0;1;1;778922,80;1;7;26006,70;5;28;2025,10;139;799;130,70;337;1669;66,10;339;1604;48,30;5112;24219;16,00;7879;37459;11,50;16036;76049;10,50;27535;129475;7,50;122697;568176;5,40;252162;1163902;4,20;0;0;4;744,30;75;21,70;161;10,20;2423;1,60;3653;3,00;12715;4,00;20089;11,80;55533;2,40;498064;2,30;IZ 803 0758;; +24067;MARDI;20/08/2024;7;19/11/2024;10;18;13;26;7;12;3;-7-10-13-18-26-;-3-12-;0;0;0;3;8;71422,40;5;17;7855,30;20;50;831,80;317;1155;66,30;345;1615;50,10;811;2802;20,30;4386;19267;14,70;10370;41344;7,60;25568;98103;6,00;20141;90712;7,80;114373;483099;4,60;280515;1152657;3,10;2;621,40;5;198,80;136;9,00;171;7,20;2153;1,40;4713;1,70;9752;3,90;14090;12,70;52368;1,90;322681;2,70;DV 355 7712;; +24066;VENDREDI;16/08/2024;6;15/11/2024;49;45;15;29;17;10;1;-15-17-29-45-49-;-1-10-;0;0;0;1;4;184603,50;0;5;34515,90;3;29;1853,50;109;663;149,30;278;1352;77,40;316;1744;42,10;3605;20538;17,90;5638;30884;13,20;15472;82155;9,20;19391;106693;8,60;84925;459663;6,30;227238;1208658;3,80;0;0;2;1234,50;40;34,20;129;10,50;1795;1,80;2748;3,30;9796;4,30;16286;12,10;40627;2,70;366466;2,60;EW 999 5827;; +24065;MARDI;13/08/2024;5;12/11/2024;15;16;40;47;39;1;6;-15-16-39-40-47-;-1-6-;0;0;0;1;1;534410,60;0;3;41633,50;6;26;1496,20;82;420;170,60;204;1048;72,20;175;977;54,40;3095;15680;16,90;3879;20234;14,60;9020;47407;11,60;18596;89519;7,40;63435;319528;6,60;146775;756665;4,40;0;0;4;504,00;37;29,40;97;11,40;1475;1,80;1948;3,80;8568;4,00;15916;10,10;30345;3,00;335340;2,40;QR 386 7838;; +24064;VENDREDI;09/08/2024;4;08/11/2024;33;23;25;44;21;4;10;-21-23-25-33-44-;-4-10-;0;0;0;0;4;163953,00;2;6;25545,60;6;33;1446,70;187;844;104,10;329;1481;62,70;409;1836;35,50;4561;21243;15,30;7059;33542;10,80;17935;87750;7,70;24084;108813;7,50;95028;454827;5,60;242121;1148588;3,60;0;0;2;1278,30;80;17,30;161;8,80;2272;1,50;3363;2,70;11716;3,70;18842;10,80;45139;2,50;383535;2,60;LP 008 3446;; +24063;MARDI;06/08/2024;3;05/11/2024;41;27;50;1;18;2;12;-1-18-27-41-50-;-2-12-;0;0;0;2;4;121482,30;4;7;16224,20;4;23;1538,00;96;489;133,20;164;881;78,10;249;1214;39,80;2685;13024;18,50;4383;21234;12,70;11069;51760;9,70;14052;68138;8,90;64234;305973;6,20;161788;759216;4,00;0;0;0;0;53;56,10;89;11,40;1346;1,90;2108;3,30;7170;4,60;12838;11,90;31515;2,70;292735;2,60;NX 605 8090;; +24062;VENDREDI;02/08/2024;2;01/11/2024;12;7;33;5;46;3;12;-5-7-12-33-46-;-3-12-;0;0;0;0;3;204919,70;6;14;10262,80;10;32;1398,50;189;817;100,90;283;1359;64,10;454;1882;32,50;4140;19020;16,00;8327;34379;9,90;19459;79750;7,90;20075;95045;8,10;105823;462879;5,20;253022;1092703;3,50;0;0;5;484,80;94;14,10;132;10,10;2144;1,50;4056;2,20;10465;4,00;15196;12,70;50942;2,10;356162;2,70;PI 666 8268;; +24061;MARDI;30/07/2024;1;29/10/2024;28;37;9;38;25;2;8;-9-25-28-37-38-;-2-8-;0;0;0;0;0;0;0;5;108423,60;8;23;1390,70;96;436;135,10;264;1224;50,80;151;831;52,60;4533;20011;10,90;4373;20354;11,90;9202;44479;10,20;23517;108011;5,00;67488;311867;5,50;144571;671590;4,10;0;0;4;434,30;52;18,50;115;8,20;2079;1,10;2094;3,00;10850;2,70;17873;7,80;32074;2,40;284465;2,40;LO 604 7342;; +24060;VENDREDI;26/07/2024;2;25/10/2024;35;37;23;19;4;8;4;-4-19-23-35-37-;-4-8-;0;1;28649442,00;1;4;152025,20;1;10;14212,30;13;54;819,70;187;902;90,40;427;1966;43,80;334;1741;34,70;5826;27669;10,90;6634;34471;9,80;13972;71010;8,80;29718;140022;5,40;94055;459322;5,20;200224;975900;3,90;1;1262,40;7;144,20;86;14,60;232;5,30;2727;1,10;3051;2,70;13219;2,90;19942;9,10;43950;2,30;358960;2,50;IX 804 7766;; +24059;MARDI;23/07/2024;1;22/10/2024;8;10;16;34;4;8;4;-4-8-10-16-34-;-4-8-;0;0;0;0;3;147222,00;2;7;14746,30;16;76;423,00;160;630;94,00;704;2862;21,80;279;1240;35,40;5911;25377;8,60;6480;28739;8,50;11984;54425;8,30;21387;97862;5,60;80962;359529;4,80;162812;731173;3,80;0;0;4;438,50;70;13,90;308;3,10;2640;,90;3030;2,10;9928;3,00;14568;9,60;38556;2,00;266844;2,60;ET 230 6121;; +24058;VENDREDI;19/07/2024;4;18/10/2024;44;22;35;48;15;6;7;-15-22-35-44-48-;-6-7-;1;2;26181999,00;1;3;240060,30;0;3;56106,00;5;42;1248,20;173;851;113,40;290;1582;64,50;359;1790;40,00;4479;23538;15,20;7517;36053;11,00;13309;71874;10,30;25825;128931;6,90;102969;513956;5,50;189547;1033719;4,40;1;1428,50;2;571,40;83;17,00;132;10,80;2082;1,70;3651;2,50;12000;3,70;21767;9,40;48324;2,40;491593;2,00;FB 529 4041;; +24057;MARDI;16/07/2024;3;15/10/2024;39;35;2;36;32;7;8;-2-32-35-36-39-;-7-8-;0;0;0;0;1;492731,30;2;5;23031,80;3;18;1992,70;76;371;178,00;173;859;81,30;154;795;61,70;2762;13715;17,80;3859;18872;14,50;7417;39280;12,90;17264;80470;7,60;63009;309957;6,20;122088;619066;5,00;0;0;0;0;41;75,00;94;11,20;1368;1,90;1941;3,70;8392;4,00;16908;9,40;30928;2,90;397607;1,90;DJ 920 2664;; +24056;VENDREDI;12/07/2024;2;11/10/2024;12;39;24;25;18;10;8;-12-18-24-25-39-;-8-10-;0;0;0;2;3;211029,30;2;8;18495,30;9;36;1280,10;154;743;114,20;332;1383;64,80;377;1835;34,30;4642;19646;16,00;7564;33345;10,50;19005;82980;7,80;22458;99530;7,90;103086;455333;5,40;258012;1112596;3,60;0;0;4;639,90;82;17,10;161;8,70;2239;1,50;3601;2,60;11341;3,90;17821;11,50;49322;2,30;380627;2,60;DO 972 0751;; +24055;MARDI;09/07/2024;1;08/10/2024;28;15;19;39;6;7;11;-6-15-19-28-39-;-7-11-;0;0;0;0;2;233146,70;2;7;15568,60;9;34;998,30;128;607;103,00;248;1213;54,40;310;1410;32,90;3679;16910;13,70;5986;25585;10,10;13334;57756;8,30;18709;86685;6,70;82314;352051;5,20;179259;792341;3,70;0;0;5;384,30;59;18,00;120;8,70;1930;1,30;2960;2,30;9344;3,50;15060;10,20;40341;2,10;315615;2,40;CC 517 0755;; +24054;VENDREDI;05/07/2024;3;04/10/2024;29;47;31;13;11;11;1;-11-13-29-31-47-;-1-11-;0;1;39132032,00;1;6;109598,20;3;14;10977,80;12;45;1063,70;175;847;104,10;332;1591;58,50;489;2056;31,80;4383;20981;15,60;7226;32900;11,10;19316;83957;8,10;21403;100491;8,10;99839;448978;5,70;264334;1150742;3,60;1;1497,20;7;171,10;82;18,20;172;8,60;2255;1,60;3449;2,80;10833;4,30;16770;12,90;48787;2,50;380840;2,80;PV 199 2903;; +24053;MARDI;02/07/2024;2;01/10/2024;46;7;2;34;35;6;8;-2-7-34-35-46-;-6-8-;0;0;0;0;2;248851,10;0;2;58160,60;3;21;1725,20;69;396;168,50;200;1009;69,90;177;972;51,00;3766;16589;14,90;4339;20357;13,50;9964;46192;11,10;22727;96176;6,40;72110;327200;6,00;163473;735440;4,30;0;0;1;2091,30;47;24,20;96;11,80;1841;1,50;2143;3,50;10658;3,30;19078;8,80;35409;2,60;352280;2,30;FG 761 0262;; +24052;VENDREDI;28/06/2024;1;27/09/2024;10;18;35;16;22;10;1;-10-16-18-22-35-;-1-10-;0;0;0;1;1;653225,30;2;5;30533,90;6;25;1902,10;142;617;141,90;347;1401;66,00;421;1847;35,20;4540;19080;17,00;7709;31109;11,60;19614;82728;8,10;22698;96503;8,40;105774;436567;5,90;272123;1139040;3,60;0;0;5;565,10;67;23,00;167;9,40;2320;1,60;3816;2,70;11754;4,10;17804;12,70;51593;2,40;400547;2,80;FO 577 0795;; +24051;MARDI;25/06/2024;12;24/09/2024;14;16;49;45;37;5;7;-14-16-37-45-49-;-5-7-;0;1;213887390,00;1;8;141110,20;2;15;17589,20;17;55;1494,10;299;1122;134,90;720;2697;59,30;621;2296;48,90;11967;44842;12,50;12947;49896;12,50;26600;103586;11,20;70237;260815;5,40;198955;764555;5,80;400999;1553498;4,60;1;2840,80;9;227,20;144;19,10;306;9,20;5221;1,30;5947;3,10;30255;2,90;56396;7,20;89653;2,50;916666;2,20;EJ 424 5315;; +24050;VENDREDI;21/06/2024;11;20/09/2024;4;3;11;7;17;12;3;-3-4-7-11-17-;-3-12-;0;0;0;7;14;86983,10;17;44;6468,40;15;74;1197,90;874;3055;53,40;853;3238;53,30;1869;6419;18,80;10769;40818;14,80;25738;91280;7,40;58429;204607;6,10;50365;198681;7,60;279392;1030776;4,60;652967;2392296;3,20;4;763,90;5;488,90;388;7,70;399;7,50;5072;1,40;11520;1,70;24114;3,90;35340;12,40;125987;1,90;807307;2,70;UP 370 6361;; +24049;MARDI;18/06/2024;10;17/09/2024;34;11;36;33;3;12;1;-3-11-33-34-36-;-1-12-;0;0;0;1;5;185067,00;2;6;36044,30;9;41;1642,90;192;800;155,10;407;1713;76,50;435;2029;45,40;6427;26555;17,30;8595;36552;14,00;23455;101523;9,40;35582;143991;8,00;133713;553256;6,60;369455;1534807;3,80;0;0;5;793,20;93;23,10;187;11,40;3118;1,70;3961;3,60;16700;4,00;28662;11,00;61919;2,80;568476;2,70;DO 202 2964;; +24048;VENDREDI;14/06/2024;9;13/09/2024;13;2;16;24;32;7;1;-2-13-16-24-32-;-1-7-;0;0;0;1;10;109895,20;3;19;13518,00;28;88;909,00;421;1604;91,80;945;3325;46,80;968;3742;29,20;11825;42911;12,70;16671;64389;9,40;35513;142255;7,90;59259;214237;6,40;217455;848501;5,10;452869;1847351;3,70;1;2613,90;11;190,10;200;12,80;413;6,20;5248;1,20;7349;2,30;26643;3,00;41603;9,00;97583;2,10;764806;2,40;OA 601 5773;; +24047;MARDI;11/06/2024;8;10/09/2024;48;15;7;34;45;7;9;-7-15-34-45-48-;-7-9-;0;0;0;0;5;168413,40;1;10;19680,50;14;56;1094,60;206;945;119,40;560;2360;50,50;361;1867;44,90;8638;34664;12,00;9302;41280;11,30;18712;82326;10,50;47821;187141;5,60;142692;616187;5,30;273912;1209516;4,40;0;0;4;864,90;93;20,60;256;7,40;3905;1,20;4318;2,90;21473;2,70;35458;7,80;64929;2,40;598955;2,30;OA 484 4606;; +24046;VENDREDI;07/06/2024;7;06/09/2024;30;26;15;37;16;8;5;-15-16-26-30-37-;-5-8-;0;0;0;2;9;136127,50;0;5;57267,40;18;79;1128,90;322;1312;125,20;1031;3821;45,40;790;2910;41,90;15001;56573;10,70;16262;60826;11,10;34796;127695;9,90;77437;294555;5,20;237909;885563;5,40;513976;1876170;4,10;1;3295,50;6;376,60;144;22,40;461;7,00;6550;1,20;7295;2,90;34566;2,90;56577;8,30;108329;2,40;957411;2,40;ES 120 1943;; +24045;MARDI;04/06/2024;6;03/09/2024;43;7;6;9;14;4;3;-6-7-9-14-43-;-3-4-;0;0;0;1;2;279488,30;1;7;18663,10;6;26;1565,00;154;647;115,80;352;1608;49,20;356;1443;38,50;4478;20966;13,20;7950;33014;9,40;16201;67722;8,50;21321;100326;6,90;110094;473384;4,60;218517;948932;3,70;1;1219,80;3;325,20;67;18,20;145;8,20;2057;1,40;3707;2,10;9842;3,80;14975;11,70;50444;1,90;358570;2,40;AU 243 4895;; +24044;VENDREDI;31/05/2024;5;30/08/2024;16;33;4;34;7;8;7;-4-7-16-33-34-;-7-8-;0;0;0;0;2;351238,00;3;3;54726,70;7;29;1763,30;167;828;113,70;358;1741;57,10;318;1539;45,40;5271;24339;14,30;9157;41733;9,30;15832;76287;9,50;28080;126555;6,90;131896;591308;4,60;232988;1102388;4,00;0;0;4;680,60;76;19,90;158;9,50;2435;1,50;4131;2,40;13041;3,60;19342;11,20;60028;2,00;475244;2,20;SA 414 4481;; +24043;MARDI;28/05/2024;4;27/08/2024;18;16;35;41;36;6;7;-16-18-35-36-41-;-6-7-;0;0;0;0;2;250693,20;2;12;9765,10;1;15;2433,20;97;513;131,00;184;973;73,00;173;928;53,80;3119;15394;16,20;4281;21209;13,10;8167;42788;12,10;18466;87237;7,20;68672;332149;5,90;130503;668155;4,70;0;0;1;1917,40;49;21,30;92;11,50;1486;1,70;2033;3,40;8663;3,80;16483;9,30;32581;2,60;372484;2,00;UM 326 7642;; +24042;VENDREDI;24/05/2024;3;23/08/2024;12;9;22;18;50;1;3;-9-12-18-22-50-;-1-3-;0;0;0;0;2;321429,70;0;6;25041,10;5;29;1613,70;212;803;107,30;424;1757;51,80;455;1760;36,30;5783;24731;12,90;8924;37089;9,60;19158;79991;8,30;26952;120936;6,60;119936;512888;4,90;257010;1114599;3,60;0;0;2;1251,90;93;14,70;174;7,90;2685;1,20;4091;2,20;12401;3,50;18285;10,90;54552;2,00;389871;2,50;OR 724 4652;; +24041;MARDI;21/05/2024;2;20/08/2024;13;11;48;14;34;9;7;-11-13-14-34-48-;-7-9-;0;0;0;0;1;461188,20;0;5;21557,40;4;33;1017,30;85;447;138,30;283;1264;51,70;167;919;49,90;4884;20234;11,30;4970;22989;11,10;9593;45943;10,30;26662;109565;5,20;79784;362186;5,00;151068;707727;4,10;0;0;2;603,60;33;29,50;130;7,60;2247;1,10;2404;2,70;12506;2,50;18497;7,80;37317;2,20;308461;2,30;HE 712 2807;; +24040;VENDREDI;17/05/2024;1;16/08/2024;31;41;46;32;18;10;1;-18-31-32-41-46-;-1-10-;0;0;0;0;2;306136,50;2;3;47699,40;4;25;1782,80;101;437;187,80;196;1009;86,00;222;1108;55,00;3185;14760;20,60;4573;21945;15,50;11118;54773;11,50;18186;83136;9,20;70868;333262;7,20;180744;853890;4,50;0;0;2;1262,50;51;27,50;93;15,00;1603;2,10;2353;3,90;9147;4,70;17174;11,80;35142;3,20;395702;2,50;HI 545 7650;; +24039;MARDI;14/05/2024;4;13/08/2024;35;2;8;28;17;7;9;-2-8-17-28-35-;-7-9-;1;1;48748900,00;2;5;104539,90;0;7;17451,90;9;43;884,90;196;856;81,80;411;1704;43,40;363;1704;30,50;5580;23530;11,00;7408;32287;8,90;14740;66637;8,10;28854;120656;5,40;95907;429438;4,80;186875;858175;3,80;0;0;3;675,70;96;11,70;201;5,50;2523;1,10;3441;2,10;13190;2,60;21604;7,50;44174;2,00;345249;2,30;LH 133 4150;; +24038;VENDREDI;10/05/2024;3;09/08/2024;29;44;13;48;28;12;4;-13-28-29-44-48-;-4-12-;0;0;0;0;3;213998,10;1;6;25007,40;4;18;2596,40;124;629;136,80;216;1241;73,30;273;1499;42,60;3561;18174;17,50;5857;28133;12,60;15383;72372;9,10;18052;93179;8,60;87571;415055;6,10;229484;1067762;3,80;0;0;2;1206,20;56;23,90;120;11,10;1864;1,70;2917;3,00;9321;4,40;15121;12,80;42222;2,50;355562;2,70;RW 693 7088;; +24037;MARDI;07/05/2024;2;06/08/2024;45;35;42;36;41;11;6;-35-36-41-42-45-;-6-11-;0;0;0;0;0;0;1;3;202575,50;7;21;1707,60;73;346;190,90;181;841;83,00;180;804;61,00;2952;13572;18,00;3512;16490;16,50;8195;38152;13,30;17414;77930;7,90;59286;272183;7,10;137564;627708;4,90;0;0;3;705,60;42;28,00;86;13,60;1504;1,90;1784;4,30;8816;4,10;17284;9,80;29977;3,20;354331;2,30;PJ 884 6301;; +24036;VENDREDI;03/05/2024;1;02/08/2024;6;49;9;30;10;4;3;-6-9-10-30-49-;-3-4-;0;0;0;1;3;211741,00;3;8;18557,70;6;24;1926,70;194;816;104,30;313;1569;57,30;424;1610;39,30;4871;21715;14,50;9083;35764;9,80;18381;75020;8,70;23355;109236;7,20;122479;508536;4,90;240546;1027342;3,90;0;0;3;852,60;87;16,30;157;8,80;2263;1,50;4222;2,20;11030;4,00;17244;11,90;57181;2,00;432283;2,30;RH 550 1796;; +24035;MARDI;30/04/2024;13;30/07/2024;24;47;13;22;33;5;1;-13-22-24-33-47-;-1-5-;1;1;166790050,00;1;8;114649,10;3;13;16489,40;15;68;981,80;329;1385;88,80;597;2517;51,60;788;3144;29,00;7334;33591;13,60;11623;51784;9,80;27729;117948;8,00;36606;165534;6,90;148899;688604;5,20;346843;1549560;3,70;0;0;8;442,40;142;13,80;240;8,00;3229;1,40;5290;2,40;16166;3,70;26516;10,60;66947;2,30;551022;2,50;FQ 403 9499;; +24034;VENDREDI;26/04/2024;12;26/07/2024;47;2;39;20;40;8;4;-2-20-39-40-47-;-4-8-;0;0;0;1;2;560301,50;2;11;23809,30;9;46;1773,40;204;987;152,20;498;2421;65,60;423;2238;49,80;8126;39302;14,20;9226;46215;13,40;20513;103414;11,20;48420;227046;6,10;144966;710644;6,20;325515;1579596;4,50;1;2319,20;0;0;100;40,90;201;11,20;3600;1,50;4252;3,60;21185;3,40;39963;8,30;66150;2,80;701841;2,30;QG 398 5688;; +24033;MARDI;23/04/2024;11;23/07/2024;9;49;11;6;32;2;10;-6-9-11-32-49-;-2-10-;0;0;0;1;4;195277,70;2;8;22819,80;8;34;1672,40;177;800;130,90;401;1759;62,90;424;1927;40,30;6058;25779;15,00;8857;37357;11,60;21371;90637;8,90;31610;135438;7,20;125591;535723;5,70;304180;1305450;3,80;1;1694,90;2;677,90;76;22,00;159;10,20;2792;1,40;3969;2,80;14542;3,60;24154;10,00;56697;2,40;471953;2,50;JX 323 5847;; +24032;VENDREDI;19/04/2024;10;19/07/2024;10;20;46;40;44;1;3;-10-20-40-44-46-;-1-3-;0;0;0;0;1;938778,20;3;11;19946,10;10;39;1752,30;161;918;137,10;416;1995;66,70;401;2069;45,10;5962;28576;16,30;9101;44084;11,80;19483;96956;10,00;34069;160692;7,30;126927;604285;6,10;283118;1364068;4,30;0;0;4;865,20;80;24,00;185;10,20;2670;1,70;4165;3,00;15118;3,90;26406;10,40;57728;2,70;579645;2,30;DC 834 9863;; +24031;MARDI;16/04/2024;9;16/07/2024;39;31;46;29;22;3;7;-22-29-31-39-46-;-3-7-;0;0;0;0;2;337636,50;1;5;31564,40;12;44;1117,20;128;601;150,60;470;1918;49,90;296;1451;46,30;7612;31921;10,50;6795;29654;12,60;13474;61758;11,30;45670;188726;4,40;106324;460357;5,70;208837;940834;4,50;0;0;6;444,50;55;26,90;215;6,70;3257;1,10;3086;3,10;19315;2,30;35979;5,90;47684;2,50;487441;2,10;MX 883 5599;; +24030;VENDREDI;12/04/2024;8;12/07/2024;45;16;12;2;3;11;2;-2-3-12-16-45-;-2-11-;0;0;0;1;2;389492,20;2;8;22757,60;7;37;1532,60;178;847;123,30;377;1862;59,30;418;1841;42,10;5710;26529;14,60;8849;39506;10,90;20721;89196;9,00;29497;138497;7,00;125186;566236;5,40;300704;1318782;3,70;0;0;3;985,10;86;18,80;177;9,10;2790;1,40;4115;2,60;14232;3,50;22051;10,70;58437;2,20;436542;2,60;OU 073 1106;; +24029;MARDI;09/04/2024;7;09/07/2024;27;46;19;23;26;2;10;-19-23-26-27-46-;-2-10-;0;0;0;0;0;0;1;5;143308,70;7;27;1565,90;113;578;134,70;284;1290;63,80;275;1382;41,80;4492;19907;14,50;6592;29808;10,80;15275;69641;8,60;22851;102086;7,10;93444;427979;5,30;218420;1007228;3,60;0;0;3;735,70;58;20,40;136;8,90;2146;1,40;3070;2,60;10772;3,50;16418;10,70;43187;2,30;333463;2,60;AR 193 8181;; +24028;VENDREDI;05/04/2024;6;05/07/2024;26;37;35;18;13;11;8;-13-18-26-35-37-;-8-11-;0;0;0;0;2;364537,40;4;13;13107,40;11;41;1294,40;172;790;123,70;285;1521;67,90;422;1804;40,20;4550;22405;16,20;7366;34693;11,60;18244;81880;9,20;23842;116993;7,80;107490;500313;5,70;268386;1187042;3,90;0;0;7;410,70;69;22,80;151;10,50;2171;1,80;3438;3,00;11671;4,20;18971;12,10;50572;2,50;434232;2,60;IX 755 5449;; +24027;MARDI;02/04/2024;5;02/07/2024;36;1;48;23;31;8;5;-1-23-31-36-48-;-5-8-;0;0;0;0;0;0;0;2;311771,80;2;24;1533,00;99;406;166,90;268;1272;56,30;176;893;56,30;4322;20899;12,00;4415;21138;13,20;9778;45917;11,30;25532;122707;5,10;69599;332177;6,00;151562;711622;4,50;0;0;1;1950,90;39;27,70;114;9,50;1927;1,30;2091;3,40;11244;3,00;20419;7,60;32702;2,70;329918;2,30;KE 998 1888;; +24026;VENDREDI;29/03/2024;4;28/06/2024;49;35;36;17;16;10;1;-16-17-35-36-49-;-1-10-;0;0;0;0;1;687795,50;0;6;26791,50;8;24;2086,20;113;588;156,80;199;1135;85,90;265;1289;53,10;3464;17335;19,70;5293;25345;15,00;12748;63117;11,20;20427;96601;8,90;82216;390707;6,90;212892;1018156;4,20;0;0;6;459,70;70;21,50;110;13,50;1771;2,10;2688;3,70;10381;4,60;18286;12,10;40732;3,00;418300;2,60;NU 277 4690;; +24025;MARDI;26/03/2024;3;25/06/2024;14;26;29;13;2;5;6;-2-13-14-26-29-;-5-6-;0;0;0;1;5;99867,40;2;5;23340,60;9;38;956,50;214;843;79,40;300;1400;50,50;356;1676;29,60;3844;19001;13,00;7663;33176;8,30;15112;65640;7,80;18686;93205;6,70;95958;427543;4,60;189149;850599;3,70;0;0;3;634,90;88;12,00;135;7,60;1771;1,40;3502;2,00;8913;3,60;13329;11,40;43984;1,90;308988;2,40;FM 038 5979;; +24024;VENDREDI;22/03/2024;2;21/06/2024;32;8;44;11;23;9;10;-8-11-23-32-44-;-9-10-;0;0;0;0;12;54013,30;8;28;5410,20;10;37;1275,20;284;1226;70,80;271;1275;72,00;688;2946;21,90;3603;17390;18,50;8470;38488;9,30;21146;93918;7,10;18745;89886;9,00;108420;483588;5,20;267569;1176361;3,50;0;0;8;329,30;144;10,00;162;8,90;1821;1,90;4017;2,40;9570;4,70;15632;13,50;51998;2,30;408166;2,50;KP 843 6737;; +24023;MARDI;19/03/2024;1;18/06/2024;32;18;7;20;16;1;3;-7-16-18-20-32-;-1-3-;0;0;0;3;3;156244,20;2;6;18258,40;8;23;1483,50;142;648;97,00;284;1238;53,60;289;1401;33,30;3937;17254;13,50;6128;26685;9,70;13424;59872;8,00;19564;87492;6,70;83748;365158;5,00;180753;803809;3,70;2;528,00;4;211,20;68;15,50;127;8,30;1829;1,40;2834;2,40;8983;3,60;13791;11,00;38968;2,20;300304;2,50;HZ 468 3439;; +24022;VENDREDI;15/03/2024;7;14/06/2024;31;40;34;4;1;4;5;-1-4-31-34-40-;-4-5-;0;1;130000000,00;1;3;420494,80;1;11;26802,70;14;47;1953,80;248;1053;160,60;932;2762;64,70;569;2273;55,20;10589;40123;15,60;13924;53453;13,10;28280;111082;11,70;58565;220823;7,10;225982;855579;5,80;470067;1770373;4,50;0;0;3;2007,40;107;30,10;420;7,80;4781;1,70;6512;3,30;26812;3,80;44917;10,70;103973;2,60;1016392;2,30;SB 624 1399;; +24021;MARDI;12/03/2024;6;11/06/2024;30;38;19;13;46;4;12;-13-19-30-38-46-;-4-12-;0;0;0;0;2;285832,30;1;4;33401,80;3;20;2080,70;135;561;136,60;227;1026;78,90;331;1325;42,90;3246;15548;18,30;5576;25020;12,60;14074;62934;9,30;17316;81999;8,70;79633;363360;6,20;207173;934872;3,80;0;0;1;2227,50;61;19,30;119;10,20;1592;1,90;2543;3,20;8683;4,40;14229;12,50;37621;2,60;329015;2,70;MF 581 4895;; +24020;VENDREDI;08/03/2024;5;07/06/2024;12;11;8;44;16;4;7;-8-11-12-16-44-;-4-7-;0;0;0;1;1;729847,10;1;9;18953,00;14;48;1106,80;275;981;99,70;674;2566;40,30;427;1867;38,90;9394;37704;9,60;11101;44407;9,10;20967;88672;8,50;44036;182988;4,90;148889;615114;4,60;285741;1244791;3,70;0;0;3;701,30;121;12,50;281;5,50;4001;,90;4902;2,10;19029;2,50;26391;8,50;66146;1,90;451452;2,40;FZ 130 0700;; +24019;MARDI;05/03/2024;4;04/06/2024;23;36;2;17;15;8;3;-2-15-17-23-36-;-3-8-;0;0;0;1;2;267433,80;1;7;17858,20;2;34;1145,10;169;746;96,10;369;1869;40,50;334;1509;35,30;5279;26006;10,20;6925;31466;9,40;13510;63100;8,70;26583;129369;5,10;93030;437488;4,80;188773;879843;3,80;1;1131,20;0;0;80;24,80;166;6,70;2278;1,20;3228;2,30;11821;2,90;18679;8,70;42381;2,10;337562;2,40;EK 216 0386;; +24018;VENDREDI;01/03/2024;3;31/05/2024;34;20;4;19;7;2;4;-4-7-19-20-34-;-2-4-;0;0;0;2;4;169602,10;2;9;17617,20;14;58;851,40;251;1001;90,80;600;2268;42,40;442;1890;35,70;7265;30073;11,20;10559;43849;8,50;19886;84586;8,20;31914;138480;6,10;135975;573714;4,60;269177;1169473;3,60;1;1487,40;7;169,90;104;14,00;260;5,60;3109;1,10;4669;2,10;13878;3,30;19838;10,80;61284;1,90;416488;2,50;EQ 807 5438;; +24017;MARDI;27/02/2024;2;28/05/2024;20;3;4;9;12;5;6;-3-4-9-12-20-;-5-6-;0;0;0;1;8;61868,90;2;10;11567,80;8;37;973,80;300;1053;63,00;397;1701;41,20;705;2348;20,90;4147;19655;12,50;10112;37856;7,20;20834;77533;6,60;18895;92139;6,70;108957;449963;4,30;219530;905443;3,40;0;0;4;485,20;134;7,90;205;5,20;1946;1,30;4540;1,50;8706;3,80;13257;11,70;49047;1,70;307624;2,50;DM 640 1485;; +24016;VENDREDI;23/02/2024;1;24/05/2024;27;49;30;24;28;1;12;-24-27-28-30-49-;-1-12-;0;0;0;0;0;0;0;5;155533,60;5;24;1911,90;103;499;169,30;244;1161;76,90;242;1231;51,00;3839;17444;17,90;5147;24559;14,20;14696;66563;9,70;20278;95080;8,30;79326;371425;6,60;234032;1049508;3,80;0;0;2;1224,40;51;26,60;124;10,70;1979;1,70;2510;3,50;10465;4,00;16844;11,60;38641;2,80;337693;2,90;CN 910 7043;; +24015;MARDI;20/02/2024;6;21/05/2024;42;31;37;48;23;7;3;-23-31-37-42-48-;-3-7-;1;1;73448160,00;0;4;143586,80;0;3;44744,80;13;42;995,40;121;520;148,10;402;1974;41,20;204;1091;52,40;6219;29695;9,60;5247;26063;12,20;10614;53216;11,10;36854;167972;4,20;82264;400597;5,60;156876;793739;4,50;0;0;6;352,20;54;21,70;170;6,80;2641;1,10;2346;3,30;15868;2,30;29269;5,80;37719;2,50;389564;2,10;IK 087 1662;; +24014;VENDREDI;16/02/2024;5;17/05/2024;26;14;13;24;8;1;2;-8-13-14-24-26-;-1-2-;0;0;0;0;2;365544,00;5;13;13143,60;14;54;985,50;270;1118;87,60;407;1891;54,80;602;2500;29,10;5065;24074;15,10;11091;45869;8,80;23734;102828;7,30;24629;118061;7,70;139195;592332;4,80;304231;1341440;3,40;0;0;7;406,30;103;15,30;176;8,70;2295;1,70;4861;2,10;11300;4,30;17130;13,30;62363;2,00;430862;2,60;DC 011 7303;; +24013;MARDI;13/02/2024;4;14/05/2024;13;18;46;17;20;9;4;-13-17-18-20-46-;-4-9-;0;0;0;1;2;269795,00;3;6;21018,50;8;34;1155,30;118;550;131,50;365;1598;47,80;289;1261;42,60;5370;24766;10,80;6180;26919;11,10;13179;58929;9,40;28244;131538;5,10;92320;406778;5,20;203255;898873;3,80;1;1183,10;5;189,20;55;21,50;162;7,20;2401;1,20;2786;2,80;12949;2,80;18974;9,00;42472;2,20;328030;2,50;NQ 226 7262;; +24012;VENDREDI;09/02/2024;3;10/05/2024;37;35;45;24;23;12;9;-23-24-35-37-45-;-9-12-;0;0;0;1;1;688624,90;1;4;40235,70;1;19;2638,40;123;600;153,90;250;1209;80,70;285;1310;52,30;3828;18226;18,80;5712;27565;13,80;14427;67644;10,50;21873;101495;8,50;88159;421884;6,40;223124;1039739;4,20;1;1538,50;0;0;64;42,60;135;11,30;2018;1,80;2881;3,50;11624;4,10;19847;11,10;43801;2,80;435600;2,50;SN 756 9757;; +24011;MARDI;06/02/2024;2;07/05/2024;45;7;2;28;21;11;5;-2-7-21-28-45-;-5-11-;0;0;0;0;2;252419,40;3;6;19664,80;6;30;1225,00;160;699;96,80;284;1327;53,90;383;1599;31,40;3664;18003;13,90;6531;29688;9,40;16196;68863;7,50;18179;89461;7,00;86732;388042;5,10;212014;890199;3,60;0;0;4;516,10;70;16,10;142;8,00;1764;1,60;3046;2,50;8995;3,90;14190;11,60;41002;2,20;310401;2,60;HM 944 5631;; +24010;VENDREDI;02/02/2024;1;03/05/2024;13;42;27;23;20;9;5;-13-20-23-27-42-;-5-9-;0;0;0;0;4;171330,20;3;7;22881,50;25;93;536,40;313;1155;79,50;642;2846;34,10;794;2593;26,30;7751;36285;9,40;10817;45275;8,40;24618;96147;7,30;36790;171481;5,00;136195;587629;4,60;291807;1199431;3,60;0;0;12;220,50;152;10,00;287;5,40;3447;1,10;4961;2,10;16884;2,90;24699;9,30;63106;2,00;453117;2,50;VE 765 3773;; +24009;MARDI;30/01/2024;3;30/04/2024;10;27;5;30;19;6;5;-5-10-19-27-30-;-5-6-;0;2;72274808,00;5;11;82195,80;8;22;9605,20;22;83;793,00;580;1933;62,70;725;3057;41,90;1043;3570;25,20;8447;36427;12,30;18664;68776;7,30;34120;129934;7,10;36743;167646;6,70;204081;806112;4,40;402394;1601176;3,50;3;674,00;11;134,80;229;8,70;319;6,10;3659;1,30;8086;1,60;16374;3,80;24236;12,00;88339;1,80;573976;2,50;SR 540 3822;; +24008;VENDREDI;26/01/2024;2;26/04/2024;32;19;41;8;42;12;9;-8-19-32-41-42-;-9-12-;0;0;0;1;9;146288,80;2;8;38463,80;14;51;1879,30;308;1288;137,00;548;2523;73,90;740;3026;43,30;8461;36883;17,70;13251;57510;12,70;33051;140388;9,70;47411;200543;8,20;200262;846899;6,10;513526;2087605;4,00;1;3136,70;7;358,40;136;22,50;269;11,60;4113;1,80;6171;3,30;23135;4,20;40476;11,10;93188;2,70;876812;2,50;RU 715 8901;; +24007;MARDI;23/01/2024;1;23/04/2024;48;23;14;50;39;12;3;-14-23-39-48-50-;-3-12-;0;0;0;0;1;482958,10;1;2;56437,60;6;18;1953,20;76;451;143,60;190;917;74,60;216;1142;42,10;2686;13501;17,80;4110;21242;12,60;9545;48410;10,30;13905;68707;8,80;62263;315496;6,00;144209;725468;4,20;0;0;2;935,50;35;28,80;105;9,80;1386;1,80;2002;3,40;7334;4,40;12780;11,70;30568;2,70;305461;2,40;KR 537 5327;; +24006;VENDREDI;19/01/2024;1;19/04/2024;50;27;28;48;44;7;12;-27-28-44-48-50-;-7-12-;0;1;17000000,00;2;3;215402,70;3;5;30205,90;4;27;1742,20;93;515;168,20;242;1212;75,50;211;1228;52,40;3770;17992;17,80;5163;26591;13,50;11197;59382;11,20;20666;97560;8,20;87291;414068;6,10;192813;952524;4,30;2;691,40;1;1106,30;45;30,70;129;10,60;1901;1,80;2584;3,50;10643;4,00;17880;11,10;42269;2,60;420119;2,30;QE 911 6397;; +24005;MARDI;16/01/2024;7;16/04/2024;18;21;33;45;10;12;8;-10-18-21-33-45-;-8-12-;1;1;87952574,00;1;3;201336,70;3;22;6416,60;3;30;1465,60;194;858;94,40;255;1203;71,10;438;2054;29,20;3444;16758;17,90;6341;30461;11,00;16295;76506;8,10;17551;87790;8,60;84567;410612;5,80;220434;1035293;3,70;0;0;0;0;83;41,20;130;9,40;1641;1,80;2896;2,80;8509;4,40;13738;12,70;39421;2,50;324690;2,70;CU 208 5890;; +24004;VENDREDI;12/01/2024;6;12/04/2024;18;16;17;49;45;9;12;-16-17-18-45-49-;-9-12-;0;0;0;1;3;281236,60;2;10;19718,80;11;41;1498,00;112;674;167,80;220;1434;83,40;261;1682;49,90;4055;22939;18,30;5828;33053;14,10;14593;78911;11,00;21925;125191;8,40;96155;531557;6,20;241543;1282524;4,10;1;1568,60;7;179,20;53;29,50;114;13,70;2029;1,80;2780;3,70;11287;4,30;18145;12,40;46122;2,70;422529;2,60;RT 208 4085;; +24003;MARDI;09/01/2024;5;09/04/2024;12;9;2;39;40;3;1;-2-9-12-39-40-;-1-3-;0;0;0;0;1;563511,30;2;7;18814,50;1;19;2159,00;113;572;132,10;269;1256;63,60;286;1323;42,40;4002;19412;14,40;5995;27088;11,50;13014;60687;9,60;20832;100262;7,00;88648;407111;5,40;194785;904413;3,90;0;0;1;2091,80;57;20,00;106;10,60;1869;1,50;2806;2,70;9520;3,80;15519;10,80;40960;2,30;335397;2,40;LN 958 0675;; +24002;VENDREDI;05/01/2024;4;05/04/2024;18;4;39;7;50;8;3;-4-7-18-39-50-;-3-8-;0;0;0;0;2;359048,50;1;7;23975,80;11;59;886,00;146;749;128,50;395;2001;50,80;319;1713;41,70;6421;32990;10,80;7652;35938;11,10;16974;77956;9,50;34592;166715;5,30;116584;538106;5,20;247530;1133782;4,00;0;0;4;697,50;60;25,00;183;8,40;2919;1,30;3509;2,90;15479;3,10;26097;8,50;53602;2,30;470400;2,30;SA 454 2143;; +24001;MARDI;02/01/2024;3;02/04/2024;7;18;49;46;15;12;10;-7-15-18-46-49-;-10-12-;0;0;0;0;0;0;0;5;123090,60;2;6;6052,50;76;404;165,50;186;822;86,00;260;1142;43,50;2762;12774;19,40;4251;19434;14,20;12122;54361;9,40;14574;67547;9,20;64733;286826;6,80;191135;841787;3,70;0;0;1;1968,40;37;29,50;98;10,90;1361;1,90;2113;3,40;7176;4,70;11833;13,30;31882;2,80;270726;2,90;VM 156 4461;; +23104;VENDREDI;29/12/2023;2;29/03/2024;2;36;19;37;3;9;6;-2-3-19-36-37-;-6-9-;0;0;0;0;3;235723,30;0;5;33055,40;14;38;1354,70;116;622;152,40;327;1684;59,50;236;1301;54,10;5616;27202;12,90;6144;30489;12,80;13203;66465;11,00;32539;150297;5,80;107439;494759;5,60;227954;1068091;4,20;0;0;8;344,70;62;24,70;161;9,50;2675;1,40;2940;3,40;15054;3,10;24451;9,00;50781;2,40;454579;2,40;JK 572 5557;; +23103;MARDI;26/12/2023;1;26/03/2024;47;27;8;30;35;9;10;-8-27-30-35-47-;-9-10-;0;0;0;0;0;0;0;0;0;3;18;32617,80;82;419;143,80;163;727;87,60;179;899;49,70;2303;11216;19,90;4138;19378;12,80;9697;44899;10,30;12833;60460;9,30;64655;294756;6,00;152666;693402;4,10;0;0;0;0;47;59,00;88;11,50;1180;2,10;1949;3,40;6569;4,80;10761;13,60;31148;2,60;291732;2,50;RK 918 9326;; +23102;VENDREDI;22/12/2023;4;22/03/2024;14;49;34;44;6;12;4;-6-14-34-44-49-;-4-12-;0;1;55662868,00;2;7;113308,60;1;5;37074,90;8;54;1069,20;136;698;152,30;259;1546;72,70;369;1739;45,40;4028;20788;19,00;6953;33434;13,10;16608;78958;10,30;22785;112843;8,80;100888;475534;6,50;255423;1187664;4,20;0;0;4;779,90;61;27,50;137;12,30;2078;2,00;3419;3,30;11841;4,50;20062;12,40;49508;2,80;473364;2,60;MS 622 2438;; +23101;MARDI;19/12/2023;3;19/03/2024;20;43;41;45;10;12;2;-10-20-41-43-45-;-2-12-;0;0;0;1;1;529973,70;0;7;17694,70;3;13;2967,70;79;396;179,40;187;915;82,10;216;1068;49,40;2868;13514;19,50;4116;19855;14,80;10261;48406;11,30;16015;73827;8,90;66465;308486;6,70;165057;763848;4,40;1;1175,50;2;470,20;44;26,70;95;11,90;1435;2,00;2022;3,80;8269;4,40;14192;11,90;32757;2,90;332498;2,50;TN 553 0599;; +23100;VENDREDI;15/12/2023;2;15/03/2024;2;13;37;38;48;9;5;-2-13-37-38-48-;-5-9-;0;0;0;0;1;695067,70;1;5;32489,70;7;35;1445,60;151;689;135,20;334;1751;56,20;294;1327;52,10;5653;27167;12,70;6437;29721;12,90;14846;64665;11,10;34064;158926;5,40;106650;470477;5,80;234048;991215;4,40;0;0;4;598,30;75;22,10;158;10,40;2700;1,50;3215;3,40;15732;3,20;28362;8,40;51466;2,60;502254;2,30;CL 667 1862;; +23099;MARDI;12/12/2023;1;12/03/2024;43;39;28;37;6;9;12;-6-28-37-39-43-;-9-12-;0;0;0;0;1;523847,30;1;2;61215,80;4;21;1815,90;100;444;158,20;193;813;91,30;280;1099;47,40;2781;12674;20,50;4546;20231;14,30;12449;50775;10,60;16384;70525;9,30;71372;304485;6,70;185652;760714;4,30;0;0;1;2357,10;45;29,10;103;12,70;1500;2,10;2291;3,70;8636;4,70;15011;12,50;35979;2,90;364465;2,50;DE 599 0522;; +23098;VENDREDI;08/12/2023;3;08/03/2024;48;30;50;42;17;4;8;-17-30-42-48-50-;-4-8-;0;1;240000000,00;1;5;1584039,10;3;16;24836,40;29;89;1390,70;433;1496;152,40;1147;3751;64,20;1067;3496;48,40;18568;59915;14,10;22280;72556;13,00;49811;163113;10,70;107577;342084;6,20;341513;1100646;6,00;773981;2482059;4,30;1;5260,50;13;323,70;221;23,80;547;9,40;8195;1,50;10175;3,40;47320;3,40;83825;9,00;154150;2,70;1557384;2,40;FK 500 2787;; +23097;MARDI;05/12/2023;2;05/03/2024;6;20;4;24;25;5;9;-4-6-20-24-25-;-5-9-;0;0;0;3;8;146494,60;6;16;17119,10;22;88;969,40;447;1517;103,50;994;3609;46,00;996;3338;34,90;13532;50374;11,50;18586;64967;10,00;41053;139986;8,60;67769;260163;5,60;252088;906029;5,10;554735;1940512;3,80;1;3054,30;10;244,30;196;15,30;410;7,30;5799;1,30;8096;2,40;29470;3,20;46094;9,50;111451;2,20;866454;2,50;NZ 837 2820;; +23096;VENDREDI;01/12/2023;1;01/03/2024;38;10;4;14;50;9;12;-4-10-14-38-50-;-9-12-;0;0;0;2;6;266632,90;6;18;20772,10;14;58;2007,90;392;1609;133,30;757;3072;73,80;952;3942;40,40;11761;45380;17,50;18562;71488;12,40;45754;174658;9,40;63049;241471;8,30;280126;1055902;5,90;693640;2566667;3,90;2;2075,10;7;474,30;175;23,10;347;11,80;5441;1,80;8615;3,10;29536;4,30;49376;12,00;127619;2,60;1137532;2,60;EI 943 3492;; +23095;MARDI;28/11/2023;7;27/02/2024;27;44;12;33;16;7;8;-12-16-27-33-44-;-7-8-;0;1;83068817,00;2;4;145719,30;0;6;22704,60;13;45;942,90;182;846;92,30;334;1500;55,00;337;1675;34,60;4323;20184;14,30;8034;36297;8,90;14635;67580;8,90;22513;103722;7,00;110729;494025;4,60;199936;940100;3,90;2;598,10;4;239,20;67;17,50;148;8,00;1911;1,50;3422;2,30;10242;3,60;15982;10,70;49303;1,90;374723;2,20;FA 161 9226;; +23094;VENDREDI;24/11/2023;6;23/02/2024;48;15;29;20;39;7;1;-15-20-29-39-48-;-1-7-;0;0;0;0;3;243253,90;0;8;21319,60;10;39;1362,10;137;704;139,00;329;1584;65,30;299;1635;44,40;5610;24643;14,70;6393;31928;12,60;13974;72282;10,40;32448;137550;6,60;102570;488145;5,80;214738;1078084;4,30;0;0;5;528,80;67;21,60;158;9,20;2507;1,40;2897;3,30;14480;3,10;25888;8,20;46058;2,60;462114;2,20;OI 764 7249;; +23093;MARDI;21/11/2023;5;20/02/2024;46;47;29;34;19;2;3;-19-29-34-46-47-;-2-3-;0;0;0;1;3;175300,00;1;5;24582,30;6;25;1531,30;74;436;161,70;192;1001;74,40;173;970;54,00;3067;14690;17,80;4284;21592;13,50;9000;45902;11,80;18601;90678;7,20;74806;352623;5,80;146621;721492;4,60;0;0;2;974,50;34;31,80;83;12,50;1405;1,90;1966;3,60;8678;3,80;16094;9,70;34177;2,50;362926;2,10;AG 692 9478;; +23092;VENDREDI;17/11/2023;4;16/02/2024;26;50;24;2;46;2;7;-2-24-26-46-50-;-2-7-;0;0;0;1;4;171009,90;0;2;79935,60;9;33;1508,90;117;646;141,90;424;1857;52,20;258;1279;53,20;7111;29115;11,70;6288;30178;12,50;12507;61288;11,50;42587;167340;5,10;103711;473573;5,70;194794;942535;4,60;0;0;5;525,10;59;24,70;205;7,00;3181;1,10;2976;3,20;18799;2,40;32000;6,60;47871;2,40;481061;2,10;RY 968 0146;; +23091;MARDI;14/11/2023;3;13/02/2024;13;50;36;16;44;3;5;-13-16-36-44-50-;-3-5-;0;0;0;0;1;486731,50;2;4;28439,20;8;30;1181,00;80;477;136,80;245;1201;57,40;188;971;49,90;3883;20003;12,10;4298;21924;12,30;8937;45060;11,10;23907;115461;5,20;68980;348491;5,50;142164;693633;4,40;0;0;3;617,90;27;36,70;118;8,60;1802;1,40;2094;3,20;10663;3,00;19176;7,70;32562;2,50;329596;2,20;IZ 946 0638;; +23090;VENDREDI;10/11/2023;2;09/02/2024;30;42;10;21;38;12;2;-10-21-30-38-42-;-2-12-;0;0;0;0;2;324751,70;2;10;15179,90;5;26;1818,50;166;716;121,60;269;1261;73,00;437;1800;35,90;3611;17183;18,80;7043;30440;11,80;17708;75435;8,90;19464;91145;8,90;95376;431046;5,90;241855;1075258;3,80;0;0;1;2556,20;72;19,70;136;10,40;1833;1,90;3351;2,80;9906;4,40;16473;12,40;45407;2,50;384492;2,60;KA 106 4774;; +23089;MARDI;07/11/2023;1;06/02/2024;8;10;11;39;30;10;4;-8-10-11-30-39-;-4-10-;0;0;0;0;0;0;1;3;189164,10;8;29;1154,60;107;536;115,00;220;1001;65,10;296;1340;34,10;3427;14771;15,50;5409;23231;11,00;13007;57012;8,30;17418;76374;7,50;74500;331651;5,40;181616;808515;3,60;0;0;2;894,10;53;18,30;114;8,60;1642;1,40;2564;2,50;8444;3,60;12694;11,30;35352;2,30;258784;2,70;ST 445 2708;; +23088;VENDREDI;03/11/2023;2;02/02/2024;31;47;21;39;8;9;5;-8-21-31-39-47-;-5-9-;0;1;31218086,00;0;6;123697,30;1;4;43365,10;6;53;1019,40;153;823;120,90;343;2208;47,60;275;1628;45,40;5158;31335;11,70;6463;35295;11,60;13710;74644;10,20;29376;170549;5,40;95499;522472;5,60;204137;1093856;4,30;0;0;1;2524,00;77;17,50;132;10,60;2266;1,50;2976;3,10;12908;3,30;23084;8,80;44336;2,50;427975;2,30;IF 499 9158;; +23087;MARDI;31/10/2023;1;30/01/2024;20;40;5;50;7;10;2;-5-7-20-40-50-;-2-10-;0;0;0;0;5;96734,80;0;3;37680,80;4;21;1676,60;106;577;112,40;270;1184;57,90;287;1402;34,30;3625;15789;15,20;5342;25925;10,30;12430;59923;8,30;18562;82503;7,30;73964;339907;5,60;174136;805649;3,80;0;0;2;951,50;45;23,40;127;8,20;1808;1,40;2555;2,70;9283;3,50;14670;10,40;35744;2,40;294519;2,50;OI 825 1787;; +23086;VENDREDI;27/10/2023;1;26/01/2024;29;48;33;49;35;8;3;-29-33-35-48-49-;-3-8-;0;1;17000000,00;0;2;322693,90;1;2;75418,80;5;25;1879,20;110;524;165,10;291;1391;65,70;205;1116;57,60;4795;23256;13,80;5161;24878;14,40;11004;53753;12,40;29068;139631;5,70;85036;405387;6,20;180060;850884;4,80;0;0;2;1302,90;52;27,30;137;10,40;2221;1,60;2566;3,70;13294;3,40;26125;8,00;40730;2,90;475913;2,10;MY 351 7471;; +23085;MARDI;24/10/2023;2;23/01/2024;18;16;8;31;34;6;9;-8-16-18-31-34-;-6-9-;0;1;26274769,00;0;1;484142,90;1;7;16164,50;10;41;859,60;141;608;106,70;331;1425;48,10;311;1235;39,00;4141;19526;12,30;5597;25009;10,70;12012;54674;9,10;22192;103331;5,80;79806;354127;5,30;171211;774426;3,90;0;0;5;385,00;68;15,70;154;6,80;1919;1,30;2593;2,70;10257;3,20;17165;9,00;36833;2,30;320028;2,40;GB 497 3419;; +23084;VENDREDI;20/10/2023;1;19/01/2024;20;40;28;2;45;5;1;-2-20-28-40-45-;-1-5-;0;0;0;0;0;0;2;7;114759,50;6;33;1436,30;136;613;142,40;343;1377;67,00;334;1379;47,00;5092;21173;15,30;6710;29067;12,40;15821;64755;10,40;28195;116541;7,00;105018;436996;5,80;237660;960647;4,30;0;0;1;2888,90;63;25,40;173;9,20;2338;1,70;3233;3,20;12834;3,90;22590;10,20;49709;2,60;469685;2,40;VU 044 6875;; +23083;MARDI;17/10/2023;5;16/01/2024;40;20;10;35;17;4;3;-10-17-20-35-40-;-3-4-;0;1;67318140,00;2;4;139123,90;4;12;10838,50;3;26;1558,10;149;724;103,00;326;1458;54,10;339;1523;36,30;3944;18681;14,80;7321;32752;9,40;14665;68098;8,40;20528;95136;7,30;96367;428190;5,10;192676;866503;4,00;1;1233,80;2;493,50;66;18,40;155;7,90;1850;1,60;3279;2,50;9448;4,00;15130;11,70;43909;2,20;380582;2,30;JX 489 7822;; +23082;VENDREDI;13/10/2023;4;12/01/2024;41;26;21;40;28;4;2;-21-26-28-40-41-;-2-4-;0;0;0;0;1;867786,70;3;12;16901,30;6;32;1974,10;222;748;155,50;505;1813;67,80;444;1621;53,30;8270;28571;15,10;10767;37127;12,90;22998;80354;11,10;47829;158565;6,80;175811;590399;5,80;375500;1274707;4,30;0;0;2;2403,20;114;23,40;250;10,40;3886;1,70;5250;3,30;22132;3,70;38753;9,90;83565;2,60;806745;2,30;KA 345 1524;; +23081;MARDI;10/10/2023;3;09/01/2024;18;33;20;22;43;3;9;-18-20-22-33-43-;-3-9-;0;0;0;0;1;515171,20;0;6;20067,30;7;37;1013,50;150;574;120,30;333;1564;46,60;262;1231;41,60;5169;23158;11,00;5773;25995;11,00;12772;55643;9,50;28075;122739;5,20;84520;381729;5,30;178217;788883;4,10;0;0;1;2109,40;61;19,20;155;7,40;2371;1,20;2628;2,90;13089;2,80;21547;7,80;39751;2,40;354369;2,30;JP 503 1005;; +23080;VENDREDI;06/10/2023;2;05/01/2024;31;43;21;29;34;2;9;-21-29-31-34-43-;-2-9-;0;0;0;0;1;687153,20;1;5;32119,80;9;39;1282,60;155;623;147,90;364;1607;60,60;313;1347;50,80;5752;24918;13,70;7196;30408;12,50;15384;66296;10,70;31964;136495;6,30;109181;465860;5,80;236705;1002138;4,30;0;0;6;504,20;75;21,50;160;10,40;2727;1,50;3457;3,20;14875;3,50;26176;9,30;52455;2,60;520209;2,30;QZ 466 3294;; +23079;MARDI;03/10/2023;1;02/01/2024;45;6;22;24;20;5;4;-6-20-22-24-45-;-4-5-;0;0;0;2;4;124708,10;1;8;14573,10;2;24;1513,00;113;509;131,40;282;1258;56,20;241;1067;46,50;3959;17205;14,40;6018;25810;10,70;12465;52762;9,70;20428;90431;6,90;88331;379813;5,10;182764;770818;4,10;0;0;2;1058,60;56;20,60;130;9,00;1838;1,50;2856;2,70;9697;3,70;14962;11,30;41866;2,20;343321;2,40;MV 249 2574;; +23078;VENDREDI;29/09/2023;4;29/12/2023;32;9;11;21;13;7;2;-9-11-13-21-32-;-2-7-;1;1;130000000,00;3;16;81127,90;1;25;12135,00;42;147;642,80;531;2294;75,80;1657;5675;32,40;1027;4422;29,20;20437;69584;9,20;24393;90926;7,90;46428;177627;7,50;92300;321029;5,00;306346;1144130;4,40;590325;2286033;3,60;2;1621,00;17;152,50;236;13,70;705;4,50;8824;,90;10676;2,00;40598;2,40;58302;8,00;136596;1,90;938343;2,40;TT 262 4377;; +23077;MARDI;26/09/2023;3;26/12/2023;14;6;23;2;19;5;7;-2-6-14-19-23-;-5-7-;0;0;0;2;6;87504,60;4;14;8764,80;24;75;509,60;274;1010;69,70;692;2645;28,10;518;1992;26,20;7867;31767;8,20;9600;38632;7,50;17664;72530;7,40;34460;146389;4,40;113319;480181;4,30;210806;918463;3,60;1;1158,80;7;132,40;136;8,40;309;3,60;3303;,80;4337;1,70;15164;2,30;21784;7,60;51691;1,80;345657;2,40;OB 498 5779;; +23076;VENDREDI;22/09/2023;2;22/12/2023;3;23;35;34;24;8;5;-3-23-24-34-35-;-5-8-;0;0;0;0;1;681329,00;1;5;31847,50;8;35;1417,10;113;643;142,00;391;2025;47,60;279;1346;50,40;7247;33739;10,00;6577;31719;11,90;14610;67383;10,40;40678;178760;4,70;111667;507911;5,20;248053;1067407;4,00;0;0;2;1451,10;57;28,20;161;9,90;3185;1,20;3162;3,30;17983;2,80;28983;8,00;52367;2,50;469595;2,40;FX 396 8800;; +23075;MARDI;19/09/2023;1;19/12/2023;10;31;41;15;42;2;5;-10-15-31-41-42-;-2-5-;0;0;0;1;1;473970,00;2;2;55387,30;2;22;1568,30;67;433;146,70;249;1158;58,00;164;883;53,40;3968;18272;12,90;4182;21182;12,40;8994;42564;11,50;22800;103564;5,70;75833;338329;5,50;148736;674676;4,40;0;0;1;1896,60;30;35,10;100;10,40;1782;1,40;1977;3,50;9976;3,20;17095;8,90;35294;2,40;321104;2,30;TL 095 8812;; +23074;VENDREDI;15/09/2023;4;15/12/2023;21;45;12;48;14;8;11;-12-14-21-45-48-;-8-11-;0;2;27475869,00;1;3;264520,20;1;8;23183,50;8;26;2221,80;147;728;146,10;319;1599;70,30;367;1650;47,90;4471;23985;16,40;7299;35296;12,40;18362;83822;9,70;24131;128333;7,70;110254;537055;5,80;274115;1272189;3,90;1;1624,40;3;433,10;67;23,20;173;9,30;2247;1,70;3368;3,20;11890;4,20;20123;11,60;52160;2,50;444605;2,60;SW 459 1895;; +23073;MARDI;12/09/2023;3;12/12/2023;14;5;42;36;40;11;2;-5-14-36-40-42-;-2-11-;0;0;0;1;2;258198,50;1;3;40230,10;5;21;1790,10;94;440;157,30;217;1025;71,40;179;999;51,40;3380;15637;16,40;4209;20855;13,70;9922;48569;10,90;18612;86644;7,40;67524;322572;6,30;159781;751416;4,30;0;0;1;2051,50;34;33,50;103;11,00;1681;1,60;2051;3,60;9213;3,80;15901;10,30;32674;2,80;322540;2,50;HF 956 4344;; +23072;VENDREDI;08/09/2023;2;08/12/2023;33;20;10;21;26;4;3;-10-20-21-26-33-;-3-4-;0;0;0;1;4;167780,70;2;5;31370,40;11;36;1357,10;174;779;115,50;375;1688;56,30;360;1548;43,10;5200;23538;14,20;9432;39063;9,50;18393;77154;8,90;26263;117661;7,10;132570;555775;4,70;260748;1102069;3,80;1;1562,70;3;416,70;68;22,90;167;9,30;2344;1,60;4220;2,40;11918;4,00;18573;12,10;60387;2,10;464471;2,40;SE 101 9319;; +23071;MARDI;05/09/2023;1;05/12/2023;41;48;7;1;24;10;12;-1-7-24-41-48-;-10-12-;0;0;0;0;3;162494,70;2;9;12659,20;4;21;1689,80;86;370;176,60;163;809;85,40;211;1057;45,90;2542;12234;19,80;3984;18242;14,80;12057;53554;9,40;13823;65178;9,30;60488;275963;6,90;186614;825123;3,70;0;0;3;660,50;48;22,40;75;14,60;1307;2,00;2044;3,50;7156;4,70;11782;13,40;30882;2,90;271695;2,90;IF 603 7505;; +23070;VENDREDI;01/09/2023;9;01/12/2023;35;43;37;5;4;5;6;-4-5-35-37-43-;-5-6-;1;1;109268140,00;2;2;470297,30;0;7;31404,60;4;34;2013,80;162;729;173,00;379;1724;77,30;346;1625;57,60;6711;29522;15,80;9262;39276;13,30;18964;80385;12,10;39496;167390;7,00;158392;640740;5,70;318837;1302539;4,50;1;2297,50;3;612,60;76;30,20;191;11,80;3139;1,80;4415;3,40;18296;3,90;29528;11,20;72927;2,50;715488;2,30;RZ 000 8026;; +23069;MARDI;29/08/2023;8;28/11/2023;21;40;23;32;49;11;8;-21-23-32-40-49-;-8-11-;0;0;0;1;1;636120,60;2;6;24778,60;7;37;1251,50;144;618;138,00;300;1349;66,80;299;1398;45,30;4505;19845;15,90;6369;28320;12,40;14859;65585;10,00;24367;104102;7,60;98029;422460;5,90;230361;965572;4,10;0;0;5;554,50;62;24,40;140;10,90;2097;1,80;2952;3,40;11456;4,10;19850;11,10;44981;2,70;438072;2,50;GM 852 9393;; +23068;VENDREDI;25/08/2023;7;24/11/2023;44;35;16;34;17;5;10;-16-17-34-35-44-;-5-10-;0;0;0;0;3;262115,90;2;6;30630,40;6;37;1547,10;157;756;139,40;411;1628;68,40;348;1635;47,90;5596;25593;15,30;7756;35610;12,20;17728;79034;10,20;32120;138950;7,00;120470;526240;5,80;278575;1197301;4,10;0;0;2;1102,90;86;21,30;196;9,30;2656;1,70;3616;3,30;15149;3,70;25904;10,20;55587;2,70;529845;2,50;WK 502 1672;; +23067;MARDI;22/08/2023;6;21/11/2023;23;9;40;2;32;6;7;-2-9-23-32-40-;-6-7-;0;0;0;0;7;82102,90;0;5;26864,30;8;35;1195,30;162;808;95,30;319;1472;55,30;303;1625;35,20;4613;20111;14,20;7447;31518;10,10;13792;64403;9,20;25720;109273;6,50;107178;447873;5,00;195747;885697;4,10;0;0;5;468,00;71;18,30;138;9,20;2076;1,50;3389;2,50;11937;3,30;19952;9,40;49192;2,10;430782;2,10;JT 636 1851;; +23066;VENDREDI;18/08/2023;5;17/11/2023;47;25;23;30;44;9;12;-23-25-30-44-47-;-9-12-;0;0;0;1;5;141503,90;0;4;41339,70;4;34;1514,80;108;644;147,30;213;1238;81,00;288;1459;48,30;3326;18387;19,10;5532;29658;13,20;14010;72652;10,00;17959;98111;9,00;86518;453505;6,10;225743;1138029;3,90;1;1402,90;2;561,10;61;22,20;102;13,60;1709;2,00;2661;3,50;8977;4,80;15143;13,30;41054;2,70;381205;2,60;HZ 033 3032;; +23065;MARDI;15/08/2023;4;14/11/2023;3;32;5;43;9;6;10;-3-5-9-32-43-;-6-10-;0;0;0;1;4;122572,40;0;2;57294,40;1;17;2099,50;91;448;146,70;225;1109;62,60;219;1137;42,90;3211;15560;15,60;4695;22553;12,00;11832;58173;8,70;16558;81329;7,50;71439;333639;5,70;174767;843678;3,60;0;0;0;0;41;68,00;111;8,90;1601;1,50;2209;2,90;7940;3,90;13336;10,70;33851;2,40;278089;2,50;AF 362 0161;; +23064;VENDREDI;11/08/2023;3;10/11/2023;10;13;34;4;6;3;5;-4-6-10-13-34-;-3-5-;0;0;0;1;3;219719,20;1;7;22007,90;19;59;813,20;256;887;99,60;580;2513;37,10;501;1871;35,00;7471;33401;9,80;9921;41304;8,80;20441;82076;8,30;35800;168411;4,90;125269;561320;4,60;253766;1104392;3,70;0;0;7;355,60;121;11,30;268;5,10;3221;1,00;4520;2,00;15665;2,70;22829;8,70;56177;2,00;403868;2,40;SR 455 8629;; +23063;MARDI;08/08/2023;2;07/11/2023;40;8;33;9;35;6;3;-8-9-33-35-40-;-3-6-;0;0;0;0;2;245845,50;0;2;57458,10;3;23;1556,20;89;440;149,80;231;1141;61,00;164;987;49,60;4352;19202;12,70;4079;21226;12,80;8822;45557;11,10;25797;109652;5,60;71646;349370;5,50;147153;728739;4,20;0;0;1;1883,70;49;21,30;106;9,70;2030;1,20;1942;3,50;12068;2,70;19855;7,60;33960;2,50;322078;2,30;ML 851 3453;; +23062;VENDREDI;04/08/2023;1;03/11/2023;15;31;48;20;44;1;3;-15-20-31-44-48-;-1-3-;0;0;0;1;4;159371,20;0;9;16554,50;8;28;1657,30;117;636;134,40;231;1303;69,30;228;1369;46,30;4165;19841;16,00;5593;27388;12,90;12004;61041;10,80;23821;112585;7,00;88883;418775;6,00;191614;921973;4,30;0;1357,80;3;362,00;52;26,10;97;13,70;1934;1,70;2633;3,40;10970;3,80;18775;10,40;41810;2,60;414545;2,30;UQ 314 6381;; +23061;MARDI;01/08/2023;6;31/10/2023;35;27;25;45;40;7;11;-25-27-35-40-45-;-7-11-;0;1;72144196,00;0;2;293061,60;0;3;45662,20;8;51;836,60;104;597;131,60;263;1279;64,90;247;1216;48,00;3764;18880;15,40;5044;25530;12,70;11204;58000;10,40;23143;109013;6,70;79512;380992;6,00;169674;824731;4,50;0;0;3;759,80;54;22,60;125;9,80;1767;1,70;2445;3,40;11051;3,50;19346;9,40;37277;2,70;398960;2,20;ID 607 7223;; +23060;VENDREDI;28/07/2023;5;27/10/2023;38;7;34;32;33;9;6;-7-32-33-34-38-;-6-9-;0;0;0;0;0;0;2;8;110849,10;5;25;2093,00;113;604;159,50;263;1588;64,10;245;1452;49,30;4976;26395;13,50;5611;30296;13,10;12260;66535;11,10;29963;150228;5,90;94594;483808;5,80;205087;1060382;4,30;0;0;2;1306,70;62;23,00;118;12,20;2233;1,60;2728;3,50;13366;3,30;23974;8,70;43733;2,70;440281;2,30;UP 999 1591;; +23059;MARDI;25/07/2023;4;24/10/2023;11;7;27;5;37;12;6;-5-7-11-27-37-;-6-12-;0;0;0;0;5;104441,70;2;15;8136,50;7;24;1583,90;164;682;102,60;331;1291;57,30;526;2353;22,10;3666;16128;16,10;6712;30007;9,60;21024;93361;5,70;17060;79211;8,20;82599;378064;5,40;235301;1066813;3,10;0;0;4;477,30;84;12,40;175;5,90;1840;1,40;3195;2,20;8364;3,90;13088;11,70;38253;2,20;269384;2,80;KO 197 6486;; +23058;VENDREDI;21/07/2023;3;20/10/2023;7;35;33;36;31;10;7;-7-31-33-35-36-;-7-10-;0;0;0;0;4;164674,30;0;4;38487,10;7;23;2084,80;90;593;148,90;270;1378;67,70;235;1297;50,50;4736;21089;15,50;5318;27088;13,50;11277;58941;11,50;28187;118614;6,90;93794;449586;5,70;189118;947756;4,40;0;0;3;823,30;38;35,10;132;10,20;2294;1,40;2536;3,50;13528;3,10;21588;9,20;43686;2,50;421083;2,30;PL 062 9336;; +23057;MARDI;18/07/2023;2;17/10/2023;34;35;12;4;45;9;1;-4-12-34-35-45-;-1-9-;0;0;0;1;2;243239,50;2;6;18949,60;4;26;1362,00;86;406;160,60;228;1059;65,10;189;1005;48,20;3497;15657;15,40;4063;19519;13,80;9903;46764;10,70;19447;87899;6,90;64128;303565;6,30;156179;723488;4,20;0;0;1;1860,40;49;20,60;101;10,10;1591;1,60;1911;3,50;8848;3,60;14970;9,90;29982;2,80;293873;2,50;HM 567 0342;; +23056;VENDREDI;14/07/2023;1;13/10/2023;5;29;48;8;35;5;6;-5-8-29-35-48-;-5-6-;0;0;0;0;2;309646,00;0;6;24123,10;4;28;1610,00;140;651;127,50;303;1413;62,10;284;1350;45,60;4398;21537;14,30;6454;30850;11,10;12441;60425;10,60;22136;113533;6,80;100813;470633;5,10;194073;919464;4,20;0;0;3;762,00;59;20,80;138;9,10;1958;1,60;2869;2,90;10093;3,90;15752;11,60;45332;2,20;387716;2,30;GZ 094 7622;; +23055;MARDI;11/07/2023;6;10/10/2023;48;28;2;11;42;4;9;-2-11-28-42-48-;-4-9-;0;2;36621161,00;0;3;195184,20;2;6;22808,80;7;43;991,30;117;568;138,20;338;1662;49,90;304;1298;44,90;5333;24767;11,70;6242;27258;11,90;14437;61942;9,70;28539;131874;5,50;92712;407310;5,60;211571;916501;4,00;0;0;5;455,40;50;25,30;161;7,70;2321;1,30;2885;2,90;12472;3,10;20663;8,80;41634;2,40;357770;2,50;WK 386 6791;; +23054;VENDREDI;07/07/2023;5;06/10/2023;2;12;19;11;38;8;4;-2-11-12-19-38-;-4-8-;0;0;0;0;1;731833,60;1;6;28506,90;16;50;1065,50;242;926;105,90;589;2466;42,00;513;2115;34,40;7661;33617;10,80;10402;42976;9,40;24046;95953;7,80;35380;165314;5,50;133944;586538;4,90;297076;1288917;3,60;0;0;5;542,00;101;14,90;228;6,50;3262;1,10;4568;2,10;15118;3,10;22279;9,70;59807;2,00;406226;2,60;FL 752 1530;; +23053;MARDI;04/07/2023;4;03/10/2023;23;36;44;17;14;8;2;-14-17-23-36-44-;-2-8-;0;0;0;0;2;272715,70;1;10;12747,60;11;58;684,50;157;719;101,70;525;2739;28,20;321;1611;33,70;6324;30195;8,90;6475;31605;9,50;13850;66346;8,50;27941;132533;5,10;87308;421101;5,10;182299;891791;3,80;0;0;5;399,00;84;12,80;238;4,60;2817;,90;2998;2,40;12544;2,70;20221;7,90;40047;2,20;321238;2,40;EP 606 9951;; +23052;VENDREDI;30/06/2023;3;29/09/2023;29;25;6;36;1;10;7;-1-6-25-29-36-;-7-10-;0;0;0;0;3;225928,10;2;6;26401,50;10;31;1591,60;126;646;140,60;360;1428;67,20;301;1474;45,80;4895;21588;15,60;6633;31013;12,10;14684;70921;9,80;26706;116296;7,30;102432;470703;5,60;222753;1064851;4,00;0;0;6;410,90;62;22,00;170;8,00;2424;1,30;3057;2,90;12795;3,30;21783;9,00;47527;2,30;416642;2,30;DF 321 7979;; +23051;MARDI;27/06/2023;2;26/09/2023;27;22;44;17;23;7;1;-17-22-23-27-44-;-1-7-;0;0;0;1;2;244467,60;1;6;19045,30;7;33;1078,50;170;763;85,90;380;1602;43,20;411;1648;29,50;5309;22649;10,70;6928;31074;8,70;14783;68314;7,40;24861;103274;5,90;91360;417505;4,60;185265;882869;3,50;0;0;6;301,20;67;14,90;152;6,50;2366;1,00;3023;2,20;11026;2,80;16028;9,00;40672;2,00;288500;2,50;VU 548 9313;; +23050;VENDREDI;23/06/2023;1;22/09/2023;11;3;33;46;1;10;11;-1-3-11-33-46-;-10-11-;0;0;0;0;2;318086,90;1;1;148684,30;4;23;2013,50;125;622;137,10;218;1242;72,60;381;1683;37,60;3443;17629;17,90;5906;28315;12,40;16964;76547;8,50;17967;91328;8,70;84867;400175;6,20;244664;1100172;3,60;0;0;3;801,80;72;18,50;122;10,80;1762;1,80;2929;3,00;9317;4,40;14404;13,30;41606;2,60;344057;2,70;NT 848 3077;; +23049;MARDI;20/06/2023;5;19/09/2023;35;28;11;32;17;6;5;-11-17-28-32-35-;-5-6-;0;1;64343648,00;2;5;111169,00;0;3;43303,30;8;28;1445,10;176;808;92,20;271;1427;55,20;342;1595;34,70;3971;19914;13,90;6738;32401;9,50;13408;64230;8,90;20330;101300;6,80;95287;445356;4,90;187634;892025;3,90;2;568,00;3;302,90;72;15,50;130;8,60;1677;1,60;2990;2,50;8979;3,90;14225;11,50;41840;2,20;343298;2,30;QQ 390 9739;; +23048;VENDREDI;16/06/2023;4;15/09/2023;34;7;41;26;44;9;11;-7-26-34-41-44-;-9-11-;0;0;0;0;2;357495,20;0;0;0;7;30;7305,10;128;725;132,20;281;1422;71,20;348;1660;42,90;3954;20597;17,20;6485;32355;12,20;15262;73053;10,10;21781;112220;7,90;96833;474646;5,90;232780;1090118;4,10;0;0;6;449,00;59;25,30;129;11,40;1873;1,90;3039;3,20;10438;4,40;17743;12,10;45415;2,60;422301;2,50;WK 325 7642;; +23047;MARDI;13/06/2023;3;12/09/2023;15;16;21;50;41;5;6;-15-16-21-41-50-;-5-6-;0;0;0;1;2;260975,40;1;4;30497,10;5;24;1583,10;87;431;162,30;224;1137;65,00;185;1003;51,80;3361;17324;15,00;4466;23096;12,50;9405;47792;11,20;19514;94572;6,90;76278;370621;5,50;154840;754062;4,30;0;0;2;1007,30;43;25,40;93;12,00;1573;1,70;2078;3,50;8609;4,00;15493;10,40;34597;2,60;354390;2,20;KC 639 8760;; +23046;VENDREDI;09/06/2023;2;08/09/2023;18;26;36;50;41;11;12;-18-26-36-41-50-;-11-12-;0;0;0;0;0;0;1;6;139523,40;5;27;1829,40;89;491;185,30;195;1106;86,90;244;1246;54,20;3298;16897;20,00;4697;24280;15,50;12531;61820;11,30;17569;90562;9,30;73935;368537;7,20;206946;984666;4,30;0;0;1;2649,60;55;26,70;107;13,50;1727;2,10;2397;4,00;9061;5,00;14886;14,20;36908;3,20;378124;2,70;RA 701 3921;; +23045;MARDI;06/06/2023;1;05/09/2023;21;13;39;32;50;10;2;-13-21-32-39-50-;-2-10-;0;0;0;0;2;248559,50;3;10;11618,40;2;13;2783,70;96;483;138,00;214;1069;65,90;208;1277;38,70;3209;15267;16,20;4241;21545;12,80;10318;53210;9,60;18391;81443;7,60;65948;323130;6,00;158557;788725;4,00;0;0;2;970,90;46;22,90;105;10,20;1543;1,70;2085;3,40;8978;3,70;15883;9,80;31769;2,70;314048;2,40;RL 043 9910;; +23044;VENDREDI;02/06/2023;8;01/09/2023;25;3;15;43;12;10;11;-3-12-15-25-43-;-10-11-;0;1;130000000,00;1;7;172851,10;4;19;14883,50;10;43;2048,40;375;1432;113,30;534;2355;72,80;1064;4021;29,90;7365;33967;17,70;14267;58189;11,50;41077;163451;7,60;38103;176285;8,50;191350;805125;5,90;539981;2190204;3,50;0;0;3;1633,90;142;18,90;264;10,10;3546;1,80;6445;2,70;18236;4,60;29932;13,00;88493;2,50;712442;2,70;NG 971 7608;; +23043;MARDI;30/05/2023;7;29/08/2023;27;47;33;29;32;8;2;-27-29-32-33-47-;-2-8-;0;0;0;2;5;117606,70;0;5;27486,60;7;25;1712,20;102;504;156,40;248;1423;58,50;204;1059;55,30;4618;23583;12,40;4641;24707;13,20;10460;55123;11,00;28909;137171;5,30;76638;390205;5,90;170177;869044;4,30;1;1162,80;4;232,50;52;22,30;117;9,80;2029;1,40;2071;3,70;12635;2,80;22682;7,40;33987;2,70;351438;2,30;KW 464 0380;; +23042;VENDREDI;26/05/2023;6;25/08/2023;38;25;15;37;41;1;7;-15-25-37-38-41-;-1-7-;0;0;0;0;4;190665,00;0;3;59415,40;6;31;1790,90;111;733;139,50;297;1525;70,80;270;1524;49,80;5182;24475;15,50;6398;33279;12,70;13016;72955;10,80;32656;142592;6,70;98254;499263;6,00;208254;1098037;4,40;0;0;3;901,30;58;25,40;158;9,20;2313;1,60;2924;3,30;14264;3,20;26948;8,00;44194;2,70;483705;2,20;KT 917 3074;; +23041;MARDI;23/05/2023;5;22/08/2023;34;5;14;6;23;11;2;-5-6-14-23-34-;-2-11-;0;0;0;0;3;187039,00;2;7;18734,60;9;38;1074,90;136;710;105,90;269;1377;57,70;405;1826;30,60;4100;19268;14,50;6649;30860;10,10;16528;74395;7,80;20482;98805;7,10;88535;417707;5,30;216974;992176;3,50;0;0;4;506,60;61;18,40;133;8,40;1922;1,40;2966;2,50;9587;3,60;15064;10,70;40059;2,20;301298;2,60;BQ 269 5299;; +23040;VENDREDI;19/05/2023;4;18/08/2023;41;27;34;49;18;3;9;-18-27-34-41-49-;-3-9-;0;0;0;1;3;239206,30;3;10;16771,90;12;44;1187,20;185;854;112,60;490;2169;46,90;380;1724;41,40;6448;30151;11,80;7236;34696;11,40;14818;70468;10,50;35099;164870;5,40;100229;491058;5,70;208853;1016739;4,40;0;0;4;645,60;81;17,40;210;6,80;2854;1,20;3199;2,90;15294;2,90;28001;7,40;45189;2,50;452667;2,20;KL 890 9887;; +23039;MARDI;16/05/2023;3;15/08/2023;14;6;9;4;37;4;11;-4-6-9-14-37-;-4-11-;0;0;0;0;1;539367,90;0;7;18008,40;3;24;1636,00;162;619;116,80;276;1249;61,20;347;1378;38,90;3728;17659;15,20;6871;28842;10,30;15689;65918;8,40;18383;90548;7,40;87718;392797;5,40;211872;921483;3,70;0;0;3;680,40;80;14,10;139;8,10;1783;1,50;3167;2,30;8795;4,00;13263;12,30;40352;2,20;296464;2,70;LB 512 4777;; +23038;VENDREDI;12/05/2023;2;11/08/2023;9;44;13;39;10;6;4;-9-10-13-39-44-;-4-6-;0;0;0;2;3;234925,90;2;8;20589,70;9;36;1425,10;148;676;139,80;480;1824;54,70;319;1446;48,50;7995;30108;11,60;7871;34193;11,40;17502;76574;9,50;41166;158399;5,50;123693;530715;5,20;273957;1183027;3,70;1;1519,90;5;243,10;64;23,30;208;7,20;3439;1,00;3533;2,80;17735;2,60;25287;8,60;55647;2,20;421268;2,50;RI 034 8136;; +23037;MARDI;09/05/2023;1;08/08/2023;21;28;13;17;46;9;7;-13-17-21-28-46-;-7-9-;0;0;0;0;0;0;0;5;126107,90;8;42;885,80;164;851;80,50;508;2057;35,20;336;1477;34,40;6106;25727;9,80;7960;35739;7,90;14439;64835;8,10;28427;120240;5,30;99796;455562;4,40;184579;859732;3,70;0;0;3;639,20;87;11,90;227;4,60;2747;,90;3566;1,90;12728;2,60;19129;8,00;44912;1,90;322473;2,30;SZ 660 9457;; +23036;VENDREDI;05/05/2023;12;04/08/2023;34;8;49;18;3;3;7;-3-8-18-34-49-;-3-7-;1;3;53041075,00;0;7;157711,00;1;9;28668,60;13;69;1164,70;306;1270;116,50;1040;4046;38,60;564;2521;43,60;15370;60931;9,00;13862;57893;10,50;25518;113767;10,00;80139;325394;4,20;201865;837510;5,20;379141;1672707;4,10;0;0;4;1067,10;126;18,30;433;5,40;6490;,90;6122;2,50;33287;2,20;50415;6,70;88440;2,10;739387;2,30;SR 029 2197;; +23035;MARDI;02/05/2023;11;01/08/2023;48;44;32;7;47;8;7;-7-32-44-47-48-;-7-8-;0;0;0;0;2;383454,10;3;6;29873,10;8;27;2067,70;168;759;135,40;388;1705;63,70;251;1469;52,00;5357;24471;15,60;7829;37168;11,40;14796;72324;10,90;29366;132698;7,20;120033;553646;5,40;224505;1069150;4,50;0;0;6;469,90;72;21,10;178;8,70;2321;1,60;3448;3,00;12691;3,80;21444;10,50;52229;2,40;516064;2,10;BY 030 2805;; +23034;VENDREDI;28/04/2023;10;28/07/2023;34;23;11;13;16;1;10;-11-13-16-23-34-;-1-10-;0;0;0;1;1;982720,10;3;14;16405,50;11;49;1459,90;210;1194;110,30;420;2040;68,20;678;3272;29,90;6195;29204;16,70;10933;51296;10,60;29304;140130;7,20;30817;147338;8,30;149809;697582;5,50;399529;1866740;3,30;0;0;2;1736,70;87;21,60;189;10,10;2892;1,60;4819;2,60;14435;4,10;22518;12,30;65927;2,30;494050;2,70;QG 416 7056;; +23033;MARDI;25/04/2023;9;25/07/2023;29;45;40;10;30;12;1;-10-29-30-40-45-;-1-12-;0;0;0;0;1;717775,80;2;5;33551,20;6;40;1306,20;111;648;148,50;323;1717;59,20;316;1695;42,10;3908;19750;18,10;6019;31233;12,70;15541;79582;9,30;22298;108657;8,20;84624;418973;6,70;228710;1119406;4,00;0;0;3;830,40;48;28,80;143;9,40;1839;1,80;2690;3,30;10279;4,10;17876;11,10;38223;2,90;353305;2,80;TY 132 5543;; +23032;VENDREDI;21/04/2023;8;21/07/2023;18;8;7;42;33;8;2;-7-8-18-33-42-;-2-8-;0;0;0;1;3;318105,80;3;13;17156,90;16;75;926,20;259;1279;100,00;782;3538;38,20;596;2747;34,60;10227;47595;9,90;11565;54053;9,80;23518;113805;8,60;50027;234770;5,00;155134;745626;5,00;329020;1600549;3,70;0;0;7;459,10;125;14,00;322;5,40;4232;1,00;4986;2,30;21051;2,60;30046;8,50;67071;2,10;494059;2,50;IK 847 8978;; +23031;MARDI;18/04/2023;7;18/07/2023;37;21;17;35;23;11;10;-17-21-23-35-37-;-10-11-;0;0;0;0;4;150414,50;0;6;23436,20;9;37;1183,70;148;645;125,00;215;1083;78,70;340;1682;35,60;2985;16568;18,00;5791;29365;11,30;15690;79242;7,80;15589;84684;8,90;79822;402950;5,80;219763;1089552;3,50;0;0;6;356,80;78;15,20;115;10,00;1399;2,00;2721;2,80;7523;4,80;12477;13,60;37004;2,60;307425;2,70;BP 573 2097;; +23030;VENDREDI;14/04/2023;6;14/07/2023;44;4;37;49;11;7;6;-4-11-37-44-49-;-6-7-;0;0;0;1;2;383595,60;0;7;25615,00;8;30;1861,60;194;867;118,60;343;1645;66,10;333;1677;45,50;5562;25265;15,10;8609;37902;11,20;15959;77447;10,20;31227;141622;6,70;127362;560348;5,40;235391;1123822;4,30;0;0;3;943,30;88;17,80;142;10,70;2471;1,50;3846;2,70;13975;3,50;23792;9,50;57086;2,20;527348;2,10;BP 590 6741;; +23029;MARDI;11/04/2023;5;11/07/2023;19;16;14;29;10;2;10;-10-14-16-19-29-;-2-10-;0;0;0;1;2;269932,10;1;10;12617,50;13;33;1190,90;171;790;91,60;323;1498;51,00;452;1874;28,60;4419;19088;14,00;7245;31615;9,40;16828;74016;7,50;21374;94853;7,10;91864;409669;5,20;215858;975578;3,50;0;0;4;490,80;60;17,30;150;7,20;2080;1,30;3290;2,20;10228;3,30;14774;10,60;41477;2,10;290245;2,70;QB 155 9413;; +23028;VENDREDI;07/04/2023;4;07/07/2023;26;28;46;5;36;12;6;-5-26-28-36-46-;-6-12-;0;0;0;1;2;352535,10;1;6;27464,40;9;33;1555,30;156;664;142,30;337;1641;60,90;331;1594;44,00;4429;19377;18,10;6638;30798;12,70;15807;71565;10,10;24620;105720;8,30;94997;429199;6,40;242286;1083158;4,10;1;1529,80;4;305,90;78;19,30;172;8,80;2177;1,70;3151;3,20;12063;3,90;20614;10,70;44887;2,70;417412;2,60;TM 201 9996;; +23027;MARDI;04/04/2023;3;04/07/2023;50;33;16;31;10;3;8;-10-16-31-33-50-;-3-8-;0;0;0;0;2;264328,70;2;5;24711,10;2;16;2405,20;99;475;149,20;282;1348;55,50;215;1112;47,30;4292;22024;11,90;4905;24416;12,00;10571;51848;10,50;24842;123299;5,30;78233;374844;5,50;163934;785097;4,20;0;0;0;0;45;66,00;119;9,20;1880;1,40;2266;3,20;11018;3,10;19520;8,20;35733;2,50;347517;2,20;BT 280 0928;; +23026;VENDREDI;31/03/2023;2;30/06/2023;28;34;47;16;18;10;5;-16-18-28-34-47-;-5-10-;0;0;0;1;2;346909,70;0;3;54052,30;6;27;1870,60;126;676;137,60;286;1514;64,90;313;1501;46,00;4485;22272;15,50;6331;31227;12,30;14911;70706;10,10;24199;119432;7,20;97357;463717;5,90;227267;1058817;4,10;1;1455,40;4;291,00;62;23,40;152;9,40;2095;1,70;2976;3,20;11540;3,90;19870;10,50;44880;2,60;415515;2,50;VQ 025 4030;; +23025;MARDI;28/03/2023;1;27/06/2023;21;16;34;36;44;9;10;-16-21-34-36-44-;-9-10-;0;0;0;0;1;482807,50;0;4;28210,00;6;19;1849,80;101;473;136,80;136;779;87,80;202;1050;45,80;2143;11497;20,90;3893;20191;13,20;9119;47747;10,40;12128;63320;9,50;60278;304758;6,20;141724;713672;4,30;0;0;1;1795,20;52;18,80;63;15,50;1047;2,30;1839;3,60;5952;5,20;10420;13,80;28195;2,80;291908;2,40;KX 475 0920;; +23024;VENDREDI;24/03/2023;5;23/06/2023;36;25;5;46;12;10;6;-5-12-25-36-46-;-6-10-;0;1;66287741,00;1;6;126453,80;2;7;25332,20;8;34;1624,40;192;864;117,70;350;1710;62,80;379;1896;39,80;4871;24195;15,60;7623;34942;12,00;18742;85287;9,20;25894;125436;7,50;111615;506644;5,90;267729;1225880;3,90;0;0;3;926,40;89;17,30;168;9,00;2323;1,60;3457;2,90;12026;3,90;20268;11,00;50869;2,40;435326;2,50;CY 773 8015;; +23023;MARDI;21/03/2023;4;20/06/2023;29;1;9;32;20;3;2;-1-9-20-29-32-;-2-3-;0;0;0;0;2;281195,20;0;6;21906,60;5;28;1462,10;182;711;106,00;275;1360;58,60;339;1379;40,60;4089;19222;14,50;7151;31063;10,00;13856;61825;9,40;22479;102509;6,80;100233;442730;5,00;194132;881019;4,00;0;0;3;698,10;89;12,90;134;8,60;1882;1,50;3276;2,30;10341;3,50;15472;10,80;44962;2,10;365837;2,20;KH 270 8685;; +23022;VENDREDI;17/03/2023;3;16/06/2023;37;5;26;15;49;8;11;-5-15-26-37-49-;-8-11-;0;0;0;2;7;103234,90;3;10;16889,30;7;31;1696,90;143;741;130,70;297;1409;72,70;402;1816;39,60;4480;21143;17,00;7125;33206;12,00;17214;77603;9,60;23571;112699;8,00;103890;478919;5,90;256954;1136779;4,00;1;1561,90;5;249,90;62;25,10;166;9,40;2214;1,70;3356;3,00;11560;4,20;19098;11,80;48003;2,60;434924;2,50;QJ 547 2111;; +23021;MARDI;14/03/2023;2;13/06/2023;26;46;28;31;16;12;11;-16-26-28-31-46-;-11-12-;0;0;0;2;3;176354,00;2;6;20608,40;7;27;1426,40;113;472;150,30;185;890;84,20;314;1289;40,80;2542;12806;20,50;4462;21120;13,90;13290;59941;9,10;13718;68655;9,60;63198;299524;6,90;193051;853081;3,90;1;1156,90;3;308,50;50;23,10;97;11,60;1300;2,10;2183;3,50;7031;5,10;11846;14,00;30840;3,00;291066;2,80;BO 892 9850;; +23020;VENDREDI;10/03/2023;1;09/06/2023;9;16;48;37;38;7;2;-9-16-37-38-48-;-2-7-;0;0;0;0;4;179080,60;1;3;55805,40;9;35;1489,80;140;620;154,90;453;1879;54,00;285;1395;51,10;7408;29236;12,20;7011;30605;13,00;14194;64889;11,40;45867;172837;5,10;111311;481535;5,80;219649;993900;4,50;0;0;3;975,20;70;22,50;198;8,00;3363;1,10;3230;3,30;20048;2,50;35607;6,60;51495;2,50;528511;2,20;JS 582 4093;; +23019;MARDI;07/03/2023;3;06/06/2023;50;24;35;13;11;6;11;-11-13-24-35-50-;-6-11-;0;1;144966361,00;0;5;186010,40;4;11;19760,70;11;52;1302,00;213;1066;116,90;495;2235;58,90;460;2142;43,20;7940;32721;14,10;10563;44979;11,40;23875;103188;9,30;39997;164414;7,00;155477;655921;5,50;362067;1519654;3,80;0;0;5;758,10;81;25,90;242;8,60;3595;1,40;4793;2,90;17958;3,60;28834;10,40;69417;2,40;577363;2,60;KL 200 9176;; +23018;VENDREDI;03/03/2023;2;02/06/2023;24;3;8;50;6;10;11;-3-6-8-24-50-;-10-11-;0;0;0;4;9;150850,40;8;23;13795,90;18;71;1392,00;445;1574;115,60;712;2768;69,50;1275;4170;32,40;9015;38046;17,70;16849;63197;11,90;47840;175575;7,90;46824;197887;8,50;230675;887391;6,00;642413;2391851;3,60;1;3350,90;10;268,00;197;17,00;323;10,20;4344;1,90;7709;2,80;22253;4,60;36391;13,20;106475;2,50;878163;2,70;VB 581 8468;; +23017;MARDI;28/02/2023;1;30/05/2023;29;33;12;37;47;10;5;-12-29-33-37-47-;-5-10-;0;0;0;2;5;104365,10;1;10;12195,90;7;29;1309,90;104;522;134,00;247;1167;63,30;237;1123;46,20;3427;16880;15,30;5091;23943;12,10;12688;55554;9,70;18412;89949;7,20;74127;345266;5,90;175090;795288;4,10;1;1113,10;1;890,50;45;24,70;119;9,30;1597;1,70;2321;3,10;8599;4,00;15134;10,60;34131;2,60;315643;2,50;KE 116 4078;; +23016;VENDREDI;24/02/2023;7;26/05/2023;42;34;7;23;48;1;3;-7-23-34-42-48-;-1-3-;1;1;102242065,00;0;2;446486,90;2;11;18972,90;15;65;1000,00;305;1295;92,40;432;1942;65,10;661;2780;31,90;6410;29277;15,10;10825;48033;10,30;23124;103245;8,90;35931;158336;7,00;146804;653739;5,30;316184;1414630;4,00;0;0;6;565,40;126;14,90;189;9,80;2796;1,60;4622;2,70;15555;3,70;26315;10,30;64153;2,30;563257;2,40;VC 786 7371;; +23015;MARDI;21/02/2023;6;23/05/2023;49;41;22;34;21;2;7;-21-22-34-41-49-;-2-7-;0;0;0;1;2;305559,40;0;3;47609,50;8;37;1202,30;134;591;138,60;332;1597;54,20;283;1240;49,00;5607;24718;12,30;5649;27372;12,40;11337;57167;11,00;34212;145813;5,20;87054;418022;5,70;171210;861278;4,50;0;0;2;1088,60;73;16,50;140;8,50;2374;1,20;2447;3,20;14599;2,50;26157;6,60;38297;2,50;398432;2,10;AE 744 7635;; +23014;VENDREDI;17/02/2023;5;19/05/2023;42;27;45;23;8;1;9;-8-23-27-42-45-;-1-9-;0;0;0;0;2;389007,50;3;10;18183,40;8;38;1490,40;193;915;114,00;434;1931;57,10;435;2131;36,30;6125;28368;13,60;8292;39444;10,90;21257;95550;8,40;31991;146242;6,60;116722;553040;5,50;285543;1322109;3,70;0;0;2;1410,80;84;18,00;191;8,00;2493;1,50;3663;2,80;13475;3,60;22444;10,00;51534;2,40;433737;2,50;BQ 304 7323;; +23013;MARDI;14/02/2023;4;16/05/2023;43;24;46;26;38;2;3;-24-26-38-43-46-;-2-3-;0;0;0;0;4;147832,90;1;2;69102,00;2;21;2049,80;132;611;129,70;236;1078;77,70;220;1144;51,40;3452;16539;17,80;5676;25640;12,80;11375;53177;11,50;20412;95339;7,70;86458;392777;5,90;173898;803215;4,60;0;0;1;2311,40;65;19,40;110;11,60;1683;1,80;2652;3,20;9561;4,10;19128;9,60;39763;2,60;433736;2,10;JF 732 7313;; +23012;VENDREDI;10/02/2023;3;12/05/2023;3;13;36;25;47;3;8;-3-13-25-36-47-;-3-8-;0;0;0;4;12;62801,60;4;17;10360,70;15;83;660,90;216;1151;87,80;529;3184;33,50;390;2099;35,70;7478;35034;10,70;8894;41898;9,90;17258;82058;9,50;37924;176046;5,30;129676;592301;5,00;250918;1160251;4,10;1;1604,80;8;160,40;88;18,20;241;6,60;3219;1,20;3986;2,60;16699;3,00;26424;8,70;58779;2,20;486536;2,30;QO 520 2312;; +23011;MARDI;07/02/2023;2;09/05/2023;8;47;34;44;2;9;3;-2-8-34-44-47-;-3-9-;0;0;0;0;5;109249,20;2;6;21277,70;5;19;2092,80;96;447;163,80;267;1268;61,00;219;1050;51,80;4411;20488;13,20;4624;22536;13,40;10345;49757;11,30;27055;121583;5,60;75929;361349;5,90;167953;778049;4,40;0;0;3;697,50;46;25,20;123;9,20;1969;1,40;2142;3,50;12160;2,90;22947;7,30;35177;2,60;365300;2,20;SL 643 1128;; +23010;VENDREDI;03/02/2023;1;05/05/2023;14;32;45;2;17;10;3;-2-14-17-32-45-;-3-10-;0;0;0;4;7;200626,30;9;30;10940,90;35;97;1053,90;443;1623;116,00;1139;3561;55,90;1125;4033;34,60;15277;49800;14,00;20104;68391;11,40;46148;159540;9,10;81348;261401;6,70;285482;969875;5,70;658299;2249589;3,90;3;1274,60;16;191,10;185;20,40;459;8,20;6599;1,40;8733;2,80;35527;3,30;60004;9,10;124936;2,40;1105837;2,40;QL 526 5035;,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,; +23009;MARDI;31/01/2023;5;02/05/2023;7;12;33;43;19;3;7;-7-12-19-33-43-;-3-7-;0;1;66081021,00;1;3;202795,00;4;5;28437,90;14;99;447,30;231;972;83,90;856;3897;22,10;374;1722;35,10;10757;47231;6,40;10051;42115;8,00;16514;74951;8,30;45150;199918;3,80;125805;544979;4,40;216465;1000376;3,80;0;0;10;220,30;100;12,20;390;3,10;4558;,60;4512;1,80;19352;1,90;25262;7,00;55147;1,80;362021;2,40;PB 840 6401;; +23008;VENDREDI;27/01/2023;4;28/04/2023;15;19;31;8;9;12;10;-8-9-15-19-31-;-10-12-;0;0;0;2;4;186490,50;5;13;13411,00;12;33;1645,50;204;797;125,50;314;1505;70,20;647;2624;28,30;3992;20087;18,40;7356;33629;12,30;25928;111310;6,90;19763;101419;9,20;99353;465139;6,30;330382;1474197;3,20;2;734,20;6;195,80;97;14,90;153;9,40;1934;1,80;3458;2,80;9529;4,70;15681;13,50;46757;2,50;350905;3,00;UF 710 0564;; +23007;MARDI;24/01/2023;3;25/04/2023;11;27;47;41;43;7;11;-11-27-41-43-47-;-7-11-;0;0;0;1;2;263096,40;0;4;30744,90;6;34;1126,60;94;496;142,20;274;1315;56,70;206;1076;48,70;3622;19025;13,70;4774;24315;12,00;9914;50173;10,80;19617;98362;6,70;72417;360151;5,70;148156;748772;4,40;0;0;1;1842,50;37;27,60;120;8,50;1763;1,40;2157;3,10;9063;3,50;14806;9,90;32674;2,50;311111;2,30;FU 933 3436;; +23006;VENDREDI;20/01/2023;2;21/04/2023;40;6;13;42;3;7;11;-3-6-13-40-42-;-7-11-;0;0;0;1;4;170372,20;0;8;19909,40;7;36;1378,00;155;726;125,80;324;1649;58,50;343;1588;42,70;5074;24580;13,80;7520;34871;10,80;17031;78980;8,90;26057;127330;6,70;110182;506287;5,30;241746;1128008;3,80;0;0;3;816,10;76;17,80;151;9,00;2367;1,40;3315;2,70;11868;3,50;19357;10,10;48952;2,20;399861;2,40;MO 149 0237;; +23005;MARDI;17/01/2023;1;18/04/2023;18;46;30;50;21;2;6;-18-21-30-46-50-;-2-6-;0;0;0;3;6;82263,60;2;3;38452,70;12;30;1197,70;90;415;159,40;253;1174;59,60;176;962;51,10;4029;18805;13,00;4228;21186;12,90;9086;46272;11,00;23521;107208;5,70;68528;330237;5,80;144638;717298;4,30;1;1005,10;6;134,00;53;18,60;115;8,70;1728;1,40;1974;3,30;10278;3,00;18318;7,90;30926;2,60;315717;2,20;PM 861 1466;; +23004;VENDREDI;13/01/2023;5;14/04/2023;13;4;33;25;37;10;5;-4-13-25-33-37-;-5-10-;0;1;68825114,00;4;5;182354,40;4;11;19372,30;15;66;1005,60;310;1124;108,70;586;2343;55,10;775;2818;32,20;7897;31940;14,20;12765;48995;10,30;31055;116575;8,00;40578;163100;7,00;170930;667206;5,30;413948;1566569;3,60;2;1103,90;7;252,30;132;16,40;238;9,10;3519;1,50;5355;2,70;18270;3,70;29106;10,90;73934;2,40;606899;2,60;JN 226 6277;; +23003;MARDI;10/01/2023;4;11/04/2023;42;11;34;13;40;10;3;-11-13-34-40-42-;-3-10-;0;0;0;0;0;0;1;5;145098,90;6;28;1528,80;105;552;142,80;308;1334;62,40;260;1342;43,60;3931;19941;14,60;5054;25943;12,50;11406;59495;10,20;21476;105242;7,00;76496;398664;5,80;175618;911043;4,10;0;0;2;990,30;49;21,50;145;7,30;1854;1,40;2291;3,10;10028;3,40;16673;9,50;34808;2,50;317355;2,40;CQ 491 0323;; +23002;VENDREDI;06/01/2023;3;07/04/2023;44;16;35;45;31;12;4;-16-31-35-44-45-;-4-12-;0;0;0;1;2;342205,30;0;0;0;4;21;9989,50;96;535;171,50;249;1290;75,20;260;1362;50,00;3306;17414;19,50;4992;25411;14,90;12801;63472;11,10;19088;94055;9,10;80744;390026;6,90;207277;986599;4,40;1;1426,40;1;1141,10;42;31,60;119;11,70;1641;2,10;2305;4,00;9276;4,70;16224;12,60;37485;3,10;396752;2,50;PJ 372 0439;; +23001;MARDI;03/01/2023;2;04/04/2023;20;29;21;46;45;10;3;-20-21-29-45-46-;-3-10-;0;0;0;0;2;260515,80;0;5;24354,70;3;21;1806,10;86;400;174,60;196;923;80,00;170;903;57,40;3378;15877;16,30;4060;20261;14,20;9049;46476;11,50;19658;90728;7,10;68362;331619;6,20;156102;755050;4,30;0;0;2;942,00;50;20,90;100;10,00;1545;1,60;1875;3,60;9104;3,50;15830;9,50;31672;2,70;306552;2,40;NP 521 2967;; +22104;VENDREDI;30/12/2022;1;31/03/2023;48;42;17;34;47;1;9;-17-34-42-47-48-;-1-9-;0;0;0;0;4;177017,30;1;7;23641,00;13;45;1145,40;118;575;165,10;268;1466;68,40;293;1419;49,70;4279;21194;16,60;5647;29331;13,40;13561;68285;10,70;25675;123442;7,10;84087;434802;6,40;199466;1011708;4,40;0;0;6;421,20;62;22,20;116;12,00;1864;1,80;2523;3,60;11271;3,80;22001;9,20;38593;2,90;421925;2,30;HJ 777 0024;; +22103;MARDI;27/12/2022;2;28/03/2023;33;31;19;34;43;5;3;-19-31-33-34-43-;-3-5-;0;1;26956990,00;0;2;259877,40;1;1;121475,20;8;32;1182,30;115;518;134,50;252;1355;54,30;213;1015;51,00;3947;20900;12,30;4779;24579;11,70;9867;48772;11,00;24573;120871;5,30;72183;363093;5,60;146691;715483;4,60;0;0;3;626,50;57;18,30;116;8,80;1651;1,50;2135;3,20;10581;3,00;19127;7,80;32319;2,60;333530;2,20;DZ 394 5017;; +22102;VENDREDI;23/12/2022;1;24/03/2023;16;17;26;23;35;5;11;-16-17-23-26-35-;-5-11-;0;0;0;1;2;363954,60;3;10;17012,40;6;34;1558,50;140;806;121,10;334;1846;55,80;370;1817;39,90;4493;25410;14,20;7553;39914;10,10;18088;88854;8,40;23713;130022;7,00;107850;548914;5,20;259306;1237331;3,70;0;0;5;518,70;65;22,10;173;8,20;2155;1,60;3285;2,90;11062;4,00;17838;11,60;48044;2,40;390092;2,60;VM 500 1739;; +22101;MARDI;20/12/2022;4;21/03/2023;1;20;11;17;27;4;5;-1-11-17-20-27-;-4-5-;0;1;51605931,00;2;7;81968,40;6;9;14900,10;15;52;803,20;289;1077;71,40;413;1916;42,40;542;2197;26,00;4950;23778;12,00;9739;40581;7,80;20505;84067;7,00;22467;111476;6,40;109444;512306;4,40;225774;1034823;3,50;2;553,10;5;177,00;128;8,50;181;6,00;2120;1,20;4202;1,70;9418;3,60;13913;11,40;47119;1,90;306347;2,50;ET 088 9165;; +22100;VENDREDI;16/12/2022;3;17/03/2023;2;15;44;35;19;7;2;-2-15-19-35-44-;-2-7-;0;0;0;1;2;354267,10;1;4;41399,00;25;55;937,80;188;810;117,30;659;2454;40,90;328;1565;45,10;9100;35975;9,80;8337;37644;10,40;15028;72341;10,10;45255;180570;4,90;123280;553257;5,00;224173;1067805;4,20;0;0;7;373,90;86;16,90;285;5,00;3856;,90;3676;2,60;18782;2,40;29448;7,10;54535;2,10;455532;2,20;LY 901 6311;; +22099;MARDI;13/12/2022;2;14/03/2023;30;3;12;26;9;10;11;-3-9-12-26-30-;-10-11-;0;0;0;1;4;131029,00;5;17;7205,50;6;23;1658,80;215;780;90,10;263;1068;69,50;784;2764;18,80;2838;14135;18,40;7073;28374;10,20;24353;92344;5,80;13949;71516;9,10;80321;353831;5,80;255115;1065206;3,10;0;0;3;660,80;98;11,10;110;9,90;1359;1,90;3224;2,20;6739;5,00;10826;14,60;37973;2,30;270813;2,90;OG 732 2723;; +22098;VENDREDI;09/12/2022;1;10/03/2023;8;46;31;27;50;3;1;-8-27-31-46-50-;-1-3-;0;0;0;0;2;343883,00;1;6;26790,30;9;35;1430,40;126;584;157,90;323;1422;68,50;283;1432;47,80;4811;21477;15,90;6076;29299;13,00;13721;64789;10,90;27525;121538;7,00;100430;452332;6,00;217802;999745;4,30;0;0;3;899,30;50;29,30;157;9,40;2191;1,60;2767;3,50;12236;3,80;21237;10,10;45569;2,60;458109;2,30;MH 156 7284;; +22097;MARDI;06/12/2022;9;07/03/2023;20;25;27;26;12;8;12;-12-20-25-26-27-;-8-12-;0;1;142897164,00;1;5;160293,30;2;4;46828,90;10;61;956,40;196;902;119,10;375;1737;65,40;572;2335;34,10;5356;24294;16,40;10010;41422;10,70;25864;104275;7,90;27091;123593;8,10;135001;576321;5,40;346444;1447771;3,50;0;0;5;599,90;101;16,10;162;10,10;2505;1,60;4524;2,40;12652;4,00;18659;12,80;59984;2,20;428589;2,70;BF 128 8115;; +22096;VENDREDI;02/12/2022;8;03/03/2023;39;12;35;21;45;11;6;-12-21-35-39-45-;-6-11-;0;0;0;0;1;1174364,10;1;13;21112,90;17;62;1378,80;250;1122;140,30;619;2555;65,10;610;2779;42,00;8946;38067;15,30;11991;53798;12,10;27936;123299;9,80;48142;202760;7,20;178792;776392;5,90;422194;1820950;4,00;0;0;5;927,20;108;23,60;266;9,50;4130;1,50;5345;3,10;21690;3,60;36230;10,20;79779;2,60;727880;2,50;HR 390 6385;; +22095;MARDI;29/11/2022;7;28/02/2023;20;38;35;15;24;12;8;-15-20-24-35-38-;-8-12-;0;0;0;0;1;614404,50;1;1;143596,40;4;24;1863,60;110;552;149,20;217;1051;82,80;258;1405;43,50;2974;16173;18,90;4964;25966;13,10;12220;64037;9,90;16744;86791;8,80;76454;389623;6,20;195924;972239;4,00;0;0;2;1066,90;63;18,80;100;11,60;1476;1,90;2251;3,40;7955;4,60;13579;12,50;34880;2,70;319577;2,60;TV 773 5123;; +22094;VENDREDI;25/11/2022;6;24/02/2023;21;19;39;35;37;2;6;-19-21-35-37-39-;-2-6-;0;0;0;0;4;192883,50;2;10;18032,00;8;39;1440,10;166;812;127,40;408;1986;55,00;324;1687;45,50;6368;30221;12,70;7107;35846;11,90;14688;78160;10,20;35360;164651;5,80;107020;533344;5,70;222500;1156230;4,20;0;0;2;1329,40;77;19,10;177;8,20;2746;1,30;3098;3,10;15138;3,00;26243;8,10;46635;2,50;458023;2,30;GA 666 0427;; +22093;MARDI;22/11/2022;5;21/02/2023;24;29;42;21;22;3;11;-21-22-24-29-42-;-3-11-;0;0;0;0;2;280000,80;1;3;43627,20;7;20;2038,30;106;503;149,20;257;1214;65,30;223;1170;47,60;3849;19094;14,60;5453;26626;11,60;12469;60413;9,50;20302;101554;6,90;84953;411289;5,30;194240;921198;3,80;0;0;1;1968,40;54;19,50;131;8,10;1754;1,50;2467;2,90;9395;3,60;14382;10,90;37947;2,30;304584;2,50;MU 402 1458;; +22092;VENDREDI;18/11/2022;4;17/02/2023;41;13;35;19;16;11;1;-13-16-19-35-41-;-1-11-;0;0;0;0;3;245766,10;1;11;15665,30;6;33;1626,40;125;658;150,20;301;1424;73,30;345;1722;42,60;4257;21279;17,20;6169;30629;13,30;15967;78666;9,60;23277;114630;8,00;95090;463190;6,20;253264;1201485;3,90;0;0;3;885,20;55;26,30;137;10,70;1939;1,80;2818;3,40;11071;4,10;18050;11,80;42993;2,80;385170;2,70;HK 181 5153;; +22091;MARDI;15/11/2022;3;14/02/2023;31;21;13;24;33;1;12;-13-21-24-31-33-;-1-12-;0;0;0;0;3;179655,80;2;6;20994,20;7;25;1569,40;136;595;121,40;269;1176;64,90;405;1932;27,70;3430;16057;16,70;5751;26062;11,40;17118;79418;7,00;17332;80918;8,30;73409;348245;6,10;218204;1026922;3,30;0;0;3;659,80;62;17,70;133;8,00;1708;1,50;2554;2,80;8589;3,90;13736;11,50;34393;2,60;272601;2,80;RT 647 3979;; +22090;VENDREDI;11/11/2022;2;10/02/2023;25;2;19;44;24;2;10;-2-19-24-25-44-;-2-10-;0;0;0;1;3;230878,40;0;8;20235,00;10;35;1440,60;162;764;121,50;368;1607;61,10;359;1585;43,50;5188;23625;14,60;7576;35260;10,90;16405;78973;9,00;25239;117657;7,30;107188;509901;5,30;241565;1164650;3,70;1;1343,40;3;358,20;80;16,70;167;8,00;2414;1,30;3432;2,50;11674;3,50;17522;11,00;48268;2,20;360109;2,60;IG 959 4548;; +22089;MARDI;08/11/2022;1;07/02/2023;50;32;33;5;3;1;8;-3-5-32-33-50-;-1-8-;0;0;0;0;3;173618,20;0;3;40577,40;2;15;2527,70;76;451;154,80;204;979;75,40;166;1108;46,80;3386;16606;15,60;4270;22369;12,90;9777;51701;10,40;19852;90609;7,20;70913;355759;5,70;164212;821545;4,00;0;0;1;1955,10;41;26,40;107;10,00;1572;1,70;2000;3,50;9082;3,70;15621;10,00;33064;2,60;312893;2,40;AA 807 8409;; +22088;VENDREDI;04/11/2022;12;03/02/2023;11;2;37;47;45;2;3;-2-11-37-45-47-;-2-3-;1;1;160788895,00;0;2;572462,00;3;10;26758,70;9;51;1634,20;221;1100;139,50;432;2139;75,80;418;2259;50,40;6795;33996;16,70;10516;51290;12,40;21038;104682;11,30;39533;193417;7,40;167359;797170;5,60;325505;1616411;4,50;0;0;5;779,00;91;23,20;190;11,20;2951;1,80;4620;3,10;17022;3,90;31011;10,00;71799;2,40;695595;2,20;OX 399 4093;; +22087;MARDI;01/11/2022;11;31/01/2023;42;48;19;37;47;6;1;-19-37-42-47-48-;-1-6-;0;0;0;0;2;370898,30;2;8;21671,20;5;25;2160,00;113;626;158,90;265;1347;78,00;267;1439;51,30;4341;22587;16,30;5332;28884;14,20;12774;70570;10,80;25976;126246;7,30;86787;446372;6,50;203623;1075611;4,30;0;0;1;2431,40;53;25,00;105;12,30;1926;1,70;2353;3,80;11418;3,60;20904;9,30;37741;2,90;407337;2,30;AE 147 7153;; +22086;VENDREDI;28/10/2022;10;27/01/2023;28;35;21;16;14;11;1;-14-16-21-28-35-;-1-11-;0;0;0;1;1;948590,30;2;16;13856,30;13;73;945,90;388;2319;54,80;403;1966;68,40;1195;7431;12,70;5293;27405;17,20;9083;45029;11,70;25831;124632;7,80;28271;144772;8,20;125489;618311;6,00;341520;1628382;3,70;0;0;6;541,70;157;11,40;190;9,30;2319;1,90;3926;3,00;12795;4,30;21487;12,10;54651;2,60;473486;2,70;HB 963 3673;; +22085;MARDI;25/10/2022;9;24/01/2023;24;7;46;34;39;6;1;-7-24-34-39-46-;-1-6-;0;0;0;0;2;357879,70;1;5;33456,90;8;32;1628,20;102;643;149,20;307;1407;72,10;249;1534;46,40;4544;22103;16,10;5899;29347;13,50;13869;72763;10,10;26703;122624;7,30;94646;453110;6,20;223075;1095415;4,10;0;0;2;1239,40;47;29,30;139;9,60;1969;1,70;2586;3,50;11606;3,60;20179;9,80;41164;2,70;402337;2,40;EQ 472 6266;; +22084;VENDREDI;21/10/2022;8;20/01/2023;26;28;38;25;40;1;6;-25-26-28-38-40-;-1-6-;0;0;0;0;2;427941,30;1;11;18184,80;9;35;1780,10;182;829;138,40;338;1731;70,00;378;1865;45,70;5644;26235;16,20;7725;36432;13,00;17540;85805;10,30;32775;146703;7,30;121242;558724;6,00;278717;1324150;4,10;0;0;2;1549,00;80;21,20;140;12,00;2427;1,70;3352;3,30;13912;3,80;24544;10,10;52180;2,60;500513;2,40;QY 962 4703;; +22083;MARDI;18/10/2022;7;17/01/2023;42;3;5;48;13;6;1;-3-5-13-42-48-;-1-6-;0;0;0;1;4;155132,30;1;5;29005,50;7;31;1457,10;133;595;139,80;295;1372;64,10;329;1504;41,10;4731;21352;14,40;6798;30673;11,20;16312;73614;8,70;24005;108701;7,10;101912;451977;5,40;233147;1064813;3,70;1;1243,30;4;248,60;64;19,40;138;8,90;2012;1,50;2912;2,80;10118;3,80;16616;10,70;44292;2,20;344473;2,50;JK 323 2687;; +22082;VENDREDI;14/10/2022;6;13/01/2023;28;48;29;24;34;7;3;-24-28-29-34-48-;-3-7-;0;0;0;1;3;261311,70;1;4;45804,60;10;47;1214,20;158;684;153,60;452;2196;50,60;293;1505;51,80;8601;37816;10,30;6987;33592;12,90;13410;69182;11,70;54488;233505;4,20;116869;542207;5,70;223033;1099185;4,50;1;1560,10;4;312,00;74;20,80;197;7,60;3455;1,10;3156;3,20;22163;2,10;38185;5,90;51124;2,40;510275;2,10;VE 527 2435;; +22081;MARDI;11/10/2022;5;10/01/2023;40;35;8;32;23;8;1;-8-23-32-35-40-;-1-8-;0;0;0;0;1;592272,50;0;5;27684,70;2;27;1596,80;136;677;117,30;235;1308;64,10;310;1654;35,60;3856;18717;15,70;5495;28746;11,40;12453;64639;9,40;21819;102097;7,20;78820;395850;5,90;178518;900105;4,10;0;0;1;2116,70;64;18,00;104;10,80;1762;1,60;2357;3,30;9604;3,80;16125;10,50;35013;2,70;335695;2,50;KX 470 3150;; +22080;VENDREDI;07/10/2022;4;06/01/2023;50;27;45;26;42;12;1;-26-27-42-45-50-;-1-12-;0;0;0;0;1;758017,10;2;7;25308,70;6;28;1970,70;89;610;166,60;249;1304;82,40;270;1399;53,90;4329;21194;17,80;5118;26962;15,60;13379;69411;11,20;24033;114346;8,30;83471;419972;7,10;234395;1151977;4,10;0;0;1;2724,80;49;30,20;116;12,90;2119;1,70;2395;4,10;11450;4,10;18978;11,50;38255;3,20;386633;2,80;TA 666 0275;; +22079;MARDI;04/10/2022;3;03/01/2023;3;28;18;43;42;12;3;-3-18-28-42-43-;-3-12-;0;0;0;2;6;92436,50;0;8;16202,90;9;34;1187,40;124;563;132,10;231;1135;69,20;300;1400;39,40;3342;16195;17,00;5660;26024;11,80;13267;61054;9,30;17636;83444;8,30;81925;379440;5,70;192022;888552;3,90;0;0;6;340,10;57;19,50;132;8,50;1708;1,60;2684;2,70;8860;3,90;13928;11,70;38060;2,40;312267;2,60;JR 131 2171;; +22078;VENDREDI;30/09/2022;2;30/12/2022;2;16;11;1;26;3;12;-1-2-11-16-26-;-3-12-;0;0;0;0;4;179820,00;2;8;21013,40;6;35;1496,00;231;939;102,70;310;1410;72,30;636;2396;29,90;4334;20078;17,80;8751;37307;10,70;24245;98004;7,50;21727;104135;8,60;114092;506261;5,60;297915;1280166;3,50;0;1484,10;2;593,60;106;13,80;146;10,10;2200;1,60;3925;2,50;10943;4,20;17712;12,00;51658;2,30;408654;2,60;FY 915 3671;; +22077;MARDI;27/09/2022;1;27/12/2022;34;4;44;20;21;3;1;-4-20-21-34-44-;-1-3-;0;0;0;0;4;128443,80;0;3;40025,90;7;28;1335,70;84;493;139,70;218;1039;70,10;236;1145;44,60;3394;16185;15,80;4841;23499;12,10;11199;54838;9,60;19405;90109;7,10;77307;357150;5,60;171076;796179;4,10;0;0;4;484,20;32;33,60;86;11,90;1532;1,70;2205;3,20;8577;3,80;14526;10,60;34857;2,50;318114;2,40;TC 019 1387;; +22076;VENDREDI;23/09/2022;6;23/12/2022;22;35;48;15;14;8;3;-14-15-22-35-48-;-3-8-;0;1;193007524,00;1;8;144591,60;3;7;38621,00;12;78;1079,50;242;1223;126,80;655;3125;52,40;544;2570;44,80;9921;46012;12,50;12232;55468;11,50;25415;116597;10,20;55727;251145;5,70;187224;826670;5,50;390148;1738164;4,20;1;2387,90;5;318,30;92;25,40;252;9,30;4088;1,40;5122;3,00;22939;3,20;38907;8,80;78557;2,40;729345;2,30;HI 522 2752;; +22075;MARDI;20/09/2022;5;20/12/2022;21;11;32;48;23;12;3;-11-21-23-32-48-;-3-12-;0;0;0;1;7;122581,80;1;11;18231,40;6;41;1523,50;211;999;115,10;333;1771;68,60;500;2396;35,60;5297;25261;16,90;9278;43062;11,00;22136;103568;8,50;28087;130620;8,20;131738;605372;5,50;322377;1453014;3,70;1;1752,60;0;0;81;38,90;143;11,70;2394;1,80;3985;2,80;12592;4,30;21198;11,80;56371;2,50;483054;2,50;PM 917 4088;; +22074;VENDREDI;16/09/2022;4;16/12/2022;49;10;45;36;27;3;4;-10-27-36-45-49-;-3-4-;0;0;0;2;6;178373,60;1;7;35733,30;7;38;2050,20;266;1121;128,00;417;2009;75,50;525;2347;45,40;6748;31691;16,80;10918;50031;11,80;22315;105337;10,50;38047;177657;7,50;162896;733690;5,70;339750;1541747;4,40;0;0;1;3893,30;110;19,30;178;12,00;2816;1,80;4687;3,00;16018;4,10;27511;11,30;69160;2,50;682570;2,20;FD 829 8683;; +22073;MARDI;13/09/2022;3;13/12/2022;40;9;12;15;47;1;11;-9-12-15-40-47-;-1-11-;0;0;0;0;2;415209,70;3;9;21564,70;7;31;1950,00;163;725;153,50;335;1694;69,40;444;1813;45,60;5001;23218;17,80;7886;35047;13,10;21433;91240;9,40;27462;126596;8,20;121033;529536;6,10;323683;1375378;3,80;0;0;4;624,50;78;21,90;163;10,40;2221;1,90;3386;3,30;12386;4,30;20633;12,10;52197;2,70;450279;2,70;DP 921 0829;; +22072;VENDREDI;09/09/2022;2;09/12/2022;26;17;23;24;27;4;9;-17-23-24-26-27-;-4-9-;0;0;0;1;7;164877,80;3;11;24522,00;22;71;1183,30;427;1547;100,00;985;3846;42,50;879;3427;33,50;14080;57158;10,00;17574;65480;9,70;40954;147134;8,10;70526;282824;5,10;240334;935906;4,80;532970;2053887;3,50;1;2722,70;7;311,10;189;14,30;415;6,40;5918;1,10;7334;2,40;29349;2,80;42235;9,20;100812;2,10;745650;2,60;VD 907 1255;; +22071;MARDI;06/09/2022;1;06/12/2022;10;44;7;29;22;4;5;-7-10-22-29-44-;-4-5-;0;0;0;2;8;66901,20;3;8;15635,90;12;38;1025,30;218;916;78,30;345;1529;49,60;520;1868;28,50;4505;20167;13,20;9264;36811;8,00;18721;73191;7,50;21409;98756;6,70;107193;465267;4,50;222101;933795;3,60;1;1143,30;6;152,40;97;11,70;138;8,10;1929;1,40;4009;1,80;9479;3,70;14467;11,40;46940;1,90;323055;2,50;KB 963 1015;; +22070;VENDREDI;02/09/2022;10;02/12/2022;7;45;12;20;13;3;12;-7-12-13-20-45-;-3-12-;0;1;128491607,00;1;5;193666,80;3;17;13312,60;7;40;1762,20;301;1227;105,80;474;2083;65,90;736;2875;33,50;6434;29649;16,20;13322;55173;9,70;31916;129049;7,70;31445;150508;8,00;172867;747913;5,10;418831;1778737;3,40;0;0;4;859,40;125;15,10;203;9,20;2986;1,50;5782;2,10;14618;4,00;21112;13,00;74488;2,00;493086;2,70;VE 788 1169;; +22069;MARDI;30/08/2022;9;29/11/2022;6;10;4;15;19;1;4;-4-6-10-15-19-;-1-4-;0;0;0;3;6;117055,20;6;18;9119,20;16;44;1161,90;447;1507;62,40;568;2278;43,70;781;2787;25,10;6842;28293;12,30;12643;48513;8,00;26028;102550;7,00;29252;128023;6,80;137790;574500;4,80;301262;1287400;3,40;1;1419,20;6;189,20;183;7,50;232;6,00;2817;1,20;5171;1,80;12016;3,60;16996;12,00;57498;2,00;367543;2,70;RJ 754 0942;; +22068;VENDREDI;26/08/2022;8;25/11/2022;22;23;44;25;38;12;11;-22-23-25-38-44-;-11-12-;0;0;0;1;6;143631,30;2;9;22379,30;4;36;1742,60;157;761;151,80;329;1559;78,30;496;2228;38,50;4303;22218;19,30;7363;36246;13,20;21948;103764;8,50;23069;117951;9,10;109787;526132;6,40;329167;1503262;3,60;1;1715,60;2;686,20;60;28,10;164;10,20;2001;2,10;3339;3,30;10731;4,90;17566;14,00;49451;2,80;420158;2,90;DV 256 7501;; +22067;MARDI;23/08/2022;7;22/11/2022;48;39;26;19;31;2;3;-19-26-31-39-48-;-2-3-;0;0;0;0;0;0;1;5;152669,50;5;22;2047,30;154;688;120,50;241;1150;76,20;276;1299;47,40;3716;18375;16,70;6016;29118;11,80;12157;59930;10,60;22272;106314;7,20;96562;444562;5,40;188411;893378;4,40;0;0;3;749,90;71;17,60;120;10,30;1670;1,80;2607;3,10;9653;4,00;18065;9,90;41757;2,40;407461;2,20;OY 818 0342;; +20222066;VENDREDI;19/08/2022;;19/10/2022;9;2;19;38;13;2;6;-2-9-13-19-38-;-2-6-;0;0;0;3;8;95084;4;11;16161,9;8;58;954,7;267;1092;93.4;756;2913;37;456;2104;36;9262;38526;9.8;11992;48895;8,6;22555;97525;8;41005;176398;5.4;147978;638876;4,6;292426;1328725;3,6;1;1471.3;4;294.2;121;12.1;346;4,2;3762;0,9;5084;1,9;16992;2,7;23194;9,2;63696;1,9;419753;1.9;GZ 976 4317;; +22065;MARDI;16/08/2022;5;15/11/2022;10;25;14;39;32;7;8;-10-14-25-32-39-;-7-8-;0;0;0;0;1;553832,80;2;8;16179,90;8;32;1259,90;148;688;107,90;252;1283;61,10;265;1353;40,70;3870;18294;15,00;6132;29745;10,30;11352;56424;10,10;21368;98087;7,00;92457;432022;5,00;168527;825623;4,20;0;0;4;497,80;75;14,70;91;12,10;1666;1,60;2655;2,70;9266;3,70;15620;10,20;40601;2,20;367805;2,10;UR 407 6925;; +22064;VENDREDI;12/08/2022;4;11/11/2022;14;42;35;17;34;10;6;-14-17-34-35-42-;-6-10-;0;0;0;0;1;734594,70;1;5;34337,30;7;33;1620,40;159;779;126,40;360;1605;64,80;386;1913;38,20;4620;22802;16,00;7142;33419;12,20;16262;80193;9,40;24239;119960;7,60;99359;471237;6,10;234712;1146192;4,00;0;0;3;881,60;62;23,30;157;9,20;2189;1,60;3246;2,90;11127;4,10;19710;10,70;44655;2,60;424562;2,40;OT 421 0975;; +22063;MARDI;09/08/2022;3;08/11/2022;47;18;21;19;27;11;5;-18-19-21-27-47-;-5-11-;0;0;0;1;1;548278,90;2;5;25628,30;2;22;1814,20;126;553;132,90;269;1356;57,30;257;1267;43,10;3899;19501;14,00;5993;28461;10,70;14566;64345;8,80;19448;99093;6,90;90261;419668;5,10;214506;943205;3,60;0;0;0;0;62;50,10;118;9,40;1742;1,60;2712;2,70;9054;3,90;14058;11,70;40138;2,30;310112;2,60;BE 416 8336;; +22062;VENDREDI;05/08/2022;2;04/11/2022;29;3;44;33;35;10;8;-3-29-33-35-44-;-8-10-;0;0;0;1;3;232396,30;0;5;32588,90;11;43;1180,30;117;594;157,30;294;1343;73,50;302;1444;48,00;4189;19596;17,70;6366;29962;12,90;15893;72826;9,90;23751;106863;8,10;94569;449494;6,10;229610;1086210;4,00;1;1445,20;6;192,70;65;22,20;147;9,60;1970;1,80;2867;3,30;11330;3,90;19339;10,70;43041;2,70;408538;2,50;QG 617 5022;; +22061;MARDI;02/08/2022;1;01/11/2022;28;48;18;40;7;11;8;-7-18-28-40-48-;-8-11-;0;0;0;1;4;128508,60;0;3;40046,10;2;28;1336,40;151;624;110,40;253;1233;59,10;339;1374;37,20;3342;16037;15,90;5905;27347;10,40;13342;58837;9,00;16890;82240;7,80;78375;358935;5,60;190180;834701;3,90;1;1100,10;2;440,00;61;18,00;123;8,80;1626;1,60;2640;2,70;8228;4,10;13351;11,90;36388;2,40;300660;2,60;EZ 401 9100;; +22060;VENDREDI;29/07/2022;3;28/10/2022;38;48;43;23;3;8;3;-3-23-38-43-48-;-3-8-;0;1;41717625,00;1;3;247760,30;2;9;19301,80;12;52;1040,50;205;886;112,40;504;2530;41,60;373;1674;44,20;6845;32761;11,30;8009;39072;10,50;15437;73325;10,40;38177;173838;5,30;116167;528289;5,50;229756;1053160;4,40;0;0;6;478,50;87;17,70;207;7,60;2867;1,30;3520;2,90;16091;3,00;26184;8,80;51731;2,50;490155;2,30;TZ 884 3683;; +22059;MARDI;26/07/2022;2;25/10/2022;3;5;17;27;25;9;1;-3-5-17-25-27-;-1-9-;0;0;0;0;5;109395,70;6;16;7989,80;10;43;926,00;254;1018;72,00;515;2020;38,30;583;2530;21,50;5557;24340;11,10;8896;38350;7,90;21608;90657;6,20;25123;108805;6,20;105623;466657;4,60;253288;1093742;3,10;0;0;5;420,20;116;9,90;214;5,40;2320;1,20;3881;1,90;10611;3,40;16025;10,50;46238;2,00;304593;2,70;RB 934 8551;; +22058;VENDREDI;22/07/2022;1;21/10/2022;32;29;50;18;16;11;4;-16-18-29-32-50-;-4-11-;0;0;0;0;0;0;1;9;112283,50;4;32;1863,40;150;779;141,00;296;1719;67,50;362;1847;44,10;4514;25769;15,80;6758;36344;12,50;16317;84710;10,00;24179;133912;7,60;100636;532663;6,00;246727;1262564;4,10;0;0;1;2838,80;72;21,60;130;12,00;2081;1,80;3131;3,30;11510;4,20;19327;11,70;45787;2,80;438660;2,50;CB 290 6960;; +22057;MARDI;19/07/2022;11;18/10/2022;41;6;27;40;23;2;12;-6-23-27-40-41-;-2-12-;0;1;230000000,00;3;5;4413400,80;3;10;30174,50;12;67;1402,70;310;1276;135,60;614;2639;69,30;644;3133;41,00;8822;37647;17,00;13375;58277;12,30;32455;142338;9,30;47021;199892;8,00;201806;857757;5,90;487454;2091175;3,90;1;2744,40;4;548,80;146;18,70;270;9,90;3789;1,70;5644;3,10;20444;4,10;34194;11,40;85332;2,60;751459;2,60;CB 011 6703;; +22056;VENDREDI;15/07/2022;10;14/10/2022;38;17;29;9;39;10;7;-9-17-29-38-39-;-7-10-;0;0;0;2;14;1884992,90;2;14;25775,50;33;107;1050,40;555;2251;91,90;1222;4163;52,50;1146;4949;31,00;14629;54892;14,00;21266;86203;9,90;44554;191410;8,30;74465;282715;6,80;280850;1140157;5,30;594492;2538124;3,80;1;3308,10;12;220,50;211;15,40;505;6,40;6110;1,30;8524;2,50;31310;3,20;50967;9,30;116097;2,30;981315;2,40;NY 259 7166;; +22055;MARDI;12/07/2022;9;11/10/2022;50;18;25;24;14;11;6;-14-18-24-25-50-;-6-11-;0;0;0;1;5;4569745,70;1;10;31243,40;10;47;2070,50;302;1314;136,40;745;2889;65,50;755;2986;44,50;10812;43803;15,20;14845;60640;12,20;34972;143449;9,60;60630;237936;7,00;225559;914593;5,70;537751;2152649;3,90;1;2985,70;2;1194,20;127;23,10;315;9,40;4748;1,50;6243;3,10;25794;3,50;41912;10,20;94392;2,50;826941;2,50;NL 693 1158;; +22054;VENDREDI;08/07/2022;8;07/10/2022;28;46;43;35;31;7;4;-28-31-35-43-46-;-4-7-;0;0;0;3;9;1453438,00;6;19;19952,40;17;90;1311,90;442;1566;138,80;1006;3945;58,20;861;3424;47,10;14612;59724;13,50;16818;69278;13,00;34655;149123;11,20;86337;345994;5,80;249636;1051572;6,00;509009;2217981;4,60;1;3461,50;9;307,60;171;19,70;413;8,10;5760;1,40;7045;3,20;34127;3,10;65330;7,60;104050;2,60;1128939;2,10;PK 211 8941;; +22053;MARDI;05/07/2022;7;04/10/2022;7;25;48;45;10;3;6;-7-10-25-45-48-;-3-6-;0;0;0;3;8;147139,80;3;6;45852,00;9;61;1404,70;320;1318;119,70;754;3044;54,80;692;2911;40,20;11830;46292;12,60;14442;59701;10,90;31836;129049;9,40;63346;247828;5,90;209827;873761;5,30;447540;1869390;4,00;1;2426,90;7;277,30;131;18,10;291;8,20;4795;1,20;5998;2,60;25695;2,90;42468;8,10;86230;2,20;719656;2,40;BA 534 6535;; +22052;VENDREDI;01/07/2022;;30/09/2022;46;6;34;24;18;12;3;-6-18-24-34-46-;-3-12-;0;0;0;0;4;318058,60;7;18;16519,00;11;80;1157,60;328;1386;123,00;582;2460;73,30;810;3286;38,50;8765;36778;17,20;14003;59403;11,80;33774;140590;9,30;44765;192169;8,20;202097;841034;5,90;486170;1998110;4,00;0;0;5;976,70;128;20,80;250;10,70;3870;1,70;5892;3,00;19415;4,30;33081;11,70;84663;2,60;768821;2,50;FI 641 7829;; +20222051;MARDI ;28/06/2022;;28/08/2022;35;10;47;42;48;11;2;-10-35-42-47-48-;-2-11-;0;0;0;0;5;184886,3;1;6;36009,1;5;32;2102,9;184;816;151,9;374;1674;78,2;402;1737;53;6391;27324;16,8;7879;35380;14,5;18173;81596;11,7;37438;158205;7,3;124324;549223;6,6;292778;1272807;4,6;0;0;1;3401,3;70;26,9;162;11,6;2637;1,7;3338;3,7;15742;3,7;28046;9,8;52726;2,9;558162;2,4;NU 014 6678;; +20222050;VENDREDI;24/06/2022;;24/08/2022;50;44;17;10;28;12;8;-10-17-28-44-50-;-8-12-;0;0;0;1;2;540484,2;2;9;28071,1;12;55;1430,7;308;1264;114,6;539;2286;67;810;3134;34,3;7144;31353;17,1;13452;55816;10,7;32739;131618;8,4;37178;160498;8,4;178270;749465;5,6;453131;1841469;3,7;0;0;4;1020,6;131;17,3;216;10,5;3073;1,8;5570;2,7;16526;4,3;26269;12,6;74701;2,5;607971;2,7;PH 612 8966;; +20222049;MARDI ;21/06/2022;;21/08/2022;23;36;39;6;47;10;8;-6-23-36-39-47-;-8-10-;0;0;0;0;3;283193,5;2;8;24820,1;9;38;1627,5;207;919;123,9;361;1721;69,9;473;2078;40,7;5678;24873;17;7964;37137;12,7;19173;86514;10,1;31922;135871;7,8;120550;538912;6,2;286708;1270534;4,2;0;0;1;3106,8;75;23;162;10,6;2388;1,8;3377;3,4;13530;4;23734;10,6;50108;2,8;501302;2,5;SZ 459 8109;; +20222048;VENDREDI;17/06/2022;;17/08/2022;21;22;38;19;31;7;11;-19-21-22-31-38-;-7-11-;0;0;0;0;1;1165998;4;17;16030,1;12;64;1326,2;333;1257;124,3;752;3032;54,5;621;2568;45,2;10492;42883;13,5;14964;59242;10,9;32503;128636;9,3;54301;222710;6,5;222215;872854;5,2;476312;1904166;3,8;0;0;2;2295,4;136;18,7;338;7,5;4426;1,4;6035;2,8;23009;3,4;36626;10,1;91677;2,2;756169;2,4;UU 153 5701;; +20222047;MARDI ;14/06/2022;;14/08/2022;7;40;2;27;34;3;11;-2-7-27-34-40-;-3-11-;0;0;0;1;4;122154,3;3;8;14274,7;4;21;1693,8;96;499;131,3;235;1110;62,4;311;1176;41,3;3401;15940;15,2;4811;23068;11,7;11892;55124;9,1;17408;83711;7,3;72863;346130;5,5;174448;795376;3,9;0;0;1;1768,9;35;28;105;9,3;1560;1,5;2194;3;8050;3,8;13071;10,9;32717;2,4;279449;2,5;RC 341 4915;; +20222046;VENDREDI;10/06/2022;;10/08/2022;37;26;40;17;36;9;12;-17-26-36-37-40-;-9-12-;0;1;64382899;1;1;728597,5;1;4;42571,3;7;29;1828,9;121;646;151,2;221;1253;82,4;322;1776;40,8;3404;18083;20;5671;29222;13,8;13928;71030;10,6;18997;99015;9,2;86499;437591;6,5;224221;1088946;4,2;0;0;3;845,7;45;31,3;119;11,8;1601;2,2;2560;3,6;8941;4,9;15809;13;38633;3;402432;2,5;PM 456 8559;; +20222045;MARDI ;07/06/2022;;07/08/2022;17;6;31;25;16;2;6;-6-16-17-25-31-;-2-6-;0;0;0;0;5;106815,1;2;12;10401,8;12;47;827,2;208;878;81,5;467;2118;35,7;343;1575;33,7;5716;25851;10,2;7051;32464;9,1;13754;64639;8,5;27836;124247;5,3;93527;431683;4,8;185313;884753;3,8;0;0;4;454,7;90;11,2;192;5,2;2360;1;2908;2,3;11551;2,7;17013;8,6;39534;2,1;297390;2,4;AR 511 7829;; +20222044;VENDREDI;03/06/2022;;03/08/2022;29;12;48;28;34;9;11;-12-28-29-34-48-;-9-11-;0;0;0;0;2;347955,6;1;3;54215,3;4;23;2202,6;107;563;165,7;263;1322;74,6;330;1452;47,7;4055;19667;17,6;6372;30360;12,7;15989;71744;10;21267;106808;8,1;98825;459619;5,9;246993;1086312;4;0;0;1;2476,7;40;34,3;128;10,7;1855;1,8;2853;3,2;9814;4,4;15841;12,6;43328;2,6;381813;2,6;IN 604 0060;; +20222043;MARDI ;31/05/2022;;31/07/2022;42;13;10;49;3;3;9;-3-10-13-42-49-;-3-9-;0;0;0;2;4;128700,6;0;2;60158,9;19;52;720,6;147;690;100;437;2030;35,9;250;1179;43,4;6455;29855;8,5;6838;30324;9,4;12619;56635;9,4;28566;132089;4,8;92546;425458;4,7;179549;820737;3,9;1;991,3;6;132,1;69;14,3;181;5,4;2772;0,8;2982;2,2;12326;2,5;17255;8,3;40132;2;286017;2,5;TO 602 3361;; +20222042;VENDREDI;27/05/2022;;27/07/2022;15;34;28;48;23;3;5;-15-23-28-34-48-;-3-5-;0;0;0;1;4;160252,9;3;9;16646,1;7;41;1138,1;141;737;116,6;375;1955;46,4;293;1421;44,9;5924;29864;10,6;6581;33243;10,7;12845;63461;10,4;32792;159182;5;99879;490862;5,1;196838;945421;4,3;0;0;3;732,2;55;22,1;153;7,9;2288;1,3;2683;3;13217;2,9;22520;7,9;41970;2,3;384144;2,3;GV 654 9838;; +20222041;MARDI ;24/05/2022;;24/07/2022;27;13;10;36;37;10;2;-10-13-27-36-37-;-2-10-;1;1;50656220;1;1;537399,7;0;3;41866,3;6;34;1150,6;108;541;133,2;282;1277;59,6;278;1316;40,6;4322;18138;14,7;5697;25446;11,7;13455;60944;9,1;22007;93118;7,2;86746;375666;5,6;201973;898612;3,8;1;1084,8;4;216,9;48;22,6;120;9;1929;1,4;2461;2,9;9767;3,4;14642;10,8;37545;2,3;300472;2,6;TG 800 0822;; +20222040;VENDREDI;20/05/2022;;20/07/2022;32;23;8;22;50;3;9;-8-22-23-32-50-;-3-9-;0;0;0;1;3;235008,1;0;6;27462,6;8;51;1006,3;136;749;126,2;418;2042;48,9;294;1549;45,3;6846;32205;10,9;7110;35345;11;15895;76630;9,5;38516;170398;5,1;113750;530839;5,2;247170;1128168;3,9;0;0;2;1284,2;57;25;168;8,4;2819;1,2;3002;3,1;15940;2,8;27057;7,6;48228;2,4;431666;2,4;SN 615 5728;; +20222039;MARDI ;17/05/2022;;17/07/2022;49;28;30;5;48;5;11;-5-28-30-48-49-;-5-11-;0;0;0;1;2;257215,2;1;2;60115,4;1;19;1970,9;84;447;154,3;191;1070;68,1;207;924;55,4;3356;16535;15,4;4855;22610;12,6;10660;48320;11;17735;90264;7,1;75113;347142;5,8;171668;756950;4,3;1;1052,2;1;841,7;41;25,6;78;13,4;1522;1,7;2199;3,2;8083;4,1;13097;11,7;33579;2,5;290248;2,6;KE 358 1334;; +20222038;VENDREDI;13/05/2022;;13/07/2022;11;45;17;48;3;8;4;-3-11-17-45-48-;-4-8-;0;0;0;3;5;160032;2;8;23376,3;15;57;1021,9;217;766;140;657;2190;51,7;541;1846;43,1;9542;32898;12,1;11382;38410;11,5;25955;88536;9,3;50946;173927;5,7;168822;573450;5,5;386060;1298894;3,9;0;0;7;554;95;22,6;283;7,6;3946;1,3;4998;2,8;21686;3,1;34232;9,1;72952;2,4;620010;2,5;FY 242 0121;; +20222037;MARDI ;10/05/2022;;10/07/2022;28;3;29;25;27;4;9;-3-25-27-28-29-;-4-9-;0;1;215840341;4;6;211426,5;2;11;26953;23;71;1300,6;419;1534;110,8;948;3546;50,7;908;3287;38,4;14514;56158;11,2;17173;65036;10,8;39227;144748;9;78810;305303;5,2;252573;946218;5,2;572525;2100825;3,8;3;979,2;10;235;181;16,2;391;7,5;5703;1,2;7125;2,7;31021;2,9;46502;9,2;103586;2,3;818102;2,6;OL 151 7591;; +20222036;VENDREDI;06/05/2022;;06/07/2022;3;24;8;18;40;11;3;-3-8-18-24-40-;-3-11-;0;0;0;1;5;265989,5;2;16;19426,9;14;89;1087,8;394;1691;105,4;867;3394;55,5;1035;3801;34,8;12426;46528;14,2;19296;73930;9,9;44914;165872;8,2;60944;234573;7,1;259432;989228;5,3;595321;2237066;3,7;0;0;4;1352,6;160;18,7;353;8,5;5217;1,4;7995;2,5;25746;3,6;40812;10,7;108150;2,2;839414;2,6;UZ 615 4334;; +20222035;MARDI ;03/05/2022;;03/07/2022;26;48;47;8;20;8;3;-8-20-26-47-48-;-3-8-;0;0;0;3;4;238540,9;1;5;44600,7;16;47;1477,8;232;892;143,4;610;2459;55;428;1852;51,3;9465;38194;12,4;11128;43316;12,2;23085;90377;10,9;50611;208019;5,7;169634;664875;5,6;351079;1383542;4,3;2;1034,6;6;275,9;96;21,5;248;8,3;3799;1,3;4585;3;20344;3,2;33146;9,1;69144;2,4;632661;2,3;HZ 505 8065;; +20222034;VENDREDI;29/04/2022;;29/06/2022;37;36;10;20;11;7;3;-10-11-20-36-37-;-3-7-;0;0;0;1;1;1222554,5;1;9;31747,9;22;75;1186,6;262;1100;149;1059;4020;43,1;525;2513;48,4;18124;68076;8,9;14725;57714;11,7;29169;119581;10,5;99566;374753;4;233357;900439;5,3;456474;1835388;4,2;0;0;7;684,7;127;20,9;429;6,2;7082;0,9;6102;2,9;38268;2,1;58884;6,6;95776;2,2;829597;2,3;NO 190 7720;; +20222033;MARDI ;26/04/2022;;26/06/2022;25;4;28;34;45;11;8;-4-25-28-34-45-;-8-11-;0;0;0;0;3;280770,6;2;5;39372,4;5;36;1703,2;170;797;141,7;395;1693;70,5;442;1753;47,8;5843;25263;16,6;8746;36848;12,6;20379;84377;10,3;31760;136157;7,7;133408;560118;5,9;320243;1301549;4,1;0;0;2;1597,9;76;23,3;194;9,1;2480;1,7;3601;3,3;13307;4,2;22023;11,7;55470;2,6;500133;2,5;RB 806 6218;; +20222032;VENDREDI;22/04/2022;;22/06/2022;11;6;35;36;21;9;1;-6-11-21-35-36-;-1-9-;0;0;0;1;5;195342;0;9;25363,6;16;56;1269,6;194;992;132;570;2289;60,4;464;2257;43,1;8388;34473;14,1;10462;45706;11,8;24068;105901;9,5;45119;184360;6,6;158116;674804;5,7;366273;1559220;3,9;0;0;8;444,6;89;22,2;231;8,5;3317;1,4;4339;3;17267;3,6;28613;10;64513;2,5;557133;2,5;BP 152 3003;; +20222031;MARDI ;19/04/2022;;19/06/2022;28;46;2;17;12;10;6;-2-12-17-28-46-;-6-10-;0;0;0;0;2;324441;2;5;30330,8;5;45;1049,7;204;770;113;399;1615;56,9;496;1906;33,9;5293;21749;14,8;8615;33018;10,9;21210;83185;8;26231;110648;7,3;114537;455485;5,6;278743;1142793;3,6;0;0;4;580,9;80;16,1;167;7,7;2218;1,4;3505;2,4;10743;3,7;17149;10,9;46933;2,2;356474;2,6;OX 396 6026;; +20222030;VENDREDI;15/04/2022;;15/06/2022;32;48;6;24;30;6;5;-6-24-30-32-48-;-5-6-;0;0;0;0;1;781552,4;2;8;22832,7;9;45;1264,3;215;928;112,9;375;1908;58;402;1754;44,3;5426;26410;14,7;8643;39470;11;16743;77433;10,4;29311;140945;6,9;126797;572191;5,3;244079;1122181;4,4;0;0;4;682;73;20,7;167;9;2290;1,6;3586;2,8;12058;3,9;19556;11,3;51977;2,4;480069;2,2;IL 682 7870;; +20222029;MARDI ;12/04/2022;;12/06/2022;30;47;35;31;21;10;2;-21-30-31-35-47-;-2-10-;0;0;0;0;1;596185,7;1;4;34834,6;5;19;2284,2;71;470;170,1;221;1116;75,7;206;1075;55,2;3532;17129;17,3;4277;23046;14,3;10673;55100;11,1;20771;96434;7,7;73209;364947;6,4;174803;873641;4,3;0;0;2;995,7;27;40,9;102;10,8;1527;1,8;1850;4;8817;3,9;16308;9,9;31256;2,9;331597;2,4;QA 116 8543;; +20222028;VENDREDI;08/04/2022;;08/06/2022;16;38;1;45;15;4;11;-1-15-16-38-45-;-4-11-;0;0;0;0;0;0;2;5;186499,3;7;34;1618,3;143;610;166,1;275;1447;74;306;1456;51,7;4064;21524;17,4;5954;29759;14,1;15006;72027;10,8;23247;119901;7,8;96909;471896;6,3;244301;1146720;4,1;0;0;2;1277,3;71;19,9;134;10,5;1851;1,9;2614;3,6;10264;4,3;17665;11,7;41146;2,8;394442;2,6;HW 474 9862;; +20222027;MARDI ;05/04/2022;;05/06/2022;13;1;24;11;49;5;6;-1-11-13-24-49-;-5-6-;0;0;0;0;4;138494,1;2;8;16184,1;8;42;960,1;217;825;90;321;1567;50,1;403;1651;33,4;4119;20735;13,3;7898;34174;9;15853;69799;8,2;19613;101715;6,8;100273;461263;4,7;200811;926680;3,7;0;0;4;453,4;86;11,7;127;7,9;1660;1,5;3263;2;8216;3,8;12377;11,8;41800;1,9;302012;2,4;SN 422 9124;; +20222026;VENDREDI;01/04/2022;;01/06/2022;21;45;31;2;7;10;4;-2-7-21-31-45-;-4-10-;0;0;0;0;2;366112,2;2;9;19014,7;6;37;1440,6;184;810;121,2;364;1607;64,5;434;2085;34,9;5429;22986;15,8;7831;35148;11,5;20862;91275;8,2;27753;118816;7,7;113649;503421;5,7;291351;1277295;3,6;0;0;2;1314,1;82;17,8;153;9,5;2365;1,5;3318;2,9;11926;3,8;19500;10,9;47926;2,5;397897;2,6;GN 645 0195;; +20222025;MARDI ;29/03/2022;;29/05/2022;43;33;11;8;42;11;6;-8-11-33-42-43-;-6-11-;0;0;0;0;5;101294,9;2;7;16910,2;4;17;2168,8;129;515;131,8;243;1021;70,3;281;1190;42,3;3590;16431;15,3;4596;22684;12,4;10677;53313;9,8;17530;86320;7,3;69210;338438;5,9;166486;807430;3,9;0;0;2;833,5;40;23,1;101;9,1;1564;1,4;2002;3;7816;3,7;12313;10,9;29780;2,5;253594;2,6;PH 399 1406;; +20222024;VENDREDI;25/03/2022;;25/05/2022;10;29;24;6;27;12;5;-6-10-24-27-29-;-5-12-;0;0;0;1;1;710352,4;6;15;11068;7;30;1723,7;236;1033;92,2;274;1519;66,2;585;2571;27,5;3994;21028;16,8;8765;40218;9,8;22830;99544;7,3;19289;105464;8,4;108137;529199;5,2;284718;1295523;3,4;0;0;2;1120,4;107;11,6;130;9,5;1806;1,7;3591;2,3;8815;4,4;13808;13,1;45343;2,2;324213;2,7;AR 411 2683;; +20222023;MARDI ;22/03/2022;;22/05/2022;7;29;17;40;21;8;11;-7-17-21-29-40-;-8-11-;0;0;0;1;3;154557,3;2;12;9030,6;4;22;1534,2;123;629;98,8;211;1078;60,9;327;1634;28,2;2876;14982;15,4;5613;26797;9,6;14106;66812;7,1;13963;72714;7,9;71919;352656;5,1;184901;870083;3,3;1;873,4;0;0;59;26,6;81;10,7;1267;1,7;2444;2,3;6263;4,3;9692;13,1;30732;2,3;233162;2,7;JF 838 0760;; +20222022;VENDREDI;18/03/2022;;18/05/2022;43;1;9;14;17;12;6;-1-9-14-17-43-;-6-12-;1;2;26825597;0;2;355494,7;0;4;41542,4;7;41;1262,3;169;706;135;339;1475;68,3;399;1877;37,7;4678;20381;17,3;7517;33136;11,9;20886;89281;8,2;23431;106380;8,3;105062;473322;5,9;283275;1262098;3,5;0;0;3;793,9;71;18,6;163;8,1;2108;1,5;3176;2,7;10729;3,8;17172;11,2;44570;2,4;348239;2,7;HI 537 1808;; +20222021;MARDI ;15/03/2022;;15/05/2022;2;26;19;12;1;8;2;-1-2-12-19-26-;-2-8-;0;0;0;0;0;0;4;8;79899,7;24;87;433,5;288;1051;66,1;679;2705;27,1;474;1858;27,7;7213;30929;8,3;9387;36928;7,7;17349;72137;7,4;29691;138426;4,6;102448;442888;4,6;205463;907842;3,6;0;0;11;157,9;124;7,7;269;3,5;2953;0,8;3877;1,6;12444;2,4;17135;8,2;42848;1,8;268993;2,6;JM 780 6574;; +20222020;VENDREDI;11/03/2022;;11/05/2022;23;22;12;19;48;6;2;-12-19-22-23-48-;-2-6-;0;0;0;1;1;684094,8;3;10;15988,4;10;50;996;224;822;111,6;621;2509;38,6;364;1741;39,1;8247;34390;9,9;9315;39716;9,5;18808;82995;8,5;37214;161325;5,3;131541;571093;4,7;262439;1180456;3,6;1;1304,7;3;347,9;91;14,3;240;5,4;3197;1;3846;2,2;14853;2,7;21198;8,9;54384;1,9;373673;2,5;HZ 865 0175;; +20222019;MARDI ;08/03/2022;;08/05/2022;18;12;31;25;21;9;2;-12-18-21-25-31-;-2-9-;0;0;0;1;4;122344,2;6;16;7148,4;18;46;774,4;190;825;79,5;389;1651;42;405;1715;28,4;4857;21459;11,3;7365;31873;8,5;15252;65731;7,7;22675;104736;5,8;91184;409157;4,7;187210;848061;3,6;0;0;6;280,8;70;13,3;173;5,4;1979;1,1;2983;2,1;9514;3;14391;9,4;38253;2;273823;2,4;UV 964 9515;; +20222018;VENDREDI;04/03/2022;;04/05/2022;40;20;43;6;42;10;12;-6-20-40-42-43-;-10-12-;0;1;78587957;1;2;394609,6;2;6;30742,2;3;29;1981,1;106;605;174,9;228;1220;91,7;316;1487;52,8;3896;19512;20,1;5531;28323;15,4;15272;74154;11;21057;105583;9,3;87334;426633;7,3;253773;1184843;4,2;1;1489,3;1;1191,4;43;34,6;106;14;1754;2,1;2536;3,9;9554;4,9;15785;13,7;39059;3,1;376593;2,8;SN 586 7936;; +20222017;MARDI ;01/03/2022;;01/05/2022;26;24;42;39;8;5;3;-8-24-26-39-42-;-3-5-;0;0;0;2;5;112506,9;1;11;11952,1;6;46;890,2;134;723;104,3;330;1742;45,7;332;1497;37,4;5146;24968;11,2;6004;29894;10,4;12278;59918;9,7;26896;132283;5,3;85142;422388;5,2;174070;842682;4,2;1;1034,1;3;275,7;51;20,2;129;8;2103;1,2;2557;2,7;10700;3;18454;8,1;35238;2,4;321589;2,3;MC 274 5810;; +20222016;VENDREDI;25/02/2022;;27/04/2022;24;10;30;42;13;1;5;-10-13-24-30-42-;-1-5-;0;0;0;1;4;184664,5;2;6;28772,7;6;39;1378,7;242;1070;92,5;488;2060;50,8;505;2232;32,9;6432;28930;12,7;9403;43199;9,4;21278;93110;8,2;31024;141194;6,5;126669;582386;5;281585;1257206;3,7;1;1384,8;3;369,2;80;17,3;179;7,7;2523;1,3;3741;2,4;12285;3,5;18913;10,6;51696;2,2;386002;2,6;MP 933 7534;; +20222015;MARDI ;22/02/2022;;24/04/2022;26;32;14;5;9;7;3;-5-9-14-26-32-;-3-7-;0;0;0;0;3;183996,6;1;3;43003;15;77;521,8;187;799;92,6;603;2694;29;326;1490;36,9;7705;35158;7,8;7526;33685;9,1;14191;65678;8,6;38485;174133;3,9;101375;457147;4,7;188738;889711;3,9;0;0;9;205,5;84;12,2;242;4,2;3075;0,8;3061;2,2;15468;2;24247;6,1;41962;2;318972;2,3;KH 634 1248;; +20222014;VENDREDI;18/02/2022;;20/04/2022;25;41;50;43;38;6;10;-25-38-41-43-50-;-6-10-;0;0;0;0;2;349382,3;1;5;32662,5;7;31;1640,9;115;533;175,8;239;1351;73,3;278;1396;49,8;3849;19585;17,7;5434;27049;14,3;12681;64231;11,2;22261;109089;8;83054;403027;6,8;192691;962264;4,6;0;0;1;2444,5;57;23,8;102;13,3;1707;1,9;2356;3,8;9846;4,3;18634;10,6;36020;3,1;414531;2,3;DI 121 0916;; +20222013;MARDI ;15/02/2022;;17/04/2022;33;20;31;9;44;12;2;-9-20-31-33-44-;-2-12-;0;0;0;1;2;248591,9;2;9;12911,1;2;22;1645,1;99;464;143,6;200;979;71,9;281;1394;35,5;2762;13358;18,5;4615;21957;12,5;11945;56956;9;14864;69371;8,9;67697;318971;6,1;174248;814913;3,8;0;0;1;1727,6;43;22,3;98;9,7;1313;1,8;2025;3,1;6973;4,3;11351;12,3;29786;2,6;263647;2,6;OO 039 8041;; +20222012;VENDREDI;11/02/2022;;13/04/2022;45;13;42;18;38;7;11;-13-18-38-42-45-;-7-11-;0;1;30928078;0;3;242348,5;2;7;24274,6;10;33;1603,8;165;746;130,6;360;1627;63,3;415;1720;42,1;5580;24950;14,5;7954;34601;11,6;16584;75151;10;29040;133275;6,8;117948;511434;5,6;248519;1115721;4,1;0;0;8;326,5;80;18,1;154;9,4;2459;1,4;3341;2,9;13005;3,5;21429;9,8;50893;2,3;445597;2,3;FY 859 0923;; +20222011;MARDI ;08/02/2022;;10/04/2022;43;34;20;6;37;7;10;-6-20-34-37-43-;-7-10-;0;0;0;0;3;172300,8;0;1;120808,6;6;19;1980,4;106;495;140;244;1002;73,1;214;1039;49,5;3627;14890;17,2;4559;20518;13,9;9813;46707;11,4;20984;86129;7,5;71159;322187;6,3;152556;718892;4,5;0;0;0;0;47;60,7;106;9,6;1669;1,5;1961;3,4;9155;3,5;16341;9,1;31176;2,7;324878;2,2;UB 190 2954;; +20222010;VENDREDI;04/02/2022;;06/04/2022;49;38;3;25;43;7;3;-3-25-38-43-49-;-3-7-;0;1;130000000;2;9;151235,5;3;11;28919,6;22;83;1193,7;326;1325;137,7;1113;4334;44,5;698;2796;48,4;18252;68027;9,9;15998;63476;11,9;31710;128892;10,9;102684;378974;4,4;248804;951638;5,6;482677;1915122;4,5;0;0;5;1113,1;138;22,4;447;6,9;7312;1;6716;3;40576;2,4;67468;6,6;102555;2,4;1007100;2,2;JT 631 4310;; +20222009;MARDI ;01/02/2022;;03/04/2022;49;36;1;19;38;9;6;-1-19-36-38-49-;-6-9-;0;0;0;1;2;301800,6;0;2;70535,7;5;29;1515,1;102;524;154,4;281;1442;59,3;228;1195;50,3;4543;21962;13,6;5389;25874;12,9;11269;55604;11,2;25495;124262;6;83612;400084;5,9;178784;861844;4,4;1;1119,2;4;223,8;47;23,8;128;8,7;1983;1,4;2312;3,2;10708;3,2;18782;8,7;35680;2,5;345114;2,3;NV 546 2186;; +20222008;VENDREDI;28/01/2022;;30/03/2022;34;10;25;29;45;10;9;-10-25-29-34-45-;-9-10-;0;0;0;0;2;385053,2;4;8;22498,3;5;26;2156,2;139;728;141,8;251;1332;81,9;328;1664;46,1;4106;20463;18,7;6918;34148;12,5;16026;77190;10,3;21569;107585;8,9;109826;514161;5,9;256248;1184365;4,1;0;0;2;1324,2;60;24,5;119;12,3;1749;2,1;2903;3,3;9521;4,8;15656;13,7;46378;2,6;418687;2,5;OO 207 7237;; +20222007;MARDI ;25/01/2022;;27/03/2022;16;6;47;18;39;2;4;-6-16-18-39-47-;-2-4-;0;0;0;0;2;272591,2;1;5;25483,6;2;20;1984,3;103;521;140,3;222;1197;64,5;221;1193;45,5;3936;18475;14,6;5195;24959;12,1;11153;53056;10,6;22542;103012;6,6;82772;379068;5,6;171949;806852;4,2;0;0;2;947;51;20,6;97;10,8;1656;1,5;2242;3,1;9269;3,5;15913;9,6;35025;2,4;326278;2,3;IN 364 8676;; +20222006;VENDREDI;21/01/2022;;23/03/2022;5;35;39;14;36;12;7;-5-14-35-36-39-;-7-12-;0;0;0;0;3;455155,7;4;14;22795,1;14;59;1684,7;332;1208;151,5;773;2442;79,2;820;2897;46,9;11700;38943;17,4;16247;56740;13,3;37132;131061;10,7;65809;215662;7,9;259974;879290;6,1;585053;2027241;4,2;0;0;3;2038,4;151;22,4;304;11,1;4788;1,7;6532;3,4;27218;3,9;47325;10,4;104960;2,6;1042335;2,3;PB 999 6610;F NWH 71157,F THD 16573,F HWD 28167,F VHM 94680,F TRH 67340,F KBX 42574,F HNS 76765,F CPV 01912,F PBP 12550,F JRZ 35853,F STP 64314,F QTJ 67362,F PXH 79858,F QMX 42431,F FWV 25413,F BJB 32849,F TLZ 26610,F CCZ 64000,F SHF 31374,F NJK 79627,F JTV 00190,F GNQ 50037,F JJD 37158,F NCN 99152,F GVZ 67432,F CBM 92592,F JJV 71787,F SDV 60006,E BBP 27831,E BDD 99803,E BBB 51860,E BCG 54031,E BFB 27852,E BDB 48546,E BFW 57864,E BBF 26358,E BDK 36121,E BFD 53629,E BFD 88478,E BBD 70418,E BBJ 34796,E BFR 73408,E BGD 84310,E BDB 62398,E BCH 27984,E BFZ 49430,Z TRM 14877,T VSG 00783,M VRZ 26827,X VSG 84995,V TRL 01585,Z TRR 94586,V TRL 87162,Z VSM 26552,J VSM 73525,T VSG 76136,J TRT 58673,V VRZ 30685,J VRZ 11907,H VSD 27040,H VSF 61310,H VSP 90761,T VSH 82930,Z QQJ 90798,X VSL 70214,H VSM 17788,M TRT 79385,T VSD 23735,T VRZ 55779,H VSK 04713,B HNT 35440,B HNV 30470,B HNQ 96583,B HMR 32763,B HPV 32773,B HNJ 64417,B HNK 86655,B HNL 54610,I MDZ 91577,I MFF 48547,I MFC 21006,P FPW 21873,P FLS 32079,P FCK 17673,P FLS 22482,P FFW 22456,P FFM 27813,P FFN 07949,P FKK 29281,P FMB 24858,P FDB 27796,R LSH 43275,R LSN 57742,R LSM 59586,R LSP 76359,S BBT 76008,S BBV 14622,N QCC 05383,W JQH 00203,O QBN 39384,; +20222005;MARDI ;18/01/2022;;20/03/2022;12;30;24;3;19;5;2;-3-12-19-24-30-;-2-5-;1;1;67441797;0;7;86794,8;6;13;10922,9;16;80;552,8;321;1255;64,9;570;2755;31,2;623;2525;23,9;6447;30831;9,8;10545;45913;7,3;21068;90734;6,9;28815;141146;5,3;116281;541879;4,4;237314;1081853;3,5;0;0;8;243,2;122;8,8;210;5,1;2571;1;4064;1,7;11254;3;16670;9,4;46894;1,9;311578;2,5;LN 606 2999;; +20222004;VENDREDI;14/01/2022;;16/03/2022;25;6;46;17;31;12;9;-6-17-25-31-46-;-9-12-;0;0;0;1;7;108903,2;2;11;16197;9;33;1681,6;168;770;132,7;239;1386;77,9;425;2062;36,8;3942;20404;18,6;7265;35096;12;18736;88805;8,8;20299;105704;9;100255;495606;6;265620;1256717;3,8;1;1377,3;4;275,4;67;20,5;112;12,2;1756;1,9;2943;3,1;9211;4,7;15197;13,2;42150;2,6;369624;2,7;GK 405 4997;; +20222003;MARDI ;11/01/2022;;13/03/2022;33;23;44;32;4;12;9;-4-23-32-33-44-;-9-12-;0;0;0;1;4;140479,6;1;7;18761,3;3;24;1704,4;94;601;125,3;168;1011;78,7;288;1613;34,7;2578;14452;19,3;4955;27126;11,5;13402;71603;8,1;14105;76170;9,2;69971;370274;5,9;187749;949409;3,7;0;0;0;0;40;70,9;77;13,1;1174;2,1;2142;3,1;6370;5;11125;13,2;30011;2,7;276907;2,6;FO 166 2052;; +20222002;VENDREDI;07/01/2022;;09/03/2022;14;2;50;37;44;7;11;-2-14-37-44-50-;-7-11-;0;0;0;1;4;175313,9;0;3;54631,6;6;35;1458,5;153;697;134,9;332;1521;65,3;323;1496;46,6;4518;22200;15,7;6654;30889;12,6;14463;71317;10,1;25900;123601;7,1;98821;457480;6;213720;1021956;4,3;1;1334,6;0;0;57;42,1;147;9;1983;1,6;2808;3,1;11147;3,7;19769;9,8;41683;2,6;419221;2,3;PL 507 0535;; +20222001;MARDI ;04/01/2022;;06/03/2022;28;41;18;35;36;6;11;-18-28-35-36-41-;-6-11-;0;0;0;0;0;0;0;4;153095,5;6;15;2408,9;98;441;150,9;170;914;76,9;250;986;50,1;2969;14142;17,4;3916;19243;14,3;9334;44666;11,4;16306;78161;7,9;59817;294292;6,6;143080;700245;4,5;0;0;1;1667,7;41;22,5;83;11,1;1325;1,7;1724;3,6;7155;4;13017;10,3;25394;3;271258;2,4;LN 456 9467;; +20211105;VENDREDI;31/12/2021;;02/03/2022;25;7;43;22;49;7;6;-7-22-25-43-49-;-6-7-;1;1;17571602;5;13;70556,7;0;14;15312,3;14;63;1059,8;238;1422;86,4;522;2488;52,2;411;2843;32,1;7132;33903;13,4;11128;52251;9,7;18524;97175;9,7;37113;175632;6,5;158994;729226;4,9;272706;1384192;4,2;2;807,2;6;215,2;92;17,5;223;7,2;2934;1,3;4594;2,3;15276;3,3;23584;9,9;65095;2;524611;2,2;JA 583 3740;; +20211104;MARDI ;28/12/2021;;27/02/2022;4;29;15;17;6;9;12;-4-6-15-17-29-;-9-12-;1;1;79411191;1;5;123889,4;3;20;7238,7;7;29;1554,9;182;855;97,1;253;1266;69,3;571;2372;26;3252;17231;17,9;7278;33956;10,1;20102;90446;7;16501;87730;8,8;91443;447127;5,4;247858;1159648;3,3;0;0;5;385,3;66;16,2;109;9,8;1453;1,8;2949;2,4;7011;4,8;11642;13,4;37984;2,3;280160;2,7;OU 447 1179;; +20211103;VENDREDI;24/12/2021;;23/02/2022;41;2;34;26;28;10;5;-2-26-28-34-41-;-5-10-;0;0;0;0;2;437555,9;1;7;29218,2;6;32;1990,7;152;722;162,5;337;1653;75;350;1652;52,7;5518;27096;16;7675;35790;13,5;17823;82458;10,9;31416;151724;7,2;121498;560096;6,1;285387;1282198;4,3;0;0;2;1497,5;60;27,7;145;11,4;2298;1,8;3134;3,5;13080;4;22650;10,7;49530;2,7;483044;2,5;MP 004 6365;; +20211102;MARDI ;21/12/2021;;20/02/2022;34;38;17;12;21;2;3;-12-17-21-34-38-;-2-3-;0;0;0;2;9;65400,7;1;12;11463,9;13;42;1020,2;199;931;84,7;331;1477;56,4;348;1810;32,3;4371;20595;14,2;8146;37634;8,6;14678;70096;8,6;21717;104598;7;107916;501404;4,6;199726;956524;3,9;1;1059,4;8;105,9;77;13,7;142;7,4;1829;1,4;3237;2,1;9136;3,6;13984;11;43405;2;327888;2,3;JG 205 1897;; +20211101;VENDREDI;17/12/2021;;16/02/2022;30;1;35;2;15;2;7;-1-2-15-30-35-;-2-7-;0;0;0;0;0;0;1;5;181673,9;12;48;1116,6;154;751;131,4;511;2196;47,5;252;1409;52;8505;34116;10,7;7593;35775;11,4;14388;70036;10,8;47987;186957;4,9;123156;547917;5,3;229778;1072072;4,3;0;0;5;505,9;64;21,9;218;6,4;3465;1;3162;2,9;19148;2,3;30133;6,8;50554;2,2;453640;2,2;RB 281 7994;; +20211100;MARDI ;14/12/2021;;13/02/2022;37;19;47;21;48;3;6;-19-21-37-47-48-;-3-6-;0;0;0;1;2;268761,2;1;5;25125,5;4;28;1397,4;119;535;134,7;299;1278;59,6;202;1122;47,7;4640;20117;13,3;4880;23986;12,4;10288;50934;10,9;27218;115410;5,8;77932;372695;5,6;161657;779628;4,3;0;0;2;924,6;48;21,4;116;8,8;1952;1,3;2051;3,3;11085;2,9;19923;7,5;33066;2,5;324962;2,2;BW 078 6554;; +20211099;VENDREDI;10/12/2021;;09/02/2022;11;21;42;38;33;8;2;-11-21-33-38-42-;-2-8-;0;0;0;0;1;702913,6;2;9;18253,6;11;51;1003,3;183;858;109,8;531;2416;41,2;388;1868;37,4;7916;34309;10,2;8054;36195;10,7;17411;78448;9,2;41368;177263;4,9;117705;511808;5,4;253415;1114785;4;0;0;6;431,3;75;19,1;206;6,9;3225;1,1;3344;2,8;16656;2,7;28720;7,3;48640;2,4;429739;2,4;GF 813 6344;; +20211098;MARDI ;07/12/2021;;06/02/2022;22;31;46;47;38;7;11;-22-31-38-46-47-;-7-11-;0;1;143469842;0;2;418527,2;5;8;24454,1;2;38;1603,5;163;744;150,8;340;1666;71,2;348;1596;52,2;5668;26098;15,9;7946;36010;12,9;16582;77524;11,1;31700;147002;7,1;117279;534527;6,1;249494;1156075;4,6;0;0;1;2871,2;60;26,5;147;10,8;2209;1,8;3227;3,3;12917;3,8;23821;9,7;47713;2,7;511775;2,2;LT 173 6325;; +20211097;VENDREDI;03/12/2021;;02/02/2022;46;21;32;22;29;9;10;-21-22-29-32-46-;-9-10-;0;0;0;1;6;197329,2;4;9;30746;8;45;1915,3;232;1029;154,2;476;2095;80,1;537;2401;49,1;7086;31282;18,8;11894;50869;12,9;27540;116931;10,4;38830;168029;8,8;185932;774721;6;438556;1816954;4,1;1;2498,9;3;666,3;102;24,4;198;12,6;3070;2;4990;3,3;16621;4,7;27933;13;76764;2,6;718197;2,5;CX 763 3302;; +20211096;MARDI ;30/11/2021;;30/01/2022;43;19;33;26;20;4;1;-19-20-26-33-43-;-1-4-;0;0;0;0;1;523711,8;0;3;40800;5;23;1657,5;105;493;142,4;212;1049;70,7;234;1105;47,2;3247;16002;16,3;4910;23029;12,6;11199;53637;10,1;18284;86639;7,5;74404;352475;5,8;172381;826573;4;0;0;1;1749,2;40;24,2;96;10,1;1346;1,8;2087;3,1;7620;4;12417;11,4;31426;2,5;277013;2,5;EY 114 1489;; +20211095;VENDREDI;26/11/2021;;26/01/2022;50;20;41;42;1;3;7;-1-20-41-42-50-;-3-7-;1;1;162289050;0;7;152675,6;1;7;35682,8;8;42;1852,3;194;927;154,6;489;2410;62,8;423;2039;52,2;9317;43207;12,3;8509;43094;13,7;18835;95975;11,5;62513;279019;4,7;142841;684207;6,1;295521;1461450;4,6;0;0;2;1693,9;66;28,5;188;10;3567;1,3;3452;3,6;23298;2,5;46179;5,9;57372;2,7;628123;2,1;JA 859 0214;; +20211094;MARDI ;23/11/2021;;23/01/2022;21;17;42;36;46;10;7;-17-21-36-42-46-;-7-10-;0;0;0;0;2;397120,9;1;8;23203,4;4;40;1445,4;158;831;128,1;352;1796;62,6;322;1819;43,4;5402;25664;15,4;7114;36160;12,2;15243;78528;10,4;31485;141863;7;108984;534870;5,8;231620;1170306;4,3;0;0;1;2476,4;71;19,3;149;9,2;2123;1,6;2725;3,3;12578;3,4;22791;8,8;42838;2,6;431208;2,3;NW 865 5429;; +20211093;VENDREDI;19/11/2021;;19/01/2022;50;26;47;22;38;6;2;-22-26-38-47-50-;-2-6-;0;0;0;2;2;493896,8;2;5;46172,7;6;49;1467,5;192;858;154,3;540;2424;57,7;426;2266;43,4;8227;36558;13,4;9209;43673;12,5;19565;95105;10,7;47478;205647;6;139493;644602;6;291946;1404040;4,4;0;0;3;1098,9;81;22,6;220;8,3;3150;1,4;3607;3,4;18113;3,1;32757;8,1;55544;2,7;590000;2,2;GX 143 6592;; +20211092;MARDI ;16/11/2021;;16/01/2022;9;32;36;19;21;4;3;-9-19-21-32-36-;-3-4-;0;0;0;1;4;180375,1;1;9;18736,2;6;31;1694,2;177;924;104,7;320;1727;59,2;365;1826;39,3;4650;24055;14,9;8031;40494;9,8;15948;80527;9,2;24812;125935;7,1;118470;572426;4,9;237147;1150170;3,9;0;0;2;1136,8;56;22,5;125;10,1;1892;1,6;3152;2,6;9664;4,1;15603;11,8;46525;2,2;385664;2,3;EW 964 8565;; +20211091;VENDREDI;12/11/2021;;12/01/2022;50;15;9;47;2;9;6;-2-9-15-47-50-;-6-9-;0;0;0;1;3;294755,3;4;10;20666,7;11;38;1693,9;173;879;134,9;540;2252;55,6;365;1786;49,3;7857;36380;12,1;9034;41559;11,8;18783;89086;10,2;41381;190793;5,8;138826;631438;5,5;290844;1347420;4,1;0;0;7;415,8;62;26;232;6,9;3080;1,3;3671;2,9;16169;3,1;25611;9,2;55437;2,4;487142;2,4;MC 718 7149;; +20211090;MARDI ;09/11/2021;;09/01/2022;33;25;36;26;17;8;1;-17-25-26-33-36-;-1-8-;0;0;0;0;1;651028,5;6;8;19019,5;7;23;2060,5;129;588;148,4;323;1431;64,4;322;1475;43,9;4359;20724;15,6;6182;29969;12;14800;72476;9,2;24586;112801;7,2;93406;444087;5,7;218716;1047359;3,9;0;0;3;724,6;60;20,1;110;10,9;1768;1,7;2479;3,2;9950;3,8;16943;10,4;38243;2,6;351578;2,4;IM 785 5464;; +20211089;VENDREDI;05/11/2021;;05/01/2022;41;25;2;12;30;11;8;-2-12-25-30-41-;-8-11-;0;0;0;0;5;161374,4;1;11;17143,5;6;30;1957,9;177;885;122,2;328;1654;69,1;441;2206;36,4;4786;23785;16,8;8278;38610;11,6;21046;94863;8,7;25611;126840;7,9;117189;548191;5,8;297917;1327463;3,8;0;0;2;1342,6;86;17,3;137;10,8;2029;1,8;3404;2,9;10904;4,3;18430;11,8;48065;2,5;414931;2,6;RP 633 2218;; +20211088;MARDI ;02/11/2021;;02/01/2022;29;43;2;3;17;4;1;-2-3-17-29-43-;-1-4-;0;0;0;1;2;286677,1;3;9;14889,1;3;23;1814,7;136;666;115,4;255;1306;62,2;317;1511;37,7;3726;18106;15,7;5886;28511;11,1;14057;67606;8,7;19726;94282;7,6;86781;412990;5,4;203390;969695;3,7;1;1034,5;1;827,6;51;20,2;98;10,5;1540;1,6;2501;2,7;8109;4;12870;11,7;35345;2,4;290679;2,5;KI 802 3707;; +20211087;VENDREDI;29/10/2021;;29/12/2021;28;10;32;15;49;9;3;-10-15-28-32-49-;-3-9-;0;0;0;1;5;155676,2;2;6;30320;13;54;1049,3;190;868;120,2;494;2259;48,8;363;1762;44;7503;33767;11,4;8388;38760;11,1;17205;80265;10;41479;187159;5,2;123200;567616;5,4;260428;1184940;4,1;1;1466,6;5;234,6;84;17,4;190;7,7;2918;1,2;3485;2,8;16777;2,7;27383;7,8;49956;2,4;439797;2,4;UF 931 7777;; +20211086;MARDI ;26/10/2021;;26/12/2021;12;44;42;15;20;2;1;-12-15-20-42-44-;-1-2-;0;0;0;0;3;181891,5;1;6;21255,5;8;23;1727,1;125;745;98,2;237;1115;69,3;273;1232;44,1;3351;16605;16,3;5781;26132;11,6;12487;58287;9,6;17671;87663;7,7;87589;394546;5,4;188884;884029;3,9;0;0;5;357,7;44;22,5;110;9;1400;1,7;2521;2,6;7316;4,2;11656;12,4;36301;2,2;288249;2,5;CP 970 9549;; +20211085;VENDREDI;22/10/2021;;22/12/2021;33;12;17;39;22;2;12;-12-17-22-33-39-;-2-12-;0;0;0;0;2;359277,2;1;12;13994,8;5;26;2011,8;147;865;111,3;341;1471;69,2;453;2286;31,3;4541;20875;17,1;7910;36139;11;20514;96218;7,7;22601;102778;8,7;112399;505361;5,6;285810;1286325;3,5;0;0;1;2442,9;52;26,1;150;9;2014;1,6;3243;2,8;10228;4,1;15198;13;47021;2,3;361595;2,7;QZ 850 6716;; +20211084;MARDI ;19/10/2021;;19/12/2021;40;4;36;41;20;1;6;-4-20-36-40-41-;-1-6-;0;0;0;0;4;135573,4;2;5;25348,5;10;27;1462,1;88;447;162,6;203;981;78,3;198;996;54,2;3362;15725;17,1;4149;20587;14,6;9620;47669;11,7;19826;90618;7,4;70045;327664;6,5;156319;750694;4,5;0;0;5;380;36;29,3;87;12,1;1377;1,9;1802;3,9;8123;4;15476;9,9;29494;2,9;325349;2,3;FL 466 0828;; +20211083;VENDREDI;15/10/2021;;15/12/2021;49;21;26;34;31;5;2;-21-26-31-34-49-;-2-5-;1;1;220000000;1;7;4608445,7;3;13;33931,7;21;97;1416,4;369;1630;155,2;993;4413;60,6;773;3579;52,5;15516;68202;13,7;18662;82993;12,6;39959;178081;10,9;89948;384869;6,1;296467;1290335;5,7;632118;2730160;4,3;1;3711,1;9;329,8;133;27,9;384;9,6;5828;1,5;7244;3,4;33656;3,4;60227;8,9;115108;2,6;1160332;2,3;UL 815 5842;; +20211082;MARDI ;12/10/2021;;12/12/2021;13;45;22;6;49;10;11;-6-13-22-45-49-;-10-11-;0;0;0;2;9;1315699,9;6;25;14703,3;21;78;1467,8;418;1567;134,5;680;3035;73,4;1066;3971;39,4;9797;44764;17,5;16821;70710;12,3;44422;178170;9,1;52240;237961;8,2;244587;1020988;6;639533;2584226;3,8;0;0;8;680,1;167;18,1;274;11;3845;1,9;6586;3;20761;4,5;33252;13,2;95765;2,6;806623;2,7;HI 479 6198;; +20211081;VENDREDI;08/10/2021;;08/12/2021;10;42;23;46;1;3;5;-1-10-23-42-46-;-3-5-;0;0;0;1;9;166984,9;7;23;15271,4;18;78;1402,6;360;1547;130,2;1026;4027;52,9;785;3194;46,8;15355;62333;12;17928;72563;11,5;38760;151970;10,2;84600;343913;5,4;271879;1094972;5,4;570294;2257847;4,2;0;0;8;670,1;158;18,8;375;7,9;5636;1,3;6753;2,9;30529;3;50978;8,5;102872;2,3;906709;2,3;BH 058 0251;; +20211080;MARDI ;05/10/2021;;05/12/2021;45;14;36;13;11;7;9;-11-13-14-36-45-;-7-9-;0;0;0;0;1;1033822,9;0;4;60405,3;20;61;1233,7;221;978;141,7;790;2802;52,3;472;2054;50,1;12128;44193;11,6;12392;50287;11,4;24393;100508;10,6;64809;235077;5,5;199504;783281;5,2;389964;1560164;4,2;0;0;7;539,2;94;22,3;309;6,7;4663;1,1;4758;2,9;24705;2,6;37571;8,1;76161;2,2;642513;2,3;NM 905 6314;; +20211079;VENDREDI;01/10/2021;;01/12/2021;1;19;15;48;26;6;12;-1-15-19-26-48-;-6-12-;0;0;0;2;7;174231,4;1;16;17815,3;19;54;1644,1;326;1267;129;653;2540;68;774;3003;40,4;9376;36320;16,7;13636;54580;12,4;34530;137241;9,1;48807;190019;8;199506;798724;6;504198;2008099;3,8;1;2417;8;241,7;122;19,8;276;8,7;3772;1,6;5447;2,9;19949;3,8;32964;10,7;78093;2,5;661919;2,6;JN 932 7034;; +20211078;MARDI ;28/09/2021;;28/12/2021;12;7;6;33;39;7;1;-6-7-12-33-39-;-1-7-;0;0;0;1;4;225472,9;1;1;210786,9;11;47;1396,9;232;964;125,4;666;2467;51,8;464;2137;42;10404;37666;11,9;12491;49107;10,2;24650;103133;9;51260;188980;5,9;185226;725287;4,9;370606;1518558;3,7;1;1727,6;4;345,5;83;20,8;255;6,7;3869;1,1;4792;2,4;19443;2,7;26263;9,6;70367;2;494709;2,5;NX 614 4402;; +20211077;VENDREDI;24/09/2021;;24/12/2021;27;12;20;2;41;10;12;-2-12-20-27-41-;-10-12-;0;0;0;1;5;259151,2;4;13;23295,3;7;51;1849,5;354;1327;130,9;573;2540;72,3;1088;3929;32,8;8529;36399;17,7;14623;58376;12,3;43486;165561;8;42739;187120;8,6;195539;804602;6,3;580154;2298981;3,5;1;2452,8;3;654;155;15,8;233;10,5;3512;1,7;5801;2,8;17260;4,4;27380;13;77399;2,6;615413;2,8;FB 984 4292;; +20211076;MARDI ;21/09/2021;;21/12/2021;20;25;26;30;38;8;2;-20-25-26-30-38-;-2-8-;0;0;0;1;2;274869,5;0;3;42827,6;5;30;1333,9;116;621;118,7;356;1692;46;231;1168;46,8;5024;24791;11;4915;26134;11,6;10537;56056;10,1;27838;136014;5;76441;388839;5,5;163015;842501;4,1;0;0;0;0;50;50,6;146;6,1;1939;1,1;2003;3;11005;2,5;18780;7;30636;2,4;272152;2,4;BS 245 9803;; +20211075;VENDREDI;17/09/2021;;17/12/2021;10;7;5;8;34;9;1;-5-7-8-10-34-;-1-9-;0;0;0;1;2;348150,6;4;13;12518,2;17;62;817,5;232;887;105,2;567;2384;41,4;486;2032;34,1;6865;29898;11,5;8970;40330;9,5;22221;96460;7,4;30640;141575;6,1;119531;551567;4,9;281249;1291010;3,4;1;1138,9;9;101,2;96;11,8;214;5,3;2411;1,1;3460;2,2;11189;3,2;15606;10,6;45991;2;300103;2,7;EG 935 7027;; +20211074;MARDI ;14/09/2021;;14/12/2021;12;38;45;18;35;2;8;-12-18-35-38-45-;-2-8-;0;0;0;0;3;164473,4;1;1;115320,4;10;24;1496,6;108;475;139,2;286;1384;50,5;184;1019;48,2;4343;21603;11,3;4441;22064;12,4;9254;47353;10,7;24622;119659;5,1;67621;334955;5,8;143515;719187;4,3;0;0;2;752,6;54;15,4;104;8;1720;1,2;1820;3;9828;2,6;17467;6,9;27222;2,5;253824;2,3;ST 166 4887;; +20211073;VENDREDI;10/09/2021;;10/12/2021;6;7;28;18;1;2;8;-1-6-7-18-28-;-2-8-;0;0;0;2;5;127306,6;4;16;9298;19;86;538,8;278;1028;83;712;2859;31,5;612;2343;27;8426;36393;8,7;9666;40988;8,6;22143;92523;7,1;35940;163738;4,8;119706;531205;4,7;260660;1163970;3,4;1;1076,4;7;123;112;9,6;255;4,2;3242;0,8;3737;1,9;13716;2,4;19565;8;46657;1,9;298352;2,6;SX 431 4676;; +20211072;MARDI ;07/09/2021;;07/12/2021;42;7;19;35;43;7;9;-7-19-35-42-43-;-7-9-;0;1;26572332;0;3;166558,5;2;7;16683,2;9;41;887,1;133;654;102,4;387;1719;41,2;242;1271;39,1;5384;24562;10,1;5398;27743;10;9636;50390;10,2;26789;121431;5,1;78982;396556;4,9;139722;727146;4,3;0;0;6;250,7;47;17,7;156;5,3;2154;0,9;2194;2,5;10440;2,5;15934;7,6;31863;2,1;265072;2,2;GK 745 4753;; +20211071;VENDREDI;03/09/2021;;03/12/2021;5;13;29;35;7;3;7;-5-7-13-29-35-;-3-7-;0;0;0;2;12;54980,5;2;7;22028,3;25;114;421,3;336;1349;65,5;1059;4933;18,9;472;2062;31,8;12380;57297;5,7;12011;52470;6,9;18561;86583;7,8;48893;228457;3,6;137477;630803;4,1;230477;1128625;3,7;2;546,9;6;145,8;114;9,5;408;2,6;4498;0,6;4543;1,6;18083;1,9;23121;6,9;53880;1,6;326328;2,4;KM 548 4193;; +20211070;MARDI ;31/08/2021;;30/11/2021;17;5;32;13;43;2;10;-5-13-17-32-43-;-2-10-;1;3;21214762;0;2;275986,4;3;12;10750,4;7;32;1255,6;141;720;102,8;263;1348;58;357;1859;29,5;3856;18459;14,8;5898;29147;10,5;14488;73331;7,7;18734;91842;7,5;78831;398312;5,4;191765;980564;3,5;0;0;1;1646,8;53;17,2;100;9,1;1628;1,4;2320;2,6;7906;3,6;12315;10,8;31615;2,3;251228;2,6;RK 713 6808;; +20211069;VENDREDI;27/08/2021;;26/11/2021;31;46;3;50;2;12;8;-2-3-31-46-50-;-8-12-;0;0;0;0;0;0;0;6;146580,9;11;29;1789,4;75;514;185,9;199;1212;83,3;207;1287;55,1;3039;18015;19,7;4551;26816;14,7;10811;64454;11,4;17307;98442;9;74628;420407;6,6;191865;1043071;4,3;0;0;3;693,1;37;31,2;81;14,2;1362;2,1;1955;3,9;7562;4,8;13113;12,8;31379;3;318783;2,6;BY 136 1121;; +20211068;MARDI ;24/08/2021;;23/11/2021;21;36;44;19;50;3;10;-19-21-36-44-50-;-3-10-;0;0;0;0;1;512584,1;0;4;29949,8;2;14;2665,3;90;513;133,9;196;1088;66,7;198;1101;46,3;2899;15634;16,3;4619;28240;10;9755;56666;9,3;16815;87604;7,3;60492;337024;6;141403;771144;4,2;0;0;1;1530,5;39;21,8;78;10,9;1266;1,6;1940;2,9;7162;3,7;12709;9,7;25357;2,7;249562;2,4;VU 587 4053;; +20211067;VENDREDI;20/08/2021;;19/11/2021;3;19;9;20;23;11;9;-3-9-19-20-23-;-9-11-;0;0;0;2;6;109566,6;1;12;12803,7;6;42;1139,4;328;1309;67,3;397;1844;50,5;672;2801;23,3;4283;23380;14;9975;46025;7,9;23939;106470;6,3;18670;106081;7,7;113383;554276;4,6;280585;1305852;3,1;0;0;3;650,7;133;8,1;160;6,7;1800;1,5;3900;1,8;7888;4,3;11517;13,7;45214;1,9;276227;2,8;VM 238 8638;; +20211066;MARDI ;17/08/2021;;16/11/2021;47;12;41;42;31;6;4;-12-31-41-42-47-;-4-6-;0;0;0;0;1;478210,5;1;6;18627,6;0;17;2047,7;59;396;161,9;213;1062;63,8;140;879;54,1;3686;16616;14,3;3500;19110;13,9;7493;40811;12,1;22087;97577;6,1;58108;298416;6,3;125272;655080;4,6;0;0;0;0;25;90,8;82;9,8;1436;1,4;1405;3,8;8762;2,9;15687;7,5;23514;2,8;251049;2,3;CX 053 7204;; +20211065;VENDREDI;13/08/2021;;12/11/2021;49;44;47;12;6;8;12;-6-12-44-47-49-;-8-12-;0;1;113156128;1;5;221900,2;0;3;86436,2;6;44;1835,6;228;935;159,1;466;2008;78,3;502;2148;51,4;7099;30340;18,2;10874;45411;13,5;26559;108054;10,6;37865;164079;8,4;169364;698112;6,2;419543;1697705;4,1;1;2073,1;3;552,8;98;21,1;177;11,7;2841;1,8;4158;3,3;14906;4,3;23917;12,6;64174;2,6;568129;2,6;OJ 202 7087;; +20211064;MARDI ;10/08/2021;;09/11/2021;47;9;48;49;37;2;7;-9-37-47-48-49-;-2-7-;0;0;0;0;0;0;0;5;159798,6;4;27;1746,1;98;541;160,5;291;1464;62,7;172;1159;55,6;5146;24538;13,1;5307;27053;13,2;10775;57882;11,5;33007;146686;5,5;82829;414997;6,1;164762;862282;4,7;0;0;2;948,8;43;24,5;120;8,7;1971;1,3;2016;3,5;12457;2,6;23871;6,4;31966;2,7;359503;2,1;CL 903 3732;; +20211063;VENDREDI;06/08/2021;;05/11/2021;32;21;7;26;14;12;4;-7-14-21-26-32-;-4-12-;0;0;0;0;8;99092;4;17;10898,5;6;34;1697,3;202;1156;91,9;346;1663;67,5;629;3552;22,2;4108;22313;17,6;8435;43955;10;23675;121748;6,7;20886;113718;8,7;109472;548208;5,7;298934;1458856;3,4;0;0;3;766,2;92;13,8;153;8,3;1701;1,8;3288;2,6;8661;4,6;13810;13,5;42226;2,4;330316;2,8;RJ 112 5381;; +20211062;MARDI ;03/08/2021;;02/11/2021;46;23;45;21;17;7;11;-17-21-23-45-46-;-7-11-;0;0;0;0;3;193278,1;1;7;19359,5;4;28;1507,5;110;570;136,4;260;1367;60,1;210;1262;45,7;3976;21882;13,1;5357;28177;11,4;10975;59138;10,1;20795;108734;6,6;83601;424487;5,3;176829;913172;4;0;0;1;1693,5;38;24,7;106;8,8;1571;1,4;2103;2,9;8246;3,5;13376;10,2;32459;2,3;284393;2,4;VF 519 7424;; +20211061;VENDREDI;30/07/2021;;29/10/2021;23;10;35;33;37;5;10;-10-23-33-35-37-;-5-10-;0;0;0;0;2;364049,2;0;10;17016,8;5;37;1432,5;149;829;117,7;281;1766;58,4;306;1737;41,7;4323;24831;14,6;6227;34600;11,6;14756;77234;9,7;23972;131983;6,9;94069;502696;5,7;218587;1133827;4;0;0;2;1048,7;53;21,9;112;10,4;1705;1,7;2458;3,1;9413;3,8;14920;11,4;36346;2,6;329328;2,5;QW 014 7486;; +20211060;MARDI ;27/07/2021;;26/10/2021;19;21;25;5;9;12;10;-5-9-19-21-25-;-10-12-;0;0;0;1;4;130748,8;5;22;5556;7;29;1312,8;172;701;100;229;1136;65,2;713;3077;16,9;2687;14194;18,3;5679;26730;10,8;22622;101650;5,3;12757;68940;9,5;68461;336665;6,1;244559;1154418;2,8;1;848,1;2;339,2;78;10,8;94;9;1125;1,8;2296;2,4;5297;5;8211;15;27890;2,5;192856;3,1;JA 020 0146;; +20211059;VENDREDI;23/07/2021;;22/10/2021;34;50;26;23;24;4;2;-23-24-26-34-50-;-2-4-;0;0;0;0;0;0;1;7;116054;5;32;1497,9;96;527;167,5;275;1459;63,9;210;1203;54,5;4489;22745;14,4;5542;29694;12,3;11215;62673;10,8;26140;128849;6,4;90208;469135;5,5;183307;982592;4,2;0;0;2;964,6;38;28,2;116;9,2;1733;1,5;2273;3,1;9819;3,4;16295;9,6;34500;2,5;333409;2,3;IH 676 1920;; +20211058;MARDI ;20/07/2021;;19/10/2021;19;48;3;41;12;6;9;-3-12-19-41-48-;-6-9-;0;0;0;1;4;120206,1;0;5;22475,3;5;28;1250;93;495;130,2;306;1445;47,1;234;1131;42,3;4262;20443;11,7;5092;24133;11;10649;51440;9,6;21348;106277;5,6;73592;357031;5,3;155305;760337;4;0;0;2;714,3;40;19,8;114;6,9;1746;1,1;2068;2,5;8206;3;12897;8,9;29203;2,2;235017;2,4;AM 956 8370;; +20211057;VENDREDI;16/07/2021;;15/10/2021;47;5;16;42;49;3;11;-5-16-42-47-49-;-3-11-;0;0;0;2;5;123838,4;0;4;36178,8;4;28;1609,8;109;544;152,6;253;1262;69,5;218;1144;53,9;3423;18008;17,1;4878;25421;13,5;11242;56981;11,2;19710;100104;7,7;74960;390232;6,2;180569;888347;4,4;1;1036,2;2;414,5;41;25,2;104;9,9;1432;1,8;1947;3,5;8357;3,9;14060;10,7;30007;2,8;304616;2,4;OF 263 5041;; +20211056;MARDI ;13/07/2021;;12/10/2021;22;11;6;8;5;3;4;-5-6-8-11-22-;-3-4-;0;1;26482986;1;5;99002,3;5;17;6805,4;12;67;537,8;350;1233;53,8;383;1676;41,8;699;2587;19;4032;19398;12,7;9525;38766;7;20139;81167;6,3;17286;87429;7;103746;458457;4,2;208484;924406;3,4;1;854,7;8;85,4;143;5,9;127;6,7;1572;1,3;3710;1,5;6758;3,9;10050;12,4;40371;1,7;245592;2,5;GX 336 1769;; +20211055;VENDREDI;09/07/2021;;08/10/2021;37;19;6;36;49;11;4;-6-19-36-37-49-;-4-11-;0;0;0;1;2;319379,4;1;5;29857,7;6;31;1499,9;107;577;148,4;236;1306;69,3;265;1318;48,2;3302;18096;17,5;5074;25590;13,8;12192;61276;10,7;18529;98577;8,1;78903;393624;6,4;199326;959979;4,2;1;1092,3;2;436,9;40;27,3;100;10,9;1392;1,9;2080;3,5;7889;4,3;13357;11,9;31680;2,8;305223;2,6;LQ 838 5498;; +20211054;MARDI ;06/07/2021;;05/10/2021;2;42;5;27;21;9;11;-2-5-21-27-42-;-9-11-;0;1;17000000;1;2;241620,4;0;6;18823,5;5;23;1529,4;121;584;110,9;195;1103;62,1;292;1417;33,9;2765;15104;15,9;5323;25950;10,3;13722;63521;7,8;13829;76100;7,9;71885;356761;5,3;185381;870053;3,5;1;829,5;3;221,2;39;21,2;71;11,6;1149;1,8;2155;2,5;5856;4,4;9333;12,9;29338;2,3;220007;2,7;JB 310 7646;; +20211053;VENDREDI;02/07/2021;;01/10/2021;49;29;38;9;14;7;10;-9-14-29-38-49-;-7-10-;0;1;77959528;1;2;423174,2;2;7;28257,9;16;41;1502,7;207;969;117,1;419;2020;59,3;368;2069;40,7;5521;27338;15,4;8264;42415;11;17393;94868;9,2;28903;145272;7,2;112590;590518;5,6;240729;1317318;4;0;0;6;372,6;94;13,2;147;8,4;2198;1,4;3205;2,5;11286;3,4;18555;9,7;43731;2,3;377189;2,3;OM 594 3615;; +20211052;MARDI ;29/06/2021;;28/09/2021;6;12;24;20;48;6;5;-6-12-20-24-48-;-5-6-;0;0;0;2;5;111254,2;1;5;26001,9;9;55;736,2;247;1139;65,4;350;1745;45,1;474;1863;29,7;4398;21751;12,7;8409;36620;8,4;14745;66073;8,7;19897;104567;6,6;101789;464731;4,7;189145;881402;4;0;0;2;821,6;95;9,6;132;6,9;1743;1,3;3273;1,8;7665;3,7;11196;11,9;39179;1,9;275061;2,4;BL 604 3203;; +20211051;VENDREDI;25/06/2021;;24/09/2021;33;29;36;1;30;12;3;-1-29-30-33-36-;-3-12-;0;0;0;0;0;0;1;7;126640,6;4;24;2179,5;89;523;184,2;193;1177;86,5;223;1203;59,5;3337;18343;19,5;5068;26737;14,9;12466;63393;11,7;18769;100417;8,9;84972;432899;6,5;210190;1049471;4,3;0;0;2;1065,8;45;26,3;85;13,9;1393;2,1;1989;3,9;7600;4,9;13115;13,1;33443;2,9;334315;2,5;JE 105 2161;; +20211050;MARDI ;22/06/2021;;21/09/2021;27;24;26;6;41;8;3;-6-24-26-27-41-;-3-8-;0;0;0;1;1;517837,3;1;3;40342,3;4;26;1449,8;105;537;129,3;278;1503;48,8;184;1098;46,9;4280;22412;11,5;5122;25742;11,1;10262;53604;9,9;23850;121431;5,3;77852;386296;5,2;159598;801213;4,1;0;0;2;768;43;19,8;115;7,4;1656;1,2;2055;2,7;8882;3;14488;8,5;30596;2,3;260100;2,3;JX 716 1550;; +20211049;VENDREDI;18/06/2021;;17/09/2021;27;20;41;11;30;3;5;-11-20-27-30-41-;-3-5-;0;0;0;0;7;96928,2;4;8;19822;12;61;809,7;193;916;99,3;550;2570;37,4;369;1808;37,3;7355;33949;9,9;8454;40030;9,4;16612;76478;9,1;35902;171005;4,9;118570;554561;4,8;228836;1061478;4;0;0;3;698,1;76;15,3;169;6,8;2690;1;3128;2,4;12726;2,8;20727;8,1;44837;2,1;354576;2,3;RV 533 2111;; +20211048;MARDI ;15/06/2021;;14/09/2021;30;31;11;26;21;2;3;-11-21-26-30-31-;-2-3-;0;0;0;0;2;241573,8;0;6;18819,9;6;32;1099,1;146;727;89,1;243;1243;55,1;285;1339;35,9;3182;16157;14,8;6488;30482;8,8;12158;58911;8,4;16304;83088;7,2;84541;400903;4,7;159921;771992;3,9;0;0;3;500,9;52;16;86;9,7;1277;1,6;2402;2,3;6382;4,1;11018;11;33126;2;264635;2,2;HW 514 9335;; +20211047;VENDREDI;11/06/2021;;10/09/2021;17;33;9;21;39;7;11;-9-17-21-33-39-;-7-11-;1;1;30382655;0;7;99796,3;8;17;9604;5;68;747,8;246;1160;80,7;422;2095;47,2;483;2449;28,4;5648;26487;13,1;9306;43854;8,8;19612;93506;7,7;27216;128929;6,7;121178;551033;5;254992;1189088;3,7;0;0;3;752,9;90;13,9;156;8;2226;1,4;3567;2,3;10916;3,6;16834;10,8;47118;2,1;369698;2,4;NO 049 7049;; +20211046;MARDI ;08/06/2021;;07/09/2021;47;26;2;36;11;9;7;-2-11-26-36-47-;-7-9-;0;0;0;0;3;169023,9;0;3;39503,6;6;29;1272,8;93;521;130,5;326;1302;55,2;216;1101;45,8;4646;20252;12,4;5294;24558;11,4;9896;48064;10,9;26711;113013;5,6;79038;367540;5,4;149904;721818;4,4;0;0;2;811,9;43;20,9;117;7,7;1820;1,2;2098;2,8;10324;2,7;17907;7,3;31370;2,3;295278;2,2;KE 956 9754;; +20211045;VENDREDI;04/06/2021;;03/09/2021;40;20;36;46;7;2;4;-7-20-36-40-46-;-2-4-;0;1;130000000;1;3;422567,4;3;15;19752,1;10;69;1337,4;278;1181;143,9;655;2623;68,5;560;2458;51,3;10247;41510;15,2;13146;54563;12,9;28419;118452;11;58215;232145;6,8;206525;843437;5,9;445125;1836279;4,3;0;0;5;905,4;117;21,4;246;10,2;3813;1,6;5076;3,3;21664;3,6;37432;9,8;78453;2,6;777029;2,3;GK 266 0524;; +20211044;MARDI ;01/06/2021;;31/08/2021;35;10;44;3;26;7;6;-3-10-26-35-44-;-6-7-;0;0;0;1;4;157524,1;1;14;10518,8;8;26;1764,1;169;772;109,4;280;1508;59,2;292;1527;41,1;4841;22450;13,9;7189;33487;10,4;13518;66919;9,7;26700;124879;6,3;105266;479419;5,1;195346;954097;4,1;0;0;4;476,7;60;17,6;119;8,9;1903;1,3;2673;2,6;9973;3,3;17161;9;39997;2,1;353193;2,1;OW 729 3397;; +20211043;VENDREDI;28/05/2021;;27/08/2021;16;40;13;11;34;1;12;-11-13-16-34-40-;-1-12-;0;0;0;1;4;198582,2;1;8;23205,9;4;23;2514,1;124;667;159,6;245;1460;77,1;333;1812;43,6;4202;21591;18,3;5806;31367;14;16709;89013;9,2;23138;119317;8,3;91443;476856;6,5;270008;1364346;3,7;0;0;3;761,4;46;27,5;106;11,9;1735;1,8;2297;3,7;9715;4,1;15723;11,7;35976;2,9;313345;2,9;JO 710 8313;; +20211042;MARDI ;25/05/2021;;24/08/2021;38;21;26;1;30;8;2;-1-21-26-30-38-;-2-8-;0;0;0;0;0;0;1;3;234077,1;6;28;1479,8;99;550;138,7;334;1669;48,3;240;1136;49,9;4983;25385;11,1;5303;27184;11,6;11340;57873;10,1;28045;139240;5,1;78644;403152;5,5;168006;872777;4,1;0;0;4;400;36;24,6;134;6,6;1933;1,1;2089;2,8;10780;2,5;18370;7;30435;2,4;267617;2,4;AN 097 3344;; +20211041;VENDREDI;21/05/2021;;20/08/2021;9;13;35;2;15;10;3;-2-9-13-15-35-;-3-10-;0;0;0;2;5;149387,9;2;5;34914,4;8;43;1264,5;195;842;118,9;407;1830;57,8;475;2005;37,1;5942;25929;14,3;8743;39484;10,5;21621;94556;8,1;29721;133141;7;122079;561023;5,2;289972;1318698;3,6;0;0;4;564,4;76;16,5;143;8,7;2285;1,3;3373;2,4;11740;3,3;17631;10,3;46658;2,2;342203;2,6;MJ 580 8475;; +20211040;MARDI ;18/05/2021;;17/08/2021;13;39;40;38;5;4;12;-5-13-38-39-40-;-4-12-;0;0;0;0;1;542038,1;0;2;63341,6;0;18;2192,1;81;460;158;165;900;85,3;209;1138;47,4;2550;14120;19,1;3947;21583;13,9;10559;55601;10;13977;75896;8,9;63578;330144;6,4;170170;869002;3,9;0;0;0;0;34;74,2;81;11,1;1084;2;1605;3,7;5899;4,8;9981;13,1;25689;2,8;241379;2,7;NG 483 3243;; +20211039;VENDREDI;14/05/2021;;13/08/2021;6;38;29;16;18;6;11;-6-16-18-29-38-;-6-11-;0;0;0;2;4;172797,8;2;7;23077,5;7;32;1572,3;158;800;115,8;362;1732;56,5;356;1830;37,6;4979;24464;14;7506;35541;10,8;17118;80947;8,8;23611;116601;7,4;101842;491563;5,5;236399;1134203;3,8;0;0;4;512,2;64;17,7;148;7,6;2077;1,3;2966;2,5;9800;3,6;15380;10,8;40080;2,3;308295;2,6;HJ 787 6281;; +20211038;MARDI ;11/05/2021;;10/08/2021;14;46;43;34;25;6;5;-14-25-34-43-46-;-5-6-;0;0;0;0;1;507955,6;2;5;23743,5;4;19;1946,1;110;498;136,7;187;998;72,1;183;1005;50,3;2719;15462;16,3;4401;22890;12,3;8617;46031;11,4;15789;88416;7,1;65159;338989;5,9;129681;674594;4,7;0;0;2;780,6;46;18,8;80;10,8;1091;1,9;1787;3,2;6281;4,3;12043;10,5;25956;2,7;292408;2,1;MY 017 0036;; +20211037;VENDREDI;07/05/2021;;06/08/2021;33;38;47;22;14;9;8;-14-22-33-38-47-;-8-9-;0;1;69463976;1;4;198903,4;3;8;23243,5;7;39;1485;204;1031;103,4;345;1687;66,8;403;2125;37,2;4397;23019;17,2;8258;41841;10,5;17713;85768;9,5;24115;126706;7,8;110303;567932;5,5;235301;1177496;4,2;1;1323,8;3;353;90;14,7;142;9,3;1734;1,9;3182;2,7;9773;4,2;17488;11;42461;2,5;420266;2,2;ON 789 2710;; +20211036;MARDI ;04/05/2021;;03/08/2021;46;13;10;28;3;11;4;-3-10-13-28-46-;-4-11-;0;0;0;0;3;188139,9;4;12;10992,8;7;32;1284;149;670;112,9;265;1366;58,5;411;1802;31,2;3426;18248;15,4;6266;29362;10,6;17079;76213;7,6;17026;91973;7,6;83782;404486;5,5;216940;1014527;3,5;0;0;1;1691,2;54;17,3;109;8,6;1415;1,6;2505;2,5;7000;4,2;11276;12,1;33329;2,3;248339;2,7;IP 493 9062;; +20211035;VENDREDI;30/04/2021;;30/07/2021;1;28;24;46;16;11;2;-1-16-24-28-46-;-2-11-;0;0;0;0;3;281487;0;10;19736,4;7;55;1117,7;130;794;142,6;323;1872;63,9;381;1936;43,4;4787;26918;15,6;6844;38059;12,3;17500;90986;9,6;25669;143162;7,3;100966;556634;5,9;251389;1322178;4;0;0;2;1145;51;24,9;139;9,1;1991;1,5;2741;3,1;10461;3,8;17771;10,4;40320;2,6;357309;2,5;JI 173 8621;; +20211034;MARDI ;27/04/2021;;27/07/2021;13;49;14;24;35;1;10;-13-14-24-35-49-;-1-10-;0;0;0;0;2;267062,2;0;1;124833,7;5;21;1851,5;92;490;146,1;201;977;77,5;199;1182;45;2839;14222;18,7;4266;22290;13,3;11211;58745;9,4;14952;75911;8,8;66263;330528;6,3;176669;883871;3,8;0;0;3;535,3;35;25,4;87;10,2;1160;1,9;1752;3,4;6220;4,5;10157;12,8;26729;2,7;233202;2,7;CS 269 2623;; +20211033;VENDREDI;23/04/2021;;23/07/2021;29;19;15;3;14;2;8;-3-14-15-19-29-;-2-8-;0;0;0;1;5;136455,9;1;9;17717,7;13;68;730,4;193;979;93,4;602;2697;35,8;476;2186;31;8394;37526;9;9340;41712;9;20638;93109;7,5;39724;181368;4,7;122045;555053;4,8;267762;1216697;3,5;0;0;3;707,7;67;17,6;221;5,3;3173;0,9;3600;2,1;15173;2,4;21916;7,8;46799;2;332652;2,5;KZ 186 1003;; +20211032;MARDI ;20/04/2021;;20/07/2021;28;46;17;5;41;11;10;-5-17-28-41-46-;-10-11-;0;1;68414615;0;3;200125;2;7;20045,3;3;28;1560,9;142;639;125,9;213;1141;74,5;324;1568;38,1;2911;15930;18,7;5265;26236;12,7;14599;69173;8,9;15123;83399;9;74427;369576;6,4;209637;1000036;3,8;0;0;2;912,4;53;19,1;84;12;1153;2,1;2060;3,2;6206;5,1;10065;14,7;29744;2,8;256490;2,8;ND 401 3611;; +20211031;VENDREDI;16/04/2021;;16/07/2021;29;11;40;48;6;9;5;-6-11-29-40-48-;-5-9-;0;0;0;0;5;154073,8;1;5;36009,5;9;51;1099,6;152;767;134,6;458;2212;49,3;354;1682;45,6;6515;32527;11,7;7591;36313;11,7;16126;75862;10,5;35469;180117;5,3;115809;551262;5,5;243000;1136433;4,3;0;0;5;472,9;49;26,8;162;8,1;2355;1,3;2989;2,9;12792;3,2;21849;8,7;44375;2,4;396656;2,4;KU 471 4103;; +20211030;MARDI ;13/04/2021;;13/07/2021;47;16;31;50;20;8;2;-16-20-31-47-50-;-2-8-;0;0;0;0;0;0;3;9;78273,2;5;31;1340,8;94;536;142,8;249;1318;61,4;190;1115;51;4506;22013;12,9;4385;24079;13,1;10037;52748;11,1;27641;133290;5,3;71854;364122;6,1;158880;804157;4,5;0;0;2;892,4;37;26,8;94;10,5;1787;1,3;1763;3,7;11011;2,8;20466;7;28833;2,8;312124;2,3;PG 009 8337;; +20211029;VENDREDI;09/04/2021;;09/07/2021;44;8;35;32;2;8;11;-2-8-32-35-44-;-8-11-;0;0;0;0;3;247364,3;7;13;13341,4;6;26;2077,7;165;777;128;324;1520;69,2;345;1662;44,4;4110;21426;17,2;6935;34882;11,8;15908;76962;9,9;21885;114624;8,1;100067;492560;5,9;233128;1114677;4,2;0;0;2;1168,7;60;21,6;148;8,7;1706;1,9;2783;3,1;9120;4,4;15160;12,5;40381;2,6;361123;2,6;QL 627 3053;; +20211028;MARDI ;06/04/2021;;06/07/2021;50;21;37;38;2;8;7;-2-21-37-38-50-;-7-8-;0;0;0;0;3;171378,8;1;2;60081;2;20;1871,3;72;421;163,7;189;982;74,2;193;952;53,7;2950;15459;16,5;4149;21900;13;8239;44909;11,8;18318;88631;7,2;70663;354897;5,7;134846;696508;4,6;0;0;0;0;29;87,8;72;12,6;1237;1,8;1689;3,6;7322;3,9;13222;10;28614;2,6;320982;2;EC 415 4458;; +20211027;VENDREDI;02/04/2021;;02/07/2021;21;47;4;40;34;2;5;-4-21-34-40-47-;-2-5-;0;1;144755907;0;1;1027964,6;2;8;30031,5;10;44;1700,7;189;1015;135,8;498;2498;58,3;441;2147;47,6;8470;38360;13,3;9800;45644;12,5;20963;95022;11,1;50788;220716;5,8;154107;694744;5,8;323808;1435512;4,5;0;0;3;1157,1;86;22,4;196;9,8;3003;1,6;3710;3,4;17990;3,3;33697;8,3;57970;2,7;619232;2,2;JU 837 6212;; +20211026;MARDI ;30/03/2021;;29/06/2021;42;24;29;28;27;8;4;-24-27-28-29-42-;-4-8-;0;0;0;0;4;200288,1;0;4;46810,6;11;39;1495,4;166;765;140,4;439;2156;52,6;331;1573;50,7;7026;33349;11,9;7832;37459;11,8;16685;81606;10,1;38448;182285;5,5;124370;586711;5,3;267355;1261780;4;0;0;4;593,3;64;20,6;153;8,6;2463;1,3;2921;3;13867;2,9;20614;9,3;46022;2,3;374426;2,5;RQ 559 8594;; +20211025;VENDREDI;26/03/2021;;25/06/2021;49;12;50;10;37;8;3;-10-12-37-49-50-;-3-8-;0;0;0;2;5;202737,6;2;21;11281,6;13;55;1341,6;216;1013;134,1;548;2595;55,3;488;2161;46,7;8988;40468;12,4;10644;47140;11,9;22682;100247;10,4;50479;225736;5,6;165613;716203;5,5;348445;1509225;4,2;0;0;5;657,3;77;23,7;220;8,3;3200;1,4;3978;3;18144;3,1;30697;8,6;61388;2,4;560282;2,3;LJ 741 9751;; +20211024;MARDI ;23/03/2021;;22/06/2021;2;26;29;36;49;3;9;-2-26-29-36-49-;-3-9-;0;0;0;0;3;236598;2;8;20736,3;8;33;1565,7;144;628;151,5;385;1756;57,3;275;1354;52,2;5960;28140;12,5;6271;30210;13;13123;65288;11,2;35215;161700;5,4;98323;473034;5,9;206974;990928;4,5;0;0;1;2156,6;54;22,1;120;9,9;2262;1,3;2369;3,3;13179;2,8;23475;7,4;37236;2,6;376263;2,3;GI 739 3104;; +20211023;VENDREDI;19/03/2021;;18/06/2021;35;44;43;9;3;10;1;-3-9-35-43-44-;-1-10-;0;0;0;1;4;220250,3;1;6;34317,4;5;26;2466,7;123;707;167,1;285;1527;81,7;362;1820;48,2;4690;22752;19,2;7017;33727;14,5;18358;88534;10,2;26421;124513;8,8;112463;530063;6,5;299348;1406445;3,9;0;0;1;2695,6;46;32,5;101;14,8;1923;1,9;2754;3,6;10531;4,4;17783;12,2;43945;2,8;399687;2,7;US 442 8833;; +20211022;MARDI ;16/03/2021;;15/06/2021;32;25;23;47;35;2;6;-23-25-32-35-47-;-2-6-;0;0;0;0;4;165026,6;0;6;25712,9;5;40;1201,3;133;628;140,9;333;1686;55,5;287;1362;48,2;5026;25162;13;5746;29856;12,2;12051;65078;10,4;29495;140955;5,8;87321;446089;5,8;180715;963090;4,3;0;0;5;397,8;55;20;114;9,6;1947;1,4;2290;3,2;11282;3;20754;7,7;33710;2,7;356950;2,2;BL 534 9058;; +20211021;VENDREDI;12/03/2021;;11/06/2021;11;4;6;44;9;10;11;-4-6-9-11-44-;-10-11-;0;0;0;1;7;123876,4;4;13;15589,5;7;35;1803,5;286;1098;105,9;354;1749;70,2;849;3167;27,2;4782;24117;17,9;10247;44346;10,8;33016;131304;6,8;22726;120258;9;126272;579055;5,9;392642;1684823;3,2;1;1468,7;4;293,7;117;12,5;144;10,1;1957;1,8;4092;2,4;9220;5;14217;15;50468;2,4;357030;2,9;PH 942 4010;; +20211020;MARDI ;09/03/2021;;08/06/2021;16;21;33;32;1;11;12;-1-16-21-32-33-;-11-12-;0;0;0;0;2;306703,5;0;5;28672,6;5;41;1089,1;85;477;172,4;192;1043;83,3;256;1386;44;2810;15161;20,1;4546;23387;14,5;13416;67182;9,4;15170;80486;9,5;69691;351440;6,8;218161;1035128;3,7;0;0;3;651,8;42;25,8;69;15,7;1208;2,2;1873;3,8;6369;5,3;10660;14,8;29082;3;263579;2,9;WG 461 7511;; +20211019;VENDREDI;05/03/2021;;04/06/2021;4;46;5;39;48;7;10;-4-5-39-46-48-;-7-10-;0;0;0;1;3;267994,9;0;5;37580,8;3;23;2544,6;101;638;168,9;319;1451;78,5;271;1338;59,8;5196;22791;17,5;6351;30928;14,4;13513;68082;12,2;31818;133487;7,5;107415;499764;6,3;235398;1129766;4,5;0;0;0;0;45;89,4;137;10,4;2096;1,7;2593;3,7;12598;3,5;22241;9,4;43070;2,7;448629;2,3;OM 097 5007;; +20211018;MARDI ;02/03/2021;;01/06/2021;24;49;19;26;2;6;7;-2-19-24-26-49-;-6-7-;0;0;0;0;2;296930,7;0;5;27759;6;30;1441;180;712;111,8;358;1537;54,7;374;1470;40,2;4829;21358;13,8;8142;33011;9,9;14486;64927;9,4;25683;114577;6,4;110452;471779;4,9;201547;928312;4;0;0;5;387,5;72;14,9;136;7,9;1852;1,4;3309;2,1;10456;3,2;16354;9,6;43957;2;356354;2,1;NS 960 1970;; +20211017;VENDREDI;26/02/2021;;28/05/2021;6;12;22;29;33;11;6;-6-12-22-29-33-;-6-11-;0;1;210000000;3;12;2494703,8;11;35;11695,7;33;108;1180,5;732;2406;97,6;1311;4760;52,1;1575;5419;32,1;17861;63073;13,8;29202;101722;9,5;64848;227281;7,9;84861;310363;7;370590;1334324;5,1;848606;3053393;3,6;1;3533,4;12;235,5;282;12,5;493;7,1;6377;1,3;10498;2,2;31325;3,5;47650;10,8;134916;2,1;960604;2,6;KF 243 5930;; +20211016;MARDI ;23/02/2021;;25/05/2021;30;9;39;18;45;3;1;-9-18-30-39-45-;-1-3-;0;0;0;0;6;3519853,7;0;9;37306,4;17;69;1515,6;434;1662;115,9;765;3152;64,6;939;3869;36,9;11195;46060;15,5;18214;72114;11;40465;161187;9,2;62448;252839;7,1;251409;1002759;5,6;554386;2239261;4;0;0;6;809,2;158;17;275;9,8;4062;1,6;6450;2,8;22320;3,8;37227;10,5;89621;2,4;801452;2,4;PI 915 7042;; +20211015;VENDREDI;19/02/2021;;21/05/2021;25;48;4;46;12;12;7;-4-12-25-46-48-;-7-12-;0;0;0;2;9;177571,4;3;15;24900,8;19;65;1789,8;473;1645;130,2;849;3260;69,4;940;3619;43,9;12709;46931;16,9;19859;73511;12;43863;165838;9,9;66686;249124;8;291007;1070213;5,8;643174;2450775;4,1;2;1559,3;9;277,2;175;17,8;332;9,3;4834;1,6;7111;2,9;25073;3,9;40248;11,3;105031;2,4;926213;2,4;PC 315 9670;; +20211014;MARDI ;16/02/2021;;18/05/2021;30;25;29;9;5;6;7;-5-9-25-29-30-;-6-7-;0;0;0;1;5;200974,1;3;17;13814,9;15;75;975,3;348;1515;88,9;592;2740;51,9;651;3204;31,2;8189;38901;12,8;13976;62736;8,8;26266;124483;8,3;39536;193196;6,5;186066;850401;4,6;342689;1678423;3,8;0;0;4;680,3;108;13,9;214;7;2902;1,3;4927;2;14140;3,3;22067;10;66011;1,8;476811;2,3;RC 048 0036;; +20211013;VENDREDI;12/02/2021;;14/05/2021;32;7;36;30;23;12;8;-7-23-30-32-36-;-8-12-;0;0;0;1;3;406714,9;6;13;21935,9;14;60;1480,3;249;1186;137,9;457;2328;74,3;597;3014;40,3;6854;33839;17,9;11433;56573;11,9;29957;142207;8,8;35447;177849;8,5;165355;810020;5,9;431681;2047679;3,7;1;1948,7;3;519,6;93;20,9;179;10,8;2524;1,9;4269;3;13331;4,6;22215;12,8;59730;2,6;528659;2,6;SY 373 9746;; +20211012;MARDI ;09/02/2021;;11/05/2021;24;5;31;10;37;9;10;-5-10-24-31-37-;-9-10-;0;0;0;0;1;934794,5;3;8;27309,6;13;56;1215,1;241;1078;116,2;321;1787;74,1;549;2494;37,3;5195;25963;17,9;10362;47850;10,8;23966;109736;8,8;26585;134742;8,6;143065;667850;5,5;336356;1553129;3,8;0;0;4;666,5;93;15,9;111;13,3;1983;1,8;3634;2,7;9970;4,6;15596;13,8;51792;2,3;414044;2,6;ME 065 0382;; +20211011;VENDREDI;05/02/2021;;07/05/2021;45;12;37;19;44;6;2;-12-19-37-44-45-;-2-6-;0;0;0;2;9;151022,6;1;12;26472,3;22;113;875,6;466;1610;113,2;950;3814;50,5;893;3449;39,2;14317;55212;12,2;18158;68836;10,9;36499;147030;9,5;74630;287111;5,9;253469;980351;5,4;520565;2108737;4;1;2517,5;12;167,8;172;14,6;322;7,8;4946;1,2;6383;2,6;25897;3;42520;8,6;90068;2,3;761007;2,4;EJ 459 7231;; +20211010;MARDI ;02/02/2021;;04/05/2021;38;18;48;20;35;9;12;-18-20-35-38-48-;-9-12-;0;0;0;0;1;576287;3;10;13468,7;4;28;1498,2;95;525;147,1;169;985;82,9;235;1251;45,8;2405;13819;20,7;4344;23549;13,5;11345;59074;10;14127;76406;9,4;64261;338644;6,7;164315;838385;4,3;0;0;2;828,6;35;26,3;68;13,5;967;2,3;1720;3,5;5703;5;9825;13,6;24907;3;258211;2,5;NU 587 9239;; +20211009;VENDREDI;29/01/2021;;30/04/2021;27;36;5;42;1;6;1;-1-5-27-36-42-;-1-6-;0;0;0;0;3;238052,2;2;4;41727,5;5;30;1732,9;133;667;143,5;347;1702;59,4;282;1549;45,9;5645;25376;14;7147;33713;11,7;14888;74940;9,8;29030;132397;6,7;111122;508307;5,5;233780;1128677;4;0;0;2;1024,8;40;28,4;116;9,8;1981;1,4;2580;2,9;10410;3,4;15895;10,4;40947;2,2;327773;2,5;FS 781 4350;; +20211008;MARDI ;26/01/2021;;27/04/2021;7;37;25;40;5;2;8;-5-7-25-37-40-;-2-8-;0;0;0;0;0;0;1;5;122065,6;5;25;1440,5;114;595;111,4;337;1670;41,9;264;1376;35,8;4975;23815;10,3;5096;26029;10,5;10990;58012;8,8;25877;124550;4,9;75464;373011;5,2;164296;824253;3,8;0;0;0;0;48;46,3;140;5,6;1824;1;1888;2,8;9564;2,6;15530;7,4;28431;2,3;234417;2,4;EF 410 9474;; +20211007;VENDREDI;22/01/2021;;23/04/2021;16;47;42;8;44;7;6;-8-16-42-44-47-;-6-7-;0;1;80566353;0;1;797450,9;0;2;93188,7;4;35;1658,6;143;722;148,1;354;1674;67,5;301;1599;49,6;5250;26200;15,1;7559;36769;12;13971;74683;11;29745;144978;6,8;118153;563565;5,5;219576;1133936;4,4;0;0;0;0;50;68,5;123;9,9;1857;1,6;2734;3;10471;3,6;18761;9,5;42195;2,3;420764;2,1;DR 848 4786;; +20211006;MARDI ;19/01/2021;;20/04/2021;25;35;31;27;43;5;6;-25-27-31-35-43-;-5-6-;0;0;0;1;6;99196,6;1;4;34775,8;5;23;1883,7;100;546;146,1;201;1282;65,8;223;1206;49,1;3209;19246;15,4;5212;27536;12;10352;55707;11;18240;103841;7,1;80696;425335;5,5;159249;843964;4,4;0;0;2;818,1;40;22,7;77;11,8;1190;1,9;1823;3,3;6693;4,2;11955;11,1;29708;2,5;297118;2,2;CD 285 4461;; +20211005;VENDREDI;15/01/2021;;16/04/2021;27;10;4;40;38;11;3;-4-10-27-38-40-;-3-11-;0;0;0;0;2;384315,1;1;3;59880,5;7;44;1271,6;161;827;124,6;342;1727;63;390;1972;38,8;4934;24461;15,6;7495;37538;11,3;18304;87508;9;25892;127458;7,5;110316;541887;5,5;265732;1251776;3,9;0;0;4;551,9;54;22,7;130;9,4;1834;1,6;2705;3;9684;3,9;15985;11,2;40408;2,5;350796;2,5;VK 667 2191;; +20211004;MARDI ;12/01/2021;;13/04/2021;10;34;2;35;18;6;9;-2-10-18-34-35-;-6-9-;0;0;0;1;1;562879,7;0;4;32888,5;11;25;1639;107;607;124,3;255;1384;57,6;293;1397;40,1;4248;20787;13,4;5226;25458;12,2;11672;56481;10,3;23600;117669;5,9;79536;385051;5,7;174019;844610;4,2;0;0;2;817,2;43;21,1;86;10,5;1534;1,4;1885;3,2;8606;3,3;15286;8,6;29790;2,5;275716;2,3;KP 202 6170;; +20211003;VENDREDI;08/01/2021;;09/04/2021;42;23;37;41;18;4;6;-18-23-37-41-42-;-4-6-;0;0;0;1;4;180403,7;1;6;28108,8;8;47;1117,6;160;723;133,8;383;1717;59,5;282;1473;48,8;6686;28209;12,7;6144;31800;12,6;13211;68850;10,8;37172;153197;5,9;100139;482668;5,9;215691;1057516;4,3;0;0;0;0;56;59,3;139;8,5;2373;1,2;2197;3,6;13218;2,8;22859;7,5;36396;2,6;361942;2,3;AZ 574 5485;; +20211002;MARDI ;05/01/2021;;06/04/2021;37;42;12;34;38;3;1;-12-34-37-38-42-;-1-3-;0;0;0;2;4;131718,1;0;0;0;2;16;10093,3;79;441;160,2;210;1054;70,8;170;971;54;2993;14869;17,6;4792;22652;12,9;10898;52480;10,3;18240;87049;7,5;69834;335506;6,1;155098;759051;4,4;1;876,9;1;701,5;27;32,4;82;10,6;1143;1,9;1725;3,4;6721;4,1;13072;9,7;26201;2,7;277668;2,2;DF 777 9665;; +20211001;VENDREDI;01/01/2021;;02/04/2021;32;16;48;44;28;1;9;-16-28-32-44-48-;-1-9-;0;1;44958140;1;8;109750,6;0;10;20520,4;9;47;1359,9;166;1008;116,8;315;1879;66,2;395;2435;35,9;5195;28333;15,4;6820;39885;12,2;16649;96183;9,4;29766;156929;7;98958;565121;6,1;235126;1323070;4,2;0;0;5;442,1;62;19,8;111;11;1868;1,6;2466;3,3;10317;3,7;19712;9;35761;2,8;370809;2,4;UA 000 7122;; +20200104;MARDI ;29/12/2020;;29/04/2021;7;40;39;45;32;8;3;-7-32-39-40-45-;-3-8-;0;0;0;0;1;581409,4;0;4;33971,2;9;28;1511,6;96;505;154,3;275;1331;61,9;185;1011;57,2;3970;21753;13,3;4705;24993;12,9;10143;53878;11,1;23845;125339;5,8;72556;384162;5,9;153496;814532;4,5;0;0;3;554,7;37;24,9;100;9,2;1432;1,6;1785;3,4;8664;3,3;16444;8,2;27244;2,7;301413;2,2;TB 433 2954;; +20200103;VENDREDI;25/12/2020;;25/04/2021;16;21;27;30;32;3;5;-16-21-27-30-32-;-3-5-;0;0;0;0;0;0;0;4;207160,1;5;55;888,9;124;812;110,9;460;2357;40,3;302;1613;41,4;6674;32983;10,1;7097;36790;10,1;13805;70599;9,8;35812;170909;4,9;107042;533541;4,9;206645;1026611;4,1;0;0;3;637,7;47;22,6;162;6,5;2260;1,1;2522;2,8;12261;2,7;19482;7,9;37624;2,3;332260;2,3;LH 798 2665;; +20200102;MARDI ;22/12/2020;;22/04/2021;42;10;50;2;16;11;4;-2-10-16-42-50-;-4-11-;0;1;43704037;1;4;156220,3;0;3;48681,6;3;15;3032,6;125;593;141,3;239;1273;69,5;303;1435;43,3;3347;18373;16,9;5183;26586;13;13685;66274;9,7;18570;99104;7,8;79770;403099;6,1;204328;995329;3,9;1;1006,2;2;402,5;36;27,9;108;9,3;1341;1,8;2032;3,3;7267;4,3;12393;11,8;30822;2,6;278323;2,6;MI 502 5000;; +20200101;VENDREDI;18/12/2020;;18/04/2021;2;21;17;3;13;7;8;-2-3-13-17-21-;-7-8-;0;0;0;2;4;192267,3;1;10;17974,4;22;80;699,8;435;1666;61,9;575;2495;43,6;667;2806;27,3;7011;30918;12,3;14939;62830;6,8;26297;111763;7,1;31474;142041;6,7;178955;757863;4;314233;1390130;3,5;1;1322,9;5;211,6;171;7,7;204;6,4;2510;1,3;5357;1,6;11380;3,6;17053;11,3;64482;1,6;402642;2,3;QJ 740 0867;; +20200100;MARDI ;15/12/2020;;15/04/2021;21;9;35;13;29;2;1;-9-13-21-29-35-;-1-2-;0;0;0;0;7;81129,1;1;11;12066,2;3;21;1968,6;196;783;97,2;286;1283;62,7;392;1907;29,6;3692;18232;15,5;7601;33176;9,5;16989;76794;7,6;18441;92101;7,7;98775;441028;5;219416;1008542;3,5;0;0;3;589,8;73;13,4;111;8,8;1381;1,7;2893;2,2;6896;4,4;11052;12,9;37738;2,1;285839;2,5;AA 954 3153;; +20200099;VENDREDI;11/12/2020;;11/04/2021;41;13;24;9;6;3;12;-6-9-13-24-41-;-3-12-;1;1;200000000;3;12;2364169,1;5;17;22819,6;20;73;1655,2;574;2271;98;957;3764;62,5;1373;5278;31,3;12455;50834;16,2;24341;91723;10;58141;219032;7,8;62053;258453;8;314250;1224812;5,3;753910;2892569;3,6;1;3052;8;305,2;230;13,2;349;8,7;4577;1,6;8495;2,4;22740;4,2;36831;12;109852;2,2;850017;2,6;OR 273 6030;; +20200098;MARDI ;08/12/2020;;08/04/2021;1;46;21;4;24;2;12;-1-4-21-24-46-;-2-12-;0;0;0;2;4;5684144,4;4;16;19431,3;18;65;1489,8;347;1319;135,2;703;2616;72;815;3106;42,6;9825;38177;17,3;14866;58844;12,5;36647;142857;9,6;52688;203482;8,1;222926;874394;6;549225;2132408;3,9;1;2464;7;281,6;127;19,4;236;10,4;3510;1,7;5197;3,1;19157;4;31567;11,3;78502;2,5;692981;2,5;UF 576 9619;; +20200097;VENDREDI;04/12/2020;;04/04/2021;14;38;20;27;34;1;11;-14-20-27-34-38-;-1-11-;0;0;0;4;6;1385679,2;8;26;15425,5;20;76;1643,7;499;1747;131,7;917;3422;71;1292;4703;36,3;13455;50925;16,7;21149;77496;12,3;56440;204601;8,6;71306;271544;7,9;300626;1107720;6,1;778710;2840869;3,8;3;1103,8;6;441,5;178;18,6;301;11;4608;1,7;7046;3,1;24459;4,2;41614;11,6;101950;2,6;895540;2,6;DN 528 1278;; +20200096;MARDI ;01/12/2020;;01/04/2021;29;49;20;14;47;12;4;-14-20-29-47-49-;-4-12-;0;0;0;0;3;328905,6;1;6;38435,3;10;53;1355,2;144;821;161,1;319;1848;75,6;378;1967;49,9;4698;26355;18,6;7382;39794;13,7;18544;97165;10,5;26020;143597;8,6;115063;606731;6,4;291381;1502470;4,1;0;0;3;833,1;46;30,1;122;11,3;1733;2;2620;3,5;9557;4,5;15848;12,7;40468;2,8;382566;2,6;FV 079 1121;; +20200095;VENDREDI;27/11/2020;;28/03/2021;8;14;16;5;2;9;8;-2-5-8-14-16-;-8-9-;0;0;0;1;3;391023,1;5;28;9791,6;24;90;948,8;583;2157;72,9;632;2979;55,8;1170;4312;27,1;8245;40603;14,3;18091;77208;8,4;35174;153979;7,8;37932;196484;7,4;208511;976295;4,7;426947;1999670;3,7;1;1673,4;8;167,3;189;8,8;213;7,8;2870;1,4;6080;1,8;13218;3,9;20020;12,2;70405;1,9;483025;2,5;VG 924 9237;; +20200094;MARDI ;24/11/2020;;25/03/2021;50;33;38;42;25;12;8;-25-33-38-42-50-;-8-12-;0;0;0;1;5;176840,4;4;12;17221;7;40;1609,1;151;830;142,8;319;1615;77,6;416;2012;43,7;4485;23825;18,4;7044;37825;12,9;17269;90012;10,1;23507;125229;8,8;103129;552652;6,3;252717;1321424;4,2;0;0;2;1151;62;20,6;92;13,9;1574;2;2406;3,5;8363;4,8;14727;12,6;36176;2,9;361397;2,5;SP 913 9425;; +20200093;VENDREDI;20/11/2020;;20/01/2021;28;50;48;39;29;5;7;-28-29-39-48-50-;-5-7-;0;0;0;2;6;209127,7;2;7;41894,2;6;61;1497,4;225;1144;147;640;3110;57,1;553;2605;47,9;10992;49635;12,5;11040;53181;13,1;23420;113770;11,4;70409;298808;5,2;181677;842595;5,8;369721;1730636;4,6;0;0;0;0;86;66,2;228;8,9;3678;1,3;3777;3,6;23189;2,7;44279;6,7;62049;2,7;682220;2,1;FT 835 8023;; +20200092;MARDI ;17/11/2020;;17/01/2021;30;25;44;19;16;6;2;-16-19-25-30-44-;-2-6-;0;0;0;1;1;609799,2;0;4;35630;16;46;965;147;750;109;362;1896;45,5;306;1576;38,5;5462;26592;11,4;6556;32885;10,3;13607;70609;8,9;27954;135999;5,6;93962;464581;5,1;192599;982134;3,9;0;0;8;202,1;48;18,7;113;7,9;1927;1,1;2285;2,6;9695;2,9;15529;8,4;33277;2,2;271015;2,4;DQ 513 0923;; +20200091;VENDREDI;13/11/2020;;13/01/2021;31;17;1;28;5;1;10;-1-5-17-28-31-;-1-10-;0;0;0;1;4;222454,1;8;26;7998,6;16;56;1156,7;395;1328;89,8;589;2073;60,8;925;3283;26,9;7409;27330;16,2;14440;50710;9,7;34975;125599;7,3;33783;133357;8,3;172944;644710;5,4;432727;1633896;3,4;0;0;5;565,6;134;11,7;183;8,5;2543;1,5;4873;2,1;11664;4,2;17174;13,3;58235;2,2;395681;2,8;AX 115 7025;; +20200090;MARDI ;10/11/2020;;10/01/2021;19;3;38;32;29;12;5;-3-19-29-32-38-;-5-12-;0;0;0;0;2;279894,1;0;3;43610,5;3;21;1940,5;87;529;141,9;188;1084;73,2;272;1360;41;2534;15129;18,4;4578;26244;11,8;11795;62688;9,2;13810;79417;8,8;68965;381713;5,7;180414;927880;3,8;0;0;0;0;24;95,7;77;10,6;985;2;1609;3,4;5343;4,8;8972;13,3;25271;2,6;220804;2,7;IB 051 3569;; +20200089;VENDREDI;06/11/2020;;06/01/2021;12;37;7;50;40;1;2;-7-12-37-40-50-;-1-2-;0;0;0;0;2;347604,2;0;6;27080,2;8;41;1234,3;140;725;128,5;244;1336;73,7;246;1488;46,5;3830;21182;16,3;6030;32150;12;13033;72026;9,9;20868;112945;7,7;94502;484902;5,6;205321;1091274;4;0;0;2;899,4;54;18,5;93;10,7;1305;1,9;2149;3,1;7245;4,3;11418;12,7;33638;2,4;299201;2,4;LB 981 4400;; +20200088;MARDI ;03/11/2020;;03/01/2021;5;7;18;20;30;7;6;-5-7-18-20-30-;-6-7-;0;0;0;2;7;70929,1;4;14;8288,6;7;43;840,5;273;1128;59;387;1664;42,2;450;1939;25,5;4548;21016;11,7;9256;39142;7;14873;69786;7,3;19898;99060;6,2;102303;470113;4,1;172514;869851;3,6;0;0;4;325,4;93;7,7;144;5;1631;1,1;3344;1,4;7195;3,1;9943;10,6;37039;1,6;225500;2,3;SB 380 8153;; +20200087;VENDREDI;30/10/2020;;30/12/2020;21;16;20;12;28;9;3;-12-16-20-21-28-;-3-9-;0;0;0;0;4;163187;2;8;19069,7;10;54;879,9;255;1047;83,6;635;2536;36,4;494;2083;31,2;7688;33733;9,6;9213;41198;8,8;18627;83988;8;34772;165331;4,9;114267;541770;4,7;230929;1105289;3,7;0;0;3;568;77;12,2;216;4,3;2727;0,8;3214;1,9;12193;2,4;17994;7,6;39495;1,9;274932;2,5;DK 004 2337;; +20200086;MARDI ;27/10/2020;;27/12/2020;13;15;28;44;32;12;3;-13-15-28-32-44-;-3-12-;0;1;87757466;1;4;152128,9;1;9;15802,2;2;24;1845,7;149;735;111;225;1152;74,8;325;1869;32,4;3090;17011;17,8;5677;30567;11;14613;76077;8,2;15228;87237;8,7;79290;421362;5,6;202029;1050009;3,6;1;860;0;0;48;32,2;76;11,3;1197;1,7;2007;2,8;5811;4,6;9108;13,7;28488;2,4;231716;2,6;QA 581 7004;; +20200085;VENDREDI;23/10/2020;;23/12/2020;19;23;10;21;15;3;12;-10-15-19-21-23-;-3-12-;0;0;0;1;3;261553,3;6;26;7053,3;4;31;1842,6;304;1410;74,6;292;1612;69;855;3375;23,1;4271;22625;17,2;10719;49699;8,7;27543;124813;6,5;19732;110757;8,8;126094;622490;4,9;324275;1551438;3,2;0;0;3;664,5;109;10,1;117;9,4;1533;1,8;3577;2;7301;4,7;10991;14,7;43327;2,1;284222;2,8;KC 160 2843;; +20200084;MARDI ;20/10/2020;;20/12/2020;15;5;42;37;6;4;3;-5-6-15-37-42-;-3-4-;0;0;0;1;3;189400,6;3;7;18971,1;3;23;1798,4;110;586;130;225;1151;69,9;230;1286;44;3375;18004;15,7;5359;28060;11,2;10998;58880;9,9;18169;97757;7,2;82769;426910;5,2;169420;878501;4,1;0;0;2;735,3;36;22,6;65;12,5;1185;1,7;1843;2,9;6489;3,9;10325;11,5;28898;2,3;254502;2,3;MW 147 9033;; +20200083;VENDREDI;16/10/2020;;16/12/2020;15;40;38;33;50;6;3;-15-33-38-40-50-;-3-6-;0;0;0;1;1;737995,3;0;5;34496,3;5;50;1074,4;122;691;143,2;316;1693;61,7;257;1440;51;5351;26653;13,7;5536;31378;13;12004;67950;11,2;34606;157351;5,8;90154;486562;5,9;187505;1029066;4,5;1;1089,1;1;871,3;38;28,6;106;10,2;1824;1,4;1922;3,7;11627;2,9;22917;6,9;31330;2,8;360338;2,1;RV 734 1637;; +20200082;MARDI ;13/10/2020;;13/12/2020;5;41;46;38;14;10;1;-5-14-38-41-46-;-1-10-;0;0;0;0;1;522745,9;0;1;122174,3;1;19;2002,8;90;410;170,9;193;949;78;205;1067;48,8;2594;12725;20,4;3973;19911;14,5;9928;51243;10,5;14613;72442;9;60077;296170;6,9;155654;772207;4,3;0;0;0;0;30;77;68;12,1;977;2,1;1481;3,7;5345;4,8;9607;12,5;22030;3;226075;2,6;CV 058 7277;; +20200081;VENDREDI;09/10/2020;;09/12/2020;15;11;35;50;41;8;5;-11-15-35-41-50-;-5-8-;0;0;0;0;2;347164,3;0;3;54092;2;44;1148,7;97;677;137,5;370;1993;49,3;260;1365;50,6;6064;31728;10,8;5685;31487;12,2;12002;62584;11,4;36411;180330;4,8;91006;477696;5,7;192059;969596;4,5;0;0;1;1936,9;43;25;132;8,1;2001;1,3;1992;3,6;12200;2,7;21643;7,2;31557;2,8;333609;2,3;KQ 131 6639;; +20200080;MARDI ;06/10/2020;;06/12/2020;41;36;4;21;47;11;9;-4-21-36-41-47-;-9-11-;0;0;0;0;3;165112,6;1;5;23153,7;2;16;2253,6;61;395;168,1;159;887;79,1;171;943;52,3;2347;13056;18,8;3349;19244;14,2;7959;43992;11,6;13176;73158;8,4;54207;295086;6,6;132587;686274;4,5;0;0;1;1402,8;22;35,4;60;12,9;879;2,2;1219;4,2;5098;4,8;9236;12,3;20083;3,2;226457;2,5;AK 982 0660;; +20200079;VENDREDI;02/10/2020;;02/12/2020;15;45;12;6;40;9;3;-6-12-15-40-45-;-3-9-;1;1;30627859;7;8;88921,7;3;8;20782,4;6;48;1078,8;171;762;125,1;489;2195;45,9;298;1502;47,1;7218;32321;10,9;7362;34706;11,3;15359;73110;10;36650;169534;5,2;111724;524166;5,3;231569;1086594;4,1;7;155,9;3;291;62;17,6;151;7,2;2559;1;2509;2,9;12405;2,7;21149;7,5;38844;2,3;329183;2,4;DO 142 6191;; +20200078;MARDI ;29/09/2020;;29/11/2020;16;14;4;44;41;11;12;-4-14-16-41-44-;-11-12-;0;0;0;0;1;511878,3;0;6;19939;5;16;2328,9;81;450;152,5;133;857;84,6;253;1430;35,6;2019;11989;21,2;3468;19701;14,4;11716;59701;8,8;11140;64119;10;52294;278746;7,2;174069;862108;3,7;0;0;1;1420,1;27;29,2;51;15,4;783;2,5;1330;3,9;4295;5,7;7371;15,6;19893;3,2;184849;3,1;LH 000 7642;; +20200077;VENDREDI;25/09/2020;;25/11/2020;34;45;19;23;37;1;7;-19-23-34-37-45-;-1-7-;0;1;130000000;0;5;247047,8;5;25;11547,8;16;78;1152,8;374;1590;104,1;812;3162;55,3;801;3694;33,3;10810;44420;13,8;14416;63635;10,7;30469;142425;8,9;59230;237240;6,5;195280;867562;5,6;412900;1921549;4;0;0;5;689,5;125;15,3;222;8,6;3482;1,3;4561;2,8;19268;3,1;34308;8,1;63749;2,4;601772;2,3;GB 600 8763;; +20200076;MARDI ;22/09/2020;;22/11/2020;28;39;25;40;16;1;6;-16-25-28-39-40-;-1-6-;0;0;0;1;5;123979;0;3;48293,2;7;21;2148,8;110;600;138,5;217;1285;68,3;234;1443;42,7;3967;20023;15,4;5224;27225;12,6;12008;64322;9,9;21562;107403;7,2;78996;404454;6;179553;944456;4,1;0;0;3;538,9;41;21,9;81;11;1331;1,6;1808;3,3;7167;3,9;12753;10,2;27602;2,6;269829;2,4;AM 862 6424;; +20200075;VENDREDI;18/09/2020;;18/11/2020;25;45;19;2;10;1;7;-2-10-19-25-45-;-1-7-;0;0;0;0;5;166116;2;6;32353,3;11;61;991,2;266;1158;96,1;581;2426;48,5;482;2436;33,9;7268;33330;12,4;9713;46944;9,8;19803;101487;8,4;35586;166868;6,2;128995;646705;5;254921;1368514;3,8;0;0;3;652,4;96;11,3;179;6;2424;1,1;3155;2,3;11760;2,9;18338;8,6;43220;2;329124;2,4;NL 914 1916;; +20200074;MARDI ;15/09/2020;;15/11/2020;38;25;30;16;20;8;6;-16-20-25-30-38-;-6-8-;0;0;0;0;3;188472;1;7;18878,1;6;36;1143,3;96;655;115,7;302;1541;52;254;1344;41,9;4582;22037;12,7;5817;29331;10,7;11904;60360;9,6;23906;113496;6,2;83183;414876;5,3;172473;873152;4,1;0;0;0;0;41;57,2;96;8,7;1601;1,3;2052;2,7;8397;3,1;14151;8,6;28917;2,3;256988;2,3;OP 735 6223;; +20200073;VENDREDI;11/09/2020;;11/11/2020;34;38;45;43;42;12;9;-34-38-42-43-45-;-9-12-;0;0;0;1;2;352375,3;0;8;20588,9;7;25;2052,1;118;580;162,9;207;1055;94,6;300;1426;49,2;2921;16223;21,6;4868;25778;15,1;13238;62894;11,5;15678;88055;10;72176;389847;7,1;191835;969300;4,6;1;1049,2;4;209,8;42;24,9;80;13,1;1127;2,3;1748;4;5938;5,5;11161;13,7;25958;3,3;303075;2,5;ES 198 7144;; +20200072;MARDI ;08/09/2020;;08/11/2020;9;35;2;41;31;12;9;-2-9-31-35-41-;-9-12-;0;0;0;0;2;261831;1;4;30597,1;3;23;1657,4;82;428;164;131;821;90,4;163;987;52,8;2218;12450;20,9;3896;20444;14,2;9250;48658;11,1;12552;68440;9,5;61601;320138;6,4;154660;783822;4,2;0;0;1;1436,8;33;24,1;43;18,5;862;2,3;1417;3,7;4940;5;7899;14,7;22948;2,8;210923;2,7;UZ 647 1420;; +20200071;VENDREDI;04/09/2020;;04/11/2020;6;29;43;40;22;7;4;-6-22-29-40-43-;-4-7-;0;0;0;1;4;176702,3;1;8;20649,1;11;38;1354;148;674;140,6;386;1866;53,6;295;1500;46,9;6113;28936;12,1;6601;32338;12,1;12870;68089;10,7;35686;163884;5,4;99342;491615;5,6;192911;1013742;4,4;0;0;2;917,1;46;22,1;125;8,1;2078;1,2;2355;2,8;12144;2,6;21353;6,9;34723;2,4;337427;2,1;BC 472 0015;; +20200070;MARDI ;01/09/2020;;01/11/2020;46;8;4;33;10;8;11;-4-8-10-33-46-;-8-11-;1;1;157170843;1;6;153948,4;2;6;35980,2;4;44;1528,2;170;1006;123,1;337;1973;66,3;358;2050;44,8;5073;28911;15,9;8898;45746;11,2;19409;100330;9,5;26897;152250;7,6;124437;654019;5,5;292633;1486934;3,9;1;1240,3;2;496,1;58;21,3;117;10,6;1721;1,8;3018;2,7;9163;4,2;14511;12,4;42134;2,4;337858;2,6;IR 486 1016;; +20200069;VENDREDI;28/08/2020;;27/11/2020;31;16;17;7;12;9;7;-7-12-16-17-31-;-7-9-;0;0;0;2;9;119361,1;4;18;13948,3;28;104;751,9;399;1731;83,2;957;4142;36,7;689;3228;33,1;12826;56369;9,4;15772;73247;8,1;26644;132973;8,3;56107;261389;5,1;198151;974840;4,3;354133;1815616;3,7;1;1400,7;9;124,5;121;11,5;300;4,6;4063;0,8;5160;1,8;18268;2,4;23713;8,6;64766;1,7;406770;2,5;WF 229 9684;; +20200068;MARDI ;25/08/2020;;24/11/2020;18;37;39;30;7;10;8;-7-18-30-37-39-;-8-10-;0;0;0;1;5;163573,8;1;11;17377,2;9;47;1266,7;203;944;116,1;336;1746;66,4;405;2037;39,9;5019;25376;16;7748;40006;11,3;17673;92808;9,1;25923;132800;7,7;107822;564682;5,7;258534;1345481;3,8;0;0;4;503;68;16,4;115;9,7;1704;1,6;2592;2,8;9190;3,8;15399;10,5;36199;2,5;313766;2,5;QF 215 6243;; +20200067;VENDREDI;21/08/2020;;20/11/2020;10;35;50;15;44;5;3;-10-15-35-44-50-;-3-5-;0;0;0;1;6;156509,1;1;9;24385,8;12;47;1454,4;177;993;126,8;484;2610;51;336;1978;47,2;7466;41568;11,2;7769;44889;11,6;15925;89552;10,8;41785;219696;5,3;125690;679218;5,4;259095;1364172;4,3;0;0;5;462,2;41;31,3;169;7,5;2374;1,3;2563;3,3;13383;3;23563;7,9;41583;2,5;408700;2,2;MH 105 6486;; +20200066;MARDI ;18/08/2020;;17/11/2020;12;34;42;17;6;12;5;-6-12-17-34-42-;-5-12-;0;0;0;1;5;131558,4;0;5;30747,3;4;30;1596,1;157;746;118,2;260;1354;68,8;427;1907;34,3;3313;18506;17,7;6678;32782;11,1;17248;80439;8,4;17511;96316;8,5;89387;457082;5,6;230608;1116083;3,7;0;0;1;1658,1;52;17,7;92;10;1170;1,9;2256;2,7;6250;4,6;9700;13,8;30562;2,4;241968;2,7;CC 440 6829;; +20200065;VENDREDI;14/08/2020;;13/11/2020;49;10;42;9;19;4;12;-9-10-19-42-49-;-4-12-;0;0;0;3;5;157446,8;1;5;36797,9;5;24;2387,8;127;670;157,5;224;1449;77;382;1810;43,3;3681;21133;18,5;6560;34068;12,8;18764;91743;8,8;18838;111645;8,8;98309;510793;6;271614;1349713;3,7;1;1050,7;2;420,3;37;28,3;90;11,6;1361;1,9;2303;3;6808;4,8;10682;14,3;33982;2,5;268117;2,8;EM 957 7653;; +20200064;MARDI ;11/08/2020;;10/11/2020;14;13;50;8;42;3;9;-8-13-14-42-50-;-3-9-;0;0;0;0;2;293190,7;0;2;68523,5;2;31;1376,9;88;530;148,3;284;1726;48,1;211;1237;47,2;5030;26334;11;4797;27592;11,8;10771;59707;10,1;27703;144296;5;80775;435472;5,3;173340;916256;4;0;0;0;0;28;79,8;88;9;1644;1,2;1670;3,2;9458;2,6;14940;7,8;27688;2,3;236254;2,4;RO 370 1123;; +20200063;VENDREDI;07/08/2020;;06/11/2020;36;20;5;41;21;11;6;-5-20-21-36-41-;-6-11-;0;0;0;1;4;195041,6;1;7;26048,2;4;36;1577,6;92;614;170,3;222;1516;72,9;243;1533;50,6;3876;23353;16,6;5099;32163;13,4;12635;76263;10,5;22376;129363;7,5;83306;497396;6,1;210211;1199060;4,1;1;992,4;1;793,9;38;26,1;72;13,7;1388;1,7;1751;3,7;8081;3,8;13855;10,4;28487;2,8;281356;2,5;HK 751 8334;; +20200062;MARDI ;04/08/2020;;03/11/2020;42;2;5;1;15;8;5;-1-2-5-15-42-;-5-8-;0;0;0;0;2;265150;0;1;123939,8;10;41;941,5;142;674;105,5;417;1933;38,8;253;1192;44,3;5651;28050;9,4;6297;31560;9,3;12579;62253;8,8;27523;141282;4,7;85100;427402;4,8;171178;843157;3,9;0;0;5;272,1;54;14;149;5;1970;0,9;2250;2,2;9200;2,5;13831;7,9;29735;2;217920;2,5;OK 001 5607;; +20200061;VENDREDI;31/07/2020;;30/10/2020;30;46;24;21;49;2;5;-21-24-30-46-49-;-2-5-;0;0;0;0;0;0;2;6;136226,5;4;27;1786,2;106;529;167,9;268;1506;62,3;219;1226;53,8;4661;24333;13,5;5122;27797;13,2;10658;58195;11,7;28448;145640;5,6;86106;450152;5,8;175195;919223;4,5;0;0;1;1680,4;29;32,1;93;10;1513;1,5;1819;3,4;9350;3,1;17056;7,9;29175;2,6;305557;2,2;DW 519 7337;; +20200060;MARDI ;28/07/2020;;27/10/2020;34;17;13;9;15;7;2;-9-13-15-17-34-;-2-7-;0;0;0;2;4;120865,8;1;7;16141,9;15;44;799,8;161;741;87,4;533;2093;32,7;298;1394;34,5;6614;28516;8,4;6721;32694;8,2;11856;60275;8,2;30848;135763;4,4;90012;434961;4,3;161071;826953;3,7;0;0;5;249,3;48;14,4;173;4;2190;0,7;2240;2;10671;2;14738;6,8;31149;1,8;212331;2,3;KI 833 7510;; +20200059;VENDREDI;24/07/2020;;23/10/2020;8;28;50;27;9;12;4;-8-9-27-28-50-;-4-12-;0;0;0;0;3;205961,3;0;7;20629,9;4;23;1955,6;94;555;149,2;196;1058;82,7;295;1595;38,5;2961;16672;18,4;5439;27279;12,5;14841;72778;8,7;15640;84996;9,1;85593;416403;5,8;238706;1125472;3,4;0;0;1;1605,1;35;25,4;72;12,3;1060;2,1;1914;3,1;5615;5;8487;15,3;29510;2,4;220156;2,9;JH 979 5073;; +20200058;MARDI ;21/07/2020;;20/10/2020;29;15;42;14;24;2;4;-14-15-24-29-42-;-2-4-;0;1;49564587;0;3;177754,5;2;3;41544,1;8;37;1049,1;89;555;128,8;304;1427;52,9;223;1204;44,1;4364;21507;12,3;5202;26912;11;10904;56710;9,7;22022;107554;6,2;80072;399902;5,2;163066;832577;4;0;0;3;457,1;39;19,5;95;8;1468;1,2;1831;2,7;7563;3,1;11689;9,5;27615;2,2;234707;2,3;RV 049 2923;; +20200057;VENDREDI;17/07/2020;;16/10/2020;7;27;41;15;43;7;9;-7-15-27-41-43-;-7-9-;0;0;0;3;7;96257,2;1;5;31495,6;13;53;925,4;196;957;94,4;550;2455;38,9;360;1739;38,5;7644;33820;9,9;8380;40886;9,1;14175;72686;9,5;37620;160145;5,2;118876;568827;4,6;208844;1030354;4,1;2;498,4;4;199,3;62;16;184;5,4;2625;0,9;2791;2,3;12504;2,5;18660;7,7;40266;2;311619;2,3;OU 340 6452;; +20200056;MARDI ;14/07/2020;;13/10/2020;30;22;37;11;47;9;10;-11-22-30-37-47-;-9-10-;0;0;0;0;0;0;2;6;101330,7;4;19;1888,1;81;487;135,6;137;818;85,4;215;1138;43,1;2027;12292;19,9;3956;21782;12,5;9547;51291;9,9;11098;66459;9,2;59649;325980;5,9;145134;778495;4;0;0;1;1227,5;26;26,2;46;14,8;762;2,2;1413;3,2;4077;5,2;6684;14,8;21512;2,6;195263;2,5;NY 772 1633;; +20200055;VENDREDI;10/07/2020;;09/10/2020;30;38;15;17;23;2;7;-15-17-23-30-38-;-2-7-;0;0;0;6;9;74781,9;4;14;11235,7;22;66;742,3;225;964;93,6;680;2649;36;413;1909;35,1;8672;36019;9,3;9628;42487;8,8;17021;80362;8,6;43749;177965;4,7;124906;573219;4,6;227549;1095560;3,9;0;0;7;266,9;66;15,7;221;4,6;2884;0,9;3243;2,1;14433;2,2;23282;6,5;41744;2;333163;2,2;UL 180 7837;; +20200054;MARDI ;07/07/2020;;06/10/2020;41;33;23;12;16;8;10;-12-16-23-33-41-;-8-10-;0;1;144542315;2;8;112962,6;2;16;13200,6;7;54;1218,2;214;1068;113,4;379;1861;68,8;499;2492;36,1;5595;27209;16,5;9380;45013;11,1;21980;108567;8,6;30484;143534;7,8;130601;631726;5,6;316109;1527227;3,7;2;654,4;3;349;75;17,4;121;10,8;1898;1,7;3083;2,8;10504;3,9;17011;11,2;42948;2,5;364199;2,6;JF 734 1511;; +20200053;VENDREDI;03/07/2020;;02/10/2020;27;4;16;39;37;3;6;-4-16-27-37-39-;-3-6-;0;0;0;1;10;126332,6;7;33;8947,2;26;98;938,4;336;1552;109,1;955;3713;48,2;671;3327;37,8;14036;52823;11,9;15598;66033;10,6;31922;138411;9,4;73256;275443;5,7;226377;940515;5,3;463034;1961364;4;0;0;6;620,8;102;20,2;295;7;4405;1,1;4919;2,8;22824;2,8;39083;7,7;72260;2,3;641409;2,3;SE 756 4471;; +20200052;MARDI ;30/06/2020;;29/09/2020;11;42;1;28;6;2;10;-1-6-11-28-42-;-2-10-;0;0;0;0;1;565114,1;5;16;8254,7;8;23;1788,6;168;654;115,8;328;1299;61,6;390;1688;33,3;4041;18420;15,2;6339;28487;11;16054;71847;8,1;20120;95075;7,4;85696;400514;5,5;209798;980320;3,6;0;0;1;1435,9;43;18,5;114;6,9;1483;1,3;2118;2,5;7179;3,5;11266;10,3;29410;2,2;216153;2,6;GL 973 8980;; +20200051;VENDREDI;26/06/2020;;25/09/2020;12;26;21;1;10;5;11;-1-10-12-21-26-;-5-11-;0;0;0;2;9;75644,7;9;18;8839,7;7;38;1304,2;248;1049;87;295;1633;59,1;719;2726;24,8;4116;22953;14,7;9253;41590;9;24020;98412;7,1;20362;117337;7,2;108449;530351;5;272674;1232996;3,5;1;914,7;1;731,8;75;12,1;104;8,7;1372;1,6;2992;2;6950;4,1;10699;12,4;35363;2,1;243546;2,7;OZ 006 2321;; +20200050;MARDI ;23/06/2020;;22/09/2020;19;46;49;12;2;8;1;-2-12-19-46-49-;-1-8-;0;0;0;0;0;0;1;3;201011,8;2;21;1694,4;90;487;134,5;252;1142;60,6;231;1120;43,4;3302;15908;15,3;4649;22351;12,1;10940;51961;9,7;17211;85896;7,1;68034;337786;5,7;156054;773857;4;0;0;1;1216,6;20;33,7;86;7,8;1158;1,4;1601;2,8;5846;3,6;9383;10,5;23069;2,4;191079;2,5;BD 519 4518;; +20200049;VENDREDI;19/06/2020;;18/09/2020;14;5;24;43;19;3;11;-5-14-19-24-43-;-3-11-;0;0;0;0;4;157456,5;2;8;18400;7;35;1309,9;147;744;113,5;311;1537;58;377;1722;36,4;3961;20217;15,5;6692;33500;10,4;16332;76928;8,4;19943;103921;7,5;93132;466080;5,3;228226;1083585;3,6;0;0;2;782,1;46;18,8;109;7,9;1341;1,6;2241;2,5;6845;3,9;10531;12;31138;2,3;233723;2,7;SD 890 4120;; +20200048;MARDI ;16/06/2020;;15/09/2020;39;9;11;4;17;2;10;-4-9-11-17-39-;-2-10-;0;0;0;0;2;228917,4;1;4;26750,8;4;16;2083;128;636;96,5;230;1128;57,5;378;1656;27,5;3256;15600;14,6;5517;26331;9,6;14398;68380;6,9;16006;76193;7,5;71327;353669;5,1;178563;879179;3,3;0;0;1;1176;39;16,7;92;7,1;1192;1,3;1942;2,2;5964;3,4;8813;10,8;24775;2,1;176330;2,6;GJ 375 0766;; +20200047;VENDREDI;12/06/2020;;11/09/2020;24;39;46;2;45;4;8;-2-24-39-45-46-;-4-8-;0;1;51583717;0;2;337141,5;1;3;52530,3;8;38;1291,7;83;565;160;310;1564;61,1;199;1158;58;4712;23702;14,1;5132;26919;13,9;10696;58277;11,9;28909;142367;5,9;81550;419915;6,3;175492;911820;4,7;0;0;4;423,1;32;29,3;88;10,6;1517;1,5;1706;3,6;9261;3,1;17661;7,7;27308;2,8;294256;2,3;TI 890 2631;; +20200046;MARDI ;09/06/2020;;08/09/2020;46;36;27;5;15;8;2;-5-15-27-36-46-;-2-8-;0;0;0;1;3;165913,8;0;2;58165,1;10;33;1098;83;550;121,3;262;1427;49,4;164;1095;45,2;4274;22240;11,1;4259;23196;11,9;9336;51249;10;23954;120489;5,1;67457;352621;5,5;144730;765947;4,1;1;692,9;5;110,8;26;26,6;94;7,3;1492;1,1;1471;3,1;8264;2,6;14182;7,1;22899;2,4;209867;2,4;BV 585 0710;; +20200045;VENDREDI;05/06/2020;;04/09/2020;24;11;37;17;5;6;3;-5-11-17-24-37-;-3-6-;0;0;0;0;3;211081,7;0;11;13454,5;13;67;688;254;1100;77,1;599;2434;36,8;448;2165;29,1;7135;31131;10,1;8812;42614;8,2;17509;84690;7,7;33144;149368;5,3;112688;545858;4,5;224871;1102940;3,6;0;0;3;521,5;77;11,2;164;5,3;2361;0,9;2768;2,1;10863;2,5;16171;7,8;36912;1,9;256110;2,4;OK 201 3475;; +20200044;MARDI ;02/06/2020;;01/09/2020;49;22;12;26;10;11;7;-10-12-22-26-49-;-7-11-;0;0;0;1;5;87507,8;2;9;11362,2;3;17;1873,6;132;547;107,2;209;1020;60,8;313;1288;33,8;2882;14935;14,5;5069;23592;10,3;11295;53698;8,4;14230;77349;7;67595;328824;5,2;150717;745473;3,7;0;0;0;0;39;43,4;73;8,2;1042;1,4;1771;2,2;5075;3,7;8157;10,8;23832;2;174043;2,5;FR 608 3244;; +20200043;VENDREDI;29/05/2020;;28/08/2020;4;11;8;46;19;4;8;-4-8-11-19-46-;-4-8-;0;1;17000000;2;5;116611,2;5;13;10482,2;47;137;309,8;203;897;87,1;909;3574;23,1;421;1730;33,5;7580;33182;8,7;8918;37668;8,5;16672;74615;8;27287;134296;5,4;104859;492834;4,6;205739;984731;3,7;0;0;20;72,7;72;11,2;303;2,6;2467;0,8;2981;1,8;8878;2,8;12465;9,4;35096;1,9;222958;2,6;MO 301 6631;; +20200042;MARDI ;26/05/2020;;25/08/2020;9;4;27;14;21;4;6;-4-9-14-21-27-;-4-6-;1;2;18571740;0;5;92895,4;2;6;18092,6;18;63;536,7;226;999;62,3;480;1993;33;412;1869;24,7;5468;24082;9,6;7345;33423;7,7;14138;67756;7;23358;106041;5,4;81565;400538;4,5;166638;842619;3,5;0;0;4;286,8;83;7,6;158;4;1901;0,8;2529;1,6;7990;2,5;11261;8,2;28189;1,8;178616;2,5;FV 414 5061;; +20200041;VENDREDI;22/05/2020;;21/08/2020;18;22;32;3;27;2;7;-3-18-22-27-32-;-2-7-;0;0;0;0;0;0;0;13;55708,2;9;53;806,2;150;842;93,4;557;2454;33,9;240;1496;39;7445;33462;8,7;7017;36228;9;12765;69537;8,7;35907;158062;4,6;99263;501467;4,6;180411;970867;3,8;0;0;3;469,1;49;15,9;171;4,5;2443;0,8;2285;2,2;11694;2,1;16640;6,8;32521;1,9;244998;2,3;ND 876 1904;; +20200040;MARDI ;19/05/2020;;18/08/2020;43;32;37;25;5;6;7;-5-25-32-37-43-;-6-7-;0;0;0;0;1;433824,4;1;6;16898,6;4;21;1503,8;76;467;124,5;159;981;62,6;162;934;46,2;2743;14287;15,1;3890;20912;11,5;7027;40300;11,1;16270;81398;6,6;60851;312561;5,4;110581;614114;4,4;0;0;2;561;22;28,3;56;11,1;928;1,6;1328;3,1;5603;3,5;9724;9,3;20928;2,4;218843;2;CU 770 5969;; +20200039;VENDREDI;15/05/2020;;14/08/2020;34;23;32;39;11;1;6;-11-23-32-34-39-;-1-6-;1;1;72969058;0;1;565971,2;2;6;22046,1;6;31;1329;161;740;102,5;318;1409;56,9;336;1844;30,5;4412;19013;14,8;6611;29810;10,5;14242;70025;8,3;24326;101435;6,9;85677;386395;5,7;193742;915870;3,9;0;0;3;544,8;47;19,3;107;8,4;1404;1,6;2290;2,6;7729;3,7;13147;10;28566;2,6;273571;2,4;TK 926 0069;; +20200038;MARDI ;12/05/2020;;10/09/2020;16;9;29;39;37;4;11;-9-16-29-37-39-;-4-11-;0;0;0;0;1;425436,6;1;11;9039,2;4;27;1147;114;482;118,3;174;1017;59,3;284;1228;34,5;2800;13634;15,5;4550;21054;11,2;11122;50289;8,7;14566;70785;7,5;62387;287434;5,8;156046;683566;3,9;0;0;1;1277,6;40;17,7;53;13,3;1024;1,7;1570;3;5212;4,2;8598;12;21318;2,7;196203;2,6;CN 495 0418;; +20200037;VENDREDI;08/05/2020;;06/09/2020;33;38;8;25;32;9;7;-8-25-32-33-38-;-7-9-;0;0;0;1;4;127034,9;0;2;59380,3;5;23;1608,3;103;481;141,6;280;1213;59,3;198;1018;49,7;4550;19060;13,2;4802;23726;11,8;9857;49676;10,5;27863;109056;5,8;79758;370064;5,4;155317;753374;4,2;0;0;2;718,6;32;24,9;86;9,2;1495;1,3;1597;3,3;9118;2,7;16620;7;26413;2,4;269695;2,1;WE 811 6067;; +20200036;MARDI ;05/05/2020;;03/09/2020;2;27;22;10;41;3;2;-2-10-22-27-41-;-2-3-;0;0;0;1;1;398633,9;1;2;46583,6;3;12;2418,2;146;611;87,4;251;1032;54,7;247;1045;38;3246;14408;13,7;6312;25736;8,6;10718;44589;9,2;16111;72201;6,9;83586;340813;4,6;147536;623612;4;1;670,3;2;268,1;55;12,1;80;8,3;1131;1,4;2133;2,1;5529;3,8;8273;11,8;28713;1,9;209128;2,3;IU 380 3989;; +20200035;VENDREDI;01/05/2020;;30/08/2020;47;20;19;45;38;2;6;-19-20-38-45-47-;-2-6-;0;0;0;2;2;232361,2;1;4;27153,3;5;18;1879,4;83;401;155,4;233;1114;59,1;157;856;54;3869;17825;12,9;4083;19899;12,9;8598;43289;11,1;24452;103637;5,6;65937;307608;5,9;138003;664701;4,4;1;715,6;3;190,8;31;23;79;9;1226;1,4;1410;3,4;8077;2,7;13732;7,6;22215;2,6;229306;2,2;VG 211 9841;; +20200034;MARDI ;28/04/2020;;27/08/2020;32;28;26;15;23;1;5;-15-23-26-28-32-;-1-5-;0;0;0;0;2;177507,9;0;3;27657,6;6;20;1292,2;98;418;113,8;211;948;53;194;922;38,3;3083;13442;13,1;4426;19829;9,9;9514;41941;8,7;15797;68361;6,5;60583;272611;5,1;133879;588301;3,8;0;0;2;528,1;32;18,3;68;8,6;1035;1,4;1436;2,7;5367;3,4;8524;10;20340;2,3;172505;2,4;PL 337 1119;; +20200033;VENDREDI;24/04/2020;;23/08/2020;28;5;1;21;19;7;4;-1-5-19-21-28-;-4-7-;0;0;0;0;2;221796,2;1;11;9424,9;16;58;556,7;275;838;70,9;600;1956;32,1;464;1666;26,5;6791;24446;9;9026;32242;7,6;16630;63734;7,2;30010;112741;4,9;104992;397681;4,4;194001;789050;3,5;0;0;6;219,5;98;7,4;188;3,8;2218;0,8;3045;1,6;9586;2,4;13989;7,6;34973;1,7;219530;2,4;MP 768 7600;; +20200032;MARDI ;21/04/2020;;20/08/2020;11;10;23;14;28;6;8;-10-11-14-23-28-;-6-8-;0;0;0;0;0;0;3;9;44642,1;14;36;658,5;206;630;69,3;340;1220;37,8;345;1302;24,9;4087;15103;10,7;6360;23666;7,6;13598;49650;6,7;17802;69348;5,8;73917;289915;4,4;153393;613372;3,3;0;0;4;251,2;66;8,4;115;4,8;1387;1;2165;1,7;6227;2,8;8568;9,5;25245;1,8;156896;2,5;DZ 292 6112;; +20200031;VENDREDI;17/04/2020;;16/08/2020;37;32;45;28;16;1;11;-16-28-32-37-45-;-1-11-;0;1;67188313;0;2;248682,9;1;7;16606;5;25;1448,2;97;424;157,3;169;846;83,3;329;1382;35,8;2625;13216;18,7;4451;20091;13,7;12204;54040;9,5;14870;72658;8,5;61970;289473;6,7;171474;765397;4,1;0;0;1;1460,9;33;24,5;59;13,7;923;2,1;1508;3,6;5361;4,7;9735;12,1;20950;3,1;217281;2,7;EP 941 6404;; +20200030;MARDI ;14/04/2020;;13/08/2020;40;34;29;6;35;11;4;-6-29-34-35-40-;-4-11-;0;0;0;0;1;357319,7;0;0;0;1;11;9956,6;52;249;192,4;96;580;87,3;140;630;56,5;1764;9521;18,6;2548;12558;15,8;6130;29444;12,5;11053;56431;7,9;42425;205136;6,8;105120;488245;4,6;0;0;0;0;16;101,2;29;19,9;613;2,3;913;4,2;3877;4,7;7147;11,8;14584;3,2;164942;2,5;RY 887 0333;; +20200029;VENDREDI;10/04/2020;;09/08/2020;33;17;3;39;10;1;4;-3-10-17-33-39-;-1-4-;0;0;0;0;0;0;0;6;97603,5;3;27;1279,8;113;621;102,5;247;1122;59,9;340;1499;31,5;3718;15836;14,9;6031;25977;10,1;14268;61580;7,9;19161;81248;7,3;86858;361603;5,1;198160;837155;3,6;0;0;1;1425,2;35;22,6;92;8,6;1226;1,6;1910;2,7;6197;4;10184;11,3;28324;2,3;222943;2,5;CK 238 9831;; +20200028;MARDI ;07/04/2020;;07/06/2020;33;14;4;48;5;7;12;-4-5-14-33-48-;-7-12-;0;0;0;1;3;115795,1;0;2;40594,8;1;8;3161;62;293;158,9;134;580;84,9;155;722;47,9;1962;8985;19,2;3269;14646;13,1;7709;35661;10;10466;48459;8,9;53752;232864;5,8;124938;557176;3,9;1;560,7;0;0;30;33,6;47;11,9;692;2;1168;3,2;3719;4,7;6097;13,4;18784;2,4;159077;2,5;IB 144 9020;; +20200027;VENDREDI;03/04/2020;;03/06/2020;50;16;34;19;46;2;6;-16-19-34-46-50-;-2-6-;0;0;0;0;1;436604,6;0;1;102041,7;10;27;1177,1;83;395;148,2;222;957;64,6;154;862;50,4;3649;16390;13,2;3601;18322;13,2;7629;39155;11,5;22408;94506;5,7;62074;288016;5,9;128638;615452;4,5;0;0;3;415,6;27;25,6;84;8,2;1179;1,4;1142;4;7313;2,9;13574;7,4;20687;2,7;228328;2,2;ST 896 1391;; +20200026;MARDI ;31/03/2020;;31/05/2020;30;12;17;24;36;9;1;-12-17-24-30-36-;-1-9-;0;0;0;0;0;0;1;8;48887,1;4;20;1153,8;91;447;95,1;221;883;50,8;233;1017;31;2860;12204;12,9;3944;17399;10,1;9196;40152;8,1;14397;62225;6,3;51907;231617;5,4;120194;537703;3,7;0;0;2;442,1;29;16,9;77;6,3;949;1,2;1377;2,3;4665;3,3;7601;9,4;17633;2,2;135411;2,6;GO 523 3393;; +20200025;VENDREDI;27/03/2020;;27/05/2020;14;41;27;46;11;3;2;-11-14-27-41-46-;-2-3-;0;0;0;0;3;134054,9;1;4;23498,1;3;11;2661,4;102;515;104,7;161;811;70,2;202;922;43,4;2748;12637;15,8;4760;20959;10,6;8895;40640;10,2;14485;68974;7,3;71085;309679;5,1;131050;594393;4,3;0;0;3;376,1;31;20,2;45;13,9;886;1,7;1528;2,7;4730;4,1;7953;11,5;23011;2,2;211511;2,1;EQ 601 1758;; +20200024;MARDI ;24/03/2020;;24/05/2020;33;12;7;26;30;11;10;-7-12-26-30-33-;-10-11-;0;0;0;0;0;0;1;7;50422,7;1;14;1487,6;87;342;112,1;125;517;78,4;252;984;28,9;1458;7338;19,4;2919;13096;12,1;10802;41534;7,1;7204;38881;9,2;39585;181198;6,2;137600;559969;3,2;0;0;1;817,7;37;12,2;42;10,8;487;2,3;1044;2,9;2510;5,7;3783;17,5;13990;2,6;102953;3,2;IW 207 3107;; +20200023;VENDREDI;20/03/2020;;20/05/2020;40;9;20;34;3;6;12;-3-9-20-34-40-;-6-12-;1;1;17000000;0;1;410858,5;1;5;19204,8;3;17;1759,3;87;372;148,1;117;646;90,1;221;1026;39,8;2050;10259;19,9;3598;17906;12,7;10256;48304;8,7;10847;54381;9,4;53601;257346;6,3;154505;705250;3,7;0;0;0;0;28;61,8;47;13,1;776;1,9;1197;3,4;4057;4,8;6852;13,1;18119;2,8;157956;2,8;DQ 754 7123;; +20200022;MARDI ;17/03/2020;;17/05/2020;5;7;16;8;20;12;2;-5-7-8-16-20-;-2-12-;0;1;63600033;0;1;404531,9;4;10;9454,5;7;20;1472,4;198;663;81,8;211;909;63;543;1871;21,5;2651;11361;17,7;6485;24878;9;17750;65329;6,4;11916;56137;9;73342;308690;5,1;198269;802072;3,2;0;0;3;387,4;83;7,7;59;10,9;984;1,6;2382;1,8;4370;4,6;6635;14,2;25916;2;158797;2,9;EY 698 5252;; +20200021;VENDREDI;13/03/2020;;13/05/2020;21;3;11;36;27;10;9;-3-11-21-27-36-;-9-10-;0;0;0;1;5;159677,5;3;10;18659,6;8;28;2075,7;269;1050;101,9;363;1458;77,6;605;2496;31,8;5001;21469;18,5;11144;43876;10,1;27439;107944;7,6;25756;108210;9,2;150457;586799;5,3;371375;1433503;3,5;0;0;3;807,9;91;14,7;102;13,2;1637;2;3525;2,5;8319;5;13443;14,6;47778;2,3;369298;2,6;OZ 957 7619;; +20200020;MARDI ;10/03/2020;;10/05/2020;23;43;30;11;37;5;12;-11-23-30-37-43-;-5-12-;0;0;0;1;4;132535,6;5;11;11263,9;10;30;1286,4;125;618;115;185;978;76,8;299;1393;37,9;2433;13649;19,3;4725;25024;11,7;12233;60287;9;12982;72480;9,1;66613;352601;5,9;174611;865052;3,8;1;756,7;3;201,8;39;19,4;63;12;863;2,1;1614;3,1;4530;5,2;8031;13,7;23095;2,7;205242;2,6;PZ 739 9603;; +20200019;VENDREDI;06/03/2020;;06/05/2020;15;43;38;45;46;11;1;-15-38-43-45-46-;-1-11-;0;0;0;0;3;233153,1;0;2;81737,5;4;24;2121,6;69;454;206,6;217;1195;82,9;234;1239;56,2;3214;17365;20;4417;24578;15,8;11602;61429;11,7;18405;98569;8,8;69935;375415;7,3;186511;960898;4,6;0;0;1;1821,9;19;53,2;77;13,1;1135;2,2;1523;4,4;6493;4,9;12454;11,8;24045;3,4;281164;2,6;MU 910 9419;; +20200018;MARDI ;03/03/2020;;03/05/2020;27;44;4;14;45;10;1;-4-14-27-44-45-;-1-10-;0;0;0;1;2;250722,9;3;8;14649,5;10;31;1177,5;105;515;130,5;205;942;75,4;239;1323;37,7;2573;12916;19,3;4267;21927;12,7;11853;60320;8,5;13470;69504;9;62072;311126;6,3;169129;849665;3,7;1;746,3;4;149,2;38;19,6;64;11,6;988;1,8;1567;3,1;5025;4,6;8320;13;22599;2,7;196089;2,7;FW 423 7925;; +20200017;VENDREDI;28/02/2020;;29/04/2020;8;11;23;20;22;4;3;-8-11-20-22-23-;-3-4-;0;1;67084644;1;4;197637,5;1;6;30794;18;86;669,1;350;1460;72,6;494;2458;45,5;644;2778;28,3;6382;31146;12,6;12964;56318;7,7;24659;110228;7,4;28327;142449;6,9;156684;718242;4,3;299898;1408162;3,5;0;0;5;419,1;116;10;164;7;2091;1,3;4363;1,7;9675;3,7;14037;12,1;53122;1,8;339607;2,4;JI 004 5663;; +20200016;MARDI ;25/02/2020;;26/04/2020;4;42;18;27;1;6;4;-1-4-18-27-42-;-4-6-;0;0;0;0;2;283293,4;0;4;33105,1;10;41;1005,9;151;698;108,8;449;1862;43,1;352;1551;36,3;6194;25527;11;6922;31969;9,8;13918;66345;8,8;28941;126041;5,6;92196;437820;5,1;190199;928177;3,8;0;0;5;312,1;67;12,9;157;5,5;2348;0,9;2627;2,2;10602;2,5;15607;8,1;34304;2;246971;2,5;OQ 161 5938;; +20200015;VENDREDI;21/02/2020;;22/04/2020;32;23;30;45;7;5;9;-7-23-30-32-45-;-5-9-;0;0;0;1;4;183306,6;2;6;28561,1;14;64;834;192;1075;91,4;395;2748;37,8;401;1893;38,5;6384;35431;10,3;8177;41353;9,8;17339;83014;9,1;34181;180129;5;114601;578223;5;240724;1158067;4;0;0;5;402,8;60;18,6;131;8,5;2083;1,3;2864;2,6;11264;3,1;18536;8,8;39409;2,3;337500;2,4;RE 501 9639;; +20200014;MARDI ;18/02/2020;;19/04/2020;34;32;11;38;47;10;2;-11-32-34-38-47-;-2-10-;0;0;0;0;2;262027,4;1;3;40826,7;3;22;1734;75;432;162,6;237;1051;70,6;194;1084;48,1;3073;14720;17,7;3916;21333;13,6;9325;51032;10,6;17320;81867;8;59434;313390;6,5;141417;752328;4,4;0;0;1;1483,5;24;34,3;80;10,3;1170;1,7;1488;3,7;6728;3,8;12221;9,8;22254;3;248114;2,4;NA 431 6997;; +20200013;VENDREDI;14/02/2020;;15/04/2020;5;35;20;41;49;10;6;-5-20-35-41-49-;-6-10-;0;0;0;1;3;244264,1;1;6;28544,2;3;22;2424,7;100;622;157,9;277;1413;73,5;306;1573;46,4;4027;20251;18;5845;27721;14,6;14149;67779;11,1;24351;114200;8;95066;433629;6,6;232793;1067373;4,3;0;0;0;0;35;95,8;117;10,2;1408;2,1;2025;3,9;8757;4,3;15773;11;33374;2,9;354316;2,4;MO 198 0573;; +20200012;MARDI ;11/02/2020;;12/04/2020;43;26;24;32;46;6;10;-24-26-32-43-46-;-6-10-;0;1;17000000;1;5;105507,3;0;5;24658,7;3;22;1745,5;93;441;160,4;210;1016;73,6;232;1073;48,9;2822;15248;17,2;4476;21754;13,4;9566;49845;10,9;15585;82255;8;63583;319038;6,5;142757;745826;4,4;0;0;0;0;42;53,1;79;10;991;2;1582;3,3;5586;4,4;10009;11,6;22658;2,9;238130;2,4;VQ 986 5562;; +20200011;VENDREDI;07/02/2020;;08/04/2020;17;15;9;25;40;9;3;-9-15-17-25-40-;-3-9-;0;1;130000000;3;10;130443,5;1;21;14517,5;33;125;759,6;351;1753;99,7;1122;4818;38,3;758;3470;37,4;15264;64186;10,1;17574;78373;9,2;35075;154867;8,7;74755;316390;5,1;240147;1051780;4,8;491544;2131657;3,8;2;996,3;8;199,2;91;21,8;347;5,7;4731;1;5490;2,4;23186;2,7;34382;8,4;74820;2,1;577805;2,5;PX 206 8108;; +20200010;MARDI ;04/02/2020;;05/04/2020;35;21;33;23;47;6;7;-21-23-33-35-47-;-6-7-;0;0;0;0;4;155596,4;0;9;16162,4;4;47;963,9;97;659;126,6;278;1573;56;222;1461;42,4;3826;22081;14;5207;32479;10,6;9613;61757;10,4;20273;116314;6,7;80717;473901;5,1;144653;910511;4,3;0;0;1;1350,4;34;22;80;9,3;1192;1,5;1706;2,9;6533;3,6;11927;9,1;25985;2,3;261869;2;JF 573 7673;; From 1742f3955565e4aed03997ca5e2fdcec65b32505 Mon Sep 17 00:00:00 2001 From: keyobs Date: Sun, 9 Aug 2026 21:06:50 +0200 Subject: [PATCH 04/68] feat(domain): add Grid model with validation and Draw type Grid enforces the V1 invariants (5 distinct numbers 1-50, 2 distinct stars 1-12, normalized ascending order) via a zod schema. Draw extends it with the historical-draw fields (id, date, source). Scaffolds domain/{grid,draw}/ per the V1 architecture. --- src/domain/draw/Draw.ts | 10 +++++++++ src/domain/grid/Grid.test.ts | 41 ++++++++++++++++++++++++++++++++++++ src/domain/grid/Grid.ts | 29 +++++++++++++++++++++++++ 3 files changed, 80 insertions(+) create mode 100644 src/domain/draw/Draw.ts create mode 100644 src/domain/grid/Grid.test.ts create mode 100644 src/domain/grid/Grid.ts diff --git a/src/domain/draw/Draw.ts b/src/domain/draw/Draw.ts new file mode 100644 index 0000000..dd0a115 --- /dev/null +++ b/src/domain/draw/Draw.ts @@ -0,0 +1,10 @@ +import type { Grid } from '../grid/Grid.ts'; + +export interface Draw { + id: string; + date: string; + numbers: Grid['numbers']; + stars: Grid['stars']; + jackpot?: number; + source: 'fdj-csv' | 'api' | 'manual'; +} diff --git a/src/domain/grid/Grid.test.ts b/src/domain/grid/Grid.test.ts new file mode 100644 index 0000000..47d6910 --- /dev/null +++ b/src/domain/grid/Grid.test.ts @@ -0,0 +1,41 @@ +import { describe, expect, it } from 'vitest'; +import { parseGrid } from './Grid.ts'; + +describe('parseGrid', () => { + it('normalizes numbers and stars in ascending order', () => { + const grid = parseGrid({ numbers: [42, 3, 19, 7, 31], stars: [9, 2] }); + + expect(grid.numbers).toEqual([3, 7, 19, 31, 42]); + expect(grid.stars).toEqual([2, 9]); + }); + + it('is invariant to input order', () => { + const a = parseGrid({ numbers: [1, 2, 3, 4, 5], stars: [1, 2] }); + const b = parseGrid({ numbers: [5, 4, 3, 2, 1], stars: [2, 1] }); + + expect(a).toEqual(b); + }); + + it('rejects duplicate numbers', () => { + expect(() => parseGrid({ numbers: [1, 1, 3, 4, 5], stars: [1, 2] })).toThrow(); + }); + + it('rejects duplicate stars', () => { + expect(() => parseGrid({ numbers: [1, 2, 3, 4, 5], stars: [1, 1] })).toThrow(); + }); + + it('rejects numbers out of the 1..50 range', () => { + expect(() => parseGrid({ numbers: [0, 2, 3, 4, 5], stars: [1, 2] })).toThrow(); + expect(() => parseGrid({ numbers: [1, 2, 3, 4, 51], stars: [1, 2] })).toThrow(); + }); + + it('rejects stars out of the 1..12 range', () => { + expect(() => parseGrid({ numbers: [1, 2, 3, 4, 5], stars: [0, 2] })).toThrow(); + expect(() => parseGrid({ numbers: [1, 2, 3, 4, 5], stars: [1, 13] })).toThrow(); + }); + + it('rejects the wrong count of numbers or stars', () => { + expect(() => parseGrid({ numbers: [1, 2, 3, 4], stars: [1, 2] })).toThrow(); + expect(() => parseGrid({ numbers: [1, 2, 3, 4, 5], stars: [1] })).toThrow(); + }); +}); diff --git a/src/domain/grid/Grid.ts b/src/domain/grid/Grid.ts new file mode 100644 index 0000000..6ddea7b --- /dev/null +++ b/src/domain/grid/Grid.ts @@ -0,0 +1,29 @@ +import { z } from 'zod'; + +export interface Grid { + numbers: [number, number, number, number, number]; + stars: [number, number]; +} + +const distinctSortedTuple = (min: number, max: number, count: number) => + z + .array(z.number().int().min(min).max(max)) + .length(count) + .refine((values) => new Set(values).size === count, { + message: `Expected ${count} distinct values between ${min} and ${max}`, + }) + .transform((values) => [...values].sort((a, b) => a - b)); + +export const gridSchema = z + .object({ + numbers: distinctSortedTuple(1, 50, 5), + stars: distinctSortedTuple(1, 12, 2), + }) + .transform( + ({ numbers, stars }): Grid => ({ + numbers: numbers as Grid['numbers'], + stars: stars as Grid['stars'], + }), + ); + +export const parseGrid = (input: unknown): Grid => gridSchema.parse(input); From 83164f6f53adfb4581a929506484657c38d35ba6 Mon Sep 17 00:00:00 2001 From: keyobs Date: Sun, 9 Aug 2026 21:07:03 +0200 Subject: [PATCH 05/68] feat(csv): parse the FDJ export into Draw[] Reads the already-sorted boules_gagnantes_en_ordre_croissant / etoiles_gagnantes_en_ordre_croissant columns and routes them through parseGrid so every parsed Draw carries the same distinctness/range guarantees as a hand-entered Grid. --- src/infrastructure/csv/parseFdjCsv.test.ts | 48 ++++++++++++++++++++++ src/infrastructure/csv/parseFdjCsv.ts | 45 ++++++++++++++++++++ 2 files changed, 93 insertions(+) create mode 100644 src/infrastructure/csv/parseFdjCsv.test.ts create mode 100644 src/infrastructure/csv/parseFdjCsv.ts diff --git a/src/infrastructure/csv/parseFdjCsv.test.ts b/src/infrastructure/csv/parseFdjCsv.test.ts new file mode 100644 index 0000000..c6014fd --- /dev/null +++ b/src/infrastructure/csv/parseFdjCsv.test.ts @@ -0,0 +1,48 @@ +import { readFileSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; +import { describe, expect, it } from 'vitest'; +import { parseFdjCsv } from './parseFdjCsv.ts'; + +const REAL_CSV_PATH = fileURLToPath( + new URL('../../../public/results/euromillions_202002.csv', import.meta.url), +); + +describe('parseFdjCsv', () => { + it('parses a minimal FDJ-shaped CSV into Draws', () => { + const csv = [ + 'annee_numero_de_tirage;date_de_tirage;boules_gagnantes_en_ordre_croissant;etoiles_gagnantes_en_ordre_croissant', + '26063;07/08/2026;-26-29-35-38-47-;-1-2-', + '26062;04/08/2026;-25-30-34-46-50-;-1-12-', + ].join('\n'); + + const draws = parseFdjCsv(csv); + + expect(draws).toEqual([ + { id: '26063', date: '2026-08-07', numbers: [26, 29, 35, 38, 47], stars: [1, 2], source: 'fdj-csv' }, + { id: '26062', date: '2026-08-04', numbers: [25, 30, 34, 46, 50], stars: [1, 12], source: 'fdj-csv' }, + ]); + }); + + it('throws on a malformed row (invalid grid)', () => { + const csv = [ + 'annee_numero_de_tirage;date_de_tirage;boules_gagnantes_en_ordre_croissant;etoiles_gagnantes_en_ordre_croissant', + '1;01/01/2024;-1-1-3-4-5-;-1-2-', + ].join('\n'); + + expect(() => parseFdjCsv(csv)).toThrow(); + }); + + it('parses the full real FDJ export without throwing', () => { + const csvText = readFileSync(REAL_CSV_PATH, 'utf8'); + + const draws = parseFdjCsv(csvText); + + expect(draws.length).toBeGreaterThan(600); + for (const draw of draws) { + expect(draw.numbers).toHaveLength(5); + expect(draw.stars).toHaveLength(2); + expect(draw.source).toBe('fdj-csv'); + expect(draw.date).toMatch(/^\d{4}-\d{2}-\d{2}$/); + } + }); +}); diff --git a/src/infrastructure/csv/parseFdjCsv.ts b/src/infrastructure/csv/parseFdjCsv.ts new file mode 100644 index 0000000..1de13fc --- /dev/null +++ b/src/infrastructure/csv/parseFdjCsv.ts @@ -0,0 +1,45 @@ +import type { Draw } from '../../domain/draw/Draw.ts'; +import { parseGrid } from '../../domain/grid/Grid.ts'; + +const DASH_NUMBER = /-(\d+)/g; + +const parseDashList = (value: string): number[] => + [...value.matchAll(DASH_NUMBER)].map((match) => Number(match[1])); + +const parseFdjDate = (value: string): string => { + const [day, month, year] = value.split('/'); + return `${year}-${month}-${day}`; +}; + +const columnIndex = (columns: string[], name: string): number => { + const index = columns.indexOf(name); + if (index === -1) throw new Error(`Missing FDJ CSV column: ${name}`); + return index; +}; + +export const parseFdjCsv = (csvText: string): Draw[] => { + const lines = csvText.split(/\r?\n/).filter((line) => line.trim().length > 0); + const [headerLine, ...rows] = lines; + const columns = headerLine.split(';'); + + const idIndex = columnIndex(columns, 'annee_numero_de_tirage'); + const dateIndex = columnIndex(columns, 'date_de_tirage'); + const numbersIndex = columnIndex(columns, 'boules_gagnantes_en_ordre_croissant'); + const starsIndex = columnIndex(columns, 'etoiles_gagnantes_en_ordre_croissant'); + + return rows.map((row) => { + const cells = row.split(';'); + const grid = parseGrid({ + numbers: parseDashList(cells[numbersIndex]), + stars: parseDashList(cells[starsIndex]), + }); + + return { + id: cells[idIndex], + date: parseFdjDate(cells[dateIndex]), + numbers: grid.numbers, + stars: grid.stars, + source: 'fdj-csv', + } satisfies Draw; + }); +}; From 0df59ac97f4d2a2a508fab64ffa7a39d98967e89 Mon Sep 17 00:00:00 2001 From: keyobs Date: Sun, 9 Aug 2026 21:07:12 +0200 Subject: [PATCH 06/68] feat(domain): add FeatureExtractor and GeometryDescriptor extractFeatures computes the V1 minimum feature set (sum/range/stats, decade buckets, entropy, parity, gaps, consecutive/same-units/>31 counts, star sum/diff/parity) from a Grid alone, with a stable key order (FEATURE_KEYS) for downstream vectorization. buildGeometryDescriptor reshapes that into the exact V1-spec GeometryDescriptor shape. --- src/domain/features/FeatureExtractor.test.ts | 55 +++++++ src/domain/features/FeatureExtractor.ts | 140 ++++++++++++++++++ .../geometry/GeometryDescriptor.test.ts | 32 ++++ src/domain/geometry/GeometryDescriptor.ts | 32 ++++ 4 files changed, 259 insertions(+) create mode 100644 src/domain/features/FeatureExtractor.test.ts create mode 100644 src/domain/features/FeatureExtractor.ts create mode 100644 src/domain/geometry/GeometryDescriptor.test.ts create mode 100644 src/domain/geometry/GeometryDescriptor.ts diff --git a/src/domain/features/FeatureExtractor.test.ts b/src/domain/features/FeatureExtractor.test.ts new file mode 100644 index 0000000..a45ed9a --- /dev/null +++ b/src/domain/features/FeatureExtractor.test.ts @@ -0,0 +1,55 @@ +import { describe, expect, it } from 'vitest'; +import { parseGrid } from '../grid/Grid.ts'; +import { FEATURE_KEYS, extractFeatures, featureVectorToArray } from './FeatureExtractor.ts'; + +describe('extractFeatures', () => { + it('is deterministic for the same grid', () => { + const grid = parseGrid({ numbers: [3, 7, 19, 31, 42], stars: [2, 9] }); + + expect(extractFeatures(grid)).toEqual(extractFeatures(grid)); + }); + + it('is invariant to the order numbers/stars were originally given in', () => { + const a = parseGrid({ numbers: [42, 3, 19, 7, 31], stars: [9, 2] }); + const b = parseGrid({ numbers: [3, 7, 19, 31, 42], stars: [2, 9] }); + + expect(extractFeatures(a)).toEqual(extractFeatures(b)); + }); + + it('computes sum, range and parity counts consistently', () => { + const grid = parseGrid({ numbers: [3, 7, 19, 31, 42], stars: [2, 9] }); + const { values } = extractFeatures(grid); + + expect(values.sum).toBe(3 + 7 + 19 + 31 + 42); + expect(values.min).toBe(3); + expect(values.max).toBe(42); + expect(values.range).toBe(39); + expect(values.oddCount + values.evenCount).toBe(5); + expect(values.decade1 + values.decade2 + values.decade3 + values.decade4 + values.decade5).toBe(5); + }); + + it('computes the four successive gaps from the sorted numbers', () => { + const grid = parseGrid({ numbers: [3, 7, 19, 31, 42], stars: [2, 9] }); + const { values } = extractFeatures(grid); + + expect([values.gap1, values.gap2, values.gap3, values.gap4]).toEqual([4, 12, 12, 11]); + }); + + it('flags a number above 31 and a pair of consecutive numbers', () => { + const grid = parseGrid({ numbers: [3, 4, 19, 31, 42], stars: [2, 9] }); + const { values } = extractFeatures(grid); + + expect(values.consecutivePairsCount).toBe(1); + expect(values.aboveThirtyOneCount).toBe(1); + }); + + it('exposes one array entry per declared feature key, in a stable order', () => { + const grid = parseGrid({ numbers: [3, 7, 19, 31, 42], stars: [2, 9] }); + const vector = extractFeatures(grid); + + const array = featureVectorToArray(vector); + + expect(array).toHaveLength(FEATURE_KEYS.length); + expect(array.every((value) => Number.isFinite(value))).toBe(true); + }); +}); diff --git a/src/domain/features/FeatureExtractor.ts b/src/domain/features/FeatureExtractor.ts new file mode 100644 index 0000000..0117bb1 --- /dev/null +++ b/src/domain/features/FeatureExtractor.ts @@ -0,0 +1,140 @@ +import type { Grid } from '../grid/Grid.ts'; + +export interface FeatureVector { + values: Record; +} + +export const FEATURE_KEYS = [ + 'sum', + 'mean', + 'median', + 'min', + 'max', + 'range', + 'variance', + 'stdDev', + 'decade1', + 'decade2', + 'decade3', + 'decade4', + 'decade5', + 'occupiedBucketCount', + 'clusterSizeMax', + 'entropy', + 'oddCount', + 'evenCount', + 'parityAlternations', + 'gap1', + 'gap2', + 'gap3', + 'gap4', + 'gapMean', + 'gapStdDev', + 'consecutivePairsCount', + 'sameUnitsPairsCount', + 'multiplesOfFiveCount', + 'aboveThirtyOneCount', + 'starsSum', + 'starsDiff', + 'starsOddCount', +] as const; + +export type FeatureKey = (typeof FEATURE_KEYS)[number]; + +export const extractFeatures = (grid: Grid): FeatureVector => { + const { numbers, stars } = grid; + + const sum = numbers.reduce((acc, n) => acc + n, 0); + const mean = sum / numbers.length; + const median = numbers[2]; + const min = numbers[0]; + const max = numbers[4]; + const range = max - min; + const variance = numbers.reduce((acc, n) => acc + (n - mean) ** 2, 0) / numbers.length; + const stdDev = Math.sqrt(variance); + + const decadeBuckets = [0, 0, 0, 0, 0]; + for (const n of numbers) { + decadeBuckets[Math.min(Math.floor((n - 1) / 10), 4)] += 1; + } + const occupiedBucketCount = decadeBuckets.filter((count) => count > 0).length; + const clusterSizeMax = Math.max(...decadeBuckets); + const entropy = decadeBuckets + .filter((count) => count > 0) + .reduce((acc, count) => { + const p = count / numbers.length; + return acc - p * Math.log2(p); + }, 0); + + const oddCount = numbers.filter((n) => n % 2 !== 0).length; + const evenCount = numbers.length - oddCount; + let parityAlternations = 0; + for (let i = 0; i < numbers.length - 1; i += 1) { + if (numbers[i] % 2 !== numbers[i + 1] % 2) parityAlternations += 1; + } + + const gaps: [number, number, number, number] = [ + numbers[1] - numbers[0], + numbers[2] - numbers[1], + numbers[3] - numbers[2], + numbers[4] - numbers[3], + ]; + const gapMean = gaps.reduce((acc, g) => acc + g, 0) / gaps.length; + const gapVariance = gaps.reduce((acc, g) => acc + (g - gapMean) ** 2, 0) / gaps.length; + const gapStdDev = Math.sqrt(gapVariance); + const consecutivePairsCount = gaps.filter((gap) => gap === 1).length; + + let sameUnitsPairsCount = 0; + for (let i = 0; i < numbers.length; i += 1) { + for (let j = i + 1; j < numbers.length; j += 1) { + if (numbers[i] % 10 === numbers[j] % 10) sameUnitsPairsCount += 1; + } + } + + const multiplesOfFiveCount = numbers.filter((n) => n % 5 === 0).length; + const aboveThirtyOneCount = numbers.filter((n) => n > 31).length; + + const starsSum = stars[0] + stars[1]; + const starsDiff = Math.abs(stars[1] - stars[0]); + const starsOddCount = stars.filter((s) => s % 2 !== 0).length; + + const values: Record = { + sum, + mean, + median, + min, + max, + range, + variance, + stdDev, + decade1: decadeBuckets[0], + decade2: decadeBuckets[1], + decade3: decadeBuckets[2], + decade4: decadeBuckets[3], + decade5: decadeBuckets[4], + occupiedBucketCount, + clusterSizeMax, + entropy, + oddCount, + evenCount, + parityAlternations, + gap1: gaps[0], + gap2: gaps[1], + gap3: gaps[2], + gap4: gaps[3], + gapMean, + gapStdDev, + consecutivePairsCount, + sameUnitsPairsCount, + multiplesOfFiveCount, + aboveThirtyOneCount, + starsSum, + starsDiff, + starsOddCount, + }; + + return { values }; +}; + +export const featureVectorToArray = (vector: FeatureVector): number[] => + FEATURE_KEYS.map((key) => vector.values[key]); diff --git a/src/domain/geometry/GeometryDescriptor.test.ts b/src/domain/geometry/GeometryDescriptor.test.ts new file mode 100644 index 0000000..2baa74b --- /dev/null +++ b/src/domain/geometry/GeometryDescriptor.test.ts @@ -0,0 +1,32 @@ +import { describe, expect, it } from 'vitest'; +import { parseGrid } from '../grid/Grid.ts'; +import { buildGeometryDescriptor } from './GeometryDescriptor.ts'; + +describe('buildGeometryDescriptor', () => { + it('mirrors the underlying FeatureVector values in the V1-spec shape', () => { + const grid = parseGrid({ numbers: [3, 7, 19, 31, 42], stars: [2, 9] }); + const descriptor = buildGeometryDescriptor(grid); + const { values } = descriptor.features; + + expect(descriptor.grid).toEqual(grid); + expect(descriptor.sum).toBe(values.sum); + expect(descriptor.range).toBe(values.range); + expect(descriptor.oddCount).toBe(values.oddCount); + expect(descriptor.evenCount).toBe(values.evenCount); + expect(descriptor.clusterSizeMax).toBe(values.clusterSizeMax); + expect(descriptor.decadeBuckets).toEqual([ + values.decade1, + values.decade2, + values.decade3, + values.decade4, + values.decade5, + ]); + expect(descriptor.gaps).toEqual([values.gap1, values.gap2, values.gap3, values.gap4]); + }); + + it('is a pure, deterministic function of the grid', () => { + const grid = parseGrid({ numbers: [3, 7, 19, 31, 42], stars: [2, 9] }); + + expect(buildGeometryDescriptor(grid)).toEqual(buildGeometryDescriptor(grid)); + }); +}); diff --git a/src/domain/geometry/GeometryDescriptor.ts b/src/domain/geometry/GeometryDescriptor.ts new file mode 100644 index 0000000..b6de59c --- /dev/null +++ b/src/domain/geometry/GeometryDescriptor.ts @@ -0,0 +1,32 @@ +import type { Grid } from '../grid/Grid.ts'; +import type { FeatureVector } from '../features/FeatureExtractor.ts'; +import { extractFeatures } from '../features/FeatureExtractor.ts'; + +export interface GeometryDescriptor { + grid: Grid; + features: FeatureVector; + decadeBuckets: [number, number, number, number, number]; + gaps: [number, number, number, number]; + sum: number; + range: number; + oddCount: number; + evenCount: number; + clusterSizeMax: number; +} + +export const buildGeometryDescriptor = (grid: Grid): GeometryDescriptor => { + const features = extractFeatures(grid); + const v = features.values; + + return { + grid, + features, + decadeBuckets: [v.decade1, v.decade2, v.decade3, v.decade4, v.decade5], + gaps: [v.gap1, v.gap2, v.gap3, v.gap4], + sum: v.sum, + range: v.range, + oddCount: v.oddCount, + evenCount: v.evenCount, + clusterSizeMax: v.clusterSizeMax, + }; +}; From baf421e8a58ab34d75f2150b3c12c50928e08895 Mon Sep 17 00:00:00 2001 From: keyobs Date: Sun, 9 Aug 2026 21:07:21 +0200 Subject: [PATCH 07/68] feat(pca): project draws into SpatialEmbedding-shaped 3D coordinates buildSpatialEmbeddings runs every draw's FeatureVector through ml-pca (centered + scaled, matching V3's StandardScaler default) and takes PC1/PC2/PC3 as x/y/z. density/outlierScore/nearestNeighbors are stubbed (0/0/[]) since clustering and density are DiscoveryModel (V3) work, explicitly deferred by ADR-0002 - only the SpatialEmbedding shape and position are real here. --- .../buildSpatialEmbeddings.test.ts | 27 +++++++++++++++++++ src/application/buildSpatialEmbeddings.ts | 25 +++++++++++++++++ src/domain/geometry/SpatialEmbedding.ts | 18 +++++++++++++ 3 files changed, 70 insertions(+) create mode 100644 src/application/buildSpatialEmbeddings.test.ts create mode 100644 src/application/buildSpatialEmbeddings.ts create mode 100644 src/domain/geometry/SpatialEmbedding.ts diff --git a/src/application/buildSpatialEmbeddings.test.ts b/src/application/buildSpatialEmbeddings.test.ts new file mode 100644 index 0000000..35a8989 --- /dev/null +++ b/src/application/buildSpatialEmbeddings.test.ts @@ -0,0 +1,27 @@ +import { readFileSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; +import { describe, expect, it } from 'vitest'; +import { parseFdjCsv } from '../infrastructure/csv/parseFdjCsv.ts'; +import { buildSpatialEmbeddings } from './buildSpatialEmbeddings.ts'; + +const REAL_CSV_PATH = fileURLToPath( + new URL('../../public/results/euromillions_202002.csv', import.meta.url), +); + +describe('buildSpatialEmbeddings', () => { + it('produces one finite 3D SpatialEmbedding per draw from the real dataset', () => { + const csvText = readFileSync(REAL_CSV_PATH, 'utf8'); + const draws = parseFdjCsv(csvText); + + const embeddings = buildSpatialEmbeddings(draws); + + expect(embeddings).toHaveLength(draws.length); + embeddings.forEach((embedding, index) => { + expect(embedding.drawId).toBe(draws[index].id); + expect(embedding.method).toBe('pca'); + expect(Number.isFinite(embedding.coordinates.x)).toBe(true); + expect(Number.isFinite(embedding.coordinates.y)).toBe(true); + expect(Number.isFinite(embedding.coordinates.z)).toBe(true); + }); + }); +}); diff --git a/src/application/buildSpatialEmbeddings.ts b/src/application/buildSpatialEmbeddings.ts new file mode 100644 index 0000000..5f5237a --- /dev/null +++ b/src/application/buildSpatialEmbeddings.ts @@ -0,0 +1,25 @@ +import { PCA } from 'ml-pca'; +import type { Draw } from '../domain/draw/Draw.ts'; +import { extractFeatures, featureVectorToArray } from '../domain/features/FeatureExtractor.ts'; +import type { SpatialEmbedding } from '../domain/geometry/SpatialEmbedding.ts'; + +export const buildSpatialEmbeddings = (draws: Draw[]): SpatialEmbedding[] => { + const matrix = draws.map((draw) => + featureVectorToArray(extractFeatures({ numbers: draw.numbers, stars: draw.stars })), + ); + + const pca = new PCA(matrix, { center: true, scale: true }); + const projected = pca.predict(matrix, { nComponents: 3 }).to2DArray(); + + return draws.map((draw, index): SpatialEmbedding => { + const [x, y, z] = projected[index]; + return { + drawId: draw.id, + coordinates: { x, y, z }, + method: 'pca', + density: 0, + outlierScore: 0, + nearestNeighbors: [], + }; + }); +}; diff --git a/src/domain/geometry/SpatialEmbedding.ts b/src/domain/geometry/SpatialEmbedding.ts new file mode 100644 index 0000000..839d56e --- /dev/null +++ b/src/domain/geometry/SpatialEmbedding.ts @@ -0,0 +1,18 @@ +export type EmbeddingMethod = 'pca' | 'umap'; + +export interface SpatialEmbedding { + drawId: string; + coordinates: { + x: number; + y: number; + z: number; + }; + method: EmbeddingMethod; + clusterId?: string; + density: number; + outlierScore: number; + nearestNeighbors: { + drawId: string; + distance: number; + }[]; +} From dc52e13531855e36dcc86bd7d59c2e6939cb3ef0 Mon Sep 17 00:00:00 2001 From: keyobs Date: Sun, 9 Aug 2026 21:07:31 +0200 Subject: [PATCH 08/68] feat(spike): render the SpatialEmbedding as a bare three.js point cloud App fetches the FDJ CSV, parses it, builds SpatialEmbeddings, and hands them to PointCloudScene (@react-three/fiber Canvas + OrbitControls, one point per draw at its PCA coordinates). Closes the ADR-0002 spike: Grid -> FeatureExtractor -> GeometryDescriptor -> PCA -> point cloud. Lives in src/spike/, separate from the real pages/geometry and features/eurospace that will replace it later. --- src/App.tsx | 36 +++++++++++++++++++++++++++++++---- src/spike/PointCloudScene.tsx | 34 +++++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+), 4 deletions(-) create mode 100644 src/spike/PointCloudScene.tsx diff --git a/src/App.tsx b/src/App.tsx index cdb9eb2..a29300b 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,5 +1,33 @@ -function App() { - return null -} +import { useEffect, useState } from 'react'; +import type { SpatialEmbedding } from './domain/geometry/SpatialEmbedding.ts'; +import { parseFdjCsv } from './infrastructure/csv/parseFdjCsv.ts'; +import { buildSpatialEmbeddings } from './application/buildSpatialEmbeddings.ts'; +import PointCloudScene from './spike/PointCloudScene.tsx'; -export default App +const CSV_URL = '/results/euromillions_202002.csv'; + +const App = () => { + const [embeddings, setEmbeddings] = useState(null); + + useEffect(() => { + let cancelled = false; + + fetch(CSV_URL) + .then((response) => response.text()) + .then((csvText) => { + if (cancelled) return; + const draws = parseFdjCsv(csvText); + setEmbeddings(buildSpatialEmbeddings(draws)); + }); + + return () => { + cancelled = true; + }; + }, []); + + if (!embeddings) return null; + + return ; +}; + +export default App; diff --git a/src/spike/PointCloudScene.tsx b/src/spike/PointCloudScene.tsx new file mode 100644 index 0000000..21999ff --- /dev/null +++ b/src/spike/PointCloudScene.tsx @@ -0,0 +1,34 @@ +import { Canvas } from '@react-three/fiber'; +import { OrbitControls } from '@react-three/drei'; +import type { SpatialEmbedding } from '../domain/geometry/SpatialEmbedding.ts'; + +interface PointCloudSceneProps { + embeddings: SpatialEmbedding[]; +} + +const PointCloudScene = ({ embeddings }: PointCloudSceneProps) => { + const positions = new Float32Array(embeddings.length * 3); + embeddings.forEach((embedding, index) => { + positions[index * 3] = embedding.coordinates.x; + positions[index * 3 + 1] = embedding.coordinates.y; + positions[index * 3 + 2] = embedding.coordinates.z; + }); + + return ( +
+ + + + + + + + + + + +
+ ); +}; + +export default PointCloudScene; From ebae9bea31695878d0bfe1adadd9243601652f4e Mon Sep 17 00:00:00 2001 From: keyobs Date: Sun, 9 Aug 2026 21:08:03 +0200 Subject: [PATCH 09/68] test(e2e): add Playwright for the dev-server smoke check Installed without its bundled browser download; driven against the system google-chrome-stable binary to visually confirm the spike's point cloud actually renders (WebGL context, no console/page errors). Pulls forward the Playwright dependency already planned in specs_v1.md. --- package.json | 1 + pnpm-lock.yaml | 29 +++++++++++++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/package.json b/package.json index 8c094bd..707d24c 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "husky": "^9.1.7", "lint-staged": "^17.3.0", "oxlint": "^1.75.0", + "playwright": "^1.62.1", "typescript": "~6.0.2", "vite": "^8.2.0", "vitest": "^4.1.10" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 813e6c7..983eaa8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -63,6 +63,9 @@ importers: oxlint: specifier: ^1.75.0 version: 1.77.0 + playwright: + specifier: ^1.62.1 + version: 1.62.1 typescript: specifier: ~6.0.2 version: 6.0.3 @@ -701,6 +704,11 @@ packages: fflate@0.8.3: resolution: {integrity: sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==} + fsevents@2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -942,6 +950,16 @@ packages: resolution: {integrity: sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==} engines: {node: '>=12'} + playwright-core@1.62.1: + resolution: {integrity: sha512-wPYSwEBJY9GHraISXqyqtx0na0LpO3XEX7jNDhntbex7tzUS7kLnZsOlFruFJB4Hi/rhDMjXGqHewDZ68nYZVw==} + engines: {node: '>=20'} + hasBin: true + + playwright@1.62.1: + resolution: {integrity: sha512-0M+L3LAD8/nm554LOla9Ayx0j0tmFZ0FBcoQ7F1VuVHpM/XpiC8RcDzBQB8W5+hA8L22THxELzeF+2WcUzvcLg==} + engines: {node: '>=20'} + hasBin: true + postcss@8.5.26: resolution: {integrity: sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==} engines: {node: ^10 || ^12 || >=14} @@ -1794,6 +1812,9 @@ snapshots: fflate@0.8.3: {} + fsevents@2.3.2: + optional: true + fsevents@2.3.3: optional: true @@ -1997,6 +2018,14 @@ snapshots: picomatch@4.0.5: {} + playwright-core@1.62.1: {} + + playwright@1.62.1: + dependencies: + playwright-core: 1.62.1 + optionalDependencies: + fsevents: 2.3.2 + postcss@8.5.26: dependencies: nanoid: 3.3.17 From 73a635f099d11f90e9852ab0801554bbbe7ffaa2 Mon Sep 17 00:00:00 2001 From: keyobs Date: Sun, 9 Aug 2026 21:22:25 +0200 Subject: [PATCH 10/68] build(v1): install router, query, charts, sass, and testing-library react-router, @tanstack/react-query, recharts, d3 for the V1 pages; sass for CSS Modules; @testing-library/react + jest-dom + jsdom for component tests. Switch vitest to the jsdom environment (needed for Testing Library) and fix the two existing tests that resolved the real CSV path via import.meta.url, which jsdom no longer serves as a file:// URL - process.cwd()-relative resolution works under both environments. --- package.json | 9 + pnpm-lock.yaml | 1477 ++++++++++++++++- pnpm-workspace.yaml | 2 + .../buildSpatialEmbeddings.test.ts | 6 +- src/infrastructure/csv/parseFdjCsv.test.ts | 6 +- src/shared/testSetup.ts | 1 + vite.config.ts | 5 +- 7 files changed, 1468 insertions(+), 38 deletions(-) create mode 100644 pnpm-workspace.yaml create mode 100644 src/shared/testSetup.ts diff --git a/package.json b/package.json index 707d24c..7fb7c4a 100644 --- a/package.json +++ b/package.json @@ -23,9 +23,13 @@ "dependencies": { "@react-three/drei": "^10.7.8", "@react-three/fiber": "^9.7.0", + "@tanstack/react-query": "^5.101.4", + "d3": "^7.9.0", "ml-pca": "^4.1.1", "react": "^19.2.8", "react-dom": "^19.2.8", + "react-router": "^8.3.0", + "recharts": "^3.10.1", "three": "^0.185.1", "zod": "^4.4.3" }, @@ -33,15 +37,20 @@ "@commitlint/cli": "^21.2.1", "@commitlint/config-conventional": "^21.2.0", "@keyobs/dx-flow": "3.1.3", + "@testing-library/jest-dom": "^7.0.0", + "@testing-library/react": "^16.3.2", + "@types/d3": "^7.4.3", "@types/node": "^24.13.3", "@types/react": "^19.2.17", "@types/react-dom": "^19.2.3", "@types/three": "^0.185.4", "@vitejs/plugin-react": "^6.0.4", "husky": "^9.1.7", + "jsdom": "^30.0.1", "lint-staged": "^17.3.0", "oxlint": "^1.75.0", "playwright": "^1.62.1", + "sass": "^1.102.0", "typescript": "~6.0.2", "vite": "^8.2.0", "vitest": "^4.1.10" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 983eaa8..37f47d1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -10,10 +10,16 @@ importers: dependencies: '@react-three/drei': specifier: ^10.7.8 - version: 10.7.8(@react-three/fiber@9.7.0(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(three@0.185.1))(@types/react@19.2.18)(@types/three@0.185.4)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(three@0.185.1) + version: 10.7.8(@react-three/fiber@9.7.0(@types/react@19.2.18)(immer@11.1.16)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(three@0.185.1))(@types/react@19.2.18)(@types/three@0.185.4)(immer@11.1.16)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(three@0.185.1) '@react-three/fiber': specifier: ^9.7.0 - version: 9.7.0(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(three@0.185.1) + version: 9.7.0(@types/react@19.2.18)(immer@11.1.16)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(three@0.185.1) + '@tanstack/react-query': + specifier: ^5.101.4 + version: 5.101.4(react@19.2.8) + d3: + specifier: ^7.9.0 + version: 7.9.0 ml-pca: specifier: ^4.1.1 version: 4.1.1 @@ -23,6 +29,12 @@ importers: react-dom: specifier: ^19.2.8 version: 19.2.8(react@19.2.8) + react-router: + specifier: ^8.3.0 + version: 8.3.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + recharts: + specifier: ^3.10.1 + version: 3.10.1(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react-is@19.2.8)(react@19.2.8)(redux@5.0.1) three: specifier: ^0.185.1 version: 0.185.1 @@ -39,6 +51,15 @@ importers: '@keyobs/dx-flow': specifier: 3.1.3 version: 3.1.3 + '@testing-library/jest-dom': + specifier: ^7.0.0 + version: 7.0.0(@testing-library/dom@10.4.1) + '@testing-library/react': + specifier: ^16.3.2 + version: 16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@types/d3': + specifier: ^7.4.3 + version: 7.4.3 '@types/node': specifier: ^24.13.3 version: 24.13.3 @@ -53,10 +74,13 @@ importers: version: 0.185.4 '@vitejs/plugin-react': specifier: ^6.0.4 - version: 6.0.5(vite@8.2.0(@types/node@24.13.3)(jiti@2.6.1)(yaml@2.9.0)) + version: 6.0.5(vite@8.2.0(@types/node@24.13.3)(jiti@2.6.1)(sass@1.102.0)(yaml@2.9.0)) husky: specifier: ^9.1.7 version: 9.1.7 + jsdom: + specifier: ^30.0.1 + version: 30.0.1 lint-staged: specifier: ^17.3.0 version: 17.3.0 @@ -66,18 +90,32 @@ importers: playwright: specifier: ^1.62.1 version: 1.62.1 + sass: + specifier: ^1.102.0 + version: 1.102.0 typescript: specifier: ~6.0.2 version: 6.0.3 vite: specifier: ^8.2.0 - version: 8.2.0(@types/node@24.13.3)(jiti@2.6.1)(yaml@2.9.0) + version: 8.2.0(@types/node@24.13.3)(jiti@2.6.1)(sass@1.102.0)(yaml@2.9.0) vitest: specifier: ^4.1.10 - version: 4.1.10(@types/node@24.13.3)(vite@8.2.0(@types/node@24.13.3)(jiti@2.6.1)(yaml@2.9.0)) + version: 4.1.10(@types/node@24.13.3)(jsdom@30.0.1)(vite@8.2.0(@types/node@24.13.3)(jiti@2.6.1)(sass@1.102.0)(yaml@2.9.0)) packages: + '@adobe/css-tools@4.5.0': + resolution: {integrity: sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q==} + + '@asamuzakjp/css-color@6.0.7': + resolution: {integrity: sha512-vC/bk1Lz7Tn/EfU9/apOTBk80/8dyGyWMowPoV1tJ52muDGsDqt2HPT2klrFUiY60MQmQv9q8yIht15JnBgDGw==} + engines: {node: ^22.13.0 || >=24.0.0} + + '@asamuzakjp/dom-selector@8.3.2': + resolution: {integrity: sha512-93Z1N+BQNXysodoicpOIyNh2drHfz/CTf9nnT0FEx72GJcIiwgydD7tGAr78j41LsYn3hlRn+LdGPuBLn1Bl8Q==} + engines: {node: ^22.13.0 || >=24.0.0} + '@babel/code-frame@7.29.7': resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==} engines: {node: '>=6.9.0'} @@ -90,6 +128,10 @@ packages: resolution: {integrity: sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==} engines: {node: '>=6.9.0'} + '@bramus/specificity@2.4.2': + resolution: {integrity: sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==} + hasBin: true + '@commitlint/cli@21.2.1': resolution: {integrity: sha512-blsZGe29hJ72VGEFVl72IVYX+1vsfINpjA9yWQA6i7OKD/McGEOXg08sKIRKjFk4JvzhV/9n0l3i6NooPLTNfg==} engines: {node: '>=22.12.0'} @@ -175,9 +217,54 @@ packages: resolution: {integrity: sha512-TzlTVpKPjaqW6qOYjQcYUDuGsLCNsvFHVBXkYGTAnf5V37jCWrE5haKNXzz0WZUtVHjrpV76L1buANjwXMfT8w==} engines: {node: '>=22'} + '@csstools/color-helpers@6.1.0': + resolution: {integrity: sha512-064IFJdjTfUqnjpCVpMOdbr8FLQBhinbZj6yRv2An2E41O/pLEXqfFRWqGq/SxlE5PEUYTlvWsG2r8MswAVvkg==} + engines: {node: '>=20.19.0'} + + '@csstools/css-calc@3.3.0': + resolution: {integrity: sha512-c5ihYsPkdG6JCkU2zTMm4+k6r7RXuGxtWYhu5DHMIiF1FHzrfmHL5so11AoFpUv/tu61xfcmT4AmKoFfMPoqdQ==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@csstools/css-parser-algorithms': ^4.0.0 + '@csstools/css-tokenizer': ^4.0.0 + + '@csstools/css-color-parser@4.1.10': + resolution: {integrity: sha512-UZhQLIUyJaaMepqehrCODwCg2KW25vFvLWBmqYFaPclYvvxzj/sG8LBOhBFCp11i9uE7t1EyS+RAoV9tztPFyw==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@csstools/css-parser-algorithms': ^4.0.0 + '@csstools/css-tokenizer': ^4.0.0 + + '@csstools/css-parser-algorithms@4.0.0': + resolution: {integrity: sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@csstools/css-tokenizer': ^4.0.0 + + '@csstools/css-syntax-patches-for-csstree@1.1.7': + resolution: {integrity: sha512-fQ+05118eQS1cofO3aJpB5efgpBZMvIzwr/sbC8kDLVA5XLG8q1kJV5yzrUAI1f7lvhPnm8fgIjzFB8/O/5Dig==} + peerDependencies: + css-tree: ^3.2.1 + peerDependenciesMeta: + css-tree: + optional: true + + '@csstools/css-tokenizer@4.0.0': + resolution: {integrity: sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==} + engines: {node: '>=20.19.0'} + '@dimforge/rapier3d-compat@0.12.0': resolution: {integrity: sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow==} + '@exodus/bytes@1.15.1': + resolution: {integrity: sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + peerDependencies: + '@noble/hashes': ^1.8.0 || ^2.0.0 + peerDependenciesMeta: + '@noble/hashes': + optional: true + '@jridgewell/sourcemap-codec@1.5.5': resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} @@ -319,6 +406,88 @@ packages: cpu: [x64] os: [win32] + '@parcel/watcher-android-arm64@2.6.0': + resolution: {integrity: sha512-trgpLSCKRC/huFjXX/Smh+0sWe4+YtKfktIToiMl59ghz7z+qkH6kMvNnUbLyRs9N11t8l4svSCs1+5B3rOAhA==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [android] + + '@parcel/watcher-darwin-arm64@2.6.0': + resolution: {integrity: sha512-Y3QV0gl7Q1zbfueunkWIERICbEojQFCgpyG7YqOGNFLsckXyI1xu9mAIUpKY9QBYzBtSkN8dBPwd3yiAO9ovMw==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [darwin] + + '@parcel/watcher-darwin-x64@2.6.0': + resolution: {integrity: sha512-Ohv6OpzhUfKYD7Beb8kDvG0jbIxORCYY1JRdZnaBtnjjkJxgD7ZVL0nw2sCYd0yTMKTvz3nnTnOF3cDifK+kvw==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [darwin] + + '@parcel/watcher-freebsd-x64@2.6.0': + resolution: {integrity: sha512-5HmXvDgs8VK+74jF9y9/2FE3/OnlcKmc56tjmSrEuZjpSZOGL+fvAu+HKJBdPs9uwoP2hE6TlSUpXZ/C5jUFmQ==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [freebsd] + + '@parcel/watcher-linux-arm-glibc@2.6.0': + resolution: {integrity: sha512-Ps/hui3A+vMbjdqlqAowK2ZL8+BO8dBjxeWXj6npTBs3jx4wWmbPpaLuqwrQrSqIVMCnpWo238bJ1U37GhQOYg==} + engines: {node: '>= 10.0.0'} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@parcel/watcher-linux-arm-musl@2.6.0': + resolution: {integrity: sha512-9c6AUHgHoG+IY88MRIHupztQiQnrbqHYQjkM2btA+Bf/wQnQMuiD0Wfk1EVv3TlNT3x41uU71rn6E4xh/+zvkw==} + engines: {node: '>= 10.0.0'} + cpu: [arm] + os: [linux] + libc: [musl] + + '@parcel/watcher-linux-arm64-glibc@2.6.0': + resolution: {integrity: sha512-yHRqS2owEXe6Hic9z6Mh1ECsCd+ODVOGvZDyciqRd21+v+o+DnXMOrw50DSpIG2sb8GPEaPPmfeCAWKPJdq46g==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@parcel/watcher-linux-arm64-musl@2.6.0': + resolution: {integrity: sha512-WhB2e/V7rqdHHWZusBSPuy5Ei8S6lSz6FE5TKKQz5h3a0O+C+mhY7vxU9b/stqvMb8beLnPY82ZrFTLKs+SrKA==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@parcel/watcher-linux-x64-glibc@2.6.0': + resolution: {integrity: sha512-ulGE6x6Oz6iAwg75T8YQSoguBWasniIbX+QWpaYPcCnDOpdWX3k+4xbEYPZVLxOuoJI+svJJPD3sEj8G7lrQ3A==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@parcel/watcher-linux-x64-musl@2.6.0': + resolution: {integrity: sha512-tkBYKt7YQrjIJWYDnto2YgO8MRkjlMTSNoRHzsXinBqbLdeOM3L32wPZJvIZxqaLMfSlS/4sUjH/6STVP/XDLw==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@parcel/watcher-win32-arm64@2.6.0': + resolution: {integrity: sha512-gIZAP23jaHjGWasY/TY6yL7NHFClf0Ga7FN+iINvk+KN94rhm94lYZhFsbYFNcA04/onvGD9kKmiJLJB2HbNwQ==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [win32] + + '@parcel/watcher-win32-x64@2.6.0': + resolution: {integrity: sha512-cA+/pXV2YkfxlIcXOQ5fSWqAzzPyD78/x5qbK/I0vUkrlYHA8TIz+MXjAbGouguKVSI4bOmkTSJ1/poVSsgt+A==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [win32] + + '@parcel/watcher@2.6.0': + resolution: {integrity: sha512-7FNeNl8NCE7aINx7WXiKQrPYZWC/hvrTsmk6zmxbI7LTXE7hVek/n8AfVgpe2y82zl3w0HvCHN0bVKMBoJcC0w==} + engines: {node: '>= 10.0.0'} + '@react-three/drei@10.7.8': resolution: {integrity: sha512-rJXyuzLm2Xq0kafHuR47ajDGbOe/pEhzIr4m8E8zwzQs0iNjloFDqBwRhrXmP/w+onLeYyN3EYPFW/cwWK/4yA==} peerDependencies: @@ -355,6 +524,17 @@ packages: react-native: optional: true + '@reduxjs/toolkit@2.12.0': + resolution: {integrity: sha512-KiT+RzZbp6mQET+Mg+h2c97+9j1sNflUxQkIHI7Yuzf6Peu+OYpmkn6nbHWmLLWj+1ZODUJFwGZ7gx3L9R9EOw==} + peerDependencies: + react: ^16.9.0 || ^17.0.0 || ^18 || ^19 + react-redux: ^7.2.1 || ^8.1.3 || ^9.0.0 + peerDependenciesMeta: + react: + optional: true + react-redux: + optional: true + '@rolldown/binding-android-arm64@1.2.3': resolution: {integrity: sha512-zrJtHDcaZJ1Fp7xf4hNl+7seH9Cn/N5TwLYkhgXREtBwAd/jaqW3uqeHxpDugJLVICWg4eW44kOQEGJ1r6jCGw==} engines: {node: ^20.19.0 || >=22.12.0} @@ -459,12 +639,144 @@ packages: '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + '@standard-schema/utils@0.3.0': + resolution: {integrity: sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==} + + '@tanstack/query-core@5.101.4': + resolution: {integrity: sha512-gNwcvOJcRbLWPOLG/2OBm+zM+Yv+MKsXKEOWC57USuZDEsI71hEErQsiEGx5wX9rzWWkfwM0fVSPoiIFSsxfiw==} + + '@tanstack/react-query@5.101.4': + resolution: {integrity: sha512-yRg2pfOCxIs4ZJW3XYYHU/WgtD04FHSnfHlpRT7h7pR77hwkdRG4wxbKe4aq6P0RvXUTBSQpQeadS1SUYUe+KA==} + peerDependencies: + react: ^18 || ^19 + + '@testing-library/dom@10.4.1': + resolution: {integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==} + engines: {node: '>=18'} + + '@testing-library/jest-dom@7.0.0': + resolution: {integrity: sha512-HKAH9C6mBo5yBG6yRO5i43L2iisencAo5z+o5P/saHUoY+miC5ivXRxHBJcFyB5ypPNxHJdK3BoF/3O4DIptMg==} + engines: {node: '>=22', npm: '>=6', yarn: '>=1'} + peerDependencies: + '@testing-library/dom': '>=10 <11' + + '@testing-library/react@16.3.2': + resolution: {integrity: sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==} + engines: {node: '>=18'} + peerDependencies: + '@testing-library/dom': ^10.0.0 + '@types/react': ^18.0.0 || ^19.0.0 + '@types/react-dom': ^18.0.0 || ^19.0.0 + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@tweenjs/tween.js@23.1.3': resolution: {integrity: sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==} + '@types/aria-query@5.0.4': + resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} + '@types/chai@5.2.3': resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + '@types/d3-array@3.2.2': + resolution: {integrity: sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==} + + '@types/d3-axis@3.0.6': + resolution: {integrity: sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==} + + '@types/d3-brush@3.0.6': + resolution: {integrity: sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==} + + '@types/d3-chord@3.0.6': + resolution: {integrity: sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==} + + '@types/d3-color@3.1.3': + resolution: {integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==} + + '@types/d3-contour@3.0.6': + resolution: {integrity: sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==} + + '@types/d3-delaunay@6.0.4': + resolution: {integrity: sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==} + + '@types/d3-dispatch@3.0.7': + resolution: {integrity: sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==} + + '@types/d3-drag@3.0.7': + resolution: {integrity: sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==} + + '@types/d3-dsv@3.0.7': + resolution: {integrity: sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==} + + '@types/d3-ease@3.0.2': + resolution: {integrity: sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==} + + '@types/d3-fetch@3.0.7': + resolution: {integrity: sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==} + + '@types/d3-force@3.0.10': + resolution: {integrity: sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==} + + '@types/d3-format@3.0.4': + resolution: {integrity: sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==} + + '@types/d3-geo@3.1.1': + resolution: {integrity: sha512-65Emv9fQiQQqphLlRkuQ5ypPsOmWPhtBGCMv61JDPEPMvsx+gzhGf74yw1a78xFKPj6zw4AgQICJoQv0vK9M2w==} + + '@types/d3-hierarchy@3.1.7': + resolution: {integrity: sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==} + + '@types/d3-interpolate@3.0.4': + resolution: {integrity: sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==} + + '@types/d3-path@3.1.1': + resolution: {integrity: sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==} + + '@types/d3-polygon@3.0.2': + resolution: {integrity: sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==} + + '@types/d3-quadtree@3.0.6': + resolution: {integrity: sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==} + + '@types/d3-random@3.0.4': + resolution: {integrity: sha512-UHYId5WTCx4L4YNel7NU00XUXXgvgpgZOvp10PuvsQENjMDXhh2RyFc0KBjO7B45ne4Ha1yVH7ii0vnzKkuzWA==} + + '@types/d3-scale-chromatic@3.1.0': + resolution: {integrity: sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==} + + '@types/d3-scale@4.0.9': + resolution: {integrity: sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==} + + '@types/d3-selection@3.0.11': + resolution: {integrity: sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==} + + '@types/d3-shape@3.1.8': + resolution: {integrity: sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==} + + '@types/d3-time-format@4.0.3': + resolution: {integrity: sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==} + + '@types/d3-time@3.0.4': + resolution: {integrity: sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==} + + '@types/d3-timer@3.0.2': + resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==} + + '@types/d3-transition@3.0.9': + resolution: {integrity: sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==} + + '@types/d3-zoom@3.0.8': + resolution: {integrity: sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==} + + '@types/d3@7.4.3': + resolution: {integrity: sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==} + '@types/deep-eql@4.0.2': resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} @@ -474,6 +786,9 @@ packages: '@types/estree@1.0.9': resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + '@types/geojson@7946.0.16': + resolution: {integrity: sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==} + '@types/node@24.13.3': resolution: {integrity: sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==} @@ -499,6 +814,9 @@ packages: '@types/three@0.185.4': resolution: {integrity: sha512-gAsBIC07NIFrxjbf7tH2t71c38uulFfk/RFoC7FNBSjMRAQ8J1x/RBvusX0N5PJouaYFJawXQqfCQ0RKUx/1nA==} + '@types/use-sync-external-store@0.0.6': + resolution: {integrity: sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==} + '@types/webxr@0.5.24': resolution: {integrity: sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg==} @@ -555,10 +873,18 @@ packages: ajv@8.20.0: resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==} + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + ansi-regex@6.2.2: resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} engines: {node: '>=12'} + ansi-styles@5.2.0: + resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} + engines: {node: '>=10'} + ansi-styles@6.2.3: resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} engines: {node: '>=12'} @@ -570,6 +896,13 @@ packages: resolution: {integrity: sha512-DhBpBfXL4SS2uC0N922MMajKR3CdrTG0u2or1PNYgXMsrSzViJrbtvT0nCLlLGUI0plam/ZZCs7aAauHtW9thw==} engines: {node: '>=22'} + aria-query@5.3.0: + resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} + + aria-query@5.3.2: + resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} + engines: {node: '>= 0.4'} + assertion-error@2.0.1: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} @@ -597,10 +930,22 @@ packages: resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} engines: {node: '>=18'} + chokidar@5.0.0: + resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==} + engines: {node: '>= 20.19.0'} + cliui@9.0.1: resolution: {integrity: sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==} engines: {node: '>=20'} + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} + + commander@7.2.0: + resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==} + engines: {node: '>= 10'} + conventional-changelog-angular@9.2.1: resolution: {integrity: sha512-oWSL6ZhnXbYraOFTK3PgRAQJ8fADDAEv5K6AdeyQPLvjFmhG8+ejL0jZZp/R7vTmGJaBvZEE+sE7dB4bCv7sAw==} engines: {node: '>=22'} @@ -617,6 +962,9 @@ packages: convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + cookie-es@3.1.1: + resolution: {integrity: sha512-UaXxwISYJPTr9hwQxMFYZ7kNhSXboMXP+Z3TRX6f1/NyaGPfuNUZOWP1pUEb75B2HjfklIYLVRfWiFZJyC6Npg==} + cosmiconfig-typescript-loader@6.3.0: resolution: {integrity: sha512-Akr82WH1Wfqatyiqpj8HDkO2o2KmJRu1FhKfSNJP3K4IdXwHfEyL7MOb62i1AGQVLtIQM+iCE9CGOtrfhR+mmA==} engines: {node: '>=v18'} @@ -643,9 +991,160 @@ packages: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} + css-tree@3.2.1: + resolution: {integrity: sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} + + css.escape@1.5.1: + resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==} + csstype@3.2.3: resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + d3-array@3.2.4: + resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==} + engines: {node: '>=12'} + + d3-axis@3.0.0: + resolution: {integrity: sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==} + engines: {node: '>=12'} + + d3-brush@3.0.0: + resolution: {integrity: sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==} + engines: {node: '>=12'} + + d3-chord@3.0.1: + resolution: {integrity: sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==} + engines: {node: '>=12'} + + d3-color@3.1.0: + resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==} + engines: {node: '>=12'} + + d3-contour@4.0.2: + resolution: {integrity: sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==} + engines: {node: '>=12'} + + d3-delaunay@6.0.4: + resolution: {integrity: sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==} + engines: {node: '>=12'} + + d3-dispatch@3.0.1: + resolution: {integrity: sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==} + engines: {node: '>=12'} + + d3-drag@3.0.0: + resolution: {integrity: sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==} + engines: {node: '>=12'} + + d3-dsv@3.0.1: + resolution: {integrity: sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==} + engines: {node: '>=12'} + hasBin: true + + d3-ease@3.0.1: + resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==} + engines: {node: '>=12'} + + d3-fetch@3.0.1: + resolution: {integrity: sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==} + engines: {node: '>=12'} + + d3-force@3.0.0: + resolution: {integrity: sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==} + engines: {node: '>=12'} + + d3-format@3.1.2: + resolution: {integrity: sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==} + engines: {node: '>=12'} + + d3-geo@3.1.1: + resolution: {integrity: sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==} + engines: {node: '>=12'} + + d3-hierarchy@3.1.2: + resolution: {integrity: sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==} + engines: {node: '>=12'} + + d3-interpolate@3.0.1: + resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==} + engines: {node: '>=12'} + + d3-path@3.1.0: + resolution: {integrity: sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==} + engines: {node: '>=12'} + + d3-polygon@3.0.1: + resolution: {integrity: sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==} + engines: {node: '>=12'} + + d3-quadtree@3.0.1: + resolution: {integrity: sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==} + engines: {node: '>=12'} + + d3-random@3.0.1: + resolution: {integrity: sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==} + engines: {node: '>=12'} + + d3-scale-chromatic@3.1.0: + resolution: {integrity: sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==} + engines: {node: '>=12'} + + d3-scale@4.0.2: + resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==} + engines: {node: '>=12'} + + d3-selection@3.0.0: + resolution: {integrity: sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==} + engines: {node: '>=12'} + + d3-shape@3.2.0: + resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==} + engines: {node: '>=12'} + + d3-time-format@4.1.0: + resolution: {integrity: sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==} + engines: {node: '>=12'} + + d3-time@3.1.0: + resolution: {integrity: sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==} + engines: {node: '>=12'} + + d3-timer@3.0.1: + resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==} + engines: {node: '>=12'} + + d3-transition@3.0.1: + resolution: {integrity: sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==} + engines: {node: '>=12'} + peerDependencies: + d3-selection: 2 - 3 + + d3-zoom@3.0.0: + resolution: {integrity: sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==} + engines: {node: '>=12'} + + d3@7.9.0: + resolution: {integrity: sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==} + engines: {node: '>=12'} + + data-urls@7.0.0: + resolution: {integrity: sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + + decimal.js-light@2.5.1: + resolution: {integrity: sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==} + + decimal.js@10.6.0: + resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} + + delaunator@5.1.0: + resolution: {integrity: sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ==} + + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + detect-gpu@5.0.70: resolution: {integrity: sha512-bqerEP1Ese6nt3rFkwPnGbsUF9a4q+gMmpTVVOEzoCyeCc+y7/RvJnQZJx1JwhgQI5Ntg0Kgat8Uu7XpBqnz1w==} @@ -653,12 +1152,22 @@ packages: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} + dom-accessibility-api@0.5.16: + resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} + + dom-accessibility-api@0.6.3: + resolution: {integrity: sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==} + draco3d@1.5.7: resolution: {integrity: sha512-m6WCKt/erDXcw+70IJXnG7M3awwQPAsZvJGX5zY7beBqpELw6RDGkYVU0W43AFxye4pDZ5i2Lbyc/NNGqwjUVQ==} emoji-regex@10.6.0: resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==} + entities@8.0.0: + resolution: {integrity: sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==} + engines: {node: '>=20.19.0'} + env-paths@2.2.1: resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} engines: {node: '>=6'} @@ -679,6 +1188,9 @@ packages: estree-walker@3.0.3: resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + eventemitter3@5.0.4: + resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==} + expect-type@1.4.0: resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==} engines: {node: '>=12.0.0'} @@ -732,35 +1244,68 @@ packages: hls.js@1.6.17: resolution: {integrity: sha512-NUplVGVuc1hSPwdB/9/cbRkUmLrYi75/hqiXKdA+l300pJNxDu96R7jRb2imDzWJqIUF4I5ThmAdp9GvOCXsuQ==} + html-encoding-sniffer@6.0.0: + resolution: {integrity: sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + husky@9.1.7: resolution: {integrity: sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==} engines: {node: '>=18'} hasBin: true + iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + ieee754@1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} immediate@3.0.6: resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==} + immer@11.1.16: + resolution: {integrity: sha512-Xs7H9rBc+kti1J6RueUvbEBkmOz7jqj11XYgf+YMXAYzu8EeE7hwZ9poLXdVfVnGmJu7QAf41T7H2KuF6QoK6Q==} + + immutable@5.1.9: + resolution: {integrity: sha512-m8nVez3rwrgmWxtLMt1ZYXB2Lv7OKYn/disyxAlSDYAlKSlFoPPfIAmAM/M5xqL4m4C/wAPw7S2/CNaUii1Hxg==} + import-fresh@3.3.1: resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} engines: {node: '>=6'} + indent-string@4.0.0: + resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} + engines: {node: '>=8'} + ini@6.0.0: resolution: {integrity: sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ==} engines: {node: ^20.17.0 || >=22.9.0} + internmap@2.0.3: + resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} + engines: {node: '>=12'} + is-any-array@3.0.0: resolution: {integrity: sha512-o4h+tylWykC4BD1vaejp6gDxoM13bwW8FGuNs4yIKpj8xbBJcRxJx8vZpq0dCr7ZDEfeKjmsi/euolKhX6f/ww==} is-arrayish@0.2.1: resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + is-plain-obj@4.1.0: resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} engines: {node: '>=12'} + is-potential-custom-element-name@1.0.1: + resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + is-promise@2.2.2: resolution: {integrity: sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==} @@ -783,6 +1328,15 @@ packages: resolution: {integrity: sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==} hasBin: true + jsdom@30.0.1: + resolution: {integrity: sha512-52v7mUVUfNQVYYqE1lcdaymWL0njO7lTLUog6ZvW2U5KsbiLk/GnZlVJ+qx0xfNJZ6Gn+KSpPNE52vurbxZwrA==} + engines: {node: ^22.22.2 || ^24.15.0 || >=26.0.0} + peerDependencies: + canvas: ^3.2.3 + peerDependenciesMeta: + canvas: + optional: true + json-parse-even-better-errors@2.3.1: resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} @@ -874,6 +1428,14 @@ packages: engines: {node: '>=22.22.1'} hasBin: true + lru-cache@11.5.2: + resolution: {integrity: sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==} + engines: {node: 20 || >=22} + + lz-string@1.5.0: + resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} + hasBin: true + maath@0.10.8: resolution: {integrity: sha512-tRvbDF0Pgqz+9XUa4jjfgAQ8/aPKmQdWXilFu2tMy4GWj4NOsx99HlULO4IeREfbO3a0sA145DZYyvXPkybm0g==} peerDependencies: @@ -883,6 +1445,9 @@ packages: magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + mdn-data@2.27.1: + resolution: {integrity: sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==} + meshline@3.3.1: resolution: {integrity: sha512-/TQj+JdZkeSUOl5Mk2J7eLcYTLiQm2IDzmlSvYm7ov15anEcDJ92GHqqazxTSreeNgfnYu24kiEvvv0WlbCdFQ==} peerDependencies: @@ -891,6 +1456,10 @@ packages: meshoptimizer@1.1.1: resolution: {integrity: sha512-oRFNWJRDA/WTrVj7NWvqa5HqE1t9MYDj2VaWirQCzCCrAd2GHrqR/sQezCxiWATPNlKTcRaPRHPJwIRoPBAp5g==} + min-indent@1.0.1: + resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} + engines: {node: '>=4'} + ml-array-max@2.0.0: resolution: {integrity: sha512-QQZ4kENwpWmyNb98UXRDFXrmtIXuXtt1+bSbda/2KA85+F+rrJP8hZk6QOkCQXM2Th9mUDYdq/PNByPdT9ID4A==} @@ -911,6 +1480,9 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + node-addon-api@7.1.1: + resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} + obug@2.1.4: resolution: {integrity: sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==} engines: {node: '>=12.20.0'} @@ -936,6 +1508,9 @@ packages: resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} engines: {node: '>=8'} + parse5@8.0.1: + resolution: {integrity: sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==} + path-key@3.1.1: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} @@ -967,14 +1542,50 @@ packages: potpack@1.0.2: resolution: {integrity: sha512-choctRBIV9EMT9WGAZHn3V7t0Z2pMQyl0EZE6pFc/6ml3ssw7Dlf/oAOvFwjm1HVsqfQN8GfeFyJ+d8tRzqueQ==} + pretty-format@27.5.1: + resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + promise-worker-transferable@1.0.4: resolution: {integrity: sha512-bN+0ehEnrXfxV2ZQvU2PetO0n4gqBD4ulq3MI1WOPLgr7/Mg9yRQkX5+0v1vagr74ZTsl7XtzlaYDo2EuCeYJw==} + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + react-dom@19.2.8: resolution: {integrity: sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==} peerDependencies: react: ^19.2.8 + react-is@17.0.2: + resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} + + react-is@19.2.8: + resolution: {integrity: sha512-s5un28nYxKJw5gvUHyW5PCC28CvBqLu9r3cWgzHT4Vo/5fqqkFcdRYsGcKf50WMPpjjFZS5d76fn3YCo2njKwQ==} + + react-redux@9.3.0: + resolution: {integrity: sha512-KQopgqFo/p/fgmAs5qz6p5RWaNAzq40WAu7fJIXnQpYxFPbJYtsJPWvGeF2rOBaY/kEuV77AVsX8TsQzKm+A/g==} + peerDependencies: + '@types/react': ^18.2.25 || ^19 + react: ^18.0 || ^19 + redux: ^5.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + redux: + optional: true + + react-router@8.3.0: + resolution: {integrity: sha512-qyPMvW83jGIct3yiieisxdk9M745anqhpIMKN5m1t6yBMfgVPpt77aHOqs5fUlEJRMCGffg9BaQLH9oPVOL7xQ==} + engines: {node: '>=22.22.0'} + peerDependencies: + react: '>=19.2.7' + react-dom: '>=19.2.7' + peerDependenciesMeta: + react-dom: + optional: true + react-use-measure@2.1.7: resolution: {integrity: sha512-KrvcAo13I/60HpwGO5jpW7E9DfusKyLPLvuHlUyP5zqnmAPhNc6qTRjUQrdTADl0lpPpDVU2/Gg51UlOGHXbdg==} peerDependencies: @@ -988,23 +1599,68 @@ packages: resolution: {integrity: sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==} engines: {node: '>=0.10.0'} - require-from-string@2.0.2: - resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} - engines: {node: '>=0.10.0'} + readdirp@5.1.1: + resolution: {integrity: sha512-Kko+Y5XQ6fM+Ce3dq3m9YGxnacYZYl9cA1wZjaF3Vbry2L3i1qVg8+CAgNPsXRArPMUMCaOR7oa9Nqntc43JKA==} + engines: {node: '>= 20.19.0'} - resolve-from@4.0.0: - resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + recharts@3.10.1: + resolution: {integrity: sha512-QXFrvt6IVcw7eeZCoyXTwkIJAX3Dv1nyVhMicXJ47GsGDDpcN8z6o644DibE9XjpBTThtsomLKnTV6lc+cVFUA==} + engines: {node: '>=18'} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-is: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + redent@3.0.0: + resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} + engines: {node: '>=8'} + + redux-thunk@3.1.0: + resolution: {integrity: sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==} + peerDependencies: + redux: ^5.0.0 + + redux@5.0.1: + resolution: {integrity: sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==} + + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + + reselect@5.2.0: + resolution: {integrity: sha512-AgZ3UOZm3YndfrJ4OYjgrT7bmCm/1iqkjvEfH/oYjzh6PD2qw4QuT3jjnXIrpdt4MTpMXclMT3lXbmRY+XRakw==} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} resolve-from@5.0.0: resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} engines: {node: '>=8'} + robust-predicates@3.0.3: + resolution: {integrity: sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA==} + rolldown@1.2.3: resolution: {integrity: sha512-rn9wpmxplLf7NLNyCk9FyWh3FM43DbY8jOzCdEPzH7uflhTftRbCEpqi6Ly2osgoU8OwObtmavMbWLaWy4LX7A==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true + rw@1.3.3: + resolution: {integrity: sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + sass@1.102.0: + resolution: {integrity: sha512-NSOyTnaQF7rTAEOtI2fwb386vL+akyiQLBZu8Na7hXCb+umJy0GAqlcMIaqACZ6Z1VgTBS4K9PG6B3IdjHGJsw==} + engines: {node: '>=20.19.0'} + hasBin: true + + saxes@6.0.0: + resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} + engines: {node: '>=v12.22.7'} + scheduler@0.27.0: resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} @@ -1059,11 +1715,18 @@ packages: resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==} engines: {node: '>=12'} + strip-indent@3.0.0: + resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} + engines: {node: '>=8'} + suspend-react@0.1.3: resolution: {integrity: sha512-aqldKgX9aZqpoDp3e8/BZ8Dm7x1pJl+qI3ZKxDN0i/IQTWUwBx/ManmlVJ3wowqbno6c2bmiIfs+Um6LbsjJyQ==} peerDependencies: react: '>=17.0' + symbol-tree@3.2.4: + resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + three-mesh-bvh@0.8.3: resolution: {integrity: sha512-4G5lBaF+g2auKX3P0yqx+MJC6oVt6sB5k+CchS6Ob0qvH0YIhuUk1eYr7ktsIpY+albCqE80/FVQGV190PmiAg==} peerDependencies: @@ -1077,6 +1740,9 @@ packages: three@0.185.1: resolution: {integrity: sha512-5aojFCXKwnjBRZvUnt3WFfEcvUJgkN5LlijRFN95hMy8WVkG4I0QNcJE+OuWvuJ0bOdStrbfXn0pkd6/QyiAlg==} + tiny-invariant@1.3.3: + resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} + tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} @@ -1092,6 +1758,21 @@ packages: resolution: {integrity: sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==} engines: {node: '>=14.0.0'} + tldts-core@7.4.10: + resolution: {integrity: sha512-KnQjp53ZekKgm/r3l+u8kJGGzYgrWdP8+Mql7a4vijh2WE0IrZWspQj/TpTxDho/YxO+AnOZnIjQcCD+q6iJsw==} + + tldts@7.4.10: + resolution: {integrity: sha512-GgouD1B+sWwvkaEq8vXC15DjQitxbvs12oIXELpconwm+Tg3zfcEv4jgzq3vtKverDXsg3VI8aRgNL2Nra0Iog==} + hasBin: true + + tough-cookie@6.0.2: + resolution: {integrity: sha512-exgYmnmL/sJpR3upZfXG5PoatXQii55xAiXGXzY+sROLZ/Y+SLcp9PgJNI9Vz37HpQ74WvDcLT8eqm+kV3FzrA==} + engines: {node: '>=16'} + + tr46@6.0.0: + resolution: {integrity: sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==} + engines: {node: '>=20'} + troika-three-text@0.52.5: resolution: {integrity: sha512-Ry3jRhic9pzcY4JduSvRRyDmVOSqEW19gT4vtK+aCiPNVcDlmkxvGG0YbFd36RTDq1wExOupXnvNF/j1oiHHDA==} peerDependencies: @@ -1116,6 +1797,10 @@ packages: undici-types@7.18.2: resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==} + undici@8.10.0: + resolution: {integrity: sha512-HvltHd7avK13QIw/oLe4qoOLyoVSoafqJ2jYOrtMRBkbYT31eiBQ8O0ehRKZiEZCMEyLFQNIADpgCWC5fALvYQ==} + engines: {node: '>=22.19.0'} + use-sync-external-store@1.6.0: resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==} peerDependencies: @@ -1125,6 +1810,9 @@ packages: resolution: {integrity: sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw==} engines: {node: '>= 4'} + victory-vendor@37.3.6: + resolution: {integrity: sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==} + vite@8.2.0: resolution: {integrity: sha512-pn+CFpM0lwDeKwmOq1ZaBK/9sjorZcgqxki6MbY/jPEVd9vichIlmlD4HmQ5wdP5EgqQCFRaACBxMC7uEGc6lQ==} engines: {node: ^20.19.0 || >=22.12.0} @@ -1209,12 +1897,32 @@ packages: jsdom: optional: true + w3c-xmlserializer@5.0.0: + resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} + engines: {node: '>=18'} + webgl-constants@1.1.1: resolution: {integrity: sha512-LkBXKjU5r9vAW7Gcu3T5u+5cvSvh5WwINdr0C+9jpzVB41cjQAP5ePArDtk/WHYdVj0GefCgM73BA7FlIiNtdg==} webgl-sdf-generator@1.1.1: resolution: {integrity: sha512-9Z0JcMTFxeE+b2x1LJTdnaT8rT8aEp7MVxkNwoycNmJWwPdzoXzMh0BjJSh/AEFP+KPYZUli814h8bJZFIZ2jA==} + webidl-conversions@8.0.1: + resolution: {integrity: sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==} + engines: {node: '>=20'} + + whatwg-mimetype@5.0.0: + resolution: {integrity: sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==} + engines: {node: '>=20'} + + whatwg-url@16.0.1: + resolution: {integrity: sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + + whatwg-url@17.1.0: + resolution: {integrity: sha512-3GeworPmc2ZfEEHP7lEbUfBX/L75wdEsi0rLNhXcXxnoN5jyq0SL5gCy06SGW2cyTIZdTvWIDQNQoza++vKeaw==} + engines: {node: ^22.14.0 || >=24.0.0} + which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} @@ -1229,6 +1937,13 @@ packages: resolution: {integrity: sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==} engines: {node: '>=18'} + xml-name-validator@5.0.0: + resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} + engines: {node: '>=18'} + + xmlchars@2.2.0: + resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + y18n@5.0.8: resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} engines: {node: '>=10'} @@ -1284,6 +1999,23 @@ packages: snapshots: + '@adobe/css-tools@4.5.0': {} + + '@asamuzakjp/css-color@6.0.7': + dependencies: + '@csstools/css-calc': 3.3.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-color-parser': 4.1.10(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 + lru-cache: 11.5.2 + + '@asamuzakjp/dom-selector@8.3.2': + dependencies: + bidi-js: 1.0.3 + css-tree: 3.2.1 + is-potential-custom-element-name: 1.0.1 + lru-cache: 11.5.2 + '@babel/code-frame@7.29.7': dependencies: '@babel/helper-validator-identifier': 7.29.7 @@ -1294,6 +2026,10 @@ snapshots: '@babel/runtime@7.29.7': {} + '@bramus/specificity@2.4.2': + dependencies: + css-tree: 3.2.1 + '@commitlint/cli@21.2.1(@types/node@24.13.3)(conventional-commits-parser@7.1.2)(typescript@6.0.3)': dependencies: '@commitlint/config-conventional': 21.2.0 @@ -1413,8 +2149,34 @@ snapshots: '@conventional-changelog/template@1.2.1': {} + '@csstools/color-helpers@6.1.0': {} + + '@csstools/css-calc@3.3.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': + dependencies: + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 + + '@csstools/css-color-parser@4.1.10(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': + dependencies: + '@csstools/color-helpers': 6.1.0 + '@csstools/css-calc': 3.3.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 + + '@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0)': + dependencies: + '@csstools/css-tokenizer': 4.0.0 + + '@csstools/css-syntax-patches-for-csstree@1.1.7(css-tree@3.2.1)': + optionalDependencies: + css-tree: 3.2.1 + + '@csstools/css-tokenizer@4.0.0': {} + '@dimforge/rapier3d-compat@0.12.0': {} + '@exodus/bytes@1.15.1': {} + '@jridgewell/sourcemap-codec@1.5.5': {} '@keyobs/dx-flow@3.1.3': {} @@ -1485,12 +2247,69 @@ snapshots: '@oxlint/binding-win32-x64-msvc@1.77.0': optional: true - '@react-three/drei@10.7.8(@react-three/fiber@9.7.0(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(three@0.185.1))(@types/react@19.2.18)(@types/three@0.185.4)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(three@0.185.1)': + '@parcel/watcher-android-arm64@2.6.0': + optional: true + + '@parcel/watcher-darwin-arm64@2.6.0': + optional: true + + '@parcel/watcher-darwin-x64@2.6.0': + optional: true + + '@parcel/watcher-freebsd-x64@2.6.0': + optional: true + + '@parcel/watcher-linux-arm-glibc@2.6.0': + optional: true + + '@parcel/watcher-linux-arm-musl@2.6.0': + optional: true + + '@parcel/watcher-linux-arm64-glibc@2.6.0': + optional: true + + '@parcel/watcher-linux-arm64-musl@2.6.0': + optional: true + + '@parcel/watcher-linux-x64-glibc@2.6.0': + optional: true + + '@parcel/watcher-linux-x64-musl@2.6.0': + optional: true + + '@parcel/watcher-win32-arm64@2.6.0': + optional: true + + '@parcel/watcher-win32-x64@2.6.0': + optional: true + + '@parcel/watcher@2.6.0': + dependencies: + detect-libc: 2.1.2 + is-glob: 4.0.3 + node-addon-api: 7.1.1 + picomatch: 4.0.5 + optionalDependencies: + '@parcel/watcher-android-arm64': 2.6.0 + '@parcel/watcher-darwin-arm64': 2.6.0 + '@parcel/watcher-darwin-x64': 2.6.0 + '@parcel/watcher-freebsd-x64': 2.6.0 + '@parcel/watcher-linux-arm-glibc': 2.6.0 + '@parcel/watcher-linux-arm-musl': 2.6.0 + '@parcel/watcher-linux-arm64-glibc': 2.6.0 + '@parcel/watcher-linux-arm64-musl': 2.6.0 + '@parcel/watcher-linux-x64-glibc': 2.6.0 + '@parcel/watcher-linux-x64-musl': 2.6.0 + '@parcel/watcher-win32-arm64': 2.6.0 + '@parcel/watcher-win32-x64': 2.6.0 + optional: true + + '@react-three/drei@10.7.8(@react-three/fiber@9.7.0(@types/react@19.2.18)(immer@11.1.16)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(three@0.185.1))(@types/react@19.2.18)(@types/three@0.185.4)(immer@11.1.16)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(three@0.185.1)': dependencies: '@babel/runtime': 7.29.7 '@mediapipe/tasks-vision': 0.10.17 '@monogrid/gainmap-js': 3.4.0(three@0.185.1) - '@react-three/fiber': 9.7.0(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(three@0.185.1) + '@react-three/fiber': 9.7.0(@types/react@19.2.18)(immer@11.1.16)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(three@0.185.1) '@use-gesture/react': 10.3.1(react@19.2.8) camera-controls: 3.1.2(three@0.185.1) cross-env: 7.0.3 @@ -1507,10 +2326,10 @@ snapshots: three-mesh-bvh: 0.8.3(three@0.185.1) three-stdlib: 2.36.1(three@0.185.1) troika-three-text: 0.52.5(three@0.185.1) - tunnel-rat: 0.1.2(@types/react@19.2.18)(react@19.2.8) + tunnel-rat: 0.1.2(@types/react@19.2.18)(immer@11.1.16)(react@19.2.8) use-sync-external-store: 1.6.0(react@19.2.8) utility-types: 3.11.0 - zustand: 5.0.14(@types/react@19.2.18)(react@19.2.8)(use-sync-external-store@1.6.0(react@19.2.8)) + zustand: 5.0.14(@types/react@19.2.18)(immer@11.1.16)(react@19.2.8)(use-sync-external-store@1.6.0(react@19.2.8)) optionalDependencies: react-dom: 19.2.8(react@19.2.8) transitivePeerDependencies: @@ -1518,7 +2337,7 @@ snapshots: - '@types/three' - immer - '@react-three/fiber@9.7.0(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(three@0.185.1)': + '@react-three/fiber@9.7.0(@types/react@19.2.18)(immer@11.1.16)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(three@0.185.1)': dependencies: '@babel/runtime': 7.29.7 '@types/webxr': 0.5.24 @@ -1531,13 +2350,25 @@ snapshots: suspend-react: 0.1.3(react@19.2.8) three: 0.185.1 use-sync-external-store: 1.6.0(react@19.2.8) - zustand: 5.0.14(@types/react@19.2.18)(react@19.2.8)(use-sync-external-store@1.6.0(react@19.2.8)) + zustand: 5.0.14(@types/react@19.2.18)(immer@11.1.16)(react@19.2.8)(use-sync-external-store@1.6.0(react@19.2.8)) optionalDependencies: react-dom: 19.2.8(react@19.2.8) transitivePeerDependencies: - '@types/react' - immer + '@reduxjs/toolkit@2.12.0(react-redux@9.3.0(@types/react@19.2.18)(react@19.2.8)(redux@5.0.1))(react@19.2.8)': + dependencies: + '@standard-schema/spec': 1.1.0 + '@standard-schema/utils': 0.3.0 + immer: 11.1.16 + redux: 5.0.1 + redux-thunk: 3.1.0(redux@5.0.1) + reselect: 5.2.0 + optionalDependencies: + react: 19.2.8 + react-redux: 9.3.0(@types/react@19.2.18)(react@19.2.8)(redux@5.0.1) + '@rolldown/binding-android-arm64@1.2.3': optional: true @@ -1590,19 +2421,180 @@ snapshots: '@standard-schema/spec@1.1.0': {} + '@standard-schema/utils@0.3.0': {} + + '@tanstack/query-core@5.101.4': {} + + '@tanstack/react-query@5.101.4(react@19.2.8)': + dependencies: + '@tanstack/query-core': 5.101.4 + react: 19.2.8 + + '@testing-library/dom@10.4.1': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/runtime': 7.29.7 + '@types/aria-query': 5.0.4 + aria-query: 5.3.0 + dom-accessibility-api: 0.5.16 + lz-string: 1.5.0 + picocolors: 1.1.1 + pretty-format: 27.5.1 + + '@testing-library/jest-dom@7.0.0(@testing-library/dom@10.4.1)': + dependencies: + '@adobe/css-tools': 4.5.0 + '@testing-library/dom': 10.4.1 + aria-query: 5.3.2 + css.escape: 1.5.1 + dom-accessibility-api: 0.6.3 + picocolors: 1.1.1 + redent: 3.0.0 + + '@testing-library/react@16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + dependencies: + '@babel/runtime': 7.29.7 + '@testing-library/dom': 10.4.1 + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + optionalDependencies: + '@types/react': 19.2.18 + '@types/react-dom': 19.2.4(@types/react@19.2.18) + '@tweenjs/tween.js@23.1.3': {} + '@types/aria-query@5.0.4': {} + '@types/chai@5.2.3': dependencies: '@types/deep-eql': 4.0.2 assertion-error: 2.0.1 + '@types/d3-array@3.2.2': {} + + '@types/d3-axis@3.0.6': + dependencies: + '@types/d3-selection': 3.0.11 + + '@types/d3-brush@3.0.6': + dependencies: + '@types/d3-selection': 3.0.11 + + '@types/d3-chord@3.0.6': {} + + '@types/d3-color@3.1.3': {} + + '@types/d3-contour@3.0.6': + dependencies: + '@types/d3-array': 3.2.2 + '@types/geojson': 7946.0.16 + + '@types/d3-delaunay@6.0.4': {} + + '@types/d3-dispatch@3.0.7': {} + + '@types/d3-drag@3.0.7': + dependencies: + '@types/d3-selection': 3.0.11 + + '@types/d3-dsv@3.0.7': {} + + '@types/d3-ease@3.0.2': {} + + '@types/d3-fetch@3.0.7': + dependencies: + '@types/d3-dsv': 3.0.7 + + '@types/d3-force@3.0.10': {} + + '@types/d3-format@3.0.4': {} + + '@types/d3-geo@3.1.1': + dependencies: + '@types/geojson': 7946.0.16 + + '@types/d3-hierarchy@3.1.7': {} + + '@types/d3-interpolate@3.0.4': + dependencies: + '@types/d3-color': 3.1.3 + + '@types/d3-path@3.1.1': {} + + '@types/d3-polygon@3.0.2': {} + + '@types/d3-quadtree@3.0.6': {} + + '@types/d3-random@3.0.4': {} + + '@types/d3-scale-chromatic@3.1.0': {} + + '@types/d3-scale@4.0.9': + dependencies: + '@types/d3-time': 3.0.4 + + '@types/d3-selection@3.0.11': {} + + '@types/d3-shape@3.1.8': + dependencies: + '@types/d3-path': 3.1.1 + + '@types/d3-time-format@4.0.3': {} + + '@types/d3-time@3.0.4': {} + + '@types/d3-timer@3.0.2': {} + + '@types/d3-transition@3.0.9': + dependencies: + '@types/d3-selection': 3.0.11 + + '@types/d3-zoom@3.0.8': + dependencies: + '@types/d3-interpolate': 3.0.4 + '@types/d3-selection': 3.0.11 + + '@types/d3@7.4.3': + dependencies: + '@types/d3-array': 3.2.2 + '@types/d3-axis': 3.0.6 + '@types/d3-brush': 3.0.6 + '@types/d3-chord': 3.0.6 + '@types/d3-color': 3.1.3 + '@types/d3-contour': 3.0.6 + '@types/d3-delaunay': 6.0.4 + '@types/d3-dispatch': 3.0.7 + '@types/d3-drag': 3.0.7 + '@types/d3-dsv': 3.0.7 + '@types/d3-ease': 3.0.2 + '@types/d3-fetch': 3.0.7 + '@types/d3-force': 3.0.10 + '@types/d3-format': 3.0.4 + '@types/d3-geo': 3.1.1 + '@types/d3-hierarchy': 3.1.7 + '@types/d3-interpolate': 3.0.4 + '@types/d3-path': 3.1.1 + '@types/d3-polygon': 3.0.2 + '@types/d3-quadtree': 3.0.6 + '@types/d3-random': 3.0.4 + '@types/d3-scale': 4.0.9 + '@types/d3-scale-chromatic': 3.1.0 + '@types/d3-selection': 3.0.11 + '@types/d3-shape': 3.1.8 + '@types/d3-time': 3.0.4 + '@types/d3-time-format': 4.0.3 + '@types/d3-timer': 3.0.2 + '@types/d3-transition': 3.0.9 + '@types/d3-zoom': 3.0.8 + '@types/deep-eql@4.0.2': {} '@types/draco3d@1.4.10': {} '@types/estree@1.0.9': {} + '@types/geojson@7946.0.16': {} + '@types/node@24.13.3': dependencies: undici-types: 7.18.2 @@ -1632,6 +2624,8 @@ snapshots: fflate: 0.8.3 meshoptimizer: 1.1.1 + '@types/use-sync-external-store@0.0.6': {} + '@types/webxr@0.5.24': {} '@use-gesture/core@10.3.1': {} @@ -1641,10 +2635,10 @@ snapshots: '@use-gesture/core': 10.3.1 react: 19.2.8 - '@vitejs/plugin-react@6.0.5(vite@8.2.0(@types/node@24.13.3)(jiti@2.6.1)(yaml@2.9.0))': + '@vitejs/plugin-react@6.0.5(vite@8.2.0(@types/node@24.13.3)(jiti@2.6.1)(sass@1.102.0)(yaml@2.9.0))': dependencies: '@rolldown/pluginutils': 1.0.1 - vite: 8.2.0(@types/node@24.13.3)(jiti@2.6.1)(yaml@2.9.0) + vite: 8.2.0(@types/node@24.13.3)(jiti@2.6.1)(sass@1.102.0)(yaml@2.9.0) '@vitest/expect@4.1.10': dependencies: @@ -1655,13 +2649,13 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.1.1 - '@vitest/mocker@4.1.10(vite@8.2.0(@types/node@24.13.3)(jiti@2.6.1)(yaml@2.9.0))': + '@vitest/mocker@4.1.10(vite@8.2.0(@types/node@24.13.3)(jiti@2.6.1)(sass@1.102.0)(yaml@2.9.0))': dependencies: '@vitest/spy': 4.1.10 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 8.2.0(@types/node@24.13.3)(jiti@2.6.1)(yaml@2.9.0) + vite: 8.2.0(@types/node@24.13.3)(jiti@2.6.1)(sass@1.102.0)(yaml@2.9.0) '@vitest/pretty-format@4.1.10': dependencies: @@ -1694,14 +2688,24 @@ snapshots: json-schema-traverse: 1.0.0 require-from-string: 2.0.2 + ansi-regex@5.0.1: {} + ansi-regex@6.2.2: {} + ansi-styles@5.2.0: {} + ansi-styles@6.2.3: {} argparse@2.0.1: {} argue-cli@3.1.0: {} + aria-query@5.3.0: + dependencies: + dequal: 2.0.3 + + aria-query@5.3.2: {} + assertion-error@2.0.1: {} base64-js@1.5.1: {} @@ -1723,12 +2727,20 @@ snapshots: chai@6.2.2: {} + chokidar@5.0.0: + dependencies: + readdirp: 5.1.1 + cliui@9.0.1: dependencies: string-width: 7.2.0 strip-ansi: 7.2.0 wrap-ansi: 9.0.2 + clsx@2.1.1: {} + + commander@7.2.0: {} + conventional-changelog-angular@9.2.1: dependencies: '@conventional-changelog/template': 1.2.1 @@ -1744,6 +2756,8 @@ snapshots: convert-source-map@2.0.0: {} + cookie-es@3.1.1: {} + cosmiconfig-typescript-loader@6.3.0(@types/node@24.13.3)(cosmiconfig@9.0.2(typescript@6.0.3))(typescript@6.0.3): dependencies: '@types/node': 24.13.3 @@ -1770,18 +2784,200 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 + css-tree@3.2.1: + dependencies: + mdn-data: 2.27.1 + source-map-js: 1.2.1 + + css.escape@1.5.1: {} + csstype@3.2.3: {} + d3-array@3.2.4: + dependencies: + internmap: 2.0.3 + + d3-axis@3.0.0: {} + + d3-brush@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-drag: 3.0.0 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-transition: 3.0.1(d3-selection@3.0.0) + + d3-chord@3.0.1: + dependencies: + d3-path: 3.1.0 + + d3-color@3.1.0: {} + + d3-contour@4.0.2: + dependencies: + d3-array: 3.2.4 + + d3-delaunay@6.0.4: + dependencies: + delaunator: 5.1.0 + + d3-dispatch@3.0.1: {} + + d3-drag@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-selection: 3.0.0 + + d3-dsv@3.0.1: + dependencies: + commander: 7.2.0 + iconv-lite: 0.6.3 + rw: 1.3.3 + + d3-ease@3.0.1: {} + + d3-fetch@3.0.1: + dependencies: + d3-dsv: 3.0.1 + + d3-force@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-quadtree: 3.0.1 + d3-timer: 3.0.1 + + d3-format@3.1.2: {} + + d3-geo@3.1.1: + dependencies: + d3-array: 3.2.4 + + d3-hierarchy@3.1.2: {} + + d3-interpolate@3.0.1: + dependencies: + d3-color: 3.1.0 + + d3-path@3.1.0: {} + + d3-polygon@3.0.1: {} + + d3-quadtree@3.0.1: {} + + d3-random@3.0.1: {} + + d3-scale-chromatic@3.1.0: + dependencies: + d3-color: 3.1.0 + d3-interpolate: 3.0.1 + + d3-scale@4.0.2: + dependencies: + d3-array: 3.2.4 + d3-format: 3.1.2 + d3-interpolate: 3.0.1 + d3-time: 3.1.0 + d3-time-format: 4.1.0 + + d3-selection@3.0.0: {} + + d3-shape@3.2.0: + dependencies: + d3-path: 3.1.0 + + d3-time-format@4.1.0: + dependencies: + d3-time: 3.1.0 + + d3-time@3.1.0: + dependencies: + d3-array: 3.2.4 + + d3-timer@3.0.1: {} + + d3-transition@3.0.1(d3-selection@3.0.0): + dependencies: + d3-color: 3.1.0 + d3-dispatch: 3.0.1 + d3-ease: 3.0.1 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-timer: 3.0.1 + + d3-zoom@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-drag: 3.0.0 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-transition: 3.0.1(d3-selection@3.0.0) + + d3@7.9.0: + dependencies: + d3-array: 3.2.4 + d3-axis: 3.0.0 + d3-brush: 3.0.0 + d3-chord: 3.0.1 + d3-color: 3.1.0 + d3-contour: 4.0.2 + d3-delaunay: 6.0.4 + d3-dispatch: 3.0.1 + d3-drag: 3.0.0 + d3-dsv: 3.0.1 + d3-ease: 3.0.1 + d3-fetch: 3.0.1 + d3-force: 3.0.0 + d3-format: 3.1.2 + d3-geo: 3.1.1 + d3-hierarchy: 3.1.2 + d3-interpolate: 3.0.1 + d3-path: 3.1.0 + d3-polygon: 3.0.1 + d3-quadtree: 3.0.1 + d3-random: 3.0.1 + d3-scale: 4.0.2 + d3-scale-chromatic: 3.1.0 + d3-selection: 3.0.0 + d3-shape: 3.2.0 + d3-time: 3.1.0 + d3-time-format: 4.1.0 + d3-timer: 3.0.1 + d3-transition: 3.0.1(d3-selection@3.0.0) + d3-zoom: 3.0.0 + + data-urls@7.0.0: + dependencies: + whatwg-mimetype: 5.0.0 + whatwg-url: 16.0.1 + transitivePeerDependencies: + - '@noble/hashes' + + decimal.js-light@2.5.1: {} + + decimal.js@10.6.0: {} + + delaunator@5.1.0: + dependencies: + robust-predicates: 3.0.3 + + dequal@2.0.3: {} + detect-gpu@5.0.70: dependencies: webgl-constants: 1.1.1 detect-libc@2.1.2: {} + dom-accessibility-api@0.5.16: {} + + dom-accessibility-api@0.6.3: {} + draco3d@1.5.7: {} emoji-regex@10.6.0: {} + entities@8.0.0: {} + env-paths@2.2.1: {} error-ex@1.3.4: @@ -1798,6 +2994,8 @@ snapshots: dependencies: '@types/estree': 1.0.9 + eventemitter3@5.0.4: {} + expect-type@1.4.0: {} fast-deep-equal@3.1.3: {} @@ -1830,25 +3028,53 @@ snapshots: hls.js@1.6.17: {} + html-encoding-sniffer@6.0.0: + dependencies: + '@exodus/bytes': 1.15.1 + transitivePeerDependencies: + - '@noble/hashes' + husky@9.1.7: {} + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + ieee754@1.2.1: {} immediate@3.0.6: {} + immer@11.1.16: {} + + immutable@5.1.9: {} + import-fresh@3.3.1: dependencies: parent-module: 1.0.1 resolve-from: 4.0.0 + indent-string@4.0.0: {} + ini@6.0.0: {} + internmap@2.0.3: {} + is-any-array@3.0.0: {} is-arrayish@0.2.1: {} + is-extglob@2.1.1: + optional: true + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + optional: true + is-plain-obj@4.1.0: {} + is-potential-custom-element-name@1.0.1: {} + is-promise@2.2.2: {} isexe@2.0.0: {} @@ -1868,6 +3094,32 @@ snapshots: dependencies: argparse: 2.0.1 + jsdom@30.0.1: + dependencies: + '@asamuzakjp/css-color': 6.0.7 + '@asamuzakjp/dom-selector': 8.3.2 + '@bramus/specificity': 2.4.2 + '@csstools/css-syntax-patches-for-csstree': 1.1.7(css-tree@3.2.1) + '@exodus/bytes': 1.15.1 + css-tree: 3.2.1 + data-urls: 7.0.0 + decimal.js: 10.6.0 + html-encoding-sniffer: 6.0.0 + is-potential-custom-element-name: 1.0.1 + lru-cache: 11.5.2 + parse5: 8.0.1 + saxes: 6.0.0 + symbol-tree: 3.2.4 + tough-cookie: 6.0.2 + undici: 8.10.0 + w3c-xmlserializer: 5.0.0 + webidl-conversions: 8.0.1 + whatwg-mimetype: 5.0.0 + whatwg-url: 17.1.0 + xml-name-validator: 5.0.0 + transitivePeerDependencies: + - '@noble/hashes' + json-parse-even-better-errors@2.3.1: {} json-schema-traverse@1.0.0: {} @@ -1935,6 +3187,10 @@ snapshots: optionalDependencies: yaml: 2.9.0 + lru-cache@11.5.2: {} + + lz-string@1.5.0: {} + maath@0.10.8(@types/three@0.185.4)(three@0.185.1): dependencies: '@types/three': 0.185.4 @@ -1944,12 +3200,16 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 + mdn-data@2.27.1: {} + meshline@3.3.1(three@0.185.1): dependencies: three: 0.185.1 meshoptimizer@1.1.1: {} + min-indent@1.0.1: {} + ml-array-max@2.0.0: dependencies: is-any-array: 3.0.0 @@ -1975,6 +3235,9 @@ snapshots: nanoid@3.3.17: {} + node-addon-api@7.1.1: + optional: true + obug@2.1.4: {} oxlint@1.77.0: @@ -2010,6 +3273,10 @@ snapshots: json-parse-even-better-errors: 2.3.1 lines-and-columns: 1.2.4 + parse5@8.0.1: + dependencies: + entities: 8.0.0 + path-key@3.1.1: {} pathe@2.0.3: {} @@ -2034,16 +3301,44 @@ snapshots: potpack@1.0.2: {} + pretty-format@27.5.1: + dependencies: + ansi-regex: 5.0.1 + ansi-styles: 5.2.0 + react-is: 17.0.2 + promise-worker-transferable@1.0.4: dependencies: is-promise: 2.2.2 lie: 3.3.0 + punycode@2.3.1: {} + react-dom@19.2.8(react@19.2.8): dependencies: react: 19.2.8 scheduler: 0.27.0 + react-is@17.0.2: {} + + react-is@19.2.8: {} + + react-redux@9.3.0(@types/react@19.2.18)(react@19.2.8)(redux@5.0.1): + dependencies: + '@types/use-sync-external-store': 0.0.6 + react: 19.2.8 + use-sync-external-store: 1.6.0(react@19.2.8) + optionalDependencies: + '@types/react': 19.2.18 + redux: 5.0.1 + + react-router@8.3.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8): + dependencies: + cookie-es: 3.1.1 + react: 19.2.8 + optionalDependencies: + react-dom: 19.2.8(react@19.2.8) + react-use-measure@2.1.7(react-dom@19.2.8(react@19.2.8))(react@19.2.8): dependencies: react: 19.2.8 @@ -2052,12 +3347,49 @@ snapshots: react@19.2.8: {} + readdirp@5.1.1: {} + + recharts@3.10.1(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react-is@19.2.8)(react@19.2.8)(redux@5.0.1): + dependencies: + '@reduxjs/toolkit': 2.12.0(react-redux@9.3.0(@types/react@19.2.18)(react@19.2.8)(redux@5.0.1))(react@19.2.8) + clsx: 2.1.1 + decimal.js-light: 2.5.1 + es-toolkit: 1.50.0 + eventemitter3: 5.0.4 + immer: 11.1.16 + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + react-is: 19.2.8 + react-redux: 9.3.0(@types/react@19.2.18)(react@19.2.8)(redux@5.0.1) + reselect: 5.2.0 + tiny-invariant: 1.3.3 + use-sync-external-store: 1.6.0(react@19.2.8) + victory-vendor: 37.3.6 + transitivePeerDependencies: + - '@types/react' + - redux + + redent@3.0.0: + dependencies: + indent-string: 4.0.0 + strip-indent: 3.0.0 + + redux-thunk@3.1.0(redux@5.0.1): + dependencies: + redux: 5.0.1 + + redux@5.0.1: {} + require-from-string@2.0.2: {} + reselect@5.2.0: {} + resolve-from@4.0.0: {} resolve-from@5.0.0: {} + robust-predicates@3.0.3: {} + rolldown@1.2.3: dependencies: '@oxc-project/types': 0.143.0 @@ -2078,6 +3410,22 @@ snapshots: '@rolldown/binding-win32-arm64-msvc': 1.2.3 '@rolldown/binding-win32-x64-msvc': 1.2.3 + rw@1.3.3: {} + + safer-buffer@2.1.2: {} + + sass@1.102.0: + dependencies: + chokidar: 5.0.0 + immutable: 5.1.9 + source-map-js: 1.2.1 + optionalDependencies: + '@parcel/watcher': 2.6.0 + + saxes@6.0.0: + dependencies: + xmlchars: 2.2.0 + scheduler@0.27.0: {} semver@7.8.5: {} @@ -2120,10 +3468,16 @@ snapshots: dependencies: ansi-regex: 6.2.2 + strip-indent@3.0.0: + dependencies: + min-indent: 1.0.1 + suspend-react@0.1.3(react@19.2.8): dependencies: react: 19.2.8 + symbol-tree@3.2.4: {} + three-mesh-bvh@0.8.3(three@0.185.1): dependencies: three: 0.185.1 @@ -2140,6 +3494,8 @@ snapshots: three@0.185.1: {} + tiny-invariant@1.3.3: {} + tinybench@2.9.0: {} tinyexec@1.3.0: {} @@ -2151,6 +3507,20 @@ snapshots: tinyrainbow@3.1.1: {} + tldts-core@7.4.10: {} + + tldts@7.4.10: + dependencies: + tldts-core: 7.4.10 + + tough-cookie@6.0.2: + dependencies: + tldts: 7.4.10 + + tr46@6.0.0: + dependencies: + punycode: 2.3.1 + troika-three-text@0.52.5(three@0.185.1): dependencies: bidi-js: 1.0.3 @@ -2165,9 +3535,9 @@ snapshots: troika-worker-utils@0.52.0: {} - tunnel-rat@0.1.2(@types/react@19.2.18)(react@19.2.8): + tunnel-rat@0.1.2(@types/react@19.2.18)(immer@11.1.16)(react@19.2.8): dependencies: - zustand: 4.5.7(@types/react@19.2.18)(react@19.2.8) + zustand: 4.5.7(@types/react@19.2.18)(immer@11.1.16)(react@19.2.8) transitivePeerDependencies: - '@types/react' - immer @@ -2177,13 +3547,32 @@ snapshots: undici-types@7.18.2: {} + undici@8.10.0: {} + use-sync-external-store@1.6.0(react@19.2.8): dependencies: react: 19.2.8 utility-types@3.11.0: {} - vite@8.2.0(@types/node@24.13.3)(jiti@2.6.1)(yaml@2.9.0): + victory-vendor@37.3.6: + dependencies: + '@types/d3-array': 3.2.2 + '@types/d3-ease': 3.0.2 + '@types/d3-interpolate': 3.0.4 + '@types/d3-scale': 4.0.9 + '@types/d3-shape': 3.1.8 + '@types/d3-time': 3.0.4 + '@types/d3-timer': 3.0.2 + d3-array: 3.2.4 + d3-ease: 3.0.1 + d3-interpolate: 3.0.1 + d3-scale: 4.0.2 + d3-shape: 3.2.0 + d3-time: 3.1.0 + d3-timer: 3.0.1 + + vite@8.2.0(@types/node@24.13.3)(jiti@2.6.1)(sass@1.102.0)(yaml@2.9.0): dependencies: lightningcss: 1.33.0 picomatch: 4.0.5 @@ -2194,12 +3583,13 @@ snapshots: '@types/node': 24.13.3 fsevents: 2.3.3 jiti: 2.6.1 + sass: 1.102.0 yaml: 2.9.0 - vitest@4.1.10(@types/node@24.13.3)(vite@8.2.0(@types/node@24.13.3)(jiti@2.6.1)(yaml@2.9.0)): + vitest@4.1.10(@types/node@24.13.3)(jsdom@30.0.1)(vite@8.2.0(@types/node@24.13.3)(jiti@2.6.1)(sass@1.102.0)(yaml@2.9.0)): dependencies: '@vitest/expect': 4.1.10 - '@vitest/mocker': 4.1.10(vite@8.2.0(@types/node@24.13.3)(jiti@2.6.1)(yaml@2.9.0)) + '@vitest/mocker': 4.1.10(vite@8.2.0(@types/node@24.13.3)(jiti@2.6.1)(sass@1.102.0)(yaml@2.9.0)) '@vitest/pretty-format': 4.1.10 '@vitest/runner': 4.1.10 '@vitest/snapshot': 4.1.10 @@ -2216,17 +3606,42 @@ snapshots: tinyexec: 1.3.0 tinyglobby: 0.2.17 tinyrainbow: 3.1.1 - vite: 8.2.0(@types/node@24.13.3)(jiti@2.6.1)(yaml@2.9.0) + vite: 8.2.0(@types/node@24.13.3)(jiti@2.6.1)(sass@1.102.0)(yaml@2.9.0) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 24.13.3 + jsdom: 30.0.1 transitivePeerDependencies: - msw + w3c-xmlserializer@5.0.0: + dependencies: + xml-name-validator: 5.0.0 + webgl-constants@1.1.1: {} webgl-sdf-generator@1.1.1: {} + webidl-conversions@8.0.1: {} + + whatwg-mimetype@5.0.0: {} + + whatwg-url@16.0.1: + dependencies: + '@exodus/bytes': 1.15.1 + tr46: 6.0.0 + webidl-conversions: 8.0.1 + transitivePeerDependencies: + - '@noble/hashes' + + whatwg-url@17.1.0: + dependencies: + '@exodus/bytes': 1.15.1 + tr46: 6.0.0 + webidl-conversions: 8.0.1 + transitivePeerDependencies: + - '@noble/hashes' + which@2.0.2: dependencies: isexe: 2.0.0 @@ -2242,6 +3657,10 @@ snapshots: string-width: 7.2.0 strip-ansi: 7.2.0 + xml-name-validator@5.0.0: {} + + xmlchars@2.2.0: {} + y18n@5.0.8: {} yaml@2.9.0: @@ -2260,15 +3679,17 @@ snapshots: zod@4.4.3: {} - zustand@4.5.7(@types/react@19.2.18)(react@19.2.8): + zustand@4.5.7(@types/react@19.2.18)(immer@11.1.16)(react@19.2.8): dependencies: use-sync-external-store: 1.6.0(react@19.2.8) optionalDependencies: '@types/react': 19.2.18 + immer: 11.1.16 react: 19.2.8 - zustand@5.0.14(@types/react@19.2.18)(react@19.2.8)(use-sync-external-store@1.6.0(react@19.2.8)): + zustand@5.0.14(@types/react@19.2.18)(immer@11.1.16)(react@19.2.8)(use-sync-external-store@1.6.0(react@19.2.8)): optionalDependencies: '@types/react': 19.2.18 + immer: 11.1.16 react: 19.2.8 use-sync-external-store: 1.6.0(react@19.2.8) diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000..620a7bb --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,2 @@ +allowBuilds: + '@parcel/watcher': true diff --git a/src/application/buildSpatialEmbeddings.test.ts b/src/application/buildSpatialEmbeddings.test.ts index 35a8989..8cc78a5 100644 --- a/src/application/buildSpatialEmbeddings.test.ts +++ b/src/application/buildSpatialEmbeddings.test.ts @@ -1,12 +1,10 @@ import { readFileSync } from 'node:fs'; -import { fileURLToPath } from 'node:url'; +import { resolve } from 'node:path'; import { describe, expect, it } from 'vitest'; import { parseFdjCsv } from '../infrastructure/csv/parseFdjCsv.ts'; import { buildSpatialEmbeddings } from './buildSpatialEmbeddings.ts'; -const REAL_CSV_PATH = fileURLToPath( - new URL('../../public/results/euromillions_202002.csv', import.meta.url), -); +const REAL_CSV_PATH = resolve(process.cwd(), 'public/results/euromillions_202002.csv'); describe('buildSpatialEmbeddings', () => { it('produces one finite 3D SpatialEmbedding per draw from the real dataset', () => { diff --git a/src/infrastructure/csv/parseFdjCsv.test.ts b/src/infrastructure/csv/parseFdjCsv.test.ts index c6014fd..0b682ed 100644 --- a/src/infrastructure/csv/parseFdjCsv.test.ts +++ b/src/infrastructure/csv/parseFdjCsv.test.ts @@ -1,11 +1,9 @@ import { readFileSync } from 'node:fs'; -import { fileURLToPath } from 'node:url'; +import { resolve } from 'node:path'; import { describe, expect, it } from 'vitest'; import { parseFdjCsv } from './parseFdjCsv.ts'; -const REAL_CSV_PATH = fileURLToPath( - new URL('../../../public/results/euromillions_202002.csv', import.meta.url), -); +const REAL_CSV_PATH = resolve(process.cwd(), 'public/results/euromillions_202002.csv'); describe('parseFdjCsv', () => { it('parses a minimal FDJ-shaped CSV into Draws', () => { diff --git a/src/shared/testSetup.ts b/src/shared/testSetup.ts new file mode 100644 index 0000000..bb02c60 --- /dev/null +++ b/src/shared/testSetup.ts @@ -0,0 +1 @@ +import '@testing-library/jest-dom/vitest'; diff --git a/vite.config.ts b/vite.config.ts index 8291117..a2d469a 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -5,7 +5,8 @@ import react from '@vitejs/plugin-react' export default defineConfig({ plugins: [react()], test: { - environment: 'node', - include: ['src/**/*.test.ts'], + environment: 'jsdom', + setupFiles: ['./src/shared/testSetup.ts'], + include: ['src/**/*.test.{ts,tsx}'], }, }) From 8be8449d1600f9c25bcdb138e0df0e09a3d5ad32 Mon Sep 17 00:00:00 2001 From: keyobs Date: Sun, 9 Aug 2026 21:23:28 +0200 Subject: [PATCH 11/68] feat(domain): add GeometryDistance between two GeometryDescriptors total + always-exposed components: Jaccard on numbers/stars, decade bucket L1 distance, normalized sum diff, normalized mean gap diff, parity diff. distance(x,x)=0 and symmetry are covered by tests; buildGeometryDescriptor already did the buckets/gaps/parity work so this reuses it instead of recomputing. --- src/domain/geometry/GeometryDistance.test.ts | 49 ++++++++++++++++++++ src/domain/geometry/GeometryDistance.ts | 49 ++++++++++++++++++++ 2 files changed, 98 insertions(+) create mode 100644 src/domain/geometry/GeometryDistance.test.ts create mode 100644 src/domain/geometry/GeometryDistance.ts diff --git a/src/domain/geometry/GeometryDistance.test.ts b/src/domain/geometry/GeometryDistance.test.ts new file mode 100644 index 0000000..ec190ab --- /dev/null +++ b/src/domain/geometry/GeometryDistance.test.ts @@ -0,0 +1,49 @@ +import { describe, expect, it } from 'vitest'; +import { parseGrid } from '../grid/Grid.ts'; +import { buildGeometryDescriptor } from './GeometryDescriptor.ts'; +import { computeGeometryDistance } from './GeometryDistance.ts'; + +const descriptorOf = (numbers: number[], stars: number[]) => + buildGeometryDescriptor(parseGrid({ numbers, stars })); + +describe('computeGeometryDistance', () => { + it('is zero for a grid compared to itself', () => { + const descriptor = descriptorOf([3, 7, 19, 31, 42], [2, 9]); + + const distance = computeGeometryDistance(descriptor, descriptor); + + expect(distance.total).toBe(0); + for (const value of Object.values(distance.components)) { + expect(value).toBe(0); + } + }); + + it('is symmetric', () => { + const a = descriptorOf([3, 7, 19, 31, 42], [2, 9]); + const b = descriptorOf([1, 2, 3, 4, 5], [1, 2]); + + expect(computeGeometryDistance(a, b)).toEqual(computeGeometryDistance(b, a)); + }); + + it('always exposes its components alongside the total', () => { + const a = descriptorOf([3, 7, 19, 31, 42], [2, 9]); + const b = descriptorOf([1, 2, 3, 4, 5], [1, 2]); + + const distance = computeGeometryDistance(a, b); + + expect(Object.keys(distance.components).sort()).toEqual( + ['buckets', 'gaps', 'numbers', 'parity', 'stars', 'sum'].sort(), + ); + }); + + it('rates a near-identical grid as closer than a wildly different one', () => { + const reference = descriptorOf([3, 7, 19, 31, 42], [2, 9]); + const almostSame = descriptorOf([3, 7, 19, 31, 43], [2, 9]); + const veryDifferent = descriptorOf([1, 2, 3, 4, 5], [1, 2]); + + const closeDistance = computeGeometryDistance(reference, almostSame); + const farDistance = computeGeometryDistance(reference, veryDifferent); + + expect(closeDistance.total).toBeLessThan(farDistance.total); + }); +}); diff --git a/src/domain/geometry/GeometryDistance.ts b/src/domain/geometry/GeometryDistance.ts new file mode 100644 index 0000000..414340c --- /dev/null +++ b/src/domain/geometry/GeometryDistance.ts @@ -0,0 +1,49 @@ +import type { GeometryDescriptor } from './GeometryDescriptor.ts'; + +export interface GeometryDistance { + total: number; + components: Record; +} + +const jaccardDistance = (a: readonly number[], b: readonly number[]): number => { + const setA = new Set(a); + const setB = new Set(b); + const union = new Set([...setA, ...setB]); + const intersectionSize = [...setA].filter((value) => setB.has(value)).length; + return 1 - intersectionSize / union.size; +}; + +const MAX_SUM_RANGE = 240 - 15; // (46+47+48+49+50) - (1+2+3+4+5) +const MAX_GAP = 49; // widest possible single gap (numbers 1 and 50) + +export const computeGeometryDistance = ( + a: GeometryDescriptor, + b: GeometryDescriptor, +): GeometryDistance => { + const bucketsDistance = + a.decadeBuckets.reduce((acc, count, index) => acc + Math.abs(count - b.decadeBuckets[index]), 0) / + 10; + + const sumDistance = Math.abs(a.sum - b.sum) / MAX_SUM_RANGE; + + const gapsDistance = + a.gaps.reduce((acc, gap, index) => acc + Math.abs(gap - b.gaps[index]), 0) / + a.gaps.length / + MAX_GAP; + + const parityDistance = Math.abs(a.oddCount - b.oddCount) / a.grid.numbers.length; + + const components: Record = { + numbers: jaccardDistance(a.grid.numbers, b.grid.numbers), + buckets: bucketsDistance, + sum: sumDistance, + gaps: gapsDistance, + parity: parityDistance, + stars: jaccardDistance(a.grid.stars, b.grid.stars), + }; + + const total = + Object.values(components).reduce((acc, value) => acc + value, 0) / Object.keys(components).length; + + return { total, components }; +}; From c1b6697cd42b5a856341c57330c23fbbb7246fd2 Mon Sep 17 00:00:00 2001 From: keyobs Date: Sun, 9 Aug 2026 21:26:49 +0200 Subject: [PATCH 12/68] feat(domain): add the four evaluation scores (structure/originality/temporal/confidence) evaluateGrid(grid, history) computes: - structure: 1 - mean distance to the 20 nearest historical draws, with sum/amplitude percentile, decade-signature and parity match rate as factors - originality: penalizes human-pick patterns (consecutive numbers, same-units pairs, multiples of five, <=31-only numbers) - purely descriptive, never framed as changing draw probability - temporal: mean distance to draws inside each TemporalWindow (1/3/6/12/25/50/all), averaged with no window privileged - matches the spec's explicit warning that 6 has no special status - confidence: inverse coefficient of variation of the nearest-neighbor distances (tight neighbor cluster = stable diagnosis) Also adds findNearestNeighbors (generic k-NN over GeometryDescriptors, reusable by /geometry) and classifyReading for the structure x originality reading matrix. Scoring formulas are a reasonable, honest first pass - not spec'd exactly - flagged for adjustment. --- src/domain/geometry/nearestNeighbors.test.ts | 24 ++++ src/domain/geometry/nearestNeighbors.ts | 27 ++++ src/domain/scoring/TemporalWindow.test.ts | 37 +++++ src/domain/scoring/TemporalWindow.ts | 24 ++++ src/domain/scoring/evaluateGrid.test.ts | 57 ++++++++ src/domain/scoring/evaluateGrid.ts | 140 +++++++++++++++++++ 6 files changed, 309 insertions(+) create mode 100644 src/domain/geometry/nearestNeighbors.test.ts create mode 100644 src/domain/geometry/nearestNeighbors.ts create mode 100644 src/domain/scoring/TemporalWindow.test.ts create mode 100644 src/domain/scoring/TemporalWindow.ts create mode 100644 src/domain/scoring/evaluateGrid.test.ts create mode 100644 src/domain/scoring/evaluateGrid.ts diff --git a/src/domain/geometry/nearestNeighbors.test.ts b/src/domain/geometry/nearestNeighbors.test.ts new file mode 100644 index 0000000..f6397ab --- /dev/null +++ b/src/domain/geometry/nearestNeighbors.test.ts @@ -0,0 +1,24 @@ +import { describe, expect, it } from 'vitest'; +import { parseGrid } from '../grid/Grid.ts'; +import { buildGeometryDescriptor } from './GeometryDescriptor.ts'; +import { findNearestNeighbors } from './nearestNeighbors.ts'; + +const descriptorOf = (numbers: number[], stars: number[]) => + buildGeometryDescriptor(parseGrid({ numbers, stars })); + +describe('findNearestNeighbors', () => { + it('returns the k closest candidates ordered by ascending distance', () => { + const target = descriptorOf([3, 7, 19, 31, 42], [2, 9]); + const candidates = [ + { item: 'almost-same', descriptor: descriptorOf([3, 7, 19, 31, 43], [2, 9]) }, + { item: 'very-different', descriptor: descriptorOf([1, 2, 3, 4, 5], [1, 2]) }, + { item: 'somewhat-different', descriptor: descriptorOf([3, 7, 19, 30, 42], [2, 9]) }, + ]; + + const neighbors = findNearestNeighbors(target, candidates, 2); + + expect(neighbors).toHaveLength(2); + expect(neighbors[0].item).toBe('almost-same'); + expect(neighbors[0].distance).toBeLessThanOrEqual(neighbors[1].distance); + }); +}); diff --git a/src/domain/geometry/nearestNeighbors.ts b/src/domain/geometry/nearestNeighbors.ts new file mode 100644 index 0000000..2ce98f6 --- /dev/null +++ b/src/domain/geometry/nearestNeighbors.ts @@ -0,0 +1,27 @@ +import type { GeometryDescriptor } from './GeometryDescriptor.ts'; +import { computeGeometryDistance } from './GeometryDistance.ts'; + +export interface GeometryCandidate { + item: T; + descriptor: GeometryDescriptor; +} + +export interface NearestNeighbor { + item: T; + descriptor: GeometryDescriptor; + distance: number; +} + +export const findNearestNeighbors = ( + target: GeometryDescriptor, + candidates: GeometryCandidate[], + k: number, +): NearestNeighbor[] => + candidates + .map(({ item, descriptor }) => ({ + item, + descriptor, + distance: computeGeometryDistance(target, descriptor).total, + })) + .sort((a, b) => a.distance - b.distance) + .slice(0, k); diff --git a/src/domain/scoring/TemporalWindow.test.ts b/src/domain/scoring/TemporalWindow.test.ts new file mode 100644 index 0000000..2c28b63 --- /dev/null +++ b/src/domain/scoring/TemporalWindow.test.ts @@ -0,0 +1,37 @@ +import { describe, expect, it } from 'vitest'; +import type { Draw } from '../draw/Draw.ts'; +import { filterByWindow, latestDrawDate } from './TemporalWindow.ts'; + +const drawOn = (date: string): Draw => ({ + id: date, + date, + numbers: [1, 2, 3, 4, 5], + stars: [1, 2], + source: 'manual', +}); + +describe('filterByWindow', () => { + const draws = [drawOn('2020-01-01'), drawOn('2023-01-01'), drawOn('2025-06-01'), drawOn('2026-01-01')]; + const referenceDate = latestDrawDate(draws); + + it('returns every draw for the "all" window', () => { + expect(filterByWindow(draws, 'all', referenceDate)).toHaveLength(4); + }); + + it('keeps only draws within N years of the reference date', () => { + const oneYear = filterByWindow(draws, 1, referenceDate); + expect(oneYear.map((d) => d.date)).toEqual(['2025-06-01', '2026-01-01']); + }); + + it('has no privileged window - a 6-year window is just another cutoff', () => { + const sixYears = filterByWindow(draws, 6, referenceDate); + expect(sixYears).toHaveLength(4); + }); +}); + +describe('latestDrawDate', () => { + it('finds the most recent draw date', () => { + const draws = [drawOn('2020-01-01'), drawOn('2026-01-01'), drawOn('2023-01-01')]; + expect(latestDrawDate(draws).toISOString().slice(0, 10)).toBe('2026-01-01'); + }); +}); diff --git a/src/domain/scoring/TemporalWindow.ts b/src/domain/scoring/TemporalWindow.ts new file mode 100644 index 0000000..dd25cd1 --- /dev/null +++ b/src/domain/scoring/TemporalWindow.ts @@ -0,0 +1,24 @@ +import type { Draw } from '../draw/Draw.ts'; + +export type TemporalWindow = 1 | 3 | 6 | 12 | 25 | 50 | 'all'; + +export const TEMPORAL_WINDOWS: TemporalWindow[] = [1, 3, 6, 12, 25, 50, 'all']; + +export const filterByWindow = ( + draws: Draw[], + window: TemporalWindow, + referenceDate: Date, +): Draw[] => { + if (window === 'all') return draws; + + const cutoff = new Date(referenceDate); + cutoff.setFullYear(cutoff.getFullYear() - window); + + return draws.filter((draw) => new Date(draw.date) >= cutoff); +}; + +export const latestDrawDate = (draws: Draw[]): Date => + draws.reduce((latest, draw) => { + const drawDate = new Date(draw.date); + return drawDate > latest ? drawDate : latest; + }, new Date(0)); diff --git a/src/domain/scoring/evaluateGrid.test.ts b/src/domain/scoring/evaluateGrid.test.ts new file mode 100644 index 0000000..2e8be72 --- /dev/null +++ b/src/domain/scoring/evaluateGrid.test.ts @@ -0,0 +1,57 @@ +import { readFileSync } from 'node:fs'; +import { resolve } from 'node:path'; +import { describe, expect, it } from 'vitest'; +import { parseFdjCsv } from '../../infrastructure/csv/parseFdjCsv.ts'; +import { parseGrid } from '../grid/Grid.ts'; +import { classifyReading, evaluateGrid } from './evaluateGrid.ts'; + +const REAL_CSV_PATH = resolve(process.cwd(), 'public/results/euromillions_202002.csv'); +const history = parseFdjCsv(readFileSync(REAL_CSV_PATH, 'utf8')); + +describe('evaluateGrid', () => { + it('produces four 0-100 scores, each with non-empty factors', () => { + const grid = parseGrid({ numbers: [3, 7, 19, 31, 42], stars: [2, 9] }); + + const scores = evaluateGrid(grid, history); + + for (const score of Object.values(scores)) { + expect(score.value).toBeGreaterThanOrEqual(0); + expect(score.value).toBeLessThanOrEqual(100); + expect(score.factors.length).toBeGreaterThan(0); + } + }); + + it('is deterministic for the same grid and history', () => { + const grid = parseGrid({ numbers: [3, 7, 19, 31, 42], stars: [2, 9] }); + + expect(evaluateGrid(grid, history)).toEqual(evaluateGrid(grid, history)); + }); + + it('rates a real historical draw as structurally closer than an arbitrary far-out grid', () => { + const realDraw = history[0]; + const commonGrid = parseGrid({ numbers: [...realDraw.numbers], stars: [...realDraw.stars] }); + const farOutGrid = parseGrid({ numbers: [1, 2, 3, 4, 5], stars: [1, 2] }); + + const commonScores = evaluateGrid(commonGrid, history); + const farOutScores = evaluateGrid(farOutGrid, history); + + expect(commonScores.structure.value).toBeGreaterThan(farOutScores.structure.value); + }); + + it('classifies a common-structure, low-originality grid as "classique"', () => { + const realDraw = history[0]; + const grid = parseGrid({ numbers: [...realDraw.numbers], stars: [...realDraw.stars] }); + + const scores = evaluateGrid(grid, history); + + expect(classifyReading(scores)).toBe( + scores.originality.value >= 50 ? 'interessante' : 'classique', + ); + }); + + it('handles an empty history without throwing', () => { + const grid = parseGrid({ numbers: [3, 7, 19, 31, 42], stars: [2, 9] }); + + expect(() => evaluateGrid(grid, [])).not.toThrow(); + }); +}); diff --git a/src/domain/scoring/evaluateGrid.ts b/src/domain/scoring/evaluateGrid.ts new file mode 100644 index 0000000..e50a412 --- /dev/null +++ b/src/domain/scoring/evaluateGrid.ts @@ -0,0 +1,140 @@ +import type { Draw } from '../draw/Draw.ts'; +import type { Grid } from '../grid/Grid.ts'; +import { extractFeatures } from '../features/FeatureExtractor.ts'; +import { buildGeometryDescriptor } from '../geometry/GeometryDescriptor.ts'; +import { computeGeometryDistance } from '../geometry/GeometryDistance.ts'; +import { findNearestNeighbors } from '../geometry/nearestNeighbors.ts'; +import { TEMPORAL_WINDOWS, filterByWindow, latestDrawDate } from './TemporalWindow.ts'; + +export interface ScoreFactor { + label: string; + value: number; +} + +export interface Score { + value: number; + factors: ScoreFactor[]; +} + +export interface EvaluationScores { + structure: Score; + originality: Score; + temporal: Score; + confidence: Score; +} + +const NEIGHBOR_SAMPLE_SIZE = 20; + +const mean = (values: number[]): number => + values.length === 0 ? 0 : values.reduce((acc, value) => acc + value, 0) / values.length; + +const standardDeviation = (values: number[]): number => { + if (values.length === 0) return 0; + const average = mean(values); + return Math.sqrt(mean(values.map((value) => (value - average) ** 2))); +}; + +const percentileRank = (value: number, population: number[]): number => + population.length === 0 + ? 50 + : (population.filter((candidate) => candidate < value).length / population.length) * 100; + +const toScoreValue = (ratio: number): number => Math.round(100 * Math.min(Math.max(ratio, 0), 1)); + +export const evaluateGrid = (grid: Grid, history: Draw[]): EvaluationScores => { + const descriptor = buildGeometryDescriptor(grid); + const historyEntries = history.map((draw) => ({ + item: draw, + descriptor: buildGeometryDescriptor({ numbers: draw.numbers, stars: draw.stars }), + })); + + const neighbors = findNearestNeighbors(descriptor, historyEntries, NEIGHBOR_SAMPLE_SIZE); + const neighborDistances = neighbors.map((neighbor) => neighbor.distance); + const meanNeighborDistance = mean(neighborDistances); + + const historySums = historyEntries.map(({ descriptor: entry }) => entry.sum); + const historyRanges = historyEntries.map(({ descriptor: entry }) => entry.range); + const sameDecadeSignatureCount = historyEntries.filter(({ descriptor: entry }) => + entry.decadeBuckets.every((count, index) => count === descriptor.decadeBuckets[index]), + ).length; + const sameParityCount = historyEntries.filter( + ({ descriptor: entry }) => entry.oddCount === descriptor.oddCount, + ).length; + + const structure: Score = { + value: toScoreValue(1 - meanNeighborDistance), + factors: [ + { label: 'meanNeighborDistance', value: meanNeighborDistance }, + { label: 'sumPercentile', value: percentileRank(descriptor.sum, historySums) }, + { label: 'amplitudePercentile', value: percentileRank(descriptor.range, historyRanges) }, + { + label: 'decadeSignatureMatchRate', + value: history.length === 0 ? 0 : (sameDecadeSignatureCount / history.length) * 100, + }, + { + label: 'parityMatchRate', + value: history.length === 0 ? 0 : (sameParityCount / history.length) * 100, + }, + ], + }; + + const features = extractFeatures(grid).values; + const humanPatternRatio = + 0.25 * Math.min(features.consecutivePairsCount / 4, 1) + + 0.25 * Math.min(features.sameUnitsPairsCount / 10, 1) + + 0.25 * Math.min(features.multiplesOfFiveCount / 5, 1) + + 0.25 * Math.min((5 - features.aboveThirtyOneCount) / 5, 1); + + const originality: Score = { + value: toScoreValue(1 - humanPatternRatio), + factors: [ + { label: 'consecutivePairsCount', value: features.consecutivePairsCount }, + { label: 'sameUnitsPairsCount', value: features.sameUnitsPairsCount }, + { label: 'multiplesOfFiveCount', value: features.multiplesOfFiveCount }, + { label: 'aboveThirtyOneCount', value: features.aboveThirtyOneCount }, + ], + }; + + const referenceDate = latestDrawDate(history); + const windowFactors = TEMPORAL_WINDOWS.map((window) => { + const windowDraws = filterByWindow(history, window, referenceDate); + const windowDistances = windowDraws.map( + (draw) => + computeGeometryDistance( + descriptor, + buildGeometryDescriptor({ numbers: draw.numbers, stars: draw.stars }), + ).total, + ); + return { label: `window_${window}`, value: toScoreValue(1 - mean(windowDistances)) }; + }); + + const temporal: Score = { + value: Math.round(mean(windowFactors.map((factor) => factor.value))), + factors: windowFactors, + }; + + const neighborDistanceStdDev = standardDeviation(neighborDistances); + const coefficientOfVariation = + meanNeighborDistance === 0 ? 0 : neighborDistanceStdDev / meanNeighborDistance; + + const confidence: Score = { + value: toScoreValue(1 - coefficientOfVariation), + factors: [ + { label: 'sampleSize', value: history.length }, + { label: 'neighborMeanDistance', value: meanNeighborDistance }, + { label: 'neighborDistanceStdDev', value: neighborDistanceStdDev }, + ], + }; + + return { structure, originality, temporal, confidence }; +}; + +export type ReadingMatrixLabel = 'classique' | 'interessante' | 'atypique' | 'tres-atypique'; + +export const classifyReading = (scores: EvaluationScores): ReadingMatrixLabel => { + const isCommonStructure = scores.structure.value >= 50; + const isHighOriginality = scores.originality.value >= 50; + + if (isCommonStructure) return isHighOriginality ? 'interessante' : 'classique'; + return isHighOriginality ? 'tres-atypique' : 'atypique'; +}; From b5b26db41c20452b32f00d9455e9656a86be9ae0 Mon Sep 17 00:00:00 2001 From: keyobs Date: Sun, 9 Aug 2026 21:27:56 +0200 Subject: [PATCH 13/68] feat(infra): add DrawRepository backed by the FDJ CSV createCsvDrawRepository implements getLatest/getAll/getByDate against a fetched CSV, parsed once and cached in memory. src/application/ drawRepository.ts wires the single app-wide instance pages will consume through TanStack Query. --- src/application/drawRepository.ts | 3 ++ .../repositories/CsvDrawRepository.test.ts | 45 +++++++++++++++++++ .../repositories/CsvDrawRepository.ts | 32 +++++++++++++ .../repositories/DrawRepository.ts | 7 +++ 4 files changed, 87 insertions(+) create mode 100644 src/application/drawRepository.ts create mode 100644 src/infrastructure/repositories/CsvDrawRepository.test.ts create mode 100644 src/infrastructure/repositories/CsvDrawRepository.ts create mode 100644 src/infrastructure/repositories/DrawRepository.ts diff --git a/src/application/drawRepository.ts b/src/application/drawRepository.ts new file mode 100644 index 0000000..8355252 --- /dev/null +++ b/src/application/drawRepository.ts @@ -0,0 +1,3 @@ +import { createCsvDrawRepository } from '../infrastructure/repositories/CsvDrawRepository.ts'; + +export const drawRepository = createCsvDrawRepository(); diff --git a/src/infrastructure/repositories/CsvDrawRepository.test.ts b/src/infrastructure/repositories/CsvDrawRepository.test.ts new file mode 100644 index 0000000..e643ff1 --- /dev/null +++ b/src/infrastructure/repositories/CsvDrawRepository.test.ts @@ -0,0 +1,45 @@ +import { readFileSync } from 'node:fs'; +import { resolve } from 'node:path'; +import { afterEach, describe, expect, it, vi } from 'vitest'; +import { createCsvDrawRepository } from './CsvDrawRepository.ts'; + +const REAL_CSV_PATH = resolve(process.cwd(), 'public/results/euromillions_202002.csv'); +const csvText = readFileSync(REAL_CSV_PATH, 'utf8'); + +describe('createCsvDrawRepository', () => { + afterEach(() => { + vi.unstubAllGlobals(); + }); + + it('fetches the CSV once and reuses it across calls', async () => { + const fetchSpy = vi.fn(() => Promise.resolve(new Response(csvText))); + vi.stubGlobal('fetch', fetchSpy); + const repository = createCsvDrawRepository('/results/test.csv'); + + const all = await repository.getAll(); + const latest = await repository.getLatest(5); + + expect(fetchSpy).toHaveBeenCalledTimes(1); + expect(all.length).toBeGreaterThan(600); + expect(latest).toHaveLength(5); + }); + + it('getLatest returns draws ordered from most to least recent', async () => { + vi.stubGlobal('fetch', () => Promise.resolve(new Response(csvText))); + const repository = createCsvDrawRepository('/results/test.csv'); + + const latest = await repository.getLatest(10); + + const dates = latest.map((draw) => new Date(draw.date).getTime()); + expect(dates).toEqual([...dates].sort((a, b) => b - a)); + }); + + it('getByDate finds an existing draw and returns null for an unknown date', async () => { + vi.stubGlobal('fetch', () => Promise.resolve(new Response(csvText))); + const repository = createCsvDrawRepository('/results/test.csv'); + const [mostRecent] = await repository.getLatest(1); + + expect(await repository.getByDate(mostRecent.date)).toEqual(mostRecent); + expect(await repository.getByDate('1900-01-01')).toBeNull(); + }); +}); diff --git a/src/infrastructure/repositories/CsvDrawRepository.ts b/src/infrastructure/repositories/CsvDrawRepository.ts new file mode 100644 index 0000000..64a0756 --- /dev/null +++ b/src/infrastructure/repositories/CsvDrawRepository.ts @@ -0,0 +1,32 @@ +import type { Draw } from '../../domain/draw/Draw.ts'; +import { parseFdjCsv } from '../csv/parseFdjCsv.ts'; +import type { DrawRepository } from './DrawRepository.ts'; + +export const DEFAULT_CSV_URL = '/results/euromillions_202002.csv'; + +export const createCsvDrawRepository = (csvUrl: string = DEFAULT_CSV_URL): DrawRepository => { + let cachedDraws: Promise | null = null; + + const loadDraws = (): Promise => { + cachedDraws ??= fetch(csvUrl) + .then((response) => response.text()) + .then((csvText) => parseFdjCsv(csvText)); + return cachedDraws; + }; + + return { + async getAll() { + return loadDraws(); + }, + async getLatest(limit) { + const draws = await loadDraws(); + return [...draws] + .sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()) + .slice(0, limit); + }, + async getByDate(date) { + const draws = await loadDraws(); + return draws.find((draw) => draw.date === date) ?? null; + }, + }; +}; diff --git a/src/infrastructure/repositories/DrawRepository.ts b/src/infrastructure/repositories/DrawRepository.ts new file mode 100644 index 0000000..651f27b --- /dev/null +++ b/src/infrastructure/repositories/DrawRepository.ts @@ -0,0 +1,7 @@ +import type { Draw } from '../../domain/draw/Draw.ts'; + +export interface DrawRepository { + getLatest(limit: number): Promise; + getAll(): Promise; + getByDate(date: string): Promise; +} From eb514bd94f59f1b720d8f7a4501dd0128b737f3e Mon Sep 17 00:00:00 2001 From: keyobs Date: Sun, 9 Aug 2026 21:29:41 +0200 Subject: [PATCH 14/68] feat(application): generate structurally-common/balanced/anti-share variations Each variation stays tied to the original grid (per-number nearest valid replacement) rather than producing a fixed universal output: structurally-common swaps toward the most frequent historical number in the same decade bucket, balanced spreads one number per decade (+ one odd/one even star), anti-share swaps out <=31 and multiples-of- five numbers. None is framed as more likely to be drawn - every valid Grid keeps the same theoretical probability. --- src/application/generateVariations.test.ts | 51 ++++++++++ src/application/generateVariations.ts | 106 +++++++++++++++++++++ 2 files changed, 157 insertions(+) create mode 100644 src/application/generateVariations.test.ts create mode 100644 src/application/generateVariations.ts diff --git a/src/application/generateVariations.test.ts b/src/application/generateVariations.test.ts new file mode 100644 index 0000000..fec0d2c --- /dev/null +++ b/src/application/generateVariations.test.ts @@ -0,0 +1,51 @@ +import { readFileSync } from 'node:fs'; +import { resolve } from 'node:path'; +import { describe, expect, it } from 'vitest'; +import { parseFdjCsv } from '../infrastructure/csv/parseFdjCsv.ts'; +import { parseGrid } from '../domain/grid/Grid.ts'; +import { generateVariations } from './generateVariations.ts'; + +const REAL_CSV_PATH = resolve(process.cwd(), 'public/results/euromillions_202002.csv'); +const history = parseFdjCsv(readFileSync(REAL_CSV_PATH, 'utf8')); + +describe('generateVariations', () => { + it('produces one valid grid per variation kind', () => { + const grid = parseGrid({ numbers: [3, 7, 19, 31, 42], stars: [2, 9] }); + + const variations = generateVariations(grid, history); + + expect(variations.map((v) => v.kind).sort()).toEqual( + ['anti-share', 'balanced', 'structurally-common'].sort(), + ); + for (const variation of variations) { + expect(new Set(variation.grid.numbers).size).toBe(5); + expect(new Set(variation.grid.stars).size).toBe(2); + } + }); + + it('spreads the balanced variation across the five decades', () => { + const grid = parseGrid({ numbers: [1, 2, 3, 4, 5], stars: [1, 2] }); + + const [, balanced] = generateVariations(grid, history); + + const buckets = balanced.grid.numbers.map((n) => Math.min(Math.floor((n - 1) / 10), 4)); + expect(new Set(buckets).size).toBe(5); + }); + + it('avoids <=31 and multiples-of-five numbers in the anti-share variation', () => { + const grid = parseGrid({ numbers: [1, 5, 10, 15, 20], stars: [1, 2] }); + + const [, , antiShare] = generateVariations(grid, history); + + for (const n of antiShare.grid.numbers) { + expect(n).toBeGreaterThan(31); + expect(n % 5).not.toBe(0); + } + }); + + it('is deterministic for the same grid and history', () => { + const grid = parseGrid({ numbers: [3, 7, 19, 31, 42], stars: [2, 9] }); + + expect(generateVariations(grid, history)).toEqual(generateVariations(grid, history)); + }); +}); diff --git a/src/application/generateVariations.ts b/src/application/generateVariations.ts new file mode 100644 index 0000000..313cc87 --- /dev/null +++ b/src/application/generateVariations.ts @@ -0,0 +1,106 @@ +import type { Draw } from '../domain/draw/Draw.ts'; +import { parseGrid } from '../domain/grid/Grid.ts'; +import type { Grid } from '../domain/grid/Grid.ts'; + +export type VariationKind = 'structurally-common' | 'balanced' | 'anti-share'; + +export interface GridVariation { + kind: VariationKind; + grid: Grid; +} + +const NUMBER_MIN = 1; +const NUMBER_MAX = 50; +const STAR_MIN = 1; +const STAR_MAX = 12; + +const decadeBucketOf = (n: number): number => Math.min(Math.floor((n - 1) / 10), 4); + +const countFrequencies = (values: number[], min: number, max: number): Record => { + const frequencies: Record = {}; + for (let value = min; value <= max; value += 1) frequencies[value] = 0; + for (const value of values) frequencies[value] += 1; + return frequencies; +}; + +const pickDistinct = (candidatesByPreference: number[], taken: Set): number => { + const choice = candidatesByPreference.find((candidate) => !taken.has(candidate)); + if (choice === undefined) throw new Error('No distinct candidate available for this variation'); + return choice; +}; + +const range = (min: number, max: number): number[] => + Array.from({ length: max - min + 1 }, (_, i) => min + i); + +const buildStructurallyCommonVariation = (grid: Grid, history: Draw[]): Grid => { + const numberFrequencies = countFrequencies(history.flatMap((draw) => draw.numbers), NUMBER_MIN, NUMBER_MAX); + const starFrequencies = countFrequencies(history.flatMap((draw) => draw.stars), STAR_MIN, STAR_MAX); + + const taken = new Set(); + const numbers = grid.numbers.map((original) => { + const bucket = decadeBucketOf(original); + const bucketMin = bucket * 10 + 1; + const bucketMax = Math.min(bucket * 10 + 10, NUMBER_MAX); + const candidates = range(bucketMin, bucketMax).sort( + (a, b) => numberFrequencies[b] - numberFrequencies[a] || Math.abs(a - original) - Math.abs(b - original), + ); + const chosen = pickDistinct(candidates, taken); + taken.add(chosen); + return chosen; + }); + + const starCandidates = range(STAR_MIN, STAR_MAX).sort( + (a, b) => starFrequencies[b] - starFrequencies[a] || Math.abs(a - grid.stars[0]) - Math.abs(b - grid.stars[0]), + ); + const stars = starCandidates.slice(0, 2); + + return parseGrid({ numbers, stars }); +}; + +const buildBalancedVariation = (grid: Grid): Grid => { + const taken = new Set(); + const numbers = grid.numbers.map((original, index) => { + const bucketMin = index * 10 + 1; + const bucketMax = Math.min(index * 10 + 10, NUMBER_MAX); + const clamped = Math.min(Math.max(original, bucketMin), bucketMax); + const candidates = range(bucketMin, bucketMax).sort((a, b) => Math.abs(a - clamped) - Math.abs(b - clamped)); + const chosen = pickDistinct(candidates, taken); + taken.add(chosen); + return chosen; + }); + + const [firstStar, secondStar] = grid.stars; + const oddCandidates = [1, 3, 5, 7, 9, 11].sort((a, b) => Math.abs(a - firstStar) - Math.abs(b - firstStar)); + const evenCandidates = [2, 4, 6, 8, 10, 12].sort((a, b) => Math.abs(a - secondStar) - Math.abs(b - secondStar)); + const stars = [oddCandidates[0], evenCandidates[0]]; + + return parseGrid({ numbers, stars }); +}; + +const isShareableNumber = (n: number): boolean => n <= 31 || n % 5 === 0; + +const buildAntiShareVariation = (grid: Grid): Grid => { + const taken = new Set(grid.numbers); + const numbers = grid.numbers.map((original) => { + if (!isShareableNumber(original)) return original; + taken.delete(original); + const candidates = range(NUMBER_MIN, NUMBER_MAX) + .filter((n) => !isShareableNumber(n)) + .sort((a, b) => Math.abs(a - original) - Math.abs(b - original)); + const chosen = pickDistinct(candidates, taken); + taken.add(chosen); + return chosen; + }); + + return parseGrid({ numbers, stars: grid.stars }); +}; + +/** + * Descriptive variations only - none is presented as more likely to be drawn. + * Every valid Grid has the same theoretical probability of being drawn. + */ +export const generateVariations = (grid: Grid, history: Draw[]): GridVariation[] => [ + { kind: 'structurally-common', grid: buildStructurallyCommonVariation(grid, history) }, + { kind: 'balanced', grid: buildBalancedVariation(grid) }, + { kind: 'anti-share', grid: buildAntiShareVariation(grid) }, +]; From d8ef39e6e5022061de64faa301d47c60e877669c Mon Sep 17 00:00:00 2001 From: keyobs Date: Mon, 10 Aug 2026 07:51:27 +0200 Subject: [PATCH 15/68] feat(app): add React Router + TanStack Query shell with the three V1 routes AppLayout renders the nav (evaluation/draws/geometry) and an always-visible non-predictive disclaimer footer (data-testid for E2E). / redirects to /evaluation. The ADR-0002 spike moves from src/App.tsx to pages/spike/SpikePage.tsx, reachable at /spike so it stays a working regression check until V4's features/eurospace replaces it. Evaluation/draws/geometry pages are placeholders, filled in by the next commits. --- src/App.tsx | 37 ++++++------------------ src/app/AppLayout.module.scss | 38 +++++++++++++++++++++++++ src/app/AppLayout.tsx | 36 +++++++++++++++++++++++ src/app/queryClient.ts | 3 ++ src/app/router.tsx | 20 +++++++++++++ src/pages/draws/DrawsPage.tsx | 5 ++++ src/pages/evaluation/EvaluationPage.tsx | 5 ++++ src/pages/geometry/GeometryPage.tsx | 5 ++++ src/pages/spike/SpikePage.tsx | 33 +++++++++++++++++++++ 9 files changed, 154 insertions(+), 28 deletions(-) create mode 100644 src/app/AppLayout.module.scss create mode 100644 src/app/AppLayout.tsx create mode 100644 src/app/queryClient.ts create mode 100644 src/app/router.tsx create mode 100644 src/pages/draws/DrawsPage.tsx create mode 100644 src/pages/evaluation/EvaluationPage.tsx create mode 100644 src/pages/geometry/GeometryPage.tsx create mode 100644 src/pages/spike/SpikePage.tsx diff --git a/src/App.tsx b/src/App.tsx index a29300b..c662881 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,33 +1,14 @@ -import { useEffect, useState } from 'react'; -import type { SpatialEmbedding } from './domain/geometry/SpatialEmbedding.ts'; -import { parseFdjCsv } from './infrastructure/csv/parseFdjCsv.ts'; -import { buildSpatialEmbeddings } from './application/buildSpatialEmbeddings.ts'; -import PointCloudScene from './spike/PointCloudScene.tsx'; - -const CSV_URL = '/results/euromillions_202002.csv'; +import { QueryClientProvider } from '@tanstack/react-query'; +import { RouterProvider } from 'react-router'; +import { queryClient } from './app/queryClient.ts'; +import { router } from './app/router.tsx'; const App = () => { - const [embeddings, setEmbeddings] = useState(null); - - useEffect(() => { - let cancelled = false; - - fetch(CSV_URL) - .then((response) => response.text()) - .then((csvText) => { - if (cancelled) return; - const draws = parseFdjCsv(csvText); - setEmbeddings(buildSpatialEmbeddings(draws)); - }); - - return () => { - cancelled = true; - }; - }, []); - - if (!embeddings) return null; - - return ; + return ( + + + + ); }; export default App; diff --git a/src/app/AppLayout.module.scss b/src/app/AppLayout.module.scss new file mode 100644 index 0000000..6c1e0d4 --- /dev/null +++ b/src/app/AppLayout.module.scss @@ -0,0 +1,38 @@ +.layout { + display: flex; + flex-direction: column; + flex: 1; +} + +.nav { + display: flex; + gap: 1.5rem; + padding: 1rem 1.5rem; + border-bottom: 1px solid var(--border); +} + +.navLink { + color: var(--text); + text-decoration: none; + font-weight: 600; + + &:hover { + text-decoration: underline; + } +} + +.navLinkActive { + text-decoration: underline; +} + +.content { + flex: 1; + padding: 1.5rem; +} + +.disclaimer { + padding: 0.75rem 1.5rem; + border-top: 1px solid var(--border); + font-size: 0.85rem; + opacity: 0.8; +} diff --git a/src/app/AppLayout.tsx b/src/app/AppLayout.tsx new file mode 100644 index 0000000..93a86d7 --- /dev/null +++ b/src/app/AppLayout.tsx @@ -0,0 +1,36 @@ +import { NavLink, Outlet } from 'react-router'; +import styles from './AppLayout.module.scss'; + +const NAV_ITEMS = [ + { to: '/evaluation', label: 'Évaluation' }, + { to: '/draws', label: 'Tirages' }, + { to: '/geometry', label: 'Géométrie' }, +]; + +const AppLayout = () => { + return ( +
+ +
+ +
+
+ Toutes les combinaisons EuroMillions valides ont la même probabilité théorique lors d'un + tirage équitable. Cette application analyse des structures historiques et des hypothèses + expérimentales ; elle ne prédit aucun tirage et ne garantit aucun gain. +
+
+ ); +}; + +export default AppLayout; diff --git a/src/app/queryClient.ts b/src/app/queryClient.ts new file mode 100644 index 0000000..6d46de5 --- /dev/null +++ b/src/app/queryClient.ts @@ -0,0 +1,3 @@ +import { QueryClient } from '@tanstack/react-query'; + +export const queryClient = new QueryClient(); diff --git a/src/app/router.tsx b/src/app/router.tsx new file mode 100644 index 0000000..629a27c --- /dev/null +++ b/src/app/router.tsx @@ -0,0 +1,20 @@ +import { Navigate, createBrowserRouter } from 'react-router'; +import AppLayout from './AppLayout.tsx'; +import EvaluationPage from '../pages/evaluation/EvaluationPage.tsx'; +import DrawsPage from '../pages/draws/DrawsPage.tsx'; +import GeometryPage from '../pages/geometry/GeometryPage.tsx'; +import SpikePage from '../pages/spike/SpikePage.tsx'; + +export const router = createBrowserRouter([ + { + path: '/', + element: , + children: [ + { index: true, element: }, + { path: 'evaluation', element: }, + { path: 'draws', element: }, + { path: 'geometry', element: }, + { path: 'spike', element: }, + ], + }, +]); diff --git a/src/pages/draws/DrawsPage.tsx b/src/pages/draws/DrawsPage.tsx new file mode 100644 index 0000000..4b5c116 --- /dev/null +++ b/src/pages/draws/DrawsPage.tsx @@ -0,0 +1,5 @@ +const DrawsPage = () => { + return

Tirages

; +}; + +export default DrawsPage; diff --git a/src/pages/evaluation/EvaluationPage.tsx b/src/pages/evaluation/EvaluationPage.tsx new file mode 100644 index 0000000..6576a68 --- /dev/null +++ b/src/pages/evaluation/EvaluationPage.tsx @@ -0,0 +1,5 @@ +const EvaluationPage = () => { + return

Évaluation

; +}; + +export default EvaluationPage; diff --git a/src/pages/geometry/GeometryPage.tsx b/src/pages/geometry/GeometryPage.tsx new file mode 100644 index 0000000..1fbca2e --- /dev/null +++ b/src/pages/geometry/GeometryPage.tsx @@ -0,0 +1,5 @@ +const GeometryPage = () => { + return

Géométrie

; +}; + +export default GeometryPage; diff --git a/src/pages/spike/SpikePage.tsx b/src/pages/spike/SpikePage.tsx new file mode 100644 index 0000000..b2c11db --- /dev/null +++ b/src/pages/spike/SpikePage.tsx @@ -0,0 +1,33 @@ +import { useEffect, useState } from 'react'; +import type { SpatialEmbedding } from '../../domain/geometry/SpatialEmbedding.ts'; +import { parseFdjCsv } from '../../infrastructure/csv/parseFdjCsv.ts'; +import { buildSpatialEmbeddings } from '../../application/buildSpatialEmbeddings.ts'; +import PointCloudScene from '../../spike/PointCloudScene.tsx'; + +const CSV_URL = '/results/euromillions_202002.csv'; + +const SpikePage = () => { + const [embeddings, setEmbeddings] = useState(null); + + useEffect(() => { + let cancelled = false; + + fetch(CSV_URL) + .then((response) => response.text()) + .then((csvText) => { + if (cancelled) return; + const draws = parseFdjCsv(csvText); + setEmbeddings(buildSpatialEmbeddings(draws)); + }); + + return () => { + cancelled = true; + }; + }, []); + + if (!embeddings) return null; + + return ; +}; + +export default SpikePage; From 3a405d556bbc58deffb5192c319b51c18a90c282 Mon Sep 17 00:00:00 2001 From: keyobs Date: Mon, 10 Aug 2026 07:56:37 +0200 Subject: [PATCH 16/68] feat(evaluation): build the /evaluation page Zod-validated 5-number + 2-star form (plain button + ChangeEvent handlers, no FormEvent), draw history via TanStack Query + drawRepository, then evaluateGrid + classifyReading render the four score cards (each showing its factors) and the reading-matrix label. generateVariations surfaces the three variations with an explicit non-predictive note. "Locate in EuroSpace" stays out entirely until V4. Smoke-tested end to end: fill form -> evaluate -> 4 cards + 3 variations render, no console errors. --- .../evaluation/EvaluationPage.module.scss | 28 +++ src/pages/evaluation/EvaluationPage.tsx | 163 +++++++++++++++++- src/pages/evaluation/ScoreCard.module.scss | 20 +++ src/pages/evaluation/ScoreCard.tsx | 27 +++ 4 files changed, 237 insertions(+), 1 deletion(-) create mode 100644 src/pages/evaluation/EvaluationPage.module.scss create mode 100644 src/pages/evaluation/ScoreCard.module.scss create mode 100644 src/pages/evaluation/ScoreCard.tsx diff --git a/src/pages/evaluation/EvaluationPage.module.scss b/src/pages/evaluation/EvaluationPage.module.scss new file mode 100644 index 0000000..80bdf87 --- /dev/null +++ b/src/pages/evaluation/EvaluationPage.module.scss @@ -0,0 +1,28 @@ +.page { + display: flex; + flex-direction: column; + gap: 1.5rem; + text-align: left; +} + +.form { + display: flex; + flex-wrap: wrap; + align-items: flex-end; + gap: 1.5rem; +} + +.numberInput { + width: 3.5rem; + margin-right: 0.35rem; +} + +.scores { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); + gap: 1rem; +} + +.error { + color: #d1435b; +} diff --git a/src/pages/evaluation/EvaluationPage.tsx b/src/pages/evaluation/EvaluationPage.tsx index 6576a68..78473bb 100644 --- a/src/pages/evaluation/EvaluationPage.tsx +++ b/src/pages/evaluation/EvaluationPage.tsx @@ -1,5 +1,166 @@ +import { useMemo, useState } from 'react'; +import type { ChangeEvent } from 'react'; +import { useQuery } from '@tanstack/react-query'; +import type { Draw } from '../../domain/draw/Draw.ts'; +import type { VariationKind } from '../../application/generateVariations.ts'; +import { generateVariations } from '../../application/generateVariations.ts'; +import { drawRepository } from '../../application/drawRepository.ts'; +import type { Grid } from '../../domain/grid/Grid.ts'; +import { parseGrid } from '../../domain/grid/Grid.ts'; +import type { EvaluationScores, ReadingMatrixLabel } from '../../domain/scoring/evaluateGrid.ts'; +import { classifyReading, evaluateGrid } from '../../domain/scoring/evaluateGrid.ts'; +import ScoreCard from './ScoreCard.tsx'; +import styles from './EvaluationPage.module.scss'; + +const EMPTY_NUMBERS = ['', '', '', '', '']; +const EMPTY_STARS = ['', '']; +const EMPTY_HISTORY: Draw[] = []; + +const READING_LABELS: Record = { + classique: 'Classique', + interessante: 'Intéressante', + atypique: 'Atypique', + 'tres-atypique': 'Très atypique', +}; + +const VARIATION_LABELS: Record = { + 'structurally-common': 'Structurellement courante', + balanced: 'Équilibrée', + 'anti-share': 'Anti-partage', +}; + const EvaluationPage = () => { - return

Évaluation

; + const [numberInputs, setNumberInputs] = useState(EMPTY_NUMBERS); + const [starInputs, setStarInputs] = useState(EMPTY_STARS); + const [grid, setGrid] = useState(null); + const [error, setError] = useState(null); + + const historyQuery = useQuery({ + queryKey: ['draws', 'all'], + queryFn: () => drawRepository.getAll(), + }); + const history = historyQuery.data ?? EMPTY_HISTORY; + + const handleNumberChange = (index: number) => (event: ChangeEvent) => { + const value = event.target.value; + setNumberInputs((previous) => previous.map((current, i) => (i === index ? value : current))); + }; + + const handleStarChange = (index: number) => (event: ChangeEvent) => { + const value = event.target.value; + setStarInputs((previous) => previous.map((current, i) => (i === index ? value : current))); + }; + + const handleEvaluate = () => { + const numbers = numberInputs.map(Number); + const stars = starInputs.map(Number); + + if ([...numbers, ...stars].some((n) => Number.isNaN(n))) { + setGrid(null); + setError('Merci de remplir les 5 numéros et les 2 étoiles.'); + return; + } + + try { + setGrid(parseGrid({ numbers, stars })); + setError(null); + } catch { + setGrid(null); + setError('Grille invalide : 5 numéros distincts entre 1 et 50, 2 étoiles distinctes entre 1 et 12.'); + } + }; + + const scores = useMemo( + () => (grid && history.length > 0 ? evaluateGrid(grid, history) : null), + [grid, history], + ); + const variations = useMemo( + () => (grid && history.length > 0 ? generateVariations(grid, history) : []), + [grid, history], + ); + const reading = scores ? classifyReading(scores) : null; + + return ( +
+

Évaluation d'une grille

+ +
+
+ Numéros (1-50) + {numberInputs.map((value, index) => ( + + ))} +
+
+ Étoiles (1-12) + {starInputs.map((value, index) => ( + + ))} +
+ +
+ + {error && ( +

+ {error} +

+ )} + + {historyQuery.isLoading &&

Chargement de l'historique...

} + + {scores && reading && ( +
+

Lecture : {READING_LABELS[reading]}

+
+ + + + +
+
+ )} + + {variations.length > 0 && ( +
+

Variations

+

+ Ces variations ne sont pas présentées comme plus susceptibles d'être tirées : toutes les + grilles valides ont la même probabilité théorique. +

+
    + {variations.map((variation) => ( +
  • + {VARIATION_LABELS[variation.kind]} : {variation.grid.numbers.join(' · ')} — étoiles{' '} + {variation.grid.stars.join(' · ')} +
  • + ))} +
+
+ )} +
+ ); }; export default EvaluationPage; diff --git a/src/pages/evaluation/ScoreCard.module.scss b/src/pages/evaluation/ScoreCard.module.scss new file mode 100644 index 0000000..48a74b0 --- /dev/null +++ b/src/pages/evaluation/ScoreCard.module.scss @@ -0,0 +1,20 @@ +.card { + border: 1px solid var(--border); + border-radius: 0.5rem; + padding: 1rem; + text-align: left; +} + +.value { + font-size: 1.5rem; + font-weight: 700; + margin: 0.25rem 0 0.75rem; +} + +.factors { + list-style: none; + padding: 0; + margin: 0; + font-size: 0.85rem; + opacity: 0.85; +} diff --git a/src/pages/evaluation/ScoreCard.tsx b/src/pages/evaluation/ScoreCard.tsx new file mode 100644 index 0000000..05ff1d6 --- /dev/null +++ b/src/pages/evaluation/ScoreCard.tsx @@ -0,0 +1,27 @@ +import type { Score } from '../../domain/scoring/evaluateGrid.ts'; +import styles from './ScoreCard.module.scss'; + +interface ScoreCardProps { + title: string; + score: Score; +} + +const ScoreCard = ({ title, score }: ScoreCardProps) => { + return ( +
+

{title}

+

+ {score.value} / 100 +

+
    + {score.factors.map((factor) => ( +
  • + {factor.label}: {Math.round(factor.value * 100) / 100} +
  • + ))} +
+
+ ); +}; + +export default ScoreCard; From 11be58e81e2f48cc8fd88c3618c04a85dcd01e6a Mon Sep 17 00:00:00 2001 From: keyobs Date: Mon, 10 Aug 2026 08:00:38 +0200 Subject: [PATCH 17/68] feat(draws): build the /draws page Table via drawRepository.getAll(): date, numbers, stars, sum, amplitude, parity, decade-signature, and distance to the chronologically previous draw (computeGeometryDistance between consecutive GeometryDescriptors). Defaults to the 50 most recent draws with a toggle to the full 680-draw history. Smoke-tested: row counts, toggle, and the oldest draw correctly showing no previous distance, no console errors. --- src/pages/draws/DrawsPage.module.scss | 20 +++++ src/pages/draws/DrawsPage.tsx | 109 +++++++++++++++++++++++++- 2 files changed, 128 insertions(+), 1 deletion(-) create mode 100644 src/pages/draws/DrawsPage.module.scss diff --git a/src/pages/draws/DrawsPage.module.scss b/src/pages/draws/DrawsPage.module.scss new file mode 100644 index 0000000..6dcc8fb --- /dev/null +++ b/src/pages/draws/DrawsPage.module.scss @@ -0,0 +1,20 @@ +.page { + display: flex; + flex-direction: column; + gap: 1rem; + text-align: left; +} + +.table { + width: 100%; + border-collapse: collapse; + font-size: 0.9rem; + + th, + td { + padding: 0.4rem 0.6rem; + border-bottom: 1px solid var(--border); + text-align: left; + white-space: nowrap; + } +} diff --git a/src/pages/draws/DrawsPage.tsx b/src/pages/draws/DrawsPage.tsx index 4b5c116..20edfd6 100644 --- a/src/pages/draws/DrawsPage.tsx +++ b/src/pages/draws/DrawsPage.tsx @@ -1,5 +1,112 @@ +import { useMemo, useState } from 'react'; +import { useQuery } from '@tanstack/react-query'; +import { drawRepository } from '../../application/drawRepository.ts'; +import type { Draw } from '../../domain/draw/Draw.ts'; +import { buildGeometryDescriptor } from '../../domain/geometry/GeometryDescriptor.ts'; +import { computeGeometryDistance } from '../../domain/geometry/GeometryDistance.ts'; +import styles from './DrawsPage.module.scss'; + +const DEFAULT_LIMIT = 50; + +interface DrawRow { + draw: Draw; + sum: number; + range: number; + oddCount: number; + evenCount: number; + signature: string; + distanceToPrevious: number | null; +} + +const buildRowsMostRecentFirst = (draws: Draw[]): DrawRow[] => { + const sortedAscending = [...draws].sort( + (a, b) => new Date(a.date).getTime() - new Date(b.date).getTime(), + ); + + const rows: DrawRow[] = []; + let previousDescriptor: ReturnType | null = null; + + for (const draw of sortedAscending) { + const descriptor = buildGeometryDescriptor({ numbers: draw.numbers, stars: draw.stars }); + rows.push({ + draw, + sum: descriptor.sum, + range: descriptor.range, + oddCount: descriptor.oddCount, + evenCount: descriptor.evenCount, + signature: descriptor.decadeBuckets.join('-'), + distanceToPrevious: previousDescriptor + ? computeGeometryDistance(descriptor, previousDescriptor).total + : null, + }); + previousDescriptor = descriptor; + } + + return rows.reverse(); +}; + const DrawsPage = () => { - return

Tirages

; + const [showAll, setShowAll] = useState(false); + + const allDrawsQuery = useQuery({ + queryKey: ['draws', 'all'], + queryFn: () => drawRepository.getAll(), + }); + + const rows = useMemo(() => { + const allRows = buildRowsMostRecentFirst(allDrawsQuery.data ?? []); + return showAll ? allRows : allRows.slice(0, DEFAULT_LIMIT); + }, [allDrawsQuery.data, showAll]); + + return ( +
+

Tirages

+ + {allDrawsQuery.isLoading &&

Chargement...

} + + {allDrawsQuery.data && ( + <> +

+ {showAll ? `Historique complet (${rows.length} tirages)` : `${rows.length} derniers tirages`} + {' — '} + +

+ + + + + + + + + + + + + + + {rows.map((row) => ( + + + + + + + + + + + ))} + +
DateNumérosÉtoilesSommeAmplitudeParitéSignatureDistance au précédent
{row.draw.date}{row.draw.numbers.join(' · ')}{row.draw.stars.join(' · ')}{row.sum}{row.range} + {row.oddCount} impair / {row.evenCount} pair + {row.signature}{row.distanceToPrevious === null ? '—' : row.distanceToPrevious.toFixed(3)}
+ + )} +
+ ); }; export default DrawsPage; From 240ff36e591273496a76ab68199446e84b9d7312 Mon Sep 17 00:00:00 2001 From: keyobs Date: Mon, 10 Aug 2026 08:26:40 +0200 Subject: [PATCH 18/68] feat(geometry): build the /geometry page Recharts scatter for sum x amplitude and sum x standard deviation, a decade-signature histogram, a gap-map arrow chain for the most recent draw, and its 10 nearest historical neighbors by GeometryDistance. The "proximity graph" bullet from the spec is implemented as this neighbor list rather than a literal network graph - same underlying distance data, simpler for V1, flagged as a simplification. Visually verified: scatter clusters realistically around sum~130/amplitude~35, no console errors. --- src/pages/geometry/GeometryPage.module.scss | 11 ++ src/pages/geometry/GeometryPage.tsx | 141 +++++++++++++++++++- 2 files changed, 151 insertions(+), 1 deletion(-) create mode 100644 src/pages/geometry/GeometryPage.module.scss diff --git a/src/pages/geometry/GeometryPage.module.scss b/src/pages/geometry/GeometryPage.module.scss new file mode 100644 index 0000000..9413a98 --- /dev/null +++ b/src/pages/geometry/GeometryPage.module.scss @@ -0,0 +1,11 @@ +.page { + display: flex; + flex-direction: column; + gap: 2rem; + text-align: left; +} + +.gapChain { + font-family: monospace; + font-size: 1.1rem; +} diff --git a/src/pages/geometry/GeometryPage.tsx b/src/pages/geometry/GeometryPage.tsx index 1fbca2e..27c8d52 100644 --- a/src/pages/geometry/GeometryPage.tsx +++ b/src/pages/geometry/GeometryPage.tsx @@ -1,5 +1,144 @@ +import { useMemo } from 'react'; +import { useQuery } from '@tanstack/react-query'; +import { + Bar, + BarChart, + CartesianGrid, + ResponsiveContainer, + Scatter, + ScatterChart, + Tooltip, + XAxis, + YAxis, +} from 'recharts'; +import { drawRepository } from '../../application/drawRepository.ts'; +import type { Draw } from '../../domain/draw/Draw.ts'; +import { extractFeatures } from '../../domain/features/FeatureExtractor.ts'; +import { buildGeometryDescriptor } from '../../domain/geometry/GeometryDescriptor.ts'; +import { findNearestNeighbors } from '../../domain/geometry/nearestNeighbors.ts'; +import styles from './GeometryPage.module.scss'; + +const DECADE_LABELS = ['1-10', '11-20', '21-30', '31-40', '41-50']; +const NEIGHBOR_COUNT = 10; +const EMPTY_DRAWS: Draw[] = []; + +const buildGapChain = (numbers: readonly number[]): string => + numbers + .map((n, index) => (index === 0 ? String(n) : `—${n - numbers[index - 1]}→ ${n}`)) + .join(' '); + const GeometryPage = () => { - return

Géométrie

; + const drawsQuery = useQuery({ + queryKey: ['draws', 'all'], + queryFn: () => drawRepository.getAll(), + }); + const draws = drawsQuery.data ?? EMPTY_DRAWS; + + const entries = useMemo( + () => + [...draws] + .sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()) + .map((draw) => ({ + draw, + descriptor: buildGeometryDescriptor({ numbers: draw.numbers, stars: draw.stars }), + stdDev: extractFeatures({ numbers: draw.numbers, stars: draw.stars }).values.stdDev, + })), + [draws], + ); + + const sumAmplitudeData = entries.map((entry) => ({ sum: entry.descriptor.sum, range: entry.descriptor.range })); + const sumStdDevData = entries.map((entry) => ({ sum: entry.descriptor.sum, stdDev: entry.stdDev })); + + const decadeHistogram = DECADE_LABELS.map((label, index) => ({ + decade: label, + count: entries.reduce((acc, entry) => acc + entry.descriptor.decadeBuckets[index], 0), + })); + + const referenceEntry = entries[0]; + const neighbors = referenceEntry + ? findNearestNeighbors( + referenceEntry.descriptor, + entries.slice(1).map((entry) => ({ item: entry.draw, descriptor: entry.descriptor })), + NEIGHBOR_COUNT, + ) + : []; + + return ( +
+

Géométrie

+ + {drawsQuery.isLoading &&

Chargement...

} + + {entries.length > 0 && ( + <> +
+

Somme × Amplitude

+ + + + + + + + + +
+ +
+

Somme × Écart-type

+ + + + + + + + + +
+ +
+

Histogramme des signatures de dizaines

+ + + + + + + + + +
+ + {referenceEntry && ( + <> +
+

Carte des écarts

+

Tirage de référence : {referenceEntry.draw.date}

+

{buildGapChain(referenceEntry.draw.numbers)}

+
+ +
+

Voisins historiques (proximité géométrique)

+

+ Les {NEIGHBOR_COUNT} tirages les plus proches du tirage de référence, par distance + géométrique - pas un graphe de proximité complet, mais la même donnée sous-jacente. +

+
    + {neighbors.map((neighbor) => ( +
  • + {neighbor.item.date} — {neighbor.item.numbers.join(' · ')} (distance{' '} + {neighbor.distance.toFixed(3)}) +
  • + ))} +
+
+ + )} + + )} +
+ ); }; export default GeometryPage; From fba4079dfeac581d9b9f0320f5dffc2df4216f9e Mon Sep 17 00:00:00 2001 From: keyobs Date: Mon, 10 Aug 2026 08:29:01 +0200 Subject: [PATCH 19/68] test(pages): add render-without-crash and interaction tests for the three V1 pages renderWithProviders + a shared real-CSV fetch fixture back EvaluationPage/DrawsPage/GeometryPage tests. Also fixes a real gap: vitest globals were off, so @testing-library/react's auto-cleanup never registered and DOM from one test leaked into the next within the same file - added an explicit cleanup() in testSetup's afterEach. --- package.json | 1 + pnpm-lock.yaml | 13 +++++++ src/pages/draws/DrawsPage.test.tsx | 20 ++++++++++ src/pages/evaluation/EvaluationPage.test.tsx | 40 ++++++++++++++++++++ src/pages/geometry/GeometryPage.test.tsx | 21 ++++++++++ src/shared/renderWithProviders.tsx | 8 ++++ src/shared/testCsvFixture.ts | 7 ++++ src/shared/testSetup.ts | 6 +++ 8 files changed, 116 insertions(+) create mode 100644 src/pages/draws/DrawsPage.test.tsx create mode 100644 src/pages/evaluation/EvaluationPage.test.tsx create mode 100644 src/pages/geometry/GeometryPage.test.tsx create mode 100644 src/shared/renderWithProviders.tsx create mode 100644 src/shared/testCsvFixture.ts diff --git a/package.json b/package.json index 7fb7c4a..d3606f5 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "@keyobs/dx-flow": "3.1.3", "@testing-library/jest-dom": "^7.0.0", "@testing-library/react": "^16.3.2", + "@testing-library/user-event": "^14.6.3", "@types/d3": "^7.4.3", "@types/node": "^24.13.3", "@types/react": "^19.2.17", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 37f47d1..27351e0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -57,6 +57,9 @@ importers: '@testing-library/react': specifier: ^16.3.2 version: 16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.4(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@testing-library/user-event': + specifier: ^14.6.3 + version: 14.6.3(@testing-library/dom@10.4.1) '@types/d3': specifier: ^7.4.3 version: 7.4.3 @@ -675,6 +678,12 @@ packages: '@types/react-dom': optional: true + '@testing-library/user-event@14.6.3': + resolution: {integrity: sha512-6dBq67jT8lE+JTE8Exm02Kt6ze43hz1jdiSpSJwtTZiT1xQQ6b7nZYTTQ9njdArdU8XklOwaDp/AbT/eYSKF4g==} + engines: {node: '>=12', npm: '>=6'} + peerDependencies: + '@testing-library/dom': '>=7.21.4' + '@tweenjs/tween.js@23.1.3': resolution: {integrity: sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==} @@ -2461,6 +2470,10 @@ snapshots: '@types/react': 19.2.18 '@types/react-dom': 19.2.4(@types/react@19.2.18) + '@testing-library/user-event@14.6.3(@testing-library/dom@10.4.1)': + dependencies: + '@testing-library/dom': 10.4.1 + '@tweenjs/tween.js@23.1.3': {} '@types/aria-query@5.0.4': {} diff --git a/src/pages/draws/DrawsPage.test.tsx b/src/pages/draws/DrawsPage.test.tsx new file mode 100644 index 0000000..68dea7a --- /dev/null +++ b/src/pages/draws/DrawsPage.test.tsx @@ -0,0 +1,20 @@ +import { afterEach, describe, expect, it, vi } from 'vitest'; +import { screen, waitFor } from '@testing-library/react'; +import { renderWithProviders } from '../../shared/renderWithProviders.tsx'; +import { REAL_CSV_TEXT } from '../../shared/testCsvFixture.ts'; +import DrawsPage from './DrawsPage.tsx'; + +describe('DrawsPage', () => { + afterEach(() => { + vi.unstubAllGlobals(); + }); + + it('renders without crashing and lists the latest draws once loaded', async () => { + vi.stubGlobal('fetch', () => Promise.resolve(new Response(REAL_CSV_TEXT))); + + renderWithProviders(); + + expect(screen.getByRole('heading', { name: 'Tirages' })).toBeInTheDocument(); + await waitFor(() => expect(screen.getAllByTestId('draw-row')).toHaveLength(50)); + }); +}); diff --git a/src/pages/evaluation/EvaluationPage.test.tsx b/src/pages/evaluation/EvaluationPage.test.tsx new file mode 100644 index 0000000..943a709 --- /dev/null +++ b/src/pages/evaluation/EvaluationPage.test.tsx @@ -0,0 +1,40 @@ +import { afterEach, describe, expect, it, vi } from 'vitest'; +import { screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { renderWithProviders } from '../../shared/renderWithProviders.tsx'; +import { REAL_CSV_TEXT } from '../../shared/testCsvFixture.ts'; +import EvaluationPage from './EvaluationPage.tsx'; + +describe('EvaluationPage', () => { + afterEach(() => { + vi.unstubAllGlobals(); + }); + + it('renders without crashing', () => { + vi.stubGlobal('fetch', () => Promise.resolve(new Response(REAL_CSV_TEXT))); + + renderWithProviders(); + + expect(screen.getByRole('heading', { name: "Évaluation d'une grille" })).toBeInTheDocument(); + }); + + it('evaluates a filled-in grid into four scores and three variations', async () => { + vi.stubGlobal('fetch', () => Promise.resolve(new Response(REAL_CSV_TEXT))); + const user = userEvent.setup(); + + renderWithProviders(); + + const numbers = [3, 7, 19, 31, 42]; + const stars = [2, 9]; + for (const [index, value] of numbers.entries()) { + await user.type(screen.getByTestId(`number-input-${index}`), String(value)); + } + for (const [index, value] of stars.entries()) { + await user.type(screen.getByTestId(`star-input-${index}`), String(value)); + } + await user.click(screen.getByTestId('evaluate-button')); + + await waitFor(() => expect(screen.getAllByTestId('score-card')).toHaveLength(4)); + expect(screen.getByTestId('variations').querySelectorAll('li')).toHaveLength(3); + }); +}); diff --git a/src/pages/geometry/GeometryPage.test.tsx b/src/pages/geometry/GeometryPage.test.tsx new file mode 100644 index 0000000..8b38f78 --- /dev/null +++ b/src/pages/geometry/GeometryPage.test.tsx @@ -0,0 +1,21 @@ +import { afterEach, describe, expect, it, vi } from 'vitest'; +import { screen, waitFor } from '@testing-library/react'; +import { renderWithProviders } from '../../shared/renderWithProviders.tsx'; +import { REAL_CSV_TEXT } from '../../shared/testCsvFixture.ts'; +import GeometryPage from './GeometryPage.tsx'; + +describe('GeometryPage', () => { + afterEach(() => { + vi.unstubAllGlobals(); + }); + + it('renders without crashing and shows the gap map and nearest neighbors once loaded', async () => { + vi.stubGlobal('fetch', () => Promise.resolve(new Response(REAL_CSV_TEXT))); + + renderWithProviders(); + + expect(screen.getByRole('heading', { name: 'Géométrie', level: 1 })).toBeInTheDocument(); + await waitFor(() => expect(screen.getAllByTestId('neighbor-row')).toHaveLength(10)); + expect(screen.getByTestId('gap-map')).toBeInTheDocument(); + }); +}); diff --git a/src/shared/renderWithProviders.tsx b/src/shared/renderWithProviders.tsx new file mode 100644 index 0000000..d801905 --- /dev/null +++ b/src/shared/renderWithProviders.tsx @@ -0,0 +1,8 @@ +import type { ReactElement } from 'react'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { render } from '@testing-library/react'; + +export const renderWithProviders = (element: ReactElement) => { + const queryClient = new QueryClient(); + return render({element}); +}; diff --git a/src/shared/testCsvFixture.ts b/src/shared/testCsvFixture.ts new file mode 100644 index 0000000..1c273b5 --- /dev/null +++ b/src/shared/testCsvFixture.ts @@ -0,0 +1,7 @@ +import { readFileSync } from 'node:fs'; +import { resolve } from 'node:path'; + +export const REAL_CSV_TEXT = readFileSync( + resolve(process.cwd(), 'public/results/euromillions_202002.csv'), + 'utf8', +); diff --git a/src/shared/testSetup.ts b/src/shared/testSetup.ts index bb02c60..97650fd 100644 --- a/src/shared/testSetup.ts +++ b/src/shared/testSetup.ts @@ -1 +1,7 @@ +import { afterEach } from 'vitest'; +import { cleanup } from '@testing-library/react'; import '@testing-library/jest-dom/vitest'; + +afterEach(() => { + cleanup(); +}); From f90cf5b9c18e40f1b98e63e550fb6f6e27540cea Mon Sep 17 00:00:00 2001 From: keyobs Date: Mon, 10 Aug 2026 08:37:21 +0200 Subject: [PATCH 20/68] fix(build): make the "check" script actually type-check the project tsc --noEmit against the root tsconfig.json checked zero files: that config only has `references`, which tsc ignores outside --build mode. Every "typecheck" run this session silently passed without checking anything. Switched check to `tsc -b` (project-references build mode, which the sub-configs already mark noEmit so nothing gets emitted). Running it for real surfaced genuine errors: test files under src/ use node:fs/node:path/process, which tsconfig.app.json's `types` didn't include - added "node" alongside "vite/client". Full project now type-checks clean under the command that actually runs the checker. --- package.json | 4 +++- tsconfig.app.json | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index d3606f5..4604027 100644 --- a/package.json +++ b/package.json @@ -10,9 +10,10 @@ "preview": "vite preview", "prepare": "husky", "lint:fix": "oxlint --fix", - "check": "tsc --noEmit", + "check": "tsc -b", "test": "vitest", "test:run": "vitest run", + "test:e2e": "playwright test", "release:patch": "dx-flow release patch", "release:minor": "dx-flow release minor", "release:major": "dx-flow release major", @@ -37,6 +38,7 @@ "@commitlint/cli": "^21.2.1", "@commitlint/config-conventional": "^21.2.0", "@keyobs/dx-flow": "3.1.3", + "@playwright/test": "^1.62.1", "@testing-library/jest-dom": "^7.0.0", "@testing-library/react": "^16.3.2", "@testing-library/user-event": "^14.6.3", diff --git a/tsconfig.app.json b/tsconfig.app.json index 6830b6f..4ce531a 100644 --- a/tsconfig.app.json +++ b/tsconfig.app.json @@ -4,7 +4,7 @@ "target": "es2023", "lib": ["ES2023", "DOM"], "module": "esnext", - "types": ["vite/client"], + "types": ["vite/client", "node"], "allowArbitraryExtensions": true, "skipLibCheck": true, From a4ed8d72d7dc5b72ef10596e4fa5699b70da354f Mon Sep 17 00:00:00 2001 From: keyobs Date: Mon, 10 Aug 2026 08:44:08 +0200 Subject: [PATCH 21/68] fix(tooling): fix the same broken tsc invocation in the pre-commit hook The dx-flow pre-commit template ran tsc --noEmit against the root tsconfig.json, which also checks zero files (see previous commit) - every pre-commit "Type checking..." pass this session was a silent no-op. Switched to tsc -b to match the now-fixed check script. --- .husky/pre-commit | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.husky/pre-commit b/.husky/pre-commit index 2041060..666f8b9 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -38,7 +38,7 @@ fi staged_ts="$(printf "%s\n" "$staged_files" | grep -E '\.(ts|tsx)$' || true)" if [ -n "$staged_ts" ]; then echo "🎓 Type checking..." - run_exec tsc --noEmit --pretty false + run_exec tsc -b --pretty false status=$? if [ "$status" -ne 0 ]; then exit "$status" From 5b218a9efa0fa306115d51d0493ff23d29611be8 Mon Sep 17 00:00:00 2001 From: keyobs Date: Mon, 10 Aug 2026 08:44:27 +0200 Subject: [PATCH 22/68] test(e2e): add Playwright E2E spec for input -> evaluation -> geometry MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit @playwright/test drives the system Chrome (channel: 'chrome', no bundled browser download) against a webServer-managed dev instance. The spec fills the evaluation form, checks the 4 score cards + 3 variations + disclaimer render, navigates to /geometry via the nav, and checks the gap map + nearest neighbors render there too - the V1 acceptance criterion "saisie -> évaluation -> géométrie en E2E". Passes end to end; webServer shuts down cleanly after the run. --- .gitignore | 7 ++++++- e2e/evaluation-to-geometry.spec.ts | 27 +++++++++++++++++++++++++++ playwright.config.ts | 17 +++++++++++++++++ pnpm-lock.yaml | 12 ++++++++++++ 4 files changed, 62 insertions(+), 1 deletion(-) create mode 100644 e2e/evaluation-to-geometry.spec.ts create mode 100644 playwright.config.ts diff --git a/.gitignore b/.gitignore index 53af622..822abca 100644 --- a/.gitignore +++ b/.gitignore @@ -31,4 +31,9 @@ devdx .claude # Husky internal hook runner (regenerated by the "prepare" script) -.husky/_ \ No newline at end of file +.husky/_ + +# Playwright +playwright-report +test-results +blob-report \ No newline at end of file diff --git a/e2e/evaluation-to-geometry.spec.ts b/e2e/evaluation-to-geometry.spec.ts new file mode 100644 index 0000000..d428a2e --- /dev/null +++ b/e2e/evaluation-to-geometry.spec.ts @@ -0,0 +1,27 @@ +import { expect, test } from '@playwright/test'; + +test('saisie -> évaluation -> géométrie', async ({ page }) => { + await page.goto('/evaluation'); + + const numbers = [3, 7, 19, 31, 42]; + const stars = [2, 9]; + for (const [index, value] of numbers.entries()) { + await page.getByTestId(`number-input-${index}`).fill(String(value)); + } + for (const [index, value] of stars.entries()) { + await page.getByTestId(`star-input-${index}`).fill(String(value)); + } + await page.getByTestId('evaluate-button').click(); + + await expect(page.getByTestId('evaluation-results')).toBeVisible(); + await expect(page.getByTestId('score-card')).toHaveCount(4); + await expect(page.getByTestId('variations').locator('li')).toHaveCount(3); + await expect(page.getByTestId('non-predictive-disclaimer')).toBeVisible(); + + await page.getByRole('link', { name: 'Géométrie' }).click(); + + await expect(page).toHaveURL(/\/geometry$/); + await expect(page.getByTestId('gap-map')).toBeVisible(); + await expect(page.getByTestId('neighbor-row').first()).toBeVisible(); + await expect(page.getByTestId('non-predictive-disclaimer')).toBeVisible(); +}); diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 0000000..73f4574 --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,17 @@ +import { defineConfig } from '@playwright/test'; + +const PORT = 5183; + +export default defineConfig({ + testDir: './e2e', + use: { + baseURL: `http://localhost:${PORT}`, + channel: 'chrome', + }, + webServer: { + command: `pnpm exec vite --port ${PORT}`, + url: `http://localhost:${PORT}`, + reuseExistingServer: true, + timeout: 30_000, + }, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 27351e0..05ad0c9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -51,6 +51,9 @@ importers: '@keyobs/dx-flow': specifier: 3.1.3 version: 3.1.3 + '@playwright/test': + specifier: ^1.62.1 + version: 1.62.1 '@testing-library/jest-dom': specifier: ^7.0.0 version: 7.0.0(@testing-library/dom@10.4.1) @@ -491,6 +494,11 @@ packages: resolution: {integrity: sha512-7FNeNl8NCE7aINx7WXiKQrPYZWC/hvrTsmk6zmxbI7LTXE7hVek/n8AfVgpe2y82zl3w0HvCHN0bVKMBoJcC0w==} engines: {node: '>= 10.0.0'} + '@playwright/test@1.62.1': + resolution: {integrity: sha512-DTcUc8qii+cpHvtOwggMtBRMjKZHXYWdw8syRYu2vtzuq4Wxphqq4NfCs5Zt44L6mA8rfDfj+PHnxFc/FeK6mQ==} + engines: {node: '>=20'} + hasBin: true + '@react-three/drei@10.7.8': resolution: {integrity: sha512-rJXyuzLm2Xq0kafHuR47ajDGbOe/pEhzIr4m8E8zwzQs0iNjloFDqBwRhrXmP/w+onLeYyN3EYPFW/cwWK/4yA==} peerDependencies: @@ -2313,6 +2321,10 @@ snapshots: '@parcel/watcher-win32-x64': 2.6.0 optional: true + '@playwright/test@1.62.1': + dependencies: + playwright: 1.62.1 + '@react-three/drei@10.7.8(@react-three/fiber@9.7.0(@types/react@19.2.18)(immer@11.1.16)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(three@0.185.1))(@types/react@19.2.18)(@types/three@0.185.4)(immer@11.1.16)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(three@0.185.1)': dependencies: '@babel/runtime': 7.29.7 From 0e2bd40d60fd4ba5b3811fa1a4f60c5dece856ff Mon Sep 17 00:00:00 2001 From: keyobs Date: Mon, 10 Aug 2026 09:40:32 +0200 Subject: [PATCH 23/68] feat(domain): add seeded PRNG for reproducible strategies and Monte Carlo mulberry32 - deterministic, same seed always gives the same sequence. Backs Strategy proposal tie-breaking and the Monte Carlo baseline in the upcoming walk-forward backtester (V2 reproducibility requirement). --- src/domain/random/seededRandom.test.ts | 34 ++++++++++++++++++++++++++ src/domain/random/seededRandom.ts | 14 +++++++++++ 2 files changed, 48 insertions(+) create mode 100644 src/domain/random/seededRandom.test.ts create mode 100644 src/domain/random/seededRandom.ts diff --git a/src/domain/random/seededRandom.test.ts b/src/domain/random/seededRandom.test.ts new file mode 100644 index 0000000..3bd7655 --- /dev/null +++ b/src/domain/random/seededRandom.test.ts @@ -0,0 +1,34 @@ +import { describe, expect, it } from 'vitest'; +import { createSeededRandom } from './seededRandom.ts'; + +describe('createSeededRandom', () => { + it('produces the same sequence for the same seed', () => { + const a = createSeededRandom(42); + const b = createSeededRandom(42); + + const sequenceA = Array.from({ length: 10 }, () => a()); + const sequenceB = Array.from({ length: 10 }, () => b()); + + expect(sequenceA).toEqual(sequenceB); + }); + + it('produces a different sequence for a different seed', () => { + const a = createSeededRandom(1); + const b = createSeededRandom(2); + + const sequenceA = Array.from({ length: 10 }, () => a()); + const sequenceB = Array.from({ length: 10 }, () => b()); + + expect(sequenceA).not.toEqual(sequenceB); + }); + + it('produces values in [0, 1)', () => { + const random = createSeededRandom(7); + + for (let i = 0; i < 1000; i += 1) { + const value = random(); + expect(value).toBeGreaterThanOrEqual(0); + expect(value).toBeLessThan(1); + } + }); +}); diff --git a/src/domain/random/seededRandom.ts b/src/domain/random/seededRandom.ts new file mode 100644 index 0000000..5fdfeeb --- /dev/null +++ b/src/domain/random/seededRandom.ts @@ -0,0 +1,14 @@ +/** + * mulberry32 - small, fast, deterministic PRNG. Same seed always produces + * the same sequence, which is what reproducible experiments (V2) need. + */ +export const createSeededRandom = (seed: number): (() => number) => { + let state = seed >>> 0; + + return () => { + state = (state + 0x6d2b79f5) | 0; + let t = Math.imul(state ^ (state >>> 15), 1 | state); + t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; + return ((t ^ (t >>> 14)) >>> 0) / 4294967296; + }; +}; From 8f14882f7851143e7c2bdbd0bb3e5adb28cc35fa Mon Sep 17 00:00:00 2001 From: keyobs Date: Mon, 10 Aug 2026 09:44:16 +0200 Subject: [PATCH 24/68] feat(domain): add Strategy model and proposeGrid MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 8 rule kinds cover every V2-spec category except "zone géométrique" (needs a real DiscoveryModel embedding, V3+): number-frequency (fréquences/rareté via signed weight), above-31, repeat-from-previous, recency, star-frequency are additive [0,1]-scored preferences; decade-spread, sum-range, parity-target are bounded greedy adjustments applied after the initial top-5/top-2 selection. Seeded via createSeededRandom for reproducibility. Always returns a valid Grid. --- src/domain/strategy/Strategy.ts | 42 +++++ src/domain/strategy/proposeGrid.test.ts | 97 ++++++++++++ src/domain/strategy/proposeGrid.ts | 196 ++++++++++++++++++++++++ 3 files changed, 335 insertions(+) create mode 100644 src/domain/strategy/Strategy.ts create mode 100644 src/domain/strategy/proposeGrid.test.ts create mode 100644 src/domain/strategy/proposeGrid.ts diff --git a/src/domain/strategy/Strategy.ts b/src/domain/strategy/Strategy.ts new file mode 100644 index 0000000..a333f68 --- /dev/null +++ b/src/domain/strategy/Strategy.ts @@ -0,0 +1,42 @@ +/** + * Rule kinds cover every V2-spec category except "zone géométrique" - + * that one targets a SpatialEmbedding region, which doesn't exist as a + * real clustered thing before V3's DiscoveryModel. + * + * Scoring kinds (number-frequency, above-31, repeat-from-previous, + * recency, star-frequency) contribute a weighted [0,1] preference per + * number/star; a negative weight flips the preference (e.g. + * number-frequency with weight -1 prefers rare numbers instead of + * frequent ones - fréquences and rareté are the same rule, signed). + * Constraint kinds (decade-spread, sum-range, parity-target) target a + * property of the whole 5-number set and are applied as a bounded + * greedy adjustment after the initial scored selection. + */ +export type StrategyRuleKind = + | 'number-frequency' + | 'above-31' + | 'repeat-from-previous' + | 'recency' + | 'decade-spread' + | 'sum-range' + | 'parity-target' + | 'star-frequency'; + +export interface StrategyRuleParams { + min?: number; + max?: number; + oddCount?: number; +} + +export interface StrategyRule { + kind: StrategyRuleKind; + weight?: number; + params?: StrategyRuleParams; +} + +export interface Strategy { + id: string; + name: string; + rules: StrategyRule[]; + seed: number; +} diff --git a/src/domain/strategy/proposeGrid.test.ts b/src/domain/strategy/proposeGrid.test.ts new file mode 100644 index 0000000..b4ac9a9 --- /dev/null +++ b/src/domain/strategy/proposeGrid.test.ts @@ -0,0 +1,97 @@ +import { readFileSync } from 'node:fs'; +import { resolve } from 'node:path'; +import { describe, expect, it } from 'vitest'; +import { parseFdjCsv } from '../../infrastructure/csv/parseFdjCsv.ts'; +import { proposeGrid } from './proposeGrid.ts'; +import type { Strategy } from './Strategy.ts'; + +const REAL_CSV_PATH = resolve(process.cwd(), 'public/results/euromillions_202002.csv'); +const history = parseFdjCsv(readFileSync(REAL_CSV_PATH, 'utf8')); + +const strategyOf = (rules: Strategy['rules'], seed = 42): Strategy => ({ + id: 'test-strategy', + name: 'Test', + rules, + seed, +}); + +describe('proposeGrid', () => { + it('is deterministic for the same seed, strategy, and history', () => { + const strategy = strategyOf([{ kind: 'number-frequency' }]); + + expect(proposeGrid(strategy, history)).toEqual(proposeGrid(strategy, history)); + }); + + it('produces a different grid for a different seed (almost always)', () => { + const withoutRules = strategyOf([]); + + const gridA = proposeGrid({ ...withoutRules, seed: 1 }, history); + const gridB = proposeGrid({ ...withoutRules, seed: 2 }, history); + + expect(gridA).not.toEqual(gridB); + }); + + it('always produces a valid Grid regardless of rule combination', () => { + const strategy = strategyOf([ + { kind: 'number-frequency', weight: 1 }, + { kind: 'above-31', weight: 0.5 }, + { kind: 'decade-spread' }, + { kind: 'sum-range', params: { min: 100, max: 150 } }, + { kind: 'parity-target', params: { oddCount: 3 } }, + { kind: 'star-frequency' }, + ]); + + const grid = proposeGrid(strategy, history); + + expect(new Set(grid.numbers).size).toBe(5); + expect(new Set(grid.stars).size).toBe(2); + }); + + it('above-31 rule with strong weight favors numbers greater than 31', () => { + const strategy = strategyOf([{ kind: 'above-31', weight: 10 }]); + + const grid = proposeGrid(strategy, history); + + expect(grid.numbers.every((n) => n > 31)).toBe(true); + }); + + it('a negative weight flips the frequency preference toward rare numbers', () => { + const frequent = strategyOf([{ kind: 'number-frequency', weight: 1 }]); + const rare = strategyOf([{ kind: 'number-frequency', weight: -1 }]); + + expect(proposeGrid(frequent, history)).not.toEqual(proposeGrid(rare, history)); + }); + + it('decade-spread produces one number per decade when feasible', () => { + const strategy = strategyOf([{ kind: 'number-frequency' }, { kind: 'decade-spread' }]); + + const grid = proposeGrid(strategy, history); + const buckets = new Set(grid.numbers.map((n) => Math.min(Math.floor((n - 1) / 10), 4))); + + expect(buckets.size).toBe(5); + }); + + it('sum-range constrains the total sum into the requested range', () => { + const strategy = strategyOf([{ kind: 'sum-range', params: { min: 100, max: 120 } }]); + + const grid = proposeGrid(strategy, history); + const sum = grid.numbers.reduce((acc, n) => acc + n, 0); + + expect(sum).toBeGreaterThanOrEqual(100); + expect(sum).toBeLessThanOrEqual(120); + }); + + it('parity-target hits the exact requested odd count when feasible', () => { + const strategy = strategyOf([{ kind: 'parity-target', params: { oddCount: 5 } }]); + + const grid = proposeGrid(strategy, history); + + expect(grid.numbers.every((n) => n % 2 !== 0)).toBe(true); + }); + + it('handles an empty history without throwing', () => { + const strategy = strategyOf([{ kind: 'number-frequency' }, { kind: 'recency' }]); + + expect(() => proposeGrid(strategy, [])).not.toThrow(); + }); +}); diff --git a/src/domain/strategy/proposeGrid.ts b/src/domain/strategy/proposeGrid.ts new file mode 100644 index 0000000..a27e167 --- /dev/null +++ b/src/domain/strategy/proposeGrid.ts @@ -0,0 +1,196 @@ +import type { Draw } from '../draw/Draw.ts'; +import type { Grid } from '../grid/Grid.ts'; +import { parseGrid } from '../grid/Grid.ts'; +import { createSeededRandom } from '../random/seededRandom.ts'; +import type { Strategy, StrategyRule, StrategyRuleParams } from './Strategy.ts'; + +const range = (min: number, max: number): number[] => + Array.from({ length: max - min + 1 }, (_, i) => min + i); + +const decadeBucketOf = (n: number): number => Math.min(Math.floor((n - 1) / 10), 4); + +const countByValue = (values: number[], min: number, max: number): Record => { + const counts: Record = {}; + for (let v = min; v <= max; v += 1) counts[v] = 0; + for (const v of values) counts[v] += 1; + return counts; +}; + +const normalizeToUnit = (values: Record): Record => { + const max = Math.max(1, ...Object.values(values)); + const normalized: Record = {}; + for (const [key, value] of Object.entries(values)) normalized[Number(key)] = value / max; + return normalized; +}; + +const sortedByDateDescending = (draws: Draw[]): Draw[] => + [...draws].sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()); + +const gapsSinceLastSeen = ( + sortedDescending: Draw[], + min: number, + max: number, + pick: (draw: Draw) => readonly number[], +): Record => { + const gaps: Record = {}; + for (let n = min; n <= max; n += 1) gaps[n] = Number.POSITIVE_INFINITY; + + for (const [index, draw] of sortedDescending.entries()) { + for (const n of pick(draw)) { + if (!Number.isFinite(gaps[n])) gaps[n] = index; + } + } + + return gaps; +}; + +const scoreNumbers = (rules: StrategyRule[], history: Draw[]): Record => { + const scores: Record = {}; + for (let n = 1; n <= 50; n += 1) scores[n] = 0; + + const sortedDescending = sortedByDateDescending(history); + const mostRecentDraw = sortedDescending[0]; + const normalizedFrequency = normalizeToUnit(countByValue(history.flatMap((draw) => draw.numbers), 1, 50)); + const gaps = gapsSinceLastSeen(sortedDescending, 1, 50, (draw) => draw.numbers); + + for (const rule of rules) { + const weight = rule.weight ?? 1; + if (rule.kind === 'number-frequency') { + for (let n = 1; n <= 50; n += 1) scores[n] += weight * normalizedFrequency[n]; + } else if (rule.kind === 'above-31') { + for (let n = 1; n <= 50; n += 1) scores[n] += weight * (n > 31 ? 1 : 0); + } else if (rule.kind === 'repeat-from-previous') { + for (let n = 1; n <= 50; n += 1) { + scores[n] += weight * (mostRecentDraw?.numbers.includes(n) ? 1 : 0); + } + } else if (rule.kind === 'recency') { + for (let n = 1; n <= 50; n += 1) scores[n] += weight * (1 / (1 + gaps[n])); + } + } + + return scores; +}; + +const scoreStars = (rules: StrategyRule[], history: Draw[]): Record => { + const scores: Record = {}; + for (let s = 1; s <= 12; s += 1) scores[s] = 0; + + const normalizedFrequency = normalizeToUnit(countByValue(history.flatMap((draw) => draw.stars), 1, 12)); + + for (const rule of rules) { + if (rule.kind === 'star-frequency') { + const weight = rule.weight ?? 1; + for (let s = 1; s <= 12; s += 1) scores[s] += weight * normalizedFrequency[s]; + } + } + + return scores; +}; + +const topN = ( + scores: Record, + min: number, + max: number, + count: number, + random: () => number, +): number[] => + range(min, max) + .map((n) => ({ n, jitteredScore: scores[n] + random() * 1e-6 })) + .sort((a, b) => b.jitteredScore - a.jitteredScore) + .slice(0, count) + .map((entry) => entry.n) + .sort((a, b) => a - b); + +const applyDecadeSpread = (numbers: number[], scores: Record): number[] => { + let current = [...numbers]; + + for (let pass = 0; pass < 10; pass += 1) { + const buckets = [0, 0, 0, 0, 0]; + for (const n of current) buckets[decadeBucketOf(n)] += 1; + + const overIndex = buckets.findIndex((count) => count >= 2); + const underIndex = buckets.findIndex((count) => count === 0); + if (overIndex === -1 || underIndex === -1) break; + + const worstInOverDecade = current + .filter((n) => decadeBucketOf(n) === overIndex) + .sort((a, b) => scores[a] - scores[b])[0]; + + const underDecadeMin = underIndex * 10 + 1; + const underDecadeMax = Math.min(underIndex * 10 + 10, 50); + const bestReplacement = range(underDecadeMin, underDecadeMax) + .filter((n) => !current.includes(n)) + .sort((a, b) => scores[b] - scores[a])[0]; + + if (bestReplacement === undefined) break; + current = current.map((n) => (n === worstInOverDecade ? bestReplacement : n)); + } + + return current; +}; + +const applySumRange = (numbers: number[], scores: Record, params: StrategyRuleParams): number[] => { + const min = params.min ?? 0; + const max = params.max ?? 250; + let current = [...numbers]; + + for (let pass = 0; pass < 20; pass += 1) { + const sum = current.reduce((acc, n) => acc + n, 0); + if (sum >= min && sum <= max) break; + + const needsIncrease = sum < min; + const worst = [...current].sort((a, b) => scores[a] - scores[b])[0]; + const replacement = range(1, 50) + .filter((n) => !current.includes(n) && (needsIncrease ? n > worst : n < worst)) + .sort((a, b) => scores[b] - scores[a])[0]; + + if (replacement === undefined) break; + current = current.map((n) => (n === worst ? replacement : n)); + } + + return current; +}; + +const applyParityTarget = ( + numbers: number[], + scores: Record, + params: StrategyRuleParams, +): number[] => { + const targetOddCount = params.oddCount ?? 3; + let current = [...numbers]; + + for (let pass = 0; pass < 10; pass += 1) { + const currentOddCount = current.filter((n) => n % 2 !== 0).length; + if (currentOddCount === targetOddCount) break; + + const needsMoreOdd = currentOddCount < targetOddCount; + const worst = [...current].sort((a, b) => scores[a] - scores[b]).find((n) => (needsMoreOdd ? n % 2 === 0 : n % 2 !== 0)); + if (worst === undefined) break; + + const replacement = range(1, 50) + .filter((n) => !current.includes(n) && (needsMoreOdd ? n % 2 !== 0 : n % 2 === 0)) + .sort((a, b) => scores[b] - scores[a])[0]; + + if (replacement === undefined) break; + current = current.map((n) => (n === worst ? replacement : n)); + } + + return current; +}; + +export const proposeGrid = (strategy: Strategy, history: Draw[]): Grid => { + const random = createSeededRandom(strategy.seed); + const numberScores = scoreNumbers(strategy.rules, history); + const starScores = scoreStars(strategy.rules, history); + + let numbers = topN(numberScores, 1, 50, 5, random); + const stars = topN(starScores, 1, 12, 2, random); + + for (const rule of strategy.rules) { + if (rule.kind === 'decade-spread') numbers = applyDecadeSpread(numbers, numberScores); + if (rule.kind === 'sum-range') numbers = applySumRange(numbers, numberScores, rule.params ?? {}); + if (rule.kind === 'parity-target') numbers = applyParityTarget(numbers, numberScores, rule.params ?? {}); + } + + return parseGrid({ numbers, stars }); +}; From 0181586d447dcac7eb47a186ddb39081b1fe0ef4 Mon Sep 17 00:00:00 2001 From: keyobs Date: Mon, 10 Aug 2026 10:01:22 +0200 Subject: [PATCH 25/68] feat(domain): add EuroMillions prize-rank lookup countMatches(a, b) for numbers/stars intersection size, and prizeRank(matchedNumbers, matchedStars) mapping to the 13 official tiers (null when nothing is won). Backs the walk-forward backtester's per-step metrics. --- src/domain/backtest/prizeRank.test.ts | 36 +++++++++++++++++++++++++++ src/domain/backtest/prizeRank.ts | 28 +++++++++++++++++++++ 2 files changed, 64 insertions(+) create mode 100644 src/domain/backtest/prizeRank.test.ts create mode 100644 src/domain/backtest/prizeRank.ts diff --git a/src/domain/backtest/prizeRank.test.ts b/src/domain/backtest/prizeRank.test.ts new file mode 100644 index 0000000..97b93ad --- /dev/null +++ b/src/domain/backtest/prizeRank.test.ts @@ -0,0 +1,36 @@ +import { describe, expect, it } from 'vitest'; +import { countMatches, prizeRank } from './prizeRank.ts'; + +describe('countMatches', () => { + it('counts the intersection size', () => { + expect(countMatches([1, 2, 3, 4, 5], [3, 4, 5, 6, 7])).toBe(3); + expect(countMatches([1, 2], [3, 4])).toBe(0); + expect(countMatches([1, 2], [1, 2])).toBe(2); + }); +}); + +describe('prizeRank', () => { + it('returns rank 1 for a perfect match', () => { + expect(prizeRank(5, 2)).toBe(1); + }); + + it('returns each of the 13 official tiers exactly once', () => { + const ranks = new Set(); + for (let numbers = 0; numbers <= 5; numbers += 1) { + for (let stars = 0; stars <= 2; stars += 1) { + const rank = prizeRank(numbers, stars); + if (rank !== null) ranks.add(rank); + } + } + expect(ranks.size).toBe(13); + expect([...ranks].sort((a, b) => a - b)).toEqual(Array.from({ length: 13 }, (_, i) => i + 1)); + }); + + it('returns null for combinations that win no prize', () => { + expect(prizeRank(0, 0)).toBeNull(); + expect(prizeRank(1, 0)).toBeNull(); + expect(prizeRank(1, 1)).toBeNull(); + expect(prizeRank(0, 1)).toBeNull(); + expect(prizeRank(0, 2)).toBeNull(); + }); +}); diff --git a/src/domain/backtest/prizeRank.ts b/src/domain/backtest/prizeRank.ts new file mode 100644 index 0000000..64eb967 --- /dev/null +++ b/src/domain/backtest/prizeRank.ts @@ -0,0 +1,28 @@ +export const countMatches = (a: readonly number[], b: readonly number[]): number => { + const setB = new Set(b); + return a.filter((n) => setB.has(n)).length; +}; + +/** The 13 official EuroMillions prize tiers, by (matched numbers, matched stars). */ +const RANK_TABLE: { matchedNumbers: number; matchedStars: number; rank: number }[] = [ + { matchedNumbers: 5, matchedStars: 2, rank: 1 }, + { matchedNumbers: 5, matchedStars: 1, rank: 2 }, + { matchedNumbers: 5, matchedStars: 0, rank: 3 }, + { matchedNumbers: 4, matchedStars: 2, rank: 4 }, + { matchedNumbers: 4, matchedStars: 1, rank: 5 }, + { matchedNumbers: 3, matchedStars: 2, rank: 6 }, + { matchedNumbers: 4, matchedStars: 0, rank: 7 }, + { matchedNumbers: 2, matchedStars: 2, rank: 8 }, + { matchedNumbers: 3, matchedStars: 1, rank: 9 }, + { matchedNumbers: 3, matchedStars: 0, rank: 10 }, + { matchedNumbers: 1, matchedStars: 2, rank: 11 }, + { matchedNumbers: 2, matchedStars: 1, rank: 12 }, + { matchedNumbers: 2, matchedStars: 0, rank: 13 }, +]; + +export const prizeRank = (matchedNumbers: number, matchedStars: number): number | null => { + const entry = RANK_TABLE.find( + (candidate) => candidate.matchedNumbers === matchedNumbers && candidate.matchedStars === matchedStars, + ); + return entry ? entry.rank : null; +}; From bc63a1e3d26842e8b687daf231efc25b712b12c4 Mon Sep 17 00:00:00 2001 From: keyobs Date: Mon, 10 Aug 2026 10:06:03 +0200 Subject: [PATCH 26/68] feat(domain): add the walk-forward StrategyBacktester For each date T in the requested range, proposeGrid only ever sees sortedAscending.slice(0, index) - draws at or after T, including any beyond the range itself, can never influence T's proposal. Verified directly: running the same test range against the full 680-draw history vs a history truncated right after the range produces identical proposals, proving draws after the range have zero effect (the spec's "toute fuite future est une erreur bloquante"). Also reproducible by seed and reports a valid empty result for a range with no draws. --- .../backtest/runWalkForwardBacktest.test.ts | 63 +++++++++++++ src/domain/backtest/runWalkForwardBacktest.ts | 89 +++++++++++++++++++ 2 files changed, 152 insertions(+) create mode 100644 src/domain/backtest/runWalkForwardBacktest.test.ts create mode 100644 src/domain/backtest/runWalkForwardBacktest.ts diff --git a/src/domain/backtest/runWalkForwardBacktest.test.ts b/src/domain/backtest/runWalkForwardBacktest.test.ts new file mode 100644 index 0000000..bfffc3a --- /dev/null +++ b/src/domain/backtest/runWalkForwardBacktest.test.ts @@ -0,0 +1,63 @@ +import { readFileSync } from 'node:fs'; +import { resolve } from 'node:path'; +import { describe, expect, it } from 'vitest'; +import { parseFdjCsv } from '../../infrastructure/csv/parseFdjCsv.ts'; +import type { Strategy } from '../strategy/Strategy.ts'; +import { runWalkForwardBacktest } from './runWalkForwardBacktest.ts'; + +const REAL_CSV_PATH = resolve(process.cwd(), 'public/results/euromillions_202002.csv'); +const history = parseFdjCsv(readFileSync(REAL_CSV_PATH, 'utf8')); +const sortedAscending = [...history].sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime()); + +const strategyOf = (rules: Strategy['rules'], seed = 42): Strategy => ({ + id: 'test-strategy', + name: 'Test', + rules, + seed, +}); + +describe('runWalkForwardBacktest', () => { + it('produces one step per draw inside the date range', () => { + const strategy = strategyOf([{ kind: 'number-frequency' }]); + const range = { start: sortedAscending[100].date, end: sortedAscending[109].date }; + + const result = runWalkForwardBacktest(strategy, history, range); + + expect(result.steps).toHaveLength(10); + expect(result.metrics.stepCount).toBe(10); + }); + + it('never lets a proposal depend on draws that happen after it (no future leakage)', () => { + const strategy = strategyOf([{ kind: 'number-frequency' }, { kind: 'recency' }]); + const testRange = { start: sortedAscending[400].date, end: sortedAscending[499].date }; + const truncatedHistory = sortedAscending.slice(0, 500); + + const resultWithFullHistory = runWalkForwardBacktest(strategy, sortedAscending, testRange); + const resultWithTruncatedHistory = runWalkForwardBacktest(strategy, truncatedHistory, testRange); + + expect(resultWithFullHistory.steps.map((step) => step.proposedGrid)).toEqual( + resultWithTruncatedHistory.steps.map((step) => step.proposedGrid), + ); + }); + + it('is reproducible: the same seed gives the same steps', () => { + const strategy = strategyOf([{ kind: 'number-frequency' }], 123); + const range = { start: sortedAscending[200].date, end: sortedAscending[219].date }; + + const resultA = runWalkForwardBacktest(strategy, history, range); + const resultB = runWalkForwardBacktest(strategy, history, range); + + expect(resultA).toEqual(resultB); + }); + + it('handles an empty date range without throwing, reporting a valid "no signal" result', () => { + const strategy = strategyOf([{ kind: 'number-frequency' }]); + const range = { start: '1900-01-01', end: '1900-01-02' }; + + const result = runWalkForwardBacktest(strategy, history, range); + + expect(result.steps).toHaveLength(0); + expect(result.metrics.meanMatchedNumbers).toBe(0); + expect(result.metrics.bestPrizeRank).toBeNull(); + }); +}); diff --git a/src/domain/backtest/runWalkForwardBacktest.ts b/src/domain/backtest/runWalkForwardBacktest.ts new file mode 100644 index 0000000..b204d49 --- /dev/null +++ b/src/domain/backtest/runWalkForwardBacktest.ts @@ -0,0 +1,89 @@ +import type { Draw } from '../draw/Draw.ts'; +import type { Grid } from '../grid/Grid.ts'; +import { proposeGrid } from '../strategy/proposeGrid.ts'; +import type { Strategy } from '../strategy/Strategy.ts'; +import { countMatches, prizeRank } from './prizeRank.ts'; + +export interface DateRange { + start: string; + end: string; +} + +export interface BacktestStepResult { + date: string; + actualDraw: Draw; + proposedGrid: Grid; + matchedNumbers: number; + matchedStars: number; + prizeRank: number | null; +} + +export interface BacktestMetrics { + stepCount: number; + meanMatchedNumbers: number; + meanMatchedStars: number; + bestPrizeRank: number | null; + prizeRankCounts: Record; +} + +export interface BacktestResult { + steps: BacktestStepResult[]; + metrics: BacktestMetrics; +} + +const isWithinRange = (date: string, range: DateRange): boolean => date >= range.start && date <= range.end; + +export const computeMetrics = (steps: BacktestStepResult[]): BacktestMetrics => { + const stepCount = steps.length; + const meanMatchedNumbers = + stepCount === 0 ? 0 : steps.reduce((acc, step) => acc + step.matchedNumbers, 0) / stepCount; + const meanMatchedStars = + stepCount === 0 ? 0 : steps.reduce((acc, step) => acc + step.matchedStars, 0) / stepCount; + + const prizeRankCounts: Record = {}; + let bestPrizeRank: number | null = null; + for (const step of steps) { + if (step.prizeRank !== null) { + prizeRankCounts[step.prizeRank] = (prizeRankCounts[step.prizeRank] ?? 0) + 1; + if (bestPrizeRank === null || step.prizeRank < bestPrizeRank) bestPrizeRank = step.prizeRank; + } + } + + return { stepCount, meanMatchedNumbers, meanMatchedStars, bestPrizeRank, prizeRankCounts }; +}; + +/** + * Walk-forward: for each date T in `dateRange`, the strategy only ever sees + * draws strictly before T (sortedAscending.slice(0, index)) - draws at or + * after T, including any beyond `dateRange` itself, can never influence the + * proposal for T. See proposeGrid.test.ts's leakage test for the guarantee. + */ +export const runWalkForwardBacktest = ( + strategy: Strategy, + draws: Draw[], + dateRange: DateRange, +): BacktestResult => { + const sortedAscending = [...draws].sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime()); + const steps: BacktestStepResult[] = []; + + for (let index = 0; index < sortedAscending.length; index += 1) { + const draw = sortedAscending[index]; + if (!isWithinRange(draw.date, dateRange)) continue; + + const historyBeforeDraw = sortedAscending.slice(0, index); + const proposedGrid = proposeGrid(strategy, historyBeforeDraw); + const matchedNumbers = countMatches(proposedGrid.numbers, draw.numbers); + const matchedStars = countMatches(proposedGrid.stars, draw.stars); + + steps.push({ + date: draw.date, + actualDraw: draw, + proposedGrid, + matchedNumbers, + matchedStars, + prizeRank: prizeRank(matchedNumbers, matchedStars), + }); + } + + return { steps, metrics: computeMetrics(steps) }; +}; From 5cf45b867b534749244927f53e87474b02c9059b Mon Sep 17 00:00:00 2001 From: keyobs Date: Mon, 10 Aug 2026 10:33:10 +0200 Subject: [PATCH 27/68] feat(domain): add baselines, Monte Carlo comparison, and a real perf fix 5 baselines (uniform-random, fixed-grid, frequent/rare-numbers, geometric-no-temporal) all reuse proposeGrid with a specific rule set - fixed-grid is the one exception, a constant Grid. Generalized runWalkForwardBacktest to take a plain (history) => Grid function instead of a Strategy, so baselines share the exact same walk-forward loop rather than a parallel implementation. runMonteCarloComparison runs N seeded uniform-random backtests and reports the tested strategy's percentile against that distribution - "no advantage detected" near the 50th percentile is an expected, valid outcome. Found a real perf bug while measuring: 500 Monte Carlo samples over a 200-step range took 24s, because scoreNumbers/scoreStars always did O(history) precomputation (frequency counts, recency gaps) even when no rule used it - the uniform-random baseline (rules: []) paid that cost on every single step for nothing. Guarded the precomputation behind which rule kinds are actually present: 24s -> 1.3s for the same run. (First attempt at the guard also skipped the cheap 'above-31' rule's own scoring loop by mistake - caught by proposeGrid.test.ts's above-31 test failing immediately, fixed by only guarding the precomputation, not the rule loop itself.) --- src/domain/backtest/baselines.test.ts | 39 ++++++++++ src/domain/backtest/baselines.ts | 73 +++++++++++++++++++ src/domain/backtest/compareBaselines.test.ts | 25 +++++++ src/domain/backtest/compareBaselines.ts | 20 +++++ .../backtest/runMonteCarloComparison.test.ts | 40 ++++++++++ .../backtest/runMonteCarloComparison.ts | 42 +++++++++++ .../backtest/runWalkForwardBacktest.test.ts | 17 +++-- src/domain/backtest/runWalkForwardBacktest.ts | 15 ++-- src/domain/strategy/proposeGrid.ts | 14 +++- 9 files changed, 270 insertions(+), 15 deletions(-) create mode 100644 src/domain/backtest/baselines.test.ts create mode 100644 src/domain/backtest/baselines.ts create mode 100644 src/domain/backtest/compareBaselines.test.ts create mode 100644 src/domain/backtest/compareBaselines.ts create mode 100644 src/domain/backtest/runMonteCarloComparison.test.ts create mode 100644 src/domain/backtest/runMonteCarloComparison.ts diff --git a/src/domain/backtest/baselines.test.ts b/src/domain/backtest/baselines.test.ts new file mode 100644 index 0000000..5056605 --- /dev/null +++ b/src/domain/backtest/baselines.test.ts @@ -0,0 +1,39 @@ +import { readFileSync } from 'node:fs'; +import { resolve } from 'node:path'; +import { describe, expect, it } from 'vitest'; +import { parseFdjCsv } from '../../infrastructure/csv/parseFdjCsv.ts'; +import { BASELINE_KINDS, proposeBaselineGrid } from './baselines.ts'; + +const REAL_CSV_PATH = resolve(process.cwd(), 'public/results/euromillions_202002.csv'); +const history = parseFdjCsv(readFileSync(REAL_CSV_PATH, 'utf8')); + +describe('proposeBaselineGrid', () => { + it('produces a valid Grid for every baseline kind', () => { + for (const kind of BASELINE_KINDS) { + const grid = proposeBaselineGrid(kind, history, 1); + expect(new Set(grid.numbers).size).toBe(5); + expect(new Set(grid.stars).size).toBe(2); + } + }); + + it('fixed-grid always returns the same grid regardless of history or seed', () => { + const a = proposeBaselineGrid('fixed-grid', history, 1); + const b = proposeBaselineGrid('fixed-grid', [], 999); + + expect(a).toEqual(b); + }); + + it('uniform-random ignores history entirely', () => { + const withHistory = proposeBaselineGrid('uniform-random', history, 7); + const withoutHistory = proposeBaselineGrid('uniform-random', [], 7); + + expect(withHistory).toEqual(withoutHistory); + }); + + it('frequent-numbers and rare-numbers disagree on a real history', () => { + const frequent = proposeBaselineGrid('frequent-numbers', history, 1); + const rare = proposeBaselineGrid('rare-numbers', history, 1); + + expect(frequent).not.toEqual(rare); + }); +}); diff --git a/src/domain/backtest/baselines.ts b/src/domain/backtest/baselines.ts new file mode 100644 index 0000000..7821dcc --- /dev/null +++ b/src/domain/backtest/baselines.ts @@ -0,0 +1,73 @@ +import type { Draw } from '../draw/Draw.ts'; +import type { Grid } from '../grid/Grid.ts'; +import { parseGrid } from '../grid/Grid.ts'; +import { proposeGrid } from '../strategy/proposeGrid.ts'; + +export type BaselineKind = 'uniform-random' | 'fixed-grid' | 'frequent-numbers' | 'rare-numbers' | 'geometric-no-temporal'; + +export const BASELINE_KINDS: BaselineKind[] = [ + 'uniform-random', + 'fixed-grid', + 'frequent-numbers', + 'rare-numbers', + 'geometric-no-temporal', +]; + +export const BASELINE_LABELS: Record = { + 'uniform-random': 'Sélection uniforme aléatoire', + 'fixed-grid': 'Grille fixe', + 'frequent-numbers': 'Numéros historiquement fréquents', + 'rare-numbers': 'Numéros historiquement rares', + 'geometric-no-temporal': 'Modèle géométrique sans composante temporelle', +}; + +// Arbitrary, documented reference point - not chosen for any structural property. +const FIXED_GRID: Grid = parseGrid({ numbers: [1, 2, 3, 4, 5], stars: [1, 2] }); + +/** + * Every baseline but fixed-grid is itself just a Strategy with a specific + * rule set - reusing proposeGrid keeps the baselines honest (same code + * path a user's own strategy would go through) instead of a parallel + * bespoke implementation for each one. + */ +export const proposeBaselineGrid = (kind: BaselineKind, history: Draw[], seed: number): Grid => { + switch (kind) { + case 'uniform-random': + return proposeGrid({ id: `baseline-${kind}`, name: BASELINE_LABELS[kind], rules: [], seed }, history); + case 'fixed-grid': + return FIXED_GRID; + case 'frequent-numbers': + return proposeGrid( + { + id: `baseline-${kind}`, + name: BASELINE_LABELS[kind], + rules: [{ kind: 'number-frequency' }, { kind: 'star-frequency' }], + seed, + }, + history, + ); + case 'rare-numbers': + return proposeGrid( + { + id: `baseline-${kind}`, + name: BASELINE_LABELS[kind], + rules: [ + { kind: 'number-frequency', weight: -1 }, + { kind: 'star-frequency', weight: -1 }, + ], + seed, + }, + history, + ); + case 'geometric-no-temporal': + return proposeGrid( + { + id: `baseline-${kind}`, + name: BASELINE_LABELS[kind], + rules: [{ kind: 'number-frequency' }, { kind: 'decade-spread' }, { kind: 'star-frequency' }], + seed, + }, + history, + ); + } +}; diff --git a/src/domain/backtest/compareBaselines.test.ts b/src/domain/backtest/compareBaselines.test.ts new file mode 100644 index 0000000..f65fa80 --- /dev/null +++ b/src/domain/backtest/compareBaselines.test.ts @@ -0,0 +1,25 @@ +import { readFileSync } from 'node:fs'; +import { resolve } from 'node:path'; +import { describe, expect, it } from 'vitest'; +import { parseFdjCsv } from '../../infrastructure/csv/parseFdjCsv.ts'; +import { compareBaselines } from './compareBaselines.ts'; + +const REAL_CSV_PATH = resolve(process.cwd(), 'public/results/euromillions_202002.csv'); +const history = parseFdjCsv(readFileSync(REAL_CSV_PATH, 'utf8')); +const sortedAscending = [...history].sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime()); + +describe('compareBaselines', () => { + it('reports mean matched numbers/stars for all five baselines', () => { + const range = { start: sortedAscending[600].date, end: sortedAscending[679].date }; + + const comparisons = compareBaselines(history, range, 42); + + expect(comparisons).toHaveLength(5); + for (const comparison of comparisons) { + expect(comparison.meanMatchedNumbers).toBeGreaterThanOrEqual(0); + expect(comparison.meanMatchedNumbers).toBeLessThanOrEqual(5); + expect(comparison.meanMatchedStars).toBeGreaterThanOrEqual(0); + expect(comparison.meanMatchedStars).toBeLessThanOrEqual(2); + } + }); +}); diff --git a/src/domain/backtest/compareBaselines.ts b/src/domain/backtest/compareBaselines.ts new file mode 100644 index 0000000..87c7884 --- /dev/null +++ b/src/domain/backtest/compareBaselines.ts @@ -0,0 +1,20 @@ +import type { Draw } from '../draw/Draw.ts'; +import { BASELINE_KINDS, BASELINE_LABELS, proposeBaselineGrid } from './baselines.ts'; +import type { DateRange } from './runWalkForwardBacktest.ts'; +import { runWalkForwardBacktest } from './runWalkForwardBacktest.ts'; + +export interface BaselineComparison { + baselineName: string; + meanMatchedNumbers: number; + meanMatchedStars: number; +} + +export const compareBaselines = (draws: Draw[], dateRange: DateRange, seed: number): BaselineComparison[] => + BASELINE_KINDS.map((kind) => { + const result = runWalkForwardBacktest((history) => proposeBaselineGrid(kind, history, seed), draws, dateRange); + return { + baselineName: BASELINE_LABELS[kind], + meanMatchedNumbers: result.metrics.meanMatchedNumbers, + meanMatchedStars: result.metrics.meanMatchedStars, + }; + }); diff --git a/src/domain/backtest/runMonteCarloComparison.test.ts b/src/domain/backtest/runMonteCarloComparison.test.ts new file mode 100644 index 0000000..9fc53c2 --- /dev/null +++ b/src/domain/backtest/runMonteCarloComparison.test.ts @@ -0,0 +1,40 @@ +import { readFileSync } from 'node:fs'; +import { resolve } from 'node:path'; +import { describe, expect, it } from 'vitest'; +import { parseFdjCsv } from '../../infrastructure/csv/parseFdjCsv.ts'; +import { runMonteCarloComparison } from './runMonteCarloComparison.ts'; + +const REAL_CSV_PATH = resolve(process.cwd(), 'public/results/euromillions_202002.csv'); +const history = parseFdjCsv(readFileSync(REAL_CSV_PATH, 'utf8')); +const sortedAscending = [...history].sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime()); +const range = { start: sortedAscending[600].date, end: sortedAscending[679].date }; + +describe('runMonteCarloComparison', () => { + it('reports a percentile within [0, 100] and a distribution of the requested size', () => { + const result = runMonteCarloComparison(1.5, history, range, 30, 1); + + expect(result.sampleCount).toBe(30); + expect(result.distribution).toHaveLength(30); + expect(result.strategyPercentile).toBeGreaterThanOrEqual(0); + expect(result.strategyPercentile).toBeLessThanOrEqual(100); + }); + + it('is reproducible: the same base seed gives the same distribution', () => { + const a = runMonteCarloComparison(1.5, history, range, 20, 5); + const b = runMonteCarloComparison(1.5, history, range, 20, 5); + + expect(a).toEqual(b); + }); + + it('rates an unrealistically high strategy score near the top percentile', () => { + const result = runMonteCarloComparison(5, history, range, 30, 1); + + expect(result.strategyPercentile).toBeGreaterThan(90); + }); + + it('rates an unrealistically low strategy score near the bottom percentile', () => { + const result = runMonteCarloComparison(0, history, range, 30, 1); + + expect(result.strategyPercentile).toBeLessThan(10); + }); +}); diff --git a/src/domain/backtest/runMonteCarloComparison.ts b/src/domain/backtest/runMonteCarloComparison.ts new file mode 100644 index 0000000..b7a7076 --- /dev/null +++ b/src/domain/backtest/runMonteCarloComparison.ts @@ -0,0 +1,42 @@ +import type { Draw } from '../draw/Draw.ts'; +import { proposeBaselineGrid } from './baselines.ts'; +import type { DateRange } from './runWalkForwardBacktest.ts'; +import { runWalkForwardBacktest } from './runWalkForwardBacktest.ts'; + +export interface MonteCarloResult { + sampleCount: number; + distribution: number[]; + strategyPercentile: number; +} + +/** + * Runs `sampleCount` independent seeded uniform-random walk-forward + * backtests and reports where the tested strategy's mean matched numbers + * falls in that random distribution. "no advantage detected" (percentile + * near 50) is a valid, expected conclusion for most strategies - every + * valid Grid has the same theoretical draw probability. + */ +export const runMonteCarloComparison = ( + strategyMeanMatchedNumbers: number, + draws: Draw[], + dateRange: DateRange, + sampleCount: number, + baseSeed: number, +): MonteCarloResult => { + const distribution: number[] = []; + + for (let i = 0; i < sampleCount; i += 1) { + const seed = baseSeed + i + 1; + const result = runWalkForwardBacktest( + (history) => proposeBaselineGrid('uniform-random', history, seed), + draws, + dateRange, + ); + distribution.push(result.metrics.meanMatchedNumbers); + } + + const belowCount = distribution.filter((value) => value < strategyMeanMatchedNumbers).length; + const strategyPercentile = sampleCount === 0 ? 50 : (belowCount / sampleCount) * 100; + + return { sampleCount, distribution, strategyPercentile }; +}; diff --git a/src/domain/backtest/runWalkForwardBacktest.test.ts b/src/domain/backtest/runWalkForwardBacktest.test.ts index bfffc3a..32675b0 100644 --- a/src/domain/backtest/runWalkForwardBacktest.test.ts +++ b/src/domain/backtest/runWalkForwardBacktest.test.ts @@ -2,6 +2,7 @@ import { readFileSync } from 'node:fs'; import { resolve } from 'node:path'; import { describe, expect, it } from 'vitest'; import { parseFdjCsv } from '../../infrastructure/csv/parseFdjCsv.ts'; +import { proposeGrid } from '../strategy/proposeGrid.ts'; import type { Strategy } from '../strategy/Strategy.ts'; import { runWalkForwardBacktest } from './runWalkForwardBacktest.ts'; @@ -21,7 +22,7 @@ describe('runWalkForwardBacktest', () => { const strategy = strategyOf([{ kind: 'number-frequency' }]); const range = { start: sortedAscending[100].date, end: sortedAscending[109].date }; - const result = runWalkForwardBacktest(strategy, history, range); + const result = runWalkForwardBacktest((h) => proposeGrid(strategy, h), history, range); expect(result.steps).toHaveLength(10); expect(result.metrics.stepCount).toBe(10); @@ -32,8 +33,12 @@ describe('runWalkForwardBacktest', () => { const testRange = { start: sortedAscending[400].date, end: sortedAscending[499].date }; const truncatedHistory = sortedAscending.slice(0, 500); - const resultWithFullHistory = runWalkForwardBacktest(strategy, sortedAscending, testRange); - const resultWithTruncatedHistory = runWalkForwardBacktest(strategy, truncatedHistory, testRange); + const resultWithFullHistory = runWalkForwardBacktest((h) => proposeGrid(strategy, h), sortedAscending, testRange); + const resultWithTruncatedHistory = runWalkForwardBacktest( + (h) => proposeGrid(strategy, h), + truncatedHistory, + testRange, + ); expect(resultWithFullHistory.steps.map((step) => step.proposedGrid)).toEqual( resultWithTruncatedHistory.steps.map((step) => step.proposedGrid), @@ -44,8 +49,8 @@ describe('runWalkForwardBacktest', () => { const strategy = strategyOf([{ kind: 'number-frequency' }], 123); const range = { start: sortedAscending[200].date, end: sortedAscending[219].date }; - const resultA = runWalkForwardBacktest(strategy, history, range); - const resultB = runWalkForwardBacktest(strategy, history, range); + const resultA = runWalkForwardBacktest((h) => proposeGrid(strategy, h), history, range); + const resultB = runWalkForwardBacktest((h) => proposeGrid(strategy, h), history, range); expect(resultA).toEqual(resultB); }); @@ -54,7 +59,7 @@ describe('runWalkForwardBacktest', () => { const strategy = strategyOf([{ kind: 'number-frequency' }]); const range = { start: '1900-01-01', end: '1900-01-02' }; - const result = runWalkForwardBacktest(strategy, history, range); + const result = runWalkForwardBacktest((h) => proposeGrid(strategy, h), history, range); expect(result.steps).toHaveLength(0); expect(result.metrics.meanMatchedNumbers).toBe(0); diff --git a/src/domain/backtest/runWalkForwardBacktest.ts b/src/domain/backtest/runWalkForwardBacktest.ts index b204d49..684aa95 100644 --- a/src/domain/backtest/runWalkForwardBacktest.ts +++ b/src/domain/backtest/runWalkForwardBacktest.ts @@ -1,9 +1,9 @@ import type { Draw } from '../draw/Draw.ts'; import type { Grid } from '../grid/Grid.ts'; -import { proposeGrid } from '../strategy/proposeGrid.ts'; -import type { Strategy } from '../strategy/Strategy.ts'; import { countMatches, prizeRank } from './prizeRank.ts'; +export type GridProposer = (history: Draw[]) => Grid; + export interface DateRange { start: string; end: string; @@ -53,13 +53,16 @@ export const computeMetrics = (steps: BacktestStepResult[]): BacktestMetrics => }; /** - * Walk-forward: for each date T in `dateRange`, the strategy only ever sees + * Walk-forward: for each date T in `dateRange`, `propose` only ever sees * draws strictly before T (sortedAscending.slice(0, index)) - draws at or * after T, including any beyond `dateRange` itself, can never influence the - * proposal for T. See proposeGrid.test.ts's leakage test for the guarantee. + * proposal for T. See this file's leakage test for the guarantee. `propose` + * works for both a Strategy (via proposeGrid) and a history-independent + * baseline like fixed-grid, which is why it's a plain function rather than + * a Strategy parameter. */ export const runWalkForwardBacktest = ( - strategy: Strategy, + propose: GridProposer, draws: Draw[], dateRange: DateRange, ): BacktestResult => { @@ -71,7 +74,7 @@ export const runWalkForwardBacktest = ( if (!isWithinRange(draw.date, dateRange)) continue; const historyBeforeDraw = sortedAscending.slice(0, index); - const proposedGrid = proposeGrid(strategy, historyBeforeDraw); + const proposedGrid = propose(historyBeforeDraw); const matchedNumbers = countMatches(proposedGrid.numbers, draw.numbers); const matchedStars = countMatches(proposedGrid.stars, draw.stars); diff --git a/src/domain/strategy/proposeGrid.ts b/src/domain/strategy/proposeGrid.ts index a27e167..8b7cf00 100644 --- a/src/domain/strategy/proposeGrid.ts +++ b/src/domain/strategy/proposeGrid.ts @@ -48,10 +48,16 @@ const scoreNumbers = (rules: StrategyRule[], history: Draw[]): Record = {}; for (let n = 1; n <= 50; n += 1) scores[n] = 0; - const sortedDescending = sortedByDateDescending(history); + const needsFrequency = rules.some((rule) => rule.kind === 'number-frequency'); + const needsRepeat = rules.some((rule) => rule.kind === 'repeat-from-previous'); + const needsRecency = rules.some((rule) => rule.kind === 'recency'); + + const sortedDescending = needsRepeat || needsRecency ? sortedByDateDescending(history) : []; const mostRecentDraw = sortedDescending[0]; - const normalizedFrequency = normalizeToUnit(countByValue(history.flatMap((draw) => draw.numbers), 1, 50)); - const gaps = gapsSinceLastSeen(sortedDescending, 1, 50, (draw) => draw.numbers); + const normalizedFrequency = needsFrequency + ? normalizeToUnit(countByValue(history.flatMap((draw) => draw.numbers), 1, 50)) + : {}; + const gaps = needsRecency ? gapsSinceLastSeen(sortedDescending, 1, 50, (draw) => draw.numbers) : {}; for (const rule of rules) { const weight = rule.weight ?? 1; @@ -75,6 +81,8 @@ const scoreStars = (rules: StrategyRule[], history: Draw[]): Record = {}; for (let s = 1; s <= 12; s += 1) scores[s] = 0; + if (!rules.some((rule) => rule.kind === 'star-frequency')) return scores; + const normalizedFrequency = normalizeToUnit(countByValue(history.flatMap((draw) => draw.stars), 1, 12)); for (const rule of rules) { From 197681c42fcb401c9a26dcd5c681cfa1e97b85a5 Mon Sep 17 00:00:00 2001 From: keyobs Date: Mon, 10 Aug 2026 11:04:34 +0200 Subject: [PATCH 28/68] feat(domain): add train/validation/test date-range split and overfitting detection splitDateRanges chronologically slices sorted history into train (oldest)/validation/test (most recent) by ratio - chronological, not random, so test never precedes train, consistent with the walk-forward no-leakage principle. detectOverfitting flags a >0.75 mean-matched- numbers gap between train and validation/test; "test beats train" and "all three comparable" both correctly report no overfitting signal. --- src/domain/backtest/detectOverfitting.test.ts | 31 ++++++++++++++ src/domain/backtest/detectOverfitting.ts | 38 +++++++++++++++++ src/domain/backtest/splitDateRanges.test.ts | 42 +++++++++++++++++++ src/domain/backtest/splitDateRanges.ts | 33 +++++++++++++++ 4 files changed, 144 insertions(+) create mode 100644 src/domain/backtest/detectOverfitting.test.ts create mode 100644 src/domain/backtest/detectOverfitting.ts create mode 100644 src/domain/backtest/splitDateRanges.test.ts create mode 100644 src/domain/backtest/splitDateRanges.ts diff --git a/src/domain/backtest/detectOverfitting.test.ts b/src/domain/backtest/detectOverfitting.test.ts new file mode 100644 index 0000000..c9fb4b6 --- /dev/null +++ b/src/domain/backtest/detectOverfitting.test.ts @@ -0,0 +1,31 @@ +import { describe, expect, it } from 'vitest'; +import type { BacktestMetrics } from './runWalkForwardBacktest.ts'; +import { detectOverfitting } from './detectOverfitting.ts'; + +const metricsWith = (meanMatchedNumbers: number): BacktestMetrics => ({ + stepCount: 10, + meanMatchedNumbers, + meanMatchedStars: 0.5, + bestPrizeRank: null, + prizeRankCounts: {}, +}); + +describe('detectOverfitting', () => { + it('flags overfitting when train far exceeds validation and test', () => { + const signal = detectOverfitting(metricsWith(3.5), metricsWith(1.0), metricsWith(0.9)); + + expect(signal.isLikelyOverfit).toBe(true); + }); + + it('does not flag overfitting when train/validation/test are comparable', () => { + const signal = detectOverfitting(metricsWith(1.2), metricsWith(1.1), metricsWith(1.0)); + + expect(signal.isLikelyOverfit).toBe(false); + }); + + it('does not flag overfitting when test happens to beat train ("no advantage" is a valid outcome)', () => { + const signal = detectOverfitting(metricsWith(0.8), metricsWith(0.9), metricsWith(1.1)); + + expect(signal.isLikelyOverfit).toBe(false); + }); +}); diff --git a/src/domain/backtest/detectOverfitting.ts b/src/domain/backtest/detectOverfitting.ts new file mode 100644 index 0000000..000a068 --- /dev/null +++ b/src/domain/backtest/detectOverfitting.ts @@ -0,0 +1,38 @@ +import type { BacktestMetrics } from './runWalkForwardBacktest.ts'; + +export interface OverfittingSignal { + isLikelyOverfit: boolean; + trainMeanMatchedNumbers: number; + validationMeanMatchedNumbers: number; + testMeanMatchedNumbers: number; + reason: string; +} + +// Out of a 0-5 matched-numbers scale; a train/validation-or-test gap past +// this is treated as a meaningful divergence rather than ordinary noise. +const OVERFIT_GAP_THRESHOLD = 0.75; + +export const detectOverfitting = ( + trainMetrics: BacktestMetrics, + validationMetrics: BacktestMetrics, + testMetrics: BacktestMetrics, +): OverfittingSignal => { + const trainMean = trainMetrics.meanMatchedNumbers; + const validationMean = validationMetrics.meanMatchedNumbers; + const testMean = testMetrics.meanMatchedNumbers; + + const isLikelyOverfit = + trainMean - validationMean > OVERFIT_GAP_THRESHOLD || trainMean - testMean > OVERFIT_GAP_THRESHOLD; + + const reason = isLikelyOverfit + ? `Train (${trainMean.toFixed(2)} numéros en moyenne) nettement supérieur à validation (${validationMean.toFixed(2)}) et/ou test (${testMean.toFixed(2)}).` + : 'Aucun signe net de surapprentissage : performances train/validation/test comparables.'; + + return { + isLikelyOverfit, + trainMeanMatchedNumbers: trainMean, + validationMeanMatchedNumbers: validationMean, + testMeanMatchedNumbers: testMean, + reason, + }; +}; diff --git a/src/domain/backtest/splitDateRanges.test.ts b/src/domain/backtest/splitDateRanges.test.ts new file mode 100644 index 0000000..fdc9fa8 --- /dev/null +++ b/src/domain/backtest/splitDateRanges.test.ts @@ -0,0 +1,42 @@ +import { readFileSync } from 'node:fs'; +import { resolve } from 'node:path'; +import { describe, expect, it } from 'vitest'; +import { parseFdjCsv } from '../../infrastructure/csv/parseFdjCsv.ts'; +import { splitDateRanges } from './splitDateRanges.ts'; + +const REAL_CSV_PATH = resolve(process.cwd(), 'public/results/euromillions_202002.csv'); +const history = parseFdjCsv(readFileSync(REAL_CSV_PATH, 'utf8')); + +const countInRange = (draws: typeof history, range: { start: string; end: string }): number => + draws.filter((draw) => draw.date >= range.start && draw.date <= range.end).length; + +describe('splitDateRanges', () => { + it('produces chronologically ordered, non-overlapping ranges', () => { + const { train, validation, test } = splitDateRanges(history, 0.7, 0.15); + + expect(train.start <= train.end).toBe(true); + expect(train.end < validation.start).toBe(true); + expect(validation.end < test.start).toBe(true); + expect(test.start <= test.end).toBe(true); + }); + + it('covers every draw exactly once across the three ranges', () => { + const { train, validation, test } = splitDateRanges(history, 0.7, 0.15); + + const total = countInRange(history, train) + countInRange(history, validation) + countInRange(history, test); + + expect(total).toBe(history.length); + }); + + it('respects the requested proportions approximately', () => { + const { train, validation } = splitDateRanges(history, 0.7, 0.15); + + const trainCount = countInRange(history, train); + expect(trainCount).toBeGreaterThan(history.length * 0.65); + expect(trainCount).toBeLessThan(history.length * 0.75); + + const validationCount = countInRange(history, validation); + expect(validationCount).toBeGreaterThan(history.length * 0.1); + expect(validationCount).toBeLessThan(history.length * 0.2); + }); +}); diff --git a/src/domain/backtest/splitDateRanges.ts b/src/domain/backtest/splitDateRanges.ts new file mode 100644 index 0000000..253ea82 --- /dev/null +++ b/src/domain/backtest/splitDateRanges.ts @@ -0,0 +1,33 @@ +import type { Draw } from '../draw/Draw.ts'; +import type { DateRange } from './runWalkForwardBacktest.ts'; + +export interface TrainValidationTestRanges { + train: DateRange; + validation: DateRange; + test: DateRange; +} + +/** + * Splits sorted history chronologically - train is the oldest slice, + * validation the middle slice, test the most recent slice. Chronological + * (not random) splitting is what keeps this consistent with the + * walk-forward, no-future-leakage principle: test never precedes train. + */ +export const splitDateRanges = ( + draws: Draw[], + trainRatio: number, + validationRatio: number, +): TrainValidationTestRanges => { + const sortedAscending = [...draws].sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime()); + const total = sortedAscending.length; + const trainEndIndex = Math.floor(total * trainRatio); + const validationEndIndex = Math.floor(total * (trainRatio + validationRatio)); + + const dateAt = (index: number): string => sortedAscending[Math.min(Math.max(index, 0), total - 1)].date; + + return { + train: { start: dateAt(0), end: dateAt(trainEndIndex - 1) }, + validation: { start: dateAt(trainEndIndex), end: dateAt(validationEndIndex - 1) }, + test: { start: dateAt(validationEndIndex), end: dateAt(total - 1) }, + }; +}; From 1ddaa7fe833f3deb7fa1a68f619ae58456abe622 Mon Sep 17 00:00:00 2001 From: keyobs Date: Mon, 10 Aug 2026 11:08:20 +0200 Subject: [PATCH 29/68] feat(application): compose Strategy + backtester + baselines into Experiment runExperiment wires the V2 pipeline into the spec's exact Experiment/ExperimentResult/EvaluatedGrid shapes: splitDateRanges for train/validation/test, walk-forward backtest on each, detectOverfitting across them, compareBaselines + runMonteCarloComparison on the test range, and one WindowComparison per requested TemporalWindow (windows span from latestDrawDate(draws), independent of the train/val/test split - "6 has no privileged status" applies here too). generatedGrids carry precomputed features/geometry per spec's note that V4 should project them without recomputing the backtest. datasetVersion/modelVersion are simple placeholder constants - no real versioning infra yet, flagged as a simplification. --- src/application/runExperiment.test.ts | 74 +++++++++++++++ src/application/runExperiment.ts | 132 ++++++++++++++++++++++++++ 2 files changed, 206 insertions(+) create mode 100644 src/application/runExperiment.test.ts create mode 100644 src/application/runExperiment.ts diff --git a/src/application/runExperiment.test.ts b/src/application/runExperiment.test.ts new file mode 100644 index 0000000..900590c --- /dev/null +++ b/src/application/runExperiment.test.ts @@ -0,0 +1,74 @@ +import { readFileSync } from 'node:fs'; +import { resolve } from 'node:path'; +import { describe, expect, it } from 'vitest'; +import { parseFdjCsv } from '../infrastructure/csv/parseFdjCsv.ts'; +import { TEMPORAL_WINDOWS } from '../domain/scoring/TemporalWindow.ts'; +import type { Strategy } from '../domain/strategy/Strategy.ts'; +import { runExperiment } from './runExperiment.ts'; + +const REAL_CSV_PATH = resolve(process.cwd(), 'public/results/euromillions_202002.csv'); +const draws = parseFdjCsv(readFileSync(REAL_CSV_PATH, 'utf8')); + +const strategyOf = (rules: Strategy['rules'], seed = 42): Strategy => ({ + id: 'test-strategy', + name: 'Test', + rules, + seed, +}); + +describe('runExperiment', () => { + it('produces a fully-populated Experiment with generatedGrids matching the test-range steps', () => { + const strategy = strategyOf([{ kind: 'number-frequency' }, { kind: 'recency' }]); + + const experiment = runExperiment({ + strategy, + draws, + trainRatio: 0.7, + validationRatio: 0.15, + windows: TEMPORAL_WINDOWS, + monteCarloSampleCount: 50, + }); + + expect(experiment.id).toBeTruthy(); + expect(experiment.results.generatedGrids.length).toBe(experiment.results.metrics.stepCount); + expect(experiment.results.baselineComparisons).toHaveLength(5); + expect(experiment.windowComparisons).toHaveLength(TEMPORAL_WINDOWS.length); + expect(experiment.monteCarlo.sampleCount).toBe(50); + expect(typeof experiment.overfitting.isLikelyOverfit).toBe('boolean'); + + for (const generated of experiment.results.generatedGrids) { + expect(generated.geometry.grid).toEqual(generated.grid); + expect(generated.score).toBeGreaterThanOrEqual(0); + expect(generated.score).toBeLessThanOrEqual(5); + } + }); + + it('is reproducible: the same strategy/seed/config gives the same experiment results', () => { + const strategy = strategyOf([{ kind: 'above-31', weight: 2 }], 7); + const config = { strategy, draws, trainRatio: 0.7, validationRatio: 0.15, windows: TEMPORAL_WINDOWS, monteCarloSampleCount: 20 }; + + const a = runExperiment(config); + const b = runExperiment(config); + + expect(a.results).toEqual(b.results); + expect(a.overfitting).toEqual(b.overfitting); + expect(a.monteCarlo).toEqual(b.monteCarlo); + expect(a.windowComparisons).toEqual(b.windowComparisons); + }); + + it('supports a strategy with no rules and still reports a valid ("no advantage detected") result', () => { + const strategy = strategyOf([], 1); + + const experiment = runExperiment({ + strategy, + draws, + trainRatio: 0.7, + validationRatio: 0.15, + windows: ['all'], + monteCarloSampleCount: 30, + }); + + expect(experiment.monteCarlo.strategyPercentile).toBeGreaterThanOrEqual(0); + expect(experiment.monteCarlo.strategyPercentile).toBeLessThanOrEqual(100); + }); +}); diff --git a/src/application/runExperiment.ts b/src/application/runExperiment.ts new file mode 100644 index 0000000..1a7e76e --- /dev/null +++ b/src/application/runExperiment.ts @@ -0,0 +1,132 @@ +import type { Draw } from '../domain/draw/Draw.ts'; +import type { Grid } from '../domain/grid/Grid.ts'; +import type { FeatureVector } from '../domain/features/FeatureExtractor.ts'; +import { extractFeatures } from '../domain/features/FeatureExtractor.ts'; +import type { GeometryDescriptor } from '../domain/geometry/GeometryDescriptor.ts'; +import { buildGeometryDescriptor } from '../domain/geometry/GeometryDescriptor.ts'; +import type { Strategy } from '../domain/strategy/Strategy.ts'; +import { proposeGrid } from '../domain/strategy/proposeGrid.ts'; +import { filterByWindow, latestDrawDate } from '../domain/scoring/TemporalWindow.ts'; +import type { TemporalWindow } from '../domain/scoring/TemporalWindow.ts'; +import type { BaselineComparison } from '../domain/backtest/compareBaselines.ts'; +import { compareBaselines } from '../domain/backtest/compareBaselines.ts'; +import type { OverfittingSignal } from '../domain/backtest/detectOverfitting.ts'; +import { detectOverfitting } from '../domain/backtest/detectOverfitting.ts'; +import type { MonteCarloResult } from '../domain/backtest/runMonteCarloComparison.ts'; +import { runMonteCarloComparison } from '../domain/backtest/runMonteCarloComparison.ts'; +import type { BacktestMetrics, DateRange } from '../domain/backtest/runWalkForwardBacktest.ts'; +import { runWalkForwardBacktest } from '../domain/backtest/runWalkForwardBacktest.ts'; +import type { TrainValidationTestRanges } from '../domain/backtest/splitDateRanges.ts'; +import { splitDateRanges } from '../domain/backtest/splitDateRanges.ts'; + +const DATASET_VERSION = 'fdj-euromillions-2020-2026'; +const MODEL_VERSION = 'izeetok-v2-strategy-1'; + +export interface EvaluatedGrid { + grid: Grid; + date: string; + features: FeatureVector; + geometry: GeometryDescriptor; + score?: number; +} + +export interface ExperimentResult { + generatedGrids: EvaluatedGrid[]; + metrics: BacktestMetrics; + baselineComparisons: BaselineComparison[]; +} + +export interface WindowComparison { + window: TemporalWindow; + metrics: BacktestMetrics; +} + +export interface Experiment { + id: string; + createdAt: string; + datasetVersion: string; + modelVersion: string; + seed: number; + trainRange: DateRange; + validationRange: DateRange; + testRange: DateRange; + windows: TemporalWindow[]; + strategy: Strategy; + results: ExperimentResult; + overfitting: OverfittingSignal; + monteCarlo: MonteCarloResult; + windowComparisons: WindowComparison[]; +} + +export interface RunExperimentConfig { + strategy: Strategy; + draws: Draw[]; + trainRatio: number; + validationRatio: number; + windows: TemporalWindow[]; + monteCarloSampleCount: number; +} + +const rangesFor = ( + window: TemporalWindow, + draws: Draw[], + referenceDate: Date, +): DateRange => { + const windowDraws = filterByWindow(draws, window, referenceDate).sort((a, b) => a.date.localeCompare(b.date)); + const fallback = referenceDate.toISOString().slice(0, 10); + + return windowDraws.length > 0 + ? { start: windowDraws[0].date, end: windowDraws[windowDraws.length - 1].date } + : { start: fallback, end: fallback }; +}; + +export const runExperiment = (config: RunExperimentConfig): Experiment => { + const { strategy, draws, trainRatio, validationRatio, windows, monteCarloSampleCount } = config; + const { train, validation, test }: TrainValidationTestRanges = splitDateRanges(draws, trainRatio, validationRatio); + const propose = (history: Draw[]): Grid => proposeGrid(strategy, history); + + const trainResult = runWalkForwardBacktest(propose, draws, train); + const validationResult = runWalkForwardBacktest(propose, draws, validation); + const testResult = runWalkForwardBacktest(propose, draws, test); + + const overfitting = detectOverfitting(trainResult.metrics, validationResult.metrics, testResult.metrics); + const baselineComparisons = compareBaselines(draws, test, strategy.seed); + const monteCarlo = runMonteCarloComparison( + testResult.metrics.meanMatchedNumbers, + draws, + test, + monteCarloSampleCount, + strategy.seed, + ); + + const generatedGrids: EvaluatedGrid[] = testResult.steps.map((step) => ({ + grid: step.proposedGrid, + date: step.date, + features: extractFeatures(step.proposedGrid), + geometry: buildGeometryDescriptor(step.proposedGrid), + score: step.matchedNumbers, + })); + + const referenceDate = latestDrawDate(draws); + const windowComparisons: WindowComparison[] = windows.map((window) => ({ + window, + metrics: runWalkForwardBacktest(propose, draws, rangesFor(window, draws, referenceDate)).metrics, + })); + + return { + id: crypto.randomUUID(), + createdAt: new Date().toISOString(), + datasetVersion: DATASET_VERSION, + modelVersion: MODEL_VERSION, + seed: strategy.seed, + trainRange: train, + validationRange: validation, + testRange: test, + windows, + strategy, + results: { generatedGrids, metrics: testResult.metrics, baselineComparisons }, + overfitting, + monteCarlo, + windowComparisons, + }; +}; From 0f73e135e6ac30cdbf6ee6f1bfdfdc9c1286ef92 Mon Sep 17 00:00:00 2001 From: keyobs Date: Mon, 10 Aug 2026 11:25:37 +0200 Subject: [PATCH 30/68] feat(laboratory): build the /laboratory page Strategy builder (toggle the 8 rule kinds, weight/params inline), train/validation/test % split, temporal-window multi-select, seed and Monte Carlo sample count, run button -> runExperiment. Results show the overfitting warning (when triggered), test metrics, baseline comparison table, Monte Carlo percentile + histogram, per-window comparison, and the generated-grids table for date-level inspection. JSON export via a Blob download (no backend, per ADR-0001). Multiple experiments accumulate in a session-local comparison list. Smoke-tested end to end: toggled extra rules incl. sum-range/decade- spread, selected extra windows, ran two experiments, confirmed all tables/chart populate and the comparison list grows - zero console errors. The repeated proposed grids across nearby dates in the generated-grids table are expected: with 580+ historical draws already accumulated, one more draw barely shifts frequency rankings, so a frequency-driven strategy is naturally stable step to step - not a bug. Not implemented (flagged, not gold-plated): true backtest cancellation/AbortSignal and Web Workers - a full run finishes in roughly a second at this dataset size (see the perf fix in the baselines/Monte Carlo commit), so there's nothing to cancel or offload yet. --- src/app/AppLayout.tsx | 1 + src/app/router.tsx | 2 + src/pages/laboratory/ExperimentResults.tsx | 131 +++++++++ .../laboratory/LaboratoryPage.module.scss | 51 ++++ src/pages/laboratory/LaboratoryPage.tsx | 269 ++++++++++++++++++ src/pages/laboratory/buildHistogram.test.ts | 24 ++ src/pages/laboratory/buildHistogram.ts | 24 ++ .../laboratory/strategyFormRules.test.ts | 33 +++ src/pages/laboratory/strategyFormRules.ts | 61 ++++ 9 files changed, 596 insertions(+) create mode 100644 src/pages/laboratory/ExperimentResults.tsx create mode 100644 src/pages/laboratory/LaboratoryPage.module.scss create mode 100644 src/pages/laboratory/LaboratoryPage.tsx create mode 100644 src/pages/laboratory/buildHistogram.test.ts create mode 100644 src/pages/laboratory/buildHistogram.ts create mode 100644 src/pages/laboratory/strategyFormRules.test.ts create mode 100644 src/pages/laboratory/strategyFormRules.ts diff --git a/src/app/AppLayout.tsx b/src/app/AppLayout.tsx index 93a86d7..ab0aa2c 100644 --- a/src/app/AppLayout.tsx +++ b/src/app/AppLayout.tsx @@ -5,6 +5,7 @@ const NAV_ITEMS = [ { to: '/evaluation', label: 'Évaluation' }, { to: '/draws', label: 'Tirages' }, { to: '/geometry', label: 'Géométrie' }, + { to: '/laboratory', label: 'Laboratoire' }, ]; const AppLayout = () => { diff --git a/src/app/router.tsx b/src/app/router.tsx index 629a27c..17dfc2d 100644 --- a/src/app/router.tsx +++ b/src/app/router.tsx @@ -3,6 +3,7 @@ import AppLayout from './AppLayout.tsx'; import EvaluationPage from '../pages/evaluation/EvaluationPage.tsx'; import DrawsPage from '../pages/draws/DrawsPage.tsx'; import GeometryPage from '../pages/geometry/GeometryPage.tsx'; +import LaboratoryPage from '../pages/laboratory/LaboratoryPage.tsx'; import SpikePage from '../pages/spike/SpikePage.tsx'; export const router = createBrowserRouter([ @@ -14,6 +15,7 @@ export const router = createBrowserRouter([ { path: 'evaluation', element: }, { path: 'draws', element: }, { path: 'geometry', element: }, + { path: 'laboratory', element: }, { path: 'spike', element: }, ], }, diff --git a/src/pages/laboratory/ExperimentResults.tsx b/src/pages/laboratory/ExperimentResults.tsx new file mode 100644 index 0000000..18c6634 --- /dev/null +++ b/src/pages/laboratory/ExperimentResults.tsx @@ -0,0 +1,131 @@ +import { Bar, BarChart, CartesianGrid, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts'; +import type { Experiment } from '../../application/runExperiment.ts'; +import { buildHistogram } from './buildHistogram.ts'; +import styles from './LaboratoryPage.module.scss'; + +interface ExperimentResultsProps { + experiment: Experiment; + onExport: (experiment: Experiment) => void; +} + +const windowLabel = (window: Experiment['windows'][number]): string => (window === 'all' ? 'Tout' : `${window} an(s)`); + +const ExperimentResults = ({ experiment, onExport }: ExperimentResultsProps) => { + const histogramData = buildHistogram(experiment.monteCarlo.distribution, 10); + const isNoAdvantageDetected = Math.abs(experiment.monteCarlo.strategyPercentile - 50) < 10; + + return ( +
+

Résultats — {experiment.strategy.name}

+ + {experiment.overfitting.isLikelyOverfit && ( +

+ ⚠ Risque de surapprentissage : {experiment.overfitting.reason} +

+ )} + +
+

+ Test — numéros trouvés en moyenne : {experiment.results.metrics.meanMatchedNumbers.toFixed(2)} / 5, + étoiles : {experiment.results.metrics.meanMatchedStars.toFixed(2)} / 2 +

+

Meilleur rang obtenu sur la période de test : {experiment.results.metrics.bestPrizeRank ?? 'aucun'}

+

+ Train : {experiment.overfitting.trainMeanMatchedNumbers.toFixed(2)} — Validation :{' '} + {experiment.overfitting.validationMeanMatchedNumbers.toFixed(2)} — Test :{' '} + {experiment.overfitting.testMeanMatchedNumbers.toFixed(2)} +

+
+ +
+

Comparaison aux références

+ + + + + + + + + + {experiment.results.baselineComparisons.map((baseline) => ( + + + + + + ))} + +
RéférenceNuméros moy.Étoiles moy.
{baseline.baselineName}{baseline.meanMatchedNumbers.toFixed(2)}{baseline.meanMatchedStars.toFixed(2)}
+
+ +
+

Simulation Monte Carlo

+

+ Percentile de la stratégie testée : {experiment.monteCarlo.strategyPercentile.toFixed(1)}e sur{' '} + {experiment.monteCarlo.sampleCount} stratégies aléatoires équivalentes. +

+ {isNoAdvantageDetected &&

Conclusion : aucun avantage détectable par rapport au hasard.

} + + + + + + + + + +
+ +
+

Comparaison par fenêtre temporelle

+ + + + + + + + + {experiment.windowComparisons.map((comparison) => ( + + + + + ))} + +
FenêtreNuméros moy.
{windowLabel(comparison.window)}{comparison.metrics.meanMatchedNumbers.toFixed(2)}
+
+ +
+

Grilles générées (période de test)

+ + + + + + + + + + + {experiment.results.generatedGrids.map((generated) => ( + + + + + + + ))} + +
DateGrille proposéeÉtoilesNuméros trouvés
{generated.date}{generated.grid.numbers.join(' · ')}{generated.grid.stars.join(' · ')}{generated.score}
+
+ + +
+ ); +}; + +export default ExperimentResults; diff --git a/src/pages/laboratory/LaboratoryPage.module.scss b/src/pages/laboratory/LaboratoryPage.module.scss new file mode 100644 index 0000000..9c34a89 --- /dev/null +++ b/src/pages/laboratory/LaboratoryPage.module.scss @@ -0,0 +1,51 @@ +.page { + display: flex; + flex-direction: column; + gap: 2rem; + text-align: left; +} + +.ruleRow { + display: flex; + align-items: center; + gap: 0.75rem; + padding: 0.25rem 0; +} + +.ruleRow input[type='number'] { + width: 4.5rem; +} + +.formRow { + display: flex; + flex-wrap: wrap; + gap: 1.5rem; + align-items: flex-end; +} + +.windowChecks { + display: flex; + flex-wrap: wrap; + gap: 1rem; +} + +.table { + width: 100%; + border-collapse: collapse; + font-size: 0.9rem; + + th, + td { + padding: 0.4rem 0.6rem; + border-bottom: 1px solid var(--border); + text-align: left; + white-space: nowrap; + } +} + +.warning { + border: 1px solid #d1435b; + border-radius: 0.5rem; + padding: 0.75rem 1rem; + color: #d1435b; +} diff --git a/src/pages/laboratory/LaboratoryPage.tsx b/src/pages/laboratory/LaboratoryPage.tsx new file mode 100644 index 0000000..8dc4c74 --- /dev/null +++ b/src/pages/laboratory/LaboratoryPage.tsx @@ -0,0 +1,269 @@ +import { useState } from 'react'; +import type { ChangeEvent } from 'react'; +import { useQuery } from '@tanstack/react-query'; +import { drawRepository } from '../../application/drawRepository.ts'; +import type { Experiment } from '../../application/runExperiment.ts'; +import { runExperiment } from '../../application/runExperiment.ts'; +import type { Draw } from '../../domain/draw/Draw.ts'; +import { TEMPORAL_WINDOWS } from '../../domain/scoring/TemporalWindow.ts'; +import type { TemporalWindow } from '../../domain/scoring/TemporalWindow.ts'; +import type { Strategy } from '../../domain/strategy/Strategy.ts'; +import ExperimentResults from './ExperimentResults.tsx'; +import { + DEFAULT_RULES_STATE, + RULE_KINDS, + RULE_LABELS, + SCORING_KINDS, + buildRulesFromState, +} from './strategyFormRules.ts'; +import type { RulesState } from './strategyFormRules.ts'; +import styles from './LaboratoryPage.module.scss'; + +const EMPTY_DRAWS: Draw[] = []; +const DEFAULT_MONTE_CARLO_SAMPLES = 200; + +const windowLabel = (window: TemporalWindow): string => (window === 'all' ? 'Tout' : `${window} an(s)`); + +const downloadExperiment = (experiment: Experiment) => { + const blob = new Blob([JSON.stringify(experiment, null, 2)], { type: 'application/json' }); + const url = URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = url; + link.download = `experiment-${experiment.id}.json`; + link.click(); + URL.revokeObjectURL(url); +}; + +const LaboratoryPage = () => { + const [strategyName, setStrategyName] = useState('Ma stratégie'); + const [seed, setSeed] = useState(42); + const [rulesState, setRulesState] = useState(DEFAULT_RULES_STATE); + const [trainPercent, setTrainPercent] = useState(70); + const [validationPercent, setValidationPercent] = useState(15); + const [selectedWindows, setSelectedWindows] = useState(['all']); + const [monteCarloSampleCount, setMonteCarloSampleCount] = useState(DEFAULT_MONTE_CARLO_SAMPLES); + const [experiments, setExperiments] = useState([]); + const [selectedExperimentId, setSelectedExperimentId] = useState(null); + + const historyQuery = useQuery({ queryKey: ['draws', 'all'], queryFn: () => drawRepository.getAll() }); + const draws = historyQuery.data ?? EMPTY_DRAWS; + const testPercent = 100 - trainPercent - validationPercent; + + const updateRule = (kind: (typeof RULE_KINDS)[number], patch: Partial) => { + setRulesState((previous) => ({ ...previous, [kind]: { ...previous[kind], ...patch } })); + }; + + const toggleWindow = (window: TemporalWindow) => { + setSelectedWindows((previous) => + previous.includes(window) ? previous.filter((w) => w !== window) : [...previous, window], + ); + }; + + const handleRunExperiment = () => { + const strategy: Strategy = { + id: crypto.randomUUID(), + name: strategyName, + rules: buildRulesFromState(rulesState), + seed, + }; + + const experiment = runExperiment({ + strategy, + draws, + trainRatio: trainPercent / 100, + validationRatio: validationPercent / 100, + windows: selectedWindows, + monteCarloSampleCount, + }); + + setExperiments((previous) => [experiment, ...previous]); + setSelectedExperimentId(experiment.id); + }; + + const selectedExperiment = experiments.find((experiment) => experiment.id === selectedExperimentId) ?? null; + + return ( +
+

Laboratoire de stratégies

+

Cette hypothèse fait-elle mieux qu'une stratégie de référence lorsqu'on la teste correctement ?

+ +
+

Construire une stratégie

+
+ + +
+ +
+ Règles + {RULE_KINDS.map((kind) => { + const config = rulesState[kind]; + return ( +
+ + {config.enabled && SCORING_KINDS.includes(kind) && ( + ) => updateRule(kind, { weight: Number(event.target.value) })} + aria-label={`Poids - ${RULE_LABELS[kind]}`} + data-testid={`rule-weight-${kind}`} + /> + )} + {config.enabled && kind === 'sum-range' && ( + <> + ) => updateRule(kind, { min: Number(event.target.value) })} + aria-label="Somme minimum" + /> + ) => updateRule(kind, { max: Number(event.target.value) })} + aria-label="Somme maximum" + /> + + )} + {config.enabled && kind === 'parity-target' && ( + ) => updateRule(kind, { oddCount: Number(event.target.value) })} + aria-label="Nombre de numéros impairs souhaité" + /> + )} +
+ ); + })} +
+
+ +
+

Configuration de l'expérience

+
+ + +

Test : {testPercent}%

+
+ +
+ Fenêtres temporelles (aucune n'a de statut privilégié) +
+ {TEMPORAL_WINDOWS.map((window) => ( + + ))} +
+
+ + + +

+ +

+
+ + {experiments.length > 0 && ( +
+

Expériences de cette session ({experiments.length})

+ + + + + + + + + + + {experiments.map((experiment) => ( + + + + + + + + ))} + +
StratégieSeedNuméros moy. (test)Percentile Monte Carlo +
{experiment.strategy.name}{experiment.seed}{experiment.results.metrics.meanMatchedNumbers.toFixed(2)}{experiment.monteCarlo.strategyPercentile.toFixed(0)}e + +
+
+ )} + + {selectedExperiment && } +
+ ); +}; + +export default LaboratoryPage; diff --git a/src/pages/laboratory/buildHistogram.test.ts b/src/pages/laboratory/buildHistogram.test.ts new file mode 100644 index 0000000..6d4d4c7 --- /dev/null +++ b/src/pages/laboratory/buildHistogram.test.ts @@ -0,0 +1,24 @@ +import { describe, expect, it } from 'vitest'; +import { buildHistogram } from './buildHistogram.ts'; + +describe('buildHistogram', () => { + it('returns an empty array for no values', () => { + expect(buildHistogram([], 10)).toEqual([]); + }); + + it('distributes every value into exactly one of the requested bins', () => { + const values = [0, 1, 2, 3, 4, 5]; + + const bins = buildHistogram(values, 5); + + expect(bins).toHaveLength(5); + expect(bins.reduce((acc, bin) => acc + bin.count, 0)).toBe(values.length); + }); + + it('puts identical values into the same bin', () => { + const bins = buildHistogram([2, 2, 2, 2], 4); + + expect(bins.reduce((acc, bin) => acc + bin.count, 0)).toBe(4); + expect(bins.filter((bin) => bin.count > 0)).toHaveLength(1); + }); +}); diff --git a/src/pages/laboratory/buildHistogram.ts b/src/pages/laboratory/buildHistogram.ts new file mode 100644 index 0000000..8fba0c4 --- /dev/null +++ b/src/pages/laboratory/buildHistogram.ts @@ -0,0 +1,24 @@ +export interface HistogramBin { + label: string; + count: number; +} + +export const buildHistogram = (values: number[], binCount: number): HistogramBin[] => { + if (values.length === 0) return []; + + const min = Math.min(...values); + const max = Math.max(...values); + const range = max - min || 1; + const binSize = range / binCount; + const bins: HistogramBin[] = Array.from({ length: binCount }, (_, i) => ({ + label: (min + i * binSize).toFixed(2), + count: 0, + })); + + for (const value of values) { + const index = Math.min(Math.floor((value - min) / binSize), binCount - 1); + bins[index].count += 1; + } + + return bins; +}; diff --git a/src/pages/laboratory/strategyFormRules.test.ts b/src/pages/laboratory/strategyFormRules.test.ts new file mode 100644 index 0000000..2e63991 --- /dev/null +++ b/src/pages/laboratory/strategyFormRules.test.ts @@ -0,0 +1,33 @@ +import { describe, expect, it } from 'vitest'; +import { DEFAULT_RULES_STATE, buildRulesFromState } from './strategyFormRules.ts'; + +describe('buildRulesFromState', () => { + it('only includes enabled rules', () => { + const rules = buildRulesFromState(DEFAULT_RULES_STATE); + + expect(rules.map((r) => r.kind).sort()).toEqual(['number-frequency', 'star-frequency']); + }); + + it('carries params for sum-range and parity-target, weight for scoring kinds', () => { + const state = { + ...DEFAULT_RULES_STATE, + 'sum-range': { enabled: true, weight: 1, min: 90, max: 130 }, + 'parity-target': { enabled: true, weight: 1, oddCount: 4 }, + 'above-31': { enabled: true, weight: -2 }, + }; + + const rules = buildRulesFromState(state); + + expect(rules).toContainEqual({ kind: 'sum-range', params: { min: 90, max: 130 } }); + expect(rules).toContainEqual({ kind: 'parity-target', params: { oddCount: 4 } }); + expect(rules).toContainEqual({ kind: 'above-31', weight: -2 }); + }); + + it('carries no params for decade-spread', () => { + const state = { ...DEFAULT_RULES_STATE, 'decade-spread': { enabled: true, weight: 1 } }; + + const rules = buildRulesFromState(state); + + expect(rules).toContainEqual({ kind: 'decade-spread' }); + }); +}); diff --git a/src/pages/laboratory/strategyFormRules.ts b/src/pages/laboratory/strategyFormRules.ts new file mode 100644 index 0000000..975827d --- /dev/null +++ b/src/pages/laboratory/strategyFormRules.ts @@ -0,0 +1,61 @@ +import type { StrategyRule, StrategyRuleKind } from '../../domain/strategy/Strategy.ts'; + +export interface RuleConfigState { + enabled: boolean; + weight: number; + min?: number; + max?: number; + oddCount?: number; +} + +export type RulesState = Record; + +export const RULE_KINDS: StrategyRuleKind[] = [ + 'number-frequency', + 'above-31', + 'repeat-from-previous', + 'recency', + 'decade-spread', + 'sum-range', + 'parity-target', + 'star-frequency', +]; + +export const SCORING_KINDS: StrategyRuleKind[] = [ + 'number-frequency', + 'above-31', + 'repeat-from-previous', + 'recency', + 'star-frequency', +]; + +export const RULE_LABELS: Record = { + 'number-frequency': 'Fréquence des numéros (poids négatif = rareté)', + 'above-31': 'Favoriser les numéros > 31', + 'repeat-from-previous': 'Répétition du tirage précédent', + recency: 'Proximité récente (poids négatif = numéros en retard)', + 'decade-spread': 'Répartition par dizaine (signature)', + 'sum-range': 'Plage de somme', + 'parity-target': 'Parité cible', + 'star-frequency': 'Fréquence des étoiles', +}; + +export const DEFAULT_RULES_STATE: RulesState = { + 'number-frequency': { enabled: true, weight: 1 }, + 'above-31': { enabled: false, weight: 1 }, + 'repeat-from-previous': { enabled: false, weight: 1 }, + recency: { enabled: false, weight: 1 }, + 'decade-spread': { enabled: false, weight: 1 }, + 'sum-range': { enabled: false, weight: 1, min: 100, max: 150 }, + 'parity-target': { enabled: false, weight: 1, oddCount: 3 }, + 'star-frequency': { enabled: true, weight: 1 }, +}; + +export const buildRulesFromState = (state: RulesState): StrategyRule[] => + RULE_KINDS.filter((kind) => state[kind].enabled).map((kind): StrategyRule => { + const config = state[kind]; + if (kind === 'sum-range') return { kind, params: { min: config.min, max: config.max } }; + if (kind === 'parity-target') return { kind, params: { oddCount: config.oddCount } }; + if (kind === 'decade-spread') return { kind }; + return { kind, weight: config.weight }; + }); From ddc7f705dcdf053fbdbbfdc3329ff330864d42e3 Mon Sep 17 00:00:00 2001 From: keyobs Date: Mon, 10 Aug 2026 12:16:42 +0200 Subject: [PATCH 31/68] test(laboratory): add component render/interaction test and E2E spec MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit LaboratoryPage.test.tsx: renders without crashing, and running a backtest populates baselines/Monte Carlo/experiment list. E2E spec covers the V2 acceptance criterion "construire une stratégie -> lancer le backtest -> voir les résultats", checking baselines, Monte Carlo, generated grids, and the experiment list end to end against a real browser. Both pass; webServer shuts down cleanly after the run. --- e2e/laboratory-backtest.spec.ts | 17 ++++++++++ src/pages/laboratory/LaboratoryPage.test.tsx | 34 ++++++++++++++++++++ 2 files changed, 51 insertions(+) create mode 100644 e2e/laboratory-backtest.spec.ts create mode 100644 src/pages/laboratory/LaboratoryPage.test.tsx diff --git a/e2e/laboratory-backtest.spec.ts b/e2e/laboratory-backtest.spec.ts new file mode 100644 index 0000000..e70d703 --- /dev/null +++ b/e2e/laboratory-backtest.spec.ts @@ -0,0 +1,17 @@ +import { expect, test } from '@playwright/test'; + +test('construire une stratégie -> lancer le backtest -> voir les résultats', async ({ page }) => { + await page.goto('/laboratory'); + + await page.getByTestId('rule-toggle-above-31').check(); + await page.getByTestId('window-toggle-1').check(); + + await page.getByTestId('run-experiment-button').click(); + + await expect(page.getByTestId('experiment-results')).toBeVisible({ timeout: 15000 }); + await expect(page.getByTestId('baseline-comparison').locator('tbody tr')).toHaveCount(5); + await expect(page.getByTestId('monte-carlo')).toBeVisible(); + await expect(page.getByTestId('generated-grid-row').first()).toBeVisible(); + await expect(page.getByTestId('experiment-row')).toHaveCount(1); + await expect(page.getByTestId('non-predictive-disclaimer')).toBeVisible(); +}); diff --git a/src/pages/laboratory/LaboratoryPage.test.tsx b/src/pages/laboratory/LaboratoryPage.test.tsx new file mode 100644 index 0000000..ca64e3c --- /dev/null +++ b/src/pages/laboratory/LaboratoryPage.test.tsx @@ -0,0 +1,34 @@ +import { afterEach, describe, expect, it, vi } from 'vitest'; +import { screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { renderWithProviders } from '../../shared/renderWithProviders.tsx'; +import { REAL_CSV_TEXT } from '../../shared/testCsvFixture.ts'; +import LaboratoryPage from './LaboratoryPage.tsx'; + +describe('LaboratoryPage', () => { + afterEach(() => { + vi.unstubAllGlobals(); + }); + + it('renders without crashing', () => { + vi.stubGlobal('fetch', () => Promise.resolve(new Response(REAL_CSV_TEXT))); + + renderWithProviders(); + + expect(screen.getByRole('heading', { name: 'Laboratoire de stratégies' })).toBeInTheDocument(); + }); + + it('runs a backtest and shows results with baselines and Monte Carlo', async () => { + vi.stubGlobal('fetch', () => Promise.resolve(new Response(REAL_CSV_TEXT))); + const user = userEvent.setup(); + + renderWithProviders(); + + await user.click(screen.getByTestId('run-experiment-button')); + + await waitFor(() => expect(screen.getByTestId('experiment-results')).toBeInTheDocument(), { timeout: 10000 }); + expect(screen.getByTestId('baseline-comparison').querySelectorAll('tbody tr')).toHaveLength(5); + expect(screen.getByTestId('monte-carlo')).toBeInTheDocument(); + expect(screen.getAllByTestId('experiment-row')).toHaveLength(1); + }); +}); From 65b3633d9c1b6e73c0baf7b74eff58bde3d96943 Mon Sep 17 00:00:00 2001 From: keyobs Date: Mon, 10 Aug 2026 14:31:14 +0200 Subject: [PATCH 32/68] fix(evaluation): replace type=number grid inputs with a proper digit-entry pattern type="number" is a well-known anti-pattern for fixed-length numeric codes (lottery balls here): spinner arrows, scroll-wheel changes the value, inconsistent keyboard behavior. Switched to type="text" + inputMode="numeric" + pattern="[0-9]*" with digit-only filtering, maxLength=2, auto-advance to the next field once a field is full, and backspace-from-empty jumps back a field - the standard code-entry UX (OTP-style). Verified with real page.keyboard.type() keystrokes, not just .fill(): typing "37" "91" "42" auto-advances correctly, and backspace on an empty field moves focus back. --- src/pages/evaluation/EvaluationPage.tsx | 86 +++++++++++++++++++------ 1 file changed, 65 insertions(+), 21 deletions(-) diff --git a/src/pages/evaluation/EvaluationPage.tsx b/src/pages/evaluation/EvaluationPage.tsx index 78473bb..dd9c705 100644 --- a/src/pages/evaluation/EvaluationPage.tsx +++ b/src/pages/evaluation/EvaluationPage.tsx @@ -1,5 +1,5 @@ -import { useMemo, useState } from 'react'; -import type { ChangeEvent } from 'react'; +import { useMemo, useRef, useState } from 'react'; +import type { ChangeEvent, KeyboardEvent } from 'react'; import { useQuery } from '@tanstack/react-query'; import type { Draw } from '../../domain/draw/Draw.ts'; import type { VariationKind } from '../../application/generateVariations.ts'; @@ -29,28 +29,24 @@ const VARIATION_LABELS: Record = { 'anti-share': 'Anti-partage', }; +const onlyDigits = (value: string): string => value.replace(/\D/g, '').slice(0, 2); + const EvaluationPage = () => { const [numberInputs, setNumberInputs] = useState(EMPTY_NUMBERS); const [starInputs, setStarInputs] = useState(EMPTY_STARS); const [grid, setGrid] = useState(null); const [error, setError] = useState(null); + const numberFieldRefs = useRef<(HTMLInputElement | null)[]>([]); + const starFieldRefs = useRef<(HTMLInputElement | null)[]>([]); + const evaluateButtonRef = useRef(null); + const historyQuery = useQuery({ queryKey: ['draws', 'all'], queryFn: () => drawRepository.getAll(), }); const history = historyQuery.data ?? EMPTY_HISTORY; - const handleNumberChange = (index: number) => (event: ChangeEvent) => { - const value = event.target.value; - setNumberInputs((previous) => previous.map((current, i) => (i === index ? value : current))); - }; - - const handleStarChange = (index: number) => (event: ChangeEvent) => { - const value = event.target.value; - setStarInputs((previous) => previous.map((current, i) => (i === index ? value : current))); - }; - const handleEvaluate = () => { const numbers = numberInputs.map(Number); const stars = starInputs.map(Number); @@ -70,6 +66,42 @@ const EvaluationPage = () => { } }; + // Lottery-ball-style entry: plain digit typing, auto-advance once a field + // is full, backspace on an empty field jumps back - avoids the classic + // type="number" pitfalls (spinner arrows, scroll-wheel changes value). + const createDigitFieldHandlers = ( + values: string[], + setValues: (updater: (previous: string[]) => string[]) => void, + refs: (HTMLInputElement | null)[], + nextFieldRef: HTMLElement | null, + ) => ({ + onChange: (index: number) => (event: ChangeEvent) => { + const digits = onlyDigits(event.target.value); + setValues((previous) => previous.map((current, i) => (i === index ? digits : current))); + if (digits.length === 2) { + (refs[index + 1] ?? nextFieldRef)?.focus(); + } + }, + onKeyDown: (index: number) => (event: KeyboardEvent) => { + if (event.key === 'Backspace' && values[index] === '' && index > 0) { + refs[index - 1]?.focus(); + } + }, + }); + + const numberHandlers = createDigitFieldHandlers( + numberInputs, + (updater) => setNumberInputs(updater), + numberFieldRefs.current, + starFieldRefs.current[0] ?? null, + ); + const starHandlers = createDigitFieldHandlers( + starInputs, + (updater) => setStarInputs(updater), + starFieldRefs.current, + evaluateButtonRef.current, + ); + const scores = useMemo( () => (grid && history.length > 0 ? evaluateGrid(grid, history) : null), [grid, history], @@ -90,12 +122,18 @@ const EvaluationPage = () => { {numberInputs.map((value, index) => ( { + numberFieldRefs.current[index] = element; + }} className={styles.numberInput} - type="number" - min={1} - max={50} + type="text" + inputMode="numeric" + pattern="[0-9]*" + maxLength={2} + autoComplete="off" value={value} - onChange={handleNumberChange(index)} + onChange={numberHandlers.onChange(index)} + onKeyDown={numberHandlers.onKeyDown(index)} data-testid={`number-input-${index}`} aria-label={`Numéro ${index + 1}`} /> @@ -106,18 +144,24 @@ const EvaluationPage = () => { {starInputs.map((value, index) => ( { + starFieldRefs.current[index] = element; + }} className={styles.numberInput} - type="number" - min={1} - max={12} + type="text" + inputMode="numeric" + pattern="[0-9]*" + maxLength={2} + autoComplete="off" value={value} - onChange={handleStarChange(index)} + onChange={starHandlers.onChange(index)} + onKeyDown={starHandlers.onKeyDown(index)} data-testid={`star-input-${index}`} aria-label={`Étoile ${index + 1}`} /> ))} - From 5dfb856dcab61ee2ee715b7e585d6b2f73598abc Mon Sep 17 00:00:00 2001 From: keyobs Date: Mon, 10 Aug 2026 15:42:30 +0200 Subject: [PATCH 33/68] style(design): dark-first design pass on nav, /evaluation, /draws, /laboratory Real design tokens in index.css (color, spacing, type scale, radius, shadow) replacing the three ad-hoc variables that were there before. Dark-first (:root is the dark palette directly, overridden under prefers-color-scheme: light) - extends the V4 spec's own committed identity (deep black, scientific/data-art, observatory feel) backward into V1/V2 instead of V1-V3 looking generic and V4 looking like a different product. - Nav: brand mark, pill-style active tab instead of underline. - /evaluation: number/star inputs restyled as proper digit-entry boxes (paired with the earlier type=number -> text+inputMode fix); each of the 4 independent score axes gets its own accent color (structure/originality/temporal/confidence) via ScoreCard's new required accent prop, reinforcing that they're separate axes, not one collapsed verdict. - /draws, /laboratory: sticky table headers, monospace numeric columns, card-style sections, checkbox/chart accent colors wired to the same tokens. /geometry deliberately untouched - it's being rebuilt entirely later. Verified in both dark and light (prefers-color-scheme) - no console errors, all 90 unit/component tests and both E2E specs still pass. --- src/app/AppLayout.module.scss | 49 ++++- src/app/AppLayout.tsx | 21 +- src/index.css | 179 ++++++++++++++++-- src/pages/draws/DrawsPage.module.scss | 43 ++++- src/pages/draws/DrawsPage.tsx | 63 +++--- .../evaluation/EvaluationPage.module.scss | 58 +++++- src/pages/evaluation/EvaluationPage.tsx | 22 ++- src/pages/evaluation/ScoreCard.module.scss | 60 +++++- src/pages/evaluation/ScoreCard.tsx | 20 +- src/pages/laboratory/ExperimentResults.tsx | 148 ++++++++------- .../laboratory/LaboratoryPage.module.scss | 112 +++++++++-- src/pages/laboratory/LaboratoryPage.tsx | 66 ++++--- 12 files changed, 634 insertions(+), 207 deletions(-) diff --git a/src/app/AppLayout.module.scss b/src/app/AppLayout.module.scss index 6c1e0d4..880c769 100644 --- a/src/app/AppLayout.module.scss +++ b/src/app/AppLayout.module.scss @@ -6,33 +6,64 @@ .nav { display: flex; - gap: 1.5rem; - padding: 1rem 1.5rem; + align-items: center; + justify-content: space-between; + gap: var(--space-5); + padding: var(--space-4) var(--space-6); border-bottom: 1px solid var(--border); + background: var(--surface); +} + +.brand { + font-family: var(--mono); + font-size: var(--text-sm); + font-weight: 600; + letter-spacing: 0.06em; + text-transform: uppercase; + color: var(--accent-primary); +} + +.navLinks { + display: flex; + gap: var(--space-2); } .navLink { - color: var(--text); + color: var(--text-muted); text-decoration: none; + font-size: var(--text-sm); font-weight: 600; + padding: var(--space-2) var(--space-3); + border-radius: var(--radius-sm); + transition: color 0.15s ease, background 0.15s ease; &:hover { - text-decoration: underline; + color: var(--text); + background: var(--surface-raised); } } .navLinkActive { - text-decoration: underline; + color: var(--bg); + background: var(--accent-primary); + + &:hover { + color: var(--bg); + background: var(--accent-primary); + opacity: 0.9; + } } .content { flex: 1; - padding: 1.5rem; + padding: var(--space-6) var(--space-7); + max-width: 100%; } .disclaimer { - padding: 0.75rem 1.5rem; + padding: var(--space-4) var(--space-7); border-top: 1px solid var(--border); - font-size: 0.85rem; - opacity: 0.8; + font-size: var(--text-xs); + color: var(--text-muted); + text-align: center; } diff --git a/src/app/AppLayout.tsx b/src/app/AppLayout.tsx index ab0aa2c..bd677b7 100644 --- a/src/app/AppLayout.tsx +++ b/src/app/AppLayout.tsx @@ -12,15 +12,18 @@ const AppLayout = () => { return (
diff --git a/src/index.css b/src/index.css index 0f0ad22..50e4093 100644 --- a/src/index.css +++ b/src/index.css @@ -1,13 +1,49 @@ :root { - --text: #6b6375; - --bg: #fff; - --border: #e5e4e7; + /* Dark-first: this is the product's primary identity (see ADR/V4 intent - + data art / observatory), overridden below for prefers-color-scheme: light. */ + --bg: #0a0c11; + --surface: #12151d; + --surface-raised: #171b25; + --border: #262b38; + --border-strong: #363c4d; + --text: #e6e8ee; + --text-muted: #8b92a5; - --sans: system-ui, 'Segoe UI', Roboto, sans-serif; + --accent-structure: #7dd3fc; + --accent-originality: #fbbf6b; + --accent-temporal: #b9a3fb; + --accent-confidence: #5fe0b5; + --accent-primary: var(--accent-structure); + --danger: #f2707d; + --danger-surface: rgba(242, 112, 125, 0.1); - font: 18px/145% var(--sans); - letter-spacing: 0.18px; - color-scheme: light dark; + --sans: 'Inter', system-ui, 'Segoe UI', Roboto, sans-serif; + --mono: 'JetBrains Mono', ui-monospace, 'SFMono-Regular', Menlo, Consolas, monospace; + + --space-1: 0.25rem; + --space-2: 0.5rem; + --space-3: 0.75rem; + --space-4: 1rem; + --space-5: 1.5rem; + --space-6: 2rem; + --space-7: 3rem; + + --text-xs: 0.8rem; + --text-sm: 0.9rem; + --text-base: 1rem; + --text-lg: 1.15rem; + --text-xl: 1.4rem; + --text-2xl: 1.85rem; + --text-3xl: 2.4rem; + + --radius-sm: 6px; + --radius-md: 10px; + --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3); + --shadow-md: 0 8px 24px rgba(0, 0, 0, 0.35); + + font: var(--text-base) / 1.55 var(--sans); + letter-spacing: 0.1px; + color-scheme: dark light; color: var(--text); background: var(--bg); font-synthesis: none; @@ -16,30 +52,137 @@ -moz-osx-font-smoothing: grayscale; @media (max-width: 1024px) { - font-size: 16px; + font-size: 15px; } } -@media (prefers-color-scheme: dark) { +@media (prefers-color-scheme: light) { :root { - --text: #9ca3af; - --bg: #16171d; - --border: #2e303a; + --bg: #f6f7fb; + --surface: #ffffff; + --surface-raised: #ffffff; + --border: #e2e5ec; + --border-strong: #c9ced9; + --text: #20232e; + --text-muted: #666e80; + + --accent-structure: #3a7ecf; + --accent-originality: #c9791f; + --accent-temporal: #7250c9; + --accent-confidence: #12886b; + --danger: #d4394b; + --danger-surface: rgba(212, 57, 75, 0.08); + + --shadow-sm: 0 1px 2px rgba(20, 24, 38, 0.06); + --shadow-md: 0 8px 24px rgba(20, 24, 38, 0.08); } } +* { + box-sizing: border-box; +} + +body { + margin: 0; +} + #root { - width: 1126px; + width: 1200px; max-width: 100%; margin: 0 auto; - text-align: center; - border-inline: 1px solid var(--border); min-height: 100svh; display: flex; flex-direction: column; - box-sizing: border-box; } -body { - margin: 0; +h1, +h2, +h3 { + font-weight: 650; + letter-spacing: -0.01em; + line-height: 1.25; +} + +h1 { + font-size: var(--text-2xl); +} + +h2 { + font-size: var(--text-xl); +} + +h3 { + font-size: var(--text-lg); +} + +a { + color: var(--accent-primary); +} + +fieldset { + border: 1px solid var(--border); + border-radius: var(--radius-md); + padding: var(--space-4); +} + +legend { + padding: 0 var(--space-2); + color: var(--text-muted); + font-size: var(--text-sm); +} + +label { + font-size: var(--text-sm); + color: var(--text-muted); +} + +input, +select { + font: inherit; + color: var(--text); + background: var(--surface); + border: 1px solid var(--border-strong); + border-radius: var(--radius-sm); + padding: var(--space-2) var(--space-3); + transition: border-color 0.15s ease; +} + +input:focus-visible, +select:focus-visible, +button:focus-visible { + outline: 2px solid var(--accent-primary); + outline-offset: 1px; +} + +input:hover, +select:hover { + border-color: var(--border-strong); +} + +button { + font: inherit; + font-weight: 600; + color: #0a0c11; + background: var(--accent-primary); + border: none; + border-radius: var(--radius-sm); + padding: var(--space-2) var(--space-4); + cursor: pointer; + transition: opacity 0.15s ease; +} + +button:hover { + opacity: 0.88; +} + +button:active { + opacity: 0.75; +} + +table { + border-collapse: collapse; +} + +::selection { + background: color-mix(in srgb, var(--accent-primary) 35%, transparent); } diff --git a/src/pages/draws/DrawsPage.module.scss b/src/pages/draws/DrawsPage.module.scss index 6dcc8fb..8d0914b 100644 --- a/src/pages/draws/DrawsPage.module.scss +++ b/src/pages/draws/DrawsPage.module.scss @@ -1,20 +1,57 @@ .page { display: flex; flex-direction: column; - gap: 1rem; + gap: var(--space-4); text-align: left; } +.toolbar { + display: flex; + align-items: center; + gap: var(--space-3); + color: var(--text-muted); + font-size: var(--text-sm); +} + +.tableWrapper { + border: 1px solid var(--border); + border-radius: var(--radius-md); + overflow: auto; + max-height: 70vh; +} + .table { width: 100%; border-collapse: collapse; - font-size: 0.9rem; + font-size: var(--text-sm); th, td { - padding: 0.4rem 0.6rem; + padding: var(--space-2) var(--space-3); border-bottom: 1px solid var(--border); text-align: left; white-space: nowrap; } + + thead th { + position: sticky; + top: 0; + background: var(--surface-raised); + color: var(--text-muted); + font-size: var(--text-xs); + text-transform: uppercase; + letter-spacing: 0.04em; + font-weight: 600; + } + + tbody tr:hover { + background: var(--surface); + } + + td:nth-child(2), + td:nth-child(3), + td:nth-child(7), + td:nth-child(8) { + font-family: var(--mono); + } } diff --git a/src/pages/draws/DrawsPage.tsx b/src/pages/draws/DrawsPage.tsx index 20edfd6..e40851f 100644 --- a/src/pages/draws/DrawsPage.tsx +++ b/src/pages/draws/DrawsPage.tsx @@ -66,43 +66,44 @@ const DrawsPage = () => { {allDrawsQuery.data && ( <> -

+

{showAll ? `Historique complet (${rows.length} tirages)` : `${rows.length} derniers tirages`} - {' — '}

- - - - - - - - - - - - - - - {rows.map((row) => ( - - - - - - - - - +
+
DateNumérosÉtoilesSommeAmplitudeParitéSignatureDistance au précédent
{row.draw.date}{row.draw.numbers.join(' · ')}{row.draw.stars.join(' · ')}{row.sum}{row.range} - {row.oddCount} impair / {row.evenCount} pair - {row.signature}{row.distanceToPrevious === null ? '—' : row.distanceToPrevious.toFixed(3)}
+ + + + + + + + + + - ))} - -
DateNumérosÉtoilesSommeAmplitudeParitéSignatureDistance au précédent
+ + + {rows.map((row) => ( + + {row.draw.date} + {row.draw.numbers.join(' · ')} + {row.draw.stars.join(' · ')} + {row.sum} + {row.range} + + {row.oddCount} impair / {row.evenCount} pair + + {row.signature} + {row.distanceToPrevious === null ? '—' : row.distanceToPrevious.toFixed(3)} + + ))} + + +
)} diff --git a/src/pages/evaluation/EvaluationPage.module.scss b/src/pages/evaluation/EvaluationPage.module.scss index 80bdf87..c080cd7 100644 --- a/src/pages/evaluation/EvaluationPage.module.scss +++ b/src/pages/evaluation/EvaluationPage.module.scss @@ -1,7 +1,7 @@ .page { display: flex; flex-direction: column; - gap: 1.5rem; + gap: var(--space-6); text-align: left; } @@ -9,20 +9,66 @@ display: flex; flex-wrap: wrap; align-items: flex-end; - gap: 1.5rem; + gap: var(--space-5); } .numberInput { - width: 3.5rem; - margin-right: 0.35rem; + width: 3rem; + height: 3rem; + padding: 0; + text-align: center; + font-family: var(--mono); + font-size: var(--text-lg); + font-weight: 600; + margin-right: var(--space-2); +} + +.starInput { + border-radius: 50%; + border-color: var(--accent-originality); } .scores { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); - gap: 1rem; + gap: var(--space-4); } .error { - color: #d1435b; + color: var(--danger); + background: var(--danger-surface); + border-radius: var(--radius-sm); + padding: var(--space-3) var(--space-4); + display: inline-block; +} + +.variationsNote { + color: var(--text-muted); + font-size: var(--text-sm); +} + +.variationsList { + list-style: none; + padding: 0; + display: flex; + flex-direction: column; + gap: var(--space-2); +} + +.variationItem { + display: flex; + gap: var(--space-3); + align-items: baseline; + font-family: var(--mono); + font-size: var(--text-sm); + padding: var(--space-2) var(--space-3); + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius-sm); +} + +.variationLabel { + font-family: var(--sans); + color: var(--text-muted); + white-space: nowrap; } diff --git a/src/pages/evaluation/EvaluationPage.tsx b/src/pages/evaluation/EvaluationPage.tsx index dd9c705..ee471b6 100644 --- a/src/pages/evaluation/EvaluationPage.tsx +++ b/src/pages/evaluation/EvaluationPage.tsx @@ -147,7 +147,7 @@ const EvaluationPage = () => { ref={(element) => { starFieldRefs.current[index] = element; }} - className={styles.numberInput} + className={`${styles.numberInput} ${styles.starInput}`} type="text" inputMode="numeric" pattern="[0-9]*" @@ -178,10 +178,10 @@ const EvaluationPage = () => {

Lecture : {READING_LABELS[reading]}

- - - - + + + +
)} @@ -189,15 +189,17 @@ const EvaluationPage = () => { {variations.length > 0 && (

Variations

-

+

Ces variations ne sont pas présentées comme plus susceptibles d'être tirées : toutes les grilles valides ont la même probabilité théorique.

-
    +
      {variations.map((variation) => ( -
    • - {VARIATION_LABELS[variation.kind]} : {variation.grid.numbers.join(' · ')} — étoiles{' '} - {variation.grid.stars.join(' · ')} +
    • + {VARIATION_LABELS[variation.kind]} + + {variation.grid.numbers.join(' · ')} — étoiles {variation.grid.stars.join(' · ')} +
    • ))}
    diff --git a/src/pages/evaluation/ScoreCard.module.scss b/src/pages/evaluation/ScoreCard.module.scss index 48a74b0..2dc1a74 100644 --- a/src/pages/evaluation/ScoreCard.module.scss +++ b/src/pages/evaluation/ScoreCard.module.scss @@ -1,20 +1,68 @@ .card { + --card-accent: var(--accent-primary); + border: 1px solid var(--border); - border-radius: 0.5rem; - padding: 1rem; + border-top: 3px solid var(--card-accent); + border-radius: var(--radius-md); + background: var(--surface); + padding: var(--space-4) var(--space-4) var(--space-3); text-align: left; + box-shadow: var(--shadow-sm); +} + +.accentStructure { + --card-accent: var(--accent-structure); +} + +.accentOriginality { + --card-accent: var(--accent-originality); +} + +.accentTemporal { + --card-accent: var(--accent-temporal); +} + +.accentConfidence { + --card-accent: var(--accent-confidence); +} + +.card h3 { + margin: 0 0 var(--space-2); + font-size: var(--text-sm); + color: var(--text-muted); + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.04em; } .value { - font-size: 1.5rem; + font-family: var(--mono); + font-size: var(--text-2xl); font-weight: 700; - margin: 0.25rem 0 0.75rem; + color: var(--card-accent); + margin: 0 0 var(--space-3); +} + +.valueMax { + font-size: var(--text-sm); + color: var(--text-muted); + font-weight: 500; + margin-left: var(--space-1); } .factors { list-style: none; padding: 0; margin: 0; - font-size: 0.85rem; - opacity: 0.85; + display: flex; + flex-direction: column; + gap: var(--space-1); + font-size: var(--text-xs); + color: var(--text-muted); + + li { + display: flex; + justify-content: space-between; + gap: var(--space-2); + } } diff --git a/src/pages/evaluation/ScoreCard.tsx b/src/pages/evaluation/ScoreCard.tsx index 05ff1d6..fb90e66 100644 --- a/src/pages/evaluation/ScoreCard.tsx +++ b/src/pages/evaluation/ScoreCard.tsx @@ -1,22 +1,34 @@ import type { Score } from '../../domain/scoring/evaluateGrid.ts'; import styles from './ScoreCard.module.scss'; +export type ScoreAccent = 'structure' | 'originality' | 'temporal' | 'confidence'; + interface ScoreCardProps { title: string; score: Score; + accent: ScoreAccent; } -const ScoreCard = ({ title, score }: ScoreCardProps) => { +const ACCENT_CLASSES: Record = { + structure: styles.accentStructure, + originality: styles.accentOriginality, + temporal: styles.accentTemporal, + confidence: styles.accentConfidence, +}; + +const ScoreCard = ({ title, score, accent }: ScoreCardProps) => { return ( -
    +

    {title}

    - {score.value} / 100 + {score.value} + /100

      {score.factors.map((factor) => (
    • - {factor.label}: {Math.round(factor.value * 100) / 100} + {factor.label} + {Math.round(factor.value * 100) / 100}
    • ))}
    diff --git a/src/pages/laboratory/ExperimentResults.tsx b/src/pages/laboratory/ExperimentResults.tsx index 18c6634..f6b596d 100644 --- a/src/pages/laboratory/ExperimentResults.tsx +++ b/src/pages/laboratory/ExperimentResults.tsx @@ -15,7 +15,7 @@ const ExperimentResults = ({ experiment, onExport }: ExperimentResultsProps) => const isNoAdvantageDetected = Math.abs(experiment.monteCarlo.strategyPercentile - 50) < 10; return ( -
    +

    Résultats — {experiment.strategy.name}

    {experiment.overfitting.isLikelyOverfit && ( @@ -24,106 +24,118 @@ const ExperimentResults = ({ experiment, onExport }: ExperimentResultsProps) =>

    )} -
    +

    - Test — numéros trouvés en moyenne : {experiment.results.metrics.meanMatchedNumbers.toFixed(2)} / 5, - étoiles : {experiment.results.metrics.meanMatchedStars.toFixed(2)} / 2 + Test — numéros trouvés en moyenne :{' '} + {experiment.results.metrics.meanMatchedNumbers.toFixed(2)} / 5, étoiles :{' '} + {experiment.results.metrics.meanMatchedStars.toFixed(2)} / 2

    -

    Meilleur rang obtenu sur la période de test : {experiment.results.metrics.bestPrizeRank ?? 'aucun'}

    - Train : {experiment.overfitting.trainMeanMatchedNumbers.toFixed(2)} — Validation :{' '} - {experiment.overfitting.validationMeanMatchedNumbers.toFixed(2)} — Test :{' '} - {experiment.overfitting.testMeanMatchedNumbers.toFixed(2)} + Meilleur rang obtenu sur la période de test :{' '} + {experiment.results.metrics.bestPrizeRank ?? 'aucun'} +

    +

    + Train : {experiment.overfitting.trainMeanMatchedNumbers.toFixed(2)} — Validation :{' '} + {experiment.overfitting.validationMeanMatchedNumbers.toFixed(2)} — Test :{' '} + {experiment.overfitting.testMeanMatchedNumbers.toFixed(2)}

    Comparaison aux références

    - - - - - - - - - - {experiment.results.baselineComparisons.map((baseline) => ( - - - - +
    +
    RéférenceNuméros moy.Étoiles moy.
    {baseline.baselineName}{baseline.meanMatchedNumbers.toFixed(2)}{baseline.meanMatchedStars.toFixed(2)}
    + + + + + - ))} - -
    RéférenceNuméros moy.Étoiles moy.
    + + + {experiment.results.baselineComparisons.map((baseline) => ( + + {baseline.baselineName} + {baseline.meanMatchedNumbers.toFixed(2)} + {baseline.meanMatchedStars.toFixed(2)} + + ))} + + +

    Simulation Monte Carlo

    - Percentile de la stratégie testée : {experiment.monteCarlo.strategyPercentile.toFixed(1)}e sur{' '} + Percentile de la stratégie testée : {experiment.monteCarlo.strategyPercentile.toFixed(1)}e sur{' '} {experiment.monteCarlo.sampleCount} stratégies aléatoires équivalentes.

    {isNoAdvantageDetected &&

    Conclusion : aucun avantage détectable par rapport au hasard.

    } - - - - - + + + + +

    Comparaison par fenêtre temporelle

    - - - - - - - - - {experiment.windowComparisons.map((comparison) => ( - - - +
    +
    FenêtreNuméros moy.
    {windowLabel(comparison.window)}{comparison.metrics.meanMatchedNumbers.toFixed(2)}
    + + + + - ))} - -
    FenêtreNuméros moy.
    + + + {experiment.windowComparisons.map((comparison) => ( + + {windowLabel(comparison.window)} + {comparison.metrics.meanMatchedNumbers.toFixed(2)} + + ))} + + +

    Grilles générées (période de test)

    - - - - - - - - - - - {experiment.results.generatedGrids.map((generated) => ( - - - - - +
    +
    DateGrille proposéeÉtoilesNuméros trouvés
    {generated.date}{generated.grid.numbers.join(' · ')}{generated.grid.stars.join(' · ')}{generated.score}
    + + + + + + - ))} - -
    DateGrille proposéeÉtoilesNuméros trouvés
    + + + {experiment.results.generatedGrids.map((generated) => ( + + {generated.date} + {generated.grid.numbers.join(' · ')} + {generated.grid.stars.join(' · ')} + {generated.score} + + ))} + + +
    - +

    + +

); }; diff --git a/src/pages/laboratory/LaboratoryPage.module.scss b/src/pages/laboratory/LaboratoryPage.module.scss index 9c34a89..63bc5b2 100644 --- a/src/pages/laboratory/LaboratoryPage.module.scss +++ b/src/pages/laboratory/LaboratoryPage.module.scss @@ -1,51 +1,135 @@ .page { display: flex; flex-direction: column; - gap: 2rem; + gap: var(--space-6); text-align: left; } +.intro { + color: var(--text-muted); + font-size: var(--text-lg); + max-width: 42rem; +} + +.section { + display: flex; + flex-direction: column; + gap: var(--space-4); + border: 1px solid var(--border); + border-radius: var(--radius-md); + background: var(--surface); + padding: var(--space-5); +} + .ruleRow { display: flex; align-items: center; - gap: 0.75rem; - padding: 0.25rem 0; -} + gap: var(--space-3); + padding: var(--space-2) 0; + border-bottom: 1px solid var(--border); -.ruleRow input[type='number'] { - width: 4.5rem; + &:last-child { + border-bottom: none; + } + + label { + display: flex; + align-items: center; + gap: var(--space-2); + color: var(--text); + font-size: var(--text-sm); + flex: 1; + } + + input[type='checkbox'] { + accent-color: var(--accent-primary); + width: 1rem; + height: 1rem; + } + + input[type='number'] { + width: 4.5rem; + font-family: var(--mono); + } } .formRow { display: flex; flex-wrap: wrap; - gap: 1.5rem; + gap: var(--space-5); align-items: flex-end; } .windowChecks { display: flex; flex-wrap: wrap; - gap: 1rem; + gap: var(--space-4); + + label { + display: flex; + align-items: center; + gap: var(--space-1); + color: var(--text); + } + + input { + accent-color: var(--accent-primary); + } +} + +.tableWrapper { + border: 1px solid var(--border); + border-radius: var(--radius-md); + overflow: auto; + max-height: 60vh; } .table { width: 100%; border-collapse: collapse; - font-size: 0.9rem; + font-size: var(--text-sm); th, td { - padding: 0.4rem 0.6rem; + padding: var(--space-2) var(--space-3); border-bottom: 1px solid var(--border); text-align: left; white-space: nowrap; } + + thead th { + position: sticky; + top: 0; + background: var(--surface-raised); + color: var(--text-muted); + font-size: var(--text-xs); + text-transform: uppercase; + letter-spacing: 0.04em; + font-weight: 600; + } + + tbody tr:hover { + background: var(--surface-raised); + } } .warning { - border: 1px solid #d1435b; - border-radius: 0.5rem; - padding: 0.75rem 1rem; - color: #d1435b; + border: 1px solid var(--danger); + background: var(--danger-surface); + color: var(--danger); + border-radius: var(--radius-md); + padding: var(--space-3) var(--space-4); +} + +.metrics { + display: flex; + flex-direction: column; + gap: var(--space-1); + color: var(--text-muted); + font-size: var(--text-sm); + + strong { + color: var(--text); + font-family: var(--mono); + } } diff --git a/src/pages/laboratory/LaboratoryPage.tsx b/src/pages/laboratory/LaboratoryPage.tsx index 8dc4c74..017f34f 100644 --- a/src/pages/laboratory/LaboratoryPage.tsx +++ b/src/pages/laboratory/LaboratoryPage.tsx @@ -85,9 +85,11 @@ const LaboratoryPage = () => { return (

Laboratoire de stratégies

-

Cette hypothèse fait-elle mieux qu'une stratégie de référence lorsqu'on la teste correctement ?

+

+ Cette hypothèse fait-elle mieux qu'une stratégie de référence lorsqu'on la teste correctement ? +

-
+

Construire une stratégie

-
+

Configuration de l'expérience

{experiments.length > 0 && ( -
+

Expériences de cette session ({experiments.length})

- - - - - - - - - - - {experiments.map((experiment) => ( - - - - - - +
+
StratégieSeedNuméros moy. (test)Percentile Monte Carlo -
{experiment.strategy.name}{experiment.seed}{experiment.results.metrics.meanMatchedNumbers.toFixed(2)}{experiment.monteCarlo.strategyPercentile.toFixed(0)}e - -
+ + + + + + + - ))} - -
StratégieSeedNuméros moy. (test)Percentile Monte Carlo
+ + + {experiments.map((experiment) => ( + + {experiment.strategy.name} + {experiment.seed} + {experiment.results.metrics.meanMatchedNumbers.toFixed(2)} + {experiment.monteCarlo.strategyPercentile.toFixed(0)}e + + + + + ))} + + +
)} From 49911f4bf17a5982c6650480511bfcd66e550a42 Mon Sep 17 00:00:00 2001 From: keyobs Date: Mon, 10 Aug 2026 15:49:31 +0200 Subject: [PATCH 34/68] feat(discovery): add train-only normalization with traceable exclusions fitNormalization computes per-feature mean/stdDev on a reference vector set and returns a frozen model; applyNormalization transforms any vectors (including ones never seen during fit) against those frozen stats - never refitting on new data is what stops a future "evaluate one new grid" consumer from leaking into its own normalization. Excludes near-constant features and features >0.95 correlated with an earlier-kept one, both reasons traceable in excludedFeatures. --- .../discovery/normalizeFeatures.test.ts | 78 ++++++++++++++++ src/domain/discovery/normalizeFeatures.ts | 93 +++++++++++++++++++ 2 files changed, 171 insertions(+) create mode 100644 src/domain/discovery/normalizeFeatures.test.ts create mode 100644 src/domain/discovery/normalizeFeatures.ts diff --git a/src/domain/discovery/normalizeFeatures.test.ts b/src/domain/discovery/normalizeFeatures.test.ts new file mode 100644 index 0000000..33aa272 --- /dev/null +++ b/src/domain/discovery/normalizeFeatures.test.ts @@ -0,0 +1,78 @@ +import { describe, expect, it } from 'vitest'; +import { readFileSync } from 'node:fs'; +import { resolve } from 'node:path'; +import { parseFdjCsv } from '../../infrastructure/csv/parseFdjCsv.ts'; +import { extractFeatures, FEATURE_KEYS } from '../features/FeatureExtractor.ts'; +import { applyNormalization, fitNormalization } from './normalizeFeatures.ts'; + +const REAL_CSV_PATH = resolve(process.cwd(), 'public/results/euromillions_202002.csv'); +const draws = parseFdjCsv(readFileSync(REAL_CSV_PATH, 'utf8')); +const realVectors = draws.map((draw) => extractFeatures({ numbers: draw.numbers, stars: draw.stars })); + +const vectorOf = (overrides: Record) => ({ + values: Object.fromEntries(FEATURE_KEYS.map((key) => [key, overrides[key] ?? 0])), +}); + +describe('fitNormalization', () => { + it('excludes a feature that never varies (near-constant)', () => { + const vectors = [ + vectorOf({ sum: 10 }), + vectorOf({ sum: 20 }), + vectorOf({ sum: 30 }), + // clusterSizeMax constant across all rows here (default 0) + ]; + + const model = fitNormalization(vectors); + + expect(model.featureKeys).not.toContain('clusterSizeMax'); + expect(model.excludedFeatures).toContainEqual({ key: 'clusterSizeMax', reason: 'near-constant' }); + }); + + it('excludes a feature that is perfectly redundant with an earlier one', () => { + const vectors = [1, 2, 3, 4, 5].map((n) => vectorOf({ sum: n, range: n * 2 })); + + const model = fitNormalization(vectors); + + expect(model.featureKeys).toContain('sum'); + expect(model.featureKeys).not.toContain('range'); + expect(model.excludedFeatures).toContainEqual({ key: 'range', reason: 'redundant', correlatedWith: 'sum' }); + }); + + it('produces a model whose stats reflect the real historical dataset', () => { + const model = fitNormalization(realVectors); + + expect(model.featureKeys.length).toBeGreaterThan(0); + expect(model.featureKeys.length).toBeLessThanOrEqual(FEATURE_KEYS.length); + for (const key of model.featureKeys) { + expect(Number.isFinite(model.means[key])).toBe(true); + expect(model.stdDevs[key]).toBeGreaterThan(0); + } + }); +}); + +describe('applyNormalization', () => { + it('transforms a new, unseen point using the fitted (frozen) stats, not the new point\'s own', () => { + const model = fitNormalization(realVectors); + const unseenPoint = vectorOf({ sum: 999 }); // wildly different from training data + + const [normalized] = applyNormalization([unseenPoint], model); + const sumIndex = model.featureKeys.indexOf('sum'); + + // Standardized against the training mean/stddev, so a far-off raw value + // produces a large z-score - not a value near 0, which is what it would + // be if normalization were (wrongly) refit on this single new point. + expect(Math.abs(normalized[sumIndex])).toBeGreaterThan(3); + }); + + it('produces one row per input vector, with one column per surviving feature', () => { + const model = fitNormalization(realVectors); + + const normalized = applyNormalization(realVectors.slice(0, 5), model); + + expect(normalized).toHaveLength(5); + for (const row of normalized) { + expect(row).toHaveLength(model.featureKeys.length); + expect(row.every((value) => Number.isFinite(value))).toBe(true); + } + }); +}); diff --git a/src/domain/discovery/normalizeFeatures.ts b/src/domain/discovery/normalizeFeatures.ts new file mode 100644 index 0000000..d84d835 --- /dev/null +++ b/src/domain/discovery/normalizeFeatures.ts @@ -0,0 +1,93 @@ +import type { FeatureVector } from '../features/FeatureExtractor.ts'; +import { FEATURE_KEYS } from '../features/FeatureExtractor.ts'; + +export interface ExcludedFeature { + key: string; + reason: 'near-constant' | 'redundant'; + correlatedWith?: string; +} + +export interface NormalizationModel { + featureKeys: string[]; + excludedFeatures: ExcludedFeature[]; + means: Record; + stdDevs: Record; +} + +const NEAR_CONSTANT_STD_THRESHOLD = 1e-9; +const REDUNDANCY_CORRELATION_THRESHOLD = 0.95; + +const mean = (values: number[]): number => values.reduce((acc, value) => acc + value, 0) / values.length; + +const standardDeviation = (values: number[], average: number): number => + Math.sqrt(mean(values.map((value) => (value - average) ** 2))); + +const pearsonCorrelation = (a: number[], b: number[]): number => { + const meanA = mean(a); + const meanB = mean(b); + let numerator = 0; + let denomA = 0; + let denomB = 0; + + for (let i = 0; i < a.length; i += 1) { + const da = a[i] - meanA; + const db = b[i] - meanB; + numerator += da * db; + denomA += da * da; + denomB += db * db; + } + + const denom = Math.sqrt(denomA * denomB); + return denom === 0 ? 0 : numerator / denom; +}; + +/** + * Fit once on the reference dataset (means/stdDevs frozen here); never + * refit when transforming new points later - that's what keeps a future + * "evaluate one new grid against the discovered structure" consumer from + * leaking that point's own value into its own normalization. + */ +export const fitNormalization = (trainVectors: FeatureVector[]): NormalizationModel => { + const excludedFeatures: ExcludedFeature[] = []; + const means: Record = {}; + const stdDevs: Record = {}; + const columnsByKey: Record = {}; + + for (const key of FEATURE_KEYS) { + const column = trainVectors.map((vector) => vector.values[key]); + const average = mean(column); + means[key] = average; + stdDevs[key] = standardDeviation(column, average); + columnsByKey[key] = column; + } + + const afterConstantCheck = FEATURE_KEYS.filter((key) => { + if (stdDevs[key] < NEAR_CONSTANT_STD_THRESHOLD) { + excludedFeatures.push({ key, reason: 'near-constant' }); + return false; + } + return true; + }); + + const survivors: string[] = []; + for (const key of afterConstantCheck) { + const redundantWith = survivors.find( + (kept) => Math.abs(pearsonCorrelation(columnsByKey[key], columnsByKey[kept])) > REDUNDANCY_CORRELATION_THRESHOLD, + ); + if (redundantWith) { + excludedFeatures.push({ key, reason: 'redundant', correlatedWith: redundantWith }); + } else { + survivors.push(key); + } + } + + return { featureKeys: survivors, excludedFeatures, means, stdDevs }; +}; + +export const applyNormalization = (vectors: FeatureVector[], model: NormalizationModel): number[][] => + vectors.map((vector) => + model.featureKeys.map((key) => { + const sd = model.stdDevs[key]; + return sd === 0 ? 0 : (vector.values[key] - model.means[key]) / sd; + }), + ); From a02acb728905f7a8a7b5a50d8b11108d330a7a4c Mon Sep 17 00:00:00 2001 From: keyobs Date: Mon, 10 Aug 2026 16:27:08 +0200 Subject: [PATCH 35/68] feat(discovery): upgrade PCA to expose explained variance and feature contributions reduceDimensionsPca runs ml-pca over already-normalized rows (scale: false, since normalizeFeatures already standardized them) and returns PC1-3 coordinates, explained variance per component (already proportional from ml-pca, decreasing PC1>=PC2>=PC3), and per-feature loadings on each component - replacing the spike's bare PCA call that exposed none of this. Verified getLoadings()'s orientation directly against ml-pca's source (transpose of the eigenvector matrix: rows are components, columns are features) rather than assuming it. --- src/domain/discovery/reduceDimensions.test.ts | 45 +++++++++++++++++++ src/domain/discovery/reduceDimensions.ts | 45 +++++++++++++++++++ 2 files changed, 90 insertions(+) create mode 100644 src/domain/discovery/reduceDimensions.test.ts create mode 100644 src/domain/discovery/reduceDimensions.ts diff --git a/src/domain/discovery/reduceDimensions.test.ts b/src/domain/discovery/reduceDimensions.test.ts new file mode 100644 index 0000000..8d9e736 --- /dev/null +++ b/src/domain/discovery/reduceDimensions.test.ts @@ -0,0 +1,45 @@ +import { readFileSync } from 'node:fs'; +import { resolve } from 'node:path'; +import { describe, expect, it } from 'vitest'; +import { parseFdjCsv } from '../../infrastructure/csv/parseFdjCsv.ts'; +import { extractFeatures } from '../features/FeatureExtractor.ts'; +import { applyNormalization, fitNormalization } from './normalizeFeatures.ts'; +import { reduceDimensionsPca } from './reduceDimensions.ts'; + +const REAL_CSV_PATH = resolve(process.cwd(), 'public/results/euromillions_202002.csv'); +const draws = parseFdjCsv(readFileSync(REAL_CSV_PATH, 'utf8')); +const vectors = draws.map((draw) => extractFeatures({ numbers: draw.numbers, stars: draw.stars })); +const normalizationModel = fitNormalization(vectors); +const normalizedRows = applyNormalization(vectors, normalizationModel); + +describe('reduceDimensionsPca', () => { + it('produces one 3D coordinate per input row', () => { + const { coordinates } = reduceDimensionsPca(normalizedRows, normalizationModel.featureKeys); + + expect(coordinates).toHaveLength(vectors.length); + for (const coordinate of coordinates) { + expect(Number.isFinite(coordinate.x)).toBe(true); + expect(Number.isFinite(coordinate.y)).toBe(true); + expect(Number.isFinite(coordinate.z)).toBe(true); + } + }); + + it('reports explained variance as decreasing proportions summing to <= 1', () => { + const { explainedVariance } = reduceDimensionsPca(normalizedRows, normalizationModel.featureKeys); + + expect(explainedVariance).toHaveLength(3); + expect(explainedVariance[0]).toBeGreaterThanOrEqual(explainedVariance[1]); + expect(explainedVariance[1]).toBeGreaterThanOrEqual(explainedVariance[2]); + expect(explainedVariance.reduce((acc, v) => acc + v, 0)).toBeLessThanOrEqual(1.0001); + }); + + it('reports one contribution (loading) entry per surviving feature', () => { + const { featureContributions } = reduceDimensionsPca(normalizedRows, normalizationModel.featureKeys); + + expect(featureContributions).toHaveLength(normalizationModel.featureKeys.length); + for (const contribution of featureContributions) { + expect(normalizationModel.featureKeys).toContain(contribution.feature); + expect(Number.isFinite(contribution.pc1)).toBe(true); + } + }); +}); diff --git a/src/domain/discovery/reduceDimensions.ts b/src/domain/discovery/reduceDimensions.ts new file mode 100644 index 0000000..8f90a5e --- /dev/null +++ b/src/domain/discovery/reduceDimensions.ts @@ -0,0 +1,45 @@ +import { PCA } from 'ml-pca'; + +export interface Coordinates3D { + x: number; + y: number; + z: number; +} + +export interface FeatureContribution { + feature: string; + pc1: number; + pc2: number; + pc3: number; +} + +export interface DimensionalityReduction { + coordinates: Coordinates3D[]; + explainedVariance: number[]; + featureContributions: FeatureContribution[]; +} + +/** + * Expects already-normalized rows (see normalizeFeatures.ts) - scale: false + * because those rows are already zero-mean/unit-variance; PCA only centers + * (a no-op here, kept for robustness) rather than re-scaling. + */ +export const reduceDimensionsPca = (normalizedRows: number[][], featureKeys: string[]): DimensionalityReduction => { + const pca = new PCA(normalizedRows, { center: true, scale: false }); + const projected = pca.predict(normalizedRows, { nComponents: 3 }).to2DArray(); + const coordinates = projected.map(([x, y, z]) => ({ x, y: y ?? 0, z: z ?? 0 })); + + // getExplainedVariance() already returns proportions of total variance (sum to 1). + const explainedVariance = pca.getExplainedVariance().slice(0, 3); + + const loadings = pca.getLoadings().to2DArray(); + const [pc1Loadings, pc2Loadings, pc3Loadings] = loadings; + const featureContributions = featureKeys.map((feature, index) => ({ + feature, + pc1: pc1Loadings?.[index] ?? 0, + pc2: pc2Loadings?.[index] ?? 0, + pc3: pc3Loadings?.[index] ?? 0, + })); + + return { coordinates, explainedVariance, featureContributions }; +}; From 3a488fb952b20216cb87e80de92ed8dae58c1bb8 Mon Sep 17 00:00:00 2001 From: keyobs Date: Mon, 10 Aug 2026 17:02:12 +0200 Subject: [PATCH 36/68] feat(discovery): add K-Means clustering with silhouette k-selection and bootstrap stability fitKMeansWithSilhouette tries every candidate k, keeps the one with the best average silhouette score (verified reproducible by seed, using ml-kmeans's own seed support down through its kmeans++ initialization - confirmed directly in its source rather than assumed). computeBootstrapStability resamples with replacement, refits K-Means on the resample, and checks whether pairs originally co-clustered stay co-clustered under the resampled model (nearest() against the bootstrap centroids) - a simplified co-clustering agreement rate rather than a full Adjusted Rand Index, capped at 200 sampled pairs per cluster to keep cost bounded regardless of cluster size. Measured the realistic full-history cost up front given the V2 Monte Carlo lesson: k=2..8 selection + 30 bootstrap iterations over all 680 draws runs in ~170ms, no optimization needed this time. --- package.json | 1 + pnpm-lock.yaml | 65 ++++++++ src/domain/discovery/kmeansClustering.test.ts | 83 ++++++++++ src/domain/discovery/kmeansClustering.ts | 150 ++++++++++++++++++ 4 files changed, 299 insertions(+) create mode 100644 src/domain/discovery/kmeansClustering.test.ts create mode 100644 src/domain/discovery/kmeansClustering.ts diff --git a/package.json b/package.json index 4604027..cdeabbf 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "@react-three/fiber": "^9.7.0", "@tanstack/react-query": "^5.101.4", "d3": "^7.9.0", + "ml-kmeans": "^7.0.1", "ml-pca": "^4.1.1", "react": "^19.2.8", "react-dom": "^19.2.8", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 05ad0c9..8d36765 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -20,6 +20,9 @@ importers: d3: specifier: ^7.9.0 version: 7.9.0 + ml-kmeans: + specifier: ^7.0.1 + version: 7.0.1 ml-pca: specifier: ^4.1.1 version: 4.1.1 @@ -930,6 +933,9 @@ packages: bidi-js@1.0.3: resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==} + binary-search@1.3.6: + resolution: {integrity: sha512-nbE1WxOTTrUWIfsfZ4aHGYu5DOuNkbxGokjV6Z2kxfJK3uaAb8zNK1muzOeipoLHZjInT4Br88BHpzevc681xA==} + buffer@6.0.3: resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} @@ -947,6 +953,9 @@ packages: resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} engines: {node: '>=18'} + cheminfo-types@1.15.0: + resolution: {integrity: sha512-shv45WN2u0yN9EHH1bisNrv+fy4Cw+eLM5lOoriP67mePrwbHZ1kJqg90C8GEU7K1A8gJsicEoVZHcuBbuul/w==} + chokidar@5.0.0: resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==} engines: {node: '>= 20.19.0'} @@ -1233,6 +1242,9 @@ packages: fflate@0.8.3: resolution: {integrity: sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==} + fft.js@4.0.4: + resolution: {integrity: sha512-f9c00hphOgeQTlDyavwTtu6RiK8AIFjD6+jvXkNkpeQ7rirK3uFWVpalkoS4LAwbdX7mfZ8aoBfFVQX1Re/8aw==} + fsevents@2.3.2: resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -1486,12 +1498,30 @@ packages: ml-array-rescale@2.0.0: resolution: {integrity: sha512-2GGtKfSno94/kIloWGvpp/U5Q5vLvLrza+SAaGsLeo6Xj4mEbA6Gqx+oTfZFkxnd1grT2X007HfJNs3T5BsiVg==} + ml-distance-euclidean@3.0.1: + resolution: {integrity: sha512-jEEu/1a73ArPmIiwOzrcah6TfhtV19dCKnnM7JvdR2xTzyVJFGgIIR78Vg8Pl9z2NVeSRoFOpzc0910sPMINsA==} + + ml-kmeans@7.0.1: + resolution: {integrity: sha512-yrBTntVdMf9gdMQnHBY1HQ5IhFdTAW8hcUhagiktqmx3lwR8VC2ONyP9oJfaVzMkummOd/XKgT64E2DgJlmfrQ==} + ml-matrix@6.15.0: resolution: {integrity: sha512-wFa1v6KP8bKp+fj0nYmRs1Pb5K4zRkXGKsOvLinvILENFIADncm4XlOI+S1M7yuACMGfI6cfk0IifDgd4j5xmw==} + ml-nearest-vector@3.0.1: + resolution: {integrity: sha512-7UQ60sPS1Dpe6N6zuEsaL3nrUGHkaiSmjWcQHfMYFfV1dgp6TiPtv3ZtdLHwpf1jS0KQDSkw9pycJO1gYBiEoA==} + ml-pca@4.1.1: resolution: {integrity: sha512-HQwswMK1dObj+ppk3EPcQMR2djWK0Cri8mAFd2nITtXHkLfO4DBBsEtiCT5KiR+2e3hQjp+lI0UyqZpdf04AlQ==} + ml-random@2.0.0: + resolution: {integrity: sha512-/ziHm0qyvsvwRy0y+EhmhTKbvq+9ghqf9UYSexKeyx8pp+9avDBLctYaopdTaJ1HU4qnJPEy7XH0CVeh9tHIgQ==} + + ml-spectra-processing@14.33.0: + resolution: {integrity: sha512-zg71v6exRvPpzDh9SiN5iNAIG8yTFpzfJrRCRSb/hWf5gMsik7q60DkO5ew4knR7xF4DbjYmNWeqLq8gWcQdEQ==} + + ml-xsadd@3.0.1: + resolution: {integrity: sha512-Fz2q6dwgzGM8wYKGArTUTZDGa4lQFA2Vi6orjGeTVRy22ZnQFKlJuwS9n8NRviqz1KHAHAzdKJwbnYhdo38uYg==} + nanoid@3.3.17: resolution: {integrity: sha512-xQLf0A3HOMlgHq0n247/LRuAOYmB7dXJ/DvAxGvsSBij45XtBSmQycu+F8ODbHwns/XyFZagyL1+J0Offw1E0g==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} @@ -2739,6 +2769,8 @@ snapshots: dependencies: require-from-string: 2.0.2 + binary-search@1.3.6: {} + buffer@6.0.3: dependencies: base64-js: 1.5.1 @@ -2752,6 +2784,8 @@ snapshots: chai@6.2.2: {} + cheminfo-types@1.15.0: {} + chokidar@5.0.0: dependencies: readdirp: 5.1.1 @@ -3035,6 +3069,8 @@ snapshots: fflate@0.8.3: {} + fft.js@4.0.4: {} + fsevents@2.3.2: optional: true @@ -3249,15 +3285,44 @@ snapshots: ml-array-max: 2.0.0 ml-array-min: 2.0.0 + ml-distance-euclidean@3.0.1: {} + + ml-kmeans@7.0.1: + dependencies: + ml-distance-euclidean: 3.0.1 + ml-matrix: 6.15.0 + ml-nearest-vector: 3.0.1 + ml-random: 2.0.0 + ml-spectra-processing: 14.33.0 + ml-matrix@6.15.0: dependencies: is-any-array: 3.0.0 ml-array-rescale: 2.0.0 + ml-nearest-vector@3.0.1: + dependencies: + ml-distance-euclidean: 3.0.1 + ml-pca@4.1.1: dependencies: ml-matrix: 6.15.0 + ml-random@2.0.0: + dependencies: + ml-xsadd: 3.0.1 + + ml-spectra-processing@14.33.0: + dependencies: + binary-search: 1.3.6 + cheminfo-types: 1.15.0 + fft.js: 4.0.4 + is-any-array: 3.0.0 + ml-matrix: 6.15.0 + ml-xsadd: 3.0.1 + + ml-xsadd@3.0.1: {} + nanoid@3.3.17: {} node-addon-api@7.1.1: diff --git a/src/domain/discovery/kmeansClustering.test.ts b/src/domain/discovery/kmeansClustering.test.ts new file mode 100644 index 0000000..8af905f --- /dev/null +++ b/src/domain/discovery/kmeansClustering.test.ts @@ -0,0 +1,83 @@ +import { readFileSync } from 'node:fs'; +import { resolve } from 'node:path'; +import { describe, expect, it } from 'vitest'; +import { parseFdjCsv } from '../../infrastructure/csv/parseFdjCsv.ts'; +import { extractFeatures } from '../features/FeatureExtractor.ts'; +import { applyNormalization, fitNormalization } from './normalizeFeatures.ts'; +import { reduceDimensionsPca } from './reduceDimensions.ts'; +import { + computeBootstrapStability, + computeSilhouetteScore, + fitKMeansWithSilhouette, +} from './kmeansClustering.ts'; + +const REAL_CSV_PATH = resolve(process.cwd(), 'public/results/euromillions_202002.csv'); +const draws = parseFdjCsv(readFileSync(REAL_CSV_PATH, 'utf8')); +const vectors = draws.map((draw) => extractFeatures({ numbers: draw.numbers, stars: draw.stars })); +const normalizationModel = fitNormalization(vectors); +const normalizedRows = applyNormalization(vectors, normalizationModel); +const pcaCoordinates = reduceDimensionsPca(normalizedRows, normalizationModel.featureKeys).coordinates; +const pcaRows = pcaCoordinates.map((c) => [c.x, c.y, c.z]); + +describe('computeSilhouetteScore', () => { + it('returns 0 for a single cluster (no separation to measure)', () => { + expect(computeSilhouetteScore(pcaRows, pcaRows.map(() => 0))).toBe(0); + }); + + it('returns a score in [-1, 1] for a real clustering', () => { + const { clusters } = { clusters: pcaRows.map((_, i) => i % 3) }; + const score = computeSilhouetteScore(pcaRows, clusters); + expect(score).toBeGreaterThanOrEqual(-1); + expect(score).toBeLessThanOrEqual(1); + }); +}); + +describe('fitKMeansWithSilhouette', () => { + it('picks a k from the candidates and assigns every row to a cluster', () => { + const result = fitKMeansWithSilhouette(pcaRows, [2, 3, 4, 5], 42); + + expect([2, 3, 4, 5]).toContain(result.k); + expect(result.clusterIds).toHaveLength(pcaRows.length); + expect(result.candidateScores).toHaveLength(4); + expect(new Set(result.clusterIds).size).toBe(result.k); + }); + + it('is reproducible for the same seed', () => { + const a = fitKMeansWithSilhouette(pcaRows, [3], 7); + const b = fitKMeansWithSilhouette(pcaRows, [3], 7); + + expect(a).toEqual(b); + }); +}); + +describe('computeBootstrapStability', () => { + it('reports a stability score in [0, 1] for every cluster', () => { + const fit = fitKMeansWithSilhouette(pcaRows, [3], 42); + + const stabilities = computeBootstrapStability(pcaRows, fit.clusterIds, fit.k, 10, 1); + + expect(stabilities).toHaveLength(fit.k); + for (const { stability } of stabilities) { + expect(stability).toBeGreaterThanOrEqual(0); + expect(stability).toBeLessThanOrEqual(1); + } + }); + + it('rates a single well-separated group of identical points as fully stable', () => { + const rows = [ + [0, 0, 0], + [0, 0, 0], + [0, 0, 0], + [10, 10, 10], + [10, 10, 10], + [10, 10, 10], + ]; + const clusterIds = [0, 0, 0, 1, 1, 1]; + + const stabilities = computeBootstrapStability(rows, clusterIds, 2, 10, 1); + + for (const { stability } of stabilities) { + expect(stability).toBeCloseTo(1, 1); + } + }); +}); diff --git a/src/domain/discovery/kmeansClustering.ts b/src/domain/discovery/kmeansClustering.ts new file mode 100644 index 0000000..3c69a90 --- /dev/null +++ b/src/domain/discovery/kmeansClustering.ts @@ -0,0 +1,150 @@ +import { kmeans } from 'ml-kmeans'; +import { createSeededRandom } from '../random/seededRandom.ts'; + +const squaredEuclidean = (a: number[], b: number[]): number => + a.reduce((acc, value, index) => acc + (value - b[index]) ** 2, 0); + +const euclidean = (a: number[], b: number[]): number => Math.sqrt(squaredEuclidean(a, b)); + +const mean = (values: number[]): number => values.reduce((acc, value) => acc + value, 0) / values.length; + +/** + * Average silhouette coefficient over all points: for each point, how much + * closer it is (on average) to its own cluster than to the nearest other + * cluster. Close to 1 = well-separated clusters, close to 0 or negative = + * the clustering isn't finding real structure. + */ +export const computeSilhouetteScore = (rows: number[][], clusterIds: number[]): number => { + const n = rows.length; + const uniqueClusters = new Set(clusterIds); + if (n < 2 || uniqueClusters.size < 2) return 0; + + let total = 0; + for (let i = 0; i < n; i += 1) { + const own = clusterIds[i]; + const distancesByCluster: Record = {}; + for (let j = 0; j < n; j += 1) { + if (i === j) continue; + (distancesByCluster[clusterIds[j]] ??= []).push(euclidean(rows[i], rows[j])); + } + + const withinClusterDistances = distancesByCluster[own] ?? []; + const a = withinClusterDistances.length > 0 ? mean(withinClusterDistances) : 0; + const otherClusterMeans = Object.entries(distancesByCluster) + .filter(([clusterId]) => Number(clusterId) !== own) + .map(([, distances]) => mean(distances)); + const b = otherClusterMeans.length > 0 ? Math.min(...otherClusterMeans) : 0; + + total += Math.max(a, b) === 0 ? 0 : (b - a) / Math.max(a, b); + } + + return total / n; +}; + +export interface KMeansCandidateScore { + k: number; + silhouetteScore: number; +} + +export interface KMeansFitResult { + k: number; + clusterIds: number[]; + centroids: number[][]; + silhouetteScore: number; + candidateScores: KMeansCandidateScore[]; +} + +/** Tries every k in `kCandidates`, keeps the one with the best silhouette score. */ +export const fitKMeansWithSilhouette = (rows: number[][], kCandidates: number[], seed: number): KMeansFitResult => { + const candidates = kCandidates.map((k) => { + const result = kmeans(rows, k, { seed, initialization: 'kmeans++' }); + return { k, silhouetteScore: computeSilhouetteScore(rows, result.clusters), result }; + }); + + const best = candidates.reduce((a, b) => (b.silhouetteScore > a.silhouetteScore ? b : a)); + + return { + k: best.k, + clusterIds: best.result.clusters, + centroids: best.result.centroids, + silhouetteScore: best.silhouetteScore, + candidateScores: candidates.map(({ k, silhouetteScore }) => ({ k, silhouetteScore })), + }; +}; + +export interface ClusterStability { + clusterId: number; + stability: number; +} + +const MAX_PAIRS_PER_CLUSTER = 200; + +const samplePairs = (indices: number[], random: () => number): [number, number][] => { + const maxPairs = Math.min(MAX_PAIRS_PER_CLUSTER, Math.floor((indices.length * (indices.length - 1)) / 2)); + const pairs: [number, number][] = []; + const seen = new Set(); + let attempts = 0; + + while (pairs.length < maxPairs && attempts < maxPairs * 20) { + attempts += 1; + const a = indices[Math.floor(random() * indices.length)]; + const b = indices[Math.floor(random() * indices.length)]; + if (a === b) continue; + const key = a < b ? `${a}-${b}` : `${b}-${a}`; + if (seen.has(key)) continue; + seen.add(key); + pairs.push(a < b ? [a, b] : [b, a]); + } + + return pairs; +}; + +/** + * Bootstrap stability: resample rows with replacement, refit K-Means on the + * resample, then check whether pairs of points originally co-clustered + * stay co-clustered under the resampled model. Averaged over + * `bootstrapIterations`. A simplified co-clustering agreement rate rather + * than a full Adjusted Rand Index - cheaper, and enough to flag "this + * family doesn't hold up" per the spec's stability requirement. + */ +export const computeBootstrapStability = ( + rows: number[][], + clusterIds: number[], + k: number, + bootstrapIterations: number, + seed: number, +): ClusterStability[] => { + const random = createSeededRandom(seed); + const n = rows.length; + + const indicesByCluster: Record = {}; + clusterIds.forEach((clusterId, index) => { + (indicesByCluster[clusterId] ??= []).push(index); + }); + + const pairsByCluster: Record = {}; + for (const [clusterId, indices] of Object.entries(indicesByCluster)) { + pairsByCluster[Number(clusterId)] = samplePairs(indices, random); + } + + const agreementCounts: Record = {}; + for (const clusterId of Object.keys(indicesByCluster)) agreementCounts[Number(clusterId)] = 0; + + for (let iteration = 0; iteration < bootstrapIterations; iteration += 1) { + const resampledRows = Array.from({ length: n }, () => rows[Math.floor(random() * n)]); + const bootstrapModel = kmeans(resampledRows, k, { seed: seed + iteration + 1, initialization: 'kmeans++' }); + const assignments = bootstrapModel.nearest(rows); + + for (const [clusterId, pairs] of Object.entries(pairsByCluster)) { + for (const [a, b] of pairs) { + if (assignments[a] === assignments[b]) agreementCounts[Number(clusterId)] += 1; + } + } + } + + return Object.entries(indicesByCluster).map(([clusterId]) => { + const id = Number(clusterId); + const totalChecks = pairsByCluster[id].length * bootstrapIterations; + return { clusterId: id, stability: totalChecks === 0 ? 1 : agreementCounts[id] / totalChecks }; + }); +}; From 9738cd0de01584b3677f3a04fe3f9b1eb2aa47f3 Mon Sep 17 00:00:00 2001 From: keyobs Date: Tue, 11 Aug 2026 09:44:51 +0200 Subject: [PATCH 37/68] feat(discovery): add k-NN density and outlier scoring For each point: mean distance to its k nearest neighbors. Points in a tight cluster get a high densityPercentile (ranked against every other point's mean k-NN distance) and low outlierScore (min-max normalized mean distance); an isolated point gets the reverse. confidence is the inverse coefficient of variation of the k neighbor distances - a tight neighbor shell is a more trustworthy density read than a scattered one. Returns neighbor indices/distances rather than Draw objects - index-to-drawId mapping happens in the DiscoveryModel composition layer, keeping this function testable on plain coordinate arrays. --- .../discovery/densityEvaluation.test.ts | 48 +++++++++++++++ src/domain/discovery/densityEvaluation.ts | 60 +++++++++++++++++++ 2 files changed, 108 insertions(+) create mode 100644 src/domain/discovery/densityEvaluation.test.ts create mode 100644 src/domain/discovery/densityEvaluation.ts diff --git a/src/domain/discovery/densityEvaluation.test.ts b/src/domain/discovery/densityEvaluation.test.ts new file mode 100644 index 0000000..bcc1269 --- /dev/null +++ b/src/domain/discovery/densityEvaluation.test.ts @@ -0,0 +1,48 @@ +import { readFileSync } from 'node:fs'; +import { resolve } from 'node:path'; +import { describe, expect, it } from 'vitest'; +import { parseFdjCsv } from '../../infrastructure/csv/parseFdjCsv.ts'; +import { extractFeatures } from '../features/FeatureExtractor.ts'; +import { applyNormalization, fitNormalization } from './normalizeFeatures.ts'; +import { reduceDimensionsPca } from './reduceDimensions.ts'; +import { computeDensityEvaluations } from './densityEvaluation.ts'; + +describe('computeDensityEvaluations', () => { + it('rates a point inside a tight cluster as denser than an isolated point', () => { + const rows = [ + [0, 0, 0], + [0.1, 0, 0], + [0, 0.1, 0], + [0.1, 0.1, 0], + [50, 50, 50], // far away, alone + ]; + + const evaluations = computeDensityEvaluations(rows, 2); + + expect(evaluations[0].densityPercentile).toBeGreaterThan(evaluations[4].densityPercentile); + expect(evaluations[4].outlierScore).toBeGreaterThan(evaluations[0].outlierScore); + }); + + it('keeps outlierScore and confidence within [0, 1] and reports k nearest neighbors', () => { + const REAL_CSV_PATH = resolve(process.cwd(), 'public/results/euromillions_202002.csv'); + const draws = parseFdjCsv(readFileSync(REAL_CSV_PATH, 'utf8')); + const vectors = draws.map((draw) => extractFeatures({ numbers: draw.numbers, stars: draw.stars })); + const model = fitNormalization(vectors); + const normalizedRows = applyNormalization(vectors, model); + const pcaRows = reduceDimensionsPca(normalizedRows, model.featureKeys).coordinates.map((c) => [c.x, c.y, c.z]); + + const evaluations = computeDensityEvaluations(pcaRows, 10); + + expect(evaluations).toHaveLength(pcaRows.length); + for (const evaluation of evaluations) { + expect(evaluation.outlierScore).toBeGreaterThanOrEqual(0); + expect(evaluation.outlierScore).toBeLessThanOrEqual(1); + expect(evaluation.confidence).toBeGreaterThanOrEqual(0); + expect(evaluation.confidence).toBeLessThanOrEqual(1); + expect(evaluation.densityPercentile).toBeGreaterThanOrEqual(0); + expect(evaluation.densityPercentile).toBeLessThanOrEqual(100); + expect(evaluation.nearestNeighborIndices).toHaveLength(10); + expect(evaluation.nearestNeighborDistances).toHaveLength(10); + } + }); +}); diff --git a/src/domain/discovery/densityEvaluation.ts b/src/domain/discovery/densityEvaluation.ts new file mode 100644 index 0000000..688abd3 --- /dev/null +++ b/src/domain/discovery/densityEvaluation.ts @@ -0,0 +1,60 @@ +const euclidean = (a: number[], b: number[]): number => + Math.sqrt(a.reduce((acc, value, index) => acc + (value - b[index]) ** 2, 0)); + +const mean = (values: number[]): number => values.reduce((acc, value) => acc + value, 0) / values.length; + +const standardDeviation = (values: number[], average: number): number => + Math.sqrt(mean(values.map((value) => (value - average) ** 2))); + +export interface DensityEvaluation { + densityPercentile: number; + outlierScore: number; + confidence: number; + nearestNeighborIndices: number[]; + nearestNeighborDistances: number[]; +} + +/** + * k-NN density: a point in a dense region has small distances to its k + * nearest neighbors; an isolated point has large ones. densityPercentile + * ranks each point against every other point's mean k-NN distance; + * outlierScore is that same distance min-max normalized; confidence is the + * inverse coefficient of variation of the k distances themselves (a tight + * neighbor shell is a more trustworthy density read than a scattered one). + */ +export const computeDensityEvaluations = (rows: number[][], k: number): DensityEvaluation[] => { + const n = rows.length; + + const neighborsOf = rows.map((row, index) => + rows + .map((other, otherIndex) => ({ otherIndex, distance: euclidean(row, other) })) + .filter((entry) => entry.otherIndex !== index) + .sort((a, b) => a.distance - b.distance) + .slice(0, k), + ); + + const meanDistances = neighborsOf.map((neighbors) => mean(neighbors.map((entry) => entry.distance))); + const minDistance = Math.min(...meanDistances); + const maxDistance = Math.max(...meanDistances); + const distanceRange = maxDistance - minDistance || 1; + + return neighborsOf.map((neighbors, index) => { + const meanDistance = meanDistances[index]; + const denserThanCount = meanDistances.filter((distance) => distance > meanDistance).length; + const densityPercentile = n <= 1 ? 100 : (denserThanCount / (n - 1)) * 100; + const outlierScore = (meanDistance - minDistance) / distanceRange; + + const neighborDistances = neighbors.map((entry) => entry.distance); + const stdDev = standardDeviation(neighborDistances, meanDistance); + const coefficientOfVariation = meanDistance === 0 ? 0 : stdDev / meanDistance; + const confidence = Math.max(0, 1 - Math.min(coefficientOfVariation, 1)); + + return { + densityPercentile, + outlierScore, + confidence, + nearestNeighborIndices: neighbors.map((entry) => entry.otherIndex), + nearestNeighborDistances: neighborDistances, + }; + }); +}; From 711de968c806860fb6438c2a4f3025ccca983843 Mon Sep 17 00:00:00 2001 From: keyobs Date: Tue, 11 Aug 2026 10:09:45 +0200 Subject: [PATCH 38/68] feat(discovery): add synthetic null-hypothesis draw generator generateSyntheticDraws produces perfectly random, independent, rule-valid draws (5 distinct 1-50, 2 distinct 1-12), seeded and deterministic. Running the same discovery pipeline against this instead of real history is the null-hypothesis baseline: whatever clustering/density structure shows up here is what pure chance produces, calibrating false positives before trusting structure found in the real dataset. --- .../discovery/generateSyntheticDraws.test.ts | 32 ++++++++++++++ .../discovery/generateSyntheticDraws.ts | 44 +++++++++++++++++++ 2 files changed, 76 insertions(+) create mode 100644 src/domain/discovery/generateSyntheticDraws.test.ts create mode 100644 src/domain/discovery/generateSyntheticDraws.ts diff --git a/src/domain/discovery/generateSyntheticDraws.test.ts b/src/domain/discovery/generateSyntheticDraws.test.ts new file mode 100644 index 0000000..4f5820e --- /dev/null +++ b/src/domain/discovery/generateSyntheticDraws.test.ts @@ -0,0 +1,32 @@ +import { describe, expect, it } from 'vitest'; +import { generateSyntheticDraws } from './generateSyntheticDraws.ts'; + +describe('generateSyntheticDraws', () => { + it('produces the requested count of valid, distinct-dated draws', () => { + const draws = generateSyntheticDraws(200, 42, '2020-01-01'); + + expect(draws).toHaveLength(200); + const dates = new Set(draws.map((draw) => draw.date)); + expect(dates.size).toBe(200); + for (const draw of draws) { + expect(new Set(draw.numbers).size).toBe(5); + expect(new Set(draw.stars).size).toBe(2); + expect(draw.numbers.every((n) => n >= 1 && n <= 50)).toBe(true); + expect(draw.stars.every((s) => s >= 1 && s <= 12)).toBe(true); + } + }); + + it('is deterministic for the same seed', () => { + const a = generateSyntheticDraws(50, 7, '2020-01-01'); + const b = generateSyntheticDraws(50, 7, '2020-01-01'); + + expect(a).toEqual(b); + }); + + it('produces different draws for a different seed', () => { + const a = generateSyntheticDraws(50, 1, '2020-01-01'); + const b = generateSyntheticDraws(50, 2, '2020-01-01'); + + expect(a).not.toEqual(b); + }); +}); diff --git a/src/domain/discovery/generateSyntheticDraws.ts b/src/domain/discovery/generateSyntheticDraws.ts new file mode 100644 index 0000000..f8714e3 --- /dev/null +++ b/src/domain/discovery/generateSyntheticDraws.ts @@ -0,0 +1,44 @@ +import type { Draw } from '../draw/Draw.ts'; +import { parseGrid } from '../grid/Grid.ts'; +import { createSeededRandom } from '../random/seededRandom.ts'; + +const pickDistinct = (count: number, min: number, max: number, random: () => number): number[] => { + const pool = Array.from({ length: max - min + 1 }, (_, i) => min + i); + const picked: number[] = []; + + for (let i = 0; i < count; i += 1) { + const index = Math.floor(random() * pool.length); + picked.push(pool[index]); + pool.splice(index, 1); + } + + return picked; +}; + +/** + * Perfectly random, independent draws respecting EuroMillions rules (5 + * distinct numbers 1-50, 2 distinct stars 1-12) - the null hypothesis: + * whatever clustering/density structure this produces is what pure chance + * looks like, the baseline real history's structure gets compared against. + */ +export const generateSyntheticDraws = (count: number, seed: number, startDate: string): Draw[] => { + const random = createSeededRandom(seed); + const start = new Date(startDate); + + return Array.from({ length: count }, (_, index) => { + const grid = parseGrid({ + numbers: pickDistinct(5, 1, 50, random), + stars: pickDistinct(2, 1, 12, random), + }); + const date = new Date(start); + date.setDate(date.getDate() + index * 3); + + return { + id: `synthetic-${index}`, + date: date.toISOString().slice(0, 10), + numbers: grid.numbers, + stars: grid.stars, + source: 'manual', + } satisfies Draw; + }); +}; From 21e7e9358810effda7e6be1dda0607ee0e4f3189 Mon Sep 17 00:00:00 2001 From: keyobs Date: Tue, 11 Aug 2026 10:45:04 +0200 Subject: [PATCH 39/68] feat(discovery): compose the real DiscoveryModel.fit() -> SpatialEmbedding[] discoverStructure wires normalization -> PCA -> K-Means (+ bootstrap stability) -> k-NN density into the spec's actual DiscoveryModel contract: every SpatialEmbedding now carries a real clusterId, density, outlierScore, and nearestNeighbors, replacing the ADR-0002 spike's stubbed 0/0/undefined/[]. Families get a description built from each cluster's most-deviated features (z-score vs the overall dataset, using a small French phrase lookup for the features that have a natural qualitative reading), a frequency, and a stability flag from the bootstrap. Also runs the identical pipeline against generateSyntheticDraws for the null-hypothesis comparison Verified reproducible by seed and fast enough for a UI action (~300ms per full call including the synthetic run, 680 real draws). --- src/application/discoverStructure.test.ts | 60 ++++++++ src/application/discoverStructure.ts | 177 ++++++++++++++++++++++ 2 files changed, 237 insertions(+) create mode 100644 src/application/discoverStructure.test.ts create mode 100644 src/application/discoverStructure.ts diff --git a/src/application/discoverStructure.test.ts b/src/application/discoverStructure.test.ts new file mode 100644 index 0000000..da97857 --- /dev/null +++ b/src/application/discoverStructure.test.ts @@ -0,0 +1,60 @@ +import { readFileSync } from 'node:fs'; +import { resolve } from 'node:path'; +import { describe, expect, it } from 'vitest'; +import { parseFdjCsv } from '../infrastructure/csv/parseFdjCsv.ts'; +import { discoverStructure } from './discoverStructure.ts'; +import type { DiscoveryConfig } from './discoverStructure.ts'; + +const REAL_CSV_PATH = resolve(process.cwd(), 'public/results/euromillions_202002.csv'); +const draws = parseFdjCsv(readFileSync(REAL_CSV_PATH, 'utf8')); + +const CONFIG: DiscoveryConfig = { + kCandidates: [2, 3, 4, 5, 6], + neighborCount: 10, + bootstrapIterations: 15, + seed: 42, +}; + +describe('discoverStructure', () => { + it('produces one real SpatialEmbedding per draw, with actual (non-stubbed) density/outlierScore/clusterId', () => { + const result = discoverStructure(draws, CONFIG); + + expect(result.embeddings).toHaveLength(draws.length); + for (const embedding of result.embeddings) { + expect(embedding.method).toBe('pca'); + expect(embedding.clusterId).toBeDefined(); + expect(embedding.nearestNeighbors).toHaveLength(CONFIG.neighborCount); + expect(Number.isFinite(embedding.density)).toBe(true); + expect(Number.isFinite(embedding.outlierScore)).toBe(true); + } + // Not every point can be the stubbed spike values (density=0, outlierScore=0 for all). + expect(result.embeddings.some((e) => e.density !== 0)).toBe(true); + expect(new Set(result.embeddings.map((e) => e.outlierScore)).size).toBeGreaterThan(1); + }); + + it('produces families covering every draw exactly once, each with a description', () => { + const result = discoverStructure(draws, CONFIG); + + const totalMembers = result.families.reduce((acc, family) => acc + Math.round(family.frequency * draws.length), 0); + expect(totalMembers).toBe(draws.length); + for (const family of result.families) { + expect(family.description.length).toBeGreaterThan(0); + expect(family.stability).toBeGreaterThanOrEqual(0); + expect(family.stability).toBeLessThanOrEqual(1); + } + }); + + it('is reproducible for the same seed and config', () => { + const a = discoverStructure(draws, CONFIG); + const b = discoverStructure(draws, CONFIG); + + expect(a).toEqual(b); + }); + + it('compares real vs synthetic clustering structure and reports a boolean conclusion either way', () => { + const result = discoverStructure(draws, CONFIG); + + expect(typeof result.nullHypothesisComparison.realShowsMoreStructure).toBe('boolean'); + expect(Number.isFinite(result.nullHypothesisComparison.syntheticSilhouetteScore)).toBe(true); + }); +}); diff --git a/src/application/discoverStructure.ts b/src/application/discoverStructure.ts new file mode 100644 index 0000000..53d25df --- /dev/null +++ b/src/application/discoverStructure.ts @@ -0,0 +1,177 @@ +import type { Draw } from '../domain/draw/Draw.ts'; +import type { FeatureKey, FeatureVector } from '../domain/features/FeatureExtractor.ts'; +import { FEATURE_KEYS, extractFeatures } from '../domain/features/FeatureExtractor.ts'; +import type { NormalizationModel } from '../domain/discovery/normalizeFeatures.ts'; +import { applyNormalization, fitNormalization } from '../domain/discovery/normalizeFeatures.ts'; +import type { DimensionalityReduction } from '../domain/discovery/reduceDimensions.ts'; +import { reduceDimensionsPca } from '../domain/discovery/reduceDimensions.ts'; +import type { KMeansCandidateScore, KMeansFitResult } from '../domain/discovery/kmeansClustering.ts'; +import { computeBootstrapStability, fitKMeansWithSilhouette } from '../domain/discovery/kmeansClustering.ts'; +import { computeDensityEvaluations } from '../domain/discovery/densityEvaluation.ts'; +import { generateSyntheticDraws } from '../domain/discovery/generateSyntheticDraws.ts'; +import type { SpatialEmbedding } from '../domain/geometry/SpatialEmbedding.ts'; + +const STABILITY_THRESHOLD = 0.5; +const FAMILY_LABELS = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H']; + +const FEATURE_DESCRIPTIONS: Partial> = { + sum: { high: 'Somme élevée', low: 'Somme faible' }, + range: { high: 'Amplitude large', low: 'Amplitude resserrée' }, + oddCount: { high: 'Majorité de numéros impairs', low: 'Majorité de numéros pairs' }, + aboveThirtyOneCount: { high: 'Plusieurs numéros supérieurs à 31', low: 'Peu de numéros supérieurs à 31' }, + consecutivePairsCount: { high: 'Plusieurs numéros consécutifs', low: "Peu de numéros consécutifs" }, + sameUnitsPairsCount: { high: "Plusieurs numéros de même unité", low: "Peu de répétition d'unité" }, + entropy: { high: 'Répartition étalée sur les dizaines', low: 'Répartition concentrée sur peu de dizaines' }, + clusterSizeMax: { high: 'Numéros concentrés dans une dizaine', low: 'Numéros bien répartis entre les dizaines' }, + starsSum: { high: 'Étoiles à valeur élevée', low: 'Étoiles à valeur faible' }, +}; + +export interface Family { + clusterId: number; + label: string; + description: string; + frequency: number; + stability: number; + isStable: boolean; +} + +export interface DiscoveryConfig { + kCandidates: number[]; + neighborCount: number; + bootstrapIterations: number; + seed: number; + syntheticSampleCount?: number; +} + +export interface NullHypothesisComparison { + realSilhouetteScore: number; + syntheticSilhouetteScore: number; + realShowsMoreStructure: boolean; +} + +export interface DiscoveryResult { + embeddings: SpatialEmbedding[]; + families: Family[]; + normalization: NormalizationModel; + pca: Pick; + clustering: { k: number; silhouetteScore: number; candidateScores: KMeansCandidateScore[] }; + nullHypothesisComparison: NullHypothesisComparison; +} + +const mean = (values: number[]): number => values.reduce((acc, value) => acc + value, 0) / values.length; + +const describeCluster = ( + clusterVectors: FeatureVector[], + overallMeans: Record, + overallStdDevs: Record, +): string[] => + FEATURE_KEYS.filter((key) => key in FEATURE_DESCRIPTIONS && overallStdDevs[key] > 0) + .map((key) => ({ + key, + zScore: (mean(clusterVectors.map((vector) => vector.values[key])) - overallMeans[key]) / overallStdDevs[key], + })) + .sort((a, b) => Math.abs(b.zScore) - Math.abs(a.zScore)) + .slice(0, 3) + .map(({ key, zScore }) => (zScore >= 0 ? FEATURE_DESCRIPTIONS[key]!.high : FEATURE_DESCRIPTIONS[key]!.low)); + +const buildFamilies = ( + vectors: FeatureVector[], + clusteringFit: KMeansFitResult, + stabilityByCluster: Map, + normalization: NormalizationModel, +): Family[] => { + const overallMeans: Record = {}; + for (const key of FEATURE_KEYS) overallMeans[key] = mean(vectors.map((vector) => vector.values[key])); + + const clusterIds = [...new Set(clusteringFit.clusterIds)].sort((a, b) => a - b); + + return clusterIds.map((clusterId, index) => { + const memberVectors = clusteringFit.clusterIds + .map((id, memberIndex) => (id === clusterId ? memberIndex : -1)) + .filter((memberIndex) => memberIndex !== -1) + .map((memberIndex) => vectors[memberIndex]); + + const stability = stabilityByCluster.get(clusterId) ?? 0; + + return { + clusterId, + label: `Famille ${FAMILY_LABELS[index] ?? index}`, + description: describeCluster(memberVectors, overallMeans, normalization.stdDevs).join(', '), + frequency: memberVectors.length / vectors.length, + stability, + isStable: stability >= STABILITY_THRESHOLD, + }; + }); +}; + +/** + * The real DiscoveryModel: normalize -> PCA -> K-Means (+ bootstrap + * stability) -> k-NN density, producing actual SpatialEmbedding[] with + * real density/outlierScore/clusterId/nearestNeighbors - replacing the + * ADR-0002 spike's stubbed 0/0/undefined/[] placeholders. Also runs the + * same pipeline on a synthetic null-hypothesis dataset for comparison. + */ +export const discoverStructure = (draws: Draw[], config: DiscoveryConfig): DiscoveryResult => { + const vectors = draws.map((draw) => extractFeatures({ numbers: draw.numbers, stars: draw.stars })); + const normalization = fitNormalization(vectors); + const normalizedRows = applyNormalization(vectors, normalization); + const pca = reduceDimensionsPca(normalizedRows, normalization.featureKeys); + const pcaRows = pca.coordinates.map((c) => [c.x, c.y, c.z]); + + const clusteringFit = fitKMeansWithSilhouette(pcaRows, config.kCandidates, config.seed); + const stabilities = computeBootstrapStability( + pcaRows, + clusteringFit.clusterIds, + clusteringFit.k, + config.bootstrapIterations, + config.seed, + ); + const stabilityByCluster = new Map(stabilities.map((entry) => [entry.clusterId, entry.stability])); + + const densityEvaluations = computeDensityEvaluations(pcaRows, config.neighborCount); + + const embeddings: SpatialEmbedding[] = draws.map((draw, index) => ({ + drawId: draw.id, + coordinates: pca.coordinates[index], + method: 'pca', + clusterId: String(clusteringFit.clusterIds[index]), + density: densityEvaluations[index].densityPercentile, + outlierScore: densityEvaluations[index].outlierScore, + nearestNeighbors: densityEvaluations[index].nearestNeighborIndices.map((neighborIndex, neighborRank) => ({ + drawId: draws[neighborIndex].id, + distance: densityEvaluations[index].nearestNeighborDistances[neighborRank], + })), + })); + + const families = buildFamilies(vectors, clusteringFit, stabilityByCluster, normalization); + + const syntheticDraws = generateSyntheticDraws( + config.syntheticSampleCount ?? draws.length, + config.seed + 1000, + draws[0]?.date ?? '2000-01-01', + ); + const syntheticVectors = syntheticDraws.map((draw) => extractFeatures({ numbers: draw.numbers, stars: draw.stars })); + const syntheticNormalization = fitNormalization(syntheticVectors); + const syntheticRows = applyNormalization(syntheticVectors, syntheticNormalization); + const syntheticPcaRows = reduceDimensionsPca(syntheticRows, syntheticNormalization.featureKeys).coordinates.map( + (c) => [c.x, c.y, c.z], + ); + const syntheticClusteringFit = fitKMeansWithSilhouette(syntheticPcaRows, config.kCandidates, config.seed); + + return { + embeddings, + families, + normalization, + pca: { explainedVariance: pca.explainedVariance, featureContributions: pca.featureContributions }, + clustering: { + k: clusteringFit.k, + silhouetteScore: clusteringFit.silhouetteScore, + candidateScores: clusteringFit.candidateScores, + }, + nullHypothesisComparison: { + realSilhouetteScore: clusteringFit.silhouetteScore, + syntheticSilhouetteScore: syntheticClusteringFit.silhouetteScore, + realShowsMoreStructure: clusteringFit.silhouetteScore > syntheticClusteringFit.silhouetteScore, + }, + }; +}; From 4020e375e5dd17293848e707eca5140cadd62240 Mon Sep 17 00:00:00 2001 From: keyobs Date: Tue, 11 Aug 2026 11:08:32 +0200 Subject: [PATCH 40/68] feat(discovery): build the /discovery page (Carte + Familles views) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Carte: PC1xPC2 scatter, one Recharts Scatter series per cluster with a categorical color + legend - "the color encodes the discovered family, not a prediction" stated directly in the UI. Familles: one card per cluster with its description, frequency, and a stable/instable badge from the bootstrap. A "Réel vs hasard" panel surfaces the null- hypothesis comparison, explained variance, and excluded-feature count. Smoke-tested in the real browser: 5 families found, all currently stable, real silhouette (0.279) came back *lower* than the synthetic random dataset's (0.287) - "aucun signal robuste détecté" is exactly the right, honest conclusion here (real EuroMillions draws shouldn't have more cluster structure than pure chance), and the page states that plainly rather than dressing it up. Zero console errors. Deferred, per the scope stated at the start of V3: UMAP, DBSCAN/ HDBSCAN, KDE/LOF/Isolation Forest, temporal dependency analysis, FDR correction, learned feature weights, and the Features/Anomalies/ Évolution-temporelle views. Carte also skips neighbor highlighting, temporal filtering, and "locate my grid" for this first pass. --- src/app/AppLayout.tsx | 1 + src/app/router.tsx | 2 + src/pages/discovery/DiscoveryPage.module.scss | 112 +++++++++++++ src/pages/discovery/DiscoveryPage.tsx | 148 ++++++++++++++++++ 4 files changed, 263 insertions(+) create mode 100644 src/pages/discovery/DiscoveryPage.module.scss create mode 100644 src/pages/discovery/DiscoveryPage.tsx diff --git a/src/app/AppLayout.tsx b/src/app/AppLayout.tsx index bd677b7..0260d66 100644 --- a/src/app/AppLayout.tsx +++ b/src/app/AppLayout.tsx @@ -6,6 +6,7 @@ const NAV_ITEMS = [ { to: '/draws', label: 'Tirages' }, { to: '/geometry', label: 'Géométrie' }, { to: '/laboratory', label: 'Laboratoire' }, + { to: '/discovery', label: 'Discovery' }, ]; const AppLayout = () => { diff --git a/src/app/router.tsx b/src/app/router.tsx index 17dfc2d..0ed6d44 100644 --- a/src/app/router.tsx +++ b/src/app/router.tsx @@ -4,6 +4,7 @@ import EvaluationPage from '../pages/evaluation/EvaluationPage.tsx'; import DrawsPage from '../pages/draws/DrawsPage.tsx'; import GeometryPage from '../pages/geometry/GeometryPage.tsx'; import LaboratoryPage from '../pages/laboratory/LaboratoryPage.tsx'; +import DiscoveryPage from '../pages/discovery/DiscoveryPage.tsx'; import SpikePage from '../pages/spike/SpikePage.tsx'; export const router = createBrowserRouter([ @@ -16,6 +17,7 @@ export const router = createBrowserRouter([ { path: 'draws', element: }, { path: 'geometry', element: }, { path: 'laboratory', element: }, + { path: 'discovery', element: }, { path: 'spike', element: }, ], }, diff --git a/src/pages/discovery/DiscoveryPage.module.scss b/src/pages/discovery/DiscoveryPage.module.scss new file mode 100644 index 0000000..611860c --- /dev/null +++ b/src/pages/discovery/DiscoveryPage.module.scss @@ -0,0 +1,112 @@ +.page { + display: flex; + flex-direction: column; + gap: var(--space-6); + text-align: left; +} + +.section { + display: flex; + flex-direction: column; + gap: var(--space-3); + border: 1px solid var(--border); + border-radius: var(--radius-md); + background: var(--surface); + padding: var(--space-5); +} + +.note { + color: var(--text-muted); + font-size: var(--text-sm); +} + +.legend { + display: flex; + flex-wrap: wrap; + gap: var(--space-4); + font-size: var(--text-sm); +} + +.legendItem { + display: flex; + align-items: center; + gap: var(--space-2); +} + +.legendSwatch { + width: 0.7rem; + height: 0.7rem; + border-radius: 50%; + display: inline-block; +} + +.familiesGrid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); + gap: var(--space-4); +} + +.familyCard { + border: 1px solid var(--border); + border-radius: var(--radius-md); + padding: var(--space-4); + border-left: 3px solid var(--family-accent, var(--accent-primary)); +} + +.familyHeader { + display: flex; + justify-content: space-between; + align-items: baseline; + gap: var(--space-2); +} + +.stabilityBadge { + font-size: var(--text-xs); + padding: 0.15rem 0.5rem; + border-radius: 999px; + font-weight: 600; +} + +.stable { + background: color-mix(in srgb, var(--accent-confidence) 20%, transparent); + color: var(--accent-confidence); +} + +.unstable { + background: var(--danger-surface); + color: var(--danger); +} + +.metrics { + display: flex; + flex-direction: column; + gap: var(--space-1); + color: var(--text-muted); + font-size: var(--text-sm); + + strong { + color: var(--text); + font-family: var(--mono); + } +} + +.table { + width: 100%; + border-collapse: collapse; + font-size: var(--text-sm); + + th, + td { + padding: var(--space-2) var(--space-3); + border-bottom: 1px solid var(--border); + text-align: left; + } + + thead th { + color: var(--text-muted); + font-size: var(--text-xs); + text-transform: uppercase; + letter-spacing: 0.04em; + font-weight: 600; + } +} diff --git a/src/pages/discovery/DiscoveryPage.tsx b/src/pages/discovery/DiscoveryPage.tsx new file mode 100644 index 0000000..078d106 --- /dev/null +++ b/src/pages/discovery/DiscoveryPage.tsx @@ -0,0 +1,148 @@ +import { useMemo } from 'react'; +import type { CSSProperties } from 'react'; +import { useQuery } from '@tanstack/react-query'; +import { CartesianGrid, ResponsiveContainer, Scatter, ScatterChart, Tooltip, XAxis, YAxis } from 'recharts'; +import { drawRepository } from '../../application/drawRepository.ts'; +import type { DiscoveryConfig } from '../../application/discoverStructure.ts'; +import { discoverStructure } from '../../application/discoverStructure.ts'; +import type { Draw } from '../../domain/draw/Draw.ts'; +import styles from './DiscoveryPage.module.scss'; + +const EMPTY_DRAWS: Draw[] = []; + +const DISCOVERY_CONFIG: DiscoveryConfig = { + kCandidates: [2, 3, 4, 5, 6, 7, 8], + neighborCount: 10, + bootstrapIterations: 20, + seed: 42, +}; + +const CLUSTER_COLORS = ['#7dd3fc', '#fbbf6b', '#b9a3fb', '#5fe0b5', '#f2707d', '#93c5fd', '#fcd34d', '#c4b5fd']; + +const colorForCluster = (clusterId: string): string => { + const index = Number(clusterId) % CLUSTER_COLORS.length; + return CLUSTER_COLORS[index]; +}; + +const DiscoveryPage = () => { + const drawsQuery = useQuery({ queryKey: ['draws', 'all'], queryFn: () => drawRepository.getAll() }); + const draws = drawsQuery.data ?? EMPTY_DRAWS; + + const discovery = useMemo(() => (draws.length > 0 ? discoverStructure(draws, DISCOVERY_CONFIG) : null), [draws]); + + const scatterSeriesByCluster = useMemo(() => { + if (!discovery) return []; + const byCluster = new Map(); + for (const embedding of discovery.embeddings) { + const clusterId = embedding.clusterId ?? 'none'; + const points = byCluster.get(clusterId) ?? []; + points.push({ x: embedding.coordinates.x, y: embedding.coordinates.y, drawId: embedding.drawId }); + byCluster.set(clusterId, points); + } + return [...byCluster.entries()].sort(([a], [b]) => Number(a) - Number(b)); + }, [discovery]); + + return ( +
+

Discovery Engine

+

+ Le système recherche quelles caractéristiques structurent réellement l'historique plutôt que de fixer des + pondérations arbitraires. Hypothèse nulle : les tirages sont compatibles avec un processus aléatoire + indépendant ; toute structure détectée est comparée à cette référence. +

+ + {drawsQuery.isLoading &&

Chargement...

} + + {discovery && ( + <> +
+

Réel vs hasard

+
+

+ Score de silhouette (historique réel) : {discovery.clustering.silhouetteScore.toFixed(3)} +

+

+ Score de silhouette (historique synthétique aléatoire) :{' '} + {discovery.nullHypothesisComparison.syntheticSilhouetteScore.toFixed(3)} +

+

+ {discovery.nullHypothesisComparison.realShowsMoreStructure + ? 'Le réel montre une structure de clustering plus marquée que le hasard, sur cette mesure.' + : 'Aucun signal robuste détecté : le réel ne montre pas plus de structure que le hasard, sur cette mesure.'} +

+

+ Variance expliquée (PC1, PC2, PC3) :{' '} + {discovery.pca.explainedVariance.map((v) => `${(v * 100).toFixed(1)}%`).join(' · ')} +

+

+ Features exclues : {discovery.normalization.excludedFeatures.length} (quasi-constantes + ou redondantes, voir le modèle de normalisation) +

+
+
+ +
+

Carte (PC1 × PC2)

+

+ Chaque point est un tirage historique projeté par PCA. La couleur encode la famille (cluster) découverte, + pas une prédiction. +

+
+ {discovery.families.map((family) => ( + + + {family.label} + + ))} +
+ + + + + + + {scatterSeriesByCluster.map(([clusterId, points]) => ( + + ))} + + +
+ +
+

Familles ({discovery.clustering.k})

+
+ {discovery.families.map((family) => ( +
+
+

{family.label}

+ + {family.isStable ? 'stable' : 'instable'} + +
+

{family.description || 'Pas de trait dominant identifié'}

+

+ Fréquence : {(family.frequency * 100).toFixed(1)}% — Stabilité bootstrap :{' '} + {(family.stability * 100).toFixed(0)}% +

+
+ ))} +
+
+ + )} +
+ ); +}; + +export default DiscoveryPage; From de8e4caa8434962099fa01f692d6117c94598cf1 Mon Sep 17 00:00:00 2001 From: keyobs Date: Tue, 11 Aug 2026 11:21:30 +0200 Subject: [PATCH 41/68] test(discovery): add render/interaction test for the /discovery page Renders without crashing, and once history loads and discoverStructure runs, families and the null-hypothesis comparison panel populate. --- src/pages/discovery/DiscoveryPage.test.tsx | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 src/pages/discovery/DiscoveryPage.test.tsx diff --git a/src/pages/discovery/DiscoveryPage.test.tsx b/src/pages/discovery/DiscoveryPage.test.tsx new file mode 100644 index 0000000..e6fb57d --- /dev/null +++ b/src/pages/discovery/DiscoveryPage.test.tsx @@ -0,0 +1,22 @@ +import { afterEach, describe, expect, it, vi } from 'vitest'; +import { screen, waitFor } from '@testing-library/react'; +import { renderWithProviders } from '../../shared/renderWithProviders.tsx'; +import { REAL_CSV_TEXT } from '../../shared/testCsvFixture.ts'; +import DiscoveryPage from './DiscoveryPage.tsx'; + +describe('DiscoveryPage', () => { + afterEach(() => { + vi.unstubAllGlobals(); + }); + + it('renders without crashing and shows families and the null-hypothesis comparison once loaded', async () => { + vi.stubGlobal('fetch', () => Promise.resolve(new Response(REAL_CSV_TEXT))); + + renderWithProviders(); + + expect(screen.getByRole('heading', { name: 'Discovery Engine' })).toBeInTheDocument(); + await waitFor(() => expect(screen.getAllByTestId('family-card').length).toBeGreaterThan(0), { timeout: 10000 }); + expect(screen.getByTestId('null-hypothesis')).toBeInTheDocument(); + expect(screen.getAllByTestId('stability-badge').length).toBeGreaterThan(0); + }); +}); From ad4cd02bba1591b013a053ac9542002cd16a8b90 Mon Sep 17 00:00:00 2001 From: keyobs Date: Tue, 11 Aug 2026 12:28:34 +0200 Subject: [PATCH 42/68] fix(evaluation): live input validation, plain-language descriptions, per-variation scores MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses devdx/fix.md: 1. Number/star inputs now color live as the user types: green when the value is within range (1-50 for numbers, 1-12 for stars - fix.md said 1-10 for stars, kept 1-12 since that's the actual EuroMillions rule already enforced everywhere else in the app; flagging in case 1-10 was actually intended), red when out of range. Hit a real CSS cascade bug along the way: .starInput's own border-color was winning over .inputInvalid/.inputValid under some conditions since combined-class order isn't reliably reflected in Vite's dev-time CSS injection - fixed with !important on the state-override classes (verified against getComputedStyle, not just visually). 2. Each score card now has a plain-language description at the bottom (what "Structure historique" etc. actually means), and factor keys like "window_1"/"sumPercentile" are now shown as readable French labels ("Sur 1 an", "Percentile de la somme") via a lookup table in ScoreCard - domain code stays untouched, only the presentation layer translates. 3. Each variation now shows an explanatory sentence (anti-partage's sharing-reduction rationale, kept alongside the existing "doesn't change your odds" disclaimer already above the list) and its own 4 scores (Structure/Originalité/Temporalité/Confiance), computed via evaluateGrid on that variation's grid, displayed compactly with color-coded dots matching each ScoreCard's accent. Verified in a real browser: live coloring confirmed via getComputedStyle (not just screenshot), all descriptions/labels render, per-variation scores populate. All 114 unit/component tests and both E2E specs still pass. --- .../evaluation/EvaluationPage.module.scss | 86 +++++++++++++++++-- src/pages/evaluation/EvaluationPage.tsx | 61 +++++++++++-- src/pages/evaluation/ScoreCard.module.scss | 9 ++ src/pages/evaluation/ScoreCard.tsx | 37 +++++++- 4 files changed, 178 insertions(+), 15 deletions(-) diff --git a/src/pages/evaluation/EvaluationPage.module.scss b/src/pages/evaluation/EvaluationPage.module.scss index c080cd7..468e681 100644 --- a/src/pages/evaluation/EvaluationPage.module.scss +++ b/src/pages/evaluation/EvaluationPage.module.scss @@ -21,6 +21,7 @@ font-size: var(--text-lg); font-weight: 600; margin-right: var(--space-2); + transition: background 0.15s ease, border-color 0.15s ease; } .starInput { @@ -28,6 +29,20 @@ border-color: var(--accent-originality); } +// !important: these are state overrides that must win regardless of which +// other input-styling class (e.g. .starInput's own border-color) is combined +// with them - and combined-class cascade order isn't reliably source order +// once Vite's dev-time CSS module injection is involved. +.inputValid { + background: color-mix(in srgb, var(--accent-confidence) 25%, var(--surface)) !important; + border-color: var(--accent-confidence) !important; +} + +.inputInvalid { + background: color-mix(in srgb, var(--danger) 25%, var(--surface)) !important; + border-color: var(--danger) !important; +} + .scores { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); @@ -52,23 +67,80 @@ padding: 0; display: flex; flex-direction: column; - gap: var(--space-2); + gap: var(--space-3); } .variationItem { display: flex; - gap: var(--space-3); - align-items: baseline; - font-family: var(--mono); + flex-direction: column; + gap: var(--space-2); font-size: var(--text-sm); - padding: var(--space-2) var(--space-3); + padding: var(--space-3) var(--space-4); background: var(--surface); border: 1px solid var(--border); - border-radius: var(--radius-sm); + border-radius: var(--radius-md); +} + +.variationHeader { + display: flex; + gap: var(--space-3); + align-items: baseline; + flex-wrap: wrap; + font-family: var(--mono); } .variationLabel { font-family: var(--sans); - color: var(--text-muted); + font-weight: 600; + color: var(--text); white-space: nowrap; } + +.variationDescription { + margin: 0; + color: var(--text-muted); + font-size: var(--text-xs); + line-height: 1.5; +} + +.variationScores { + display: flex; + flex-wrap: wrap; + gap: var(--space-4); + font-size: var(--text-xs); + color: var(--text-muted); + padding-top: var(--space-2); + border-top: 1px solid var(--border); +} + +.variationScore { + display: flex; + align-items: center; + gap: var(--space-1); +} + +.dotStructure, +.dotOriginality, +.dotTemporal, +.dotConfidence { + width: 0.5rem; + height: 0.5rem; + border-radius: 50%; + display: inline-block; +} + +.dotStructure { + background: var(--accent-structure); +} + +.dotOriginality { + background: var(--accent-originality); +} + +.dotTemporal { + background: var(--accent-temporal); +} + +.dotConfidence { + background: var(--accent-confidence); +} diff --git a/src/pages/evaluation/EvaluationPage.tsx b/src/pages/evaluation/EvaluationPage.tsx index ee471b6..726bac5 100644 --- a/src/pages/evaluation/EvaluationPage.tsx +++ b/src/pages/evaluation/EvaluationPage.tsx @@ -12,6 +12,11 @@ import { classifyReading, evaluateGrid } from '../../domain/scoring/evaluateGrid import ScoreCard from './ScoreCard.tsx'; import styles from './EvaluationPage.module.scss'; +const NUMBER_MIN = 1; +const NUMBER_MAX = 50; +const STAR_MIN = 1; +const STAR_MAX = 12; + const EMPTY_NUMBERS = ['', '', '', '', '']; const EMPTY_STARS = ['', '']; const EMPTY_HISTORY: Draw[] = []; @@ -29,8 +34,23 @@ const VARIATION_LABELS: Record = { 'anti-share': 'Anti-partage', }; +const VARIATION_DESCRIPTIONS: Record = { + 'structurally-common': + 'Reproduit un profil (somme, répartition par dizaine, écarts) proche des tirages historiquement fréquents.', + balanced: + 'Répartit les numéros sur les cinq dizaines et équilibre pairs/impairs, pour une grille aux caractéristiques neutres.', + 'anti-share': + 'Moins de chance de partager un gain, car elle évite les numéros calendaires (≤31) que beaucoup de joueurs choisissent - cela ne change pas vos chances de gagner.', +}; + const onlyDigits = (value: string): string => value.replace(/\D/g, '').slice(0, 2); +const validityClass = (value: string, min: number, max: number): string => { + if (value === '') return ''; + const parsed = Number(value); + return parsed >= min && parsed <= max ? styles.inputValid : styles.inputInvalid; +}; + const EvaluationPage = () => { const [numberInputs, setNumberInputs] = useState(EMPTY_NUMBERS); const [starInputs, setStarInputs] = useState(EMPTY_STARS); @@ -107,7 +127,13 @@ const EvaluationPage = () => { [grid, history], ); const variations = useMemo( - () => (grid && history.length > 0 ? generateVariations(grid, history) : []), + () => + grid && history.length > 0 + ? generateVariations(grid, history).map((variation) => ({ + ...variation, + scores: evaluateGrid(variation.grid, history), + })) + : [], [grid, history], ); const reading = scores ? classifyReading(scores) : null; @@ -125,7 +151,7 @@ const EvaluationPage = () => { ref={(element) => { numberFieldRefs.current[index] = element; }} - className={styles.numberInput} + className={`${styles.numberInput} ${validityClass(value, NUMBER_MIN, NUMBER_MAX)}`} type="text" inputMode="numeric" pattern="[0-9]*" @@ -147,7 +173,7 @@ const EvaluationPage = () => { ref={(element) => { starFieldRefs.current[index] = element; }} - className={`${styles.numberInput} ${styles.starInput}`} + className={`${styles.numberInput} ${styles.starInput} ${validityClass(value, STAR_MIN, STAR_MAX)}`} type="text" inputMode="numeric" pattern="[0-9]*" @@ -196,10 +222,31 @@ const EvaluationPage = () => {
    {variations.map((variation) => (
  • - {VARIATION_LABELS[variation.kind]} - - {variation.grid.numbers.join(' · ')} — étoiles {variation.grid.stars.join(' · ')} - +
    + {VARIATION_LABELS[variation.kind]} + + {variation.grid.numbers.join(' · ')} — étoiles {variation.grid.stars.join(' · ')} + +
    +

    {VARIATION_DESCRIPTIONS[variation.kind]}

    +
    + + + Structure historique : {variation.scores.structure.value} + + + + Originalité estimée : {variation.scores.originality.value} + + + + Temporalité : {variation.scores.temporal.value} + + + + Confiance : {variation.scores.confidence.value} + +
  • ))}
diff --git a/src/pages/evaluation/ScoreCard.module.scss b/src/pages/evaluation/ScoreCard.module.scss index 2dc1a74..3f0bc82 100644 --- a/src/pages/evaluation/ScoreCard.module.scss +++ b/src/pages/evaluation/ScoreCard.module.scss @@ -66,3 +66,12 @@ gap: var(--space-2); } } + +.description { + margin: var(--space-3) 0 0; + padding-top: var(--space-3); + border-top: 1px solid var(--border); + font-size: var(--text-xs); + line-height: 1.5; + color: var(--text-muted); +} diff --git a/src/pages/evaluation/ScoreCard.tsx b/src/pages/evaluation/ScoreCard.tsx index fb90e66..ac0a15a 100644 --- a/src/pages/evaluation/ScoreCard.tsx +++ b/src/pages/evaluation/ScoreCard.tsx @@ -16,6 +16,40 @@ const ACCENT_CLASSES: Record = { confidence: styles.accentConfidence, }; +const ACCENT_DESCRIPTIONS: Record = { + structure: + "Indique si la forme de cette grille (somme, écarts, répartition par dizaine) ressemble aux tirages historiques les plus courants.", + originality: + "Évalue si cette grille correspond à des choix populaires chez les joueurs (dates, numéros consécutifs...) - sans effet sur vos chances réelles de gagner.", + temporal: "Compare cette grille aux tirages récents, sur des périodes allant d'un an à tout l'historique.", + confidence: + 'Indique la fiabilité du diagnostic ci-dessus : plus les tirages proches de cette grille sont cohérents entre eux, plus la confiance est élevée.', +}; + +const FACTOR_LABELS: Record = { + meanNeighborDistance: 'Distance moyenne aux tirages proches', + sumPercentile: 'Percentile de la somme', + amplitudePercentile: "Percentile de l'amplitude", + decadeSignatureMatchRate: 'Tirages avec la même répartition par dizaine', + parityMatchRate: 'Tirages avec la même répartition pair/impair', + consecutivePairsCount: 'Numéros consécutifs', + sameUnitsPairsCount: 'Numéros finissant par le même chiffre', + multiplesOfFiveCount: 'Multiples de 5', + aboveThirtyOneCount: 'Numéros supérieurs à 31', + window_1: 'Sur 1 an', + window_3: 'Sur 3 ans', + window_6: 'Sur 6 ans', + window_12: 'Sur 12 ans', + window_25: 'Sur 25 ans', + window_50: 'Sur 50 ans', + window_all: "Sur tout l'historique", + sampleSize: 'Nombre de tirages analysés', + neighborMeanDistance: 'Distance moyenne aux tirages proches', + neighborDistanceStdDev: 'Écart-type de cette distance', +}; + +const factorLabel = (rawLabel: string): string => FACTOR_LABELS[rawLabel] ?? rawLabel; + const ScoreCard = ({ title, score, accent }: ScoreCardProps) => { return (
@@ -27,11 +61,12 @@ const ScoreCard = ({ title, score, accent }: ScoreCardProps) => {
    {score.factors.map((factor) => (
  • - {factor.label} + {factorLabel(factor.label)} {Math.round(factor.value * 100) / 100}
  • ))}
+

{ACCENT_DESCRIPTIONS[accent]}

); }; From f3249f140c28ec7b55c1054fcbb374b6ca47a746 Mon Sep 17 00:00:00 2001 From: keyobs Date: Tue, 11 Aug 2026 12:37:17 +0200 Subject: [PATCH 43/68] fix(evaluation): flag duplicate numbers/stars red on the second occurrence A Grid needs 5 distinct numbers and 2 distinct stars; validityClass now checks the earlier entries in the same array (numbers or stars separately) and marks a repeated value invalid regardless of it being in range - only the first occurrence of a value can be green. Verified with getComputedStyle: typing 7 twice keeps the first green and turns the second red, same for stars. --- src/pages/evaluation/EvaluationPage.tsx | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/pages/evaluation/EvaluationPage.tsx b/src/pages/evaluation/EvaluationPage.tsx index 726bac5..a1c252c 100644 --- a/src/pages/evaluation/EvaluationPage.tsx +++ b/src/pages/evaluation/EvaluationPage.tsx @@ -45,8 +45,14 @@ const VARIATION_DESCRIPTIONS: Record = { const onlyDigits = (value: string): string => value.replace(/\D/g, '').slice(0, 2); -const validityClass = (value: string, min: number, max: number): string => { +// A value repeated at an earlier index makes this (later) field invalid +// too, even if it's otherwise in range - a Grid needs 5 distinct numbers +// and 2 distinct stars, so only the *first* occurrence of a value can be +// valid. +const validityClass = (values: string[], index: number, min: number, max: number): string => { + const value = values[index]; if (value === '') return ''; + if (values.slice(0, index).includes(value)) return styles.inputInvalid; const parsed = Number(value); return parsed >= min && parsed <= max ? styles.inputValid : styles.inputInvalid; }; @@ -151,7 +157,7 @@ const EvaluationPage = () => { ref={(element) => { numberFieldRefs.current[index] = element; }} - className={`${styles.numberInput} ${validityClass(value, NUMBER_MIN, NUMBER_MAX)}`} + className={`${styles.numberInput} ${validityClass(numberInputs, index, NUMBER_MIN, NUMBER_MAX)}`} type="text" inputMode="numeric" pattern="[0-9]*" @@ -173,7 +179,7 @@ const EvaluationPage = () => { ref={(element) => { starFieldRefs.current[index] = element; }} - className={`${styles.numberInput} ${styles.starInput} ${validityClass(value, STAR_MIN, STAR_MAX)}`} + className={`${styles.numberInput} ${styles.starInput} ${validityClass(starInputs, index, STAR_MIN, STAR_MAX)}`} type="text" inputMode="numeric" pattern="[0-9]*" From 57f872c961edca2839bb77bf1dd398ad8db037ac Mon Sep 17 00:00:00 2001 From: keyobs Date: Tue, 11 Aug 2026 13:53:39 +0200 Subject: [PATCH 44/68] fix(geometry): reference grid banner, chart descriptions, highlighted point, readable gap map MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses the new /geometry section of devdx/fix.md: 1. A "grille de référence" banner at the top of the page shows the reference grid's numbers/stars as Bubbles, so it's always visible while scrolling through the charts below. 2. A plain-language description under every chart/section title (what "Somme x Écart-type" etc. actually shows). 3. Both scatter charts now overlay a second Scatter series containing just the reference grid's own point, rendered as a bigger outlined dot in a distinct accent color - clearly distinguishable from the rest of the historical cloud. 4. "Carte des écarts" rebuilt: the old plain-text chain ("26 —3→ 29 —6→...") looked like it might already be showing a grid and a variation together - it wasn't, it was just one grid's own gaps, which is exactly the confusion that prompted this fix. Now it shows two clearly labeled NumberChain rows (grid numbers as round Bubbles linked by gap-labeled arrows): the reference grid (highlighted variant) and one of its proposed variations (plain variant), so the two shapes are visually comparable and distinct. New reusable src/components/bubble/: Bubble (a single round number/star display, visually matching /evaluation's input bubbles but non-interactive) and NumberChain (Bubbles + gap arrows). Also extracted VARIATION_LABELS/VARIATION_DESCRIPTIONS out of EvaluationPage into application/variationLabels.ts so /geometry reuses the same text instead of duplicating it. Verified in a real browser (bubble counts, screenshots of the banner/ gap-map/full page) - zero console errors. All 116 unit/component tests and both E2E specs still pass. --- src/application/variationLabels.ts | 16 +++ src/components/bubble/Bubble.module.scss | 29 +++++ src/components/bubble/Bubble.tsx | 22 ++++ src/components/bubble/NumberChain.module.scss | 29 +++++ src/components/bubble/NumberChain.test.tsx | 18 +++ src/components/bubble/NumberChain.tsx | 27 +++++ src/pages/evaluation/EvaluationPage.tsx | 17 +-- src/pages/geometry/GeometryPage.module.scss | 73 +++++++++++- src/pages/geometry/GeometryPage.tsx | 104 +++++++++++++++--- 9 files changed, 301 insertions(+), 34 deletions(-) create mode 100644 src/application/variationLabels.ts create mode 100644 src/components/bubble/Bubble.module.scss create mode 100644 src/components/bubble/Bubble.tsx create mode 100644 src/components/bubble/NumberChain.module.scss create mode 100644 src/components/bubble/NumberChain.test.tsx create mode 100644 src/components/bubble/NumberChain.tsx diff --git a/src/application/variationLabels.ts b/src/application/variationLabels.ts new file mode 100644 index 0000000..176821b --- /dev/null +++ b/src/application/variationLabels.ts @@ -0,0 +1,16 @@ +import type { VariationKind } from './generateVariations.ts'; + +export const VARIATION_LABELS: Record = { + 'structurally-common': 'Structurellement courante', + balanced: 'Équilibrée', + 'anti-share': 'Anti-partage', +}; + +export const VARIATION_DESCRIPTIONS: Record = { + 'structurally-common': + 'Reproduit un profil (somme, répartition par dizaine, écarts) proche des tirages historiquement fréquents.', + balanced: + 'Répartit les numéros sur les cinq dizaines et équilibre pairs/impairs, pour une grille aux caractéristiques neutres.', + 'anti-share': + 'Moins de chance de partager un gain, car elle évite les numéros calendaires (≤31) que beaucoup de joueurs choisissent - cela ne change pas vos chances de gagner.', +}; diff --git a/src/components/bubble/Bubble.module.scss b/src/components/bubble/Bubble.module.scss new file mode 100644 index 0000000..e8e0fb4 --- /dev/null +++ b/src/components/bubble/Bubble.module.scss @@ -0,0 +1,29 @@ +.bubble { + display: inline-flex; + align-items: center; + justify-content: center; + width: 2.75rem; + height: 2.75rem; + flex-shrink: 0; + border-radius: 50%; + font-family: var(--mono); + font-weight: 700; + font-size: var(--text-base); + border: 1px solid var(--border-strong); + background: var(--surface); + color: var(--text); +} + +.number { + border-color: var(--accent-structure); +} + +.star { + border-color: var(--accent-originality); +} + +.highlight { + border-color: var(--accent-temporal); + background: color-mix(in srgb, var(--accent-temporal) 20%, var(--surface)); + box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent-temporal) 40%, transparent); +} diff --git a/src/components/bubble/Bubble.tsx b/src/components/bubble/Bubble.tsx new file mode 100644 index 0000000..d98cc73 --- /dev/null +++ b/src/components/bubble/Bubble.tsx @@ -0,0 +1,22 @@ +import styles from './Bubble.module.scss'; + +export type BubbleVariant = 'number' | 'star' | 'highlight'; + +interface BubbleProps { + value: number; + variant?: BubbleVariant; +} + +const VARIANT_CLASSES: Record = { + number: styles.number, + star: styles.star, + highlight: styles.highlight, +}; + +const Bubble = ({ value, variant = 'number' }: BubbleProps) => ( + + {value} + +); + +export default Bubble; diff --git a/src/components/bubble/NumberChain.module.scss b/src/components/bubble/NumberChain.module.scss new file mode 100644 index 0000000..41943a9 --- /dev/null +++ b/src/components/bubble/NumberChain.module.scss @@ -0,0 +1,29 @@ +.chain { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 0.15rem; +} + +.step { + display: flex; + align-items: center; +} + +.arrow { + display: flex; + flex-direction: column; + align-items: center; + margin: 0 var(--space-2); + color: var(--text-muted); +} + +.gapValue { + font-family: var(--mono); + font-size: var(--text-xs); +} + +.arrowLine { + font-size: 1.1rem; + line-height: 1; +} diff --git a/src/components/bubble/NumberChain.test.tsx b/src/components/bubble/NumberChain.test.tsx new file mode 100644 index 0000000..7619728 --- /dev/null +++ b/src/components/bubble/NumberChain.test.tsx @@ -0,0 +1,18 @@ +import { describe, expect, it } from 'vitest'; +import { render, screen } from '@testing-library/react'; +import NumberChain from './NumberChain.tsx'; + +describe('NumberChain', () => { + it('renders one bubble per number and one gap arrow between each pair', () => { + render(); + + expect(screen.getAllByTestId('bubble').map((el) => el.textContent)).toEqual(['26', '29', '35', '38', '47']); + }); + + it('labels each arrow with the correct gap', () => { + const { container } = render(); + + const gaps = [...container.querySelectorAll('[class*="gapValue"]')].map((el) => el.textContent); + expect(gaps).toEqual(['3', '6', '3', '9']); + }); +}); diff --git a/src/components/bubble/NumberChain.tsx b/src/components/bubble/NumberChain.tsx new file mode 100644 index 0000000..e7f9030 --- /dev/null +++ b/src/components/bubble/NumberChain.tsx @@ -0,0 +1,27 @@ +import type { BubbleVariant } from './Bubble.tsx'; +import Bubble from './Bubble.tsx'; +import styles from './NumberChain.module.scss'; + +interface NumberChainProps { + numbers: readonly number[]; + variant?: BubbleVariant; +} + +/** Numbers as Bubbles linked by arrows labelled with the gap between them. */ +const NumberChain = ({ numbers, variant = 'number' }: NumberChainProps) => ( +
+ {numbers.map((value, index) => ( + + {index > 0 && ( + + )} + + + ))} +
+); + +export default NumberChain; diff --git a/src/pages/evaluation/EvaluationPage.tsx b/src/pages/evaluation/EvaluationPage.tsx index a1c252c..b7fe2c9 100644 --- a/src/pages/evaluation/EvaluationPage.tsx +++ b/src/pages/evaluation/EvaluationPage.tsx @@ -2,8 +2,8 @@ import { useMemo, useRef, useState } from 'react'; import type { ChangeEvent, KeyboardEvent } from 'react'; import { useQuery } from '@tanstack/react-query'; import type { Draw } from '../../domain/draw/Draw.ts'; -import type { VariationKind } from '../../application/generateVariations.ts'; import { generateVariations } from '../../application/generateVariations.ts'; +import { VARIATION_DESCRIPTIONS, VARIATION_LABELS } from '../../application/variationLabels.ts'; import { drawRepository } from '../../application/drawRepository.ts'; import type { Grid } from '../../domain/grid/Grid.ts'; import { parseGrid } from '../../domain/grid/Grid.ts'; @@ -28,21 +28,6 @@ const READING_LABELS: Record = { 'tres-atypique': 'Très atypique', }; -const VARIATION_LABELS: Record = { - 'structurally-common': 'Structurellement courante', - balanced: 'Équilibrée', - 'anti-share': 'Anti-partage', -}; - -const VARIATION_DESCRIPTIONS: Record = { - 'structurally-common': - 'Reproduit un profil (somme, répartition par dizaine, écarts) proche des tirages historiquement fréquents.', - balanced: - 'Répartit les numéros sur les cinq dizaines et équilibre pairs/impairs, pour une grille aux caractéristiques neutres.', - 'anti-share': - 'Moins de chance de partager un gain, car elle évite les numéros calendaires (≤31) que beaucoup de joueurs choisissent - cela ne change pas vos chances de gagner.', -}; - const onlyDigits = (value: string): string => value.replace(/\D/g, '').slice(0, 2); // A value repeated at an earlier index makes this (later) field invalid diff --git a/src/pages/geometry/GeometryPage.module.scss b/src/pages/geometry/GeometryPage.module.scss index 9413a98..5f0f739 100644 --- a/src/pages/geometry/GeometryPage.module.scss +++ b/src/pages/geometry/GeometryPage.module.scss @@ -1,11 +1,76 @@ .page { display: flex; flex-direction: column; - gap: 2rem; + gap: var(--space-6); text-align: left; } -.gapChain { - font-family: monospace; - font-size: 1.1rem; +.section { + display: flex; + flex-direction: column; + gap: var(--space-3); + border: 1px solid var(--border); + border-radius: var(--radius-md); + background: var(--surface); + padding: var(--space-5); +} + +.description { + margin: 0; + color: var(--text-muted); + font-size: var(--text-sm); + line-height: 1.5; +} + +.referenceBanner { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: var(--space-4); + border: 1px solid var(--border); + border-radius: var(--radius-md); + background: var(--surface); + padding: var(--space-4) var(--space-5); +} + +.referenceMeta { + display: flex; + flex-direction: column; + gap: var(--space-1); +} + +.referenceLabel { + font-size: var(--text-xs); + text-transform: uppercase; + letter-spacing: 0.04em; + color: var(--text-muted); +} + +.referenceBubbles { + display: flex; + align-items: center; + gap: var(--space-2); +} + +.chainGroup { + display: flex; + flex-direction: column; + gap: var(--space-2); + padding: var(--space-3) 0; + + &:not(:last-child) { + border-bottom: 1px solid var(--border); + } +} + +.chainLabel { + font-size: var(--text-sm); + font-weight: 600; + color: var(--text); +} + +.chainNote { + margin: 0; + color: var(--text-muted); + font-size: var(--text-xs); } diff --git a/src/pages/geometry/GeometryPage.tsx b/src/pages/geometry/GeometryPage.tsx index 27c8d52..1c13533 100644 --- a/src/pages/geometry/GeometryPage.tsx +++ b/src/pages/geometry/GeometryPage.tsx @@ -11,7 +11,11 @@ import { XAxis, YAxis, } from 'recharts'; +import Bubble from '../../components/bubble/Bubble.tsx'; +import NumberChain from '../../components/bubble/NumberChain.tsx'; import { drawRepository } from '../../application/drawRepository.ts'; +import { generateVariations } from '../../application/generateVariations.ts'; +import { VARIATION_LABELS } from '../../application/variationLabels.ts'; import type { Draw } from '../../domain/draw/Draw.ts'; import { extractFeatures } from '../../domain/features/FeatureExtractor.ts'; import { buildGeometryDescriptor } from '../../domain/geometry/GeometryDescriptor.ts'; @@ -22,10 +26,14 @@ const DECADE_LABELS = ['1-10', '11-20', '21-30', '31-40', '41-50']; const NEIGHBOR_COUNT = 10; const EMPTY_DRAWS: Draw[] = []; -const buildGapChain = (numbers: readonly number[]): string => - numbers - .map((n, index) => (index === 0 ? String(n) : `—${n - numbers[index - 1]}→ ${n}`)) - .join(' '); +interface HighlightDotProps { + cx?: number; + cy?: number; +} + +const HighlightDot = ({ cx, cy }: HighlightDotProps) => ( + +); const GeometryPage = () => { const drawsQuery = useQuery({ @@ -55,6 +63,12 @@ const GeometryPage = () => { })); const referenceEntry = entries[0]; + + const referenceSumAmplitude = referenceEntry + ? [{ sum: referenceEntry.descriptor.sum, range: referenceEntry.descriptor.range }] + : []; + const referenceSumStdDev = referenceEntry ? [{ sum: referenceEntry.descriptor.sum, stdDev: referenceEntry.stdDev }] : []; + const neighbors = referenceEntry ? findNearestNeighbors( referenceEntry.descriptor, @@ -63,16 +77,47 @@ const GeometryPage = () => { ) : []; + const referenceVariation = useMemo(() => { + if (!referenceEntry || draws.length === 0) return null; + const [variation] = generateVariations( + { numbers: referenceEntry.draw.numbers, stars: referenceEntry.draw.stars }, + draws, + ); + return variation ?? null; + }, [referenceEntry, draws]); + return (

Géométrie

{drawsQuery.isLoading &&

Chargement...

} + {referenceEntry && ( +
+
+ Grille de référence + {referenceEntry.draw.date} +
+
+ {referenceEntry.draw.numbers.map((n) => ( + + ))} + {referenceEntry.draw.stars.map((s) => ( + + ))} +
+
+ )} + {entries.length > 0 && ( <> -
+

Somme × Amplitude

+

+ Chaque point est un tirage historique : la somme des 5 numéros (axe horizontal) et l'amplitude, l'écart + entre le plus petit et le plus grand numéro (axe vertical). Le point mis en évidence est la grille de + référence ci-dessus. +

@@ -80,12 +125,17 @@ const GeometryPage = () => { +
-
+

Somme × Écart-type

+

+ Même principe, avec l'écart-type (axe vertical) : il mesure si les 5 numéros sont plutôt regroupés ou + dispersés autour de leur moyenne. +

@@ -93,12 +143,17 @@ const GeometryPage = () => { +
-
+

Histogramme des signatures de dizaines

+

+ Sur tout l'historique, combien de numéros sont tombés dans chacune des cinq tranches de dizaines + (1-10, 11-20, 21-30, 31-40, 41-50). +

@@ -112,17 +167,38 @@ const GeometryPage = () => { {referenceEntry && ( <> -
+

Carte des écarts

-

Tirage de référence : {referenceEntry.draw.date}

-

{buildGapChain(referenceEntry.draw.numbers)}

+

+ Les 5 numéros d'une grille, dans l'ordre croissant, reliés par l'écart (la distance) qui les + sépare - une façon de visualiser sa "forme" plutôt que sa seule liste de numéros. +

+ +
+ Grille de référence ({referenceEntry.draw.date}) + +
+ + {referenceVariation && ( +
+ + Variation proposée : {VARIATION_LABELS[referenceVariation.kind]} + + +

+ Comparez les deux formes : mêmes principes de calcul, écarts différents. Cette variation n'est + pas présentée comme plus susceptible d'être tirée. +

+
+ )}
-
+

Voisins historiques (proximité géométrique)

-

- Les {NEIGHBOR_COUNT} tirages les plus proches du tirage de référence, par distance - géométrique - pas un graphe de proximité complet, mais la même donnée sous-jacente. +

+ Les {NEIGHBOR_COUNT} tirages passés dont la forme géométrique (somme, écarts, répartition) est la + plus proche de la grille de référence - pas un graphe de proximité complet, mais la même donnée + sous-jacente.

    {neighbors.map((neighbor) => ( From a0996794818bfc3b041c7f175037d0b4bb6c6fd1 Mon Sep 17 00:00:00 2001 From: keyobs Date: Wed, 12 Aug 2026 09:43:26 +0200 Subject: [PATCH 45/68] fix(geometry): link reference grid to /evaluation input, add source selector The reference grid on /geometry was silently just the latest historical draw, disconnected from whatever the user typed on /evaluation, and none of the charts marked it with a visually distinct color. Persist the last evaluated grid to localStorage and let /geometry pick between it, the latest draw, or a free-form grid typed directly on the page; give the reference grid a dedicated accent color across all charts, add legends, a decade-histogram marker, and a visual nearest-neighbors list. Co-Authored-By: Claude Sonnet 5 --- src/application/evaluatedGridRepository.ts | 3 + src/components/bubble/Bubble.module.scss | 6 +- .../chartLegend/ChartLegend.module.scss | 23 ++ src/components/chartLegend/ChartLegend.tsx | 23 ++ .../gridInput/GridInputForm.module.scss | 51 ++++ src/components/gridInput/GridInputForm.tsx | 159 +++++++++++++ src/index.css | 2 + .../repositories/EvaluatedGridRepository.ts | 7 + .../LocalStorageEvaluatedGridRepository.ts | 23 ++ .../evaluation/EvaluationPage.module.scss | 46 ---- src/pages/evaluation/EvaluationPage.test.tsx | 21 ++ src/pages/evaluation/EvaluationPage.tsx | 146 +----------- src/pages/geometry/GeometryPage.module.scss | 91 ++++++++ src/pages/geometry/GeometryPage.test.tsx | 60 ++++- src/pages/geometry/GeometryPage.tsx | 221 +++++++++++++++--- 15 files changed, 655 insertions(+), 227 deletions(-) create mode 100644 src/application/evaluatedGridRepository.ts create mode 100644 src/components/chartLegend/ChartLegend.module.scss create mode 100644 src/components/chartLegend/ChartLegend.tsx create mode 100644 src/components/gridInput/GridInputForm.module.scss create mode 100644 src/components/gridInput/GridInputForm.tsx create mode 100644 src/infrastructure/repositories/EvaluatedGridRepository.ts create mode 100644 src/infrastructure/repositories/LocalStorageEvaluatedGridRepository.ts diff --git a/src/application/evaluatedGridRepository.ts b/src/application/evaluatedGridRepository.ts new file mode 100644 index 0000000..48ae0e3 --- /dev/null +++ b/src/application/evaluatedGridRepository.ts @@ -0,0 +1,3 @@ +import { createLocalStorageEvaluatedGridRepository } from '../infrastructure/repositories/LocalStorageEvaluatedGridRepository.ts'; + +export const evaluatedGridRepository = createLocalStorageEvaluatedGridRepository(); diff --git a/src/components/bubble/Bubble.module.scss b/src/components/bubble/Bubble.module.scss index e8e0fb4..00922c7 100644 --- a/src/components/bubble/Bubble.module.scss +++ b/src/components/bubble/Bubble.module.scss @@ -23,7 +23,7 @@ } .highlight { - border-color: var(--accent-temporal); - background: color-mix(in srgb, var(--accent-temporal) 20%, var(--surface)); - box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent-temporal) 40%, transparent); + border-color: var(--accent-reference); + background: color-mix(in srgb, var(--accent-reference) 20%, var(--surface)); + box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent-reference) 40%, transparent); } diff --git a/src/components/chartLegend/ChartLegend.module.scss b/src/components/chartLegend/ChartLegend.module.scss new file mode 100644 index 0000000..b77289e --- /dev/null +++ b/src/components/chartLegend/ChartLegend.module.scss @@ -0,0 +1,23 @@ +.legend { + display: flex; + flex-wrap: wrap; + gap: var(--space-4); + list-style: none; + margin: 0; + padding: 0; + font-size: var(--text-xs); + color: var(--text-muted); +} + +.legendItem { + display: flex; + align-items: center; + gap: var(--space-2); +} + +.swatch { + width: 0.65rem; + height: 0.65rem; + border-radius: 50%; + flex-shrink: 0; +} diff --git a/src/components/chartLegend/ChartLegend.tsx b/src/components/chartLegend/ChartLegend.tsx new file mode 100644 index 0000000..94053a5 --- /dev/null +++ b/src/components/chartLegend/ChartLegend.tsx @@ -0,0 +1,23 @@ +import styles from './ChartLegend.module.scss'; + +interface ChartLegendItem { + color: string; + label: string; +} + +interface ChartLegendProps { + items: ChartLegendItem[]; +} + +const ChartLegend = ({ items }: ChartLegendProps) => ( +
      + {items.map((item) => ( +
    • +
    • + ))} +
    +); + +export default ChartLegend; diff --git a/src/components/gridInput/GridInputForm.module.scss b/src/components/gridInput/GridInputForm.module.scss new file mode 100644 index 0000000..65919ba --- /dev/null +++ b/src/components/gridInput/GridInputForm.module.scss @@ -0,0 +1,51 @@ +.formWrapper { + display: flex; + flex-direction: column; + gap: var(--space-3); +} + +.form { + display: flex; + flex-wrap: wrap; + align-items: flex-end; + gap: var(--space-5); +} + +.numberInput { + width: 3rem; + height: 3rem; + padding: 0; + text-align: center; + font-family: var(--mono); + font-size: var(--text-lg); + font-weight: 600; + margin-right: var(--space-2); + transition: background 0.15s ease, border-color 0.15s ease; +} + +.starInput { + border-radius: 50%; + border-color: var(--accent-originality); +} + +// !important: these are state overrides that must win regardless of which +// other input-styling class (e.g. .starInput's own border-color) is combined +// with them - and combined-class cascade order isn't reliably source order +// once Vite's dev-time CSS module injection is involved. +.inputValid { + background: color-mix(in srgb, var(--accent-confidence) 25%, var(--surface)) !important; + border-color: var(--accent-confidence) !important; +} + +.inputInvalid { + background: color-mix(in srgb, var(--danger) 25%, var(--surface)) !important; + border-color: var(--danger) !important; +} + +.error { + color: var(--danger); + background: var(--danger-surface); + border-radius: var(--radius-sm); + padding: var(--space-3) var(--space-4); + display: inline-block; +} diff --git a/src/components/gridInput/GridInputForm.tsx b/src/components/gridInput/GridInputForm.tsx new file mode 100644 index 0000000..bac765d --- /dev/null +++ b/src/components/gridInput/GridInputForm.tsx @@ -0,0 +1,159 @@ +import { useRef, useState } from 'react'; +import type { ChangeEvent, KeyboardEvent } from 'react'; +import type { Grid } from '../../domain/grid/Grid.ts'; +import { parseGrid } from '../../domain/grid/Grid.ts'; +import styles from './GridInputForm.module.scss'; + +const NUMBER_MIN = 1; +const NUMBER_MAX = 50; +const STAR_MIN = 1; +const STAR_MAX = 12; + +const EMPTY_NUMBERS = ['', '', '', '', '']; +const EMPTY_STARS = ['', '']; + +const onlyDigits = (value: string): string => value.replace(/\D/g, '').slice(0, 2); + +// A value repeated at an earlier index makes this (later) field invalid +// too, even if it's otherwise in range - a Grid needs 5 distinct numbers +// and 2 distinct stars, so only the *first* occurrence of a value can be +// valid. +const validityClass = (values: string[], index: number, min: number, max: number): string => { + const value = values[index]; + if (value === '') return ''; + if (values.slice(0, index).includes(value)) return styles.inputInvalid; + const parsed = Number(value); + return parsed >= min && parsed <= max ? styles.inputValid : styles.inputInvalid; +}; + +interface GridInputFormProps { + onSubmit: (grid: Grid) => void; + submitLabel: string; + submitButtonTestId?: string; +} + +const GridInputForm = ({ onSubmit, submitLabel, submitButtonTestId = 'evaluate-button' }: GridInputFormProps) => { + const [numberInputs, setNumberInputs] = useState(EMPTY_NUMBERS); + const [starInputs, setStarInputs] = useState(EMPTY_STARS); + const [error, setError] = useState(null); + + const numberFieldRefs = useRef<(HTMLInputElement | null)[]>([]); + const starFieldRefs = useRef<(HTMLInputElement | null)[]>([]); + const submitButtonRef = useRef(null); + + const handleSubmit = () => { + const numbers = numberInputs.map(Number); + const stars = starInputs.map(Number); + + if ([...numbers, ...stars].some((n) => Number.isNaN(n))) { + setError('Merci de remplir les 5 numéros et les 2 étoiles.'); + return; + } + + try { + const grid = parseGrid({ numbers, stars }); + setError(null); + onSubmit(grid); + } catch { + setError('Grille invalide : 5 numéros distincts entre 1 et 50, 2 étoiles distinctes entre 1 et 12.'); + } + }; + + // Lottery-ball-style entry: plain digit typing, auto-advance once a field + // is full, backspace on an empty field jumps back - avoids the classic + // type="number" pitfalls (spinner arrows, scroll-wheel changes value). + const createDigitFieldHandlers = ( + values: string[], + setValues: (updater: (previous: string[]) => string[]) => void, + refs: (HTMLInputElement | null)[], + nextFieldRef: HTMLElement | null, + ) => ({ + onChange: (index: number) => (event: ChangeEvent) => { + const digits = onlyDigits(event.target.value); + setValues((previous) => previous.map((current, i) => (i === index ? digits : current))); + if (digits.length === 2) { + (refs[index + 1] ?? nextFieldRef)?.focus(); + } + }, + onKeyDown: (index: number) => (event: KeyboardEvent) => { + if (event.key === 'Backspace' && values[index] === '' && index > 0) { + refs[index - 1]?.focus(); + } + }, + }); + + const numberHandlers = createDigitFieldHandlers( + numberInputs, + (updater) => setNumberInputs(updater), + numberFieldRefs.current, + starFieldRefs.current[0] ?? null, + ); + const starHandlers = createDigitFieldHandlers( + starInputs, + (updater) => setStarInputs(updater), + starFieldRefs.current, + submitButtonRef.current, + ); + + return ( +
    +
    +
    + Numéros (1-50) + {numberInputs.map((value, index) => ( + { + numberFieldRefs.current[index] = element; + }} + className={`${styles.numberInput} ${validityClass(numberInputs, index, NUMBER_MIN, NUMBER_MAX)}`} + type="text" + inputMode="numeric" + pattern="[0-9]*" + maxLength={2} + autoComplete="off" + value={value} + onChange={numberHandlers.onChange(index)} + onKeyDown={numberHandlers.onKeyDown(index)} + data-testid={`number-input-${index}`} + aria-label={`Numéro ${index + 1}`} + /> + ))} +
    +
    + Étoiles (1-12) + {starInputs.map((value, index) => ( + { + starFieldRefs.current[index] = element; + }} + className={`${styles.numberInput} ${styles.starInput} ${validityClass(starInputs, index, STAR_MIN, STAR_MAX)}`} + type="text" + inputMode="numeric" + pattern="[0-9]*" + maxLength={2} + autoComplete="off" + value={value} + onChange={starHandlers.onChange(index)} + onKeyDown={starHandlers.onKeyDown(index)} + data-testid={`star-input-${index}`} + aria-label={`Étoile ${index + 1}`} + /> + ))} +
    + +
    + + {error && ( +

    + {error} +

    + )} +
    + ); +}; + +export default GridInputForm; diff --git a/src/index.css b/src/index.css index 50e4093..9db6410 100644 --- a/src/index.css +++ b/src/index.css @@ -14,6 +14,7 @@ --accent-temporal: #b9a3fb; --accent-confidence: #5fe0b5; --accent-primary: var(--accent-structure); + --accent-reference: #ff5eae; --danger: #f2707d; --danger-surface: rgba(242, 112, 125, 0.1); @@ -70,6 +71,7 @@ --accent-originality: #c9791f; --accent-temporal: #7250c9; --accent-confidence: #12886b; + --accent-reference: #c2185b; --danger: #d4394b; --danger-surface: rgba(212, 57, 75, 0.08); diff --git a/src/infrastructure/repositories/EvaluatedGridRepository.ts b/src/infrastructure/repositories/EvaluatedGridRepository.ts new file mode 100644 index 0000000..23a852c --- /dev/null +++ b/src/infrastructure/repositories/EvaluatedGridRepository.ts @@ -0,0 +1,7 @@ +import type { Grid } from '../../domain/grid/Grid.ts'; + +export interface EvaluatedGridRepository { + getLast(): Grid | null; + save(grid: Grid): void; + clear(): void; +} diff --git a/src/infrastructure/repositories/LocalStorageEvaluatedGridRepository.ts b/src/infrastructure/repositories/LocalStorageEvaluatedGridRepository.ts new file mode 100644 index 0000000..d256500 --- /dev/null +++ b/src/infrastructure/repositories/LocalStorageEvaluatedGridRepository.ts @@ -0,0 +1,23 @@ +import type { Grid } from '../../domain/grid/Grid.ts'; +import { gridSchema } from '../../domain/grid/Grid.ts'; +import type { EvaluatedGridRepository } from './EvaluatedGridRepository.ts'; + +const STORAGE_KEY = 'izeetok:evaluated-grid'; + +export const createLocalStorageEvaluatedGridRepository = (): EvaluatedGridRepository => ({ + getLast: () => { + const raw = localStorage.getItem(STORAGE_KEY); + if (!raw) return null; + try { + return gridSchema.parse(JSON.parse(raw)); + } catch { + return null; + } + }, + save: (grid: Grid) => { + localStorage.setItem(STORAGE_KEY, JSON.stringify(grid)); + }, + clear: () => { + localStorage.removeItem(STORAGE_KEY); + }, +}); diff --git a/src/pages/evaluation/EvaluationPage.module.scss b/src/pages/evaluation/EvaluationPage.module.scss index 468e681..bffedef 100644 --- a/src/pages/evaluation/EvaluationPage.module.scss +++ b/src/pages/evaluation/EvaluationPage.module.scss @@ -5,58 +5,12 @@ text-align: left; } -.form { - display: flex; - flex-wrap: wrap; - align-items: flex-end; - gap: var(--space-5); -} - -.numberInput { - width: 3rem; - height: 3rem; - padding: 0; - text-align: center; - font-family: var(--mono); - font-size: var(--text-lg); - font-weight: 600; - margin-right: var(--space-2); - transition: background 0.15s ease, border-color 0.15s ease; -} - -.starInput { - border-radius: 50%; - border-color: var(--accent-originality); -} - -// !important: these are state overrides that must win regardless of which -// other input-styling class (e.g. .starInput's own border-color) is combined -// with them - and combined-class cascade order isn't reliably source order -// once Vite's dev-time CSS module injection is involved. -.inputValid { - background: color-mix(in srgb, var(--accent-confidence) 25%, var(--surface)) !important; - border-color: var(--accent-confidence) !important; -} - -.inputInvalid { - background: color-mix(in srgb, var(--danger) 25%, var(--surface)) !important; - border-color: var(--danger) !important; -} - .scores { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: var(--space-4); } -.error { - color: var(--danger); - background: var(--danger-surface); - border-radius: var(--radius-sm); - padding: var(--space-3) var(--space-4); - display: inline-block; -} - .variationsNote { color: var(--text-muted); font-size: var(--text-sm); diff --git a/src/pages/evaluation/EvaluationPage.test.tsx b/src/pages/evaluation/EvaluationPage.test.tsx index 943a709..163feb1 100644 --- a/src/pages/evaluation/EvaluationPage.test.tsx +++ b/src/pages/evaluation/EvaluationPage.test.tsx @@ -3,11 +3,13 @@ import { screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { renderWithProviders } from '../../shared/renderWithProviders.tsx'; import { REAL_CSV_TEXT } from '../../shared/testCsvFixture.ts'; +import { evaluatedGridRepository } from '../../application/evaluatedGridRepository.ts'; import EvaluationPage from './EvaluationPage.tsx'; describe('EvaluationPage', () => { afterEach(() => { vi.unstubAllGlobals(); + localStorage.clear(); }); it('renders without crashing', () => { @@ -37,4 +39,23 @@ describe('EvaluationPage', () => { await waitFor(() => expect(screen.getAllByTestId('score-card')).toHaveLength(4)); expect(screen.getByTestId('variations').querySelectorAll('li')).toHaveLength(3); }); + + it('saves the evaluated grid so /geometry can reuse it as reference', async () => { + vi.stubGlobal('fetch', () => Promise.resolve(new Response(REAL_CSV_TEXT))); + const user = userEvent.setup(); + + renderWithProviders(); + + const numbers = [3, 7, 19, 31, 42]; + const stars = [2, 9]; + for (const [index, value] of numbers.entries()) { + await user.type(screen.getByTestId(`number-input-${index}`), String(value)); + } + for (const [index, value] of stars.entries()) { + await user.type(screen.getByTestId(`star-input-${index}`), String(value)); + } + await user.click(screen.getByTestId('evaluate-button')); + + await waitFor(() => expect(evaluatedGridRepository.getLast()).toEqual({ numbers, stars })); + }); }); diff --git a/src/pages/evaluation/EvaluationPage.tsx b/src/pages/evaluation/EvaluationPage.tsx index b7fe2c9..f569b0f 100644 --- a/src/pages/evaluation/EvaluationPage.tsx +++ b/src/pages/evaluation/EvaluationPage.tsx @@ -1,24 +1,17 @@ -import { useMemo, useRef, useState } from 'react'; -import type { ChangeEvent, KeyboardEvent } from 'react'; +import { useMemo, useState } from 'react'; import { useQuery } from '@tanstack/react-query'; import type { Draw } from '../../domain/draw/Draw.ts'; import { generateVariations } from '../../application/generateVariations.ts'; import { VARIATION_DESCRIPTIONS, VARIATION_LABELS } from '../../application/variationLabels.ts'; import { drawRepository } from '../../application/drawRepository.ts'; +import { evaluatedGridRepository } from '../../application/evaluatedGridRepository.ts'; import type { Grid } from '../../domain/grid/Grid.ts'; -import { parseGrid } from '../../domain/grid/Grid.ts'; import type { EvaluationScores, ReadingMatrixLabel } from '../../domain/scoring/evaluateGrid.ts'; import { classifyReading, evaluateGrid } from '../../domain/scoring/evaluateGrid.ts'; +import GridInputForm from '../../components/gridInput/GridInputForm.tsx'; import ScoreCard from './ScoreCard.tsx'; import styles from './EvaluationPage.module.scss'; -const NUMBER_MIN = 1; -const NUMBER_MAX = 50; -const STAR_MIN = 1; -const STAR_MAX = 12; - -const EMPTY_NUMBERS = ['', '', '', '', '']; -const EMPTY_STARS = ['', '']; const EMPTY_HISTORY: Draw[] = []; const READING_LABELS: Record = { @@ -28,29 +21,8 @@ const READING_LABELS: Record = { 'tres-atypique': 'Très atypique', }; -const onlyDigits = (value: string): string => value.replace(/\D/g, '').slice(0, 2); - -// A value repeated at an earlier index makes this (later) field invalid -// too, even if it's otherwise in range - a Grid needs 5 distinct numbers -// and 2 distinct stars, so only the *first* occurrence of a value can be -// valid. -const validityClass = (values: string[], index: number, min: number, max: number): string => { - const value = values[index]; - if (value === '') return ''; - if (values.slice(0, index).includes(value)) return styles.inputInvalid; - const parsed = Number(value); - return parsed >= min && parsed <= max ? styles.inputValid : styles.inputInvalid; -}; - const EvaluationPage = () => { - const [numberInputs, setNumberInputs] = useState(EMPTY_NUMBERS); - const [starInputs, setStarInputs] = useState(EMPTY_STARS); const [grid, setGrid] = useState(null); - const [error, setError] = useState(null); - - const numberFieldRefs = useRef<(HTMLInputElement | null)[]>([]); - const starFieldRefs = useRef<(HTMLInputElement | null)[]>([]); - const evaluateButtonRef = useRef(null); const historyQuery = useQuery({ queryKey: ['draws', 'all'], @@ -58,61 +30,11 @@ const EvaluationPage = () => { }); const history = historyQuery.data ?? EMPTY_HISTORY; - const handleEvaluate = () => { - const numbers = numberInputs.map(Number); - const stars = starInputs.map(Number); - - if ([...numbers, ...stars].some((n) => Number.isNaN(n))) { - setGrid(null); - setError('Merci de remplir les 5 numéros et les 2 étoiles.'); - return; - } - - try { - setGrid(parseGrid({ numbers, stars })); - setError(null); - } catch { - setGrid(null); - setError('Grille invalide : 5 numéros distincts entre 1 et 50, 2 étoiles distinctes entre 1 et 12.'); - } + const handleGridSubmit = (submittedGrid: Grid) => { + setGrid(submittedGrid); + evaluatedGridRepository.save(submittedGrid); }; - // Lottery-ball-style entry: plain digit typing, auto-advance once a field - // is full, backspace on an empty field jumps back - avoids the classic - // type="number" pitfalls (spinner arrows, scroll-wheel changes value). - const createDigitFieldHandlers = ( - values: string[], - setValues: (updater: (previous: string[]) => string[]) => void, - refs: (HTMLInputElement | null)[], - nextFieldRef: HTMLElement | null, - ) => ({ - onChange: (index: number) => (event: ChangeEvent) => { - const digits = onlyDigits(event.target.value); - setValues((previous) => previous.map((current, i) => (i === index ? digits : current))); - if (digits.length === 2) { - (refs[index + 1] ?? nextFieldRef)?.focus(); - } - }, - onKeyDown: (index: number) => (event: KeyboardEvent) => { - if (event.key === 'Backspace' && values[index] === '' && index > 0) { - refs[index - 1]?.focus(); - } - }, - }); - - const numberHandlers = createDigitFieldHandlers( - numberInputs, - (updater) => setNumberInputs(updater), - numberFieldRefs.current, - starFieldRefs.current[0] ?? null, - ); - const starHandlers = createDigitFieldHandlers( - starInputs, - (updater) => setStarInputs(updater), - starFieldRefs.current, - evaluateButtonRef.current, - ); - const scores = useMemo( () => (grid && history.length > 0 ? evaluateGrid(grid, history) : null), [grid, history], @@ -133,61 +55,7 @@ const EvaluationPage = () => {

    Évaluation d'une grille

    -
    -
    - Numéros (1-50) - {numberInputs.map((value, index) => ( - { - numberFieldRefs.current[index] = element; - }} - className={`${styles.numberInput} ${validityClass(numberInputs, index, NUMBER_MIN, NUMBER_MAX)}`} - type="text" - inputMode="numeric" - pattern="[0-9]*" - maxLength={2} - autoComplete="off" - value={value} - onChange={numberHandlers.onChange(index)} - onKeyDown={numberHandlers.onKeyDown(index)} - data-testid={`number-input-${index}`} - aria-label={`Numéro ${index + 1}`} - /> - ))} -
    -
    - Étoiles (1-12) - {starInputs.map((value, index) => ( - { - starFieldRefs.current[index] = element; - }} - className={`${styles.numberInput} ${styles.starInput} ${validityClass(starInputs, index, STAR_MIN, STAR_MAX)}`} - type="text" - inputMode="numeric" - pattern="[0-9]*" - maxLength={2} - autoComplete="off" - value={value} - onChange={starHandlers.onChange(index)} - onKeyDown={starHandlers.onKeyDown(index)} - data-testid={`star-input-${index}`} - aria-label={`Étoile ${index + 1}`} - /> - ))} -
    - -
    - - {error && ( -

    - {error} -

    - )} + {historyQuery.isLoading &&

    Chargement de l'historique...

    } diff --git a/src/pages/geometry/GeometryPage.module.scss b/src/pages/geometry/GeometryPage.module.scss index 5f0f739..2745860 100644 --- a/src/pages/geometry/GeometryPage.module.scss +++ b/src/pages/geometry/GeometryPage.module.scss @@ -5,6 +5,51 @@ text-align: left; } +.intro { + margin: 0; + color: var(--text-muted); + font-size: var(--text-sm); + line-height: 1.6; + max-width: 62ch; +} + +.sourceSelector { + display: flex; + flex-wrap: wrap; + gap: var(--space-2); +} + +.sourceButton, +.sourceButtonActive { + background: var(--surface); + color: var(--text); + border: 1px solid var(--border-strong); + + &:disabled { + opacity: 0.5; + cursor: not-allowed; + } +} + +.sourceButtonActive { + background: var(--accent-primary); + color: #0a0c11; + border-color: var(--accent-primary); +} + +.sourceHint { + margin: 0; + color: var(--text-muted); + font-size: var(--text-xs); +} + +.emptyCustomHint { + margin: 0; + color: var(--text-muted); + font-size: var(--text-sm); + font-style: italic; +} + .section { display: flex; flex-direction: column; @@ -74,3 +119,49 @@ color: var(--text-muted); font-size: var(--text-xs); } + +.neighborList { + list-style: none; + margin: 0; + padding: 0; + display: flex; + flex-direction: column; + gap: var(--space-3); +} + +.neighborRow { + display: grid; + grid-template-columns: 6rem 1fr 8rem; + align-items: center; + gap: var(--space-3); + padding: var(--space-2) 0; + + &:not(:last-child) { + border-bottom: 1px solid var(--border); + } +} + +.neighborDate { + font-size: var(--text-xs); + color: var(--text-muted); + font-family: var(--mono); +} + +.neighborBubbles { + display: flex; + gap: var(--space-1); +} + +.neighborProximity { + height: 0.5rem; + border-radius: var(--radius-sm); + background: var(--border); + overflow: hidden; +} + +.neighborProximityFill { + display: block; + height: 100%; + background: var(--accent-reference); + border-radius: var(--radius-sm); +} diff --git a/src/pages/geometry/GeometryPage.test.tsx b/src/pages/geometry/GeometryPage.test.tsx index 8b38f78..3b1f6d9 100644 --- a/src/pages/geometry/GeometryPage.test.tsx +++ b/src/pages/geometry/GeometryPage.test.tsx @@ -1,12 +1,19 @@ -import { afterEach, describe, expect, it, vi } from 'vitest'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import { renderWithProviders } from '../../shared/renderWithProviders.tsx'; import { REAL_CSV_TEXT } from '../../shared/testCsvFixture.ts'; +import { evaluatedGridRepository } from '../../application/evaluatedGridRepository.ts'; import GeometryPage from './GeometryPage.tsx'; describe('GeometryPage', () => { + beforeEach(() => { + localStorage.clear(); + }); + afterEach(() => { vi.unstubAllGlobals(); + localStorage.clear(); }); it('renders without crashing and shows the gap map and nearest neighbors once loaded', async () => { @@ -18,4 +25,55 @@ describe('GeometryPage', () => { await waitFor(() => expect(screen.getAllByTestId('neighbor-row')).toHaveLength(10)); expect(screen.getByTestId('gap-map')).toBeInTheDocument(); }); + + it('defaults to the latest draw and disables the "evaluated" source when none was saved', async () => { + vi.stubGlobal('fetch', () => Promise.resolve(new Response(REAL_CSV_TEXT))); + + renderWithProviders(); + + await waitFor(() => expect(screen.getByTestId('reference-grid-banner')).toBeInTheDocument()); + expect(screen.getByTestId('reference-source-latest')).toHaveAttribute('aria-checked', 'true'); + expect(screen.getByTestId('reference-source-evaluated')).toBeDisabled(); + }); + + it('uses the grid evaluated on /evaluation as the reference when one was saved', async () => { + vi.stubGlobal('fetch', () => Promise.resolve(new Response(REAL_CSV_TEXT))); + evaluatedGridRepository.save({ numbers: [3, 7, 19, 31, 42], stars: [2, 9] }); + + renderWithProviders(); + + await waitFor(() => + expect(screen.getByTestId('reference-source-evaluated')).toHaveAttribute('aria-checked', 'true'), + ); + const banner = screen.getByTestId('reference-grid-banner'); + for (const value of [3, 7, 19, 31, 42, 2, 9]) { + expect(banner).toHaveTextContent(String(value)); + } + }); + + it('lets the user type a grid directly on the page to analyze it', async () => { + vi.stubGlobal('fetch', () => Promise.resolve(new Response(REAL_CSV_TEXT))); + const user = userEvent.setup(); + + renderWithProviders(); + await waitFor(() => expect(screen.getByTestId('reference-grid-banner')).toBeInTheDocument()); + + await user.click(screen.getByTestId('reference-source-custom')); + expect(screen.getByTestId('custom-grid-empty')).toBeInTheDocument(); + + const numbers = [4, 8, 15, 23, 44]; + const stars = [1, 5]; + for (const [index, value] of numbers.entries()) { + await user.type(screen.getByTestId(`number-input-${index}`), String(value)); + } + for (const [index, value] of stars.entries()) { + await user.type(screen.getByTestId(`star-input-${index}`), String(value)); + } + await user.click(screen.getByTestId('custom-grid-submit')); + + const banner = screen.getByTestId('reference-grid-banner'); + for (const value of [...numbers, ...stars]) { + expect(banner).toHaveTextContent(String(value)); + } + }); }); diff --git a/src/pages/geometry/GeometryPage.tsx b/src/pages/geometry/GeometryPage.tsx index 1c13533..9720dc1 100644 --- a/src/pages/geometry/GeometryPage.tsx +++ b/src/pages/geometry/GeometryPage.tsx @@ -1,4 +1,4 @@ -import { useMemo } from 'react'; +import { useMemo, useState } from 'react'; import { useQuery } from '@tanstack/react-query'; import { Bar, @@ -13,10 +13,14 @@ import { } from 'recharts'; import Bubble from '../../components/bubble/Bubble.tsx'; import NumberChain from '../../components/bubble/NumberChain.tsx'; +import ChartLegend from '../../components/chartLegend/ChartLegend.tsx'; +import GridInputForm from '../../components/gridInput/GridInputForm.tsx'; import { drawRepository } from '../../application/drawRepository.ts'; +import { evaluatedGridRepository } from '../../application/evaluatedGridRepository.ts'; import { generateVariations } from '../../application/generateVariations.ts'; import { VARIATION_LABELS } from '../../application/variationLabels.ts'; import type { Draw } from '../../domain/draw/Draw.ts'; +import type { Grid } from '../../domain/grid/Grid.ts'; import { extractFeatures } from '../../domain/features/FeatureExtractor.ts'; import { buildGeometryDescriptor } from '../../domain/geometry/GeometryDescriptor.ts'; import { findNearestNeighbors } from '../../domain/geometry/nearestNeighbors.ts'; @@ -26,13 +30,45 @@ const DECADE_LABELS = ['1-10', '11-20', '21-30', '31-40', '41-50']; const NEIGHBOR_COUNT = 10; const EMPTY_DRAWS: Draw[] = []; +type ReferenceSource = 'latest' | 'evaluated' | 'custom'; + +const SOURCE_LABELS: Record = { + latest: 'Dernier tirage', + evaluated: 'Grille évaluée', + custom: 'Saisie libre', +}; + +const SOURCE_HINTS: Record = { + latest: 'Le dernier tirage officiel enregistré, pris comme exemple.', + evaluated: "La grille que vous avez évaluée sur /evaluation.", + custom: "Entrez n'importe quelle grille pour la situer dans l'historique.", +}; + interface HighlightDotProps { cx?: number; cy?: number; } const HighlightDot = ({ cx, cy }: HighlightDotProps) => ( - + +); + +interface DecadeBarProps { + x?: number; + y?: number; + width?: number; + height?: number; + fill?: string; + payload?: { hasReference: boolean }; +} + +const DecadeBar = ({ x = 0, y = 0, width = 0, height = 0, fill, payload }: DecadeBarProps) => ( + + + {payload?.hasReference && ( + + )} + ); const GeometryPage = () => { @@ -42,6 +78,11 @@ const GeometryPage = () => { }); const draws = drawsQuery.data ?? EMPTY_DRAWS; + const evaluatedGrid = useMemo(() => evaluatedGridRepository.getLast(), []); + + const [source, setSource] = useState(evaluatedGrid ? 'evaluated' : 'latest'); + const [customGrid, setCustomGrid] = useState(null); + const entries = useMemo( () => [...draws] @@ -54,59 +95,120 @@ const GeometryPage = () => { [draws], ); + const latestEntry = entries[0]; + const latestGrid: Grid | null = latestEntry ? { numbers: latestEntry.draw.numbers, stars: latestEntry.draw.stars } : null; + + const referenceGrid: Grid | null = + source === 'latest' ? latestGrid : source === 'evaluated' ? evaluatedGrid : customGrid; + + const referenceDescriptor = useMemo( + () => (referenceGrid ? buildGeometryDescriptor(referenceGrid) : null), + [referenceGrid], + ); + const referenceStdDev = useMemo( + () => (referenceGrid ? extractFeatures(referenceGrid).values.stdDev : null), + [referenceGrid], + ); + const sumAmplitudeData = entries.map((entry) => ({ sum: entry.descriptor.sum, range: entry.descriptor.range })); const sumStdDevData = entries.map((entry) => ({ sum: entry.descriptor.sum, stdDev: entry.stdDev })); const decadeHistogram = DECADE_LABELS.map((label, index) => ({ decade: label, count: entries.reduce((acc, entry) => acc + entry.descriptor.decadeBuckets[index], 0), + hasReference: Boolean(referenceDescriptor && referenceDescriptor.decadeBuckets[index] > 0), })); - const referenceEntry = entries[0]; - - const referenceSumAmplitude = referenceEntry - ? [{ sum: referenceEntry.descriptor.sum, range: referenceEntry.descriptor.range }] + const referenceSumAmplitude = referenceDescriptor + ? [{ sum: referenceDescriptor.sum, range: referenceDescriptor.range }] : []; - const referenceSumStdDev = referenceEntry ? [{ sum: referenceEntry.descriptor.sum, stdDev: referenceEntry.stdDev }] : []; + const referenceSumStdDev = + referenceDescriptor && referenceStdDev != null ? [{ sum: referenceDescriptor.sum, stdDev: referenceStdDev }] : []; - const neighbors = referenceEntry + // Excludes the reference draw from its own neighbor candidates only when it + // actually comes from the historical list ('latest') - an evaluated/custom + // grid isn't part of `entries`, so nothing needs excluding for it. + const neighborCandidates = source === 'latest' ? entries.slice(1) : entries; + const neighbors = referenceDescriptor ? findNearestNeighbors( - referenceEntry.descriptor, - entries.slice(1).map((entry) => ({ item: entry.draw, descriptor: entry.descriptor })), + referenceDescriptor, + neighborCandidates.map((entry) => ({ item: entry.draw, descriptor: entry.descriptor })), NEIGHBOR_COUNT, ) : []; + const maxNeighborDistance = neighbors.length > 0 ? Math.max(...neighbors.map((n) => n.distance)) : 0; const referenceVariation = useMemo(() => { - if (!referenceEntry || draws.length === 0) return null; - const [variation] = generateVariations( - { numbers: referenceEntry.draw.numbers, stars: referenceEntry.draw.stars }, - draws, - ); + if (!referenceGrid || draws.length === 0) return null; + const [variation] = generateVariations(referenceGrid, draws); return variation ?? null; - }, [referenceEntry, draws]); + }, [referenceGrid, draws]); return (

    Géométrie

    +

    + Cette page situe une grille - le dernier tirage, votre grille évaluée sur /evaluation, ou une + saisie libre - au sein de l'historique complet des tirages, à travers plusieurs mesures + géométriques (somme, amplitude, écart-type, répartition par dizaines, écarts entre numéros). + Elle ne prédit rien : toutes les grilles ont la même probabilité théorique de sortir - elle + sert seulement à comparer une forme à des formes déjà observées. +

    {drawsQuery.isLoading &&

    Chargement...

    } - {referenceEntry && ( +
    + {(Object.keys(SOURCE_LABELS) as ReferenceSource[]).map((candidate) => ( + + ))} +
    +

    {SOURCE_HINTS[source]}

    + + {source === 'custom' && ( + + )} + + {referenceGrid ? (
    Grille de référence - {referenceEntry.draw.date} + {source === 'latest' ? latestEntry?.draw.date : SOURCE_LABELS[source]}
    - {referenceEntry.draw.numbers.map((n) => ( + {referenceGrid.numbers.map((n) => ( ))} - {referenceEntry.draw.stars.map((s) => ( + {referenceGrid.stars.map((s) => ( ))}
    + ) : ( + source === 'custom' && ( +

    + Saisissez une grille ci-dessus pour l'analyser. +

    + ) )} {entries.length > 0 && ( @@ -115,8 +217,9 @@ const GeometryPage = () => {

    Somme × Amplitude

    Chaque point est un tirage historique : la somme des 5 numéros (axe horizontal) et l'amplitude, l'écart - entre le plus petit et le plus grand numéro (axe vertical). Le point mis en évidence est la grille de - référence ci-dessus. + entre le plus petit et le plus grand numéro (axe vertical). Un point isolé, loin du nuage principal, + signifie une somme ou une amplitude inhabituelle par rapport à l'historique - ça ne présage en rien de + la suite.

    @@ -128,13 +231,20 @@ const GeometryPage = () => { +

Somme × Écart-type

Même principe, avec l'écart-type (axe vertical) : il mesure si les 5 numéros sont plutôt regroupés ou - dispersés autour de leur moyenne. + dispersés autour de leur moyenne. Là encore, un point excentré indique juste un profil rare dans + l'historique, pas un signal prédictif.

@@ -146,13 +256,21 @@ const GeometryPage = () => { +

Histogramme des signatures de dizaines

Sur tout l'historique, combien de numéros sont tombés dans chacune des cinq tranches de dizaines - (1-10, 11-20, 21-30, 31-40, 41-50). + (1-10, 11-20, 21-30, 31-40, 41-50). Le repère au-dessus d'une barre indique les dizaines où tombent les + numéros de la grille de référence - une barre plus haute que les autres reflète simplement une + fréquence historique, pas une dizaine "due".

@@ -160,12 +278,18 @@ const GeometryPage = () => { - + +
- {referenceEntry && ( + {referenceGrid && referenceDescriptor && ( <>

Carte des écarts

@@ -175,8 +299,11 @@ const GeometryPage = () => {

- Grille de référence ({referenceEntry.draw.date}) - + + Grille de référence + {source === 'latest' && latestEntry ? ` (${latestEntry.draw.date})` : ''} + +
{referenceVariation && ( @@ -196,17 +323,35 @@ const GeometryPage = () => {

Voisins historiques (proximité géométrique)

- Les {NEIGHBOR_COUNT} tirages passés dont la forme géométrique (somme, écarts, répartition) est la - plus proche de la grille de référence - pas un graphe de proximité complet, mais la même donnée - sous-jacente. + Ce ne sont pas les derniers tirages : ce sont, parmi tout l'historique, les {NEIGHBOR_COUNT} dont la + forme géométrique (somme, écarts, répartition) ressemble le plus à la grille de référence. Une + ressemblance de forme ne signifie pas une probabilité de sortie plus élevée pour l'un ou l'autre - + la barre ci-dessous représente juste cette proximité relative, du plus proche au moins proche.

-
    - {neighbors.map((neighbor) => ( -
  • - {neighbor.item.date} — {neighbor.item.numbers.join(' · ')} (distance{' '} - {neighbor.distance.toFixed(3)}) -
  • - ))} +
      + {neighbors.map((neighbor) => { + const proximity = maxNeighborDistance > 0 ? 1 - neighbor.distance / maxNeighborDistance : 1; + return ( +
    • + {neighbor.item.date} +
      + {neighbor.item.numbers.map((n) => ( + + ))} +
      +
      + +
      +
    • + ); + })}
From 02cada01f358c7d1e96f6fa5a62438fc5a7afdfb Mon Sep 17 00:00:00 2001 From: keyobs Date: Wed, 12 Aug 2026 10:08:03 +0200 Subject: [PATCH 46/68] chore(agents): stop ignoring .claude and .agents so agent tooling is versioned Track CLAUDE.md, the skills (.agents/skills + .claude/skills symlinks), CONTEXT.md and docs/adr/ - they were sitting on disk but .gitignore's blanket ".claude" rule kept CLAUDE.md itself from ever being committed. settings.local.json stays out via the user's global gitignore. Prepares the ground for .claude/agents/ and .agents/workflows/ (agent-integration). Co-Authored-By: Claude Sonnet 5 --- .agents/skills/domain-modeling/ADR-FORMAT.md | 47 +++++ .../skills/domain-modeling/CONTEXT-FORMAT.md | 60 ++++++ .agents/skills/domain-modeling/SKILL.md | 74 +++++++ .../skills/domain-modeling/agents/openai.yaml | 3 + .agents/skills/grill-me/SKILL.md | 7 + .agents/skills/grill-me/agents/openai.yaml | 5 + .agents/skills/grill-with-docs/SKILL.md | 7 + .../skills/grill-with-docs/agents/openai.yaml | 5 + .agents/skills/grilling/SKILL.md | 22 +++ .agents/skills/grilling/agents/openai.yaml | 3 + .claude/CLAUDE.md | 181 ++++++++++++++++++ .claude/skills/domain-modeling | 1 + .claude/skills/grill-me | 1 + .claude/skills/grill-with-docs | 1 + .claude/skills/grilling | 1 + .gitignore | 5 - CONTEXT.md | 57 ++++++ .../adr/0001-client-side-only-architecture.md | 7 + ...002-vertical-spike-before-full-buildout.md | 5 + 19 files changed, 487 insertions(+), 5 deletions(-) create mode 100644 .agents/skills/domain-modeling/ADR-FORMAT.md create mode 100644 .agents/skills/domain-modeling/CONTEXT-FORMAT.md create mode 100644 .agents/skills/domain-modeling/SKILL.md create mode 100644 .agents/skills/domain-modeling/agents/openai.yaml create mode 100644 .agents/skills/grill-me/SKILL.md create mode 100644 .agents/skills/grill-me/agents/openai.yaml create mode 100644 .agents/skills/grill-with-docs/SKILL.md create mode 100644 .agents/skills/grill-with-docs/agents/openai.yaml create mode 100644 .agents/skills/grilling/SKILL.md create mode 100644 .agents/skills/grilling/agents/openai.yaml create mode 100644 .claude/CLAUDE.md create mode 120000 .claude/skills/domain-modeling create mode 120000 .claude/skills/grill-me create mode 120000 .claude/skills/grill-with-docs create mode 120000 .claude/skills/grilling create mode 100644 CONTEXT.md create mode 100644 docs/adr/0001-client-side-only-architecture.md create mode 100644 docs/adr/0002-vertical-spike-before-full-buildout.md diff --git a/.agents/skills/domain-modeling/ADR-FORMAT.md b/.agents/skills/domain-modeling/ADR-FORMAT.md new file mode 100644 index 0000000..da7e78e --- /dev/null +++ b/.agents/skills/domain-modeling/ADR-FORMAT.md @@ -0,0 +1,47 @@ +# ADR Format + +ADRs live in `docs/adr/` and use sequential numbering: `0001-slug.md`, `0002-slug.md`, etc. + +Create the `docs/adr/` directory lazily — only when the first ADR is needed. + +## Template + +```md +# {Short title of the decision} + +{1-3 sentences: what's the context, what did we decide, and why.} +``` + +That's it. An ADR can be a single paragraph. The value is in recording *that* a decision was made and *why* — not in filling out sections. + +## Optional sections + +Only include these when they add genuine value. Most ADRs won't need them. + +- **Status** frontmatter (`proposed | accepted | deprecated | superseded by ADR-NNNN`) — useful when decisions are revisited +- **Considered Options** — only when the rejected alternatives are worth remembering +- **Consequences** — only when non-obvious downstream effects need to be called out + +## Numbering + +Scan `docs/adr/` for the highest existing number and increment by one. + +## When to offer an ADR + +All three of these must be true: + +1. **Hard to reverse** — the cost of changing your mind later is meaningful +2. **Surprising without context** — a future reader will look at the code and wonder "why on earth did they do it this way?" +3. **The result of a real trade-off** — there were genuine alternatives and you picked one for specific reasons + +If a decision is easy to reverse, skip it — you'll just reverse it. If it's not surprising, nobody will wonder why. If there was no real alternative, there's nothing to record beyond "we did the obvious thing." + +### What qualifies + +- **Architectural shape.** "We're using a monorepo." "The write model is event-sourced, the read model is projected into Postgres." +- **Integration patterns between contexts.** "Ordering and Billing communicate via domain events, not synchronous HTTP." +- **Technology choices that carry lock-in.** Database, message bus, auth provider, deployment target. Not every library — just the ones that would take a quarter to swap out. +- **Boundary and scope decisions.** "Customer data is owned by the Customer context; other contexts reference it by ID only." The explicit no-s are as valuable as the yes-s. +- **Deliberate deviations from the obvious path.** "We're using manual SQL instead of an ORM because X." Anything where a reasonable reader would assume the opposite. These stop the next engineer from "fixing" something that was deliberate. +- **Constraints not visible in the code.** "We can't use AWS because of compliance requirements." "Response times must be under 200ms because of the partner API contract." +- **Rejected alternatives when the rejection is non-obvious.** If you considered GraphQL and picked REST for subtle reasons, record it — otherwise someone will suggest GraphQL again in six months. diff --git a/.agents/skills/domain-modeling/CONTEXT-FORMAT.md b/.agents/skills/domain-modeling/CONTEXT-FORMAT.md new file mode 100644 index 0000000..eaf2a18 --- /dev/null +++ b/.agents/skills/domain-modeling/CONTEXT-FORMAT.md @@ -0,0 +1,60 @@ +# CONTEXT.md Format + +## Structure + +```md +# {Context Name} + +{One or two sentence description of what this context is and why it exists.} + +## Language + +**Order**: +{A one or two sentence description of the term} +_Avoid_: Purchase, transaction + +**Invoice**: +A request for payment sent to a customer after delivery. +_Avoid_: Bill, payment request + +**Customer**: +A person or organization that places orders. +_Avoid_: Client, buyer, account +``` + +## Rules + +- **Be opinionated.** When multiple words exist for the same concept, pick the best one and list the others under `_Avoid_`. +- **Keep definitions tight.** One or two sentences max. Define what it IS, not what it does. +- **Only include terms specific to this project's context.** General programming concepts (timeouts, error types, utility patterns) don't belong even if the project uses them extensively. Before adding a term, ask: is this a concept unique to this context, or a general programming concept? Only the former belongs. +- **Group terms under subheadings** when natural clusters emerge. If all terms belong to a single cohesive area, a flat list is fine. + +## Single vs multi-context repos + +**Single context (most repos):** One `CONTEXT.md` at the repo root. + +**Multiple contexts:** A `CONTEXT-MAP.md` at the repo root lists the contexts, where they live, and how they relate to each other: + +```md +# Context Map + +## Contexts + +- [Ordering](./src/ordering/CONTEXT.md) — receives and tracks customer orders +- [Billing](./src/billing/CONTEXT.md) — generates invoices and processes payments +- [Fulfillment](./src/fulfillment/CONTEXT.md) — manages warehouse picking and shipping + +## Relationships + +- **Ordering → Fulfillment**: Ordering emits `OrderPlaced` events; Fulfillment consumes them to start picking +- **Fulfillment → Billing**: Fulfillment emits `ShipmentDispatched` events; Billing consumes them to generate invoices +- **Ordering ↔ Billing**: Shared types for `CustomerId` and `Money` +``` + +The skill infers which structure applies: + +- If `CONTEXT-MAP.md` exists, read it to find contexts +- If only a root `CONTEXT.md` exists, single context +- If neither exists, create a root `CONTEXT.md` lazily when the first term is resolved + +When multiple contexts exist, infer which one the current topic relates to. If unclear, ask. diff --git a/.agents/skills/domain-modeling/SKILL.md b/.agents/skills/domain-modeling/SKILL.md new file mode 100644 index 0000000..d0f7e1a --- /dev/null +++ b/.agents/skills/domain-modeling/SKILL.md @@ -0,0 +1,74 @@ +--- +name: domain-modeling +description: Build and sharpen a project's domain model. Use when the user wants to pin down domain terminology or a ubiquitous language, record an architectural decision, or when another skill needs to maintain the domain model. +--- + +# Domain Modeling + +Actively build and sharpen the project's domain model as you design. This is the *active* discipline — challenging terms, inventing edge-case scenarios, and writing the glossary and decisions down the moment they crystallise. (Merely *reading* `CONTEXT.md` for vocabulary is not this skill — that's a one-line habit any skill can do. This skill is for when you're changing the model, not just consuming it.) + +## File structure + +Most repos have a single context: + +``` +/ +├── CONTEXT.md +├── docs/ +│ └── adr/ +│ ├── 0001-event-sourced-orders.md +│ └── 0002-postgres-for-write-model.md +└── src/ +``` + +If a `CONTEXT-MAP.md` exists at the root, the repo has multiple contexts. The map points to where each one lives: + +``` +/ +├── CONTEXT-MAP.md +├── docs/ +│ └── adr/ ← system-wide decisions +├── src/ +│ ├── ordering/ +│ │ ├── CONTEXT.md +│ │ └── docs/adr/ ← context-specific decisions +│ └── billing/ +│ ├── CONTEXT.md +│ └── docs/adr/ +``` + +Create files lazily — only when you have something to write. If no `CONTEXT.md` exists, create one when the first term is resolved. If no `docs/adr/` exists, create it when the first ADR is needed. + +## During the session + +### Challenge against the glossary + +When the user uses a term that conflicts with the existing language in `CONTEXT.md`, call it out immediately. "Your glossary defines 'cancellation' as X, but you seem to mean Y — which is it?" + +### Sharpen fuzzy language + +When the user uses vague or overloaded terms, propose a precise canonical term. "You're saying 'account' — do you mean the Customer or the User? Those are different things." + +### Discuss concrete scenarios + +When domain relationships are being discussed, stress-test them with specific scenarios. Invent scenarios that probe edge cases and force the user to be precise about the boundaries between concepts. + +### Cross-reference with code + +When the user states how something works, check whether the code agrees. If you find a contradiction, surface it: "Your code cancels entire Orders, but you just said partial cancellation is possible — which is right?" + +### Update CONTEXT.md inline + +When a term is resolved, update `CONTEXT.md` right there. Don't batch these up — capture them as they happen. Use the format in [CONTEXT-FORMAT.md](./CONTEXT-FORMAT.md). + +`CONTEXT.md` should be totally devoid of implementation details. Do not treat `CONTEXT.md` as a spec, a scratch pad, or a repository for implementation decisions. It is a glossary and nothing else. + +### Offer ADRs sparingly + +Only offer to create an ADR when all three are true: + +1. **Hard to reverse** — the cost of changing your mind later is meaningful +2. **Surprising without context** — a future reader will wonder "why did they do it this way?" +3. **The result of a real trade-off** — there were genuine alternatives and you picked one for specific reasons + +If any of the three is missing, skip the ADR. Use the format in [ADR-FORMAT.md](./ADR-FORMAT.md). diff --git a/.agents/skills/domain-modeling/agents/openai.yaml b/.agents/skills/domain-modeling/agents/openai.yaml new file mode 100644 index 0000000..7f1522d --- /dev/null +++ b/.agents/skills/domain-modeling/agents/openai.yaml @@ -0,0 +1,3 @@ +interface: + display_name: "Domain Modeling" + short_description: "Build and sharpen a domain model" diff --git a/.agents/skills/grill-me/SKILL.md b/.agents/skills/grill-me/SKILL.md new file mode 100644 index 0000000..9470cfc --- /dev/null +++ b/.agents/skills/grill-me/SKILL.md @@ -0,0 +1,7 @@ +--- +name: grill-me +description: A relentless interview to sharpen a plan or design. +disable-model-invocation: true +--- + +Run a `/grilling` session. diff --git a/.agents/skills/grill-me/agents/openai.yaml b/.agents/skills/grill-me/agents/openai.yaml new file mode 100644 index 0000000..4d6fb0c --- /dev/null +++ b/.agents/skills/grill-me/agents/openai.yaml @@ -0,0 +1,5 @@ +interface: + display_name: "Grill Me" + short_description: "Sharpen a plan through interview" +policy: + allow_implicit_invocation: false diff --git a/.agents/skills/grill-with-docs/SKILL.md b/.agents/skills/grill-with-docs/SKILL.md new file mode 100644 index 0000000..bed05d2 --- /dev/null +++ b/.agents/skills/grill-with-docs/SKILL.md @@ -0,0 +1,7 @@ +--- +name: grill-with-docs +description: A relentless interview to sharpen a plan or design, which also creates docs (ADR's and glossary) as we go. +disable-model-invocation: true +--- + +Run a `/grilling` session, using the `/domain-modeling` skill. diff --git a/.agents/skills/grill-with-docs/agents/openai.yaml b/.agents/skills/grill-with-docs/agents/openai.yaml new file mode 100644 index 0000000..5dbe278 --- /dev/null +++ b/.agents/skills/grill-with-docs/agents/openai.yaml @@ -0,0 +1,5 @@ +interface: + display_name: "Grill with Docs" + short_description: "Grill a design and write its docs" +policy: + allow_implicit_invocation: false diff --git a/.agents/skills/grilling/SKILL.md b/.agents/skills/grilling/SKILL.md new file mode 100644 index 0000000..95bd01e --- /dev/null +++ b/.agents/skills/grilling/SKILL.md @@ -0,0 +1,22 @@ +--- +name: grilling +description: Grill the user relentlessly about a plan, decision, or idea. Use when the user wants to stress-test their thinking, or uses any 'grill' trigger phrases. +--- + +Interview the user relentlessly until you reach a shared understanding. Map this as a **design tree**: every decision branches into the decisions that hang off it. + +Work the tree in **rounds**. The **frontier** is every decision whose prerequisites are already settled — the questions you can ask _now_ without guessing at answers you haven't heard yet. Ask the whole frontier in one round: number each question and give your recommended answer. Then wait for the user's answers before the next round. + +Each question should be formatted like so: + +``` +❓ **Q1** - ****: + +➡️ +``` + +Each round the user answers reshapes the tree — settled decisions push the frontier outward and unblock questions that depended on them. Recompute the frontier and ask the next round. A question whose answer depends on another question still open in this round belongs to a _later_ round, not this one. + +Finding _facts_ is your job, never the user's. When a frontier question needs a fact from the environment (filesystem, tools, etc.), dispatch a sub-agent to find it — don't ask the user for anything you could look up yourself. Don't block on it: a running exploration is an unsettled prerequisite, so only the questions downstream of it wait for the sub-agent to report — ask the rest of the frontier now. The _decisions_ are the user's — put each to them and wait. + +The session is done when the frontier is empty: every branch of the design tree visited, nothing left silently assumed. Do not act on it until the user confirms you have reached a shared understanding. diff --git a/.agents/skills/grilling/agents/openai.yaml b/.agents/skills/grilling/agents/openai.yaml new file mode 100644 index 0000000..ddbdb96 --- /dev/null +++ b/.agents/skills/grilling/agents/openai.yaml @@ -0,0 +1,3 @@ +interface: + display_name: "Grilling" + short_description: "Stress-test thinking a round of questions at a time" diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md new file mode 100644 index 0000000..c940ab8 --- /dev/null +++ b/.claude/CLAUDE.md @@ -0,0 +1,181 @@ +## Projet + +**EuroMillions Geometry Lab** (izeetok) — SPA React qui traite les grilles/tirages EuroMillions +comme des objets mathématiques (features, géométrie, PCA/UMAP, clustering, backtesting), sans +jamais suggérer une capacité prédictive. Roadmap en 4 versions, voir `devdx/specs_v{1..4}.md` : +V1 analyse une grille, V2 teste une hypothèse (backtest), V3 construit l'univers mathématique +(embeddings/discovery), V4 permet de le visualiser en 3D (`/eurospace`). + +Décisions d'architecture actées, voir `docs/adr/` : +- **ADR-0001** : aucun backend — SPA 100% client-side (déployée sur Vercel), dataset de tirages en + CSV committé (source FDJ), état utilisateur en `localStorage`. +- **ADR-0002** : on construit d'abord un spike vertical (`Grid → FeatureExtractor → + GeometryDescriptor → PCA → coordonnées shape SpatialEmbedding → point cloud three.js`) avant de + bâtir V1 à V4 en entier — ça valide le contrat `SpatialEmbedding` (V3) que V4 consomme, avant + d'investir dans les pages complètes. + +Vocabulaire du domaine (Grid, Draw, FeatureVector, GeometryDescriptor, SpatialEmbedding, +DiscoveryModel, Family, Strategy, Experiment, TemporalWindow, les 4 scores...) : voir `CONTEXT.md` +à la racine. Le principe non-négociable répété dans chaque spec : toutes les combinaisons ont la +même probabilité théorique ; l'app analyse des structures historiques, elle ne prédit rien. + +## Périmètre + +Repo à plat (pas de monorepo `apps/`) : je lis et j'écris dans tout le repo — `src/`, `public/`, +`docs/`, racine. `devdx/` (specs sources) reste ignoré par git mais en lecture libre pour le +contexte. `.claude/` et `.agents/` sont versionnés (outillage agentique partagé avec l'équipe : +`CLAUDE.md`, `.claude/agents/`, `.agents/workflows/`, skills). + +Process avant tout travail de code non trivial ou ambigu : +1. Je propose une approche/plan. +2. Je liste précisément les **fichiers à modifier/créer** et **ce que je vais y faire**. +3. Une fois le plan/la tâche approuvé (y compris via la todo-list de session), j'exécute sans + redemander fichier par fichier pour ce qui est déjà dans le plan. +4. Je fais un **récap des fichiers touchés et des modifications réellement apportées**. + +En cas d'ambiguïté dans les specs (V1-V4) ou dans une ADR : je pose la question, je ne fais pas +d'hypothèse implicite. + +### Commandes + +- **Libres sans demander** : tout ce qui **vérifie ou installe en local** — `pnpm lint`, `pnpm + test`, `pnpm build`, `pnpm check` (tsc), `pnpm exec vitest`, `pnpm dev` (serveur dev), `pnpm + add`/`remove` (dépendances), `pnpm exec dx-flow ...`. Git en local : `status`, `diff`, `log`, + `add`, `commit`. +- **Autorisation requise avant** : `git push` (et *a fortiori* tout `--force`), suppression de + branche, réécriture d'historique (`rebase`, `reset --hard`, `amend`), toute action GitHub + (PR, issue, release), et toute commande destructive hors dépôt. + +## Stack + +Stack V1 (specs), déjà en place ou à installer au fil des tâches : + +| package | rôle | +|---|---| +| `react` / `react-dom` `^19` | Framework UI | +| `typescript` `~6.0` | Typage statique | +| `vite` `^8` | Build tool | +| `react-router` | Routing (V1 : `/evaluation`, `/draws`, `/geometry` ; V2 ajoute `/laboratory` ; V3 `/discovery` ; V4 `/eurospace`) | +| `@tanstack/react-query` | Fetch / cache (CSV, futur adapter API) | +| `zod` | Validation (`Grid`, formulaires) | +| SCSS / CSS Modules | Styles | +| `recharts` | Visualisations 2D ; `d3` seulement pour le sur-mesure avancé | +| `vitest` + Testing Library + Playwright | Tests (unit + E2E) | +| `three` / `@react-three/fiber` / `@react-three/drei` | V3 (point cloud spike) puis V4 (`/eurospace`) | +| `pnpm` | Package manager | +| `oxlint` | Linter (voir plus bas — remplace Biome) | +| `@keyobs/dx-flow` (husky + commitlint + lint-staged) | Hooks Git, commits conventionnels, scripts de release | + +Pas d'i18n (i18next) ni de Storybook : aucune des specs V1-V4 n'en a besoin pour l'instant. À +reconsidérer seulement si un besoin réel apparaît. + +## Structure + +Architecture V1 (le domaine mathématique ne dépend jamais de React ni d'une lib de rendu) : + +``` +src/ + app/ + pages/ + evaluation/ + draws/ + geometry/ + domain/ + grid/ + draw/ + features/ + geometry/ + scoring/ + application/ + infrastructure/ + repositories/ + csv/ + api/ + shared/ +``` + +V2 ajoute `pages/laboratory/` + le moteur de backtest dans `domain`/`application`. V3 ajoute +`pages/discovery/` + `domain/discovery` (embeddings, clustering). V4 ajoute `features/eurospace/` +et `pages/eurospace/` (voir `devdx/specs_v4.md#architecture-proposée` pour le détail). + +**Alias d'import** : pas encore configurés (imports relatifs pour l'instant). La section +"Ordre des imports" ci-dessous décrit la cible (`@models/`, `@api/`, `@utils/`, `@hooks/`, +`@providers/`, `@components/`) à mettre en place via `tsconfig`/`vite.config` quand le nombre de +fichiers le justifiera — ne pas inventer ces alias avant qu'ils existent réellement. + +## Style de dev + +- Composants fonctionnels uniquement, définis en arrow function + export par défaut, jamais + `export function XxxComponent() {...}` : + ```ts + const XxxComponent = (props: XxxComponentProps) => { ... }; + export default XxxComponent; + ``` + **Providers** : Context et Provider séparés dans deux fichiers (`XxxContext.tsx` = `createContext` + + hook `useXxx`, tout en export nommé ; `XxxProvider.tsx` = le composant seul, arrow + export par + défaut comme les autres composants) — évite de mélanger export par défaut et export nommé dans un + même fichier. Ex : `ApiContext.tsx`/`ApiProvider.tsx`. +- Props en interface nommée (`interface XxxProps`) +- Pas de `any`, pas de `unknown` sans narrowing immédiat +- Imports React : hooks nommés individuellement — `import { useState, useEffect } from 'react'` +- Imports terminés par un point-virgule (`;`) +- Ne pas utiliser `FormEvent`/`FormEventHandler` (dépréciés dans les types React) : utiliser + `SubmitEvent`, `ChangeEvent`, ou `SyntheticEvent` selon le cas +- Éviter l'attribut `autoFocus` (a11y) +- Commentaires dans le code : toujours en anglais +- Pas d'abstraction prématurée — 3 lignes similaires ne justifient pas un helper +- `data-testid` sur tout élément interactif ou observable (boutons, lignes de table, états vides...) + — pattern `{prefix}-{role}` pour les éléments répétés +- **Fichiers : toujours `Read` l'état courant avant modification** — ne jamais supposer que la + mémoire de session est à jour. `Edit` ciblé, jamais `Write` sur un fichier existant. +- Context React uniquement pour les valeurs partagées quasi-statiques ; `useState`/`useReducer` + pour l'état local +- Le domaine mathématique (`domain/`) reste pur TypeScript, testable sans React et sans lib de + rendu (three.js compris) — c'est la règle centrale des ADR/specs + +### Ordre des imports +``` +1. CSS / SCSS / SASS +2. Libs (react, react-router, @tanstack/react-query, …) +3. App — dans cet ordre : + 3a. types / models (@models/… ou src/domain relatif tant que l'alias n'existe pas) + 3b. api (@api/… ou src/infrastructure relatif) + 3c. utils (@utils/…) + 3d. hooks (@hooks/… ou hook colocalisé de la feature) + 3e. providers (@providers/…) + 3f. composants partagés (@components/…) + 3g. composants colocation (même dossier feature, imports relatifs ./…) +``` + +### Nommage & fichiers +- Composants React `.tsx` : PascalCase (`Gates.tsx`) +- Hooks `.ts` : camelCase (`useGates.ts`) +- Fichiers non-hook `.ts`/`.scss` : camelCase +- Fichiers test : camelCase (`Gates.test.tsx`) + +### Tests +- **Vitest** — objectif **80% de couverture**, en particulier sur `domain/` (logique scientifique) +- Composants : au minimum un test de rendu sans crash +- **Privilégier des helpers purs testables sans React** quand la logique s'y prête : une logique + extraite d'un composant est une logique qui reste couverte +- Playwright pour l'E2E (parcours saisie → évaluation → géométrie, etc., voir critères d'acceptation + de chaque spec) +- Invariants scientifiques toujours couverts : invariance à l'ordre d'une `Grid`, `distance(x,x)=0` + et symétrie, absence de fuite temporelle dans les backtests, déterminisme du seed + +## Git workflow + +- Hébergé sur GitHub. +- **Branches** : `main` protégée (aucun commit/push direct — bloqué par le hook `pre-commit` de + `dx-flow`), `develop` = branche de travail courante. Pas de branche par tâche par défaut ; on + crée une branche dédiée seulement si explicitement demandé. +- **Commits** : autorisés — un commit conventionnel par tâche terminée, sur `develop`, sauf + instruction contraire. Format imposé par `commitlint.config.mjs` : + `type(scope): subject` (**scope obligatoire**). Types acceptés (insensibles à la casse) : `feat`, + `fix`, `doc`, `style`, `refactor`, `perf`, `test`, `build`, `ci`, `chore`, `core`, `revert`, + `merge`, `config`, `clean`. +- **Push** : jamais sans autorisation explicite, même sur `develop`. +- Hooks `dx-flow` actifs : `pre-commit` (branche protégée, garde anti-`.env`, `lint-staged`, + `tsc --noEmit` sur les fichiers staged), `commit-msg` (commitlint), `pre-push` (`lint:fix` puis + `test:run` — bloque le push vers `develop` si les tests échouent). + diff --git a/.claude/skills/domain-modeling b/.claude/skills/domain-modeling new file mode 120000 index 0000000..e672a60 --- /dev/null +++ b/.claude/skills/domain-modeling @@ -0,0 +1 @@ +../../.agents/skills/domain-modeling \ No newline at end of file diff --git a/.claude/skills/grill-me b/.claude/skills/grill-me new file mode 120000 index 0000000..eea91a8 --- /dev/null +++ b/.claude/skills/grill-me @@ -0,0 +1 @@ +../../.agents/skills/grill-me \ No newline at end of file diff --git a/.claude/skills/grill-with-docs b/.claude/skills/grill-with-docs new file mode 120000 index 0000000..f6cbb9c --- /dev/null +++ b/.claude/skills/grill-with-docs @@ -0,0 +1 @@ +../../.agents/skills/grill-with-docs \ No newline at end of file diff --git a/.claude/skills/grilling b/.claude/skills/grilling new file mode 120000 index 0000000..e712452 --- /dev/null +++ b/.claude/skills/grilling @@ -0,0 +1 @@ +../../.agents/skills/grilling \ No newline at end of file diff --git a/.gitignore b/.gitignore index 822abca..ac884a5 100644 --- a/.gitignore +++ b/.gitignore @@ -25,11 +25,6 @@ dist-ssr devdx -# Skills installed via skills-lock.json (regenerated from the lockfile, like node_modules) -.agents/skills -.claude/skills -.claude - # Husky internal hook runner (regenerated by the "prepare" script) .husky/_ diff --git a/CONTEXT.md b/CONTEXT.md new file mode 100644 index 0000000..0bb4dff --- /dev/null +++ b/CONTEXT.md @@ -0,0 +1,57 @@ +# EuroMillions Geometry Lab + +A data-visualization app that treats EuroMillions draws and user-chosen number grids as mathematical objects, analyzing their historical structure without claiming predictive power. + +## Language + +### Core objects + +**Grid**: +Five distinct numbers (1–50) plus two distinct stars (1–12) — either a historical draw's outcome or a user's proposed selection. +_Avoid_: Combination, ticket, selection + +**Draw**: +A `Grid` that actually occurred on a specific date, sourced from official results (CSV/API/manual entry). +_Avoid_: Result, tirage + +**FeatureVector**: +The numeric measurements computed from a `Grid` (sum, amplitude, decade distribution, gaps, parity, etc.) — the raw input to every downstream model. +_Avoid_: Stats, metrics + +**GeometryDescriptor**: +The geometric shape derived from a `Grid`'s `FeatureVector` — decade buckets, gaps, sum, range, odd/even counts, cluster size — independent of any particular rendering. +_Avoid_: Shape + +### Analysis pipeline + +**SpatialEmbedding**: +The single source of spatial truth for a `Draw`: its 3D coordinates (from PCA or UMAP), cluster membership, local density, outlier score, and nearest neighbors. Computed once by the `DiscoveryModel`; consumers such as EuroSpace never recalculate it. +_Avoid_: Projection, position + +**DiscoveryModel**: +The model that fits clusters, density, and stability measures across the full draw history to produce `SpatialEmbedding`s. +_Avoid_: Clustering engine + +**Family**: +A named, described group of structurally similar draws discovered by the `DiscoveryModel`, always paired with a stability rating — an unstable family is still described, but weighted lightly by scores. +_Avoid_: Cluster (used interchangeably in places, but "Family" is the user-facing term), group + +### Strategy & experimentation + +**Strategy**: +A configurable, seeded, reproducible rule set that proposes a `Grid` given a subset of history — the unit under test in a backtest. +_Avoid_: Model, algorithm + +**Experiment**: +One walk-forward backtest run of a `Strategy` against a dataset/model version, seed, and set of `TemporalWindow`s, producing an `ExperimentResult`. +_Avoid_: Backtest run, test + +**TemporalWindow**: +A historical lookback period (1/3/6/12/25/50/"all" years) used to evaluate temporal proximity or scope a `Strategy` run. None of these windows — including "6" — has privileged scientific status; they're just available lenses. +_Avoid_: Timeframe, period + +### Scores + +**Structure Score / Originality Score / Temporal Signal Score / Confidence Score**: +The four independent axes an evaluated `Grid` is scored on: historical structural proximity, likely-human-choice risk, validated temporal effects, and diagnostic stability. Always shown together as separate axes — never collapsed into one opaque verdict. +_Avoid_: Score (alone — ambiguous which axis) diff --git a/docs/adr/0001-client-side-only-architecture.md b/docs/adr/0001-client-side-only-architecture.md new file mode 100644 index 0000000..84c7c8b --- /dev/null +++ b/docs/adr/0001-client-side-only-architecture.md @@ -0,0 +1,7 @@ +# Client-side-only architecture, no backend + +The app targets the author, friends, and recruiters viewing a portfolio piece — not a large public audience — with a dataset of roughly 2,000 historical draws. We decided against any backend: the app is a static SPA (deployed on Vercel) where all compute (CSV parsing, feature extraction, PCA/UMAP, clustering, backtesting) runs client-side, the draw dataset is a manually-refreshed CSV committed to the repo rather than fetched from a live API, and any per-user state (saved grids) lives in `localStorage` only, with no accounts. + +This keeps hosting and ops at zero, avoids redistributing FDJ's draw data through a public API of our own (its licensing terms are ambiguous), and doubles as a stronger portfolio signal than a conventional CRUD backend would. + +**Revisit if**: in-browser PCA/UMAP/clustering performance can't hit V4's "fluid on a standard laptop" target, or a genuinely compelling shared/multi-user feature emerges that justifies the added complexity. diff --git a/docs/adr/0002-vertical-spike-before-full-buildout.md b/docs/adr/0002-vertical-spike-before-full-buildout.md new file mode 100644 index 0000000..b5adce7 --- /dev/null +++ b/docs/adr/0002-vertical-spike-before-full-buildout.md @@ -0,0 +1,5 @@ +# Vertical spike before full V1-V4 buildout + +The V4 spec is written as a consumer contract against V3's `SpatialEmbedding`, but none of V1 through V4 is built yet. Rather than building each version fully in sequence (V1 → V2 → V3 → V4), we're building a thin end-to-end slice first: `Grid → FeatureExtractor → GeometryDescriptor → PCA → SpatialEmbedding-shaped coordinates → a bare three.js point cloud`, deferring clustering, the Ship/Autopilot/HUD navigation layer, and every other page (`/evaluation`, `/draws`, `/geometry`, `/laboratory`, `/discovery`) until the spike proves the contract out. + +A mismatch discovered in the `SpatialEmbedding` shape only once real 3D rendering is attempted against it would be far more expensive to fix after V1-V3 were already fully built in isolation. From f783ac54e0a3ec24dd066e3c60720dc3ca142e6b Mon Sep 17 00:00:00 2001 From: keyobs Date: Wed, 12 Aug 2026 16:04:50 +0200 Subject: [PATCH 47/68] feat(agents): add architect subagent Turns a feature/architecture request into a plan for this repo before any code is written: explores real usage instead of assuming from naming, consults domain-modeling for domain vocabulary, calibrates exploration depth and blocking questions to the stakes, and only treats a decision as settled once the user has explicitly confirmed it. Crash-tested blind against the src/ modularization brief in devdx/new-architecture.md (twice - once exposing a stale file pointer it correctly refused to guess around) before tightening the prompt's positive-guidance gaps. Co-Authored-By: Claude Sonnet 5 --- .claude/agents/architect.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 .claude/agents/architect.md diff --git a/.claude/agents/architect.md b/.claude/agents/architect.md new file mode 100644 index 0000000..bf06a7b --- /dev/null +++ b/.claude/agents/architect.md @@ -0,0 +1,15 @@ +--- +name: architect +description: Turns a feature request, bug, or architecture question into an implementation plan for this repo. Explores the current structure and conventions before proposing anything, consults the domain-modeling skill when domain vocabulary is involved, and stops at a plan — no code, no edits. Use before any non-trivial or ambiguous change. +tools: Read, Grep, Glob, Bash, Skill, TaskCreate, TaskUpdate, TaskList +--- + +You are the architect for this repo. Given a request, produce a plan — never write or edit code, never use Edit/Write. + +1. **Explore before proposing, proportionally to the request.** Read the actual files involved, don't reason from memory of the codebase. If you're about to claim "X is used by Y", grep for it and check — don't assume from naming. Calibrate the depth to the stakes: a one-file fix can be verified quickly; a structural question requires tracing real imports before naming a destination for anything. +2. **Read `CLAUDE.md` and `docs/adr/` first** for current structure, conventions, and decisions already made. Never hardcode a folder layout in your own reasoning — the live source of truth is those files, not this prompt. +3. **Use the `domain-modeling` skill** when the request touches domain vocabulary (see `CONTEXT.md`), introduces a new term, or could conflict with an existing ADR. +4. **Surface ambiguity, calibrated.** When a reasonable, easily-reversible default exists, state it explicitly in your plan and proceed on that basis, flagged as a default — don't resolve it silently, but don't block on it either. Reserve blocking questions for choices that are hard to reverse or for readings that are genuinely equally plausible. +5. **Output a plan**: the precise list of files to create/modify and what changes in each — matching this repo's own process rule (`CLAUDE.md` → "je liste précisément les fichiers à modifier/créer"). No code. +6. **Flag ADR-worthy decisions, don't write them.** If a choice is hard to reverse, surprising without context, and a real trade-off (the domain-modeling skill's 3-part test), say so explicitly. Treat a decision as settled only once the user has explicitly confirmed it in the conversation — not merely because nobody has objected. +7. **Default to showing your reasoning and a concrete recommendation, not silence.** An open question should come with your best-guess answer, unless voicing that guess would itself be presumptuous (see point 4). From 5c88fb788ce1cdc04fe7530637ae3d3f7c204ba6 Mon Sep 17 00:00:00 2001 From: keyobs Date: Wed, 12 Aug 2026 16:38:03 +0200 Subject: [PATCH 48/68] feat(agents): add test-strategist subagent Identifies which tests are worth writing for a given diff or plan, anchored on this repo's own testing conventions (coverage target on the pure domain/analysis layer, the named scientific invariants, colocated Vitest style, Playwright for user journeys) rather than generic advice. Advisory only, same read-only tool set as architect - produces a prioritized list, never test files. Co-Authored-By: Claude Sonnet 5 --- .claude/agents/test-strategist.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 .claude/agents/test-strategist.md diff --git a/.claude/agents/test-strategist.md b/.claude/agents/test-strategist.md new file mode 100644 index 0000000..f1684c3 --- /dev/null +++ b/.claude/agents/test-strategist.md @@ -0,0 +1,15 @@ +--- +name: test-strategist +description: Given a diff, a plan, or a piece of domain logic, identifies which tests are actually worth writing for this repo — and which aren't. Anchors on this repo's own testing conventions rather than generic test advice. Produces a prioritized list, not test files — no code, no edits. Use after a plan is implemented or when reviewing test coverage for existing logic. +tools: Read, Grep, Glob, Bash, Skill, TaskCreate, TaskUpdate, TaskList +--- + +You are the test strategist for this repo. Given a diff, a plan, or existing code, identify which tests matter — never write test files yourself, never use Edit/Write. + +1. **Read the actual diff/files, proportionally to their reach.** A small pure-function change needs a quick check; a change to `analysis/` (the pure scientific core) needs you to trace what invariants it touches before recommending anything. +2. **Ground every recommendation in this repo's own conventions**, not generic testing advice — read `CLAUDE.md`'s "Tests" section and the sibling `*.test.ts` files for the code under discussion before proposing new ones. Mirror the existing style (colocated, camelCase, Vitest + Testing Library). +3. **Prioritize the scientific invariants CLAUDE.md names explicitly** wherever a change touches the pure domain/analysis layer — order-invariance of a `Grid`, `distance(x, x) = 0` and symmetry, no temporal leakage in backtests, seed determinism. For each one that applies, check whether a test already covers it and name the precise gap (file + missing case), not a generic "add more tests." +4. **Favor pure, React-free helpers when logic is testable that way** — this repo already prefers extracting logic out of components for exactly that reason; point out when a component change could be tested more cheaply by extracting first. +5. **For UI/page changes**, recommend at minimum a render-without-crash test, and call out when a full user journey (e.g. saisie → évaluation → géométrie) needs Playwright coverage instead of — or in addition to — a unit test. +6. **Output a prioritized list**: missing tests worth writing, existing tests that are redundant/misplaced, and why — grounded in what actually changed, not a checklist run against every file in sight. +7. **Default to stating your judgment call and reasoning, not silence.** When "is this worth testing" is a genuine judgment call, say what you'd do and why, flagged as a recommendation — don't just list options. From 90444d3792b1474ec5704b83bbf433acaf82fd84 Mon Sep 17 00:00:00 2001 From: keyobs Date: Thu, 13 Aug 2026 10:21:39 +0200 Subject: [PATCH 49/68] feat(agents): add quality-reviewer subagent Reviews a diff against this repo's own documented conventions in CLAUDE.md (component style, file naming, import order, domain purity, data-testid patterns, premature-abstraction avoidance) rather than generic bug-hunting, which /code-review already covers. Read-only, reports via ReportFindings, anchors every finding to a citable rule - completes the architect/test-strategist/quality-reviewer trio from evolution.md's feature workflow diagram. Co-Authored-By: Claude Sonnet 5 --- .claude/agents/quality-reviewer.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 .claude/agents/quality-reviewer.md diff --git a/.claude/agents/quality-reviewer.md b/.claude/agents/quality-reviewer.md new file mode 100644 index 0000000..fe909e7 --- /dev/null +++ b/.claude/agents/quality-reviewer.md @@ -0,0 +1,14 @@ +--- +name: quality-reviewer +description: Reviews a diff against this repo's own documented conventions in CLAUDE.md (component style, file naming, import order, domain purity, data-testid patterns, premature-abstraction avoidance) — not generic bug-hunting, that's what /code-review is for. Reports findings via ReportFindings, ranked by how directly they violate a stated convention. Use after implementation, before it's considered done. +tools: Read, Grep, Glob, Bash, Skill, ReportFindings +--- + +You are the quality reviewer for this repo. Given a diff (or a set of files), check it against this repo's own stated conventions — not general code quality, that's `/code-review`'s job. + +1. **Read `CLAUDE.md`'s "Style de dev" section first**, every time — conventions can change, don't rely on memory of a previous review. +2. **Check the diff file by file against the concrete rules**: arrow-function components with default export, named `XxxProps` interfaces, no `any`/un-narrowed `unknown`, individually-named React hook imports, semicolon-terminated imports, `SubmitEvent`/`ChangeEvent`/`SyntheticEvent` instead of the deprecated `FormEvent*`, no `autoFocus`, English-only comments, PascalCase components / camelCase hooks and other files, `data-testid="{prefix}-{role}"` on interactive/observable elements, the documented import order. +3. **Check structural rules that only make sense with context**: does new domain/analysis logic stay free of React and rendering imports; does a new Context/Provider pair follow the two-file split; is a new abstraction justified by real reuse (three similar lines don't earn a helper) rather than introduced pre-emptively. +4. **When a rule doesn't apply cleanly** (the convention is ambiguous for this specific case, or two documented rules pull in different directions), say so explicitly with your best reading rather than silently picking one — flag it the same way `architect` would. +5. **Report via `ReportFindings`**, ranked most-clearly-a-convention-violation first. Anchor every finding to the specific `CLAUDE.md` rule it violates — a finding with no citable rule behind it belongs in `/code-review`, not here. +6. **Say when there's nothing to report.** A clean diff against these conventions is a valid, useful outcome — don't manufacture findings to seem thorough. From 50a1b37c87d24ee0abe98e8c1b52b2c827f522e3 Mon Sep 17 00:00:00 2001 From: keyobs Date: Thu, 13 Aug 2026 10:39:55 +0200 Subject: [PATCH 50/68] feat(agents): add feature workflow doc Ties architect, grill-me, test-strategist, and quality-reviewer into the plan -> challenge -> human validation -> implement -> test -> review -> validate -> (ADR if it earned one) sequence sketched in evolution.md. Entry test for when to bother with the full sequence at all: can you already name every touched file and confirm none is imported outside the one feature you're changing - a checkable bar instead of "use judgment" or "if it feels obvious." Co-Authored-By: Claude Sonnet 5 --- .agents/workflows/feature.md | 39 ++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 .agents/workflows/feature.md diff --git a/.agents/workflows/feature.md b/.agents/workflows/feature.md new file mode 100644 index 0000000..2f25bb3 --- /dev/null +++ b/.agents/workflows/feature.md @@ -0,0 +1,39 @@ +# Feature workflow + +Entry test: if you can already name every file the change touches, and none of them are imported by more than the one feature you're changing, go straight to step 4. Otherwise — or if you're not sure — start at step 1. That's the same bar `CLAUDE.md` sets ("non trivial ou ambigu"), made checkable: naming the files is easy to fake from memory, so the test is really "did you grep to confirm it," not "does it feel obvious." + +## 1. Plan + +Hand the request to `architect`. It explores the real codebase before proposing anything, reaches for the `domain-modeling` skill itself when domain vocabulary is involved (see `CONTEXT.md`), and stops at a plan — no code. It states defaults for easily-reversible choices and only raises blocking questions for the ones that are hard to reverse or genuinely ambiguous. + +## 2. Challenge + +Run `grill-me` against the plan. Resolve every blocking question it raises before moving on. + +## 3. Validate with the human + +Per this repo's own process rule (`CLAUDE.md` → "Process avant tout travail de code non trivial ou ambigu"), get explicit confirmation of the plan — including the precise file list — before writing code. `architect` treats a decision as settled only once you've confirmed it in conversation, not by default; this step is where that confirmation actually happens. + +## 4. Implement + +Implement the approved plan incrementally. Read a file's current state before editing it — never assume session memory is up to date (`CLAUDE.md`). + +## 5. Test strategy + +Ask `test-strategist` for a prioritized list of what's actually worth testing for this diff — anchored on this repo's own conventions and named scientific invariants, not generic advice. Write the tests it identifies as valuable; skip the ones it explicitly says aren't. + +## 6. Quality review + +Run `quality-reviewer` against the final diff. It checks against `CLAUDE.md`'s documented conventions specifically (component style, naming, import order, domain purity, `data-testid` patterns) — it doesn't hunt bugs. Also run `/code-review` when the diff touches code shared across features, external I/O, or anything else hard to reverse; a single-feature change with no cross-cutting reach is already covered by `quality-reviewer` + `test-strategist` alone. + +## 7. Validate + +Run: +- `pnpm check` +- `pnpm lint` +- `pnpm test:run` +- `pnpm build` + +## 8. Record the decision, if it earned one + +If `architect` or `quality-reviewer` flagged the change as ADR-worthy (hard to reverse, surprising without context, a real trade-off), write the ADR now that it's actually settled — via the `domain-modeling` skill, following `ADR-FORMAT.md`. Don't write it earlier: an ADR records a decision, not the deliberation that led to it. From fceff879bc468b8eab4f02b6d005e97eefe0feef Mon Sep 17 00:00:00 2001 From: keyobs Date: Thu, 13 Aug 2026 23:00:08 +0200 Subject: [PATCH 51/68] feat(ci): gate main/develop with GitHub Actions, add deploy-troubleshooter Adds the CI gate this repo's Vercel deployment (ADR-0001) relies on: a reusable test.yml (check/lint/test:run), ci.yml as the required check on push to main/develop and on every PR, and simulate-deploy.yml as a build-only dry run on develop pushes - mirroring the pocket-pbt pattern, adapted for a flat repo with no monorepo filters and no custom deploy job, since Vercel's own Git integration (not the CLI) builds and deploys on push once code reaches its production branch. Also: vercel.json's SPA rewrite (react-router routes would 404 on reload without it), packageManager pin (pnpm/action-setup needs it to resolve a version), .vercel added to .gitignore, and a read-only deploy-troubleshooter agent that diagnoses a failed run from its actual log output instead of guessing from the workflow file's intent. Co-Authored-By: Claude Sonnet 5 --- .claude/agents/deploy-troubleshooter.md | 14 +++++++ .github/workflows/ci.yml | 16 ++++++++ .github/workflows/simulate-deploy.yml | 49 +++++++++++++++++++++++++ .github/workflows/test.yml | 43 ++++++++++++++++++++++ .gitignore | 5 ++- package.json | 1 + vercel.json | 3 ++ 7 files changed, 130 insertions(+), 1 deletion(-) create mode 100644 .claude/agents/deploy-troubleshooter.md create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/simulate-deploy.yml create mode 100644 .github/workflows/test.yml create mode 100644 vercel.json diff --git a/.claude/agents/deploy-troubleshooter.md b/.claude/agents/deploy-troubleshooter.md new file mode 100644 index 0000000..6993c5d --- /dev/null +++ b/.claude/agents/deploy-troubleshooter.md @@ -0,0 +1,14 @@ +--- +name: deploy-troubleshooter +description: Diagnoses a failed CI run or a broken Vercel deployment for this repo. Reads the actual failure output before proposing anything, tells apart a gate failure (check/lint/test) from a build failure from a Vercel-side issue, and stops at a diagnosis — no code, no edits, no CLI deploys. +tools: Read, Grep, Glob, Bash, Skill +--- + +You are the deploy troubleshooter for this repo. Given a failed CI run or a broken deployment, find the actual cause — never write or edit code, never use Edit/Write, never run a Vercel deploy yourself (this repo deploys through Vercel's native Git integration, not the CLI). + +1. **Start from the real failure output, not the workflow file's intent.** Use `gh run view --log-failed` (or the specific run/job the user points you to) to read what actually happened before reasoning about what should have happened. +2. **Name which stage failed before proposing a fix**: the CI gate (`pnpm check`/`lint`/`test:run` in `test.yml`), the build (`pnpm build` in `simulate-deploy.yml` or Vercel's own build), or Vercel's deploy/runtime (only visible in the Vercel dashboard, since this repo has no CLI token configured here) — each has a different fix path, and guessing the wrong one wastes a cycle. +3. **For a Vercel-side failure**, you can't fetch its logs directly without an authenticated CLI — say so, and point to exactly what to check in the Vercel dashboard (the specific deployment, the build or function logs) rather than guessing at the cause from the app code alone. +4. **Ground your diagnosis in the exact command and its exact output** — quote the failing line, not a paraphrase. +5. **Give the command to reproduce the failure locally** whenever the failing stage runs a command that exists in `package.json` (`pnpm check`, `pnpm lint`, `pnpm test:run`, `pnpm build`) — that's almost always faster to iterate on than re-running CI. +6. **Default to a likely root cause and a concrete next step**, not just a list of possibilities — unless the log genuinely supports more than one equally plausible cause, in which case say so and name what would distinguish them. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..f259796 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,16 @@ +name: CI + +on: + push: + branches: [main, develop] + pull_request: + +permissions: + contents: read + +jobs: + tests: + name: Tests + uses: ./.github/workflows/test.yml + with: + ref: ${{ github.ref }} diff --git a/.github/workflows/simulate-deploy.yml b/.github/workflows/simulate-deploy.yml new file mode 100644 index 0000000..670829f --- /dev/null +++ b/.github/workflows/simulate-deploy.yml @@ -0,0 +1,49 @@ +name: Simulate deploy + +on: + push: + branches: [develop] + workflow_dispatch: + inputs: + ref: + description: "Branch or tag to simulate" + required: true + type: string + +permissions: + contents: read + +jobs: + tests: + name: Tests + uses: ./.github/workflows/test.yml + with: + ref: ${{ inputs.ref || github.ref }} + + build: + name: Build + needs: tests + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + ref: ${{ inputs.ref || github.ref }} + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + cache: "pnpm" + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Build + run: pnpm build + + - name: Simulation complete + run: echo "Build succeeded for '${{ inputs.ref || github.ref_name }}' - Vercel's own git integration will build and deploy this the same way once it reaches its production branch." diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..434f50d --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,43 @@ +name: Tests + +on: + workflow_call: + inputs: + ref: + description: "Ref (branch or tag) to test" + required: false + type: string + default: "" + +permissions: + contents: read + +jobs: + test: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + ref: ${{ inputs.ref || github.ref }} + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + cache: "pnpm" + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Type check + run: pnpm check + + - name: Lint + run: pnpm lint + + - name: Run tests + run: pnpm test:run diff --git a/.gitignore b/.gitignore index ac884a5..83bc09b 100644 --- a/.gitignore +++ b/.gitignore @@ -31,4 +31,7 @@ devdx # Playwright playwright-report test-results -blob-report \ No newline at end of file +blob-report + +# Vercel CLI local link artifacts +.vercel \ No newline at end of file diff --git a/package.json b/package.json index cdeabbf..83e1c40 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,7 @@ "private": true, "version": "0.0.0", "type": "module", + "packageManager": "pnpm@11.0.9", "scripts": { "dev": "vite", "build": "tsc -b && vite build", diff --git a/vercel.json b/vercel.json new file mode 100644 index 0000000..0f32683 --- /dev/null +++ b/vercel.json @@ -0,0 +1,3 @@ +{ + "rewrites": [{ "source": "/(.*)", "destination": "/index.html" }] +} From fa88173de4f2d29429d6dfc3adc595d4290b4df9 Mon Sep 17 00:00:00 2001 From: keyobs Date: Thu, 13 Aug 2026 23:05:16 +0200 Subject: [PATCH 52/68] feat(ci): deploy develop to GitHub Pages, keep Vercel for main develop now gets its own real deployment (tests -> build -> GitHub Pages) instead of a build-only dry run, so there's a working staging environment independent of Vercel before code reaches main - redundancy across two separate platforms rather than relying on one. GitHub Pages serves from a /izeetok/ subpath (no custom domain), unlike Vercel's root domain, so both the Vite base and the router's basename now read from VITE_BASE_PATH (set only in the Pages build job; unset everywhere else keeps root-relative paths). Also copies index.html to 404.html in that build - Pages has no server-side rewrites, so client-side routes need the fallback trick Vercel's vercel.json rewrite already handles for main. deploy-troubleshooter updated to know about both targets and where each one's logs actually live. Co-Authored-By: Claude Sonnet 5 --- .claude/agents/deploy-troubleshooter.md | 8 +-- .github/workflows/deploy-develop-pages.yml | 75 ++++++++++++++++++++++ .github/workflows/simulate-deploy.yml | 49 -------------- src/app/router.tsx | 33 +++++----- vite.config.ts | 1 + 5 files changed, 98 insertions(+), 68 deletions(-) create mode 100644 .github/workflows/deploy-develop-pages.yml delete mode 100644 .github/workflows/simulate-deploy.yml diff --git a/.claude/agents/deploy-troubleshooter.md b/.claude/agents/deploy-troubleshooter.md index 6993c5d..cb0787f 100644 --- a/.claude/agents/deploy-troubleshooter.md +++ b/.claude/agents/deploy-troubleshooter.md @@ -1,14 +1,14 @@ --- name: deploy-troubleshooter -description: Diagnoses a failed CI run or a broken Vercel deployment for this repo. Reads the actual failure output before proposing anything, tells apart a gate failure (check/lint/test) from a build failure from a Vercel-side issue, and stops at a diagnosis — no code, no edits, no CLI deploys. +description: Diagnoses a failed CI run or a broken deployment for this repo — Vercel (production, on main) or GitHub Pages (staging, on develop). Reads the actual failure output before proposing anything, tells apart a gate failure (check/lint/test) from a build failure from a platform-side issue, and stops at a diagnosis — no code, no edits, no CLI deploys. tools: Read, Grep, Glob, Bash, Skill --- -You are the deploy troubleshooter for this repo. Given a failed CI run or a broken deployment, find the actual cause — never write or edit code, never use Edit/Write, never run a Vercel deploy yourself (this repo deploys through Vercel's native Git integration, not the CLI). +You are the deploy troubleshooter for this repo. Given a failed CI run or a broken deployment, find the actual cause — never write or edit code, never use Edit/Write, never run a deploy yourself (`main` deploys to Vercel through its native Git integration; `develop` deploys to GitHub Pages through `deploy-develop-pages.yml` — neither goes through a CLI you'd invoke here). 1. **Start from the real failure output, not the workflow file's intent.** Use `gh run view --log-failed` (or the specific run/job the user points you to) to read what actually happened before reasoning about what should have happened. -2. **Name which stage failed before proposing a fix**: the CI gate (`pnpm check`/`lint`/`test:run` in `test.yml`), the build (`pnpm build` in `simulate-deploy.yml` or Vercel's own build), or Vercel's deploy/runtime (only visible in the Vercel dashboard, since this repo has no CLI token configured here) — each has a different fix path, and guessing the wrong one wastes a cycle. -3. **For a Vercel-side failure**, you can't fetch its logs directly without an authenticated CLI — say so, and point to exactly what to check in the Vercel dashboard (the specific deployment, the build or function logs) rather than guessing at the cause from the app code alone. +2. **Name which stage failed before proposing a fix**: the CI gate (`pnpm check`/`lint`/`test:run` in `test.yml`), the build (`pnpm build`, with `VITE_BASE_PATH=/izeetok/` set only for the GitHub Pages build), the GitHub Pages deploy step (`actions/deploy-pages`, visible in the run log), or Vercel's deploy/runtime (only visible in the Vercel dashboard, since this repo has no CLI token configured here) — each has a different fix path, and guessing the wrong one wastes a cycle. +3. **For a Vercel-side failure**, you can't fetch its logs directly without an authenticated CLI — say so, and point to exactly what to check in the Vercel dashboard (the specific deployment, the build or function logs) rather than guessing at the cause from the app code alone. A GitHub Pages failure, by contrast, is fully visible in the Actions run log — use that directly. 4. **Ground your diagnosis in the exact command and its exact output** — quote the failing line, not a paraphrase. 5. **Give the command to reproduce the failure locally** whenever the failing stage runs a command that exists in `package.json` (`pnpm check`, `pnpm lint`, `pnpm test:run`, `pnpm build`) — that's almost always faster to iterate on than re-running CI. 6. **Default to a likely root cause and a concrete next step**, not just a list of possibilities — unless the log genuinely supports more than one equally plausible cause, in which case say so and name what would distinguish them. diff --git a/.github/workflows/deploy-develop-pages.yml b/.github/workflows/deploy-develop-pages.yml new file mode 100644 index 0000000..7b1b96e --- /dev/null +++ b/.github/workflows/deploy-develop-pages.yml @@ -0,0 +1,75 @@ +name: Deploy develop to GitHub Pages + +on: + push: + branches: [develop] + workflow_dispatch: + inputs: + ref: + description: "Branch or tag to deploy" + required: false + type: string + default: "" + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: "pages" + cancel-in-progress: false + +jobs: + tests: + name: Tests + uses: ./.github/workflows/test.yml + with: + ref: ${{ inputs.ref || github.ref }} + + build: + name: Build + needs: tests + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + ref: ${{ inputs.ref || github.ref }} + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + cache: "pnpm" + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Build + env: + VITE_BASE_PATH: "/izeetok/" + run: pnpm build + + - name: SPA fallback for GitHub Pages (no server-side rewrites there) + run: cp dist/index.html dist/404.html + + - name: Upload Pages artifact + uses: actions/upload-pages-artifact@v3 + with: + path: ./dist + + deploy: + name: Deploy to GitHub Pages + needs: build + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/.github/workflows/simulate-deploy.yml b/.github/workflows/simulate-deploy.yml deleted file mode 100644 index 670829f..0000000 --- a/.github/workflows/simulate-deploy.yml +++ /dev/null @@ -1,49 +0,0 @@ -name: Simulate deploy - -on: - push: - branches: [develop] - workflow_dispatch: - inputs: - ref: - description: "Branch or tag to simulate" - required: true - type: string - -permissions: - contents: read - -jobs: - tests: - name: Tests - uses: ./.github/workflows/test.yml - with: - ref: ${{ inputs.ref || github.ref }} - - build: - name: Build - needs: tests - runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - ref: ${{ inputs.ref || github.ref }} - - - name: Setup pnpm - uses: pnpm/action-setup@v4 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: 22 - cache: "pnpm" - - - name: Install dependencies - run: pnpm install --frozen-lockfile - - - name: Build - run: pnpm build - - - name: Simulation complete - run: echo "Build succeeded for '${{ inputs.ref || github.ref_name }}' - Vercel's own git integration will build and deploy this the same way once it reaches its production branch." diff --git a/src/app/router.tsx b/src/app/router.tsx index 0ed6d44..1b98480 100644 --- a/src/app/router.tsx +++ b/src/app/router.tsx @@ -7,18 +7,21 @@ import LaboratoryPage from '../pages/laboratory/LaboratoryPage.tsx'; import DiscoveryPage from '../pages/discovery/DiscoveryPage.tsx'; import SpikePage from '../pages/spike/SpikePage.tsx'; -export const router = createBrowserRouter([ - { - path: '/', - element: , - children: [ - { index: true, element: }, - { path: 'evaluation', element: }, - { path: 'draws', element: }, - { path: 'geometry', element: }, - { path: 'laboratory', element: }, - { path: 'discovery', element: }, - { path: 'spike', element: }, - ], - }, -]); +export const router = createBrowserRouter( + [ + { + path: '/', + element: , + children: [ + { index: true, element: }, + { path: 'evaluation', element: }, + { path: 'draws', element: }, + { path: 'geometry', element: }, + { path: 'laboratory', element: }, + { path: 'discovery', element: }, + { path: 'spike', element: }, + ], + }, + ], + { basename: import.meta.env.VITE_BASE_PATH || '/' }, +); diff --git a/vite.config.ts b/vite.config.ts index a2d469a..55102a5 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -3,6 +3,7 @@ import react from '@vitejs/plugin-react' // https://vite.dev/config/ export default defineConfig({ + base: process.env.VITE_BASE_PATH || '/', plugins: [react()], test: { environment: 'jsdom', From 1905676888eefab3003bc5e826d4126cedda0755 Mon Sep 17 00:00:00 2001 From: keyobs Date: Thu, 13 Aug 2026 23:37:47 +0200 Subject: [PATCH 53/68] fix(ci): authenticate the private registry, pin Node 24, unbreak Node20 actions pnpm install was failing in CI with a 401 on @keyobs/dx-flow: locally it resolves through the dev's own ~/.npmrc token, which CI has no equivalent of. Inject the token into .npmrc at runtime from a new GH_PACKAGES_TOKEN repo secret (read:packages PAT) instead of committing an env-interpolated auth line - that would have shadowed everyone's working local token the moment NODE_AUTH_TOKEN isn't exported, which it isn't by default. Also pins engines.node to 24.x (matching actual dev machines, not the 22 the old workflow happened to hardcode) and replaces the Node20-era actions/checkout@v4 + actions/setup-node@v4 + pnpm/action-setup@v4 with actions/checkout@v7 + pnpm/setup@v2 (the latter is pnpm's own recommended replacement for action-setup on pnpm v11+, and installs the Node runtime in the same step). Bumps upload-pages-artifact and deploy-pages to v5 for the same Node24 reason before they warn too. Co-Authored-By: Claude Sonnet 5 --- .claude/agents/deploy-troubleshooter.md | 2 +- .github/workflows/deploy-develop-pages.yml | 20 ++++++++++---------- .github/workflows/test.yml | 16 ++++++++-------- package.json | 3 +++ 4 files changed, 22 insertions(+), 19 deletions(-) diff --git a/.claude/agents/deploy-troubleshooter.md b/.claude/agents/deploy-troubleshooter.md index cb0787f..a0004d2 100644 --- a/.claude/agents/deploy-troubleshooter.md +++ b/.claude/agents/deploy-troubleshooter.md @@ -7,7 +7,7 @@ tools: Read, Grep, Glob, Bash, Skill You are the deploy troubleshooter for this repo. Given a failed CI run or a broken deployment, find the actual cause — never write or edit code, never use Edit/Write, never run a deploy yourself (`main` deploys to Vercel through its native Git integration; `develop` deploys to GitHub Pages through `deploy-develop-pages.yml` — neither goes through a CLI you'd invoke here). 1. **Start from the real failure output, not the workflow file's intent.** Use `gh run view --log-failed` (or the specific run/job the user points you to) to read what actually happened before reasoning about what should have happened. -2. **Name which stage failed before proposing a fix**: the CI gate (`pnpm check`/`lint`/`test:run` in `test.yml`), the build (`pnpm build`, with `VITE_BASE_PATH=/izeetok/` set only for the GitHub Pages build), the GitHub Pages deploy step (`actions/deploy-pages`, visible in the run log), or Vercel's deploy/runtime (only visible in the Vercel dashboard, since this repo has no CLI token configured here) — each has a different fix path, and guessing the wrong one wastes a cycle. +2. **Name which stage failed before proposing a fix**: `pnpm install` itself (this repo depends on `@keyobs/dx-flow`, a private GitHub Packages registry — a bare `401 Unauthorized` at install time almost always means the `GH_PACKAGES_TOKEN` secret is missing, expired, or lacks `read:packages`, not a lockfile/code problem), the CI gate (`pnpm check`/`lint`/`test:run` in `test.yml`), the build (`pnpm build`, with `VITE_BASE_PATH=/izeetok/` set only for the GitHub Pages build), the GitHub Pages deploy step (`actions/deploy-pages`, visible in the run log), or Vercel's deploy/runtime (only visible in the Vercel dashboard, since this repo has no CLI token configured here) — each has a different fix path, and guessing the wrong one wastes a cycle. 3. **For a Vercel-side failure**, you can't fetch its logs directly without an authenticated CLI — say so, and point to exactly what to check in the Vercel dashboard (the specific deployment, the build or function logs) rather than guessing at the cause from the app code alone. A GitHub Pages failure, by contrast, is fully visible in the Actions run log — use that directly. 4. **Ground your diagnosis in the exact command and its exact output** — quote the failing line, not a paraphrase. 5. **Give the command to reproduce the failure locally** whenever the failing stage runs a command that exists in `package.json` (`pnpm check`, `pnpm lint`, `pnpm test:run`, `pnpm build`) — that's almost always faster to iterate on than re-running CI. diff --git a/.github/workflows/deploy-develop-pages.yml b/.github/workflows/deploy-develop-pages.yml index 7b1b96e..7db08ac 100644 --- a/.github/workflows/deploy-develop-pages.yml +++ b/.github/workflows/deploy-develop-pages.yml @@ -33,18 +33,18 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v7 with: ref: ${{ inputs.ref || github.ref }} - - name: Setup pnpm - uses: pnpm/action-setup@v4 - - - name: Setup Node.js - uses: actions/setup-node@v4 + - name: Setup pnpm + Node.js + uses: pnpm/setup@v2 with: - node-version: 22 - cache: "pnpm" + version: 11.0.9 + runtime: node@24 + + - name: Configure private registry auth + run: echo "//npm.pkg.github.com/:_authToken=${{ secrets.GH_PACKAGES_TOKEN }}" >> .npmrc - name: Install dependencies run: pnpm install --frozen-lockfile @@ -58,7 +58,7 @@ jobs: run: cp dist/index.html dist/404.html - name: Upload Pages artifact - uses: actions/upload-pages-artifact@v3 + uses: actions/upload-pages-artifact@v5 with: path: ./dist @@ -72,4 +72,4 @@ jobs: steps: - name: Deploy to GitHub Pages id: deployment - uses: actions/deploy-pages@v4 + uses: actions/deploy-pages@v5 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 434f50d..22e0fa8 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -17,18 +17,18 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v7 with: ref: ${{ inputs.ref || github.ref }} - - name: Setup pnpm - uses: pnpm/action-setup@v4 - - - name: Setup Node.js - uses: actions/setup-node@v4 + - name: Setup pnpm + Node.js + uses: pnpm/setup@v2 with: - node-version: 22 - cache: "pnpm" + version: 11.0.9 + runtime: node@24 + + - name: Configure private registry auth + run: echo "//npm.pkg.github.com/:_authToken=${{ secrets.GH_PACKAGES_TOKEN }}" >> .npmrc - name: Install dependencies run: pnpm install --frozen-lockfile diff --git a/package.json b/package.json index 83e1c40..5cf593f 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,9 @@ "version": "0.0.0", "type": "module", "packageManager": "pnpm@11.0.9", + "engines": { + "node": "24.x" + }, "scripts": { "dev": "vite", "build": "tsc -b && vite build", From 1b4e166c3a9837fc61aed53a78993da934cb92fc Mon Sep 17 00:00:00 2001 From: keyobs Date: Fri, 14 Aug 2026 00:19:58 +0200 Subject: [PATCH 54/68] fix(ci): use GITHUB_TOKEN for the packages registry, not a manual secret dx-flow granted izeetok Actions access on the @keyobs/dx-flow package, so the automatic per-run GITHUB_TOKEN can now authenticate to npm.pkg.github.com (GitHub Packages requires a token even for public packages, unlike npmjs - it's not a private-package thing). Drops the GH_PACKAGES_TOKEN secret this would otherwise have required someone to create and rotate; adds permissions: packages: read to both workflows so that token actually carries package-read scope. Also fixed pnpm/setup's install:true default, which would have run pnpm install before the auth line was appended and hit the same 401 again. Co-Authored-By: Claude Sonnet 5 --- .claude/agents/deploy-troubleshooter.md | 2 +- .github/workflows/deploy-develop-pages.yml | 4 +++- .github/workflows/test.yml | 4 +++- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/.claude/agents/deploy-troubleshooter.md b/.claude/agents/deploy-troubleshooter.md index a0004d2..9ae7642 100644 --- a/.claude/agents/deploy-troubleshooter.md +++ b/.claude/agents/deploy-troubleshooter.md @@ -7,7 +7,7 @@ tools: Read, Grep, Glob, Bash, Skill You are the deploy troubleshooter for this repo. Given a failed CI run or a broken deployment, find the actual cause — never write or edit code, never use Edit/Write, never run a deploy yourself (`main` deploys to Vercel through its native Git integration; `develop` deploys to GitHub Pages through `deploy-develop-pages.yml` — neither goes through a CLI you'd invoke here). 1. **Start from the real failure output, not the workflow file's intent.** Use `gh run view --log-failed` (or the specific run/job the user points you to) to read what actually happened before reasoning about what should have happened. -2. **Name which stage failed before proposing a fix**: `pnpm install` itself (this repo depends on `@keyobs/dx-flow`, a private GitHub Packages registry — a bare `401 Unauthorized` at install time almost always means the `GH_PACKAGES_TOKEN` secret is missing, expired, or lacks `read:packages`, not a lockfile/code problem), the CI gate (`pnpm check`/`lint`/`test:run` in `test.yml`), the build (`pnpm build`, with `VITE_BASE_PATH=/izeetok/` set only for the GitHub Pages build), the GitHub Pages deploy step (`actions/deploy-pages`, visible in the run log), or Vercel's deploy/runtime (only visible in the Vercel dashboard, since this repo has no CLI token configured here) — each has a different fix path, and guessing the wrong one wastes a cycle. +2. **Name which stage failed before proposing a fix**: `pnpm install` itself (this repo depends on `@keyobs/dx-flow` from GitHub Packages — even public packages there require auth, unlike npmjs; a bare `401 Unauthorized` at install time means either the workflow's `permissions: packages: read` is missing, or `dx-flow`'s "Manage Actions access" no longer grants this repo — check that before assuming a lockfile/code problem), the CI gate (`pnpm check`/`lint`/`test:run` in `test.yml`), the build (`pnpm build`, with `VITE_BASE_PATH=/izeetok/` set only for the GitHub Pages build), the GitHub Pages deploy step (`actions/deploy-pages`, visible in the run log), or Vercel's deploy/runtime (only visible in the Vercel dashboard, since this repo has no CLI token configured here) — each has a different fix path, and guessing the wrong one wastes a cycle. 3. **For a Vercel-side failure**, you can't fetch its logs directly without an authenticated CLI — say so, and point to exactly what to check in the Vercel dashboard (the specific deployment, the build or function logs) rather than guessing at the cause from the app code alone. A GitHub Pages failure, by contrast, is fully visible in the Actions run log — use that directly. 4. **Ground your diagnosis in the exact command and its exact output** — quote the failing line, not a paraphrase. 5. **Give the command to reproduce the failure locally** whenever the failing stage runs a command that exists in `package.json` (`pnpm check`, `pnpm lint`, `pnpm test:run`, `pnpm build`) — that's almost always faster to iterate on than re-running CI. diff --git a/.github/workflows/deploy-develop-pages.yml b/.github/workflows/deploy-develop-pages.yml index 7db08ac..89857ff 100644 --- a/.github/workflows/deploy-develop-pages.yml +++ b/.github/workflows/deploy-develop-pages.yml @@ -13,6 +13,7 @@ on: permissions: contents: read + packages: read pages: write id-token: write @@ -42,9 +43,10 @@ jobs: with: version: 11.0.9 runtime: node@24 + install: false - name: Configure private registry auth - run: echo "//npm.pkg.github.com/:_authToken=${{ secrets.GH_PACKAGES_TOKEN }}" >> .npmrc + run: echo "//npm.pkg.github.com/:_authToken=${{ secrets.GITHUB_TOKEN }}" >> .npmrc - name: Install dependencies run: pnpm install --frozen-lockfile diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 22e0fa8..9f30806 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -11,6 +11,7 @@ on: permissions: contents: read + packages: read jobs: test: @@ -26,9 +27,10 @@ jobs: with: version: 11.0.9 runtime: node@24 + install: false - name: Configure private registry auth - run: echo "//npm.pkg.github.com/:_authToken=${{ secrets.GH_PACKAGES_TOKEN }}" >> .npmrc + run: echo "//npm.pkg.github.com/:_authToken=${{ secrets.GITHUB_TOKEN }}" >> .npmrc - name: Install dependencies run: pnpm install --frozen-lockfile From 8eecd81a801ebd32d8ece317864d50f61180f90e Mon Sep 17 00:00:00 2001 From: keyobs Date: Fri, 14 Aug 2026 00:31:40 +0200 Subject: [PATCH 55/68] fix(ci): stop double-running tests on develop pushes ci.yml and deploy-develop-pages.yml both triggered on push to develop, each independently calling test.yml - two parallel test runs per push. ci.yml only needs to cover push to main (defense in depth alongside Vercel's native deploy) and pull_request (the required check for merges into either branch); deploy-develop-pages.yml already gates its own build+deploy with the same test job on develop pushes. Co-Authored-By: Claude Sonnet 5 --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f259796..a54a917 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,7 +2,7 @@ name: CI on: push: - branches: [main, develop] + branches: [main] pull_request: permissions: From 314c723f54de6f054fb9d22faa696ef62240fc58 Mon Sep 17 00:00:00 2001 From: keyobs Date: Fri, 14 Aug 2026 00:34:37 +0200 Subject: [PATCH 56/68] fix(ci): grant packages: read on ci.yml too Reusable-workflow permissions are capped by the caller's own permissions block, not just by what the callee requests. test.yml asks for packages: read (to install @keyobs/dx-flow), but ci.yml - which also calls test.yml - only granted contents: read, so GitHub rejected the call outright ("packages: read... but is only allowed packages: none"). deploy-develop-pages.yml already had this right; ci.yml didn't. Co-Authored-By: Claude Sonnet 5 --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a54a917..b29a686 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,6 +7,7 @@ on: permissions: contents: read + packages: read jobs: tests: From c0cbeaf5e8155be367ed28cdfde10cbd2634f198 Mon Sep 17 00:00:00 2001 From: keyobs Date: Fri, 14 Aug 2026 00:37:48 +0200 Subject: [PATCH 57/68] fix(ci): drop the redundant push:main trigger on ci.yml With "Include administrators" enabled on main's branch protection, nobody can push there without going through a PR - the required check already ran against the PR's head commit via pull_request before any merge is possible. Running it again on the resulting push to main was pure duplicate work with no bypass scenario left to guard against. Co-Authored-By: Claude Sonnet 5 --- .github/workflows/ci.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b29a686..69c6fa8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,8 +1,6 @@ name: CI on: - push: - branches: [main] pull_request: permissions: From 2e4a425e8a3cd597ce44ee9bc0c9ee3212305ec6 Mon Sep 17 00:00:00 2001 From: keyobs Date: Fri, 14 Aug 2026 00:46:47 +0200 Subject: [PATCH 58/68] fix(draws): resolve the CSV path against the deployed base, not the domain root DEFAULT_CSV_URL and SpikePage's own copy of it were hardcoded to /results/euromillions_202002.csv, which only resolves on a root deploy (Vercel). On GitHub Pages (served from /izeetok/), every page that loads draws - /draws, /evaluation, /geometry, /laboratory, /discovery - 404'd fetching it. Both now build the URL from Vite's import.meta.env.BASE_URL, the same mechanism already used for the router's basename, so it resolves correctly under any base path. Co-Authored-By: Claude Sonnet 5 --- src/infrastructure/repositories/CsvDrawRepository.ts | 2 +- src/pages/spike/SpikePage.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/infrastructure/repositories/CsvDrawRepository.ts b/src/infrastructure/repositories/CsvDrawRepository.ts index 64a0756..b987225 100644 --- a/src/infrastructure/repositories/CsvDrawRepository.ts +++ b/src/infrastructure/repositories/CsvDrawRepository.ts @@ -2,7 +2,7 @@ import type { Draw } from '../../domain/draw/Draw.ts'; import { parseFdjCsv } from '../csv/parseFdjCsv.ts'; import type { DrawRepository } from './DrawRepository.ts'; -export const DEFAULT_CSV_URL = '/results/euromillions_202002.csv'; +export const DEFAULT_CSV_URL = `${import.meta.env.BASE_URL}results/euromillions_202002.csv`; export const createCsvDrawRepository = (csvUrl: string = DEFAULT_CSV_URL): DrawRepository => { let cachedDraws: Promise | null = null; diff --git a/src/pages/spike/SpikePage.tsx b/src/pages/spike/SpikePage.tsx index b2c11db..d1a8b8c 100644 --- a/src/pages/spike/SpikePage.tsx +++ b/src/pages/spike/SpikePage.tsx @@ -4,7 +4,7 @@ import { parseFdjCsv } from '../../infrastructure/csv/parseFdjCsv.ts'; import { buildSpatialEmbeddings } from '../../application/buildSpatialEmbeddings.ts'; import PointCloudScene from '../../spike/PointCloudScene.tsx'; -const CSV_URL = '/results/euromillions_202002.csv'; +const CSV_URL = `${import.meta.env.BASE_URL}results/euromillions_202002.csv`; const SpikePage = () => { const [embeddings, setEmbeddings] = useState(null); From 33a5e393ef67d5b3479756315079f0c6bf025540 Mon Sep 17 00:00:00 2001 From: keyobs Date: Fri, 14 Aug 2026 01:14:59 +0200 Subject: [PATCH 59/68] feat(evaluation): flag whether a grid already appeared in the historical draws Adds findExactMatch, a pure domain helper comparing a Grid against Draw history for an exact numbers+stars match - kept separate from evaluateGrid's four scores since this is a binary historical fact, not a continuous score, and mixing it in would collapse two different kinds of information into one. Surfaces it on /evaluation next to the reading-matrix line, with copy that keeps the app's non-predictive stance: a repeat draw doesn't change future odds, and the "never appeared" case is scoped to the CSV's actual coverage (since Feb 2020) rather than claiming EuroMillions history at large. Co-Authored-By: Claude Sonnet 5 --- src/domain/draw/findExactMatch.test.ts | 49 +++++++++++++++++++ src/domain/draw/findExactMatch.ts | 9 ++++ .../evaluation/EvaluationPage.module.scss | 5 ++ src/pages/evaluation/EvaluationPage.test.tsx | 42 ++++++++++++++++ src/pages/evaluation/EvaluationPage.tsx | 20 ++++++++ 5 files changed, 125 insertions(+) create mode 100644 src/domain/draw/findExactMatch.test.ts create mode 100644 src/domain/draw/findExactMatch.ts diff --git a/src/domain/draw/findExactMatch.test.ts b/src/domain/draw/findExactMatch.test.ts new file mode 100644 index 0000000..f7f8987 --- /dev/null +++ b/src/domain/draw/findExactMatch.test.ts @@ -0,0 +1,49 @@ +import { describe, expect, it } from 'vitest'; +import { parseGrid } from '../grid/Grid.ts'; +import type { Draw } from './Draw.ts'; +import { findExactMatch } from './findExactMatch.ts'; + +const buildDraw = (overrides: Partial = {}): Draw => ({ + id: '1', + date: '2024-01-05', + numbers: [3, 7, 19, 31, 42], + stars: [2, 9], + source: 'fdj-csv', + ...overrides, +}); + +describe('findExactMatch', () => { + it('finds the draw matching the same numbers and stars', () => { + const grid = parseGrid({ numbers: [3, 7, 19, 31, 42], stars: [2, 9] }); + const draw = buildDraw(); + + expect(findExactMatch(grid, [draw])).toEqual(draw); + }); + + it('is invariant to input order, since Grid is always sorted', () => { + const grid = parseGrid({ numbers: [42, 31, 19, 7, 3], stars: [9, 2] }); + const draw = buildDraw(); + + expect(findExactMatch(grid, [draw])).toEqual(draw); + }); + + it('returns null when no draw matches', () => { + const grid = parseGrid({ numbers: [1, 2, 3, 4, 5], stars: [1, 2] }); + const draw = buildDraw(); + + expect(findExactMatch(grid, [draw])).toBeNull(); + }); + + it('does not match when only the numbers are the same and the stars differ', () => { + const grid = parseGrid({ numbers: [3, 7, 19, 31, 42], stars: [1, 5] }); + const draw = buildDraw(); + + expect(findExactMatch(grid, [draw])).toBeNull(); + }); + + it('returns null for an empty history', () => { + const grid = parseGrid({ numbers: [3, 7, 19, 31, 42], stars: [2, 9] }); + + expect(findExactMatch(grid, [])).toBeNull(); + }); +}); diff --git a/src/domain/draw/findExactMatch.ts b/src/domain/draw/findExactMatch.ts new file mode 100644 index 0000000..1ab70e9 --- /dev/null +++ b/src/domain/draw/findExactMatch.ts @@ -0,0 +1,9 @@ +import type { Grid } from '../grid/Grid.ts'; +import type { Draw } from './Draw.ts'; + +export const findExactMatch = (grid: Grid, draws: Draw[]): Draw | null => + draws.find( + (draw) => + draw.numbers.every((number, index) => number === grid.numbers[index]) && + draw.stars.every((star, index) => star === grid.stars[index]), + ) ?? null; diff --git a/src/pages/evaluation/EvaluationPage.module.scss b/src/pages/evaluation/EvaluationPage.module.scss index bffedef..f210b59 100644 --- a/src/pages/evaluation/EvaluationPage.module.scss +++ b/src/pages/evaluation/EvaluationPage.module.scss @@ -5,6 +5,11 @@ text-align: left; } +.exactMatch { + color: var(--text-muted); + font-size: var(--text-sm); +} + .scores { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); diff --git a/src/pages/evaluation/EvaluationPage.test.tsx b/src/pages/evaluation/EvaluationPage.test.tsx index 163feb1..039a77a 100644 --- a/src/pages/evaluation/EvaluationPage.test.tsx +++ b/src/pages/evaluation/EvaluationPage.test.tsx @@ -40,6 +40,48 @@ describe('EvaluationPage', () => { expect(screen.getByTestId('variations').querySelectorAll('li')).toHaveLength(3); }); + it('flags a grid that never appeared in the available history', async () => { + vi.stubGlobal('fetch', () => Promise.resolve(new Response(REAL_CSV_TEXT))); + const user = userEvent.setup(); + + renderWithProviders(); + + const numbers = [3, 7, 19, 31, 42]; + const stars = [2, 9]; + for (const [index, value] of numbers.entries()) { + await user.type(screen.getByTestId(`number-input-${index}`), String(value)); + } + for (const [index, value] of stars.entries()) { + await user.type(screen.getByTestId(`star-input-${index}`), String(value)); + } + await user.click(screen.getByTestId('evaluate-button')); + + await waitFor(() => + expect(screen.getByTestId('exact-match-banner')).toHaveTextContent("n'est jamais sortie"), + ); + }); + + it('flags a grid that exactly matches a past draw, with its date', async () => { + vi.stubGlobal('fetch', () => Promise.resolve(new Response(REAL_CSV_TEXT))); + const user = userEvent.setup(); + + renderWithProviders(); + + const numbers = [21, 23, 33, 35, 47]; + const stars = [6, 7]; + for (const [index, value] of numbers.entries()) { + await user.type(screen.getByTestId(`number-input-${index}`), String(value)); + } + for (const [index, value] of stars.entries()) { + await user.type(screen.getByTestId(`star-input-${index}`), String(value)); + } + await user.click(screen.getByTestId('evaluate-button')); + + await waitFor(() => + expect(screen.getByTestId('exact-match-banner')).toHaveTextContent('déjà sortie le 2020-02-04'), + ); + }); + it('saves the evaluated grid so /geometry can reuse it as reference', async () => { vi.stubGlobal('fetch', () => Promise.resolve(new Response(REAL_CSV_TEXT))); const user = userEvent.setup(); diff --git a/src/pages/evaluation/EvaluationPage.tsx b/src/pages/evaluation/EvaluationPage.tsx index f569b0f..847c3b1 100644 --- a/src/pages/evaluation/EvaluationPage.tsx +++ b/src/pages/evaluation/EvaluationPage.tsx @@ -8,6 +8,7 @@ import { evaluatedGridRepository } from '../../application/evaluatedGridReposito import type { Grid } from '../../domain/grid/Grid.ts'; import type { EvaluationScores, ReadingMatrixLabel } from '../../domain/scoring/evaluateGrid.ts'; import { classifyReading, evaluateGrid } from '../../domain/scoring/evaluateGrid.ts'; +import { findExactMatch } from '../../domain/draw/findExactMatch.ts'; import GridInputForm from '../../components/gridInput/GridInputForm.tsx'; import ScoreCard from './ScoreCard.tsx'; import styles from './EvaluationPage.module.scss'; @@ -51,6 +52,18 @@ const EvaluationPage = () => { ); const reading = scores ? classifyReading(scores) : null; + const exactMatch = useMemo( + () => (grid && history.length > 0 ? findExactMatch(grid, history) : null), + [grid, history], + ); + const earliestDrawDate = useMemo( + () => + history.length === 0 + ? null + : history.reduce((earliest, draw) => (draw.date < earliest ? draw.date : earliest), history[0].date), + [history], + ); + return (

Évaluation d'une grille

@@ -62,6 +75,13 @@ const EvaluationPage = () => { {scores && reading && (

Lecture : {READING_LABELS[reading]}

+ {earliestDrawDate && ( +

+ {exactMatch + ? `Cette grille est déjà sortie le ${exactMatch.date} — la retirer ne change rien à ses chances de sortir à nouveau.` + : `Cette grille n'est jamais sortie dans l'historique disponible (depuis ${earliestDrawDate}) — comme la grande majorité des combinaisons possibles.`} +

+ )}
From afe626d289adafd830ad7c55798bf71cf49a2abb Mon Sep 17 00:00:00 2001 From: keyobs Date: Fri, 14 Aug 2026 01:17:48 +0200 Subject: [PATCH 60/68] feat(evaluation): also flag when the 5 numbers already came out, stars aside findNumbersOnlyMatches finds every past draw sharing the same 5 numbers regardless of stars - a looser, more frequent match than the exact one. Shown as a separate banner, only when there's no full exact match already (that message already covers the stronger case), listing every date it happened rather than just the first, since a repeat within 680 draws - while rare - isn't impossible. Co-Authored-By: Claude Sonnet 5 --- src/domain/draw/findExactMatch.test.ts | 33 +++++++++++++++++++- src/domain/draw/findExactMatch.ts | 4 +++ src/pages/evaluation/EvaluationPage.test.tsx | 22 +++++++++++++ src/pages/evaluation/EvaluationPage.tsx | 12 ++++++- 4 files changed, 69 insertions(+), 2 deletions(-) diff --git a/src/domain/draw/findExactMatch.test.ts b/src/domain/draw/findExactMatch.test.ts index f7f8987..b04dda2 100644 --- a/src/domain/draw/findExactMatch.test.ts +++ b/src/domain/draw/findExactMatch.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from 'vitest'; import { parseGrid } from '../grid/Grid.ts'; import type { Draw } from './Draw.ts'; -import { findExactMatch } from './findExactMatch.ts'; +import { findExactMatch, findNumbersOnlyMatches } from './findExactMatch.ts'; const buildDraw = (overrides: Partial = {}): Draw => ({ id: '1', @@ -47,3 +47,34 @@ describe('findExactMatch', () => { expect(findExactMatch(grid, [])).toBeNull(); }); }); + +describe('findNumbersOnlyMatches', () => { + it('matches a draw with the same numbers but different stars', () => { + const grid = parseGrid({ numbers: [3, 7, 19, 31, 42], stars: [1, 5] }); + const draw = buildDraw(); + + expect(findNumbersOnlyMatches(grid, [draw])).toEqual([draw]); + }); + + it('also matches a draw with the same numbers and the same stars', () => { + const grid = parseGrid({ numbers: [3, 7, 19, 31, 42], stars: [2, 9] }); + const draw = buildDraw(); + + expect(findNumbersOnlyMatches(grid, [draw])).toEqual([draw]); + }); + + it('returns every matching draw, not just the first', () => { + const grid = parseGrid({ numbers: [3, 7, 19, 31, 42], stars: [1, 5] }); + const first = buildDraw({ id: '1', date: '2021-03-01', stars: [4, 8] }); + const second = buildDraw({ id: '2', date: '2023-06-10', stars: [1, 5] }); + + expect(findNumbersOnlyMatches(grid, [first, second])).toEqual([first, second]); + }); + + it('returns an empty array when no draw shares the same numbers', () => { + const grid = parseGrid({ numbers: [1, 2, 3, 4, 5], stars: [1, 2] }); + const draw = buildDraw(); + + expect(findNumbersOnlyMatches(grid, [draw])).toEqual([]); + }); +}); diff --git a/src/domain/draw/findExactMatch.ts b/src/domain/draw/findExactMatch.ts index 1ab70e9..a4e5b48 100644 --- a/src/domain/draw/findExactMatch.ts +++ b/src/domain/draw/findExactMatch.ts @@ -7,3 +7,7 @@ export const findExactMatch = (grid: Grid, draws: Draw[]): Draw | null => draw.numbers.every((number, index) => number === grid.numbers[index]) && draw.stars.every((star, index) => star === grid.stars[index]), ) ?? null; + +/** Draws sharing the same 5 numbers, regardless of which stars came out - a looser match than findExactMatch. */ +export const findNumbersOnlyMatches = (grid: Grid, draws: Draw[]): Draw[] => + draws.filter((draw) => draw.numbers.every((number, index) => number === grid.numbers[index])); diff --git a/src/pages/evaluation/EvaluationPage.test.tsx b/src/pages/evaluation/EvaluationPage.test.tsx index 039a77a..336a9f1 100644 --- a/src/pages/evaluation/EvaluationPage.test.tsx +++ b/src/pages/evaluation/EvaluationPage.test.tsx @@ -82,6 +82,28 @@ describe('EvaluationPage', () => { ); }); + it('flags when the 5 numbers match a past draw but the stars differ', async () => { + vi.stubGlobal('fetch', () => Promise.resolve(new Response(REAL_CSV_TEXT))); + const user = userEvent.setup(); + + renderWithProviders(); + + const numbers = [21, 23, 33, 35, 47]; + const stars = [3, 4]; + for (const [index, value] of numbers.entries()) { + await user.type(screen.getByTestId(`number-input-${index}`), String(value)); + } + for (const [index, value] of stars.entries()) { + await user.type(screen.getByTestId(`star-input-${index}`), String(value)); + } + await user.click(screen.getByTestId('evaluate-button')); + + await waitFor(() => + expect(screen.getByTestId('numbers-only-match-banner')).toHaveTextContent('2020-02-04'), + ); + expect(screen.getByTestId('exact-match-banner')).toHaveTextContent("n'est jamais sortie"); + }); + it('saves the evaluated grid so /geometry can reuse it as reference', async () => { vi.stubGlobal('fetch', () => Promise.resolve(new Response(REAL_CSV_TEXT))); const user = userEvent.setup(); diff --git a/src/pages/evaluation/EvaluationPage.tsx b/src/pages/evaluation/EvaluationPage.tsx index 847c3b1..ae29fe7 100644 --- a/src/pages/evaluation/EvaluationPage.tsx +++ b/src/pages/evaluation/EvaluationPage.tsx @@ -8,7 +8,7 @@ import { evaluatedGridRepository } from '../../application/evaluatedGridReposito import type { Grid } from '../../domain/grid/Grid.ts'; import type { EvaluationScores, ReadingMatrixLabel } from '../../domain/scoring/evaluateGrid.ts'; import { classifyReading, evaluateGrid } from '../../domain/scoring/evaluateGrid.ts'; -import { findExactMatch } from '../../domain/draw/findExactMatch.ts'; +import { findExactMatch, findNumbersOnlyMatches } from '../../domain/draw/findExactMatch.ts'; import GridInputForm from '../../components/gridInput/GridInputForm.tsx'; import ScoreCard from './ScoreCard.tsx'; import styles from './EvaluationPage.module.scss'; @@ -56,6 +56,10 @@ const EvaluationPage = () => { () => (grid && history.length > 0 ? findExactMatch(grid, history) : null), [grid, history], ); + const numbersOnlyMatches = useMemo( + () => (grid && history.length > 0 && !exactMatch ? findNumbersOnlyMatches(grid, history) : []), + [grid, history, exactMatch], + ); const earliestDrawDate = useMemo( () => history.length === 0 @@ -82,6 +86,12 @@ const EvaluationPage = () => { : `Cette grille n'est jamais sortie dans l'historique disponible (depuis ${earliestDrawDate}) — comme la grande majorité des combinaisons possibles.`}

)} + {numbersOnlyMatches.length > 0 && ( +

+ Les 5 numéros de cette grille sont déjà sortis, avec des étoiles différentes :{' '} + {numbersOnlyMatches.map((draw) => draw.date).join(', ')}. +

+ )}
From 4a2658c7a9a3611a67baa26a3b578045fbe08db2 Mon Sep 17 00:00:00 2001 From: keyobs Date: Fri, 14 Aug 2026 01:21:11 +0200 Subject: [PATCH 61/68] feat(app): make the top navbar sticky on scroll Co-Authored-By: Claude Sonnet 5 --- src/app/AppLayout.module.scss | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/app/AppLayout.module.scss b/src/app/AppLayout.module.scss index 880c769..5ee1284 100644 --- a/src/app/AppLayout.module.scss +++ b/src/app/AppLayout.module.scss @@ -12,6 +12,9 @@ padding: var(--space-4) var(--space-6); border-bottom: 1px solid var(--border); background: var(--surface); + position: sticky; + top: 0; + z-index: 10; } .brand { From 0813d9030bd8138b7e7b12665e5110ff4b1d1a0a Mon Sep 17 00:00:00 2001 From: keyobs Date: Fri, 14 Aug 2026 01:24:48 +0200 Subject: [PATCH 62/68] feat(evaluation): restore the last evaluated grid on page load EvaluationPage always started blank even though evaluatedGridRepository already persisted the grid for /geometry's benefit - navigating away and back lost both the form input and the results. GridInputForm gets an optional initialGrid prop (used only by /evaluation for now; /geometry's free-form input is unaffected) to seed its digit inputs on mount, and EvaluationPage lazily initializes its grid state from the repository instead of null. Co-Authored-By: Claude Sonnet 5 --- src/components/gridInput/GridInputForm.tsx | 16 +++++++++-- src/pages/evaluation/EvaluationPage.test.tsx | 29 ++++++++++++++++++++ src/pages/evaluation/EvaluationPage.tsx | 9 ++++-- 3 files changed, 49 insertions(+), 5 deletions(-) diff --git a/src/components/gridInput/GridInputForm.tsx b/src/components/gridInput/GridInputForm.tsx index bac765d..38ac0d9 100644 --- a/src/components/gridInput/GridInputForm.tsx +++ b/src/components/gridInput/GridInputForm.tsx @@ -30,11 +30,21 @@ interface GridInputFormProps { onSubmit: (grid: Grid) => void; submitLabel: string; submitButtonTestId?: string; + initialGrid?: Grid | null; } -const GridInputForm = ({ onSubmit, submitLabel, submitButtonTestId = 'evaluate-button' }: GridInputFormProps) => { - const [numberInputs, setNumberInputs] = useState(EMPTY_NUMBERS); - const [starInputs, setStarInputs] = useState(EMPTY_STARS); +const GridInputForm = ({ + onSubmit, + submitLabel, + submitButtonTestId = 'evaluate-button', + initialGrid = null, +}: GridInputFormProps) => { + const [numberInputs, setNumberInputs] = useState( + initialGrid ? initialGrid.numbers.map(String) : EMPTY_NUMBERS, + ); + const [starInputs, setStarInputs] = useState( + initialGrid ? initialGrid.stars.map(String) : EMPTY_STARS, + ); const [error, setError] = useState(null); const numberFieldRefs = useRef<(HTMLInputElement | null)[]>([]); diff --git a/src/pages/evaluation/EvaluationPage.test.tsx b/src/pages/evaluation/EvaluationPage.test.tsx index 336a9f1..ab48985 100644 --- a/src/pages/evaluation/EvaluationPage.test.tsx +++ b/src/pages/evaluation/EvaluationPage.test.tsx @@ -104,6 +104,35 @@ describe('EvaluationPage', () => { expect(screen.getByTestId('exact-match-banner')).toHaveTextContent("n'est jamais sortie"); }); + it('restores the last evaluated grid when the page is remounted', async () => { + vi.stubGlobal('fetch', () => Promise.resolve(new Response(REAL_CSV_TEXT))); + const user = userEvent.setup(); + + const first = renderWithProviders(); + + const numbers = [3, 7, 19, 31, 42]; + const stars = [2, 9]; + for (const [index, value] of numbers.entries()) { + await user.type(screen.getByTestId(`number-input-${index}`), String(value)); + } + for (const [index, value] of stars.entries()) { + await user.type(screen.getByTestId(`star-input-${index}`), String(value)); + } + await user.click(screen.getByTestId('evaluate-button')); + await waitFor(() => expect(screen.getAllByTestId('score-card')).toHaveLength(4)); + + first.unmount(); + renderWithProviders(); + + for (const [index, value] of numbers.entries()) { + expect(screen.getByTestId(`number-input-${index}`)).toHaveValue(String(value)); + } + for (const [index, value] of stars.entries()) { + expect(screen.getByTestId(`star-input-${index}`)).toHaveValue(String(value)); + } + await waitFor(() => expect(screen.getAllByTestId('score-card')).toHaveLength(4)); + }); + it('saves the evaluated grid so /geometry can reuse it as reference', async () => { vi.stubGlobal('fetch', () => Promise.resolve(new Response(REAL_CSV_TEXT))); const user = userEvent.setup(); diff --git a/src/pages/evaluation/EvaluationPage.tsx b/src/pages/evaluation/EvaluationPage.tsx index ae29fe7..adc236f 100644 --- a/src/pages/evaluation/EvaluationPage.tsx +++ b/src/pages/evaluation/EvaluationPage.tsx @@ -23,7 +23,7 @@ const READING_LABELS: Record = { }; const EvaluationPage = () => { - const [grid, setGrid] = useState(null); + const [grid, setGrid] = useState(() => evaluatedGridRepository.getLast()); const historyQuery = useQuery({ queryKey: ['draws', 'all'], @@ -72,7 +72,12 @@ const EvaluationPage = () => {

Évaluation d'une grille

- + {historyQuery.isLoading &&

Chargement de l'historique...

} From 82290c8207b8f3397cc622fd75c5374499e79c61 Mon Sep 17 00:00:00 2001 From: keyobs Date: Fri, 14 Aug 2026 01:36:05 +0200 Subject: [PATCH 63/68] feat(agents): add release-manager subagent Drafts CHANGELOG.md from conventional commits since the last tag and recommends a semver bump (patch/minor/major) with reasoning - neither of which any existing tooling does. dx-flow's own release:* scripts only bump package.json and tag; there's no changelog generation anywhere in the repo. Stops at the draft: never runs pnpm release:*, npm version, or git tag itself, matching the read-only-except-explicit family convention already established for the other agents. Co-Authored-By: Claude Sonnet 5 --- .claude/agents/release-manager.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 .claude/agents/release-manager.md diff --git a/.claude/agents/release-manager.md b/.claude/agents/release-manager.md new file mode 100644 index 0000000..1f4ca3d --- /dev/null +++ b/.claude/agents/release-manager.md @@ -0,0 +1,14 @@ +--- +name: release-manager +description: Drafts CHANGELOG.md entries from conventional commits since the last release and recommends which version bump (patch/minor/major) they warrant. Never runs the actual release command (pnpm release:*) itself - that stays a human-triggered action once the draft is approved. +tools: Read, Grep, Glob, Bash, Edit, Write +--- + +You are the release manager for this repo. Given a request to prepare a release, draft the changelog and a version recommendation - never run `pnpm release:*`, `npm version`, or `git tag` yourself. + +1. **Gather commits since the last tag** (`git tag --sort=-creatordate` then `git log ..HEAD`; if no tag exists, use the full history). Read `commitlint.config.mjs`'s type list each time rather than assuming it's unchanged - it's the source of truth for what a commit's type means here. +2. **Categorize by type, not by guesswork.** Group commits under their actual conventional-commit type; skip anything that isn't user-facing (`chore`, `ci`, `test`, `build`) unless it's directly relevant context for a `feat`/`fix` in the same release. +3. **Recommend a semver bump with reasoning**: any `feat` → at least minor; `fix`/`perf`/`refactor`/etc. only → patch; a `!` after type/scope or a `BREAKING CHANGE:` footer → major. State which commits drove the recommendation, not just the number. +4. **Draft `CHANGELOG.md`** (create it, Keep-a-Changelog-styled, at the repo root if it doesn't exist yet - mirrors how `@keyobs/dx-flow` itself keeps one). Rephrase commit subjects for a reader who wasn't there - accurate, not just pasted verbatim - grouped by type, under the proposed version and today's date. +5. **Say so when a release isn't warranted** - if everything since the last tag is `chore`/`ci`/`test`, say that plainly instead of manufacturing a changelog entry to look useful. +6. **Stop at the draft.** Once `CHANGELOG.md` is written and the bump is recommended, tell the user exactly which `pnpm release:*` command matches your recommendation - don't run it. From 6d4a90d40c4878333db7e9c761112d4712d783b2 Mon Sep 17 00:00:00 2001 From: keyobs Date: Fri, 14 Aug 2026 01:50:20 +0200 Subject: [PATCH 64/68] doc(readme): rewrite in English, document architecture and agent tooling Replaces the unused Vite boilerplate with an actual README: what the app is and isn't (non-predictive), stack, commands, deployment targets and links, current src/ layout, and the versioned .claude/agents + .agents/skills + .agents/workflows setup. Co-Authored-By: Claude Sonnet 5 --- README.md | 75 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 74 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index d0d4eef..e039b73 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,75 @@ -# React + TypeScript + Vite +# EuroMillions Geometry Lab +Why predict when you could analyze? + +All combinations have the same theoretical probability: this app lets you analyze a combination against historical structures — it predicts nothing. + +Since every grid has the same odds of being drawn, this app instead lets you look at them through the lens of mathematical objects (features, geometry, PCA/UMAP, clustering, backtesting) — never suggesting any predictive power. + + +## Stack + +React 19 +TypeScript +Vite +react-router, TanStack Query, Zod +recharts, three.js/@react-three +pnpm. + +No backend — CSV dataset committed to the repo, user state in localStorage. + +## Getting started + +``` +pnpm install +pnpm dev +``` + +## Commands + +| Command | Role | +|---|---| +| `pnpm dev` | Dev server | +| `pnpm build` | Production build | +| `pnpm check` | Type-check (`tsc -b`) | +| `pnpm lint` / `pnpm lint:fix` | Lint (oxlint) | +| `pnpm test` / `pnpm test:run` | Unit tests (Vitest) | +| `pnpm test:e2e` | E2E tests (Playwright) | +| `pnpm release:patch/minor/major` | Version bump (dx-flow) | + +## Deployment + +`main` → Vercel (production, native Git integration). +`develop` → GitHub Pages: [keyobs.github.io/izeetok](https://keyobs.github.io/izeetok) + +## Architecture + +React SPA. +The mathematical domain stays pure TypeScript, decoupled from React: + +``` +src/ + app/ router, layout, config (queryClient...) + domain/ pure scientific logic — Grid, Draw, features, geometry, scoring, backtest... + application/ orchestration — repositories, variation generation, embeddings... + infrastructure/ data access — CSV, concrete repository implementations + pages/ one page per route (/evaluation, /geometry, /draws, /laboratory, /discovery) + components/ generic reusable components, no business logic + shared/ cross-cutting test tooling +``` + + +## Claude Code & agents + +The agentic tooling is versioned, not just used locally: + +- `.claude/CLAUDE.md` — conventions and working process for Claude Code on this repo. +- `.claude/agents/` — specialized subagents: + - `architect` — explores and plans a feature/change, doesn't write code + - `test-strategist` — identifies which tests are worth writing + - `quality-reviewer` — checks a diff against this repo's conventions + - `deploy-troubleshooter` — diagnoses a broken CI/deploy run + - `release-manager` — drafts the changelog and recommends the version bump +- `.agents/skills/` — shared skills (`domain-modeling`, `grilling`, `grill-me`, `grill-with-docs`). +- `.agents/workflows/feature.md` — chains plan → challenge → human validation → + implementation → tests → review → validation, for any non-trivial feature. From 8abc2ced515581f9e335d2c342ea80efdf2cadd3 Mon Sep 17 00:00:00 2001 From: keyobs Date: Fri, 14 Aug 2026 02:01:09 +0200 Subject: [PATCH 65/68] doc(changelog): add CHANGELOG.md for the 0.1.0 release First entry, drafted by release-manager from the full commit history (no prior tag existed). Recommends a minor bump: the release is dominated by feat commits (grid domain, evaluation, /evaluation, /draws, /geometry, /laboratory, /discovery, agent tooling, CI/CD) with no breaking-change marker anywhere in the log. Co-Authored-By: Claude Sonnet 5 --- CHANGELOG.md | 52 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..9989c94 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,52 @@ +# Changelog + +All notable changes to this project are documented in this file. +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and this project follows conventional commits (see `commitlint.config.mjs`) for +release-note generation. + +## [Unreleased] + +## [0.1.0] - 2026-08-14 + +First release: grid evaluation, historical draws, geometry, backtesting lab, +structure discovery, plus the versioned agent tooling and CI/CD. + +### Added + +- Core domain model: `Grid`, `Draw`, feature extraction, geometry descriptors + and distance. +- Four evaluation scores (structure, originality, temporal, confidence) with + a reading classification. +- Grid variation generator (structurally-common, balanced, anti-share). +- `/evaluation`: grid input, scores, variations, already-drawn flags, + restores the last evaluated grid on reload. +- `/draws`: historical draws table with per-draw geometry and distance to the + previous draw. +- `/geometry`: scatter charts, decade histogram, gap map, nearest neighbors, + selectable reference grid. +- `/laboratory`: strategy builder, walk-forward backtester, baselines, Monte + Carlo comparison. +- `/discovery`: feature normalization, PCA, K-Means clustering, density/outlier + scoring. +- Sticky top navigation. +- CI (lint/type-check/test) gating `main` and `develop`; GitHub Pages + deployment for `develop` alongside Vercel for `main`. +- Versioned agent tooling: architect, test-strategist, quality-reviewer, + deploy-troubleshooter, release-manager, and a documented feature workflow. +- English README. + +### Changed + +- Reworked the UI with design tokens and a dark-first palette. + +### Fixed + +- Digit-entry inputs on `/evaluation` (auto-advance, live validity) instead + of native number spinners. +- Duplicate numbers/stars flagged red past their first occurrence. +- `/geometry`: plain-language descriptions, persistent reference banner, + highlighted reference point, readable gap map. +- CSV path now resolves under a sub-path deployment (was 404ing on GitHub + Pages). +- `check` script and pre-commit hook now actually type-check the project. From cfa5f5a8b2ddc9c8f32b0828d609629879e7feb8 Mon Sep 17 00:00:00 2001 From: keyobs Date: Fri, 14 Aug 2026 02:01:19 +0200 Subject: [PATCH 66/68] fix(agents): tighten release-manager's brevity and self-verification It drafted a first CHANGELOG.md that was accurate but far too verbose (every sub-parameter and count spelled out per bullet), and separately once reported the file as written without ever having called Write - composed the text in its own reasoning and asserted a result it hadn't produced. Both caught by re-reading the actual file after the fact. Adds: one line per entry, no sub-detail dump; and read the file back after writing it, report only what's confirmed on disk. Co-Authored-By: Claude Sonnet 5 --- .claude/agents/release-manager.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.claude/agents/release-manager.md b/.claude/agents/release-manager.md index 1f4ca3d..62b8496 100644 --- a/.claude/agents/release-manager.md +++ b/.claude/agents/release-manager.md @@ -9,6 +9,7 @@ You are the release manager for this repo. Given a request to prepare a release, 1. **Gather commits since the last tag** (`git tag --sort=-creatordate` then `git log ..HEAD`; if no tag exists, use the full history). Read `commitlint.config.mjs`'s type list each time rather than assuming it's unchanged - it's the source of truth for what a commit's type means here. 2. **Categorize by type, not by guesswork.** Group commits under their actual conventional-commit type; skip anything that isn't user-facing (`chore`, `ci`, `test`, `build`) unless it's directly relevant context for a `feat`/`fix` in the same release. 3. **Recommend a semver bump with reasoning**: any `feat` → at least minor; `fix`/`perf`/`refactor`/etc. only → patch; a `!` after type/scope or a `BREAKING CHANGE:` footer → major. State which commits drove the recommendation, not just the number. -4. **Draft `CHANGELOG.md`** (create it, Keep-a-Changelog-styled, at the repo root if it doesn't exist yet - mirrors how `@keyobs/dx-flow` itself keeps one). Rephrase commit subjects for a reader who wasn't there - accurate, not just pasted verbatim - grouped by type, under the proposed version and today's date. +4. **Draft `CHANGELOG.md`** (create it, Keep-a-Changelog-styled, at the repo root if it doesn't exist yet - mirrors how `@keyobs/dx-flow` itself keeps one). Write it in English, matching this repo's commit message and README language. One line per entry - name the change, not every parameter/count/sub-detail behind it; save the depth for someone who opens the diff. Rephrase commit subjects for a reader who wasn't there, grouped by type, under the proposed version and today's date. 5. **Say so when a release isn't warranted** - if everything since the last tag is `chore`/`ci`/`test`, say that plainly instead of manufacturing a changelog entry to look useful. -6. **Stop at the draft.** Once `CHANGELOG.md` is written and the bump is recommended, tell the user exactly which `pnpm release:*` command matches your recommendation - don't run it. +6. **Verify the write before reporting it.** After calling Write/Edit on `CHANGELOG.md`, read it back and confirm the content is actually there - report only what you just confirmed on disk, never what you composed in your own reasoning but didn't verify was saved. +7. **Stop at the draft.** Once `CHANGELOG.md` is written and the bump is recommended, tell the user exactly which `pnpm release:*` command matches your recommendation - don't run it. From a4f839d383e24193fd69ad05358304dbdf344473 Mon Sep 17 00:00:00 2001 From: keyobs Date: Fri, 14 Aug 2026 02:03:28 +0200 Subject: [PATCH 67/68] chore(skills): sync skills-lock.json with the installed skills Was tracking only grill-with-docs; the other three skills already versioned under .agents/skills/ (domain-modeling, grill-me, grilling) were installed but never recorded in the lockfile. Co-Authored-By: Claude Sonnet 5 --- skills-lock.json | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/skills-lock.json b/skills-lock.json index deaf0b3..921621f 100644 --- a/skills-lock.json +++ b/skills-lock.json @@ -1,11 +1,29 @@ { "version": 1, "skills": { + "domain-modeling": { + "source": "mattpocock/skills", + "sourceType": "github", + "skillPath": "skills/engineering/domain-modeling/SKILL.md", + "computedHash": "363cb0f53b0b431e7c00086ad1f823500b7e1b70b5616ee969c979f0934e9e6e" + }, + "grill-me": { + "source": "mattpocock/skills", + "sourceType": "github", + "skillPath": "skills/productivity/grill-me/SKILL.md", + "computedHash": "f361db4e15e6bfd562a9282b1dccda513910a50061f9e838ce017be9c69dde3f" + }, "grill-with-docs": { "source": "mattpocock/skills", "sourceType": "github", "skillPath": "skills/engineering/grill-with-docs/SKILL.md", "computedHash": "9c460cbd94fd3c63cdef967dbdb6e66ca687103cdc380cd37834e4d10b738f78" + }, + "grilling": { + "source": "mattpocock/skills", + "sourceType": "github", + "skillPath": "skills/productivity/grilling/SKILL.md", + "computedHash": "4ebdd12fe61ff3abf20cff6683740e2b9b3739454be0a3f4a928a1c4d07d6b34" } } } From 1d2494b2d24fd0a3341863d22db6a6cead86c5c0 Mon Sep 17 00:00:00 2001 From: keyobs Date: Fri, 14 Aug 2026 02:04:20 +0200 Subject: [PATCH 68/68] chore(release): 0.1.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5cf593f..dfb8291 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "izeetok", "private": true, - "version": "0.0.0", + "version": "0.1.0", "type": "module", "packageManager": "pnpm@11.0.9", "engines": {