diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..26a77e5
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,9 @@
+node_modules/
+www/
+dist/
+dist-electron/
+android/
+ios/
+.vite/
+.vitest/
+coverage/
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..16c7df5
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,12 @@
+# Changelog — Gestion table tournante Libre
+
+## 2026.10.05.01 — 5 octobre 2026
+
+**Ce qui change pour vous**
+- L'application s'ouvre et affiche sa version dans un bandeau permanent et dans
+ le titre de sa fenêtre.
+
+**Version de format des fichiers** : aucune — cette version n'enregistre encore
+aucun événement.
+
+**En remplaçant** : rien à faire, c'est la première version.
diff --git a/capacitor.config.json b/capacitor.config.json
new file mode 100644
index 0000000..e08428a
--- /dev/null
+++ b/capacitor.config.json
@@ -0,0 +1,5 @@
+{
+ "appId": "ca.technolibre.gestiontabletournante",
+ "appName": "Gestion table tournante Libre",
+ "webDir": "www"
+}
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..4b15a74
--- /dev/null
+++ b/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 0000000..49380b7
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,2835 @@
+{
+ "name": "gestion-table-tournante-libre",
+ "version": "2026.1005.1",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "gestion-table-tournante-libre",
+ "version": "2026.1005.1",
+ "license": "AGPL-3.0-or-later",
+ "devDependencies": {
+ "@capacitor/cli": "^8.5.2",
+ "@capacitor/core": "^8.5.2",
+ "@sveltejs/vite-plugin-svelte": "^7.3.1",
+ "@vitest/browser-playwright": "^5.0.3",
+ "fast-check": "^4.10.2",
+ "playwright": "^1.63.0",
+ "svelte": "^5.57.1",
+ "vite": "^8.3.2",
+ "vitest": "^5.0.3"
+ }
+ },
+ "node_modules/@blazediff/core": {
+ "version": "1.10.0",
+ "resolved": "https://registry.npmjs.org/@blazediff/core/-/core-1.10.0.tgz",
+ "integrity": "sha512-AOQff0zgR7cGsZL+4E7hVkmujoPUpm0J9xzWGWZj5wCjd3gmxESXAPfKyuzs93VdpQNFhHlBhfOjrcZ+XTERtQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@capacitor/cli": {
+ "version": "8.5.2",
+ "resolved": "https://registry.npmjs.org/@capacitor/cli/-/cli-8.5.2.tgz",
+ "integrity": "sha512-O2kpA5ZPJUAmliDCcmpMf8xnJwMezxlFPUrlyOAjbL3U9MFNY3l4ZpPI/UXwvyPmR8ETcVF9pFr+VdWq61rqTQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@ionic/cli-framework-output": "^2.2.8",
+ "@ionic/utils-subprocess": "^3.0.1",
+ "@ionic/utils-terminal": "^2.3.5",
+ "commander": "^12.1.0",
+ "debug": "^4.4.0",
+ "env-paths": "^2.2.0",
+ "fs-extra": "^11.2.0",
+ "kleur": "^4.1.5",
+ "native-run": "^2.0.3",
+ "open": "^8.4.0",
+ "plist": "^3.1.0",
+ "prompts": "^2.4.2",
+ "rimraf": "^6.0.1",
+ "semver": "^7.6.3",
+ "tar": "^7.5.3",
+ "tslib": "^2.8.1",
+ "xcode": "^3.0.1",
+ "xml2js": "^0.6.2"
+ },
+ "bin": {
+ "cap": "bin/capacitor",
+ "capacitor": "bin/capacitor"
+ },
+ "engines": {
+ "node": ">=22.0.0"
+ }
+ },
+ "node_modules/@capacitor/core": {
+ "version": "8.5.2",
+ "resolved": "https://registry.npmjs.org/@capacitor/core/-/core-8.5.2.tgz",
+ "integrity": "sha512-agnQbGdhGw/KZFuLf7H9SY2o+oE7upkr/WPQsYaokXP6f9chaE8iVmbKkfN6SVl4w3pmz7oZ3Ur1hbkmy1hx+w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^2.1.0"
+ }
+ },
+ "node_modules/@ionic/cli-framework-output": {
+ "version": "2.2.8",
+ "resolved": "https://registry.npmjs.org/@ionic/cli-framework-output/-/cli-framework-output-2.2.8.tgz",
+ "integrity": "sha512-TshtaFQsovB4NWRBydbNFawql6yul7d5bMiW1WYYf17hd99V6xdDdk3vtF51bw6sLkxON3bDQpWsnUc9/hVo3g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@ionic/utils-terminal": "2.3.5",
+ "debug": "^4.0.0",
+ "tslib": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=16.0.0"
+ }
+ },
+ "node_modules/@ionic/utils-array": {
+ "version": "2.1.6",
+ "resolved": "https://registry.npmjs.org/@ionic/utils-array/-/utils-array-2.1.6.tgz",
+ "integrity": "sha512-0JZ1Zkp3wURnv8oq6Qt7fMPo5MpjbLoUoa9Bu2Q4PJuSDWM8H8gwF3dQO7VTeUj3/0o1IB1wGkFWZZYgUXZMUg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.0.0",
+ "tslib": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=16.0.0"
+ }
+ },
+ "node_modules/@ionic/utils-fs": {
+ "version": "3.1.7",
+ "resolved": "https://registry.npmjs.org/@ionic/utils-fs/-/utils-fs-3.1.7.tgz",
+ "integrity": "sha512-2EknRvMVfhnyhL1VhFkSLa5gOcycK91VnjfrTB0kbqkTFCOXyXgVLI5whzq7SLrgD9t1aqos3lMMQyVzaQ5gVA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/fs-extra": "^8.0.0",
+ "debug": "^4.0.0",
+ "fs-extra": "^9.0.0",
+ "tslib": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=16.0.0"
+ }
+ },
+ "node_modules/@ionic/utils-fs/node_modules/fs-extra": {
+ "version": "9.1.0",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz",
+ "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "at-least-node": "^1.0.0",
+ "graceful-fs": "^4.2.0",
+ "jsonfile": "^6.0.1",
+ "universalify": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@ionic/utils-object": {
+ "version": "2.1.6",
+ "resolved": "https://registry.npmjs.org/@ionic/utils-object/-/utils-object-2.1.6.tgz",
+ "integrity": "sha512-vCl7sl6JjBHFw99CuAqHljYJpcE88YaH2ZW4ELiC/Zwxl5tiwn4kbdP/gxi2OT3MQb1vOtgAmSNRtusvgxI8ww==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.0.0",
+ "tslib": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=16.0.0"
+ }
+ },
+ "node_modules/@ionic/utils-process": {
+ "version": "2.1.12",
+ "resolved": "https://registry.npmjs.org/@ionic/utils-process/-/utils-process-2.1.12.tgz",
+ "integrity": "sha512-Jqkgyq7zBs/v/J3YvKtQQiIcxfJyplPgECMWgdO0E1fKrrH8EF0QGHNJ9mJCn6PYe2UtHNS8JJf5G21e09DfYg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@ionic/utils-object": "2.1.6",
+ "@ionic/utils-terminal": "2.3.5",
+ "debug": "^4.0.0",
+ "signal-exit": "^3.0.3",
+ "tree-kill": "^1.2.2",
+ "tslib": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=16.0.0"
+ }
+ },
+ "node_modules/@ionic/utils-stream": {
+ "version": "3.1.7",
+ "resolved": "https://registry.npmjs.org/@ionic/utils-stream/-/utils-stream-3.1.7.tgz",
+ "integrity": "sha512-eSELBE7NWNFIHTbTC2jiMvh1ABKGIpGdUIvARsNPMNQhxJB3wpwdiVnoBoTYp+5a6UUIww4Kpg7v6S7iTctH1w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.0.0",
+ "tslib": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=16.0.0"
+ }
+ },
+ "node_modules/@ionic/utils-subprocess": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/@ionic/utils-subprocess/-/utils-subprocess-3.0.1.tgz",
+ "integrity": "sha512-cT4te3AQQPeIM9WCwIg8ohroJ8TjsYaMb2G4ZEgv9YzeDqHZ4JpeIKqG2SoaA3GmVQ3sOfhPM6Ox9sxphV/d1A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@ionic/utils-array": "2.1.6",
+ "@ionic/utils-fs": "3.1.7",
+ "@ionic/utils-process": "2.1.12",
+ "@ionic/utils-stream": "3.1.7",
+ "@ionic/utils-terminal": "2.3.5",
+ "cross-spawn": "^7.0.3",
+ "debug": "^4.0.0",
+ "tslib": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=16.0.0"
+ }
+ },
+ "node_modules/@ionic/utils-terminal": {
+ "version": "2.3.5",
+ "resolved": "https://registry.npmjs.org/@ionic/utils-terminal/-/utils-terminal-2.3.5.tgz",
+ "integrity": "sha512-3cKScz9Jx2/Pr9ijj1OzGlBDfcmx7OMVBt4+P1uRR0SSW4cm1/y3Mo4OY3lfkuaYifMNBW8Wz6lQHbs1bihr7A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/slice-ansi": "^4.0.0",
+ "debug": "^4.0.0",
+ "signal-exit": "^3.0.3",
+ "slice-ansi": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0",
+ "tslib": "^2.0.1",
+ "untildify": "^4.0.0",
+ "wrap-ansi": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=16.0.0"
+ }
+ },
+ "node_modules/@isaacs/fs-minipass": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz",
+ "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "minipass": "^7.0.4"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@jridgewell/gen-mapping": {
+ "version": "0.3.13",
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
+ "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.0",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/remapping": {
+ "version": "2.3.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
+ "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.5",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/resolve-uri": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz",
+ "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.31",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
+ "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.1.0",
+ "@jridgewell/sourcemap-codec": "^1.4.14"
+ }
+ },
+ "node_modules/@oxc-project/types": {
+ "version": "0.152.0",
+ "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.152.0.tgz",
+ "integrity": "sha512-oM/5rLBm2tPkg0iBgkH/FOeR3PCDpY19GTgAZjMFM8h9WI9VW7cLgzp6nwtarYKmovavIQZ+Fe/RKX/8C8O/Rw==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/oxc-project"
+ }
+ },
+ "node_modules/@polka/url": {
+ "version": "1.0.0-next.29",
+ "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz",
+ "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@rolldown/binding-android-arm-eabi": {
+ "version": "1.2.12",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.12.tgz",
+ "integrity": "sha512-dB/a1214qKfHMXCpgqR4OZT+jS4kTyEXbQGJPqzobt5EwH5rX080pxE37alt3RzvR1bf1Yz/yGqRfrYAxuPw0A==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-android-arm64": {
+ "version": "1.2.12",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.12.tgz",
+ "integrity": "sha512-7KHFgQ5VJxIHcLlrwrc3Xbds7oTNQT7Pgi9gQCJKrd2VGab/UksIOYp6VD8MzCstGxOKMgNamPwUCfxPdP1OHg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-darwin-arm64": {
+ "version": "1.2.12",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.12.tgz",
+ "integrity": "sha512-3YIhqHD96nA5SaYNRBR16HnGv4oavZvXfD/ayHM+oYZ0WD/8lBAtf6zQua4kEyAvpqrluKXl0lnOBoiNby7x9w==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-darwin-x64": {
+ "version": "1.2.12",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.12.tgz",
+ "integrity": "sha512-UuuJ35MFw4gmFOrE9pEqIV+K3syIKveph+Qc1/ljHZVdoDW4pz/JHR/eMVom+TZGl/5OOvGJOWaOCVt3ZfqhxA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-freebsd-x64": {
+ "version": "1.2.12",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.12.tgz",
+ "integrity": "sha512-uMvssit0a4W+/7D8CbHUvG719mH3R2jwXAlh/XcPvuHTE0g++LymF88DCGNX0HM2rBOn0xrzgXktIB6fLSJBTQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-arm-gnueabihf": {
+ "version": "1.2.12",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.12.tgz",
+ "integrity": "sha512-XcFu0R0xWnwzSf4IQgFH1rJIckPN1pLy2R+4r9IDB7Yfu/ys9cVqfa4pBrMHj7a3gl8mIR4nRNPg0e5IvEVs6g==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-arm64-gnu": {
+ "version": "1.2.12",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.12.tgz",
+ "integrity": "sha512-260UrKgn8tz39ak+SMDOirKzr7V04M9dWPw5llW00SwBivCZoWcRBKV1d8cXnRkUmSZA3BdiUmBHWk7734Ulpw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-arm64-musl": {
+ "version": "1.2.12",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.12.tgz",
+ "integrity": "sha512-5YK1I9SqDkbPgc1IA8BgDl34suqUS2q0KWnBrirm0E51YjOs6eo6dV6jbQfNE/argHRSvd0QUGgtpIoYx+WWpw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-ppc64-gnu": {
+ "version": "1.2.12",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.12.tgz",
+ "integrity": "sha512-Rkcrmp7eFRg74yL5fXEU91JEWbdEPLevWwGtXpmhbjlD1StScbWTmO94Bhly+Mo+ketKYkdmM1vNUKeWSlx8cQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-s390x-gnu": {
+ "version": "1.2.12",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.12.tgz",
+ "integrity": "sha512-qvK4DuAsQc2BSjlx+Xr+IzOIvvxbGZqxFwdWfG6F518Erj0GGISyQbJ6pIappnOxlNPzNHvo/L0BwB30GZ+zVw==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-x64-gnu": {
+ "version": "1.2.12",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.12.tgz",
+ "integrity": "sha512-Q9uLBO53Xd4QIq1WOycVQyPP1O4HhraEV2qqb3uTrnVw6QZih9duY4vNXOivL1xoUS1/z+W8eF4NMfl2a8Sdjw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-x64-musl": {
+ "version": "1.2.12",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.12.tgz",
+ "integrity": "sha512-3IBxWFMjbOZskDPKv8Lf9BCnahlKuHthWkYnyIxOH/QcJrFcS4EmcenthApkwr/5+nEqZlLzeYbxeMaX7A5u4g==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-openharmony-arm64": {
+ "version": "1.2.12",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.12.tgz",
+ "integrity": "sha512-xtX61xg4LKPkPWilZU1ynKClz5Gj4bf74LML4r3eVLWumKnGjoEr1OSHQhMdbBDoYTi+yjrujvpZe2pUnqCrrA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-win32-arm64-msvc": {
+ "version": "1.2.12",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.12.tgz",
+ "integrity": "sha512-At7fPB6PCaIjzgIhEZFxuT+BBFqiQibJDT4d3PhiR3f4E7bbMZF4aKblbFfEM3sETRDd1YiQx/+U/g/B/ou5Ew==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-win32-x64-msvc": {
+ "version": "1.2.12",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.12.tgz",
+ "integrity": "sha512-WIw2haVKwjuYdXkHaoC0mF8Le71TuCBxjrdKqLbJGctbBABj+ClfmNvtbOnzpq3RokNo5+V1qhtSzJyXorsklQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/pluginutils": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz",
+ "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@sveltejs/acorn-typescript": {
+ "version": "1.0.13",
+ "resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.13.tgz",
+ "integrity": "sha512-wgKggnhZVL9Bfx1OaKKTrYY9BFRk6C8UAkQNUcIv1+llzYrIqy+RZm5HPKzn0NpEBvTVhTqB4kQyllZywsRBRQ==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "acorn": "^8.9.0"
+ }
+ },
+ "node_modules/@sveltejs/vite-plugin-svelte": {
+ "version": "7.3.1",
+ "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-7.3.1.tgz",
+ "integrity": "sha512-ZPsLN8B1e/En+Ak5s4V7srFDT532oS0qieLsQwu63NGKsS+iAjoO2Js1BochlHlglcU+Pt7WAO3C5Ee+4f6gVA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "deepmerge": "^4.3.1",
+ "magic-string": "^1.0.0",
+ "obug": "^2.1.0",
+ "vitefu": "^1.1.2"
+ },
+ "engines": {
+ "node": "^20.19 || ^22.12 || >=24"
+ },
+ "peerDependencies": {
+ "svelte": "^5.46.4",
+ "vite": "^8.0.0-beta.7 || ^8.0.0"
+ }
+ },
+ "node_modules/@types/chai": {
+ "version": "5.2.3",
+ "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz",
+ "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/deep-eql": "*",
+ "assertion-error": "^2.0.1"
+ }
+ },
+ "node_modules/@types/deep-eql": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz",
+ "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.9",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
+ "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/fs-extra": {
+ "version": "8.1.5",
+ "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-8.1.5.tgz",
+ "integrity": "sha512-0dzKcwO+S8s2kuF5Z9oUWatQJj5Uq/iqphEtE3GQJVRRYm/tD1LglU2UnXi2A8jLq5umkGouOXOR9y0n613ZwQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/node": {
+ "version": "26.6.4",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.4.tgz",
+ "integrity": "sha512-ldVPDCzj7fsaGZrLB0NuHuTvJcsNasysBAqMolr/cgxrLd1xbqxIr3XJiPnHHJUCxj5sNF1vnRj9aWnrVh5Jcg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~8.9.0"
+ }
+ },
+ "node_modules/@types/slice-ansi": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/@types/slice-ansi/-/slice-ansi-4.0.0.tgz",
+ "integrity": "sha512-+OpjSaq85gvlZAYINyzKpLeiFkSC4EsC6IIiT6v6TLSU5k5U83fHGj9Lel8oKEXM0HqgrMVCjXPDPVICtxF7EQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@vitest/browser": {
+ "version": "5.0.3",
+ "resolved": "https://registry.npmjs.org/@vitest/browser/-/browser-5.0.3.tgz",
+ "integrity": "sha512-7KHIkVA/gTRxQV9qPa9qLK+g5INZG6iRrlwXSFG5EKsOomW3UzzW7wsdt1Yp5PufzQ43lniuONlb4lUVCC2qSg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@blazediff/core": "1.10.0",
+ "@vitest/mocker": "5.0.3",
+ "@vitest/ui": "5.0.3",
+ "@vitest/utils": "5.0.3",
+ "magic-string": "^1.2.3",
+ "pngjs": "^7.0.0",
+ "sirv": "^3.0.2",
+ "tinyrainbow": "^3.1.1",
+ "ws": "^8.21.3"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "vitest": "5.0.3"
+ }
+ },
+ "node_modules/@vitest/browser-playwright": {
+ "version": "5.0.3",
+ "resolved": "https://registry.npmjs.org/@vitest/browser-playwright/-/browser-playwright-5.0.3.tgz",
+ "integrity": "sha512-rYBHwIFL/ZtI6dl0rbrAatScXCrvX+TL2BX2AGOigkC1Y6u3rIWw8KMQJJMS4drSa1ksMCmZU2wpgwdE7kqu3g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/browser": "5.0.3",
+ "@vitest/mocker": "5.0.3",
+ "tinyrainbow": "^3.1.1"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "playwright": "*",
+ "vitest": "5.0.3"
+ },
+ "peerDependenciesMeta": {
+ "playwright": {
+ "optional": false
+ }
+ }
+ },
+ "node_modules/@vitest/mocker": {
+ "version": "5.0.3",
+ "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-5.0.3.tgz",
+ "integrity": "sha512-T8sWAIbkSyAjkwTcaEc3Iu0o9A27X1/kdXrizhZkGuSKScRQtRzclfAMpOTcGdXCsqxeWlpGy3XjqaW8CpLORg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/trace-mapping": "0.3.31",
+ "@vitest/spy": "5.0.3",
+ "estree-walker": "^3.0.3",
+ "magic-string": "^1.2.3"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "msw": "^2.4.9",
+ "vite": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "msw": {
+ "optional": true
+ },
+ "vite": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@vitest/pretty-format": {
+ "version": "5.0.3",
+ "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-5.0.3.tgz",
+ "integrity": "sha512-3hr5eV5z2ff0l2Q1vaKQbB4j3M3tElw8stDokEOTqXJB3Es5VrT8gBRIMY59gGuFp8Y61sWlWFK1aNUH5dHDYw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tinyrainbow": "^3.1.1"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/spy": {
+ "version": "5.0.3",
+ "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-5.0.3.tgz",
+ "integrity": "sha512-XhFysQTB8AZ+P4gMi+Lpo99vg2AZi0qKpaB9yXQl37+CaMEAPO3iH/wGVnSyL5MPERiLezpqTVtrR6UZH5GCXg==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/ui": {
+ "version": "5.0.3",
+ "resolved": "https://registry.npmjs.org/@vitest/ui/-/ui-5.0.3.tgz",
+ "integrity": "sha512-9dfvqDBvSbHM6y9YVXxAypkl2z7mrWozK/JFo237qI0cj/3M/kzDKiVZqzi3TE54Ler7Jditud7OMBZCyUvRHA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/utils": "5.0.3",
+ "fflate": "^0.8.3",
+ "flatted": "^3.4.4",
+ "pathe": "^2.0.3",
+ "sirv": "^3.0.2",
+ "tinyrainbow": "^3.1.1"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "vitest": "5.0.3"
+ }
+ },
+ "node_modules/@vitest/utils": {
+ "version": "5.0.3",
+ "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-5.0.3.tgz",
+ "integrity": "sha512-77fDTZv8eu1CzlIfgPU2yrl5MXSUaUAOvgdZEeVm3X3WIxUubIX2SforqS6176lT32HCF6uBVMYVqfHxw7o6eg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/pretty-format": "5.0.3",
+ "convert-source-map": "^2.0.0",
+ "tinyrainbow": "^3.1.1"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@xmldom/xmldom": {
+ "version": "0.9.12",
+ "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.9.12.tgz",
+ "integrity": "sha512-5AXjrcMClTryPe9LgZrygpB1lj7s0S9E0+W+AHaVKAVyHanafK86iPSvG5xHVSp/jC+VH1UXu0TAEmY279xH7A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.6"
+ }
+ },
+ "node_modules/acorn": {
+ "version": "8.18.0",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz",
+ "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "acorn": "bin/acorn"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/aria-query": {
+ "version": "5.3.1",
+ "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.1.tgz",
+ "integrity": "sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/assertion-error": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
+ "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/astral-regex": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz",
+ "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/at-least-node": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz",
+ "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">= 4.0.0"
+ }
+ },
+ "node_modules/axobject-query": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz",
+ "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/balanced-match": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
+ "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "18 || 20 || >=22"
+ }
+ },
+ "node_modules/base64-js": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
+ "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/big-integer": {
+ "version": "1.6.52",
+ "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz",
+ "integrity": "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==",
+ "dev": true,
+ "license": "Unlicense",
+ "engines": {
+ "node": ">=0.6"
+ }
+ },
+ "node_modules/bplist-creator": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/bplist-creator/-/bplist-creator-0.1.0.tgz",
+ "integrity": "sha512-sXaHZicyEEmY86WyueLTQesbeoH/mquvarJaQNbjuOQO+7gbFcDEWqKmcWA4cOTLzFlfgvkiVxolk1k5bBIpmg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "stream-buffers": "2.2.x"
+ }
+ },
+ "node_modules/bplist-parser": {
+ "version": "0.3.2",
+ "resolved": "https://registry.npmjs.org/bplist-parser/-/bplist-parser-0.3.2.tgz",
+ "integrity": "sha512-apC2+fspHGI3mMKj+dGevkGo/tCqVB8jMb6i+OX+E29p0Iposz07fABkRIfVUPNd5A5VbuOz1bZbnmkKLYF+wQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "big-integer": "1.6.x"
+ },
+ "engines": {
+ "node": ">= 5.10.0"
+ }
+ },
+ "node_modules/brace-expansion": {
+ "version": "5.0.12",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz",
+ "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^4.0.2"
+ },
+ "engines": {
+ "node": "20 || >=22"
+ }
+ },
+ "node_modules/buffer-crc32": {
+ "version": "0.2.13",
+ "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
+ "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/chai": {
+ "version": "6.3.0",
+ "resolved": "https://registry.npmjs.org/chai/-/chai-6.3.0.tgz",
+ "integrity": "sha512-XWAtwJ6OHO+tj0EKCs0Y2UamnyOxseZWltU4x2U2wh8g4AigdjwvtUjvLP2tqkA/avxHEtzxNaqGq/YGNwckKg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/chownr": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz",
+ "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/clsx": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
+ "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/commander": {
+ "version": "12.1.0",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz",
+ "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/convert-source-map": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
+ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/cross-spawn": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
+ "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "path-key": "^3.1.0",
+ "shebang-command": "^2.0.0",
+ "which": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/deepmerge": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
+ "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/define-lazy-prop": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz",
+ "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/detect-libc": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
+ "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/devalue": {
+ "version": "5.9.4",
+ "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.9.4.tgz",
+ "integrity": "sha512-sPAT4pztbu6586/hrhOnMKS17IJrvg12mXiSPSS3W5qDeN2RGgvZ0diZCm31dBbnevfVmujNO3IM2wrS4Y2Rhg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/elementtree": {
+ "version": "0.1.7",
+ "resolved": "https://registry.npmjs.org/elementtree/-/elementtree-0.1.7.tgz",
+ "integrity": "sha512-wkgGT6kugeQk/P6VZ/f4T+4HB41BVgNBq5CDIZVbQ02nvTVqAiVTbskxxu3eA/X96lMlfYOwnLQpN2v5E1zDEg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "sax": "1.1.4"
+ },
+ "engines": {
+ "node": ">= 0.4.0"
+ }
+ },
+ "node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/env-paths": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz",
+ "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/es-module-lexer": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.2.tgz",
+ "integrity": "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/esm-env": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz",
+ "integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/esrap": {
+ "version": "2.4.0",
+ "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.4.0.tgz",
+ "integrity": "sha512-CQPcneEHw/czj32uwQC52nYnax8H4SXt25FCM+LIwGZ49cMIxpOj3bP8wmzcI+Z0HIy5KkClaPhRLfCySzwHPA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.4.15"
+ },
+ "peerDependencies": {
+ "@typescript-eslint/types": "^8.2.0"
+ },
+ "peerDependenciesMeta": {
+ "@typescript-eslint/types": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/estree-walker": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
+ "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "^1.0.0"
+ }
+ },
+ "node_modules/expect-type": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz",
+ "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
+ "node_modules/fast-check": {
+ "version": "4.10.2",
+ "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-4.10.2.tgz",
+ "integrity": "sha512-iK2f+YrcmoeGqk6fA0ea2bptcu/itMIm4NfEozq6N25+aG6h7s5HZbB/k1aV7b5w5sFLMCbbtRUsTVR+BgC3xw==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://github.com/sponsors/dubzzz"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/fast-check"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "pure-rand": "^8.0.0"
+ },
+ "engines": {
+ "node": ">=12.17.0"
+ }
+ },
+ "node_modules/fd-slicer": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz",
+ "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "pend": "~1.2.0"
+ }
+ },
+ "node_modules/fdir": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/fflate": {
+ "version": "0.8.3",
+ "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.3.tgz",
+ "integrity": "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/flatted": {
+ "version": "3.4.4",
+ "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.4.tgz",
+ "integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/fs-extra": {
+ "version": "11.4.1",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.4.1.tgz",
+ "integrity": "sha512-KYAb4c9BJQI6QqGKthV68OHe0badztdXJWKo0WtBA9IuCFPTKvE5ZdUBglP833aMjhaSPNO4A5j/EkzZtGlKjA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "graceful-fs": "^4.2.0",
+ "jsonfile": "^6.0.1",
+ "universalify": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=14.14"
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/glob": {
+ "version": "13.0.6",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz",
+ "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "minimatch": "^10.2.2",
+ "minipass": "^7.1.3",
+ "path-scurry": "^2.0.2"
+ },
+ "engines": {
+ "node": "18 || 20 || >=22"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/graceful-fs": {
+ "version": "4.2.11",
+ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
+ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/ini": {
+ "version": "4.1.3",
+ "resolved": "https://registry.npmjs.org/ini/-/ini-4.1.3.tgz",
+ "integrity": "sha512-X7rqawQBvfdjS10YU1y1YVreA3SsLrW9dX2CewP2EbBJM4ypVNLDkO5y04gejPwKIY9lR+7r9gn3rFPt/kmWFg==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/is-docker": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz",
+ "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "is-docker": "cli.js"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-reference": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz",
+ "integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "^1.0.6"
+ }
+ },
+ "node_modules/is-wsl": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz",
+ "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-docker": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/isexe": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/jsonfile": {
+ "version": "6.2.1",
+ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz",
+ "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "universalify": "^2.0.0"
+ },
+ "optionalDependencies": {
+ "graceful-fs": "^4.1.6"
+ }
+ },
+ "node_modules/kleur": {
+ "version": "4.1.5",
+ "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz",
+ "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/lightningcss": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz",
+ "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==",
+ "dev": true,
+ "license": "MPL-2.0",
+ "dependencies": {
+ "detect-libc": "^2.0.3"
+ },
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ },
+ "optionalDependencies": {
+ "lightningcss-android-arm64": "1.33.0",
+ "lightningcss-darwin-arm64": "1.33.0",
+ "lightningcss-darwin-x64": "1.33.0",
+ "lightningcss-freebsd-x64": "1.33.0",
+ "lightningcss-linux-arm-gnueabihf": "1.33.0",
+ "lightningcss-linux-arm64-gnu": "1.33.0",
+ "lightningcss-linux-arm64-musl": "1.33.0",
+ "lightningcss-linux-x64-gnu": "1.33.0",
+ "lightningcss-linux-x64-musl": "1.33.0",
+ "lightningcss-win32-arm64-msvc": "1.33.0",
+ "lightningcss-win32-x64-msvc": "1.33.0"
+ }
+ },
+ "node_modules/lightningcss-android-arm64": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz",
+ "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-darwin-arm64": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz",
+ "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-darwin-x64": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz",
+ "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-freebsd-x64": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz",
+ "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm-gnueabihf": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz",
+ "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm64-gnu": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz",
+ "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm64-musl": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz",
+ "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-x64-gnu": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz",
+ "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-x64-musl": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz",
+ "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-win32-arm64-msvc": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz",
+ "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-win32-x64-msvc": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz",
+ "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/locate-character": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz",
+ "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/lru-cache": {
+ "version": "11.5.3",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.3.tgz",
+ "integrity": "sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "engines": {
+ "node": "20 || >=22"
+ }
+ },
+ "node_modules/magic-string": {
+ "version": "1.4.3",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.4.3.tgz",
+ "integrity": "sha512-z12OxmaPGE0F4xlpGdjuAUckipLpQlTAuAmyGhNoD7ODpYUzvDfvtxUbjM/jwznyP178oju/KDdyi220wNJ0RQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.6.0"
+ }
+ },
+ "node_modules/minimatch": {
+ "version": "10.2.6",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz",
+ "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "brace-expansion": "^5.0.8"
+ },
+ "engines": {
+ "node": "18 || 20 || >=22"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/minipass": {
+ "version": "7.1.3",
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz",
+ "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ }
+ },
+ "node_modules/minizlib": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz",
+ "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "minipass": "^7.1.2"
+ },
+ "engines": {
+ "node": ">= 18"
+ }
+ },
+ "node_modules/mrmime": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz",
+ "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.20",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.20.tgz",
+ "integrity": "sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/native-run": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/native-run/-/native-run-2.0.3.tgz",
+ "integrity": "sha512-U1PllBuzW5d1gfan+88L+Hky2eZx+9gv3Pf6rNBxKbORxi7boHzqiA6QFGSnqMem4j0A9tZ08NMIs5+0m/VS1Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@ionic/utils-fs": "^3.1.7",
+ "@ionic/utils-terminal": "^2.3.4",
+ "bplist-parser": "^0.3.2",
+ "debug": "^4.3.4",
+ "elementtree": "^0.1.7",
+ "ini": "^4.1.1",
+ "plist": "^3.1.0",
+ "split2": "^4.2.0",
+ "through2": "^4.0.2",
+ "tslib": "^2.6.2",
+ "yauzl": "^2.10.0"
+ },
+ "bin": {
+ "native-run": "bin/native-run"
+ },
+ "engines": {
+ "node": ">=16.0.0"
+ }
+ },
+ "node_modules/obug": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/obug/-/obug-2.2.1.tgz",
+ "integrity": "sha512-XrsrhT5sybtKI6wakr2SPOlGZWWYbUXZ7a0jT8/QOeAPau+1X/bSegNe5YR75oJmEZQbKningirmGOEJCIk61Q==",
+ "dev": true,
+ "funding": [
+ "https://github.com/sponsors/sxzz",
+ "https://opencollective.com/debug"
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.20.0"
+ }
+ },
+ "node_modules/open": {
+ "version": "8.4.2",
+ "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz",
+ "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "define-lazy-prop": "^2.0.0",
+ "is-docker": "^2.1.1",
+ "is-wsl": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/package-json-from-dist": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
+ "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
+ "dev": true,
+ "license": "BlueOak-1.0.0"
+ },
+ "node_modules/path-key": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-scurry": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz",
+ "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "lru-cache": "^11.0.0",
+ "minipass": "^7.1.2"
+ },
+ "engines": {
+ "node": "18 || 20 || >=22"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/pathe": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
+ "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/pend": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",
+ "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/picomatch": {
+ "version": "4.0.7",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz",
+ "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/playwright": {
+ "version": "1.63.0",
+ "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.63.0.tgz",
+ "integrity": "sha512-+7ziBLidS4NaNCdt57SUDT+wYmmd5fmiQejUic/kb+YsYSCPyOOE9sebzMjNmQrsnNpDJqd4WHvV/8lfKfUDUg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "playwright-core": "1.63.0"
+ },
+ "bin": {
+ "playwright": "cli.js"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/playwright-core": {
+ "version": "1.63.0",
+ "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.63.0.tgz",
+ "integrity": "sha512-rYCsBF/M5HjUch52bbtVONEFjv6Xu8sm8h72dNlR5bzIE1fvC/bxgspzkjSfU+MweEMmPM8KJebG6nnyxo5mCg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "playwright-core": "cli.js"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/plist": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/plist/-/plist-3.1.1.tgz",
+ "integrity": "sha512-ZIfcLJC+7E7FBFnDxm9MPmt7D+DidyQ26lewieO75AdhA2ayMtsJSES0iWzqJQbcVRSrTufQoy0DR94xHue0oA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@xmldom/xmldom": "^0.9.10",
+ "base64-js": "^1.5.1",
+ "xmlbuilder": "^15.1.1"
+ },
+ "engines": {
+ "node": ">=10.4.0"
+ }
+ },
+ "node_modules/pngjs": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-7.0.0.tgz",
+ "integrity": "sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.19.0"
+ }
+ },
+ "node_modules/postcss": {
+ "version": "8.5.29",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.29.tgz",
+ "integrity": "sha512-49cGhUbXj8Qenv0iTMxA1cFBzxXoctpC9Ujd77t1WcbJIr6nF/eI7g/8MgxrYldFRuAXvja7xQRwavoW7kgrxQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.19",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.2"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/prompts": {
+ "version": "2.4.2",
+ "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz",
+ "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "kleur": "^3.0.3",
+ "sisteransi": "^1.0.5"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/prompts/node_modules/kleur": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz",
+ "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/pure-rand": {
+ "version": "8.4.2",
+ "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-8.4.2.tgz",
+ "integrity": "sha512-vvuOGgcuPJAirlHvuQw1TrOiw7ptaIXXmIbNuiNOY6lNGJJH49PQ1Kj4nd783nPdQhQdicgOjVI2yI/9BD6/Ng==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://github.com/sponsors/dubzzz"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/fast-check"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/readable-stream": {
+ "version": "3.6.2",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
+ "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "inherits": "^2.0.3",
+ "string_decoder": "^1.1.1",
+ "util-deprecate": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/rimraf": {
+ "version": "6.1.3",
+ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.1.3.tgz",
+ "integrity": "sha512-LKg+Cr2ZF61fkcaK1UdkH2yEBBKnYjTyWzTJT6KNPcSPaiT7HSdhtMXQuN5wkTX0Xu72KQ1l8S42rlmexS2hSA==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "glob": "^13.0.3",
+ "package-json-from-dist": "^1.0.1"
+ },
+ "bin": {
+ "rimraf": "dist/esm/bin.mjs"
+ },
+ "engines": {
+ "node": "20 || >=22"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/rolldown": {
+ "version": "1.2.12",
+ "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.12.tgz",
+ "integrity": "sha512-8wafseiaG80xmXSfqidUNqZcylTlhmPZZt+za2m+js2sFZ8dTNlhIOV2WcbIPx2hgwPBJpEUGFAMZ9bgBBLTSQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@oxc-project/types": "=0.152.0",
+ "@rolldown/pluginutils": "^1.0.0"
+ },
+ "bin": {
+ "rolldown": "bin/cli.mjs"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "optionalDependencies": {
+ "@rolldown/binding-android-arm-eabi": "1.2.12",
+ "@rolldown/binding-android-arm64": "1.2.12",
+ "@rolldown/binding-darwin-arm64": "1.2.12",
+ "@rolldown/binding-darwin-x64": "1.2.12",
+ "@rolldown/binding-freebsd-x64": "1.2.12",
+ "@rolldown/binding-linux-arm-gnueabihf": "1.2.12",
+ "@rolldown/binding-linux-arm64-gnu": "1.2.12",
+ "@rolldown/binding-linux-arm64-musl": "1.2.12",
+ "@rolldown/binding-linux-ppc64-gnu": "1.2.12",
+ "@rolldown/binding-linux-s390x-gnu": "1.2.12",
+ "@rolldown/binding-linux-x64-gnu": "1.2.12",
+ "@rolldown/binding-linux-x64-musl": "1.2.12",
+ "@rolldown/binding-openharmony-arm64": "1.2.12",
+ "@rolldown/binding-win32-arm64-msvc": "1.2.12",
+ "@rolldown/binding-win32-x64-msvc": "1.2.12"
+ }
+ },
+ "node_modules/safe-buffer": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/sax": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/sax/-/sax-1.1.4.tgz",
+ "integrity": "sha512-5f3k2PbGGp+YtKJjOItpg3P99IMD84E4HOvcfleTb5joCHNXYLsR9yWFPOYGgaeMPDubQILTCMdsFb2OMeOjtg==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/semver": {
+ "version": "7.8.5",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
+ "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/shebang-command": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+ "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "shebang-regex": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/shebang-regex": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/signal-exit": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
+ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/simple-plist": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/simple-plist/-/simple-plist-1.3.1.tgz",
+ "integrity": "sha512-iMSw5i0XseMnrhtIzRb7XpQEXepa9xhWxGUojHBL43SIpQuDQkh3Wpy67ZbDzZVr6EKxvwVChnVpdl8hEVLDiw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "bplist-creator": "0.1.0",
+ "bplist-parser": "0.3.1",
+ "plist": "^3.0.5"
+ }
+ },
+ "node_modules/simple-plist/node_modules/bplist-parser": {
+ "version": "0.3.1",
+ "resolved": "https://registry.npmjs.org/bplist-parser/-/bplist-parser-0.3.1.tgz",
+ "integrity": "sha512-PyJxiNtA5T2PlLIeBot4lbp7rj4OadzjnMZD/G5zuBNt8ei/yCU7+wW0h2bag9vr8c+/WuRWmSxbqAl9hL1rBA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "big-integer": "1.6.x"
+ },
+ "engines": {
+ "node": ">= 5.10.0"
+ }
+ },
+ "node_modules/sirv": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz",
+ "integrity": "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@polka/url": "^1.0.0-next.24",
+ "mrmime": "^2.0.0",
+ "totalist": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/sisteransi": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz",
+ "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/slice-ansi": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz",
+ "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "astral-regex": "^2.0.0",
+ "is-fullwidth-code-point": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/slice-ansi?sponsor=1"
+ }
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz",
+ "integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/split2": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
+ "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">= 10.x"
+ }
+ },
+ "node_modules/std-env": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.3.0.tgz",
+ "integrity": "sha512-OtU/EgQ1kIm5KwqQpBC6ZEMXrZRui11w8zgfTWp8cdO9B8OaPsbA8bTHO2P+HNo1VlUTGMVBwPhydu6poeXiag==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/stream-buffers": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/stream-buffers/-/stream-buffers-2.2.0.tgz",
+ "integrity": "sha512-uyQK/mx5QjHun80FLJTfaWE7JtwfRMKBLkMne6udYOmvH0CawotVa7TfgYHzAnpphn4+TweIx1QKMnRIbipmUg==",
+ "dev": true,
+ "license": "Unlicense",
+ "engines": {
+ "node": ">= 0.10.0"
+ }
+ },
+ "node_modules/string_decoder": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
+ "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "safe-buffer": "~5.2.0"
+ }
+ },
+ "node_modules/string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/svelte": {
+ "version": "5.57.1",
+ "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.57.1.tgz",
+ "integrity": "sha512-Uqj49lWKB+iSSnneuwiYYJ7MZgkB+eXr0LXBhv4uDuAkXqnWmq65Sxflfvp0Lc6MdKjMUxGaeOKWJqz5SNiVIA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/remapping": "^2.3.5",
+ "@jridgewell/sourcemap-codec": "^1.6.0",
+ "@sveltejs/acorn-typescript": "^1.0.13",
+ "@types/estree": "^1.0.9",
+ "acorn": "^8.18.0",
+ "aria-query": "5.3.1",
+ "axobject-query": "^4.1.0",
+ "clsx": "^2.1.1",
+ "devalue": "^5.9.2",
+ "esm-env": "^1.2.1",
+ "esrap": "^2.3.6",
+ "is-reference": "^3.0.3",
+ "locate-character": "^3.0.0",
+ "magic-string": "^0.30.11",
+ "zimmerframe": "^1.1.2"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/svelte/node_modules/magic-string": {
+ "version": "0.30.21",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
+ "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.5"
+ }
+ },
+ "node_modules/tar": {
+ "version": "7.5.22",
+ "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.22.tgz",
+ "integrity": "sha512-MFO/QzvtAOmJbkhOaCTvbGcFN9L9b+JunIsDwaKljSOdcLMea3NJ1k9Usz/rjdfSXTq4dfzfeS7W4p4YOAAHeA==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "@isaacs/fs-minipass": "^4.0.0",
+ "chownr": "^3.0.0",
+ "minipass": "^7.1.2",
+ "minizlib": "^3.1.0",
+ "yallist": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/through2": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/through2/-/through2-4.0.2.tgz",
+ "integrity": "sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "readable-stream": "3"
+ }
+ },
+ "node_modules/tinybench": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-6.2.0.tgz",
+ "integrity": "sha512-78U2TlB2CnVenajOFzf3BKSm0J6oz5L0NV7g32LCPccvYc0lbWvys4d3uUUCS2B1N8PAf2+aekR8i1KbC3HO7Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/tinyexec": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.1.tgz",
+ "integrity": "sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tinyglobby": {
+ "version": "0.2.17",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
+ "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.4"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/SuperchupuDev"
+ }
+ },
+ "node_modules/tinyrainbow": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.2.0.tgz",
+ "integrity": "sha512-LgO3D9yZJjApUiuUfl9iFAwrtaX4+lok3wJIqttGoKCHlWUqHqbQpnxCf82L8FjgKsh4iGo78hqJwgL8F6To2A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/totalist": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz",
+ "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/tree-kill": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz",
+ "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "tree-kill": "cli.js"
+ }
+ },
+ "node_modules/tslib": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+ "dev": true,
+ "license": "0BSD"
+ },
+ "node_modules/undici-types": {
+ "version": "8.9.0",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.9.0.tgz",
+ "integrity": "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/universalify": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
+ "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 10.0.0"
+ }
+ },
+ "node_modules/untildify": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz",
+ "integrity": "sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/util-deprecate": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/uuid": {
+ "version": "7.0.3",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-7.0.3.tgz",
+ "integrity": "sha512-DPSke0pXhTZgoF/d+WSt2QaKMCFSfx7QegxEWT+JOuHF5aWrKEn0G+ztjuJg/gG8/ItK+rbPCD/yNv8yyih6Cg==",
+ "deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "uuid": "dist/bin/uuid"
+ }
+ },
+ "node_modules/vite": {
+ "version": "8.3.2",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-8.3.2.tgz",
+ "integrity": "sha512-SQr1x6W5vVSbROg7vsyXIaxK9b0G7zsT68acdWWRmnBUsgDieLCRG+Rep9WdZgcposvv/GSnr4GUUBqB3vXq6w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "lightningcss": "^1.33.0",
+ "picomatch": "^4.0.7",
+ "postcss": "^8.5.28",
+ "rolldown": "~1.2.11",
+ "tinyglobby": "^0.2.17"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^20.19.0 || >=22.12.0",
+ "@vitejs/devtools": "^0.7.1",
+ "esbuild": "^0.27.0 || ^0.28.0",
+ "jiti": ">=1.21.0",
+ "less": "^4.0.0",
+ "sass": "^1.70.0",
+ "sass-embedded": "^1.70.0",
+ "stylus": ">=0.54.8",
+ "sugarss": "^5.0.0",
+ "terser": "^5.16.0",
+ "tsx": "^4.8.1",
+ "yaml": "^2.4.2"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "@vitejs/devtools": {
+ "optional": true
+ },
+ "esbuild": {
+ "optional": true
+ },
+ "jiti": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ },
+ "tsx": {
+ "optional": true
+ },
+ "yaml": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vitefu": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.1.3.tgz",
+ "integrity": "sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg==",
+ "dev": true,
+ "license": "MIT",
+ "workspaces": [
+ "tests/deps/*",
+ "tests/projects/*",
+ "tests/projects/workspace/packages/*"
+ ],
+ "peerDependencies": {
+ "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "vite": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vitest": {
+ "version": "5.0.3",
+ "resolved": "https://registry.npmjs.org/vitest/-/vitest-5.0.3.tgz",
+ "integrity": "sha512-xMw97S3rjdtj5dkVat7jCsqWBpvchs3RlpQctUqwJD0KkERk40vz2fJ77lDwW/Vzh/pk18eItYAzkodhSes3jQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/chai": "^5.2.2",
+ "@vitest/mocker": "5.0.3",
+ "chai": "^6.2.2",
+ "es-module-lexer": "^2.3.2",
+ "expect-type": "^1.4.0",
+ "magic-string": "^1.2.3",
+ "obug": "^2.1.4",
+ "picomatch": "^4.0.7",
+ "std-env": "^4.2.0",
+ "tinybench": "^6.1.4",
+ "tinyexec": "^1.3.0",
+ "tinyglobby": "^0.2.17",
+ "why-is-node-running": "3.2.1"
+ },
+ "bin": {
+ "vitest": "vitest.mjs"
+ },
+ "engines": {
+ "node": "^22.12.0 || ^24.0.0 || >=26.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "@edge-runtime/vm": "*",
+ "@opentelemetry/api": "^1.9.0",
+ "@types/node": "^22.0.0 || >=24.0.0",
+ "@vitest/browser-playwright": "5.0.3",
+ "@vitest/browser-preview": "5.0.3",
+ "@vitest/browser-webdriverio": "^5.0.0-beta.5 || >=5.0.0",
+ "@vitest/coverage-istanbul": "5.0.3",
+ "@vitest/coverage-v8": "5.0.3",
+ "@vitest/ui": "5.0.3",
+ "happy-dom": "*",
+ "jsdom": "*",
+ "vite": "^6.4.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
+ },
+ "vite": {
+ "optional": false
+ }
+ }
+ },
+ "node_modules/which": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "node-which": "bin/node-which"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/why-is-node-running": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-3.2.1.tgz",
+ "integrity": "sha512-Tb2FUhB4vUsGQlfSquQLYkApkuPAFQXGFzxWKHHumVz2dK+X1RUm/HnID4+TfIGYJ1kTcwOaCk/buYCEJr6YjQ==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "why-is-node-running": "cli.js"
+ },
+ "engines": {
+ "node": ">=20.11"
+ }
+ },
+ "node_modules/wrap-ansi": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+ "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
+ "node_modules/ws": {
+ "version": "8.22.0",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.22.0.tgz",
+ "integrity": "sha512-Ydggc987+RO0AnWtZ/7Wq9FtNvcrL1b/RO0ud9mWjUPgDrsAAwQSF51sm2hm1XofbU/4jkpGEsLFsZZxU+1DOg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.0.0"
+ },
+ "peerDependencies": {
+ "bufferutil": "^4.0.1",
+ "utf-8-validate": ">=5.0.2"
+ },
+ "peerDependenciesMeta": {
+ "bufferutil": {
+ "optional": true
+ },
+ "utf-8-validate": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/xcode": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/xcode/-/xcode-3.0.1.tgz",
+ "integrity": "sha512-kCz5k7J7XbJtjABOvkc5lJmkiDh8VhjVCGNiqdKCscmVpdVUpEAyXv1xmCLkQJ5dsHqx3IPO4XW+NTDhU/fatA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "simple-plist": "^1.1.0",
+ "uuid": "^7.0.3"
+ },
+ "engines": {
+ "node": ">=10.0.0"
+ }
+ },
+ "node_modules/xml2js": {
+ "version": "0.6.2",
+ "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz",
+ "integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "sax": ">=0.6.0",
+ "xmlbuilder": "~11.0.0"
+ },
+ "engines": {
+ "node": ">=4.0.0"
+ }
+ },
+ "node_modules/xml2js/node_modules/xmlbuilder": {
+ "version": "11.0.1",
+ "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz",
+ "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/xmlbuilder": {
+ "version": "15.1.1",
+ "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz",
+ "integrity": "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8.0"
+ }
+ },
+ "node_modules/yallist": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz",
+ "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/yauzl": {
+ "version": "2.10.0",
+ "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz",
+ "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "buffer-crc32": "~0.2.3",
+ "fd-slicer": "~1.1.0"
+ }
+ },
+ "node_modules/zimmerframe": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.5.tgz",
+ "integrity": "sha512-msJxIvYDYcoNL+PJsu+7qmpDWsYmAxTY+2TNYXXF0hzBzBk0BMecOqDOG/EckUoKCuKwObfbugIl8QpqHDXeFA==",
+ "dev": true,
+ "license": "MIT"
+ }
+ }
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..860891a
--- /dev/null
+++ b/package.json
@@ -0,0 +1,32 @@
+{
+ "name": "gestion-table-tournante-libre",
+ "productName": "Gestion table tournante Libre",
+ "version": "2026.1005.1",
+ "description": "Placement tournant des participants d’un événement, hors ligne",
+ "license": "AGPL-3.0-or-later",
+ "author": "TechnoLibre (http://www.technolibre.ca)",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "dev": "vite --mode developpement",
+ "build": "vite build --mode developpement",
+ "build:livraison": "node scripts/version.js controler && vite build --mode livraison",
+ "build:documentation": "vite build --mode documentation",
+ "test": "vitest run --project node",
+ "test:surveille": "vitest --project node",
+ "test:long": "vitest run --project node-long",
+ "test:navigateur": "vitest run --project navigateur",
+ "test:tout": "vitest run"
+ },
+ "devDependencies": {
+ "@capacitor/cli": "^8.5.2",
+ "@capacitor/core": "^8.5.2",
+ "@sveltejs/vite-plugin-svelte": "^7.3.1",
+ "@vitest/browser-playwright": "^5.0.3",
+ "fast-check": "^4.10.2",
+ "playwright": "^1.63.0",
+ "svelte": "^5.57.1",
+ "vite": "^8.3.2",
+ "vitest": "^5.0.3"
+ }
+}
diff --git a/scripts/version.js b/scripts/version.js
new file mode 100644
index 0000000..1a12801
--- /dev/null
+++ b/scripts/version.js
@@ -0,0 +1,724 @@
+// © 2026 TechnoLibre (http://www.technolibre.ca)
+// License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
+
+// La version datée (§ 18). version.json en est l'unique source ; ce script en
+// dérive tout le reste et contrôle que rien n'en diverge.
+//
+// Une version s'écrit sous quatre formes :
+// affichée AAAA.MM.JJ.NN, quatre champs à longueur fixe, rang de 01 à 99 ;
+// son ordre alphabétique est l'ordre chronologique ;
+// technique AAAA.(MM×100+JJ).NN, trois entiers sans zéro de tête, la forme
+// qu'exigent package.json et electron-builder ;
+// Windows la forme technique suivie de « .0 » : les quatre champs de la
+// ressource de version de l'exécutable, de 16 bits chacun ;
+// nom gestion_table_tournante_libre_v___.exe, les
+// points devenus soulignés, seul point restant celui de
+// l'extension.
+// Le jour étant inférieur à 100, MM×100+JJ se redécompose d'une seule façon :
+// la dérivation est monotone et bijective.
+//
+// En ligne de commande, la racine du projet est le parent du répertoire du
+// script :
+// node scripts/version.js engendrer réécrit src/version.genere.js, le
+// champ version de package.json et les
+// deux champs de version du paquet
+// racine de package-lock.json ;
+// node scripts/version.js controler applique les huit points du § 18.5 à
+// la date civile locale de la machine ;
+// imprime les échecs et sort en code 1,
+// ou sort en code 0.
+// Ce script n'appartient ni au moteur ni au générateur de démonstrations, et
+// l'interdit de lecture d'horloge du § 14.7 ne le vise pas : dateCivileLocale,
+// qu'appelle la commande « controler », lit l'horloge de la machine.
+import { mkdirSync, readdirSync, readFileSync, realpathSync, writeFileSync } from 'node:fs';
+import { dirname, join } from 'node:path';
+import { fileURLToPath } from 'node:url';
+
+// Le nom du livrable (§ 18.2) ne s'écrit qu'ici : le produit, « _v », la
+// version dont les points deviennent des soulignés, l'extension. nomDuLivrable
+// sert à deriver pour une version et au gabarit pour les champs nommés ; le
+// point 7 compare chaque nom cité à ce calcul.
+const PRODUIT = 'gestion_table_tournante_libre';
+const PREFIXE_NOM = `${PRODUIT}_v`;
+const EXTENSION = '.exe';
+const nomDuLivrable = (version) => `${PREFIXE_NOM}${version.replaceAll('.', '_')}${EXTENSION}`;
+const GABARIT_NOM = nomDuLivrable('...');
+const UN_JOUR_MS = 86_400_000;
+
+const MOIS = [
+ 'janvier', 'février', 'mars', 'avril', 'mai', 'juin',
+ 'juillet', 'août', 'septembre', 'octobre', 'novembre', 'décembre',
+];
+
+// Forme affichée AAAA.MM.JJ.NN. Le rang s'y lit aussi sur trois chiffres ou
+// plus sans zéro de tête : la forme est alors reconnue, et le refus nomme le
+// plafond du rang. Un rang « 001 » reste une faute de forme.
+const FORME_AFFICHEE = /^(\d{4})\.(\d{2})\.(\d{2})\.(\d{2}|[1-9]\d{2,})$/;
+// Trois entiers sans zéro de tête ; MM×100+JJ s'écrit sur trois ou quatre
+// chiffres, le rang sur un ou deux.
+const FORME_TECHNIQUE = /^(\d{4})\.([1-9]\d{2,3})\.([1-9]\d?)$/;
+
+const deuxChiffres = (n) => String(n).padStart(2, '0');
+
+function estBissextile(annee) {
+ return (annee % 4 === 0 && annee % 100 !== 0) || annee % 400 === 0;
+}
+
+function joursDuMois(annee, mois) {
+ if (mois === 2) return estBissextile(annee) ? 29 : 28;
+ return mois === 4 || mois === 6 || mois === 9 || mois === 11 ? 30 : 31;
+}
+
+// Lève quand { annee, mois, jour } n'est pas une date du calendrier
+// grégorien dont l'année s'écrit sur quatre chiffres sans zéro de tête — la
+// forme technique écrit l'année en entier, et un zéro de tête s'y perdrait.
+// « contexte » ouvre le message.
+function exigerDate({ annee, mois, jour }, contexte) {
+ if (!Number.isInteger(annee) || annee < 1000 || annee > 9999) {
+ throw new RangeError(`${contexte} : année ${annee} hors de 1000 à 9999`);
+ }
+ if (!Number.isInteger(mois) || mois < 1 || mois > 12) {
+ throw new RangeError(`${contexte} : mois ${mois} hors de 1 à 12`);
+ }
+ const dernier = joursDuMois(annee, mois);
+ if (!Number.isInteger(jour) || jour < 1 || jour > dernier) {
+ throw new RangeError(
+ `${contexte} : jour ${jour} hors de 1 à ${dernier} en ${MOIS[mois - 1]} ${annee}`,
+ );
+ }
+}
+
+/**
+ * Formes dérivées d'une version affichée AAAA.MM.JJ.NN : { affichee,
+ * technique, windows, nomFichier, annee, mois, jour, rang }, les quatre
+ * derniers en entiers. Lève sur une forme non conforme, une date civile
+ * invalide, un rang hors de 1 à 99.
+ */
+export function deriver(versionAffichee) {
+ if (typeof versionAffichee !== 'string') {
+ throw new TypeError(`version attendue sous forme de chaîne, reçu ${typeof versionAffichee}`);
+ }
+ const contexte = `version « ${versionAffichee} »`;
+ const champs = FORME_AFFICHEE.exec(versionAffichee);
+ if (champs === null) {
+ throw new Error(
+ `${contexte} : forme attendue AAAA.MM.JJ.NN, quatre champs à longueur fixe (§ 18.1)`,
+ );
+ }
+ const [annee, mois, jour, rang] = champs.slice(1).map(Number);
+ exigerDate({ annee, mois, jour }, contexte);
+ if (rang > 99) throw new RangeError(`${contexte} : rang ${rang}, le rang plafonne à 99 (§ 18.1)`);
+ if (rang < 1) throw new RangeError(`${contexte} : rang 00 hors de 01 à 99`);
+ const technique = `${annee}.${mois * 100 + jour}.${rang}`;
+ return {
+ affichee: versionAffichee,
+ technique,
+ windows: `${technique}.0`,
+ nomFichier: nomDuLivrable(versionAffichee),
+ annee,
+ mois,
+ jour,
+ rang,
+ };
+}
+
+/**
+ * Version affichée d'une version technique AAAA.(MM×100+JJ).NN. Lève quand
+ * la chaîne ne désigne pas exactement une version : forme non conforme, zéro
+ * de tête, date civile invalide, rang hors de 1 à 99. Sur une chaîne
+ * acceptée, deriver(redecomposer(t)).technique === t.
+ */
+export function redecomposer(versionTechnique) {
+ if (typeof versionTechnique !== 'string') {
+ throw new TypeError(
+ `version technique attendue sous forme de chaîne, reçu ${typeof versionTechnique}`,
+ );
+ }
+ const contexte = `version technique « ${versionTechnique} »`;
+ const champs = FORME_TECHNIQUE.exec(versionTechnique);
+ if (champs === null) {
+ throw new Error(
+ `${contexte} : forme attendue AAAA.(MM×100+JJ).NN, trois entiers sans zéro de tête, rang de 1 à 99 (§ 18.1)`,
+ );
+ }
+ const [annee, moisJour, rang] = champs.slice(1).map(Number);
+ const mois = Math.floor(moisJour / 100);
+ const jour = moisJour % 100;
+ exigerDate({ annee, mois, jour }, contexte);
+ return `${annee}.${deuxChiffres(mois)}.${deuxChiffres(jour)}.${deuxChiffres(rang)}`;
+}
+
+/**
+ * Date en toutes lettres du titre d'une section du changelog : « 9 mars
+ * 2031 », « 1er janvier 2027 ». Lève sur une date hors du calendrier.
+ */
+export function dateEnLettres({ annee, mois, jour }) {
+ exigerDate({ annee, mois, jour }, 'date');
+ return `${jour === 1 ? '1er' : jour} ${MOIS[mois - 1]} ${annee}`;
+}
+
+// --- Le module engendré et les recopies ------------------------------------
+
+const MODULE = 'src/version.genere.js';
+const CONSEIL_ENGENDRER = 'lancer « node scripts/version.js engendrer »';
+
+const EN_TETE_LICENCE = [
+ '// © 2026 TechnoLibre (http://www.technolibre.ca)',
+ '// License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)',
+];
+
+/**
+ * Contenu exact de src/version.genere.js pour une version affichée :
+ * l'en-tête de licence, une ligne vide, puis la constante VERSION sous ses
+ * formes affichée et technique. Le module ne porte que la version ; la
+ * provenance de la construction vient du mode de Vite.
+ */
+export function engendrerModule(versionAffichee) {
+ const { affichee, technique } = deriver(versionAffichee);
+ return [
+ ...EN_TETE_LICENCE,
+ '',
+ '// Engendré par scripts/version.js depuis version.json — ne pas modifier.',
+ 'export const VERSION = Object.freeze({',
+ ` affichee: '${affichee}',`,
+ ` technique: '${technique}',`,
+ '});',
+ '',
+ ].join('\n');
+}
+
+// JSON écrit comme npm l'écrit : indentation de deux espaces, saut de ligne
+// final.
+const enJson = (valeur) => `${JSON.stringify(valeur, null, 2)}\n`;
+
+// Contenu d'un fichier relatif à la racine, ou null s'il n'existe pas.
+function lireTexte(racine, chemin) {
+ try {
+ return readFileSync(join(racine, chemin), 'utf8');
+ } catch (erreur) {
+ if (erreur.code === 'ENOENT') return null;
+ throw erreur;
+ }
+}
+
+// Valeur JSON d'un fichier relatif à la racine. Lève en nommant le fichier
+// quand il est illisible, et quand il manque, avec « raisonSiAbsent » pour
+// suite du message.
+function lireJson(racine, chemin, raisonSiAbsent) {
+ const texte = lireTexte(racine, chemin);
+ if (texte === null) throw new Error(`${chemin} absent : ${raisonSiAbsent}`);
+ try {
+ return JSON.parse(texte);
+ } catch (erreur) {
+ throw new Error(`${chemin} illisible : ${erreur.message}`);
+ }
+}
+
+// Version affichée que porte version.json, non vérifiée. Lève sur un
+// fichier absent, illisible ou sans champ « version » textuel.
+function lireVersion(racine) {
+ const donnees = lireJson(racine, 'version.json', "c'est l'unique source de la version (§ 18.3)");
+ if (typeof donnees?.version !== 'string') {
+ throw new Error('version.json : champ « version » absent ou non textuel');
+ }
+ return donnees.version;
+}
+
+// Formes dérivées de la version de version.json. Chaque refus nomme
+// version.json.
+function versionDeLaSource(racine) {
+ const affichee = lireVersion(racine);
+ try {
+ return deriver(affichee);
+ } catch (erreur) {
+ throw new Error(`version.json : ${erreur.message}`, { cause: erreur });
+ }
+}
+
+// Vrai pour un objet JSON, ni tableau ni null.
+const estObjet = (valeur) =>
+ typeof valeur === 'object' && valeur !== null && !Array.isArray(valeur);
+
+const RAISON_PAQUET = 'il porte la forme technique de la version (§ 18.3)';
+const RAISON_VERROU = `« npm install » le crée, puis ${CONSEIL_ENGENDRER}`;
+
+/**
+ * Réécrit, depuis version.json, le module engendré, le champ version de
+ * package.json et les champs version et packages[""].version de
+ * package-lock.json, en forme technique. Tout est lu et vérifié avant la
+ * première écriture : une version non conforme, un package.json ou un verrou
+ * absent font lever sans rien écrire. Un fichier déjà à jour n'est pas
+ * réécrit. Rend les chemins réécrits, relatifs à la racine.
+ */
+export function engendrer(racine) {
+ const { affichee, technique } = versionDeLaSource(racine);
+ const paquet = lireJson(racine, 'package.json', RAISON_PAQUET);
+ if (!estObjet(paquet)) throw new Error('package.json : un objet JSON est attendu');
+ const verrou = lireJson(racine, 'package-lock.json', RAISON_VERROU);
+ if (!estObjet(verrou) || !estObjet(verrou.packages) || !estObjet(verrou.packages[''])) {
+ throw new Error('package-lock.json : paquet racine packages[""] absent');
+ }
+ const cibles = [
+ [MODULE, engendrerModule(affichee)],
+ ['package.json', enJson({ ...paquet, version: technique })],
+ [
+ 'package-lock.json',
+ enJson({
+ ...verrou,
+ version: technique,
+ packages: { ...verrou.packages, '': { ...verrou.packages[''], version: technique } },
+ }),
+ ],
+ ];
+ const reecrits = [];
+ for (const [chemin, contenu] of cibles) {
+ if (lireTexte(racine, chemin) === contenu) continue;
+ mkdirSync(dirname(join(racine, chemin)), { recursive: true });
+ writeFileSync(join(racine, chemin), contenu);
+ reecrits.push(chemin);
+ }
+ return reecrits;
+}
+
+// --- Le contrôle du § 18.5 ------------------------------------------------
+
+// Les deux lignes obligatoires de la section de tête du changelog (§ 18.4) :
+// le libellé en gras, les deux-points, puis une réponse.
+const LIGNES_OBLIGATOIRES = [
+ ['**Version de format des fichiers**', /^\*\*Version de format des fichiers\*\*\s*:\s*\S/],
+ ['**En remplaçant**', /^\*\*En remplaçant\*\*\s*:\s*\S/],
+];
+
+// Périmètre du balayage du point 6, posé ici et non après coup : les fichiers
+// de la racine, et les arbres des sources, des scripts, de la coquille et des
+// épreuves. Seuls ces arbres sont parcourus : les dépendances et les sorties
+// de construction (www/, dist*/, android/) n'en font pas partie, et un
+// répertoire node_modules n'est jamais descendu. En sont exclus :
+// - version.json, package-lock.json, le module engendré et le changelog, où
+// la version se recopie par construction et que les points 1 à 3
+// contrôlent ;
+// - les documents (*.md), dont le point 7 contrôle les noms cités ;
+// - les épreuves de la dérivation, qui citent des versions par nécessité ;
+// - les données d'épreuve, sous test/fixtures/.
+// package.json reste balayé : la forme technique y est à sa place, la forme
+// affichée non.
+const ARBRES_BALAYES = ['src', 'scripts', 'electron', 'test'];
+const FICHIERS_EXCLUS = new Set([
+ 'version.json',
+ 'package-lock.json',
+ MODULE,
+ 'CHANGELOG.md',
+ 'scripts/version.test.js',
+]);
+const REPERTOIRES_EXCLUS = new Set(['test/fixtures']);
+const DOCUMENT = /\.md$/i;
+const ARBRE_DES_SOURCES = 'src/';
+
+// Une chaîne de chaque forme, bornée : ni chiffre ni point devant, aucun
+// chiffre derrière. La forme Windows contient la forme technique.
+const CHAINE_AFFICHEE = /(? deriver(chiffres.join('.')).nomFichier) === nom;
+}
+
+// Résultat de « calcul », ou null s'il lève.
+function essayer(calcul) {
+ try {
+ return calcul();
+ } catch {
+ return null;
+ }
+}
+
+// Ordre de deux versions dérivées, comparées champ par champ comme quatre
+// entiers : négatif quand a précède b.
+function comparerVersions(a, b) {
+ return a.annee - b.annee || a.mois - b.mois || a.jour - b.jour || a.rang - b.rang;
+}
+
+// Rang d'une date civile dans le calendrier, en jours : la différence de
+// deux rangs compte les jours qui séparent les deux dates.
+const numeroDeJour = ({ annee, mois, jour }) => Date.UTC(annee, mois - 1, jour) / UN_JOUR_MS;
+
+// { annee, mois, jour } d'une date civile AAAA-MM-JJ ; lève sur toute autre
+// forme et sur une date hors du calendrier.
+function lireDateCivile(dateCivile) {
+ const champs =
+ typeof dateCivile === 'string' ? /^(\d{4})-(\d{2})-(\d{2})$/.exec(dateCivile) : null;
+ if (champs === null) {
+ throw new TypeError(`dateCivile attendue sous la forme AAAA-MM-JJ, reçu ${String(dateCivile)}`);
+ }
+ const [annee, mois, jour] = champs.slice(1).map(Number);
+ exigerDate({ annee, mois, jour }, `dateCivile « ${dateCivile} »`);
+ return { annee, mois, jour };
+}
+
+// Sections du changelog : chaque titre « ## », avec son numéro de ligne, son
+// texte et les lignes de son corps. Un bloc de code, de la ligne qui l'ouvre
+// par ``` ou ~~~ à celle qui le ferme, n'apporte ni titre ni ligne de corps :
+// ce qu'il contient s'affiche comme du code, et une ligne obligatoire citée
+// là n'est pas écrite.
+function sectionsDuChangelog(texte) {
+ const sections = [];
+ let dansUnBloc = false;
+ texte.split(/\r?\n/).forEach((ligne, i) => {
+ if (/^\s*(```|~~~)/.test(ligne)) {
+ dansUnBloc = !dansUnBloc;
+ return;
+ }
+ if (dansUnBloc) return;
+ const titre = /^## (.*)$/.exec(ligne);
+ if (titre !== null) {
+ sections.push({ ligne: i + 1, titre: titre[1].trim(), corps: [] });
+ } else if (sections.length > 0) {
+ sections[sections.length - 1].corps.push(ligne);
+ }
+ });
+ return sections;
+}
+
+// Chemins relatifs, en « / », des fichiers d'un arbre, sans descendre dans
+// node_modules ni dans un répertoire exclu. Un arbre absent n'ajoute rien.
+function parcourir(racine, arbre, chemins) {
+ let entrees;
+ try {
+ entrees = readdirSync(join(racine, arbre), { withFileTypes: true });
+ } catch (erreur) {
+ if (erreur.code === 'ENOENT' || erreur.code === 'ENOTDIR') return;
+ throw erreur;
+ }
+ for (const entree of entrees) {
+ const chemin = `${arbre}/${entree.name}`;
+ if (entree.isDirectory()) {
+ if (entree.name !== 'node_modules' && !REPERTOIRES_EXCLUS.has(chemin)) {
+ parcourir(racine, chemin, chemins);
+ }
+ } else if (entree.isFile()) {
+ chemins.push(chemin);
+ }
+ }
+}
+
+// Fichiers de la racine, triés ; un répertoire ou un lien n'en est pas.
+function fichiersDeLaRacine(racine) {
+ return readdirSync(racine, { withFileTypes: true })
+ .filter((entree) => entree.isFile())
+ .map((entree) => entree.name)
+ .sort();
+}
+
+// Fichiers du périmètre du point 6, triés.
+function fichiersBalayes(racine) {
+ const chemins = fichiersDeLaRacine(racine);
+ for (const arbre of ARBRES_BALAYES) parcourir(racine, arbre, chemins);
+ return chemins.filter((chemin) => !FICHIERS_EXCLUS.has(chemin) && !DOCUMENT.test(chemin)).sort();
+}
+
+// Chaque occurrence du motif dans le texte : { ligne, chaine }.
+function occurrences(texte, motif) {
+ const trouvees = [];
+ texte.split(/\r?\n/).forEach((ligne, i) => {
+ for (const [chaine] of ligne.matchAll(motif)) trouvees.push({ ligne: i + 1, chaine });
+ });
+ return trouvees;
+}
+
+// Point 1 : package.json et les deux champs du verrou portent la forme
+// technique dérivée de version.json, et elle se redécompose en la version
+// affichée.
+function controlerRecopies(racine, { affichee, technique }, echec) {
+ try {
+ const porte = lireJson(racine, 'package.json', RAISON_PAQUET)?.version;
+ const retour = essayer(() => redecomposer(porte));
+ if (porte !== technique || retour !== affichee) {
+ const lue = retour === null ? '' : ` (soit ${retour})`;
+ echec(
+ 1,
+ `package.json porte la version ${JSON.stringify(porte)}${lue}, version.json donne ${affichee}, soit ${technique} : ${CONSEIL_ENGENDRER}`,
+ );
+ }
+ } catch (erreur) {
+ echec(1, erreur.message);
+ }
+ try {
+ const verrou = lireJson(racine, 'package-lock.json', RAISON_VERROU);
+ const champs = [
+ ['version', verrou?.version],
+ ['packages[""].version', verrou?.packages?.['']?.version],
+ ];
+ for (const [champ, porte] of champs) {
+ if (porte !== technique) {
+ echec(
+ 1,
+ `package-lock.json, champ « ${champ} » : ${JSON.stringify(porte)}, version.json donne ${technique} : ${CONSEIL_ENGENDRER}`,
+ );
+ }
+ }
+ } catch (erreur) {
+ echec(1, erreur.message);
+ }
+}
+
+// Point 2 : le module engendré reproduit, caractère pour caractère, ce que
+// le script engendre ; l'échec cite la première ligne qui diffère.
+function controlerModule(racine, { affichee }, echec) {
+ const present = lireTexte(racine, MODULE);
+ if (present === null) {
+ echec(2, `${MODULE} absent : ${CONSEIL_ENGENDRER}`);
+ return;
+ }
+ const attendu = engendrerModule(affichee);
+ if (present === attendu) return;
+ const lignesPresentes = present.split('\n');
+ const lignesAttendues = attendu.split('\n');
+ const longueur = Math.max(lignesPresentes.length, lignesAttendues.length);
+ let i = 0;
+ while (i < longueur && lignesPresentes[i] === lignesAttendues[i]) i += 1;
+ const citer = (ligne) => (ligne === undefined ? 'la fin du fichier' : `« ${ligne} »`);
+ echec(
+ 2,
+ `${MODULE}, ligne ${i + 1} : ${citer(lignesPresentes[i])} au lieu de ${citer(lignesAttendues[i])}, que le script engendre : ${CONSEIL_ENGENDRER}`,
+ );
+}
+
+// Point 3 : la section de tête porte la version courante, la date en lettres
+// que cette version engendre, et les deux lignes obligatoires. Sans version
+// valide, seules la forme du titre et les deux lignes se contrôlent.
+function controlerTete(sections, version, echec) {
+ const tete = sections[0];
+ if (tete === undefined) {
+ echec(3, 'CHANGELOG.md : aucune section « ## » ; la section de tête porte la version courante');
+ return;
+ }
+ const lieu = `CHANGELOG.md, ligne ${tete.ligne}`;
+ const titre = /^(\S+) — (.+)$/.exec(tete.titre);
+ if (titre === null) {
+ echec(3, `${lieu} : le titre « ## ${tete.titre} » n'a pas la forme « ## — »`);
+ } else if (version !== null) {
+ const [, porte, date] = titre;
+ const attendue = dateEnLettres(version);
+ if (porte !== version.affichee) {
+ echec(
+ 3,
+ `${lieu} : la section de tête porte ${porte}, version.json porte ${version.affichee} ; la section de cette version s'écrit en tête (§ 18.4)`,
+ );
+ } else if (date !== attendue) {
+ echec(3, `${lieu} : la date « ${date} » n'est pas « ${attendue} », que la version ${porte} engendre`);
+ }
+ }
+ for (const [libelle, motif] of LIGNES_OBLIGATOIRES) {
+ if (!tete.corps.some((ligne) => motif.test(ligne))) {
+ echec(3, `${lieu} : la ligne obligatoire « ${libelle} : … » manque à la section de tête (§ 18.4)`);
+ }
+ }
+}
+
+// Point 4 : chaque titre de section s'ouvre sur une version, aucune version
+// n'est portée deux fois, et chaque section porte une version strictement
+// antérieure à celle de la section qui la précède.
+function controlerOrdre(sections, echec) {
+ const vues = new Map();
+ let precedente = null;
+ for (const section of sections) {
+ const version = essayer(() => deriver(section.titre.split(/\s/)[0]));
+ if (version === null) {
+ echec(
+ 4,
+ `CHANGELOG.md, ligne ${section.ligne} : le titre « ## ${section.titre} » ne s'ouvre pas sur une version AAAA.MM.JJ.NN`,
+ );
+ continue;
+ }
+ const deja = vues.get(version.affichee);
+ if (deja !== undefined) {
+ echec(4, `CHANGELOG.md, lignes ${deja} et ${section.ligne} : deux sections portent la version ${version.affichee}`);
+ } else {
+ vues.set(version.affichee, section.ligne);
+ if (precedente !== null && comparerVersions(version, precedente.version) > 0) {
+ echec(
+ 4,
+ `CHANGELOG.md, ligne ${section.ligne} : la section ${version.affichee} suit la section ${precedente.version.affichee} (ligne ${precedente.ligne}) ; les versions vont en ordre strictement décroissant`,
+ );
+ }
+ }
+ precedente = { version, ligne: section.ligne };
+ }
+}
+
+// Point 5 : la date de la version ne dépasse pas de plus d'un jour la date
+// civile de la machine. La tolérance absorbe l'écart de fuseau.
+function controlerDate(version, civile, echec) {
+ const avance = numeroDeJour(version) - numeroDeJour(civile);
+ if (avance > 1) {
+ echec(
+ 5,
+ `version.json : la version ${version.affichee} est datée du ${dateEnLettres(version)}, ${avance} jours après la date civile de la machine, le ${dateEnLettres(civile)} ; la tolérance est d'un jour`,
+ );
+ }
+}
+
+// Point 6 : aucune chaîne de forme affichée hors de version.json, du module
+// engendré et du changelog ; aucune de forme technique hors de package.json
+// et du module engendré. Rend le nombre de fichiers de src/ examinés, que
+// contrôle le point 8. Un fichier binaire, qui porte un octet nul, n'est pas
+// examiné.
+function controlerFormes(racine, echec) {
+ let sourcesExaminees = 0;
+ for (const chemin of fichiersBalayes(racine)) {
+ const texte = lireTexte(racine, chemin);
+ if (texte === null || texte.includes('\u0000')) continue;
+ if (chemin.startsWith(ARBRE_DES_SOURCES)) sourcesExaminees += 1;
+ for (const { ligne, chaine } of occurrences(texte, CHAINE_AFFICHEE)) {
+ echec(
+ 6,
+ `${chemin}, ligne ${ligne} : « ${chaine} », forme affichée, hors de version.json, du module engendré et du changelog`,
+ );
+ }
+ if (chemin === 'package.json') continue;
+ for (const { ligne, chaine } of occurrences(texte, CHAINE_TECHNIQUE)) {
+ echec(6, `${chemin}, ligne ${ligne} : « ${chaine} », forme technique, hors de package.json et du module engendré`);
+ }
+ }
+ return sourcesExaminees;
+}
+
+// Point 7 : aucun document ne porte le littéral #_# ni ne cite un nom
+// d'exécutable hors du gabarit du § 18.2. Rend le nombre de documents
+// examinés, que contrôle le point 8.
+function controlerDocuments(racine, echec) {
+ const documents = fichiersDeLaRacine(racine).filter(
+ (nom) => DOCUMENTS_FIXES.includes(nom) || GUIDE.test(nom),
+ );
+ for (const document of documents) {
+ const texte = lireTexte(racine, document);
+ for (const { ligne, chaine } of occurrences(texte, LITTERAL_NON_SUBSTITUE)) {
+ echec(7, `${document}, ligne ${ligne} : littéral « ${chaine} », un gabarit non substitué`);
+ }
+ for (const { ligne, chaine } of occurrences(texte, NOM_CITE)) {
+ if (!nomConforme(chaine)) {
+ echec(7, `${document}, ligne ${ligne} : « ${chaine} » ne se conforme pas au gabarit ${GABARIT_NOM} (§ 18.2)`);
+ }
+ }
+ }
+ return documents.length;
+}
+
+/**
+ * Applique à l'arbre « racine » les huit points du § 18.5, à la date civile
+ * « dateCivile » (AAAA-MM-JJ) de la machine qui construit. Rend la liste des
+ * échecs, chacun préfixé du numéro de son point (« 1. … ») et nommant
+ * l'endroit qui diverge ; liste vide = conforme. Lève sur une dateCivile mal
+ * formée.
+ */
+export function controler(racine, { dateCivile }) {
+ const civile = lireDateCivile(dateCivile);
+ const echecs = [];
+ const echec = (point, texte) => echecs.push(`${point}. ${texte}`);
+
+ let version = null;
+ try {
+ version = versionDeLaSource(racine);
+ } catch (erreur) {
+ echec(1, erreur.message);
+ }
+ if (version !== null) {
+ controlerRecopies(racine, version, echec);
+ controlerModule(racine, version, echec);
+ }
+ const changelog = lireTexte(racine, 'CHANGELOG.md');
+ if (changelog === null) {
+ echec(3, "CHANGELOG.md absent : la section de la version courante s'écrit à la livraison (§ 18.4)");
+ } else {
+ const sections = sectionsDuChangelog(changelog);
+ controlerTete(sections, version, echec);
+ controlerOrdre(sections, echec);
+ }
+ if (version !== null) controlerDate(version, civile, echec);
+ const sourcesExaminees = controlerFormes(racine, echec);
+ const documentsExamines = controlerDocuments(racine, echec);
+ if (sourcesExaminees === 0) {
+ echec(8, `le balayage du point 6 n'examine aucun fichier sous ${ARBRE_DES_SOURCES} : zéro fichier examiné n'est pas zéro problème (§ 14.2)`);
+ }
+ if (documentsExamines === 0) {
+ echec(8, 'le balayage du point 7 n\'examine aucun document (README.md, CHANGELOG.md, GUIDE-*.md)');
+ }
+ return echecs;
+}
+
+/**
+ * Date civile locale d'un instant, sous la forme AAAA-MM-JJ ; par défaut,
+ * celle de la machine au moment de l'appel.
+ */
+export function dateCivileLocale(instant = new Date()) {
+ return `${instant.getFullYear()}-${deuxChiffres(instant.getMonth() + 1)}-${deuxChiffres(instant.getDate())}`;
+}
+
+// --- Ligne de commande ------------------------------------------------------
+
+const USAGE = 'usage : node scripts/version.js engendrer | controler';
+
+// Exécute une commande sur l'arbre « racine » et rend le code de sortie : 0
+// réussi, 1 échec, 2 commande inconnue.
+function executer(commande, racine) {
+ if (commande === 'engendrer') {
+ try {
+ const reecrits = engendrer(racine);
+ const version = lireVersion(racine);
+ console.log(
+ reecrits.length === 0
+ ? `Version ${version} : les recopies sont à jour.`
+ : `Version ${version} : réécrit ${reecrits.join(', ')}.`,
+ );
+ return 0;
+ } catch (erreur) {
+ console.error(`Version : ${erreur.message}`);
+ return 1;
+ }
+ }
+ if (commande === 'controler') {
+ const echecs = controler(racine, { dateCivile: dateCivileLocale() });
+ if (echecs.length === 0) {
+ console.log(`Version ${lireVersion(racine)} : conforme aux huit points du § 18.5.`);
+ return 0;
+ }
+ console.error(`Version : ${echecs.length} échec(s) au contrôle du § 18.5.`);
+ for (const echec of echecs) console.error(echec);
+ return 1;
+ }
+ console.error(USAGE);
+ return 2;
+}
+
+// Vrai quand ce fichier est le script que Node a lancé, et non un module
+// importé.
+function estLanceDirectement() {
+ if (process.argv[1] === undefined) return false;
+ try {
+ return realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url));
+ } catch {
+ return false;
+ }
+}
+
+if (estLanceDirectement()) {
+ process.exitCode = executer(process.argv[2], fileURLToPath(new URL('..', import.meta.url)));
+}
diff --git a/scripts/version.test.js b/scripts/version.test.js
new file mode 100644
index 0000000..1939bf2
--- /dev/null
+++ b/scripts/version.test.js
@@ -0,0 +1,858 @@
+// © 2026 TechnoLibre (http://www.technolibre.ca)
+// License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
+
+// Épreuves de la version datée (§ 18). Ce fichier cite des versions en clair
+// par nécessité : le balayage du point 6 du § 18.5 l'exclut, et lui seul.
+import assert from 'node:assert/strict';
+import { spawnSync } from 'node:child_process';
+import {
+ copyFileSync,
+ mkdirSync,
+ mkdtempSync,
+ readFileSync,
+ rmSync,
+ unlinkSync,
+ writeFileSync,
+} from 'node:fs';
+import { tmpdir } from 'node:os';
+import { dirname, join } from 'node:path';
+import { fileURLToPath } from 'node:url';
+import { describe, test } from '../test/lanceur.js';
+import {
+ controler,
+ dateCivileLocale,
+ dateEnLettres,
+ deriver,
+ engendrer,
+ engendrerModule,
+ redecomposer,
+} from './version.js';
+
+const RACINE = fileURLToPath(new URL('..', import.meta.url));
+const SCRIPT = fileURLToPath(new URL('./version.js', import.meta.url));
+
+const UN_JOUR_MS = 86_400_000;
+
+// Les jours d'une année, énumérés par le calendrier de Date.UTC : l'oracle ne
+// partage rien avec la règle des mois et des années bissextiles du module
+// éprouvé.
+function joursDe(annee) {
+ const jours = [];
+ for (
+ let instant = Date.UTC(annee, 0, 1);
+ new Date(instant).getUTCFullYear() === annee;
+ instant += UN_JOUR_MS
+ ) {
+ const date = new Date(instant);
+ jours.push({ annee, mois: date.getUTCMonth() + 1, jour: date.getUTCDate() });
+ }
+ return jours;
+}
+
+// Dernier jour du mois, lu dans le même calendrier : le jour 0 d'un mois est
+// la veille de son premier jour.
+function dernierJour(annee, mois) {
+ return new Date(Date.UTC(annee, mois, 0)).getUTCDate();
+}
+
+const deux = (n) => String(n).padStart(2, '0');
+const forme = ({ annee, mois, jour }, rang) =>
+ `${annee}.${deux(mois)}.${deux(jour)}.${deux(rang)}`;
+const champs = (technique) => technique.split('.').map(Number);
+
+// Vrai quand a précède strictement b, comparés champ à champ comme des
+// entiers.
+function precede(a, b) {
+ for (let i = 0; i < a.length; i += 1) {
+ if (a[i] !== b[i]) return a[i] < b[i];
+ }
+ return false;
+}
+
+const MOIS = [
+ 'janvier', 'février', 'mars', 'avril', 'mai', 'juin',
+ 'juillet', 'août', 'septembre', 'octobre', 'novembre', 'décembre',
+];
+
+describe('version : dérivation (§ 18.1, § 18.2)', () => {
+ test('une version affichée donne ses formes technique et Windows et le nom du livrable', () => {
+ assert.deepEqual(deriver('2026.10.05.01'), {
+ affichee: '2026.10.05.01',
+ technique: '2026.1005.1',
+ windows: '2026.1005.1.0',
+ nomFichier: 'gestion_table_tournante_libre_v2026_10_05_01.exe',
+ annee: 2026,
+ mois: 10,
+ jour: 5,
+ rang: 1,
+ });
+ });
+
+ test('le mois garde son zéro de tête dans la forme affichée et le perd dans la technique', () => {
+ assert.equal(deriver('2026.01.05.01').technique, '2026.105.1');
+ assert.equal(redecomposer('2026.105.1'), '2026.01.05.01');
+ });
+
+ test('la forme technique croît avec la date, champ à champ', () => {
+ const suite = ['2026.01.05.01', '2026.10.05.01', '2026.12.31.01', '2027.01.05.01'].map(
+ (version) => champs(deriver(version).technique),
+ );
+ assert.deepEqual(suite, [
+ [2026, 105, 1],
+ [2026, 1005, 1],
+ [2026, 1231, 1],
+ [2027, 105, 1],
+ ]);
+ for (let i = 1; i < suite.length; i += 1) {
+ assert.ok(precede(suite[i - 1], suite[i]), `${suite[i - 1]} < ${suite[i]}`);
+ }
+ assert.ok(
+ precede(champs(deriver('2026.10.05.01').technique), champs(deriver('2026.10.05.02').technique)),
+ );
+ });
+
+ test('aller-retour sur chaque jour de 2026 et de 2028, sur les 99 rangs', () => {
+ const jours = [...joursDe(2026), ...joursDe(2028)];
+ assert.equal(jours.length, 365 + 366);
+ const affichees = [];
+ const noms = [];
+ let veille = null;
+ jours.forEach((date, i) => {
+ const rang = 1 + (i % 99);
+ const version = forme(date, rang);
+ const derivee = deriver(version);
+ assert.equal(derivee.technique, `${date.annee}.${date.mois * 100 + date.jour}.${rang}`);
+ assert.equal(derivee.windows, `${derivee.technique}.0`);
+ assert.equal(
+ derivee.nomFichier,
+ `gestion_table_tournante_libre_v${version.replaceAll('.', '_')}.exe`,
+ );
+ assert.equal(redecomposer(derivee.technique), version);
+ // Chaque champ de la ressource de version Windows tient sur 16 bits.
+ for (const champ of champs(derivee.windows)) assert.ok(champ <= 0xffff, derivee.windows);
+ const jour = champs(derivee.technique).slice(0, 2);
+ if (veille !== null) assert.ok(precede(veille, jour), `${veille} < ${jour}`);
+ veille = jour;
+ affichees.push(forme(date, 1));
+ noms.push(deriver(forme(date, 1)).nomFichier);
+ });
+ // L'ordre alphabétique des formes affichées et des noms de fichiers est
+ // l'ordre chronologique.
+ assert.deepEqual([...affichees].sort(), affichees);
+ assert.deepEqual([...noms].sort(), noms);
+ });
+
+ test('le dernier jour de chaque mois passe, le lendemain est refusé', () => {
+ let mois = 0;
+ for (const annee of [2026, 2028]) {
+ for (let m = 1; m <= 12; m += 1) {
+ const dernier = dernierJour(annee, m);
+ assert.doesNotThrow(() => deriver(forme({ annee, mois: m, jour: dernier }, 1)));
+ assert.throws(() => deriver(forme({ annee, mois: m, jour: dernier + 1 }, 1)), /jour/);
+ assert.throws(() => deriver(forme({ annee, mois: m, jour: 0 }, 1)), /jour/);
+ mois += 1;
+ }
+ }
+ assert.equal(mois, 24);
+ assert.equal(dernierJour(2026, 2), 28);
+ assert.equal(dernierJour(2028, 2), 29);
+ });
+
+ test('refuse une forme non conforme, une date invalide, un rang hors de 01 à 99', () => {
+ const refus = [
+ ['2026.1.5.1', /forme/],
+ ['2026.13.01.01', /mois/],
+ ['2026.00.10.01', /mois/],
+ ['2026.02.30.01', /jour/],
+ ['2026.10.05.00', /rang/],
+ ['2026.10.05.100', /rang.*99/],
+ ['2026.10.05.001', /forme/], // zéro de tête : une faute de forme, pas de plafond
+ ['2026.13.45.100', /mois/], // la date se contrôle avant le plafond du rang
+ ['2026.10.05.1', /forme/],
+ ['2026.10.05', /forme/],
+ ['2026.10.05.01 ', /forme/],
+ ['v2026.10.05.01', /forme/],
+ ['2026-10-05-01', /forme/],
+ ['0999.10.05.01', /année/],
+ ];
+ for (const [version, motif] of refus) {
+ assert.throws(() => deriver(version), motif, version);
+ }
+ assert.throws(() => deriver(20261005), TypeError);
+ });
+
+ test('accepte le rang 99', () => {
+ assert.equal(deriver('2026.10.05.99').technique, '2026.1005.99');
+ assert.equal(redecomposer('2026.1005.99'), '2026.10.05.99');
+ });
+
+ test('redecomposer refuse ce qui ne désigne pas exactement une version', () => {
+ const refus = [
+ ['2026.0105.1', /forme/], // zéro de tête
+ ['2026.1005.01', /forme/], // zéro de tête
+ ['2026.1005.0', /forme/], // rang 0
+ ['2026.1005.100', /forme/], // rang au-delà de 99
+ ['2026.99.1', /forme/], // mois et jour sur deux chiffres
+ ['2026.1005', /forme/], // deux champs
+ ['2026.10.05.01', /forme/], // forme affichée
+ ['26.1005.1', /forme/], // année sur deux chiffres
+ ['2026.1305.1', /mois/], // mois 13
+ ['2026.230.1', /jour/], // 30 février
+ ['2026.1000.1', /jour/], // jour 0
+ ];
+ for (const [technique, motif] of refus) {
+ assert.throws(() => redecomposer(technique), motif, technique);
+ }
+ assert.throws(() => redecomposer(2026.1005), TypeError);
+ });
+});
+
+describe('version : date en lettres (§ 18.4)', () => {
+ test('écrit le jour, le mois en lettres et l’année ; le premier du mois est « 1er »', () => {
+ assert.equal(dateEnLettres({ annee: 2026, mois: 10, jour: 5 }), '5 octobre 2026');
+ assert.equal(dateEnLettres({ annee: 2027, mois: 1, jour: 1 }), '1er janvier 2027');
+ assert.equal(dateEnLettres({ annee: 2028, mois: 2, jour: 29 }), '29 février 2028');
+ });
+
+ test('nomme les douze mois', () => {
+ assert.deepEqual(
+ MOIS.map((_, i) => dateEnLettres({ annee: 2026, mois: i + 1, jour: 2 })),
+ MOIS.map((nom) => `2 ${nom} 2026`),
+ );
+ });
+
+ test('refuse une date hors du calendrier', () => {
+ assert.throws(() => dateEnLettres({ annee: 2026, mois: 2, jour: 29 }), /jour/);
+ assert.throws(() => dateEnLettres({ annee: 2026, mois: 13, jour: 1 }), /mois/);
+ });
+});
+
+// --- Arbres d'épreuve -------------------------------------------------------
+
+const VERSION = '2026.10.05.01';
+const TECHNIQUE = '2026.1005.1';
+const DATE = '2026-10-05';
+
+// Le module engendré pour VERSION, octet pour octet.
+const MODULE = [
+ '// © 2026 TechnoLibre (http://www.technolibre.ca)',
+ '// License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)',
+ '',
+ '// Engendré par scripts/version.js depuis version.json — ne pas modifier.',
+ 'export const VERSION = Object.freeze({',
+ " affichee: '2026.10.05.01',",
+ " technique: '2026.1005.1',",
+ '});',
+ '',
+].join('\n');
+
+const TITRE_CHANGELOG = '# Changelog — Gestion table tournante Libre';
+
+// Une section de changelog complète : titre, une rubrique, les deux lignes
+// obligatoires.
+const section = (titre) =>
+ [
+ `## ${titre}`,
+ '',
+ '**Ce qui change pour vous**',
+ '- Le bandeau affiche la version.',
+ '',
+ '**Version de format des fichiers** : inchangée.',
+ '',
+ '**En remplaçant** : rien à faire.',
+ '',
+ ].join('\n');
+
+const changelog = (...titres) => [TITRE_CHANGELOG, '', ...titres.map(section)].join('\n');
+
+const enJson = (valeur) => `${JSON.stringify(valeur, null, 2)}\n`;
+
+function ecrire(racine, chemin, contenu) {
+ const complet = join(racine, chemin);
+ mkdirSync(dirname(complet), { recursive: true });
+ writeFileSync(complet, contenu);
+}
+
+const lire = (racine, chemin) => readFileSync(join(racine, chemin), 'utf8');
+
+function modifierJson(racine, chemin, modification) {
+ const valeur = JSON.parse(lire(racine, chemin));
+ modification(valeur);
+ ecrire(racine, chemin, enJson(valeur));
+}
+
+// Arbre conforme à VERSION et à DATE : la source, ses trois recopies, un
+// changelog, un README qui cite le gabarit du nom, une source et une
+// configuration à la racine.
+function arbreConforme() {
+ const racine = mkdtempSync(join(tmpdir(), 'version-'));
+ ecrire(racine, 'version.json', `{ "version": "${VERSION}" }\n`);
+ ecrire(racine, 'package.json', enJson({ name: 'essai', version: TECHNIQUE, type: 'module' }));
+ ecrire(
+ racine,
+ 'package-lock.json',
+ enJson({
+ name: 'essai',
+ version: TECHNIQUE,
+ lockfileVersion: 3,
+ requires: true,
+ packages: { '': { name: 'essai', version: TECHNIQUE } },
+ }),
+ );
+ ecrire(racine, 'src/version.genere.js', MODULE);
+ ecrire(
+ racine,
+ 'src/main.js',
+ "import { VERSION } from './version.genere.js';\nexport const titre = VERSION.affichee;\n",
+ );
+ ecrire(racine, 'vite.config.js', 'export default {};\n');
+ ecrire(racine, 'CHANGELOG.md', changelog(`${VERSION} — 5 octobre 2026`));
+ ecrire(
+ racine,
+ 'README.md',
+ "# Essai\n\nL'exécutable se nomme `gestion_table_tournante_libre_v___.exe`.\n",
+ );
+ return racine;
+}
+
+// Exécute l'épreuve sur un arbre conforme neuf, supprimé ensuite.
+function surArbre(epreuve) {
+ const racine = arbreConforme();
+ try {
+ return epreuve(racine);
+ } finally {
+ rmSync(racine, { recursive: true, force: true });
+ }
+}
+
+// Numéros de point des échecs, sans doublon, croissants. Un échec sans
+// numéro fait échouer l'épreuve.
+function points(echecs) {
+ const numeros = echecs.map((echec) => {
+ const numero = /^([1-8])\. /.exec(echec);
+ assert.ok(numero, `échec sans numéro de point : ${echec}`);
+ return Number(numero[1]);
+ });
+ return [...new Set(numeros)].sort((a, b) => a - b);
+}
+
+// Contrôle l'arbre et exige que les échecs nomment exactement les points
+// attendus, et que chaque motif se lise dans l'un d'eux.
+function exigerEchecs(racine, attendus, motifs, dateCivile = DATE) {
+ const echecs = controler(racine, { dateCivile });
+ const liste = echecs.join('\n');
+ assert.deepEqual(points(echecs), attendus, liste);
+ for (const motif of motifs) {
+ assert.ok(
+ echecs.some((echec) => motif.test(echec)),
+ `${motif} ne se lit dans aucun échec :\n${liste}`,
+ );
+ }
+ return echecs;
+}
+
+describe('version : contrôle du § 18.5, un défaut à la fois', () => {
+ test("l'arbre de référence est conforme", () =>
+ surArbre((racine) => {
+ assert.deepEqual(controler(racine, { dateCivile: DATE }), []);
+ }));
+
+ test('point 1 : package.json porte une autre version technique', () =>
+ surArbre((racine) => {
+ modifierJson(racine, 'package.json', (paquet) => {
+ paquet.version = '2026.1006.1';
+ });
+ exigerEchecs(racine, [1], [/^1\. package\.json .*2026\.1006\.1.*2026\.1005\.1/]);
+ }));
+
+ test('point 1 : le champ version du verrou porte une autre version', () =>
+ surArbre((racine) => {
+ modifierJson(racine, 'package-lock.json', (verrou) => {
+ verrou.version = '2026.1004.1';
+ });
+ exigerEchecs(racine, [1], [/^1\. package-lock\.json, champ « version » .*2026\.1004\.1/]);
+ }));
+
+ test('point 1 : le champ packages[""].version du verrou porte une autre version', () =>
+ surArbre((racine) => {
+ modifierJson(racine, 'package-lock.json', (verrou) => {
+ verrou.packages[''].version = '2026.1004.1';
+ });
+ exigerEchecs(
+ racine,
+ [1],
+ [/^1\. package-lock\.json, champ « packages\[""\]\.version » .*2026\.1004\.1/],
+ );
+ }));
+
+ test('point 1 : le verrou manque', () =>
+ surArbre((racine) => {
+ unlinkSync(join(racine, 'package-lock.json'));
+ exigerEchecs(racine, [1], [/^1\. package-lock\.json absent/]);
+ }));
+
+ test('point 1 : version.json manque', () =>
+ surArbre((racine) => {
+ unlinkSync(join(racine, 'version.json'));
+ exigerEchecs(racine, [1], [/^1\. version\.json absent/]);
+ }));
+
+ test('point 1 : version.json porte une version non conforme', () =>
+ surArbre((racine) => {
+ ecrire(racine, 'version.json', '{ "version": "2026.10.05.100" }\n');
+ exigerEchecs(racine, [1], [/^1\. version\.json : .*rang 100.*99/]);
+ }));
+
+ test('point 2 : le module engendré est modifié à la main', () =>
+ surArbre((racine) => {
+ ecrire(racine, 'src/version.genere.js', MODULE.replace("'2026.1005.1'", "'2026.1005.2'"));
+ exigerEchecs(racine, [2], [/^2\. src\/version\.genere\.js, ligne 7 /]);
+ }));
+
+ test('point 2 : le module engendré manque', () =>
+ surArbre((racine) => {
+ unlinkSync(join(racine, 'src/version.genere.js'));
+ exigerEchecs(racine, [2], [/^2\. src\/version\.genere\.js absent/]);
+ }));
+
+ test('point 3 : le titre de tête ne porte pas la version courante', () =>
+ surArbre((racine) => {
+ ecrire(racine, 'CHANGELOG.md', changelog('2026.10.04.01 — 4 octobre 2026'));
+ exigerEchecs(racine, [3], [/^3\. CHANGELOG\.md, ligne 3 : .*2026\.10\.04\.01.*2026\.10\.05\.01/]);
+ }));
+
+ test('point 3 : la date en lettres du titre de tête est fausse', () =>
+ surArbre((racine) => {
+ ecrire(racine, 'CHANGELOG.md', changelog(`${VERSION} — 6 octobre 2026`));
+ exigerEchecs(racine, [3], [/^3\. CHANGELOG\.md, ligne 3 : .*6 octobre 2026.*5 octobre 2026/]);
+ }));
+
+ test('point 3 : la ligne de la version de format manque', () =>
+ surArbre((racine) => {
+ const texte = lire(racine, 'CHANGELOG.md');
+ ecrire(racine, 'CHANGELOG.md', texte.replace('**Version de format des fichiers** : inchangée.\n', ''));
+ exigerEchecs(racine, [3], [/^3\. CHANGELOG\.md.*Version de format des fichiers/]);
+ }));
+
+ test('point 3 : la ligne « En remplaçant » manque', () =>
+ surArbre((racine) => {
+ const texte = lire(racine, 'CHANGELOG.md');
+ ecrire(racine, 'CHANGELOG.md', texte.replace('**En remplaçant** : rien à faire.\n', ''));
+ exigerEchecs(racine, [3], [/^3\. CHANGELOG\.md.*En remplaçant/]);
+ }));
+
+ test('point 3 : le changelog manque', () =>
+ surArbre((racine) => {
+ unlinkSync(join(racine, 'CHANGELOG.md'));
+ exigerEchecs(racine, [3], [/^3\. CHANGELOG\.md absent/]);
+ }));
+
+ test('point 3 : une ligne obligatoire absente de la section de tête échoue, même portée par une section plus ancienne', () => {
+ const lignes = [
+ ['**Version de format des fichiers** : inchangée.\n', /^3\. CHANGELOG\.md, ligne 3 : .*Version de format des fichiers/],
+ ['**En remplaçant** : rien à faire.\n', /^3\. CHANGELOG\.md, ligne 3 : .*En remplaçant/],
+ ];
+ for (const [ligne, motif] of lignes) {
+ surArbre((racine) => {
+ const texte = changelog(`${VERSION} — 5 octobre 2026`, '2026.10.04.01 — 4 octobre 2026');
+ // replace ne retire que la première occurrence : celle de la section de tête.
+ ecrire(racine, 'CHANGELOG.md', texte.replace(ligne, ''));
+ exigerEchecs(racine, [3], [motif]);
+ });
+ }
+ });
+
+ test('point 3 : une ligne obligatoire citée dans un bloc de code ne compte pas', () =>
+ surArbre((racine) => {
+ const texte = lire(racine, 'CHANGELOG.md');
+ ecrire(
+ racine,
+ 'CHANGELOG.md',
+ texte.replace('**En remplaçant** : rien à faire.\n', '```\n**En remplaçant** : rien à faire.\n```\n'),
+ );
+ exigerEchecs(racine, [3], [/^3\. CHANGELOG\.md, ligne 3 : .*En remplaçant/]);
+ }));
+
+ test('points 3 et 4 : une section « À venir » en tête', () =>
+ surArbre((racine) => {
+ // La section « À venir » porte ses deux lignes obligatoires : seul son
+ // titre fait tomber le point 3.
+ ecrire(racine, 'CHANGELOG.md', changelog('À venir', `${VERSION} — 5 octobre 2026`));
+ exigerEchecs(racine, [3, 4], [
+ /^3\. CHANGELOG\.md, ligne 3 : le titre « ## À venir » n'a pas la forme/,
+ /^4\. CHANGELOG\.md, ligne 3 : le titre « ## À venir » ne s'ouvre pas sur une version/,
+ ]);
+ }));
+
+ test('point 4 : une section « À venir » entre deux versions', () =>
+ surArbre((racine) => {
+ ecrire(
+ racine,
+ 'CHANGELOG.md',
+ changelog(`${VERSION} — 5 octobre 2026`, 'À venir', '2026.10.04.01 — 4 octobre 2026'),
+ );
+ exigerEchecs(racine, [4], [
+ /^4\. CHANGELOG\.md, ligne 12 : le titre « ## À venir » ne s'ouvre pas sur une version/,
+ ]);
+ }));
+
+ test('points 3 et 4 : un titre « ## » cité dans un bloc de code ne découpe pas la section', () =>
+ surArbre((racine) => {
+ const texte = lire(racine, 'CHANGELOG.md');
+ ecrire(
+ racine,
+ 'CHANGELOG.md',
+ texte.replace(
+ '- Le bandeau affiche la version.\n',
+ '- Le bandeau affiche la version.\n\n```markdown\n## Notes de la soirée\n```\n',
+ ),
+ );
+ assert.deepEqual(controler(racine, { dateCivile: DATE }), []);
+ }));
+
+ test('points 3 et 4 : un sous-titre « ### » ne découpe pas la section', () =>
+ surArbre((racine) => {
+ const texte = lire(racine, 'CHANGELOG.md');
+ ecrire(
+ racine,
+ 'CHANGELOG.md',
+ texte.replace('**Ce qui change pour vous**\n', '**Ce qui change pour vous**\n\n### Le bandeau\n'),
+ );
+ assert.deepEqual(controler(racine, { dateCivile: DATE }), []);
+ }));
+
+ test('point 4 : deux sections portent la même version', () =>
+ surArbre((racine) => {
+ ecrire(
+ racine,
+ 'CHANGELOG.md',
+ changelog(`${VERSION} — 5 octobre 2026`, `${VERSION} — 5 octobre 2026`),
+ );
+ exigerEchecs(racine, [4], [/^4\. CHANGELOG\.md, lignes 3 et 12 : .*2026\.10\.05\.01/]);
+ }));
+
+ test('point 4 : une section plus récente suit une plus ancienne', () =>
+ surArbre((racine) => {
+ ecrire(
+ racine,
+ 'CHANGELOG.md',
+ changelog(`${VERSION} — 5 octobre 2026`, '2026.10.06.01 — 6 octobre 2026'),
+ );
+ exigerEchecs(racine, [4], [/^4\. CHANGELOG\.md, ligne 12 : .*2026\.10\.06\.01.*2026\.10\.05\.01/]);
+ }));
+
+ test('point 4 : des sections en ordre strictement décroissant sont admises', () =>
+ surArbre((racine) => {
+ ecrire(
+ racine,
+ 'CHANGELOG.md',
+ changelog(
+ `${VERSION} — 5 octobre 2026`,
+ '2026.10.04.02 — 4 octobre 2026',
+ '2026.10.04.01 — 4 octobre 2026',
+ '2025.12.31.01 — 31 décembre 2025',
+ ),
+ );
+ assert.deepEqual(controler(racine, { dateCivile: DATE }), []);
+ }));
+
+ test("point 5 : la version est datée de plus d'un jour après la date civile", () =>
+ surArbre((racine) => {
+ exigerEchecs(racine, [5], [/^5\. version\.json : .*5 octobre 2026.*3 octobre 2026/], '2026-10-03');
+ }));
+
+ test("point 5 : un jour d'avance sur la date civile est toléré", () =>
+ surArbre((racine) => {
+ assert.deepEqual(controler(racine, { dateCivile: '2026-10-04' }), []);
+ assert.deepEqual(controler(racine, { dateCivile: '2027-03-01' }), []);
+ }));
+
+ test('point 6 : une forme affichée dans src/ hors du module engendré', () =>
+ surArbre((racine) => {
+ ecrire(racine, 'src/interface/bandeau.js', "// Bandeau.\n\nexport const v = 'v2026.10.05.01';\n");
+ exigerEchecs(racine, [6], [/^6\. src\/interface\/bandeau\.js, ligne 3 : « 2026\.10\.05\.01 », forme affichée/]);
+ }));
+
+ test('point 6 : une forme technique hors de package.json et du module engendré', () =>
+ surArbre((racine) => {
+ ecrire(racine, 'vite.config.js', "export default { define: { V: '2026.1005.1' } };\n");
+ ecrire(racine, 'scripts/construire.sh', '#!/bin/sh\nWINDOWS=2026.1005.1.0\n');
+ exigerEchecs(racine, [6], [
+ /^6\. vite\.config\.js, ligne 1 : « 2026\.1005\.1 », forme technique/,
+ /^6\. scripts\/construire\.sh, ligne 2 : « 2026\.1005\.1 », forme technique/,
+ ]);
+ }));
+
+ test('point 6 : une forme affichée dans package.json', () =>
+ surArbre((racine) => {
+ modifierJson(racine, 'package.json', (paquet) => {
+ paquet.description = 'version 2026.10.05.01';
+ });
+ exigerEchecs(racine, [6], [/^6\. package\.json, ligne \d+ : « 2026\.10\.05\.01 », forme affichée/]);
+ }));
+
+ test('point 6 : les épreuves de test/ sont dans le périmètre', () =>
+ surArbre((racine) => {
+ ecrire(racine, 'test/bandeau.test.js', "export const attendu = '2026.10.05.01';\n");
+ exigerEchecs(racine, [6], [/^6\. test\/bandeau\.test\.js, ligne 1 /]);
+ }));
+
+ test('point 6 : la coquille electron/ est dans le périmètre', () =>
+ surArbre((racine) => {
+ ecrire(racine, 'electron/main.js', "const titre = 'Gestion table tournante Libre — 2026.10.05.01';\n");
+ exigerEchecs(racine, [6], [/^6\. electron\/main\.js, ligne 1 : « 2026\.10\.05\.01 », forme affichée/]);
+ }));
+
+ test('point 6 : une version ne se lit pas dans un nombre plus long qui la contient', () =>
+ surArbre((racine) => {
+ // Un chiffre ou un point devant, un chiffre derrière : chaque ligne
+ // prolonge une version d'un côté, sous l'une des deux formes.
+ ecrire(
+ racine,
+ 'src/donnees.js',
+ [
+ "export const a = '12026.10.05.01';",
+ "export const b = '1.2026.10.05.01';",
+ "export const c = '2026.10.05.012';",
+ "export const d = '12026.1005.1';",
+ "export const e = '1.2026.1005.1';",
+ "export const f = '2026.1005.123';",
+ '',
+ ].join('\n'),
+ );
+ assert.deepEqual(controler(racine, { dateCivile: DATE }), []);
+ }));
+
+ test("point 6 : un fichier binaire, qui porte un octet nul, n'est pas lu comme du texte", () =>
+ surArbre((racine) => {
+ ecrire(racine, 'src/interface/icone.png', `PNG\u0000${VERSION} ${TECHNIQUE}\n`);
+ assert.deepEqual(controler(racine, { dateCivile: DATE }), []);
+ }));
+
+ test('point 6 : données, guides, dépendances et sorties restent hors du périmètre', () =>
+ surArbre((racine) => {
+ const versions = `${VERSION} ${TECHNIQUE} ${TECHNIQUE}.0\n`;
+ for (const chemin of [
+ 'scripts/version.test.js',
+ 'test/fixtures/demonstration.json',
+ 'GUIDE-USAGE.md',
+ 'docs/notes.md',
+ 'node_modules/paquet/index.js',
+ 'src/node_modules/paquet/index.js',
+ 'www/assets/index.js',
+ 'dist/sortie.js',
+ 'dist-electron/main.js',
+ 'android/app/build.gradle',
+ ]) {
+ ecrire(racine, chemin, versions);
+ }
+ ecrire(racine, 'README.md', `${lire(racine, 'README.md')}\nVersion ${versions}`);
+ assert.deepEqual(controler(racine, { dateCivile: DATE }), []);
+ }));
+
+ test('point 7 : un document porte le littéral #_#', () =>
+ surArbre((racine) => {
+ ecrire(racine, 'README.md', `${lire(racine, 'README.md')}\nVersion : #_#\n`);
+ exigerEchecs(racine, [7], [/^7\. README\.md, ligne 5 : .*#_#/]);
+ }));
+
+ test("point 7 : un document cite un nom d'exécutable hors du gabarit", () =>
+ surArbre((racine) => {
+ const fautifs = [
+ 'gestion_table_tournante_libre_v2026.10.05.01.exe',
+ 'gestion_table_tournante_libre_v2026_10_05_1.exe',
+ 'gestion_table_tournante_libre_v2026_13_05_01.exe',
+ 'gestion_table_tournante_libre_v.exe',
+ 'gestion_table_tournante_libre.exe',
+ 'Gestion_Table_Tournante_Libre_v2026_10_05_01.EXE',
+ ];
+ ecrire(racine, 'GUIDE-WINDOWS.md', fautifs.map((nom) => `Lancer \`${nom}\`.\n`).join(''));
+ const echecs = exigerEchecs(
+ racine,
+ [7],
+ fautifs.map((nom, i) => new RegExp(`^7\\. GUIDE-WINDOWS\\.md, ligne ${i + 1} : « ${nom.replace(/[.<>]/g, '\\$&')} »`)),
+ );
+ assert.equal(echecs.length, fautifs.length, echecs.join('\n'));
+ }));
+
+ test('point 7 : le gabarit, un nom conforme et le préfixe seul sont admis', () =>
+ surArbre((racine) => {
+ ecrire(
+ racine,
+ 'GUIDE-WINDOWS.md',
+ [
+ 'Le fichier dont le nom commence par `gestion_table_tournante_libre_v`.',
+ 'Gabarit : `gestion_table_tournante_libre_v___.exe`.',
+ 'Par exemple gestion_table_tournante_libre_v2026_10_05_01.exe.',
+ '',
+ ].join('\n'),
+ );
+ assert.deepEqual(controler(racine, { dateCivile: DATE }), []);
+ }));
+
+ test('point 7 : le changelog est balayé', () =>
+ surArbre((racine) => {
+ const texte = lire(racine, 'CHANGELOG.md');
+ ecrire(
+ racine,
+ 'CHANGELOG.md',
+ texte.replace(
+ '- Le bandeau affiche la version.\n',
+ '- Le bandeau affiche la version.\n- Lancer `gestion_table_tournante_libre.exe`.\n- Version #_#.\n',
+ ),
+ );
+ const echecs = exigerEchecs(racine, [7], [
+ /^7\. CHANGELOG\.md, ligne 7 : « gestion_table_tournante_libre\.exe »/,
+ /^7\. CHANGELOG\.md, ligne 8 : littéral « #_# »/,
+ ]);
+ assert.equal(echecs.length, 2, echecs.join('\n'));
+ }));
+
+ test('point 8 : un balayage qui ne trouve aucune source échoue', () =>
+ surArbre((racine) => {
+ rmSync(join(racine, 'src'), { recursive: true });
+ // Sans src/, le module engendré manque aussi : le point 2 tombe avec.
+ exigerEchecs(racine, [2, 8], [/^8\. .*src\//]);
+ }));
+
+ test('point 8 : un src/ qui ne porte que le module engendré ne compte aucune source', () =>
+ surArbre((racine) => {
+ unlinkSync(join(racine, 'src/main.js'));
+ const echecs = exigerEchecs(racine, [8], [/^8\. le balayage du point 6 n'examine aucun fichier sous src\//]);
+ assert.equal(echecs.length, 1, echecs.join('\n'));
+ }));
+
+ test('point 8 : un balayage qui ne trouve aucun document échoue', () =>
+ surArbre((racine) => {
+ unlinkSync(join(racine, 'README.md'));
+ unlinkSync(join(racine, 'CHANGELOG.md'));
+ // Sans changelog, le point 3 tombe avec.
+ exigerEchecs(racine, [3, 8], [/^8\. le balayage du point 7 n'examine aucun document/]);
+ }));
+
+ test("modifier version.json seul fait tomber les points 1, 2 et 3, chacun nommant l'endroit resté en arrière", () =>
+ surArbre((racine) => {
+ ecrire(racine, 'version.json', '{ "version": "2026.10.05.02" }\n');
+ const echecs = exigerEchecs(racine, [1, 2, 3], [
+ /^1\. package\.json /,
+ /^1\. package-lock\.json, champ « version » /,
+ /^1\. package-lock\.json, champ « packages\[""\]\.version » /,
+ /^2\. src\/version\.genere\.js, ligne 6 /,
+ /^3\. CHANGELOG\.md, ligne 3 : /,
+ ]);
+ assert.equal(echecs.length, 5, echecs.join('\n'));
+ engendrer(racine);
+ exigerEchecs(racine, [3], [/^3\. CHANGELOG\.md, ligne 3 : /]);
+ const texte = lire(racine, 'CHANGELOG.md');
+ ecrire(
+ racine,
+ 'CHANGELOG.md',
+ texte.replace(`${TITRE_CHANGELOG}\n\n`, `${TITRE_CHANGELOG}\n\n${section('2026.10.05.02 — 5 octobre 2026')}\n`),
+ );
+ assert.deepEqual(controler(racine, { dateCivile: DATE }), []);
+ }));
+
+ test('refuse une date civile mal formée', () => {
+ assert.throws(() => controler(RACINE, { dateCivile: '2026-10-5' }), TypeError);
+ assert.throws(() => controler(RACINE, { dateCivile: '2026-02-30' }), /jour/);
+ });
+});
+
+describe('version : engendrement', () => {
+ test("engendrerModule rend le module, l'en-tête de licence en tête", () => {
+ assert.equal(engendrerModule(VERSION), MODULE);
+ });
+
+ test('engendrer réécrit le module, package.json et les deux champs du verrou', () =>
+ surArbre((racine) => {
+ ecrire(racine, 'version.json', '{ "version": "2026.10.06.02" }\n');
+ const paquet = JSON.parse(lire(racine, 'package.json'));
+ const verrou = JSON.parse(lire(racine, 'package-lock.json'));
+ const reecrits = engendrer(racine);
+ assert.deepEqual(reecrits, ['src/version.genere.js', 'package.json', 'package-lock.json']);
+ assert.equal(lire(racine, 'src/version.genere.js'), engendrerModule('2026.10.06.02'));
+ assert.equal(lire(racine, 'package.json'), enJson({ ...paquet, version: '2026.1006.2' }));
+ verrou.version = '2026.1006.2';
+ verrou.packages[''].version = '2026.1006.2';
+ assert.equal(lire(racine, 'package-lock.json'), enJson(verrou));
+ }));
+
+ test('sur un arbre à jour, engendrer ne réécrit rien', () =>
+ surArbre((racine) => {
+ const fichiers = ['src/version.genere.js', 'package.json', 'package-lock.json'];
+ const avant = fichiers.map((chemin) => lire(racine, chemin));
+ assert.deepEqual(engendrer(racine), []);
+ assert.deepEqual(fichiers.map((chemin) => lire(racine, chemin)), avant);
+ }));
+
+ test('engendrer lève quand le verrou manque, sans rien écrire', () =>
+ surArbre((racine) => {
+ ecrire(racine, 'version.json', '{ "version": "2026.10.06.02" }\n');
+ unlinkSync(join(racine, 'package-lock.json'));
+ assert.throws(() => engendrer(racine), /package-lock\.json absent.*npm install/);
+ assert.equal(lire(racine, 'src/version.genere.js'), MODULE);
+ assert.equal(JSON.parse(lire(racine, 'package.json')).version, TECHNIQUE);
+ }));
+
+ test('engendrer refuse le centième rang de la journée, sans rien écrire', () =>
+ surArbre((racine) => {
+ ecrire(racine, 'version.json', '{ "version": "2026.10.05.100" }\n');
+ assert.throws(() => engendrer(racine), /^Error: version\.json : .*99/);
+ assert.equal(lire(racine, 'src/version.genere.js'), MODULE);
+ }));
+});
+
+describe('version : date civile de la machine', () => {
+ test('dateCivileLocale écrit la date locale sous la forme AAAA-MM-JJ', () => {
+ assert.equal(dateCivileLocale(new Date(2026, 9, 5, 23, 59)), '2026-10-05');
+ assert.equal(dateCivileLocale(new Date(2027, 0, 1, 0, 0)), '2027-01-01');
+ });
+});
+
+describe('version : ligne de commande', () => {
+ // Le script copié dans l'arbre d'épreuve prend pour racine le parent de
+ // son répertoire, comme dans le projet.
+ function lancer(racine, ...arguments_) {
+ const script = join(racine, 'scripts', 'version.js');
+ mkdirSync(dirname(script), { recursive: true });
+ copyFileSync(SCRIPT, script);
+ return spawnSync(process.execPath, [script, ...arguments_], { encoding: 'utf8' });
+ }
+
+ test('controler sort en 0 sur un arbre conforme, en 1 en imprimant les échecs sinon', () =>
+ surArbre((racine) => {
+ const conforme = lancer(racine, 'controler');
+ assert.equal(conforme.status, 0, conforme.stderr);
+ modifierJson(racine, 'package.json', (paquet) => {
+ paquet.version = '2026.1006.1';
+ });
+ const fautif = lancer(racine, 'controler');
+ assert.equal(fautif.status, 1, fautif.stdout);
+ assert.match(fautif.stderr, /^1\. package\.json /m);
+ }));
+
+ test('engendrer réécrit les recopies, et sort en 1 quand le verrou manque', () =>
+ surArbre((racine) => {
+ ecrire(racine, 'version.json', '{ "version": "2026.10.05.02" }\n');
+ const reussi = lancer(racine, 'engendrer');
+ assert.equal(reussi.status, 0, reussi.stderr);
+ assert.equal(lire(racine, 'src/version.genere.js'), engendrerModule('2026.10.05.02'));
+ unlinkSync(join(racine, 'package-lock.json'));
+ const echoue = lancer(racine, 'engendrer');
+ assert.equal(echoue.status, 1, echoue.stdout);
+ assert.match(echoue.stderr, /package-lock\.json absent/);
+ }));
+
+ test("une commande inconnue sort en 2 et rappelle l'usage", () =>
+ surArbre((racine) => {
+ const inconnue = lancer(racine, 'publier');
+ assert.equal(inconnue.status, 2);
+ assert.match(inconnue.stderr, /engendrer.*controler/);
+ }));
+});
+
+describe('version : arbre réel', () => {
+ test("l'arbre réel du projet est conforme, à la date civile de la machine", () => {
+ assert.deepEqual(controler(RACINE, { dateCivile: dateCivileLocale() }), []);
+ });
+});
diff --git a/src/interface/App.navigateur.test.js b/src/interface/App.navigateur.test.js
new file mode 100644
index 0000000..0512019
--- /dev/null
+++ b/src/interface/App.navigateur.test.js
@@ -0,0 +1,242 @@
+// © 2026 TechnoLibre (http://www.technolibre.ca)
+// License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
+
+// Épreuve de rendu du bandeau d'application (§ 8.4, § 18.6), dans un vrai
+// moteur de rendu. Le test interroge le rendu, jamais la présence d'une
+// classe (§ 14.2) : la visibilité de chaque élément, la boîte où la mise en
+// page le pose et l'élément qu'atteint le centre de cette boîte, le style
+// calculé, le texte, le titre de la fenêtre. Il ne lit aucun pixel : le
+// contraste se calcule sur les couleurs calculées, qui ne sont les couleurs
+// peintes que sans opacité partielle, filtre ni mode de fusion — ce qu'une
+// épreuve vérifie dans l'ascendance de chaque texte (§ 14.5). Les épreuves de
+// visibilité, de contraste et d'opacité passent par les deux thèmes : le
+// protocole de débogage de Chromium (CDP) émule prefers-color-scheme.
+//
+// Le projet navigateur n'importe pas node:assert/strict : côté navigateur,
+// Vite le remplace par un module qui lève une erreur à tout accès. Il emploie
+// expect, et prend test et describe chez Vitest plutôt que dans
+// test/lanceur.js, dont le repli vers node:test ne vaut que pour la série node.
+import { afterEach, beforeEach, describe, expect, test } from 'vitest';
+import { cdp } from 'vitest/browser';
+import { flushSync, mount, unmount } from 'svelte';
+import App from './App.svelte';
+import { VERSION } from '../version.genere.js';
+
+const TRANSPARENT = 'rgba(0, 0, 0, 0)';
+const THEMES = ['light', 'dark'];
+
+// Composantes [r, g, b, a] d'une couleur calculée. Chromium la rend sous la
+// forme « rgb(r, g, b) » ou « rgba(r, g, b, a) » quand la source est une
+// couleur sRGB ; toute autre forme fait échouer l'épreuve plutôt que de la
+// laisser passer sans mesure.
+function composantes(couleur) {
+ const m = /^rgba?\((\d+), (\d+), (\d+)(?:, ([\d.]+))?\)$/.exec(couleur);
+ if (m === null) {
+ throw new Error(`couleur calculée illisible : ${couleur}`);
+ }
+ const alpha = m[4] === undefined ? 1 : Number(m[4]);
+ return [Number(m[1]), Number(m[2]), Number(m[3]), alpha];
+}
+
+// Luminance relative d'une couleur sRGB, au sens des WCAG.
+function luminance([r, g, b]) {
+ const lineaire = (c) => {
+ const s = c / 255;
+ return s <= 0.04045 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4;
+ };
+ return 0.2126 * lineaire(r) + 0.7152 * lineaire(g) + 0.0722 * lineaire(b);
+}
+
+function contraste(a, b) {
+ const la = luminance(a);
+ const lb = luminance(b);
+ return (Math.max(la, lb) + 0.05) / (Math.min(la, lb) + 0.05);
+}
+
+// L'élément, puis chacun de ses ancêtres jusqu'à la racine du document.
+function ascendance(element) {
+ const chaine = [];
+ for (let n = element; n !== null; n = n.parentElement) {
+ chaine.push(n);
+ }
+ return chaine;
+}
+
+// Fond peint sous un élément : le premier fond non transparent de son
+// ascendance, ou null.
+function fondPeint(element) {
+ for (const n of ascendance(element)) {
+ const fond = composantes(getComputedStyle(n).backgroundColor);
+ if (fond[3] !== 0) {
+ return fond;
+ }
+ }
+ return null;
+}
+
+// Éléments du sous-arbre qui portent eux-mêmes un nœud de texte non vide.
+function porteursDeTexte(racine) {
+ return [racine, ...racine.querySelectorAll('*')].filter((el) =>
+ [...el.childNodes].some(
+ (n) => n.nodeType === Node.TEXT_NODE && n.textContent.trim() !== '',
+ ),
+ );
+}
+
+// Nom lisible d'un élément dans un message d'échec.
+function designation(element) {
+ const classe = element.getAttribute('class');
+ return classe ? `${element.localName}.${classe}` : element.localName;
+}
+
+// Exécute mesure() sous le thème demandé et rend sa valeur. L'émulation est
+// levée ensuite, même quand mesure() échoue : le test suivant retrouve la
+// préférence propre du navigateur.
+async function sousTheme(theme, mesure) {
+ await cdp().send('Emulation.setEmulatedMedia', {
+ features: [{ name: 'prefers-color-scheme', value: theme }],
+ });
+ try {
+ expect(matchMedia('(prefers-color-scheme: dark)').matches, theme).toBe(
+ theme === 'dark',
+ );
+ return mesure();
+ } finally {
+ await cdp().send('Emulation.setEmulatedMedia', { features: [] });
+ }
+}
+
+let composant;
+
+beforeEach(() => {
+ composant = mount(App, { target: document.body });
+ flushSync();
+});
+
+afterEach(() => {
+ unmount(composant);
+});
+
+function bandeau() {
+ return document.body.querySelector('header.bandeau');
+}
+
+// Le bandeau, puis chacun des éléments qui y portent un texte.
+function bandeauEtTextes() {
+ return [...new Set([bandeau(), ...porteursDeTexte(bandeau())])];
+}
+
+describe("bandeau d'application", () => {
+ test('existe et porte la version affichée', () => {
+ expect(bandeau()).not.toBeNull();
+ expect(bandeau().textContent).toContain(VERSION.affichee);
+ });
+
+ test('le bandeau et chacun de ses textes sont visibles dans la fenêtre, dans les deux thèmes (§ 14.2, § 18.6)', async () => {
+ // Le texte, le titre et le style calculé se relisent sur un élément qui
+ // ne peint rien. L'épreuve interroge donc la mise en page : chaque élément
+ // est visible pour checkVisibility (display, visibility, opacité nulle,
+ // content-visibility), sa boîte est non vide et tient dans la fenêtre, et
+ // le point central de cette boîte atteint l'élément ou l'un de ses
+ // descendants — un élément recouvert, ou rogné par un ancêtre, laisse le
+ // point à un autre. elementFromPoint suit l'atteinte du pointeur : un
+ // élément en pointer-events: none n'y répond pas, et l'épreuve le refuse.
+ for (const theme of THEMES) {
+ await sousTheme(theme, () => {
+ const elements = bandeauEtTextes();
+ expect(elements.length).toBeGreaterThan(1);
+ for (const element of elements) {
+ const lieu = `${theme} : ${designation(element)}`;
+ expect(
+ element.checkVisibility({ opacityProperty: true, visibilityProperty: true }),
+ `${lieu} : checkVisibility`,
+ ).toBe(true);
+ const boite = element.getBoundingClientRect();
+ expect(boite.width, `${lieu} : largeur`).toBeGreaterThan(0);
+ expect(boite.height, `${lieu} : hauteur`).toBeGreaterThan(0);
+ expect(boite.left, `${lieu} : bord gauche`).toBeGreaterThanOrEqual(0);
+ expect(boite.top, `${lieu} : bord haut`).toBeGreaterThanOrEqual(0);
+ expect(boite.right, `${lieu} : bord droit`).toBeLessThanOrEqual(innerWidth);
+ expect(boite.bottom, `${lieu} : bord bas`).toBeLessThanOrEqual(innerHeight);
+ const atteint = document.elementFromPoint(
+ boite.left + boite.width / 2,
+ boite.top + boite.height / 2,
+ );
+ expect(
+ element.contains(atteint),
+ `${lieu} : le centre de sa boîte atteint ${atteint === null ? 'aucun élément' : designation(atteint)}`,
+ ).toBe(true);
+ }
+ });
+ }
+ });
+
+ test('porte la provenance de la construction', () => {
+ expect(import.meta.env.MODE).toBe('test');
+ expect(bandeau().textContent).toContain('épreuve');
+ });
+
+ test('le titre de la fenêtre porte le nom et la version affichée', () => {
+ expect(document.title).toBe(
+ `Gestion table tournante Libre — ${VERSION.affichee}`,
+ );
+ });
+
+ test('le texte et le fond du bandeau ont une couleur calculée', () => {
+ const style = getComputedStyle(bandeau());
+ expect(style.color).not.toBe(TRANSPARENT);
+ expect(style.backgroundColor).not.toBe(TRANSPARENT);
+ });
+
+ test('chaque texte du bandeau tient 4,5:1 sur un fond opaque, dans les deux thèmes (§ 14.5)', async () => {
+ const couleursParTheme = [];
+ for (const theme of THEMES) {
+ const couleurs = await sousTheme(theme, () => {
+ const textes = porteursDeTexte(bandeau());
+ expect(textes.length).toBeGreaterThan(0);
+ for (const element of textes) {
+ const lieu = `${theme} : ${designation(element)}`;
+ const texte = composantes(getComputedStyle(element).color);
+ const fond = fondPeint(element);
+ expect(fond, lieu).not.toBeNull();
+ expect(texte[3], lieu).toBe(1);
+ expect(fond[3], lieu).toBe(1);
+ expect(contraste(texte, fond), lieu).toBeGreaterThanOrEqual(4.5);
+ }
+ return [document.body, bandeau()].map((el) => {
+ const style = getComputedStyle(el);
+ return `${style.color} sur ${style.backgroundColor}`;
+ });
+ });
+ couleursParTheme.push(couleurs);
+ }
+ // Une émulation sans effet, ou un thème sombre qui ne redéfinit aucune
+ // couleur, ferait mesurer deux fois le thème clair.
+ expect(
+ couleursParTheme[1],
+ `mêmes couleurs dans les deux thèmes : ${couleursParTheme[0].join(' ; ')}`,
+ ).not.toEqual(couleursParTheme[0]);
+ });
+
+ test("aucune opacité partielle, aucun filtre ni mode de fusion sous un texte, dans les deux thèmes (§ 14.5)", async () => {
+ // opacity et filter ne s'héritent pas : posés sur un ancêtre, ils changent
+ // le pixel peint sans changer le style calculé du texte. Chaque niveau de
+ // l'ascendance est donc relu, jusqu'à la racine du document.
+ for (const theme of THEMES) {
+ await sousTheme(theme, () => {
+ const textes = porteursDeTexte(bandeau());
+ expect(textes.length).toBeGreaterThan(0);
+ for (const element of textes) {
+ for (const n of ascendance(element)) {
+ const style = getComputedStyle(n);
+ const lieu = `${theme} : ${designation(n)} sous ${designation(element)}`;
+ expect(style.opacity, lieu).toBe('1');
+ expect(style.filter, lieu).toBe('none');
+ expect(style.backdropFilter, lieu).toBe('none');
+ expect(style.mixBlendMode, lieu).toBe('normal');
+ }
+ }
+ });
+ }
+ });
+});
diff --git a/src/interface/App.svelte b/src/interface/App.svelte
new file mode 100644
index 0000000..2164260
--- /dev/null
+++ b/src/interface/App.svelte
@@ -0,0 +1,52 @@
+
+
+
+
+
+ {titre}
+
+
+
+ {titre}
+ {provenance}
+
+
+
diff --git a/src/interface/jetons.css b/src/interface/jetons.css
new file mode 100644
index 0000000..ee04242
--- /dev/null
+++ b/src/interface/jetons.css
@@ -0,0 +1,42 @@
+/* © 2026 TechnoLibre (http://www.technolibre.ca)
+ License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl) */
+
+/* Jetons de l'interface : couleurs, typographie, espacements, rayons. Le thème
+ suit prefers-color-scheme ; chaque thème redéfinit les mêmes jetons de
+ couleur, et les composants ne lisent que les jetons (§ 13.3).
+
+ Chaque couleur de texte tient au moins 4,5:1 sur le fond où elle est peinte,
+ et aucune n'emploie de canal alpha : la couleur relue dans le style calculé
+ est alors la couleur peinte (§ 14.5). */
+
+:root {
+ color-scheme: light;
+
+ --couleur-page-fond: #f6f7f9;
+ --couleur-page-texte: #1c2128;
+ --couleur-bandeau-fond: #1f3a5f;
+ --couleur-bandeau-texte: #ffffff;
+ --couleur-bandeau-filet: #8fb3d9;
+
+ --police-interface: system-ui, sans-serif;
+ --taille-texte-bandeau: 0.9375rem;
+
+ --espace-1: 0.25rem;
+ --espace-2: 0.5rem;
+ --espace-3: 0.75rem;
+ --espace-4: 1rem;
+
+ --rayon-1: 0.25rem;
+}
+
+@media (prefers-color-scheme: dark) {
+ :root {
+ color-scheme: dark;
+
+ --couleur-page-fond: #121519;
+ --couleur-page-texte: #e3e6ea;
+ --couleur-bandeau-fond: #1d2c3f;
+ --couleur-bandeau-texte: #f2f4f7;
+ --couleur-bandeau-filet: #5b7fa6;
+ }
+}
diff --git a/src/interface/libelles.js b/src/interface/libelles.js
new file mode 100644
index 0000000..5d5132c
--- /dev/null
+++ b/src/interface/libelles.js
@@ -0,0 +1,37 @@
+// © 2026 TechnoLibre (http://www.technolibre.ca)
+// License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
+
+// Table des chaînes visibles de l'interface. Un composant n'écrit aucune
+// chaîne visible en dur : il les lit ici, seul endroit qu'une traduction
+// remplace (§ 14.6).
+
+const NOM_APPLICATION = 'Gestion table tournante Libre';
+
+// Mode de construction de Vite → provenance affichée dans le bandeau
+// (§ 18.6). Une Map ne répond qu'aux clés qu'on y a posées : un mode nommé
+// comme une propriété héritée d'Object ne rend pas une fonction.
+const PROVENANCES = new Map([
+ ['developpement', 'développement'],
+ ['livraison', 'livraison'],
+ ['documentation', 'documentation'],
+ ['test', 'épreuve'],
+]);
+
+// Libellé de la provenance pour un mode de Vite. Un mode sans libellé, comme
+// celui d'un « vite build » lancé sans --mode, s'affiche sous son propre nom.
+export function libelleProvenance(mode) {
+ return PROVENANCES.get(mode) ?? mode;
+}
+
+// Vrai quand le mode a un libellé déclaré. Comparer libelleProvenance(mode) à
+// mode ne le dit pas : « livraison » et « documentation » ont un libellé
+// identique à leur nom.
+export function aUnLibelle(mode) {
+ return PROVENANCES.has(mode);
+}
+
+// Nom de l'application suivi de la version affichée : texte du bandeau et
+// titre de la fenêtre (§ 18.6).
+export function titreAvecVersion(version) {
+ return `${NOM_APPLICATION} — ${version}`;
+}
diff --git a/src/interface/libelles.test.js b/src/interface/libelles.test.js
new file mode 100644
index 0000000..86e0bd9
--- /dev/null
+++ b/src/interface/libelles.test.js
@@ -0,0 +1,50 @@
+// © 2026 TechnoLibre (http://www.technolibre.ca)
+// License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
+
+// Chaque mode que pose un script npm (« --mode … » dans package.json) a son
+// libellé de provenance, comme le mode test que pose Vitest ; un mode sans
+// libellé s'affiche sous son propre nom plutôt que de laisser le bandeau muet
+// sur la provenance.
+import assert from 'node:assert/strict';
+import { readFileSync } from 'node:fs';
+import { describe, test } from '../../test/lanceur.js';
+import { aUnLibelle, libelleProvenance, titreAvecVersion } from './libelles.js';
+
+// Modes nommés par les scripts de package.json, triés, sans doublon.
+function modesDesScripts() {
+ const paquet = JSON.parse(
+ readFileSync(new URL('../../package.json', import.meta.url), 'utf8'),
+ );
+ const modes = new Set();
+ for (const commande of Object.values(paquet.scripts)) {
+ for (const [, mode] of commande.matchAll(/--mode[= ](\S+)/g)) {
+ modes.add(mode);
+ }
+ }
+ return [...modes].sort();
+}
+
+describe('libellés', () => {
+ test('libelleProvenance nomme les quatre modes de construction', () => {
+ assert.equal(libelleProvenance('developpement'), 'développement');
+ assert.equal(libelleProvenance('livraison'), 'livraison');
+ assert.equal(libelleProvenance('documentation'), 'documentation');
+ assert.equal(libelleProvenance('test'), 'épreuve');
+ });
+
+ test('chaque mode que pose un script npm a un libellé de provenance', () => {
+ const modes = modesDesScripts();
+ assert.ok(modes.length > 0, 'aucun « --mode » dans les scripts de package.json');
+ assert.deepEqual(modes.filter((mode) => !aUnLibelle(mode)), []);
+ });
+
+ test('libelleProvenance rend un mode sans libellé sous son propre nom', () => {
+ assert.equal(libelleProvenance('production'), 'production');
+ assert.equal(libelleProvenance('constructor'), 'constructor');
+ assert.equal(aUnLibelle('constructor'), false);
+ });
+
+ test('titreAvecVersion joint le nom et la version par un tiret cadratin', () => {
+ assert.equal(titreAvecVersion('X'), 'Gestion table tournante Libre — X');
+ });
+});
diff --git a/src/main.js b/src/main.js
new file mode 100644
index 0000000..cdbaa06
--- /dev/null
+++ b/src/main.js
@@ -0,0 +1,8 @@
+// © 2026 TechnoLibre (http://www.technolibre.ca)
+// License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
+
+// Point d'entrée de la page : monte l'interface dans le corps du document.
+import { mount } from 'svelte';
+import App from './interface/App.svelte';
+
+mount(App, { target: document.body });
diff --git a/src/version.genere.js b/src/version.genere.js
new file mode 100644
index 0000000..438f81e
--- /dev/null
+++ b/src/version.genere.js
@@ -0,0 +1,8 @@
+// © 2026 TechnoLibre (http://www.technolibre.ca)
+// License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
+
+// Engendré par scripts/version.js depuis version.json — ne pas modifier.
+export const VERSION = Object.freeze({
+ affichee: '2026.10.05.01',
+ technique: '2026.1005.1',
+});
diff --git a/test/construction.long.test.js b/test/construction.long.test.js
new file mode 100644
index 0000000..d3c3a32
--- /dev/null
+++ b/test/construction.long.test.js
@@ -0,0 +1,109 @@
+// © 2026 TechnoLibre (http://www.technolibre.ca)
+// License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
+
+// Le paquet livré n'emporte rien de l'outillage d'épreuve (§ 14.9, § 14.15) :
+// ni fichier de test, ni le lanceur, ni donnée d'épreuve, ni paquet d'un outil
+// d'épreuve. Le test construit la page par l'API de Vite, avec la
+// configuration et le mode de la livraison, dans un répertoire temporaire ;
+// il relit les modules que contient le paquet, les noms d'origine de chaque
+// actif, puis le chemin et le contenu de chaque fichier écrit.
+import assert from 'node:assert/strict';
+import { mkdtempSync, readdirSync, readFileSync, rmSync } from 'node:fs';
+import { tmpdir } from 'node:os';
+import { join, relative, resolve, sep } from 'node:path';
+import { fileURLToPath } from 'node:url';
+import { build } from 'vite';
+import { describe, test } from './lanceur.js';
+
+const RACINE = fileURLToPath(new URL('..', import.meta.url));
+
+// Paquets qui ne servent qu'aux épreuves.
+const OUTILS_EPREUVE =
+ /[\\/]node_modules[\\/](?:vitest|@vitest|playwright|playwright-core|fast-check)[\\/]/;
+
+// Vrai quand un module vient de l'outillage d'épreuve : l'arbre test/
+// (lanceur, données d'épreuve), un fichier *.test.js rangé à côté de son
+// module, un paquet d'épreuve, ou un module de Node que Vite remplace pour le
+// navigateur, comme node:assert.
+function moduleDEpreuve(id) {
+ return (
+ id.startsWith(join(RACINE, 'test') + sep) ||
+ /\.test\.[cm]?js(?:\?|$)/.test(id) ||
+ OUTILS_EPREUVE.test(id) ||
+ id.includes('__vite-browser-external')
+ );
+}
+
+// Traces de l'outillage dans le chemin ou le contenu d'un fichier écrit. Les
+// motifs portent l'extension : le domaine emploie le mot « lanceur » (lanceur
+// portable, § 8.6), et un code minifié peut écrire « x.test.call ».
+const TRACES_EPREUVE = [/vitest/i, /node:assert/, /lanceur\.js/, /\.test\.[cm]?js/];
+
+// Construit la page comme la livraison : vite.config.js, mode livraison,
+// NODE_ENV=production. Vitest pose NODE_ENV=test, dont Vite déduirait une
+// construction de développement — composants compilés en mode dev, paquets
+// résolus sous la condition « development ».
+async function construireLivraison(sortie) {
+ const nodeEnv = process.env.NODE_ENV;
+ process.env.NODE_ENV = 'production';
+ try {
+ return await build({
+ root: RACINE,
+ configFile: join(RACINE, 'vite.config.js'),
+ mode: 'livraison',
+ logLevel: 'error',
+ // Sans incrustation, chaque actif sort en fichier et garde ses noms
+ // d'origine ; incrusté en data:, il ne laisserait aucune trace lisible.
+ build: { outDir: sortie, emptyOutDir: true, assetsInlineLimit: 0 },
+ });
+ } finally {
+ if (nodeEnv === undefined) delete process.env.NODE_ENV;
+ else process.env.NODE_ENV = nodeEnv;
+ }
+}
+
+describe('construction', () => {
+ test("le paquet livré n'emporte aucun fichier ni module d'épreuve (§ 14.9)", async () => {
+ const sortie = mkdtempSync(join(tmpdir(), 'construction-'));
+ try {
+ const resultat = await construireLivraison(sortie);
+ const sorties = (Array.isArray(resultat) ? resultat : [resultat]).flatMap(
+ (r) => r.output,
+ );
+ const modules = sorties
+ .filter((s) => s.type === 'chunk')
+ .flatMap((s) => s.moduleIds);
+ assert.ok(
+ modules.some((id) => id.startsWith(join(RACINE, 'src') + sep)),
+ `aucun module de src/ parmi ${modules.length} modules examinés`,
+ );
+ assert.deepEqual(modules.filter(moduleDEpreuve), []);
+
+ // Un actif n'est pas un module : moduleIds ne le voit pas. Une donnée
+ // jointe par new URL(…, import.meta.url) n'apparaît que là.
+ const actifs = sorties
+ .filter((s) => s.type === 'asset')
+ .flatMap((s) => (s.originalFileNames ?? []).map((n) => resolve(RACINE, n)));
+ assert.ok(actifs.length > 0, `aucun actif d'origine connue parmi ${sorties.length} sorties`);
+ assert.deepEqual(actifs.filter(moduleDEpreuve), []);
+
+ const fichiers = readdirSync(sortie, { recursive: true, withFileTypes: true })
+ .filter((e) => e.isFile())
+ .map((e) => join(e.parentPath, e.name));
+ assert.ok(
+ fichiers.some((f) => f.endsWith('.js')),
+ `aucun script parmi ${fichiers.length} fichiers écrits`,
+ );
+ const traces = fichiers.flatMap((f) => {
+ const nom = relative(sortie, f);
+ const contenu = readFileSync(f, 'utf8');
+ return TRACES_EPREUVE.filter((m) => m.test(nom) || m.test(contenu)).map(
+ (m) => `${nom} : ${m}`,
+ );
+ });
+ assert.deepEqual(traces, []);
+ } finally {
+ rmSync(sortie, { recursive: true, force: true });
+ }
+ });
+});
diff --git a/test/lanceur.js b/test/lanceur.js
new file mode 100644
index 0000000..1e92ba5
--- /dev/null
+++ b/test/lanceur.js
@@ -0,0 +1,7 @@
+// © 2026 TechnoLibre (http://www.technolibre.ca)
+// License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
+
+// Point d'import unique des primitives de test. Remplacer cette ligne par
+// « export { test, describe } from 'node:test'; » fait tourner la série node
+// sous « node --test » sans toucher aux fichiers de test (§ 14.8).
+export { test, describe } from 'vitest';
diff --git a/test/licence.test.js b/test/licence.test.js
new file mode 100644
index 0000000..1e305bc
--- /dev/null
+++ b/test/licence.test.js
@@ -0,0 +1,146 @@
+// © 2026 TechnoLibre (http://www.technolibre.ca)
+// License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
+
+// Chaque fichier source du projet s'ouvre sur l'en-tête de licence : la ligne
+// du titulaire, puis celle de la licence, après un éventuel shebang ou la
+// déclaration . Les deux lignes sont des commentaires du
+// langage du fichier ; le texte ne varie pas. package.json déclare la même
+// licence et le même titulaire, que reprennent les outils d'emballage.
+import assert from 'node:assert/strict';
+import { readdirSync, readFileSync } from 'node:fs';
+import { extname, join, relative, sep } from 'node:path';
+import { fileURLToPath } from 'node:url';
+import { describe, test } from './lanceur.js';
+
+const RACINE = fileURLToPath(new URL('..', import.meta.url));
+
+const TITULAIRE = /^© \d{4} TechnoLibre \(http:\/\/www\.technolibre\.ca\)$/;
+const LICENCE = 'License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)';
+
+// Marqueurs de commentaire, par extension de fichier source. Un commentaire
+// de ligne n'a pas de fermant.
+const COMMENTAIRE = new Map([
+ ['.js', { ouvrant: '//', fermant: null }],
+ ['.cjs', { ouvrant: '//', fermant: null }],
+ ['.mjs', { ouvrant: '//', fermant: null }],
+ ['.sh', { ouvrant: '#', fermant: null }],
+ ['.svelte', { ouvrant: '' }],
+ ['.html', { ouvrant: '' }],
+ ['.css', { ouvrant: '/*', fermant: '*/' }],
+]);
+
+// Périmètre : les fichiers de la racine et les arbres des sources, des
+// épreuves, des scripts et de la coquille. Les dépendances et les sorties de
+// construction n'y entrent pas, ni les données d'épreuve, qui sont des
+// données et non des sources. Le séparateur final borne l'exclusion au
+// répertoire test/fixtures/ : un module test/fixtures.js reste une source.
+const ARBRES = ['src', 'test', 'scripts', 'electron'];
+const DONNEES_EPREUVE = join(RACINE, 'test', 'fixtures') + sep;
+
+function fichiersSources() {
+ const chemins = readdirSync(RACINE, { withFileTypes: true })
+ .filter((e) => e.isFile())
+ .map((e) => join(RACINE, e.name));
+ for (const arbre of ARBRES) {
+ const dossier = join(RACINE, arbre);
+ let entrees;
+ try {
+ entrees = readdirSync(dossier, { withFileTypes: true, recursive: true });
+ } catch (erreur) {
+ if (erreur.code === 'ENOENT') continue;
+ throw erreur;
+ }
+ for (const e of entrees) {
+ if (e.isFile()) chemins.push(join(e.parentPath, e.name));
+ }
+ }
+ return chemins
+ .filter((chemin) => COMMENTAIRE.has(extname(chemin)))
+ .filter((chemin) => !chemin.startsWith(DONNEES_EPREUVE))
+ .sort();
+}
+
+// Vrai quand les deux lignes sont des commentaires, et que le commentaire ne
+// se prolonge pas au-delà de la seconde. Un commentaire de ligne ouvre
+// chacune d'elles. Un commentaire de bloc court sur les deux — ouvert sur la
+// première sans s'y fermer, fermé en fin de seconde — ou s'ouvre et se ferme
+// sur chacune. Une seconde ligne hors commentaire se lit comme du code : un
+// texte affiché avant le bandeau dans un composant, une règle de style
+// invalidée dans une feuille, une commande dans un script.
+function enCommentaire(premiere, seconde, { ouvrant, fermant }) {
+ const suite = seconde.trim();
+ if (!premiere.startsWith(ouvrant)) return false;
+ if (fermant === null) return suite.startsWith(ouvrant);
+ const courant = !premiere.includes(fermant) && suite.endsWith(fermant);
+ const repete =
+ premiere.trimEnd().endsWith(fermant) &&
+ suite.startsWith(ouvrant) &&
+ suite.endsWith(fermant);
+ return courant || repete;
+}
+
+// Vrai quand le contenu s'ouvre sur l'en-tête, après un éventuel shebang ou
+// la déclaration : deux lignes de commentaire du langage de
+// l'extension, dont le texte, débarrassé des marqueurs, est celui de
+// l'en-tête.
+function porteLEnTete(contenu, extension) {
+ let lignes = contenu.split('\n');
+ if (lignes[0].startsWith('#!')) lignes = lignes.slice(1);
+ if (/^$/i.test(lignes[0])) lignes = lignes.slice(1);
+ const [premiere = '', seconde = ''] = lignes;
+ const texte = (ligne) =>
+ ligne
+ .replace(/^\s*(?:\/\/|#||\*\/)?\s*$/, '');
+ return (
+ enCommentaire(premiere, seconde, COMMENTAIRE.get(extension)) &&
+ TITULAIRE.test(texte(premiere)) &&
+ texte(seconde) === LICENCE
+ );
+}
+
+describe('licence', () => {
+ test("chaque fichier source s'ouvre sur l'en-tête de licence", () => {
+ const fichiers = fichiersSources();
+ assert.ok(fichiers.length > 0, 'aucun fichier source examiné');
+ const sansEnTete = fichiers
+ .filter((chemin) => !porteLEnTete(readFileSync(chemin, 'utf8'), extname(chemin)))
+ .map((chemin) => relative(RACINE, chemin));
+ assert.deepEqual(sansEnTete, []);
+ });
+
+ test("l'en-tête tient en deux lignes de commentaire du langage du fichier", () => {
+ const titulaire = '© 2026 TechnoLibre (http://www.technolibre.ca)';
+ // [extension, contenu, admis] : les formes admises ; puis des secondes
+ // lignes laissées hors commentaire, que le langage lirait comme du code,
+ // et un commentaire qui se prolonge au-delà de la seconde ligne.
+ const cas = [
+ ['.js', `// ${titulaire}\n// ${LICENCE}\n`, true],
+ ['.sh', `#!/bin/sh\n# ${titulaire}\n# ${LICENCE}\n`, true],
+ ['.svelte', `\n`, true],
+ ['.svelte', `\n\n`, true],
+ ['.html', `\n\n`, true],
+ ['.css', `/* ${titulaire}\n ${LICENCE} */\n`, true],
+ ['.css', `/* ${titulaire} */\n/* ${LICENCE} */\n`, true],
+ ['.js', `// ${titulaire}\n${LICENCE}\n`, false],
+ ['.sh', `#!/bin/sh\n# ${titulaire}\n${LICENCE}\n`, false],
+ ['.svelte', `\n${LICENCE}\n`, false],
+ ['.html', `\n\n${LICENCE}\n`, false],
+ ['.html', `\n`, false],
+ ['.css', `/* ${titulaire} */\n${LICENCE}\n`, false],
+ ['.css', `/* ${titulaire} */\n * ${LICENCE} */\n`, false],
+ ];
+ const ecarts = cas
+ .filter(([extension, contenu, admis]) => porteLEnTete(contenu, extension) !== admis)
+ .map(([extension, contenu, admis]) =>
+ `${extension} ${admis ? 'refusé' : 'admis'} : ${JSON.stringify(contenu)}`,
+ );
+ assert.deepEqual(ecarts, []);
+ });
+
+ test('package.json déclare la licence et le titulaire', () => {
+ const paquet = JSON.parse(readFileSync(join(RACINE, 'package.json'), 'utf8'));
+ assert.equal(paquet.license, 'AGPL-3.0-or-later');
+ assert.equal(paquet.author, 'TechnoLibre (http://www.technolibre.ca)');
+ });
+});
diff --git a/test/projets.test.js b/test/projets.test.js
new file mode 100644
index 0000000..c140739
--- /dev/null
+++ b/test/projets.test.js
@@ -0,0 +1,33 @@
+// © 2026 TechnoLibre (http://www.technolibre.ca)
+// License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
+
+// Le projet node n'expose aucun DOM et ne compile aucun composant : un module
+// qui toucherait document ou window, ou qui importerait un composant Svelte,
+// y échoue au lieu de passer sur un environnement que la série node ne doit
+// pas fournir (§ 14.4, § 14.8).
+import assert from 'node:assert/strict';
+import { existsSync } from 'node:fs';
+import { describe, test } from './lanceur.js';
+
+describe('projet node', () => {
+ test('globalThis.document vaut undefined', () => {
+ assert.equal(globalThis.document, undefined);
+ });
+
+ test('globalThis.window vaut undefined', () => {
+ assert.equal(globalThis.window, undefined);
+ });
+
+ test('un composant Svelte ne se charge pas', async () => {
+ // Le composant existe, et son import échoue sur la lecture du source :
+ // sans greffon Svelte, l'analyse des imports de Vite rejette la syntaxe
+ // d'un .svelte, et sous « node --test » Node en refuse l'extension. Un
+ // refus venu d'ailleurs, comme un module qui lirait window au chargement,
+ // fait échouer l'épreuve au lieu de la satisfaire.
+ assert.ok(existsSync(new URL('../src/interface/App.svelte', import.meta.url)));
+ await assert.rejects(
+ import('../src/interface/App.svelte'),
+ /Failed to parse source for import analysis|Unknown file extension/,
+ );
+ });
+});
diff --git a/version.json b/version.json
new file mode 100644
index 0000000..0ef39de
--- /dev/null
+++ b/version.json
@@ -0,0 +1 @@
+{ "version": "2026.10.05.01" }
diff --git a/vite.config.js b/vite.config.js
new file mode 100644
index 0000000..372a00b
--- /dev/null
+++ b/vite.config.js
@@ -0,0 +1,18 @@
+// © 2026 TechnoLibre (http://www.technolibre.ca)
+// License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
+
+// Construit la page que servent la plateforme web de Capacitor et la coquille
+// Electron. La base relative fait commencer chaque chemin d'actif par « ./ » :
+// la coquille charge la page par file://, où un chemin absolu « /assets/… »
+// désigne la racine du disque.
+import { defineConfig } from 'vite';
+import { svelte } from '@sveltejs/vite-plugin-svelte';
+
+export default defineConfig({
+ base: './',
+ plugins: [svelte()],
+ build: {
+ outDir: 'www',
+ emptyOutDir: true,
+ },
+});
diff --git a/vitest.config.js b/vitest.config.js
new file mode 100644
index 0000000..03fe9aa
--- /dev/null
+++ b/vitest.config.js
@@ -0,0 +1,63 @@
+// © 2026 TechnoLibre (http://www.technolibre.ca)
+// License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
+
+// Un seul lanceur, trois projets (§ 14.4, § 14.8).
+//
+// node fonctions pures, sans DOM et sans greffon Svelte : un module qui
+// importerait un composant échoue au chargement.
+// node-long épreuves lourdes, hors de la série surveillée. Leur durée croît
+// avec l'application : chacune dispose de deux minutes, là où le
+// délai par défaut de Vitest, 5 s, la ferait échouer par
+// dépassement et non sur un défaut. Ce délai arrête un test
+// bloqué ; il n'est pas le plafond de la série (§ 14.14).
+// navigateur un vrai moteur de rendu. Le projet étend vite.config.js : un
+// module s'y résout par les mêmes greffons et réglages que dans la
+// construction du livrable, sans seconde configuration à tenir
+// d'accord avec la première.
+import { defineConfig } from 'vitest/config';
+import { playwright } from '@vitest/browser-playwright';
+
+export default defineConfig({
+ test: {
+ // Sous surveillance, une épreuve se relance quand un module de son graphe
+ // d'imports change. Les fichiers des motifs se lisent hors de tout
+ // graphe : le contrôle de version (§ 18.5) lit les premiers par le
+ // système de fichiers ; la coquille s'exécute dans un processus à part,
+ // la configuration d'electron-builder se charge par un require natif et
+ // lit capacitor.config.json, et le script de construction se lance
+ // depuis une copie. Chaque motif rattache ses fichiers aux épreuves qui
+ // les lisent, et au contrôle de version quand son point 6 les balaie. Un
+ // fichier qui répond à un motif ne relance que les épreuves nommées, et
+ // plus celles qui l'importeraient : un motif ne nomme donc aucun fichier
+ // qu'une épreuve importe. L'option se lit au niveau du lanceur : posée
+ // dans un projet, elle est ignorée sans avertissement.
+ watchTriggerPatterns: [
+ { pattern: /(^|\/)(version\.json|package-lock\.json|CHANGELOG\.md|README\.md|GUIDE-[^/]*\.md)$/,
+ testsToRun: () => 'scripts/version.test.js' },
+ { pattern: /(^|\/)electron\/[^/]+$/,
+ testsToRun: () => ['test/coquille.test.js', 'test/livrable.test.js', 'scripts/version.test.js'] },
+ { pattern: /(^|\/)test\/electron_factice\.js$/,
+ testsToRun: () => ['test/coquille.test.js', 'scripts/version.test.js'] },
+ { pattern: /(^|\/)(electron-builder\.config\.cjs|capacitor\.config\.json)$/,
+ testsToRun: () => ['test/livrable.test.js', 'scripts/construire_windows.test.js', 'scripts/version.test.js'] },
+ { pattern: /(^|\/)scripts\/construire_windows\.sh$/,
+ testsToRun: () => ['scripts/construire_windows.test.js', 'scripts/version.test.js'] },
+ ],
+ projects: [
+ { test: {
+ name: 'node', environment: 'node',
+ include: ['src/**/*.test.js', 'scripts/**/*.test.js', 'test/**/*.test.js'],
+ exclude: ['**/*.long.test.js', '**/*.navigateur.test.js', 'node_modules/**'] } },
+ { test: {
+ name: 'node-long', environment: 'node',
+ include: ['src/**/*.long.test.js', 'test/**/*.long.test.js'],
+ testTimeout: 120_000 } },
+ { extends: './vite.config.js',
+ test: {
+ name: 'navigateur',
+ include: ['src/**/*.navigateur.test.js'],
+ browser: { enabled: true, headless: true, provider: playwright(),
+ instances: [{ browser: 'chromium' }] } } },
+ ],
+ },
+});