[UPD] spec: storage, CSV and workstation rules as the code has them

The spec said less than the code enforces, so a reader could not tell a rule
from an accident. It now states: ten minutes of inactivity before read mode
(§ 8.4); reglages_locaux.json (§ 8.5); the probe that creates the chosen
working folder, the temporary Chromium profile, and the name bound on the
longest path an event writes, trash and atomic write included (§ 8.6); the
proposition counter and per-proposition seat flag (§ 8.9); the closed list of
exclu values, encoding refusals and the one-column file (§ 10.1); the layer
and determinism guards, the coverage floors, and the workstation (§ 13, § 16).
Checked: 1266 node tests on the result.

--- FR ---

[UPD] spec : stockage, CSV et poste de développement selon le code

Le spec en disait moins que le code n'en impose : un lecteur ne distinguait
pas une règle d'un hasard. Il énonce désormais : dix minutes d'inactivité
avant la lecture (§ 8.4) ; reglages_locaux.json (§ 8.5) ; la sonde qui crée le
dossier de travail retenu, le profil temporaire de Chromium et la borne posée
sur le plus long chemin écrit, corbeille et écriture atomique comprises
(§ 8.6) ; le compteur des propositions et le drapeau des sièges (§ 8.9) ; la
liste fermée d'exclu, les refus d'encodage, le fichier à une colonne
(§ 10.1) ; les gardes des couches et du déterminisme, les seuils de
couverture et le poste de développement (§ 13, § 16).
Vérifié : 1266 épreuves node sur le résultat.

Assisted-by: Claude Opus 5.5
Claude-Session: https://claude.ai/code/session_01EUXSGcwCLSC69FWEdCtWSb
This commit is contained in:
Mathieu Benoit 2026-10-06 20:42:00 -04:00
parent aa5fafab1a
commit fa2fc0eb2b

238
spec.md
View file

@ -1191,8 +1191,9 @@ tienne : si naviguer exigeait de passer en écriture, l'opérateur y resterait e
permanence et le mode lecture ne protégerait plus rien.
**On en sort par un seul geste délibéré** — une commande « Modifier » à place
fixe — et le logiciel y retourne **seul après inactivité**. Ce retour ne coûte
rien et ne perd rien, puisque tout geste achevé est déjà sur le disque.
fixe — et le logiciel y retourne **seul après dix minutes d'inactivité**. Ce
retour ne coûte rien et ne perd rien, puisque tout geste achevé est déjà sur le
disque.
**Le compteur d'inactivité ne court pas pendant un geste** : il est suspendu
tant qu'un pointeur est enfoncé, qu'un champ contient une saisie non validée ou
@ -1241,11 +1242,11 @@ involontaire, ou déplacer une personne dans un brouillon exigerait de
« débloquer ».
**Le cadrage se range avec le mode, hors du fichier** — dans un **fichier de
réglages locaux** du dossier de travail (§ 8.6), distinct des fichiers
d'événements. Écrire le zoom dans le fichier d'état contredirait l'équivalence du
§ 8.2, et **zoomer sur un plan bloqué écrirait dans le fichier d'un plan qui
refuse toute modification**. Ce réglage n'est ni transmis avec le fichier, ni
restitué par un retour arrière.
réglages locaux**, `reglages_locaux.json` à la racine du dossier de travail
(§ 8.6), distinct des fichiers d'événements. Écrire le zoom dans le fichier
d'état contredirait l'équivalence du § 8.2, et **zoomer sur un plan bloqué
écrirait dans le fichier d'un plan qui refuse toute modification**. Ce réglage
n'est ni transmis avec le fichier, ni restitué par un retour arrière.
Ce fichier porte **deux choses, et deux seulement** :
@ -1287,12 +1288,12 @@ règle écrite, et l'affiche :
l'interdit. Le livrable est pour cette raison un **exécutable portable à
fichier unique, non un installateur** (§ 16).
3. Sinon, le logiciel **éprouve l'écriture** dans `data/` à côté de
l'exécutable : il écrit un fichier témoin, le relit, l'efface, et efface un
témoin resté d'une séance précédente. Un contrôle de droits ne répond pas à
la question posée — les droits effectifs dépendent d'héritages,
d'appartenances de groupe, de stratégies d'antivirus, et un support amovible
en lecture seule présente un dossier d'apparence inscriptible. Une écriture
suivie d'une relecture répond.
l'exécutable, qu'il crée au besoin : il écrit un fichier témoin, le relit,
l'efface, et efface un témoin resté d'une séance précédente. Un contrôle de
droits ne répond pas à la question posée — les droits effectifs dépendent
d'héritages, d'appartenances de groupe, de stratégies d'antivirus, et un
support amovible en lecture seule présente un dossier d'apparence
inscriptible. Une écriture suivie d'une relecture répond.
4. L'épreuve réussit → `data/` est le dossier de travail. C'est le **mode
portable** : l'exécutable et les événements se déplacent ensemble, et la
sauvegarde est la copie d'un seul dossier.
@ -1300,6 +1301,13 @@ règle écrite, et l'affiche :
un dossier nommé d'après le produit, dans les *Documents* de l'opérateur. Le
logiciel **dit pourquoi**, une fois, à la première ouverture, sans bloquer.
**Le dossier retenu est éprouvé à son tour, et c'est cette épreuve qui le
crée** : le dossier des *Documents* naît à la détermination, avant que la liste
des événements s'ouvre, et non à la première écriture. Aucun geste ne crée
ensuite le dossier où il écrit : un dossier de travail qui disparaît en cours de
séance ne renaît pas vide, en silence. Si l'épreuve échoue, le logiciel le dit
avant d'ouvrir la liste, puisqu'aucun geste ne s'y écrirait.
**Ce qui arrive ensuite est aussi réglé.**
- Si `data/` existe, **contient des événements** et cesse d'être inscriptible, le
@ -1308,7 +1316,8 @@ règle écrite, et l'affiche :
de la veille a disparu, et l'opérateur conclurait à une perte.
- La même règle vaut **en cours de séance**, pas seulement au démarrage : une clé
se retire. Quand une écriture échoue, le logiciel nomme le fichier et le
dossier, et propose d'écrire ailleurs. Il énonce alors que l'équivalence du
dossier, et propose d'écrire ailleurs, dans un dossier qu'il crée au besoin
puisque l'opérateur l'a désigné. Il énonce alors que l'équivalence du
§ 8.2 est rompue **par le support**, au lieu de laisser croire que le dernier
geste est sur le disque.
- Si le dossier de travail est neuf et vide alors que l'autre emplacement
@ -1332,6 +1341,16 @@ propriétaire — et non du chemin.
Ce que le logiciel refuse, inchangé : **jamais de répertoire de données caché**,
jamais `AppData`, jamais un emplacement que l'opérateur ne peut pas nommer.
**Le profil de Chromium n'est pas un répertoire de données.** Chromium,
qu'emporte la coquille Electron, range par défaut son profil — cache,
préférences, stockage de la page — dans un dossier persistant des données
applicatives de l'utilisateur. Le logiciel n'y garde rien : la coquille pose ce
profil dans un dossier neuf du dossier temporaire du système, et l'efface à la
sortie ; chaque démarrage efface aussi ceux des séances terminées, que Chromium
a récrits après la sortie ou qu'une séance coupée a laissés. Un exécutable
portable ne laisse ainsi, hors du dossier de travail, rien qui survive à la
séance suivante.
**Deux fichiers par événement :**
| fichier | contenu | écriture |
@ -1347,12 +1366,20 @@ opaques est inutilisable dans un explorateur. La dérivation retire les
caractères que Windows refuse, **refuse les noms réservés quelle que soit la
casse**, **compare les collisions sans égard à la casse** (le système de
livraison y est insensible, celui de développement non), **borne le chemin
complet sur le plus long des deux suffixes** — `.gtt-journal.jsonl`, dix-neuf
caractères, et non `.gtt.json`, neuf ; sinon l'état tient et son journal déborde
—, et **retombe sur un nom générique** quand la dérivation ne laisse rien. **La
dérivation reçoit la racine du dossier de travail en paramètre**, celle-ci
n'étant plus connue à l'écriture du code, et son test l'exerce sur les deux
issues. **L'autorité reste le nom inscrit dans le fichier.**
complet sur le plus long chemin que l'événement écrit**, et **retombe sur un nom
générique** quand la dérivation ne laisse rien. Ce plus long chemin assemble les
plus longues parties qu'un fichier de l'événement peut porter : le dossier daté
de la corbeille (§ 8.7) au rang de collision le plus long — deux suppressions
dans la même seconde se départagent par `_2`, `_3`… jusqu'à `_99` —, le plus long
suffixe, celui de la génération de secours, `.gtt.json.precedent`, dix-neuf
caractères, et le suffixe `.ecriture` de l'écriture atomique (§ 8.8) :
`corbeille/AAAA-MM-JJ_HH-MM-SS_99/<nom>.gtt.json.precedent.ecriture`, soixante
et un caractères outre le nom. `.gtt-journal.jsonl` n'en compte que dix-huit, et
`.gtt.json` neuf : une borne posée sur l'état ou sur le journal laisserait
déborder tout le reste. **La dérivation reçoit la racine du dossier de travail
en paramètre**, celle-ci n'étant plus connue à l'écriture du code, et son test
l'exerce sur les deux issues. **L'autorité reste le nom inscrit dans le
fichier.**
**Pourquoi l'historique n'est pas dans le fichier d'état**, pour deux raisons
distinctes. *La taille* : un plan de 260 personnes sur 4 tours pèse de l'ordre
@ -1500,6 +1527,15 @@ Une proposition porte un **identifiant entier séquentiel**, et les propositions
sont rangées **en liste ordonnée** — jamais en objet indexé par une clé, dont
l'ordre n'est pas garanti par la forme. L'identifiant vient de la place dans la
suite de graines dérivées (§ 5.7), et ne dérive ni de l'horloge ni d'un tirage.
Il se compte à partir d'un compteur du fichier, `prochainsIds.proposition`, qui
dépasse tout identifiant de proposition jamais attribué, celui d'où vient le
placement retenu compris : une génération numérote ses propositions dans
l'ordre de ses graines à partir de lui, puis le porte au-delà de la dernière. Le
compteur ne recule jamais — ni quand une commande efface les propositions
(§ 5.7), ni quand une main l'abaisse dans le fichier, l'ouverture le relevant
au-delà de chaque identifiant qu'elle lit. Les générations s'accumulent ainsi
sans qu'un identifiant se répète ni revienne, et le retenu désigne toujours la
même proposition d'origine.
Chaque proposition déclare, **une fois** :
@ -1507,6 +1543,8 @@ Chaque proposition déclare, **une fois** :
- la **liste des capacités** correspondantes ;
- le **nombre de tours** ;
- l'**ensemble des identifiants de participants** qu'elle place ;
- le drapeau `siegesAttribues` : ses **sièges sont-ils attribués** (§ 5.3),
l'ordre d'une liste de table étant alors celui des sièges ;
- sa **graine dérivée** (§ 5.7), le **compte d'arrêt** et la **longueur de
l'historique** d'acceptation de la recherche qui l'a produite : les trois
réglages qui la déterminent.
@ -1532,7 +1570,13 @@ siège n'est pas stocké : il est la position dans la liste.
**Quand les sièges ne sont pas attribués** (§ 5.3), l'ordre à l'intérieur d'une
table ne porte aucune information : le logiciel l'écrit **trié par identifiant
croissant**. Sans cette règle, deux états identiques produisent deux fichiers
différents, et le § 8.8 est violé par la seule forme du stockage.
différents, et le § 8.8 est violé par la seule forme du stockage. **C'est le
drapeau de la proposition qui décide**, jamais le réglage courant de
l'événement : changer le réglage n'efface pas l'ordre des sièges d'une
proposition déjà produite. Le placement retenu a la même forme et son propre
drapeau, avec l'identifiant de la proposition dont il vient. L'ensemble des
participants et chaque réserve se trient par identifiant croissant dans tous
les cas : ce sont des ensembles.
```
# illustration, annotée ; le fichier livré est du JSON strict
@ -1719,25 +1763,47 @@ Les en-têtes se reconnaissent **sans tenir compte de la casse, des accents ni
des espaces**. **Deux colonnes reconnues sous le même nom** ne se départagent
pas toutes seules : le logiciel n'associe ni l'une ni l'autre et demande.
**La valeur de `exclu`** appartient à une liste fermée ; toute autre valeur
**refuse la ligne**. Traiter l'inconnu comme `non` placerait dans la salle une
personne qui s'est désistée, et la chaise vide ne se verrait que le soir même.
**La valeur de `exclu`** appartient à une liste fermée : `oui`, `o`, `vrai`,
`1` ou `x` pour une exclusion ; `non`, `n`, `faux`, `0` ou vide pour aucune ;
sans égard à la casse, aux accents ni aux espaces. Toute autre valeur **refuse
la ligne**. Traiter l'inconnu comme `non` placerait dans la salle une personne
qui s'est désistée, et la chaise vide ne se verrait que le soir même.
**L'encodage se tranche en quatre temps, dans cet ordre :** marque d'ordre
d'octets UTF-8 ; marque UTF-16 ; **un octet nul dans les quatre premiers
kibioctets sans marque** → refus global avec le remède nommé ; sinon décodage
UTF-8 strict, et repli sur windows-1252 en cas d'échec.
kibioctets sans marque** → refus global avec le remède nommé
(`UTF16_SANS_MARQUE`) ; sinon décodage UTF-8 strict, et repli sur windows-1252
en cas d'échec.
> L'étape 3 n'est pas une précaution de principe : **un texte UTF-16 dont le
> contenu est latin est valide en UTF-8**, chaque octet nul s'y décodant comme
> le caractère nul. Un ordre à trois temps produirait des noms entrelardés de
> caractères nuls, qui ne font échouer aucun calcul. Le mode de défaillance est
> muet : « Benoît » importé en « Benoît » se découvre sur le plan imprimé.
**Une marque déclare l'encodage, et rien ne la dément.** Des octets qui ne sont
pas l'UTF-8 ou l'UTF-16 qu'elle annonce — une séquence invalide, un nombre
impair d'octets, un substitut isolé — refusent le fichier (`UTF8_INVALIDE`,
`UTF16_INVALIDE`) : un repli sur windows-1252 changerait chaque accent de la
partie valide en caractères parasites, « Benoît » en « Benoît », et un décodage
indulgent sèmerait des caractères de remplacement dans les noms. Quel que soit
le temps qui a décodé, **un texte qui porte le caractère nul est refusé**
(`CARACTERE_NUL`) — un collage aussi, qui ne porte pas d'octets : le refus
rattrape un octet nul au-delà des quatre kibioctets, et un fichier que sa marque
fait lire sans erreur sous un autre encodage, la marque de l'UTF-32 LE
commençant par celle de l'UTF-16 LE. Chaque refus d'encodage nomme le même
remède : réenregistrer le fichier en UTF-8.
> L'étape 3 n'est pas une précaution de principe : **un texte UTF-16 sans
> marque se décode sans erreur dans l'un ou l'autre des temps suivants**. En
> ASCII, il est de l'UTF-8 valide, chaque octet nul s'y décodant comme le
> caractère nul ; accentué, il cesse de l'être et retombe sur windows-1252, qui
> accepte tout octet. Dans les deux cas, les noms arrivent entrelardés de
> caractères nuls, qui ne font échouer aucun calcul. Un ordre à trois temps
> produirait ce défaut muet, qui ne se découvre que sur le plan imprimé.
**Le séparateur** se choisit entre `;`, `,` et la tabulation, en **analysant**
les vingt premiers enregistrements : le candidat retenu termine sans guillemet
ouvert, donne le même nombre de champs partout, **et ce nombre dépasse 1**. À
égalité, celui qui donne **le plus d'en-têtes reconnus**.
égalité, celui qui donne **le plus d'en-têtes reconnus**. Quand aucun candidat
ne convient et que la première ligne entière est un en-tête reconnu, **le
fichier n'a qu'une colonne** — une liste de noms — et chaque ligne est un seul
champ : un nom qui porte une virgule reste entier. Sinon, le fichier est
refusé, et le refus nomme ce qui écarte le séparateur.
> Les deux dernières conditions ne sont pas des raffinements. Sur un fichier
> séparé par des virgules, `;` donne lui aussi un nombre de champs parfaitement
@ -1759,11 +1825,16 @@ une appartenance nommée « ».
**La ligne invalide : importer le reste.** Le logiciel importe les lignes
valides et **réexporte les refusées** en un CSV reprenant les colonnes
d'origine, augmenté de `ligne` et `motif` — deux colonnes qu'aucun en-tête ne
reconnaît, donc le fichier corrigé se réimporte tel quel. Sur 260 lignes, un
refus global coûterait l'import entier pour une faute en ligne 213.
reconnaît, donc le fichier corrigé se réimporte tel quel. `ligne` porte le
numéro de l'enregistrement, en-tête compris : celui qu'un tableur affiche, un
champ cité sur plusieurs lignes du texte n'y comptant qu'une fois. Sur 260
lignes, un refus global coûterait l'import entier pour une faute en ligne 213.
Trois cas restent des refus **globaux** : aucun en-tête reconnaissable, aucune
colonne associée à `nom`, zéro ligne valide.
Hors de l'encodage et du séparateur, quatre cas restent des refus **globaux** :
aucun en-tête reconnaissable, aucune colonne associée à `nom`, zéro ligne
valide, et **un guillemet resté ouvert** jusqu'à la fin du texte
(`GUILLEMET_OUVERT`, avec l'enregistrement où il s'ouvre) — tout ce qui le suit
tiendrait dans un seul champ, et deux personnes se fondraient en une.
**L'import entier est une seule entrée d'historique.**
@ -2780,8 +2851,8 @@ complet au disque.
Le code que Vite compile vit sous **`src/`**, en modules nommés d'après les
couches du § 13.4 : `src/moteur`, `src/geometrie`, `src/stockage`, `src/csv`,
`src/pdf`, `src/demo`, `src/interface`. À la racine du projet,
**`version.json`** est l'unique source de la version (§ 18.3) et
`src/pdf`, `src/demo`, `src/application`, `src/interface`. À la racine du
projet, **`version.json`** est l'unique source de la version (§ 18.3) et
**`src/version.genere.js`** le module qu'elle engendre — **versionné dans le
dépôt**, parce qu'une séance de développement lancée sans l'étape de construction
doit afficher une version et non une importation manquante, et **contrôlé par
@ -2876,11 +2947,18 @@ qu'elle décide de l'endroit où vit la soirée.
**La frontière des couches est gardée mécaniquement**, et pas seulement par
discipline : un test du **graphe d'imports** refuse qu'un module de `src/moteur`
ou de `src/geometrie` importe de `src/interface`, de `src/stockage` ou d'une
interface de plateforme. Le projet `node` ne chargeant pas le greffon Svelte
(§ 14.8), un test de moteur qui importerait un composant échoue au chargement —
mais cette garde-là ne couvre que les composants, et un module qui touche
`document` ne tomberait qu'à l'exécution de la branche fautive.
ou de `src/geometrie` importe de `src/interface`, de `src/application`, de
`src/stockage` ou d'une interface de plateforme. Il refuse de même que
`src/stockage` importe de `src/csv`, de `src/application`, de `src/interface` ou
d'une plateforme, ou touche le navigateur hors de ses deux implémentations du
système de fichiers, qu'aucun autre de ses modules n'importe ; que `src/csv`,
qui lit le modèle du stockage, importe de `src/application`, de `src/interface`
ou d'une plateforme ; et que `src/application` importe de `src/interface` ou de
Svelte. Un type que la documentation d'un module importe compte comme un
import. Le projet `node` ne chargeant pas le greffon Svelte (§ 14.8), un test de
moteur qui importerait un composant échoue au chargement — mais cette garde-là
ne couvre que les composants, et un module qui touche `document` ne tomberait
qu'à l'exécution de la branche fautive.
### 13.5 Ce que le SVG donne et que le canvas ferait payer
@ -3016,12 +3094,15 @@ est écrit en dur.
### 14.7 Le déterminisme
Aucune source non reproductible — `Math.random`, `Date.now`, `performance.now`,
`crypto.getRandomValues` — dans le **moteur** ni dans le **générateur de
démonstrations**. Un test de l'arborescence le refuse, **et échoue si son
balayage ne trouve aucun fichier**. Son périmètre est nommé : `src/moteur` et
`src/demo`, à l'exclusion du code d'épreuve, où un tirage sert légitimement
(§ 14.12). L'interdit ne porte pas sur le reste de l'application : l'historique
horodate ses entrées, et le banc de mesure du § 19.10 relève des temps par image.
`crypto.getRandomValues` — dans le **moteur**, dans le **générateur de
démonstrations**, ni dans le **stockage** et l'**analyseur CSV**, qui reçoivent
l'horloge et l'aléa en paramètre. Un test de l'arborescence le refuse, **et
échoue si son balayage ne trouve aucun fichier**. Son périmètre est nommé :
`src/moteur`, `src/demo`, `src/stockage` et `src/csv`, à l'exclusion du code
d'épreuve, où un tirage sert légitimement (§ 14.12). L'interdit ne porte pas sur
le reste de l'application : l'application lit l'horloge pour horodater les
entrées de l'historique, que le stockage écrit telles qu'il les reçoit, et le
banc de mesure du § 19.10 relève des temps par image.
**Ce n'est pas une règle d'hygiène.** L'interdit de `performance.now` dans le
moteur est ce qui rend vraie la phrase « à graine et entrée égales, placement
@ -3230,10 +3311,12 @@ fichiers, que recopier un dossier suffit à fausser.
entrées corrompu à la trente-septième : trente-six entrées retenues,
soixante-quatre annoncées écartées.
**La dérivation des noms refuse `con` comme `CON`**, et borne le chemin sur
`.gtt-journal.jsonl`, dix-neuf caractères, et non sur `.gtt.json`, neuf. Elle
reçoit la racine en paramètre et son test l'exerce sur les **deux** dossiers de
travail possibles (§ 8.6).
**La dérivation des noms refuse `con` comme `CON`**, et borne le plus long
chemin que l'événement écrit, corbeille et écriture atomique comprises (§ 8.6) :
le test recompose ce chemin en entier, et échoue sur une borne posée sur
`.gtt.json`, neuf caractères, sur le seul `.gtt-journal.jsonl`, dix-huit, ou qui
oublie la corbeille ou l'écriture atomique. Elle reçoit la racine en paramètre et
son test l'exerce sur les **deux** dossiers de travail possibles (§ 8.6).
**Un renommage atomique qui échoue ne détruit pas la cible.** L'implémentation
d'épreuve échoue au renommage : la cible conserve son contenu antérieur, et le
@ -3473,6 +3556,19 @@ branche non couverte y est un cas que l'opérateur rencontre au pire moment. Une
branche qu'on ne sait pas couvrir est soit du code mort — on le retire — soit un
cas que la spécification n'a pas prévu — on l'écrit.
**La mesure est sa propre commande**, `make couverture` : la série `node` sous
instrumentation, hors des budgets du § 14.14. Elle publie un tableau par couche
et par module, et un rapport à parcourir qui montre, ligne à ligne, ce qui n'est
jamais exécuté. Les deux seuils portent sur quatre fichiers —
`src/moteur/indicateurs.js` et `src/moteur/plafond.js`, `src/stockage/depot.js`
et `src/stockage/journal.js` — et la commande échoue en deçà. Un test de
l'arborescence refuse un seuil abaissé, un seuil global, et un seuil posé sur un
fichier absent ou hors de la mesure : l'outil tiendrait ce dernier pour atteint,
faute de branche à compter. Il refuse de même une commande qui ne mesure pas —
sans instrumentation, la série passe et aucun seuil ne se lit —, une commande
dont les arguments redéfinissent la mesure ou ce qu'elle exécute, un tableau qui
tait les modules pleins, et l'absence du rapport ligne à ligne.
**Ce qui l'empêche de devenir une case à cocher** ne vient pas de l'outil de
couverture :
@ -3875,6 +3971,42 @@ que le lecteur reproduise exactement ce qu'il lit. Il est relu par quelqu'un qui
n'a pas écrit le logiciel ; un guide relu par son auteur ne révèle aucune étape
manquante.
### 16.2 Le poste de développement
Le projet s'installe et se lance d'une commande, la même sur chaque système
Linux, NixOS compris, et sous macOS ; Windows a la sienne. Une étape laissée à
la main est celle qu'un nouveau venu oublie, sans savoir ensuite laquelle.
| commande | ce qu'elle fait |
|---|---|
| `./install.sh` | pose les paquets du système qui manquent — par apt, dnf, zypper ou pacman, sous `sudo` —, puis le Node du projet et les dépendances que fixe `package-lock.json` ; relancée, ne refait que ce qui manque |
| `./install_dev.sh` | ajoute le Chromium des épreuves du navigateur, des polices et, sur amd64, `podman`, sous lequel se construit l'exécutable |
| `./run.sh` | lance la coquille Electron sur une session graphique ; sans écran, ou là où Electron n'est pas publié, sert l'application à un navigateur par le serveur de Vite |
| `make` | lance l'application comme `./run.sh` ; `make help` nomme les autres cibles — les trois séries d'épreuves, la couverture (§ 14.13), la construction, le contrôle de version (§ 18.5), l'essai de démarrage |
**Le Node du projet a une seule source** : la majeure écrite dans
`.node-version`, que lisent les scripts bash, les scripts PowerShell et
`shell.nix`. L'installation télécharge cette version de nodejs.org, la vérifie
contre sa somme et la range hors du dépôt, dans le dossier de l'utilisateur :
elle n'exige aucun droit d'administration et ne dépend pas du Node que porte —
ou ne porte pas — le système. Chaque cible du `Makefile` passe par ce Node.
**Sous Windows**, `install.cmd`, `install_dev.cmd` et `run.cmd` passent la main à
des scripts PowerShell 5.1, la version que porte tout Windows. **Sous NixOS**,
où les binaires téléchargés ne trouvent ni leur chargeur ni leurs bibliothèques
aux chemins qu'ils attendent, `scripts/installation/shell.nix` déclare Node,
Electron et Chromium d'un nixpkgs épinglé par révision et par somme ; les scripts
du projet s'y relancent d'eux-mêmes.
**`make verifier_systemes` éprouve l'installation elle-même.** Sur chaque système
du catalogue — les familles Debian, Fedora, openSUSE et Arch, et Nix —, un
conteneur `podman` exécute, sous un compte ordinaire qui passe par `sudo`,
`./install_dev.sh`, puis les trois séries d'épreuves, l'essai de démarrage
d'Electron et le serveur de Vite ; les scripts PowerShell s'y éprouvent sous
`pwsh`. Ce qu'un conteneur n'éprouve pas est nommé, et reste à vérifier sur une
machine : le bac à sable d'Electron, une autre architecture que celle de l'hôte,
NixOS lui-même, une vraie session graphique, Windows et macOS réels.
---
## 17. Ce que ce document ne tranche pas