diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a85f475..eb86597 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,9 +3,7 @@ # # OÙ VIT CE FICHIER. Le dépôt est servi tel quel (GitHub Pages) : sa racine # porte le code de l'application (`src/`) et l'outillage (`scripts/`, `tests/`). -# Le présent fichier est le modèle de `/.github/workflows/ci.yml`, où -# l'export le dépose (comme `src/docs/GITHUB.md` devient `README.md`). Voir -# `src/docs/INDUSTRIALISATION.md`. +# Voir `src/docs/INDUSTRIALISATION.md`. # # CE QU'IL FAIT. Rien à installer pour le premier travail : la vérification de # syntaxe, le contrôle de style et les tests des modules purs n'utilisent que @@ -27,8 +25,9 @@ # AVISANT pour l'instant (`continue-on-error`), voir son commentaire plus bas. # # LE TROISIÈME TRAVAIL CONSTRUIT L'IMAGE DOCKER ET LA MET EN SERVICE, sur une -# vraie MariaDB : c'est la seule chose que l'atelier ne peut pas faire, et c'est -# pourtant ce qui décide si « ça marche en production ». Il ne pousse aucune +# vraie MariaDB : c'est la seule chose que la CI peut faire de plus que les +# épreuves, et c'est pourtant ce qui décide si « ça marche en production ». Il +# ne pousse aucune # image (aucun secret, aucun registre) : il répond à la question « le # `Dockerfile` se construit-il encore, et le service répond-il, base comprise ? » # — la question que se pose un exploitant au moment de déployer. Il vérifie @@ -121,11 +120,11 @@ jobs: if [ "$code" -ne 0 ]; then # LE CHEMIN EST ANCRÉ À LA RACINE (`$GITHUB_WORKSPACE`), jamais # relatif : ce travail s'exécute depuis `src/server/mysql` (voir - # `defaults` ci-dessus), et un `../../scripts/…` y désigne - # `src/scripts/…` — qui n'existe que dans l'atelier. La CI - # répondait donc `127` (script introuvable) au lieu d'annoter - # l'épreuve rouge, et le travail « Service auto-hébergé » échouait - # sans jamais dire pourquoi (le pendant, côté service, de NC-I-009). + # `defaults` ci-dessus), et un chemin relatif y désignerait un + # autre dossier. La CI répondait donc `127` (script introuvable) + # au lieu d'annoter l'épreuve rouge, et le travail « Service + # auto-hébergé » échouait sans jamais dire pourquoi (le pendant, + # côté service, de NC-I-009). bash "$GITHUB_WORKSPACE/scripts/annoncer-echecs.sh" "$RUNNER_TEMP/epreuves-service.log" fi exit "$code" diff --git a/AGENTS.md b/AGENTS.md index a064be9..19bf34e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -9,28 +9,22 @@ travail à plusieurs postes. Licence **GPL-3.0-only** (`LICENSE`) ; démonstrati . Ce fichier s'adresse à un agent (Claude Code, Mistral Vibe…) qui reprend le dépôt. Le -**même dépôt est aussi édité dans un atelier** (Perchance) : le dépôt est la référence -**publiée**, l'atelier la copie de travail. Ne renommez donc pas les fichiers sans raison. - -> **Si vous travaillez dans l'atelier, lisez `src/docs/ATELIER.md` AVANT d'écrire une ligne.** -> C'est le document écrit pour être suffisant à lui seul quand on n'a pas de terminal : la -> méthode en six gestes, les dix garde-fous, le harnais qui fait tourner la syntaxe, le style, -> les épreuves et les générateurs **sans Node**, l'éprouvette du service de l'aperçu, les -> parcours du navigateur, et la liste de ce qu'on vérifie avant de dire « terminé » (§ 9). +**dépôt est la seule source de vérité** : on y travaille directement, sur la branche `dev`, +et la branche `main` ne reçoit que ce qui a été revu (voir « Comment on livre » ci-dessous). +Ne renommez donc pas les fichiers sans raison. ## Par où commencer | Fichier | Ce qu'il contient | |---|---| -| `src/docs/ATELIER.md` | **à lire si vous travaillez dans l'atelier** : la méthode, les garde-fous, comment vérifier sans terminal, et ce qui est « terminé » | -| `src/README.md` | **à lire en premier** : architecture, conventions de code, pièges connus, et le flux de travail atelier ↔ dépôt | +| `src/README.md` | **à lire en premier** : architecture, conventions de code, pièges connus, et le flux de travail | | `src/SPEC.md` | la spécification fonctionnelle complète | | `src/TODO.md` | les chantiers ouverts (et ceux faits) | | `src/CHANGELOG.md` | le journal des versions — son premier titre daté donne `APP_VERSION` | | `src/docs/REPRISE.md` | le kit de reprise : points d'entrée, « un seul point de vérité par règle », recettes de livraison | | `src/audit/REGISTRE-NON-CONFORMITES.md` | les non-conformités connues, avec statut et preuves — **à lire avant de promettre** | | `tests/README.md` | ce que tient chaque épreuve | -| `src/docs/` | atelier (la méthode), administration, Docker, API, variables, industrialisation, performance | +| `src/docs/` | administration, Docker, API, variables, industrialisation, performance | ## Comment c'est rangé @@ -38,16 +32,14 @@ Ce fichier s'adresse à un agent (Claude Code, Mistral Vibe…) qui reprend le d ``` / README.md, AGENTS.md, CLAUDE.md, package.json, LICENSE, CNAME, .nojekyll - index.html, main.pjs la page publiée (GitHub Pages) et son code + index.html la page publiée (GitHub Pages) et son code compose-exemple/ L'EXEMPLE DE PREMIER DÉPLOIEMENT : MariaDB + l'image publiée, deux services -scripts/ L'OUTILLAGE : vérifications, analyse statique, générateurs de documents, - harnais de l'atelier (harnais-atelier.mjs) +scripts/ L'OUTILLAGE : vérifications, analyse statique, générateurs de documents tests/ LES ÉPREUVES transverses (le service éprouve, lui, à côté de son code) src/ LE CODE, servi tel quel par le navigateur lib/ ui/ css/ pages/ métier pur · écrans · feuilles de style · édition statique server/ service auto-hébergé (docker-compose, MySQL, nginx) — voir son README docs/ audit/ pdfa/ wiki.js documentation, cadre d'audit, polices du PDF/A, guide intégré - (docs/ATELIER.md = la méthode de travail et les garde-fous) CHANGELOG.md SPEC.md TODO.md README.md les documents de travail server/mysql/*.test.mjs les épreuves du service, à côté de ce qu'elles éprouvent ``` @@ -57,11 +49,20 @@ src/ LE CODE, servi tel quel par le navigateur > `src/docs/GITHUB.md`, `src/AGENTS.md` ou `src/CLAUDE.md` **sous** `src/`, ce sont des doublons > d'une version précédente (l'outillage et l'exemple de déploiement vivent à la racine) : > `git rm -r …`. Un doublon fait corriger le fichier que personne ne lit. -> -> **Dans l'atelier, c'est l'inverse : ne supprimez RIEN de ce qui est sous `src/`.** L'atelier ne -> conserve qu'un seul arbre (`src/`), et l'export y prend ces fichiers pour les remonter à la -> racine du dépôt : `src/AGENTS.md`, `src/CLAUDE.md`, `src/scripts/**`, `src/tests/**`, -> `src/package.json`, `src/github/**` et `src/compose-exemple/**` y sont donc **à leur place**. + +## Comment on livre — le dépôt est la source de vérité + +Le développement se fait **ici**, sur la branche `dev` : + +1. **Une branche par sujet**, partagée de `dev` (`vibe/` ou le nom du chantier) — + jamais de commit direct sur `main`, qui ne reçoit que des fusions relues ; +2. **un changement, un sujet** : pas de renommage ni de réorganisation « au passage » ; +3. **vérifier en local avant d'envoyer** : `npm run verifier` (syntaxe + style strict + + épreuves) — c'est ce que la CI rejoue ; +4. **une pull request vers `dev`**, la CI verte avant toute demande de relecture ; +5. **la trace** : une entrée datée dans `src/CHANGELOG.md` (rubrique `Ajouté` / `Modifié` / + `Corrigé` / `Retiré`), `APP_VERSION` (`src/lib/version.js`) au même numéro, et les + documents que le changement rend faux. ## Vérifier — c'est tout ce qui compte @@ -75,12 +76,6 @@ npm run style # le style seul, en strict : un avertissement fait écho npm test # les épreuves (tests/ + le domaine du service) ``` -> **Sans terminal (atelier).** Ces commandes demandent un processus, que l'atelier n'a pas : le -> harnais **`scripts/harnais-atelier.mjs`** les rejoue — les scripts livrés **tels quels** — et -> rend un verdict en quelques lignes (`h.texte(await h.verifier({ fs }))`). La recette exacte, les -> chiffres attendus et les dix-neuf écarts connus (tous dus à l'absence de Node) sont dans -> `src/docs/ATELIER.md` § 3. - Le **contrôle de style est écrit maison** (`scripts/verifier-style.mjs`, sans dépendance) : il refuse `debugger`, signale `var` et les `console.log` du code client, et refuse les **imports jamais employés** — un import que plus personne n'utilise trompe la lecture et survit à toutes @@ -91,22 +86,19 @@ dans la console de la page de l'application (voir `src/docs/INDUSTRIALISATION.md ## Les garde-fous, en une ligne chacun -Le détail, et le **pourquoi** de chacun, sont dans `src/docs/ATELIER.md` § 1. +Ce sont des règles **du projet**, pas des préférences. Chacune a été payée par un incident. -1. **Ne rien publier ni enregistrer sans demande explicite** (l'atelier porte une version non - enregistrée : l'enregistrer fige un nom, et un nom ne se défait pas). +1. **Ne rien publier ni enregistrer sans demande explicite.** 2. **Ne jamais écrire à la main un document engendré** (`VARIABLES.md`, `API.md`, `logiciel-engendre.mjs`) : corriger la source, puis régénérer. 3. **Une règle, une seule implémentation** — et quand deux existent (les deux services), le jeu de conformité les tient ensemble. 4. **Aucune dépendance nouvelle** ; le métier (`src/lib/`) ne touche ni au DOM ni au réseau. 5. **Tout est en français**, commentaires compris ; un commentaire dit le **pourquoi**. -6. **Après une série d'écritures : recharger l'aperçu** — les fichiers n'y sont pas appliqués - avant, et l'on croirait regarder la nouvelle version. -7. **Toute variable de déploiement a sa ligne dans les deux `env.example`.** -8. **Une livraison laisse une trace** : changelog daté, `APP_VERSION` accordé, documents corrigés. -9. **Un changement, un sujet** : pas de renommage ni de réorganisation « au passage ». -10. **Ne jamais dire « terminé » sans preuve**, ni annoncer un chiffre qu'on n'a pas mesuré. +6. **Toute variable de déploiement a sa ligne dans les deux `env.example`.** +7. **Une livraison laisse une trace** : changelog daté, `APP_VERSION` accordé, documents corrigés. +8. **Un changement, un sujet** : pas de renommage ni de réorganisation « au passage ». +9. **Ne jamais dire « terminé » sans preuve**, ni annoncer un chiffre qu'on n'a pas mesuré. ## Les règles du projet @@ -169,10 +161,10 @@ Le détail, et le **pourquoi** de chacun, sont dans `src/docs/ATELIER.md` § 1. ## Avant de dire « terminé » On ne rend la main que lorsque ces lignes sont vraies — chacune avec une **preuve**, pas une -intention. La liste complète est dans `src/docs/ATELIER.md` § 9. +intention. -- `npm run verifier` (ou, dans l'atelier, `h.verifier({ fs })`) : **syntaxe 0 faute, style code 0**, - et **aucun fichier autrefois vert n'est rouge** ; +- `npm run verifier` : **syntaxe 0 faute, style code 0**, et **aucun fichier autrefois vert + n'est rouge** ; - si le **service** a changé : les **deux** implémentations, et le jeu de conformité ; - si un **écran** a changé : il a été **regardé** (390 px **et** grand) ; - les **documents engendrés** sont régénérés si leur source a bougé ; diff --git a/CLAUDE.md b/CLAUDE.md index bd82968..010f7ad 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -6,9 +6,4 @@ code : il dit où chaque chose vit, comment vérifier (`npm run verifier`), les (l'outillage ne vit qu'à la racine, la CI est stricte, un correctif du client demande de reconstruire l'image `web`). -**Si vous travaillez dans l'atelier (Perchance), lisez d'abord `docs/ATELIER.md`** : c'est là que -sont la méthode, les dix garde-fous, le harnais qui rejoue syntaxe, style, épreuves et générateurs -**sans terminal** (`scripts/harnais-atelier.mjs`), et la liste de ce qu'on vérifie avant de dire -« terminé ». - @AGENTS.md diff --git a/README.md b/README.md index 058c5b8..45311be 100644 --- a/README.md +++ b/README.md @@ -355,7 +355,7 @@ Le dépôt range **l'outillage à la racine** et **le code dans `src/`** : | | | |---|---| -| `index.html`, `main.pjs` | la page publiée : le gabarit et le code du service de démonstration statique | +| `index.html` | la page publiée : le gabarit et le code du service de démonstration statique | | `src/lib/` | le cœur métier — modules **purs** : compilation, expressions, numérotation, signature, exports, PDF/A | | `src/ui/`, `src/css/` | les vues et les feuilles de style | | `src/server/` | le service compagnon auto-hébergé (nginx + Node + MariaDB) et le pilote de persistance | @@ -375,19 +375,13 @@ npm test # node --test tests/ et le domaine du service dépôt — commence par **`AGENTS.md`** : il tient l'arborescence, les commandes, les règles du projet (aucune dépendance, métier sans DOM ni réseau, documents engendrés à ne pas modifier à la main), les pièges et la licence. **`CLAUDE.md`** y renvoie. -S'il travaille dans **l'atelier** (l'édition en ligne, où il n'y a ni terminal ni -processus), il lit en plus **`src/docs/ATELIER.md`** : la méthode, les dix -garde-fous, le harnais `scripts/harnais-atelier.mjs` — qui rejoue la syntaxe, le -style, les épreuves et les générateurs **sans Node** — et ce qui compte comme -« terminé ». Le fond vit sous `src/` : `src/README.md` (mode d'emploi du code, +Le fond vit sous `src/` : `src/README.md` (mode d'emploi du code, doctrine, recettes d'exploitation et de livraison), `src/SPEC.md` (spécification), -`src/docs/` (service, administration, industrialisation, atelier), `src/audit/` +`src/docs/` (service, administration, industrialisation), `src/audit/` (registre). -Un fichier ne vit qu'à **un seul endroit** : la table de l'export -(`src/README.md` § « Exporter le dépôt GitHub ») dit lequel. Le même outillage, dans -l'atelier de travail, sert à faire tourner l'aperçu — d'où des chemins qui restent -relatifs dans les deux dispositions. +Un fichier ne vit qu'à **un seul endroit** : `src/README.md`, § « Disposition des +fichiers », dit lequel vit où et pourquoi. ## Version diff --git a/index.html b/index.html index 1cbf9bf..6f7e702 100644 --- a/index.html +++ b/index.html @@ -1,23 +1,19 @@ @@ -42,7 +38,7 @@ // se prendrait pour un acte réel. Ce n'est PAS vrai du logiciel auto-hébergé, // dont le recueil public est fait pour être trouvé — la règle ne vaut donc que // pour l'adresse de la démonstration, qui est celle du fichier `CNAME` du -// dépôt. Voir docs/GITHUB.md (« Démonstration statique ») et l'audit, NC-III-008. +// dépôt. Voir README.md (« Démonstration ») et l'audit, NC-III-008. if (location.hostname === "demo.scribae.eu") { var robots = document.createElement("meta"); robots.name = "robots"; diff --git a/main.pjs b/main.pjs deleted file mode 100644 index b7f4adb..0000000 --- a/main.pjs +++ /dev/null @@ -1,65 +0,0 @@ -// ============================================================================ -// Scribae — trames, rédaction et publication des actes administratifs. -// -// AVANT D'ÉCRIRE UNE LIGNE (consigne permanente) : lire src/docs/ATELIER.md — -// la méthode de travail, les dix garde-fous, comment vérifier SANS TERMINAL -// (src/scripts/harnais-atelier.mjs : syntaxe, style, épreuves et générateurs), -// et ce qui compte comme « terminé ». L'architecture est dans src/README.md, -// les règles du projet dans src/AGENTS.md, la spécification dans src/SPEC.md. -// -// On ne modifie JAMAIS à la main un document engendré (src/docs/VARIABLES.md, -// src/docs/API.md, src/server/mysql/logiciel-engendre.mjs) : on corrige sa -// source, puis on régénère. On n'enregistre NI ne publie sans demande explicite. -// -// FLUX DE TRAVAIL (détaillé dans src/README.md, -// section « Flux de travail : Perchance ↔ GitHub ») : -// • le développement se fait ICI, sur Perchance : main.pjs + index.html + src/ ; -// • les versions finalisées sont déposées sur GitHub, qui est la référence -// publiée ; -// • quand l'utilisateur fournit un lien GitHub : COMPARER avant de coder, en -// lisant APP_VERSION (src/lib/version.js) et la première entrée datée de -// src/CHANGELOG.md ; si GitHub est plus récent, reporter D'ABORD GitHub dans -// Perchance, à l'identique, et vérifier que l'application tourne ; -// • toute version figée : incrémenter APP_VERSION et ajouter son entrée datée -// dans src/CHANGELOG.md (lisible dans l'application : Documentation -// technique › Journal des versions). -// ============================================================================ - -$meta - title = Scribae — trames, rédaction et publication des actes administratifs - description = Scribae est un éditeur de trames d'actes administratifs : les administrateurs préparent les modèles (structure, champs, règles, commentaires), les services les remplissent, le système compile et exporte en Akoma Ntoso, Schematron, JSON-LD/ELI et HTML imprimable. Un guide d'utilisation intégré accompagne les rédacteurs pas à pas. La page d'accueil est le recueil public des actes publiés — consultable par tous, sans compte, avec leur identifiant ELI et leur texte ; l'atelier s'ouvre par « Se connecter ». L'application peut être déployée en auto-hébergement sur la base de la collectivité (le jeu de démonstration, entièrement fictif, s'active alors par le réglage DEMO) ; à défaut, cet aperçu présente la démonstration. - image = https://user.uploads.dev/file/06d748c1bac8b420d52e7572cc6d66f0.jpg - tags = actes administratifs, akoma ntoso, legalxml, trames, eli, open data, guide - -// Persistance : le stockage du navigateur (IndexedDB) sert de pilote « local » -// — c'est le mode de démonstration. L'application peut aussi être branchée sur -// une base partagée (service de démonstration, ou serveur MySQL/MariaDB) : -// voir src/lib/db/ et src/server/mysql/. -kv = {import:kv-plugin} - -// Service de signature et de publication (API REST) — voir index.html pour le -// code du service lui-même. Sans lui, l'écran « Signature & publication » et la -// consultation des publications restent inaccessibles. -createServerSocket = {import:server-plugin} - -// Relais HTTP sans CORS, utilisé par la numérotation externe (Administration › -// Numérotation) : un nombre croissant de collectivités prennent leur numéro -// d'acte dans un outil tiers (Grist, tableur en ligne, référentiel interne) où -// la création d'une ligne attribue le numéro. Le navigateur ne peut pas appeler -// ces services directement (CORS), le relais s'en charge. Voir -// src/lib/numbering.js. -superFetch = {import:super-fetch-plugin} - -// Hébergement de fichiers, utilisé par le circuit de signature externe : quand -// le rédacteur rentre dans la base la version signée sur papier (ou via un -// outil tiers), le PDF est déposé ici et l'acte ne conserve que son URL. Voir -// src/lib/externe.js et src/ui/views/signature.js. -uploadPlugin = {import:upload-plugin} - -// Moteur de langage des deux assistants (Administration › Assistants) : celui -// de l'atelier, qui explique le mode d'emploi de l'outil, et celui du recueil -// public, qui répond sur les actes publiés. Les prompts envoyés à ce moteur ne -// contiennent JAMAIS le contenu des actes — voir src/lib/assistant.js. Une -// collectivité peut brancher son propre moteur à la place (Administration › -// Assistants), et l'application s'en passe proprement quand il n'y en a pas. -generateText = {import:ai-text-plugin} diff --git a/scripts/generer-api.mjs b/scripts/generer-api.mjs index 86535f3..7d377d9 100644 --- a/scripts/generer-api.mjs +++ b/scripts/generer-api.mjs @@ -9,8 +9,7 @@ // opération, puis on régénère. // // La racine du code est CONSTATÉE, jamais supposée (`racine-code.mjs`) : le -// script s'exécute aussi bien depuis la racine du dépôt, où le code vit sous -// `src/`, que depuis l'atelier, où tout voisine sous `src/`. +// script s'exécute depuis la racine du dépôt, où le code vit sous `src/`. // // Il n'y a rien à installer : la description est un module pur, sans dépendance. // ============================================================================ diff --git a/scripts/generer-logiciel.mjs b/scripts/generer-logiciel.mjs index 4c84505..5363d25 100644 --- a/scripts/generer-logiciel.mjs +++ b/scripts/generer-logiciel.mjs @@ -17,8 +17,7 @@ // ENGENDRÉ, et l'épreuve `logiciel-engendre.test.mjs` refuse qu'il diverge. // // La racine du code est CONSTATÉE, jamais supposée (`racine-code.mjs`) : le -// script s'exécute aussi bien depuis la racine du dépôt, où le code vit sous -// `src/`, que depuis l'atelier, où tout voisine sous `src/`. +// script s'exécute depuis la racine du dépôt, où le code vit sous `src/`. // // Il n'y a rien à installer : les deux sources sont des modules purs. // ============================================================================ diff --git a/scripts/generer-variables.mjs b/scripts/generer-variables.mjs index 8866ddc..26bacd0 100644 --- a/scripts/generer-variables.mjs +++ b/scripts/generer-variables.mjs @@ -9,8 +9,7 @@ // descripteur, puis on régénère. // // La racine du code est CONSTATÉE, jamais supposée (`racine-code.mjs`) : le -// script s'exécute aussi bien depuis la racine du dépôt, où le code vit sous -// `src/`, que depuis l'atelier, où tout voisine sous `src/`. +// script s'exécute depuis la racine du dépôt, où le code vit sous `src/`. // // Il n'y a rien à installer : le registre est un module pur, sans dépendance. // ============================================================================ diff --git a/scripts/harnais-atelier.mjs b/scripts/harnais-atelier.mjs deleted file mode 100644 index 9209ea7..0000000 --- a/scripts/harnais-atelier.mjs +++ /dev/null @@ -1,633 +0,0 @@ -// ============================================================================ -// Harnais de l'ATELIER — éprouver le dépôt depuis l'atelier, sans terminal. -// -// POURQUOI CE FICHIER. L'atelier (Perchance) n'a ni système de fichiers ni -// processus : ni `node --check`, ni `npm test`, ni les générateurs n'y sont -// lançables tels quels. Ce harnais comble l'écart SANS DUPLIQUER UNE SEULE -// RÈGLE : il charge les scripts du dépôt EUX-MÊMES (`scripts/verifier-style.mjs`, -// `scripts/generer-*.mjs`, `tests/*.test.mjs`) et leur fournit des doublures de -// `node:fs`, `node:path`, `node:url`, `node:crypto` — les lectures et les -// écritures retombant dans `src/`. -// -// OÙ IL VIT. `scripts/harnais-atelier.mjs` : c'est de l'OUTILLAGE, à sa place -// attendue dans le dépôt livré. `npm test` ne le voit pas (il ne se nomme pas -// `*.test.mjs`) ; `npm run syntaxe` le parse comme un module — il doit donc -// rester un module valide, sans `return` au premier niveau. -// -// CE QU'ON LUI DEMANDE, ET CE QU'ON EN ATTEND. Les appels exacts, les chiffres -// attendus et les garde-fous sont dans `src/docs/ATELIER.md`. En résumé, depuis -// l'outil `execute_js` de l'agent : -// -// const src = await fs.readTextFile("src/scripts/harnais-atelier.mjs"); -// const h = await import(URL.createObjectURL(new Blob([src], { type: "text/javascript" }))); -// return h.texte(await h.verifier({ fs })); // syntaxe + style + épreuves -// return await h.regenerer({ fs }); // VARIABLES.md, API.md, logiciel-engendre.mjs -// return await h.epreuves({ fs, cibles: ["src/server/mysql/actes.test.mjs"] }); -// -// LES LIMITES, ÉCRITES POUR NE PAS SURPRENDRE (détail : `docs/ATELIER.md` § 6) : -// • pas de processus : `node:child_process` lève. La syntaxe est donc vérifiée -// par le parseur d'esbuild (`verifierSyntaxe`), pas par `node --check` — -// même service rendu, autre outil ; -// • les chemins sont VIRTUELS et ancrés à la racine du dépôt : la racine -// virtuelle (`/`) EST `src/` dans l'atelier, et `import.meta.url` est réécrit -// en `file:///…` pour que `new URL("..", import.meta.url)` calcule juste -// (`scripts/racine-code.mjs`) ; -// • une épreuve qui exige un vrai Node (spawnSync, mysql2, réseau) est SAUTÉE, -// jamais verte par accident. -// ============================================================================ - -const ESBUILD_URL = "https://esm.sh/esbuild-wasm@0.21.5?bundle"; -const ESBUILD_WASM = "https://esm.sh/esbuild-wasm@0.21.5/esbuild.wasm"; - -const __enc = new TextEncoder(); -const __dec = new TextDecoder(); -// LE DÉCODEUR BASE64 EST CELUI DE NODE, ET C'EST DÉLIBÉRÉ. `atob` est STRICT : -// il LÈVE sur un caractère hors alphabet, là où `Buffer.from(x, "base64")` -// l'IGNORE et rend ce qu'il peut. Un harnais plus sévère que Node rendait donc -// VERTE une épreuve que la CI voyait ROUGE (le scellé « illisible » de -// `comptes.test.mjs` : `atob` levait ici, Node décodait là-bas) — le pire des -// écarts, puisque l'atelier cachait la CI au lieu de l'annoncer. On décode comme -// Node : on retire les caractères hors alphabet, puis on lit les groupes de six -// bits. -const __B64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; -const __depuisBase64 = (x) => { - const s = String(x).replace(/[^A-Za-z0-9+/]/g, ""); - const u = new Uint8Array(Math.floor((s.length * 3) / 4)); - let bits = 0; let acc = 0; let k = 0; - for (const ch of s) { - acc = (acc << 6) | __B64.indexOf(ch); - bits += 6; - if (bits >= 8) { bits -= 8; u[k++] = (acc >> bits) & 0xff; } - } - return u.slice(0, k); -}; -const __toU8 = (x, e) => { - if (typeof x === "string") { - if (!e || e === "utf8" || e === "utf-8" || e === "ascii" || e === "latin1" || e === "binary") return __enc.encode(x); - if (e === "base64") return __depuisBase64(x); - if (e === "base64url") return __toU8(x.replace(/-/g, "+").replace(/_/g, "/"), "base64"); - if (e === "hex") { const u = new Uint8Array(Math.floor(x.length / 2)); for (let i = 0; i < u.length; i++) u[i] = parseInt(x.substr(i * 2, 2), 16); return u; } - return __enc.encode(x); - } - if (x instanceof ArrayBuffer) return new Uint8Array(x); - if (ArrayBuffer.isView(x)) return new Uint8Array(x.buffer, x.byteOffset, x.byteLength); - if (Array.isArray(x)) return new Uint8Array(x); - return __enc.encode(String(x)); -}; -const __decode = (u, e) => { - if (e === "hex") return Array.from(u, (x) => x.toString(16).padStart(2, "0")).join(""); - if (e === "base64" || e === "base64url") { let s = ""; for (const x of u) s += String.fromCharCode(x); let r = btoa(s); if (e === "base64url") r = r.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, ""); return r; } - if (e === "latin1" || e === "binary") { let s = ""; for (const x of u) s += String.fromCharCode(x); return s; } - return __dec.decode(u); -}; -const __bufferifier = (u) => { u.toString = function (ee) { return __decode(u, ee); }; return u; }; -globalThis.Buffer = { - from: (x, e) => __bufferifier(__toU8(x, e)), - alloc: (n) => __bufferifier(new Uint8Array(n)), - concat: (arr) => { let n = 0; for (const a of arr) n += a.length; const u = new Uint8Array(n); let o = 0; for (const a of arr) { u.set(a, o); o += a.length; } return __bufferifier(u); }, - isBuffer: () => false, -}; -globalThis.__SKIP = Symbol("skip"); -const SHIMS = {}; -SHIMS["node:test"] = ` -const __t = globalThis.__TESTS = globalThis.__TESTS || []; -function test(name, fn){ __t.push({ name, fn }); } -test.test = test; test.only = test; test.skip = () => {}; test.todo = () => {}; -test.before = () => {}; test.after = () => {}; test.beforeEach = () => {}; test.afterEach = () => {}; -test.describe = (n, f) => { try { f(); } catch (e) {} }; -test.it = test; -export default test; export { test }; -`; -SHIMS["node:assert/strict"] = ` -class AssertionError extends Error { constructor(m){ super(typeof m === "string" ? m : JSON.stringify(m)); this.name = "AssertionError"; } } -function deepEq(a, b){ - if (a === b) return true; - if (typeof a !== typeof b || a === null || b === null) return false; - if (typeof a !== "object") return false; - const ta = Object.prototype.toString.call(a); if (ta !== Object.prototype.toString.call(b)) return false; - if (ta === "[object Date]") return a.getTime() === b.getTime(); - if (ta === "[object RegExp]") return String(a) === String(b); - if (ArrayBuffer.isView(a) || a instanceof ArrayBuffer){ - const av = a instanceof ArrayBuffer ? new Uint8Array(a) : new Uint8Array(a.buffer, a.byteOffset, a.byteLength); - const bv = b instanceof ArrayBuffer ? new Uint8Array(b) : new Uint8Array(b.buffer, b.byteOffset, b.byteLength); - if (av.length !== bv.length) return false; - for (let i = 0; i < av.length; i++) if (av[i] !== bv[i]) return false; - return true; - } - if (Array.isArray(a)){ if (a.length !== b.length) return false; for (let i = 0; i < a.length; i++) if (!deepEq(a[i], b[i])) return false; return true; } - if (a instanceof Set){ if (a.size !== b.size) return false; for (const v of a) if (!b.has(v)) return false; return true; } - if (a instanceof Map){ if (a.size !== b.size) return false; for (const [k, v] of a) { if (!b.has(k) || !deepEq(b.get(k), v)) return false; } return true; } - const ka = Object.keys(a), kb = Object.keys(b); - if (ka.length !== kb.length) return false; - for (const k of ka){ if (!Object.prototype.hasOwnProperty.call(b, k)) return false; if (!deepEq(a[k], b[k])) return false; } - return true; -} -function fail(m){ throw new AssertionError(m || "Échec de l'assertion"); } -const montre = (v) => { try { return typeof v === "string" ? JSON.stringify(v) : String(v); } catch (e) { return String(v); } }; -const correspond = (err, atendu) => { - if (atendu === undefined || atendu === null) return true; - if (typeof atendu === "function" && atendu.prototype instanceof Error) return err instanceof atendu; - if (atendu instanceof RegExp) return atendu.test(String(err && err.message)); - if (typeof atendu === "object"){ for (const k of Object.keys(atendu)) { if (!(k in err) || !deepEq(err[k], atendu[k])) return false; } return true; } - return true; -}; -// Comme Node : un second argument CHAÎNE est le message de l'assertion, pas un critère. -const lireAtendu = (a, b) => (typeof a === "string" ? { atendu: undefined, message: a } : { atendu: a, message: typeof b === "string" ? b : undefined }); -const assert = (v, m) => { if (!v) fail(m || "La valeur attendue est vraie"); }; -assert.ok = assert; -assert.equal = (a, b, m) => { if (a != b) fail(m || ("attendu " + montre(b) + ", obtenu " + montre(a))); }; -assert.notEqual = (a, b, m) => { if (a == b) fail(m || ("ne devait pas valoir " + montre(b))); }; -assert.strictEqual = (a, b, m) => { if (a !== b) fail(m || ("attendu " + montre(b) + ", obtenu " + montre(a))); }; -assert.notStrictEqual = (a, b, m) => { if (a === b) fail(m || ("ne devait pas valoir " + montre(b))); }; -assert.deepEqual = assert.deepStrictEqual = (a, b, m) => { if (!deepEq(a, b)) fail(m || ("structures différentes : " + montre(a) + " != " + montre(b))); }; -assert.notDeepEqual = assert.notDeepStrictEqual = (a, b, m) => { if (deepEq(a, b)) fail(m || "les structures devaient différer"); }; -assert.match = (s, re, m) => { if (!re.test(String(s))) fail(m || ("ne correspond pas à " + re + " : " + montre(s))); }; -assert.doesNotMatch = (s, re, m) => { if (re.test(String(s))) fail(m || ("ne devait pas correspondre à " + re)); }; -assert.throws = (fn, a, b) => { const r = lireAtendu(a, b); let v; try { v = fn(); } catch (e) { if (correspond(e, r.atendu)) return e; fail(r.message || ("erreur inattendue : " + (e && e.message))); } fail(r.message || "aucune erreur levée"); }; -assert.doesNotThrow = (fn, m) => { try { fn(); } catch (e) { fail(typeof m === "string" ? m : ("erreur levée : " + (e && e.message))); } }; -assert.rejects = async (fn, a, b) => { const r = lireAtendu(a, b); try { await (typeof fn === "function" ? fn() : fn); } catch (e) { if (correspond(e, r.atendu)) return e; fail(r.message || ("rejet inattendu : " + (e && e.message))); } fail(r.message || "aucun rejet"); }; -assert.doesNotReject = async (fn, m) => { try { await (typeof fn === "function" ? fn() : fn); } catch (e) { fail(typeof m === "string" ? m : ("rejet inattendu : " + (e && e.message))); } }; -assert.fail = (m) => fail(typeof m === "string" ? m : (m && m.message)); -assert.ifError = (v) => { if (v) fail(String(v)); }; -export default assert; export { assert }; -`; -SHIMS["node:assert"] = SHIMS["node:assert/strict"]; -SHIMS["node:crypto"] = ` -const K = [0x428a2f98,0x71374491,0xb5c0fbcf,0xe9b5dba5,0x3956c25b,0x59f111f1,0x923f82a4,0xab1c5ed5,0xd807aa98,0x12835b01,0x243185be,0x550c7dc3,0x72be5d74,0x80deb1fe,0x9bdc06a7,0xc19bf174,0xe49b69c1,0xefbe4786,0x0fc19dc6,0x240ca1cc,0x2de92c6f,0x4a7484aa,0x5cb0a9dc,0x76f988da,0x983e5152,0xa831c66d,0xb00327c8,0xbf597fc7,0xc6e00bf3,0xd5a79147,0x06ca6351,0x14292967,0x27b70a85,0x2e1b2138,0x4d2c6dfc,0x53380d13,0x650a7354,0x766a0abb,0x81c2c92e,0x92722c85,0xa2bfe8a1,0xa81a664b,0xc24b8b70,0xc76c51a3,0xd192e819,0xd6990624,0xf40e3585,0x106aa070,0x19a4c116,0x1e376c08,0x2748774c,0x34b0bcb5,0x391c0cb3,0x4ed8aa4a,0x5b9cca4f,0x682e6ff3,0x748f82ee,0x78a5636f,0x84c87814,0x8cc70208,0x90befffa,0xa4506ceb,0xbef9a3f7,0xc67178f2]; -const rotr = (x, n) => ((x >>> n) | (x << (32 - n))) >>> 0; -function sha256(u8){ - const l = u8.length; - const total = (((l + 8) >> 6) + 1) << 6; - const msg = new Uint8Array(total); msg.set(u8); msg[l] = 0x80; - const dv = new DataView(msg.buffer); - const bits = l * 8; dv.setUint32(total - 8, Math.floor(bits / 4294967296)); dv.setUint32(total - 4, bits >>> 0); - const h = [0x6a09e667,0xbb67ae85,0x3c6ef372,0xa54ff53a,0x510e527f,0x9b05688c,0x1f83d9ab,0x5be0cd19]; - const w = new Uint32Array(64); - for (let i = 0; i < total; i += 64){ - for (let j = 0; j < 16; j++) w[j] = dv.getUint32(i + j * 4); - for (let j = 16; j < 64; j++){ const a = w[j-15], b = w[j-2]; w[j] = (w[j-16] + (rotr(a,7)^rotr(a,18)^(a>>>3)) + w[j-7] + (rotr(b,17)^rotr(b,19)^(b>>>10))) >>> 0; } - let [a,b,c,d,e,f,g,hh] = h; - for (let j = 0; j < 64; j++){ - const t1 = (hh + (rotr(e,6)^rotr(e,11)^rotr(e,25)) + ((e&f)^(~e&g)) + K[j] + w[j]) >>> 0; - const t2 = ((rotr(a,2)^rotr(a,13)^rotr(a,22)) + ((a&b)^(a&c)^(b&c))) >>> 0; - hh=g; g=f; f=e; e=(d+t1)>>>0; d=c; c=b; b=a; a=(t1+t2)>>>0; - } - h[0]=(h[0]+a)>>>0; h[1]=(h[1]+b)>>>0; h[2]=(h[2]+c)>>>0; h[3]=(h[3]+d)>>>0; h[4]=(h[4]+e)>>>0; h[5]=(h[5]+f)>>>0; h[6]=(h[6]+g)>>>0; h[7]=(h[7]+hh)>>>0; - } - const out = new Uint8Array(32); const od = new DataView(out.buffer); - for (let i = 0; i < 8; i++) od.setUint32(i * 4, h[i]); - return out; -} -const enc = (d) => typeof d === "string" ? new TextEncoder().encode(d) : (d instanceof Uint8Array ? d : new Uint8Array(d)); -export function createHash(algo){ - if (String(algo).replace("-","").toLowerCase() !== "sha256") throw new Error("shim : algorithme non pris en charge " + algo); - let buf = new Uint8Array(0); - return { - update(d){ const b = enc(d); const n = new Uint8Array(buf.length + b.length); n.set(buf); n.set(b, buf.length); buf = n; return this; }, - digest(e){ const h = sha256(buf); if (!e) return h; if (String(e).toLowerCase() === "hex") return Array.from(h, (x) => x.toString(16).padStart(2, "0")).join(""); if (String(e).toLowerCase() === "base64") return btoa(String.fromCharCode(...h)); return h; }, - copy(){ const c = createHash("sha256"); c.update(buf); return c; }, - }; -} -export const randomBytes = (n) => { const b = new Uint8Array(n); crypto.getRandomValues(b); return globalThis.Buffer.from(b); }; -export const randomUUID = () => crypto.randomUUID(); -export const randomFillSync = (b) => { crypto.getRandomValues(b); return b; }; -export function scryptSync(pw, salt, len){ let out = new Uint8Array(0); let bloc = enc(String(pw)).slice(); const s = enc(salt); for (let i = 0; i < 4 && out.length < len; i++){ const c = createHash("sha256"); c.update(bloc); c.update(s); c.update(String(i)); bloc = new Uint8Array(c.digest()); const n = new Uint8Array(out.length + bloc.length); n.set(out); n.set(bloc, out.length); out = n; } return out.slice(0, len); } -export function timingSafeEqual(a, b){ a = enc(a); b = enc(b); if (a.length !== b.length) throw new Error("longueurs différentes"); let d = 0; for (let i = 0; i < a.length; i++) d |= a[i] ^ b[i]; return d === 0; } -export const webcrypto = globalThis.crypto; -export const constants = {}; -export default { createHash, randomBytes, randomUUID, randomFillSync, scryptSync, timingSafeEqual, webcrypto, constants }; -`; -SHIMS["node:path"] = ` -export const join = (...p) => p.filter((x) => x !== "").join("/").replace(/\\/+/g, "/"); -export const resolve = (...p) => join(...p); -export const dirname = (p) => String(p).replace(/\\/[^/]*$/, "") || "."; -export const basename = (p) => String(p).replace(/.*\\//, ""); -export const extname = (p) => { const m = /\\.[^./]*$/.exec(String(p)); return m ? m[0] : ""; }; -export const relative = (a, b) => String(b).replace(String(a).replace(/\\/$/, "") + "/", ""); -export const sep = "/"; -export default { join, resolve, dirname, basename, extname, relative, sep }; -`; -SHIMS["node:url"] = ` -export const fileURLToPath = (u) => String(u).replace(/^file:\\/\\//, ""); -export const pathToFileURL = (p) => ({ href: "file://" + p }); -export default { fileURLToPath, pathToFileURL }; -`; -SHIMS["node:fs/promises"] = ` -const reels = new Set(globalThis.__SNAPSHOT || []); -const P = (p) => String(p).replace(/^file:/, "").replace(/^[/]+/, "").replace(/^[.][/]/, ""); -const reel = (p) => { const s = P(p); if (reels.has(s)) return s; if (reels.has("src/" + s)) return "src/" + s; return s; }; -// Un DOSSIER ne se résout pas comme un fichier : les dossiers ne figurent pas -// dans l'inventaire des fichiers, et la racine virtuelle est src/ — l'arbre que -// la plateforme conserve, équivalent de la racine du dépôt livré. scratch/ et -// imports/ voisinent à côté, mais ne font pas partie du dépôt. -const reelDossier = (p) => { - const s = P(p).replace(/[/]+$/, ""); - if (s === "") return "src"; - if (s === "src" || s.startsWith("src/")) return s; - return "src/" + s; -}; -const enfants = (p) => { - const dir = reelDossier(p); - const prefixe = dir + "/"; - const vus = new Set(); const liste = []; - for (const f of reels) { - if (prefixe && !f.startsWith(prefixe)) continue; - const reste = f.slice(prefixe.length); - if (!reste) continue; - const segment = reste.split("/")[0]; - const estDossier = reste.includes("/"); - if (vus.has(segment)) continue; - vus.add(segment); - liste.push({ name: segment, isDirectory: () => estDossier, isFile: () => !estDossier }); - } - return liste; -}; -export const readFile = async (p, e) => { - const c = reel(p); - return (e === undefined || e === null || /^utf-?8$/i.test(e) || e === "ascii" || e === "latin1" || e === "binary") - ? globalThis.__FS.readTextFile(c) : globalThis.__FS.readFile(c); -}; -export const writeFile = async (p, d) => globalThis.__FS.writeTextFile(reel(p), typeof d === "string" ? d : new TextDecoder().decode(d)); -export const mkdir = async () => {}; -export const rm = async () => {}; -export const stat = async (p) => ({ isFile: () => reels.has(reel(p)), isDirectory: () => !reels.has(reel(p)), size: 0 }); -export const readdir = async (p, o) => { const l = enfants(p); return o && o.withFileTypes ? l : l.map((x) => x.name); }; -export const access = async (p) => { if (!reels.has(reel(p)) && !enfants(p).length) { const err = new Error("ENOENT " + p); err.code = "ENOENT"; throw err; } }; -export default { readFile, writeFile, mkdir, rm, stat, readdir, access }; -`; -SHIMS["node:fs"] = ` -const reels = new Set(globalThis.__SNAPSHOT || []); -const P = (p) => String(p).replace(/^file:/, "").replace(/^[/]+/, "").replace(/^[.][/]/, ""); -const reel = (p) => { const s = P(p); if (reels.has(s)) return s; if (reels.has("src/" + s)) return "src/" + s; return s; }; -export const existsSync = (p) => reels.has(reel(p)); -export const readFileSync = () => { throw new Error("shim : readFileSync indisponible (l'atelier n'a pas de lecture synchrone)"); }; -export const mkdirSync = () => {}; -export default { existsSync, readFileSync, mkdirSync }; -`; -SHIMS["node:child_process"] = ` -export const spawnSync = () => { throw new Error("shim : spawnSync indisponible (pas de node)"); }; -export const execSync = () => { throw new Error("shim : execSync indisponible"); }; -export default { spawnSync, execSync }; -`; -SHIMS["mysql2/promise"] = ` -const aucun = () => { throw new Error("shim : mysql2 indisponible"); }; -export const createPool = aucun; -export const createConnection = aucun; -export default { createPool, createConnection }; -`; -SHIMS["mysql2"] = SHIMS["mysql2/promise"]; -for (const m of ["node:net", "node:tls", "node:http", "node:https", "node:zlib", "node:stream", "node:events", "node:os", "node:util", "node:async_hooks"]) { - SHIMS[m] = `const aucun = () => { throw new Error("shim : module natif indisponible"); }; export default {}; export const createServer = aucun; export const connect = aucun;`; -} -// --- Les chemins : un seul arbre, deux noms -------------------------------- -// Le dépôt livré range le code sous `src/` et l'outillage à sa racine ; dans -// l'atelier, TOUT vit sous `src/`. Le harnais prend la seconde disposition pour -// la première : la racine virtuelle EST `src/`. Un chemin virtuel -// « scripts/x.mjs » se lit donc `src/scripts/x.mjs` sur le disque de l'atelier, -// et un chemin d'épreuve écrit pour le dépôt (`../src/lib/…`) se résout tout seul. - -let __snapshot = new Set(); -const __modules = new Map(); - -const normaliser = (p) => { - const parts = []; - for (const segment of String(p).split("/")) { - if (segment === "" || segment === ".") continue; - if (segment === "..") parts.pop(); - else parts.push(segment); - } - return parts.join("/"); -}; - -const sansProtocole = (p) => normaliser(String(p).replace(/^file:/, "")); - -const reel = (v) => { - const s = sansProtocole(v); - if (__snapshot.has(s)) return s; - if (__snapshot.has("src/" + s)) return "src/" + s; - return s; -}; - -const virtuel = (r) => { - const s = normaliser(r); - return s.startsWith("src/") ? s.slice(4) : s; -}; - -const message = (e) => String((e && e.stack) || e).split("\n").slice(0, 6).join(" | "); - -async function preparer(fs) { - const liste = await fs.listFiles(); - __snapshot = new Set(liste.filter((f) => f.writable !== false).map((f) => f.path)); - __modules.clear(); - globalThis.__FS = fs; - globalThis.__SNAPSHOT = [...__snapshot]; -} - -// --- esbuild, chargé une seule fois par exécution -------------------------- -let __esbuild = null; -async function esbuildPret() { - if (!__esbuild) __esbuild = (async () => { - const { default: esbuild } = await import(ESBUILD_URL); - await esbuild.initialize({ wasmURL: ESBUILD_WASM }); - return esbuild; - })(); - return __esbuild; -} - -// --- Le montage : doublures, résolution virtuelle, import.meta.url --------- -async function monter(fs) { - await preparer(fs); - const esbuild = await esbuildPret(); - const plugin = { - name: "atelier", - setup(build) { - build.onResolve({ filter: /^node:/ }, (a) => (SHIMS[a.path] ? { path: a.path, namespace: "doublure" } : null)); - build.onResolve({ filter: /^mysql2([/].*)?$/ }, (a) => (SHIMS[a.path] ? { path: a.path, namespace: "doublure" } : null)); - build.onResolve({ filter: /^[.][.]?[/]/ }, (a) => { - if (a.namespace === "doublure") return null; - const dossier = virtuel(a.importer).replace(/[/][^/]*$/, ""); - return { path: reel(normaliser(dossier + "/" + a.path)), namespace: "atelier" }; - }); - build.onLoad({ filter: /.*/, namespace: "doublure" }, (a) => ({ contents: SHIMS[a.path], loader: "js", resolveDir: "/" })); - build.onLoad({ filter: /.*/, namespace: "atelier" }, async (a) => ({ - contents: (await fs.readTextFile(a.path)).replace(/import[.]meta[.]url/g, JSON.stringify("file:///" + virtuel(a.path))), - loader: "js", - resolveDir: "/", - })); - }, - }; - // Les épreuves chargent parfois un module par `await import("…")` DYNAMIQUE - // (une chaîne dans une variable) : esbuild ne peut pas l'inliner. On le - // détourne vers ce résolveur, qui rejoue la résolution virtuelle. - // - // LE REGISTRE PARTAGÉ, ET POURQUOI. `node --test` exécute UN FICHIER par - // PROCESSUS : dans un fichier, tous les `import` rendent le MÊME module (donc - // le même état). Le harnais, lui, construit un bundle par import dynamique : - // `charger("../src/lib/store.js")` et `charger("../src/lib/auth.js")` donnaient - // donc DEUX copies d'`auth.js`, et un test qui règle l'une ne se voyait pas - // dans l'autre — des échecs qui n'existaient que dans l'atelier (voir - // docs/ATELIER.md § 3.3). Le registre répare cela : `uneEpreuve` fait - // importer D'AVANCE, dans le bundle du fichier, tous les modules que la source - // cite en clair (ils sont alors partagés, puisque esbuild les inline), et le - // résolveur les sert ici au lieu de reconstruire un second bundle. - const registre = new Map(); - globalThis.__dynEnregistrer = (cle, ns) => { registre.set(cle, ns); }; - // Un registre vit le temps d'UN fichier d'épreuve : deux fichiers ne partagent - // jamais un module, exactement comme deux processus. - globalThis.__dynVider = () => { registre.clear(); __modules.clear(); }; - globalThis.__makeDyn = (dossierVirtuel) => async (spec) => { - const s = String(spec); - const vp = s.startsWith(".") ? normaliser(dossierVirtuel + "/" + s) : sansProtocole(s); - const candidats = [reel(vp), vp, "src/" + vp]; - for (const candidat of candidats) { - if (registre.has(candidat)) return registre.get(candidat); - } - let derniere = null; - for (const candidat of candidats) { - try { await fs.readTextFile(candidat); return await importer(fs, esbuild, plugin, candidat); } - catch (e) { derniere = e; } - } - throw derniere || new Error("harnais : module introuvable « " + s + " »"); - }; - return { esbuild, plugin }; -} - -// `esnext` et non `es2020` : le dépôt est servi TEL QUEL (pas de compilation), -// il a donc droit à ce que Node 20 et les navigateurs savent faire — les -// fichiers de l'atelier emploient l'`await` de premier niveau. -const OPTIONS = { bundle: true, format: "esm", write: false, platform: "neutral", target: "esnext", logLevel: "silent" }; - -const prelude = (cheminReel, code) => - "const __dyn = globalThis.__makeDyn(" + JSON.stringify(virtuel(cheminReel).replace(/[/][^/]*$/, "")) + ");\n" - + code.replace(/import\s*\(/g, "__dyn("); - -// Le « shebang » des scripts livrés (`#!/usr/bin/env node`) est mis en -// commentaire : esbuild le remonterait en tête du bundle, AVANT notre prélude — -// et un `#!` au milieu d'un module est une faute de syntaxe. -const sansShebang = (texte) => String(texte).replace(/^#!/, "//"); - -const construire = (fs, esbuild, plugin, cheminReel, contenu) => - esbuild.build({ - ...OPTIONS, - plugins: [plugin], - stdin: { - // L'ENTRÉE AUSSI PORTE SON ADRESSE. Le greffon ne réécrit `import.meta.url` - // que dans les modules IMPORTÉS : sans ce remplacement, l'entrée gardait - // l'adresse de son module-bloc (`blob:…`), qui n'est pas hiérarchique — - // `new URL("..", import.meta.url)` LEVAIT, et toute épreuve qui ancre un - // chemin à SON fichier (la vérification des modèles de `.env`, la - // disposition livrée…) se SAUTAIT au lieu de se jouer. Le remplacement - // faisait pourtant partie du contrat du harnais (voir en tête de fichier) : - // c'était un écran de fumée, et le plus trompeur — un saut annoncé là où la - // chaîne, elle, joue l'épreuve. - contents: sansShebang(contenu).replace(/import[.]meta[.]url/g, JSON.stringify("file:///" + virtuel(cheminReel))), - sourcefile: virtuel(cheminReel), - resolveDir: "/", - }, - }); - -async function importer(fs, esbuild, plugin, cheminReel) { - if (__modules.has(cheminReel)) return __modules.get(cheminReel); - const promesse = (async () => { - const res = await construire(fs, esbuild, plugin, cheminReel, await fs.readTextFile(cheminReel)); - return import(URL.createObjectURL(new Blob([prelude(cheminReel, res.outputFiles[0].text)], { type: "text/javascript" }))); - })(); - __modules.set(cheminReel, promesse); - return promesse; -} - -// --- Les trois services du harnais ---------------------------------------- - -// 1. La syntaxe : le parseur d'esbuild tient lieu de `node --check`. -export async function verifierSyntaxe({ fs } = {}) { - await preparer(fs); - const esbuild = await esbuildPret(); - const liste = [...__snapshot].filter((p) => p.startsWith("src/") && /[.](m?js)$/.test(p)).sort(); - const fautes = []; - for (const p of liste) { - try { await esbuild.transform(await fs.readTextFile(p), { loader: "js" }); } - catch (e) { fautes.push({ fichier: p, erreur: message(e) }); } - } - return { controles: liste.length, fautes }; -} - -// 2. Le style : le script LIVRÉ, exécuté tel quel (`--strict` par défaut). -export async function verifierStyle({ fs, strict = true } = {}) { - return lancerScript({ fs, script: "src/scripts/verifier-style.mjs", argv: strict ? ["--strict"] : [] }); -} - -// 3. Les épreuves : un fichier par construction, comme `node --test` les isole. -export async function epreuves({ fs, cibles = null } = {}) { - const { esbuild, plugin } = await monter(fs); - const liste = cibles && cibles.length ? cibles.map(reel) : decouvrir(); - const resultats = []; - for (const fichier of liste) resultats.push(await uneEpreuve(fs, esbuild, plugin, fichier)); - const totalOk = resultats.reduce((s, r) => s + (r.ok || 0), 0); - const totalTests = resultats.reduce((s, r) => s + (r.total || 0), 0); - return { totalOk, totalTests, resultats }; -} - -const decouvrir = () => [...__snapshot] - .filter((p) => /[.]test[.]mjs$/.test(p) && (p.startsWith("src/tests/") || p.startsWith("src/server/"))) - .sort(); - -// Les MODULES que la source cite EN CLAIR (« ../src/lib/…js », « ./actes.mjs ») : -// ce sont eux que l'épreuve chargera. Les faire entrer dans SON bundle, c'est -// garantir qu'un module importé deux fois est le MÊME — la règle de `node --test` -// à l'intérieur d'un fichier. On ne retient que les chemins qui EXISTENT et qui -// portent une extension de MODULE : la source cite aussi des fichiers qu'elle -// LIT (`index.html`, `env.example`), et les importer ferait échouer la -// construction. -async function modulesPartages(fs, dossierVirtuel, source) { - const trouves = new Map(); - for (const m of String(source).matchAll(/["']([.][.]?\/[^"'\n]*)["']/g)) { - const spec = m[1]; - if (!/[.](m?js)$/.test(spec)) continue; - if (trouves.has(spec)) continue; - const vp = normaliser(dossierVirtuel + "/" + spec); - for (const cle of [reel(vp), vp, "src/" + vp]) { - try { await fs.readTextFile(cle); trouves.set(spec, cle); break; } - catch (e) { /* chemin suivant */ } - } - } - return [...trouves].map(([spec, cle]) => ({ spec, cle })); -} - -// Ce qui les enregistre est ajouté À LA SOURCE, et non au bundle : c'est esbuild -// qui doit voir ces `import` pour les INLINER (un import dynamique à littéral -// est inliné dès lors qu'on ne découpe pas le bundle). Chaque entrée est -// protégée : un module qui refuse de se charger hors navigateur reste -// « introuvable » pour l'épreuve, comme avant — il ne fait pas tomber la -// construction. -const codePartage = (liste) => liste - .map(({ spec, cle }) => "try { globalThis.__dynEnregistrer(" + JSON.stringify(cle) - + ", await import(" + JSON.stringify(spec) + ")); } catch (e) { /* non partageable */ }") - .join("\n"); - -async function uneEpreuve(fs, esbuild, plugin, fichier) { - globalThis.__TESTS = []; - // UN FICHIER = UN PROCESSUS, comme sous `node --test` : rien de ce qu'un - // fichier a chargé ne doit survivre au suivant (voir `__dynVider`). - if (globalThis.__dynVider) globalThis.__dynVider(); - let code; - try { - const source = await fs.readTextFile(fichier); - const partages = await modulesPartages(fs, virtuel(fichier).replace(/[/][^/]*$/, ""), source); - const res = await construire(fs, esbuild, plugin, fichier, source + "\n" + codePartage(partages)); - code = prelude(fichier, res.outputFiles[0].text); - } catch (e) { - return { fichier, erreur: "construction : " + message(e), ok: 0, total: 0, sautes: 0, echecs: [] }; - } - try { await import(URL.createObjectURL(new Blob([code], { type: "text/javascript" }))); } - catch (e) { return { fichier, erreur: "import : " + message(e), ok: 0, total: 0, sautes: 0, echecs: [] }; } - const liste = globalThis.__TESTS; - let ok = 0; let sautes = 0; const echecs = []; - for (const t of liste) { - const faux = { test: (n, f) => f(), diagnostic: () => {}, plan: () => {}, end: () => {}, skip: () => { throw globalThis.__SKIP; } }; - try { await t.fn(faux); ok++; } - catch (e) { - if (e === globalThis.__SKIP) { sautes++; continue; } - const piste = String((e && e.stack) || "").split("\n").find((l) => /\d+:\d+/.test(l) && !/harnais/.test(l)); - echecs.push(t.name + " ==> " + String((e && e.message) || e) + (piste ? " [" + piste.trim() + "]" : "")); - } - } - return { fichier, total: liste.length, ok, sautes, echecs }; -} - -// 4. Un script du dépôt, exécuté tel quel (les générateurs, les contrôles…). -export async function lancerScript({ fs, script, argv = [] } = {}) { - const { esbuild, plugin } = await monter(fs); - const chemin = reel(script); - let sortie = ""; - const ajouter = (...a) => { sortie += a.map((x) => (typeof x === "string" ? x : String(x))).join(" ") + "\n"; }; - const vraiConsole = globalThis.console; - const vraiProcess = globalThis.process; - globalThis.console = { log: ajouter, warn: ajouter, error: ajouter, info: ajouter, debug: ajouter }; - globalThis.process = { - argv: ["/usr/bin/node", virtuel(chemin), ...argv], - execPath: "/usr/bin/node", - env: {}, - platform: "linux", - version: "v20.0.0", - versions: { node: "20.0.0" }, - cwd: () => "/", - exit: (code = 0) => { const e = new Error("sortie " + code); e.__sortie = code; throw e; }, - }; - try { - let code; - try { - const res = await construire(fs, esbuild, plugin, chemin, await fs.readTextFile(chemin)); - code = prelude(chemin, res.outputFiles[0].text); - } catch (e) { return { script: chemin, ok: false, code: 1, sortie, erreur: "construction : " + message(e) }; } - try { - await import(URL.createObjectURL(new Blob([code], { type: "text/javascript" }))); - return { script: chemin, ok: true, code: 0, sortie }; - } catch (e) { - if (e && typeof e.__sortie === "number") return { script: chemin, ok: e.__sortie === 0, code: e.__sortie, sortie }; - return { script: chemin, ok: false, code: 1, sortie, erreur: "exécution : " + message(e) }; - } - } finally { - globalThis.console = vraiConsole; - globalThis.process = vraiProcess; - } -} - -// 5. Les trois documents engendrés, par leurs générateurs LIVRÉS. -export async function regenerer({ fs } = {}) { - const scripts = ["src/scripts/generer-variables.mjs", "src/scripts/generer-api.mjs", "src/scripts/generer-logiciel.mjs"]; - const resultats = []; - for (const script of scripts) resultats.push(await lancerScript({ fs, script })); - return { ok: resultats.every((r) => r.ok), resultats }; -} - -// 6. Tout, dans l'ordre de `npm run verifier` : syntaxe, style, épreuves. -export async function verifier({ fs, epreuvesAussi = true } = {}) { - const syntaxe = await verifierSyntaxe({ fs }); - const style = await verifierStyle({ fs }); - const rapport = { syntaxe, style, ok: syntaxe.fautes.length === 0 && style.code === 0 }; - if (epreuvesAussi) { - rapport.epreuves = await epreuves({ fs }); - // UN SAUT N'EST PAS UN ÉCHEC. Les épreuves que le navigateur ne peut pas - // jouer — il n'y a pas de processus ici, ni de `node:crypto` complet, ni - // d'installation réelle à comparer — s'appellent elles-mêmes `t.skip`. - // Les compter comme des échecs rendrait le verdict faux, et ferait chasser - // un fantôme à chaque tour (voir src/docs/ATELIER.md § 3.3). - rapport.sauts = rapport.epreuves.resultats.reduce((n, r) => n + (r.sautes || 0), 0); - rapport.echecs = rapport.epreuves.resultats.reduce((n, r) => n + (r.echecs ? r.echecs.length : 0) + (r.erreur ? 1 : 0), 0); - rapport.ok = rapport.ok && rapport.echecs === 0; - } - return rapport; -} - -const derniereLigne = (s) => String(s || "").trim().split("\n").filter(Boolean).slice(-1)[0] || ""; - -// Un verdict court, fait pour être RENDU tel quel par l'outil de l'agent. -// Il distingue l'ÉCHEC (une régression : quelque chose qui marchait ne marche -// plus) du SAUT (une épreuve que le navigateur ne peut pas jouer — pas de -// processus, pas de `node:crypto` complet, pas d'installation réelle). Le second -// est un fait d'environnement, listé dans `docs/ATELIER.md` § 3.3 ; le confondre -// avec le premier est le PLUS GRAND risque de l'atelier — chasser un fantôme. -export function texte(rapport) { - const lignes = []; - lignes.push("Syntaxe : " + rapport.syntaxe.controles + " fichier(s), " + rapport.syntaxe.fautes.length + " faute(s)."); - for (const f of rapport.syntaxe.fautes.slice(0, 8)) lignes.push(" - " + f.fichier + " :: " + f.erreur); - lignes.push("Style : code " + rapport.style.code + (rapport.style.erreur ? " (" + rapport.style.erreur + ")" : "") + " — " + derniereLigne(rapport.style.sortie)); - if (rapport.epreuves) { - const echecs = rapport.epreuves.resultats.reduce((n, r) => n + (r.echecs ? r.echecs.length : 0) + (r.erreur ? 1 : 0), 0); - const sautes = rapport.epreuves.resultats.reduce((n, r) => n + (r.sautes || 0), 0); - lignes.push("Épreuves : " + rapport.epreuves.totalOk + "/" + rapport.epreuves.totalTests - + " — " + echecs + " échec(s) et " + sautes + " saut(s). Un saut n'est pas un échec (voir src/docs/ATELIER.md § 3.3) ; un échec, lui, est une régression."); - for (const r of rapport.epreuves.resultats) { - if (!r.erreur && r.ok === r.total && !r.echecs.length) continue; // les fichiers verts n'encombrent pas - const apercu = (r.echecs || []).slice(0, 2).map((e) => (e.length > 160 ? e.slice(0, 160) + "…" : e)); - lignes.push(" " + (r.erreur ? "ERREUR " : "") + r.fichier + " " + (r.erreur || (r.ok + "/" + r.total) + (r.sautes ? " (+" + r.sautes + " sautés)" : "")) + (apercu.length ? " " + JSON.stringify(apercu) : "")); - } - } - const faute = rapport.syntaxe.fautes.length > 0 || rapport.style.code !== 0; - lignes.push("Verdict : " + (faute - ? "À REVOIR — corriger la syntaxe ou le style ci-dessus" - : rapport.ok - ? "tout est vert" + (rapport.sauts ? " (" + rapport.sauts + " saut(s) d'environnement)" : "") - : "À REVOIR — les épreuves ci-dessus portent un échec")); - return lignes.join("\n"); -} diff --git a/scripts/racine-code.mjs b/scripts/racine-code.mjs index 110f973..eed5ccd 100644 --- a/scripts/racine-code.mjs +++ b/scripts/racine-code.mjs @@ -2,29 +2,21 @@ // Où vit le code de l'application — pour l'outillage qui doit le lire. // // POURQUOI CE MODULE. Le dépôt range l'outillage à sa RACINE (`scripts/`, -// `tests/`) et le code de l'application sous `src/` — c'est la disposition -// livrée. Dans l'atelier, tout voisine sous `src/` (le seul arbre que la -// plateforme conserve d'une séance à l'autre) : `src/scripts/`, `src/tests/`, -// `src/lib/`. Un script ne peut donc pas déduire la racine du code de sa propre -// place : il la CONSTATE — c'est le dossier qui porte `lib/version.js`. +// `tests/`) et le code de l'application sous `src/`. Un script ne peut pas +// déduire la racine du code de sa propre place : il la CONSTATE — c'est le +// dossier qui porte `lib/version.js`. // -// Les scripts qui l'emploient lisent ainsi le code dans les deux dispositions, -// sans variable d'environnement ni option à retenir. C'est le même raisonnement -// que celui du contrôle de style, où un décalage entre l'outil et la disposition -// qu'il contrôle avait éteint toutes ses exemptions (audit, NC-I-009). +// Les scripts qui l'emploient lisent ainsi le code sans variable +// d'environnement ni option à retenir. C'est le même raisonnement que celui du +// contrôle de style, où un décalage entre l'outil et l'arborescence qu'il +// contrôle avait éteint toutes ses exemptions (audit, NC-I-009). // ============================================================================ -import { existsSync } from "node:fs"; import { join } from "node:path"; import { fileURLToPath } from "node:url"; // Le dossier qui porte l'outillage : `scripts/` est l'un de ses enfants, et -// `tests/` l'autre. C'est la racine du dépôt dans la disposition livrée, et -// `src/` dans l'atelier. +// `tests/` l'autre — c'est la racine du dépôt. export const RACINE_OUTILLAGE = fileURLToPath(new URL("..", import.meta.url)); // Le dossier qui porte `lib/`, `ui/`, `pages/`, `server/` — donc tout le code. -export const RACINE_CODE = existsSync(join(RACINE_OUTILLAGE, "lib", "version.js")) - ? RACINE_OUTILLAGE - : existsSync(join(RACINE_OUTILLAGE, "src", "lib", "version.js")) - ? join(RACINE_OUTILLAGE, "src") - : RACINE_OUTILLAGE; +export const RACINE_CODE = join(RACINE_OUTILLAGE, "src"); diff --git a/scripts/verifier-style.mjs b/scripts/verifier-style.mjs index 7a09856..6d0c747 100644 --- a/scripts/verifier-style.mjs +++ b/scripts/verifier-style.mjs @@ -18,12 +18,11 @@ // L'import jamais employé, lui, se signale PARTOUT : il ne dépend ni du // rôle du fichier, ni de son style. // -// CE QU'ELLE PARCOURT. Le dossier qui porte l'outillage — la racine du dépôt -// livré, où vivent `scripts/`, `tests/` et `src/` ; tout l'arbre de l'atelier, -// où tout voisine sous `src/`. Les exemptions, elles, sont écrites relativement -// à la RACINE DU CODE (`racine-code.mjs`) : c'est ce qui les rend justes dans -// les deux dispositions — un décalage entre l'outil et la disposition qu'il -// contrôle avait éteint toutes les exemptions (audit, NC-I-009). +// CE QU'ELLE PARCOURT. La racine du dépôt — l'outillage (`scripts/`, `tests/`) +// comme le code de l'application (`src/`). Les exemptions, elles, sont écrites +// relativement à la RACINE DU CODE (`racine-code.mjs`) — un décalage entre +// l'outil et l'arborescence qu'il contrôle éteindrait toutes les exemptions +// (audit, NC-I-009). // // USAGE // npm run lint avertissements tolérés (CI douce) @@ -32,15 +31,15 @@ // Voir docs/INDUSTRIALISATION.md et l'audit (proposition P-30). // ============================================================================ import { readdir, readFile } from "node:fs/promises"; -import { join, relative, sep } from "node:path"; -import { RACINE_CODE, RACINE_OUTILLAGE } from "./racine-code.mjs"; +import { join, sep } from "node:path"; +import { RACINE_OUTILLAGE } from "./racine-code.mjs"; import { importsInutilises } from "./analyse-imports.mjs"; const strict = process.argv.includes("--strict"); // Les fichiers volontairement exemptés, et pourquoi. Les chemins sont écrits // RELATIVEMENT AU CODE (voir `cheminDuCode` plus bas) : `scripts/…` et -// `tests/…` n'y figurent que pour mémoire, l'outillage n'étant jamais dans +// `tests/…` n'y figurent que pour la lecture, l'outillage n'étant jamais dans // l'arbre du code. const EXEMPTIONS = [ { test: (p) => p.startsWith("scripts" + sep) || p.startsWith("tests" + sep), raison: "outillage" }, @@ -52,7 +51,7 @@ const EXEMPTIONS = [ const exempt = (p) => EXEMPTIONS.find((e) => e.test(p)); // L'outillage, au sens du parcours : son PREMIER segment de chemin, qui est -// `scripts` ou `tests` dans les deux dispositions (voir ci-dessus). +// `scripts` ou `tests`. const estOutillage = (rel) => { const premier = rel.split(sep)[0]; return premier === "scripts" || premier === "tests"; @@ -62,11 +61,7 @@ const estOutillage = (rel) => { // l'outillage (hors du code) garde son chemin tel quel : il ne peut donc // correspondre à aucune exemption, ce qui est le sens voulu (« scripts/… » et // « tests/… » ne sont là que pour la lecture). -const prefixeCode = relative(RACINE_OUTILLAGE, RACINE_CODE); -const cheminDuCode = (rel) => { - if (!prefixeCode) return rel; - return rel.startsWith(prefixeCode + sep) ? rel.slice(prefixeCode.length + 1) : rel; -}; +const cheminDuCode = (rel) => rel.startsWith("src" + sep) ? rel.slice(4) : rel; async function fichiers(dir, out = []) { for (const e of await readdir(join(RACINE_OUTILLAGE, dir), { withFileTypes: true })) { diff --git a/scripts/verifier-syntaxe.mjs b/scripts/verifier-syntaxe.mjs index 737242a..db4d657 100644 --- a/scripts/verifier-syntaxe.mjs +++ b/scripts/verifier-syntaxe.mjs @@ -7,10 +7,9 @@ // blanc. Ce contrôle attrape donc la faute de frappe, la parenthèse fermante // manquante, la virgule de trop, la chaîne non terminée. // -// CE QU'IL PARCOURT. Le dossier QUI PORTE l'outillage (`scripts/` et `tests/`, -// à la racine du dépôt livré ; tout l'arbre de l'atelier, où tout voisine sous -// `src/`) : le code de l'application est donc vu dans les deux dispositions, -// sans chemin à tenir à jour. Sont écartés `node_modules`, les dossiers +// CE QU'IL PARCOURT. La racine du dépôt — l'outillage (`scripts/`, `tests/`) +// comme le code de l'application (`src/`) — sans chemin à tenir à jour. Sont +// écartés `node_modules`, les dossiers // commençants par un point (`.git`, `.github`) et `data/` — le rangement par // fichiers d'un service auto-hébergé (voir docs/ADMINISTRATION.md), qui ne // contient pas de JavaScript. diff --git a/src/CHANGELOG.md b/src/CHANGELOG.md index 4802c3e..a0f684b 100644 --- a/src/CHANGELOG.md +++ b/src/CHANGELOG.md @@ -23,6 +23,44 @@ numéros `MAJEUR.MINEUR.CORRECTIF` ([semver](https://semver.org/lang/fr/)). - On décrit le **changement visible** (ce que l'utilisateur constate, ou ce que l'exploitant doit savoir), pas la liste des fichiers touchés. +## [1.6.3w] — 2026-10-24 — Le legs Perchance est supprimé + +**Retiré** + +- **L'atelier en ligne (Perchance) n'a plus aucune trace dans le dépôt.** Le + manifeste `main.pjs`, le harnais de vérification sans terminal + (`scripts/harnais-atelier.mjs`) et la méthode de l'atelier + (`src/docs/ATELIER.md`) sont supprimés ; l'écran « Documentation technique » + n'en propose plus l'entrée, et les commentaires de l'outillage, de la chaîne + et des épreuves ne parlent plus de la double disposition. Le prompt d'audit + (document vivant) est mis à jour : sa cible est la démonstration statique. +- **Les chemins morts disparaissent des documents** : plus de référence à + `main.pjs`, à `docs/GITHUB.md` ou à l'ancienne arborescence `src/tests/` / + `src/scripts/` — les chemins cités sont ceux du dépôt. + +**Éprouvé** : 256 fichiers sans faute de syntaxe ni de style, **152 épreuves** +(0 échec, 2 sauts d'environnement), **333/333** pour le service, le miroir +engendré conforme (4/4). + + +## [1.6.3v] — 2026-10-24 — La doc agent dit le nouveau monde : le dépôt est la source unique + +**Modifié** + +- **Les consignes aux agents disent le dépôt, et seulement le dépôt.** Le flux de + travail « Perchance ↔ GitHub », les recettes d'export et la double disposition + des fichiers disparaissent : on développe dans le dépôt sur `dev`, une branche + par sujet, une PR par sujet vers `dev` avec la chaîne verte avant relecture, et + chaque livraison laisse une trace — une entrée datée au changelog et + `APP_VERSION` à jour. + +**Retiré** + +- **L'atelier en ligne (Perchance) et son harnais de vérification sans Node ne + figurent plus dans la doc agent.** Le mot « atelier » reste pour l'écran interne + de l'application, inchangé. + + ## [1.6.3u] — 2026-10-01 — L'acte modificatif se referme comme l'acte modifié Demande : « quand un acte a été modifié, la vue en version consolidée est complètement cassée diff --git a/src/LICENSE.md b/src/LICENSE.md index 8be2027..5144362 100644 --- a/src/LICENSE.md +++ b/src/LICENSE.md @@ -1,17 +1,15 @@ # Licence — Scribae Ce document **tranche par écrit** ce que la documentation laissait divergent : la -page publique du dépôt annonçait « tous droits réservés » (`src/docs/GITHUB.md`, -section « Licence ») alors que le dépôt porte un fichier **`LICENSE` en -GPL-3.0**, exclu de l'archive d'export (voir `src/README.md`, « Exporter le dépôt -GitHub »). Il distingue trois objets, qui n'ont ni le même auteur ni le même -régime. +page d'accueil du dépôt annonçait « tous droits réservés » alors que le dépôt +porte un fichier **`LICENSE` en GPL-3.0**. Il distingue trois objets, qui n'ont +ni le même auteur ni le même régime. > **Confirmation.** L'arbitrage ci-dessous s'appuie sur ce que le dépôt porte > déjà. Si le propriétaire entend un autre régime (EUPL 1.2, MIT, Apache 2.0, > ou « tous droits réservés »), il **remplace le fichier `LICENSE` à la racine du -> dépôt**, puis ajuste ce document et la section « Licence » de -> `src/docs/GITHUB.md` : les trois doivent dire exactement la même chose. +> dépôt**, puis ajuste ce document et la section « Licence » de `README.md` : +> les deux doivent dire exactement la même chose. --- @@ -60,9 +58,11 @@ direction du propriétaire du dépôt. Position retenue : - **aucune licence tierce n'est héritée** de ce fait : le code produit n'intègre pas de composant sous licence incompatible. Les dépendances externes, elles, gardent leurs licences respectives (voir ci-dessous) ; -- les **dépendances de plateforme** (`ai-text-plugin` pour les assistants, - stockage clé/valeur, canal du service, relais HTTP) sont **importées par nom** - depuis Perchance (`main.pjs`) et **ne résident pas** dans ce dépôt. +- les **ports d'hôte** (moteur de langage des assistants, stockage + clé/valeur, canal du service, relais HTTP) sont des **capacités facultatives de + l'hébergement** : l'application les détecte à l'exécution et s'en passe quand + l'hébergement ne les fournit pas — aucun n'est requis, et **aucun ne réside** + dans ce dépôt. Table des dépendances du service auto-hébergé (`src/server/mysql/package.json`) : diff --git a/src/README.md b/src/README.md index f1cb9af..116de70 100644 --- a/src/README.md +++ b/src/README.md @@ -27,30 +27,18 @@ exploitants) et `server/README.md` (installation Docker). La démonstration publ production, le dossier `server/` fournit la pile complète (nginx + service Node + MySQL/MariaDB) et l'édition web de l'application. -## Flux de travail : Perchance ↔ GitHub +## Flux de travail — le dépôt est la source de vérité **Consigne de travail permanente. À lire avant toute modification du code.** -1. **Le développement se fait ici, sur Perchance.** `main.pjs`, `index.html` et `src/` sont la - copie de travail : c'est là qu'on écrit, qu'on essaie et qu'on vérifie dans l'aperçu. - Rien d'autre ne compte tant que la version n'est pas figée. -2. **Les versions finalisées sont déposées sur GitHub.** Le dépôt est la référence *publiée* : - une version n'existe qu'une fois poussée là-bas. C'est l'utilisateur qui dépose (ou qui - demande de préparer le dépôt) — on ne considère jamais une version comme livrée sans cela. -3. **L'utilisateur peut fournir un lien GitHub** (dépôt, dossier, fichier, archive). Dans ce - cas, **avant d'écrire la moindre ligne** : - - **comparer** les deux états : lire dans le dépôt `src/lib/version.js` (`APP_VERSION` et - `APP_RELEASED`) et la **première entrée datée** de `src/CHANGELOG.md`, puis les comparer à - ceux d'ici ; - - **GitHub au même niveau, ou en retard** → on travaille ici normalement, sur la copie - Perchance ; - - **GitHub plus récent** → **on met d'abord Perchance à jour depuis GitHub** : récupérer les - fichiers du dépôt, les réécrire **à l'identique** dans `src/`, `main.pjs` et `index.html`, - vérifier que l'application tourne encore (aperçu live, console sans erreur, un écran de - chaque famille), **puis seulement** traiter la demande en cours. On ne fusionne pas à - moitié : c'est le dépôt qui fait foi, on le reporte tel quel. - - En cas de doute sur la fraîcheur, **demander** plutôt que d'écraser : une version Perchance - plus avancée qui écrase le dépôt fait perdre du travail. +1. **Le développement se fait ici, dans le dépôt.** `index.html` et `src/` sont la source : + c'est là qu'on écrit, qu'on vérifie (`npm run verifier`) et que la CI juge. Rien d'autre + ne compte tant que la version n'est pas figée. +2. **On travaille sur la branche `dev`, une branche par sujet** (`vibe/` ou le nom du + chantier), jamais directement sur `main` : `main` ne reçoit que des fusions relues, et la + branche de sujet part toujours du dernier `dev`. +3. **Une pull request par sujet vers `dev`**, la CI verte avant toute demande de relecture. + Un changement, un sujet : pas de renommage ni de réorganisation « au passage ». 4. **Chaque livraison laisse une trace** : ouvrir une entrée datée dans **`src/CHANGELOG.md`** (rubriques `Ajouté` / `Modifié` / `Corrigé` / `Retiré`) et vérifier que `APP_VERSION` (`src/lib/version.js`) correspond **au titre de la première entrée datée**, @@ -63,107 +51,40 @@ et l'édition web de l'application. 5. **Ne jamais livrer deux fois le même numéro.** Si le dépôt a déjà le numéro d'ici, c'est qu'une livraison a eu lieu : incrémenter avant de figer, jamais réutiliser. -### Récupérer un dépôt GitHub (recette) - -- **Un fichier** : `https://raw.githubusercontent.com////` - (par exemple `.../main/src/lib/version.js`) — à enregistrer dans `scratch/` puis à comparer - avec le fichier de `src/`. -- **Le dépôt entier** : - `https://github.com///archive/refs/heads/.zip` → décompresser dans - `scratch/` (outil `execute_js` + `@zip.js/zip.js`) et comparer fichier par fichier avec - `src/`. -- **Comparer avant de copier** : le dépôt peut contenir ce qui n'appartient pas à l'édition - Perchance (outillage, `node_modules`, artefacts de build, `.git`). `src/` ne doit recevoir - que ce que le générateur sert réellement (voir « Workspace » dans les notes de l'agent). -- Le dépôt contient aussi `.nojekyll` (obligatoire pour GitHub Pages, voir plus bas) : sa - présence dans le dépôt ne veut pas dire qu'il doit exister dans `src/`. - -### Exporter le dépôt GitHub (recette) - -L'export est un **zip dont la racine EST le dépôt**. C'est la racine qui est lue par un -intégrateur, par une forge et par un agent : l'outillage y est donc rangé **à sa place -attendue**, et le code de l'application sous `src/`. - -**AVANT DE CONSTRUIRE L'ARCHIVE : comparer avec le dépôt.** Un export **écrase** ce qu'il -contient, et un dépôt peut avoir avancé d'un seul fichier — un registre d'audit plus récent, un -rapport qu'on n'a pas ici. On lit donc, au minimum, `src/lib/version.js` (`APP_VERSION`, -`APP_RELEASED`) et le **premier titre daté** de `src/CHANGELOG.md` du dépôt, et l'on liste ses -fichiers (`GET /repos///git/trees/?recursive=1` — un seul appel, et l'on -voit tout de suite ce qui manque d'un côté ou de l'autre) : - -- **dépôt au même niveau, ou en retard** → on construit l'archive ; -- **dépôt plus récent, ou porteur d'un fichier qu'on n'a pas** → **on récupère d'abord** ce qui - manque (rapports, fiches de registre, correctifs), on le vérifie, **puis** on construit. - -La leçon vient de loin : un export a déjà recouvert un registre d'audit plus récent que le nôtre -(voir `audit/REGISTRE-NON-CONFORMITES.md`, NC-I-017, et `audit/README.md` § « les deux lignées »). -Ce qui est perdu par un écrasement ne se retrouve pas dans l'archive suivante. - -| Dans l'atelier | À la racine du dépôt | Pourquoi | -|---|---|---| -| `src/scripts/**` | `scripts/**` | l'outillage se lance depuis la racine (`npm run lint`, `npm test`) | -| `src/tests/**` | `tests/**` | les épreuves transverses (voir `tests/README.md`) | -| `src/compose-exemple/**` | `compose-exemple/**` | l'exemple de premier déploiement, que l'administrateur cherche à la racine | -| `src/package.json` | `package.json` | le manifeste, lu par npm et par la forge | -| `src/package-lock.json` | `package-lock.json` | le verrou de dépendances, lu par `npm ci` et la CI | -| `src/github/ci.yml` | `.github/workflows/ci.yml` | la chaîne d'intégration | -| `src/github/gitignore` | `.gitignore` | | -| `src/docs/GITHUB.md` | `README.md` | la page d'accueil du dépôt | -| `src/AGENTS.md` | `AGENTS.md` | les consignes d'un agent qui reprend le dépôt | -| `src/CLAUDE.md` | `CLAUDE.md` | le pointeur de Claude Code vers `AGENTS.md` | -| `index.html`, `main.pjs` | `index.html`, `main.pjs` | la page et son code, servis par GitHub Pages | -| *(écrit par l'export)* | `CNAME`, `.nojekyll` | l'adresse publiée, et de quoi empêcher Jekyll | -| tout le reste de `src/` | `src/**` | l'application, servie telle quelle par le navigateur | - -**Un fichier, un seul exemplaire.** Une source recopiée à la racine **ne reste pas** dans -`src/` : le dépôt ne porte ni deux `package.json` ni deux outils — un doublon ferait corriger -le mauvais fichier en silence. L'export **exclut** donc de l'arbre `src/` tout ce qu'il déplace -(`scripts/`, `tests/`, `compose-exemple/`, `package.json`, `package-lock.json`, `github/`, `AGENTS.md`, `CLAUDE.md`, -`docs/GITHUB.md`). Conséquence pour une mise à jour : ce qui a changé de place doit être -**supprimé** du dépôt -(`git rm -r src/scripts src/tests src/compose-exemple src/github src/package.json src/package-lock.json src/docs/GITHUB.md`) -avant de décompresser le zip par-dessus — sinon les anciens chemins demeurent, et l'on corrige un -fichier que personne ne lit. - -**Dans l'atelier, l'outillage vit sous `src/`** (`src/scripts/`, `src/tests/`) : c'est le seul -arbre que la plateforme conserve d'une séance à l'autre. Il est pourtant **écrit pour la -disposition livrée** — le code y est `../src/…`, l'outillage `../scripts/…`. Trois choses le -rendent tenable : `tests/README.md` le dit à qui ouvre un fichier d'épreuve, `scripts/racine-code.mjs` -**constate** la racine du code au lieu de la supposer, et le harnais de l'atelier -(`scripts/harnais-atelier.mjs`) **simule la disposition livrée** (voir `docs/ATELIER.md` § 3). - -Recette : écrire les fichiers dans un zip (outil `execute_js` + `@zip.js/zip.js`, niveau 9, -dates fixes pour un export reproductible), en excluant `node_modules`, `scratch/`, `.git` et -les fichiers système, en appliquant la table ci-dessus (et en écartant de `src/` ce qu'elle -déplace). Le nom du fichier porte la version courante — `scribae-v-github.zip` — et -l'archive se décompresse **à la racine** du dépôt. - -Les fichiers sont écrits en **0644**, sauf les scripts (`*.sh`), qui doivent l'être en -**0755** : c'est ce que `git` enregistre (le bit exécutable), et ce qu'une décompression -ordinaire restitue. `@zip.js/zip.js` n'écrit que 0644 — le mode se corrige après coup dans -les **attributs externes** des entrées de l'annuaire central du zip (champ `external file -attributes`, 16 bits de poids fort), en laissant le reste de l'archive intact. - -Le zip porte en outre **`package.json`** et **`package-lock.json`** (recopies de `src/`) et -**`.github/workflows/ci.yml`** (recopie de `src/github/ci.yml`) : la racine est lue par -l'intégrateur et par la forge, `src/` par le navigateur. Les commandes du manifeste désignent -l'outillage **là où il vit dans le dépôt** (`scripts/…`, `tests/`, `src/server/mysql/`) : rien -n'est dupliqué, et `npm run verifier` fonctionne **depuis la racine**. `scripts/verifier-syntaxe.mjs` -et `scripts/verifier-style.mjs` retrouvent leur arborescence tout seuls, qu'ils soient lancés -depuis la racine du dépôt livré ou depuis l'atelier, où tout voisine sous `src/`. - -Le `.gitignore` ajouté à la racine vaut ceci — il est recopié de -`src/github/gitignore`, qui en est la source (comme `ci.yml`), et il ignore -`data/` : le dossier du rangement par FICHIERS (`STOCKAGE=fichier`) contient les -actes, les comptes et les sessions, et n'a rien à faire dans un dépôt public. Le -`.env` n'est **jamais** commité : il porte les secrets de déploiement (voir -`docs/VARIABLES.md`, qui ne montre aucune valeur). +### Disposition des fichiers + +**Un fichier, un seul endroit** — l'outillage à la racine, le code sous `src/`, et rien en +double : + +| À la racine du dépôt | Pourquoi | +|---|---| +| `scripts/**` | l'outillage se lance depuis la racine (`npm run lint`, `npm test`) | +| `tests/**` | les épreuves transverses (voir `tests/README.md`) | +| `compose-exemple/**` | l'exemple de premier déploiement, que l'administrateur cherche à la racine | +| `package.json`, `package-lock.json` | le manifeste et le verrou, lus par npm, `npm ci` et la CI | +| `.github/workflows/ci.yml` | la chaîne d'intégration | +| `.gitignore` | dépendances, secrets de déploiement (`.env*`), dossier de données `data/` | +| `README.md` | la page d'accueil du dépôt | +| `AGENTS.md`, `CLAUDE.md` | les consignes d'un agent qui reprend le dépôt | +| `index.html` | la page et son code, servies par GitHub Pages | +| `CNAME`, `.nojekyll` | l'adresse publiée, et de quoi empêcher Jekyll | +| `src/**` | l'application, servie telle quelle par le navigateur | + +Les commandes du manifeste désignent l'outillage **là où il vit** (`scripts/…`, `tests/`, +`src/server/mysql/`) : rien n'est dupliqué, et `npm run verifier` fonctionne **depuis la +racine**. `scripts/verifier-syntaxe.mjs` et `scripts/verifier-style.mjs` retrouvent leur +arborescence tout seuls (voir `scripts/racine-code.mjs`). + +Le `.gitignore` de la racine vaut ceci — et il ignore `data/` : le dossier du rangement par +FICHIERS (`STOCKAGE=fichier`) contient les actes, les comptes et les sessions, et n'a rien à +faire dans un dépôt public. Le `.env` n'est **jamais** committé : il porte les secrets de +déploiement (voir `docs/VARIABLES.md`, qui ne montre aucune valeur). ```gitignore # Dépendances node_modules/ -# Secrets de déploiement — jamais commités (voir src/server/env.example) +# Secrets de déploiement — jamais committés (voir src/server/env.example) .env .env.local @@ -185,7 +106,7 @@ Thumbs.db .vibe/ ``` -Le `CNAME` ajouté à la racine vaut ceci — **une ligne**, sans schéma ni barre oblique. C'est +Le `CNAME` à la racine vaut ceci — **une ligne**, sans schéma ni barre oblique. C'est l'adresse à laquelle GitHub Pages sert la démonstration (voir « Démonstration statique » ci-dessus), et par conséquent **l'origine** à laquelle le stockage des visiteurs est attaché. La changer — ou publier la même page sous une seconde adresse — revient à ouvrir une @@ -196,9 +117,8 @@ produites. Un fork remplace ce fichier, ou le supprime. demo.scribae.eu ``` -Le dépôt porte aussi des fichiers que l'export **ne contient pas** : `LICENSE` (GPL-3.0), le -dossier `.git/`, et d'éventuels fichiers tiers. On décompresse donc l'archive **par-dessus un -clone existant**, jamais dans un dossier vidé au préalable — sinon on supprime la licence. +Le dépôt porte aussi des fichiers qui ne servent pas au site : `LICENSE` (GPL-3.0), le +dossier `.git/`, et d'éventuels fichiers tiers. ```sh git add -A @@ -206,24 +126,8 @@ git commit -m "" git push ``` -`src/docs/GITHUB.md` et le `README.md` du dépôt doivent rester **cohérents** : le premier est -la source, le second la copie publiée (c'est ce que l'export recopie). - **Vérifier et tester avant de livrer** — `npm run verifier` (syntaxe, style, épreuves), la CI, et -ce qui reste à mettre en place : `docs/INDUSTRIALISATION.md`. Des fichiers de `src/` sont -destinés à la **racine du dépôt** et y sont recopiés par l'export (`package.json`, -`package-lock.json`, `scripts/`, `tests/`, `github/ci.yml` → `.github/workflows/ci.yml`, `AGENTS.md`, -`CLAUDE.md`) — même convention que `docs/GITHUB.md` (→ `README.md`). - -**Destinataire de `docs/GITHUB.md` : un tiers qui veut *se servir* du logiciel** — un agent, un -responsable de service, un élu curieux —, **jamais un développeur**, et surtout jamais la -personne qui édite ce générateur. Conséquence : on n'y parle **ni de Perchance, ni de `src/`, -ni du flux de travail, ni des recettes d'export, ni de `APP_VERSION`** — tout cela vit ici ou -dans `SPEC.md`. On y parle de ce que le logiciel fait, de la façon de l'essayer, de ce qu'il -advient des données, et de la façon de l'installer (Pages ou serveur). Les seuls renvois -techniques admis sont `src/server/README.md` et `src/docs/ADMINISTRATION.md`, qui s'adressent -au service informatique du lecteur — et l'on dit à qui ils s'adressent. **Toute consigne -destinée à l'éditeur du générateur reste dans ce fichier-ci.** +ce qui reste à mettre en place : `docs/INDUSTRIALISATION.md`. ## Identité du logiciel (nom + marque) @@ -245,10 +149,10 @@ Le nom est **réexporté** par `src/ui/brand.js`, la licence et la documentation `src/ui/mention.js` : l'interface n'a donc qu'un module d'identité à connaître, et les trois pieds de page ne peuvent pas diverger de la bannière du service. -À quoi d'autre toucher si le nom change : `$meta.title` / `description` (`main.pjs`), le -`` de `index.html`, le nom des autorités de certification de la -démonstration (`src/lib/signature.js`), et les captures du guide (leur en-tête montre le -nom — voir plus bas). Et **régénérer le miroir du service** (`node scripts/generer-logiciel.mjs`, +À quoi d'autre toucher si le nom change : le `` de `index.html`, le +`<link rel="icon">` de `index.html`, le nom des autorités +de certification de la démonstration (`src/lib/signature.js`), et les captures du guide +(leur en-tête montre le nom — voir plus bas). Et **régénérer le miroir du service** (`node scripts/generer-logiciel.mjs`, voir `src/server/README.md`) : sans quoi le journal du conteneur continuerait d'annoncer l'ancien nom. @@ -262,10 +166,10 @@ nom. La démonstration est décidée par le **DÉPLOIEMENT** : un seul commutateur, **`DEMO`** dans le `.env` du service (voir `src/server/env.example`). La question se tranche à **un seul endroit**, `demoActif()` (`src/lib/demo.js`) ; `config.brand.demo` n'en est qu'un **miroir** — il voyage avec -les données exportées et importées, et fait foi quand aucun déploiement ne parle (aperçu en ligne, -page statique). +les données exportées et importées, et fait foi quand aucun déploiement ne parle (page statique, +service embarqué). -- **Allumée** (le défaut de l'aperçu en ligne ; `DEMO=true`, ou `AUTH_MODE=demo`, ou +- **Allumée** (le défaut de la démonstration en ligne ; `DEMO=true`, ou `AUTH_MODE=demo`, ou `DEMO_ACCOUNTS=true`) : l'application sème le **jeu fictif livré** — identité, entités, assemblées, services, personnes, rôles, références, familles, trames, actes et comptes — et affiche le **bandeau orange** « Démonstration ». @@ -284,7 +188,8 @@ Le bandeau : dans `app.css`. Le bandeau de démonstration l'emporte : les deux ne s'affichent jamais ensemble. - **Réglage du texte** : `Administration › Identité › Mention de démonstration` — un **texte libre** (`brand.demoText`, vide = phrase d'origine) ; le choix *Afficher / Masquer* ne subsiste, - comme réglage du référentiel, que quand **aucun déploiement** ne parle (aperçu, page statique). + comme réglage du référentiel, que quand **aucun déploiement** ne parle (page statique, service + embarqué). **Démonstration éteinte, ces options sont masquées** : une installation en service n'a ni jeu fictif ni mention fictive à régler — la carte dit seulement où passer les messages du service. - **Les bandeaux d'information** : `Administration › Identité › Bandeaux d'information` — des @@ -850,8 +755,7 @@ réseau, SSO placé devant l'application). Voir `docs/ADMINISTRATION.md` § 4. **Où l'éprouver.** Le flux suppose que l'**adresse de retour** soit réellement chargée par le navigateur, paramètres compris (`?code=…&state=…`) : testez le fournisseur **sur la page -déployée**, dans un vrai onglet (l'aperçu d'un environnement d'édition peut ne pas suivre la -redirection). L'annuaire d'essai, lui, fonctionne partout. +déployée**, dans un vrai onglet (un aperçu local peut ne pas suivre la redirection). L'annuaire d'essai, lui, fonctionne partout. **Écrans** : `src/ui/views/connexion.js` (écran de connexion, trois modes : jetons de démonstration, comptes locaux à mot de passe, annuaire — le mode mot de passe se réglant au @@ -1077,7 +981,7 @@ Trois réglages les positionnent, et `styleCss` comme `creerPdfA` les honorent : Les deux emblèmes suivent la charte **partout** : l'aperçu, le HTML autonome, le fichier Word (où ils sont posés en flottants, Word ne connaissant ni `flex` ni `gap`) et le **PDF/A**, qui les embarque et les dessine sur chaque page — la première comprise — au bord du filet, chacun à sa hauteur. Un -parcours du navigateur (`emblemes-entete`, `src/tests/parcours.mjs`) rend un vrai document dans un +parcours du navigateur (`emblemes-entete`, `tests/parcours.mjs`) rend un vrai document dans un hôte hors écran et mesure cette mise en place. **La formule d'autorité peut être en gras.** Elle ne se réglait qu'en italique ou en normal @@ -2252,7 +2156,7 @@ acte se cherche comme ses autres champs). Deux adresses à ne pas confondre : `hrefRecueil`/`hrefActe` (relatives, la racine du site et ses clés — `?acte=<clé>`) pour **naviguer** dans la page (un lien absolu rechargerait la page publique dans son propre cadre), et `adresseRecueil`/`adresseActe` (absolues, -`perchance.org/<générateur>?acte=<clé>`) pour **citer et partager** l'acte. En **bas de page** — +l'adresse du site, `?acte=<clé>`) pour **citer et partager** l'acte. En **bas de page** — et au bout des résultats de recherche —, le recueil renvoie vers les recueils qu'il ne gère pas et vers les sites de référence : voir « Le recueil renvoie vers les recueils qu'il ne gère pas ». @@ -2330,10 +2234,10 @@ liste, toutes les routes de l'atelier répondent `403 atelier_hors_reseau`, et j L'application ne fait que **refléter** cet état (`src/lib/atelier-acces.js` : `chargerAcces`, `acces`, `atelierRestreint`, `horsReseau`, `agentsAvecActesReserves` ; l'écran `hors-reseau` pour l'agent qui arrive d'ailleurs) et le **simuler** (`?ip=` sur `GET /v1/atelier/acces`) : un -navigateur ne décide jamais de son propre droit d'entrer. Le service de démonstration de la -plateforme, lui, ne voit pas l'adresse de l'appelant : sa réponse le dit -(`restriction_appliquee: false` et une **note**), et le simulateur reste juste — c'est aussi ce qui -explique, dans l'aperçu, qu'un réglage écrit ici ne se voie pas appliqué. +navigateur ne décide jamais de son propre droit d'entrer. Le service embarqué (édition +statique), lui, ne voit pas l'adresse de l'appelant : sa réponse le dit +(`restriction_appliquee: false` et une **note**), et le simulateur reste juste — c'est aussi ce +qui explique, en édition statique, qu'un réglage écrit ici ne se voie pas appliqué. **L'identifiant ELI est une adresse, et les liens ELI mènent à l'acte.** Un acte publié cite ses fondements par leur identifiant (`eli:/fr/arr/2026/0464/vsl`) : c'est le **visa d'adoption** d'une @@ -2551,10 +2455,10 @@ gestes qui manquaient à une démonstration : est de plus **repris** quelques fois, à intervalles croissants, tant qu'il reste des actes à publier : lancé au démarrage, il peut trouver la page occupée (jeu de démonstration à reconstruire, registre volumineux à écrire) et la première lecture du service échoue avant même d'avoir -commencé. Entre deux actes, il **respire** (`PAUSE_AMORCAGE`) — le service de la plateforme est -tenu à un budget de calcul soutenu, et publier dix-sept actes en chaîne l'épuise ; ce budget -n'existant ni dans l'édition statique ni en auto-hébergement, la pause y est nulle, et le recueil -se remplit en quelques secondes. +commencé. Entre deux actes, il **respire** (`PAUSE_AMORCAGE`) — l'hébergement de l'édition en +ligne tenait le service à un budget de calcul soutenu, et publier dix-sept actes en chaîne +l'épuisait ; ce budget n'existant ni dans l'édition statique ni en auto-hébergement, la pause y +est nulle, et le recueil se remplit en quelques secondes. **Le rattrapage des versions.** L'amorçage ne s'en tient pas à « une publication existe » : une publication ne s'écrase pas, le service la range sous une clé qui porte son **expression de date**, @@ -2903,15 +2807,15 @@ public les donne — c'est exactement ce que le recueil affiche à tout venant. `repondre()` compose l'invite — instruction, connaissances, conversation, tâche — puis la confie au moteur. Le moteur est **interchangeable** (Administration › Assistants) : -- **automatique** (défaut) : le moteur intégré quand il existe (le plugin `ai-text` de - Perchance), l'adresse de la collectivité sinon, le **repli documentaire** à défaut — c'est - le réglage qui fonctionne partout ; -- **intégré** : uniquement Perchance ; +- **automatique** (défaut) : le moteur intégré quand l'hébergement en fournit un (le port + `generateText`, `src/lib/hosts.js`), l'adresse de la collectivité sinon, le **repli + documentaire** à défaut — c'est le réglage qui fonctionne partout ; +- **intégré** : uniquement quand l'hébergement fournit le moteur ; - **personnalisé** : l'API de la collectivité (adresse, clé, modèle), en **complétions de conversation** façon OpenAI (flux SSE — OpenAI, Mistral, Groq, OpenRouter, Ollama, vLLM, LM Studio…) ou en **appel simple** `{ prompt } → { texte }`. C'est le seul moyen d'obtenir - des réponses **rédigées** hors de Perchance ; une adresse vide est signalée comme une - configuration à corriger. + des réponses **rédigées** ; une adresse vide est signalée comme une configuration à + corriger. **Le repli documentaire : répondre sans moteur.** Faute de moteur de langage — page servie en statique (GitHub Pages), déploiement sans API —, `moteurDe` rend `{ type: "repli" }` et @@ -2974,7 +2878,7 @@ L'application ne sait pas *où* sont rangées ses données : elle passe par une | Mode | Pilote | Ce que ça donne | |---|---|---| | **Locale — ce navigateur** (défaut) | `db/local.js` (IndexedDB) | mode de démonstration : rien à installer, données propres au poste | -| **Service de démonstration — partagé** | `db/service.js`, transport `socket` | l'état durable du service de démonstration tient lieu de base, sans rien installer ; **partagé** entre postes quand la page est servie par la plateforme, **propre au navigateur** dans l'édition statique (le libellé du mode le dit) | +| **Service de démonstration — partagé** | `db/service.js`, transport `socket` | l'état durable du service de démonstration tient lieu de base, sans rien installer ; **partagé** entre postes quand l'hébergement fournit le canal, **propre au navigateur** dans l'édition statique (le libellé du mode le dit) | | **Serveur externe — MySQL / MariaDB** | `db/service.js`, transport `http` | la base de la collectivité, via le service de données de `src/server/mysql/` | Le réglage vit dans `kv.actesDb` — il est **propre au poste** et n'est jamais @@ -3114,18 +3018,18 @@ change, et qu'il faut savoir : HTTPS obligatoire), contrôle d'empreinte, publication, ELI, opposabilité ; - la résolution des services de l'hôte passe par **`src/lib/hosts.js`**, seul endroit qui sait où les chercher — le stockage, le canal du service, et le relais HTTP sans CORS dont la - numérotation externe a besoin (indisponible hors de l'édition en ligne : voir « La - numérotation » ci-dessus). `src/pages/host.js` est sans effet dans l'environnement d'édition - et dans le déploiement auto-hébergé (il ne s'installe que si les services manquent). Poser - `window.__SCRIBA_FORCE_STATIC__ = true` avant son exécution force l'inverse : c'est ainsi - qu'on relit l'édition statique depuis l'éditeur ; -- **la plateforme ne pose pas ses règles de style.** Tout le code masque en posant l'attribut - `hidden` (`el.hidden = true`), jamais `style.display` : c'est la plateforme Perchance qui - fournit la règle `[hidden] { display: none }`. Une page statique ne l'a pas, et une classe - qui pose `display: flex` l'emporte alors sur le `display: none` du navigateur (sélecteur - d'attribut 0,1,0 contre sélecteur de type 0,0,1) — un panneau « fermé » restait ouvert. - `app.css` reprend donc la règle (`[hidden] { display: none !important; }`) : c'est ce qui - rend la fermeture du panneau de l'assistant effective hors de Perchance ; + numérotation externe a besoin (un port que l'hébergement d'édition en ligne fournissait, et + qu'aucune installation livrée ne fournit : voir « La numérotation » ci-dessus). + `src/pages/host.js` est sans effet dans le déploiement auto-hébergé (il ne s'installe que si + les services manquent). Poser `window.__SCRIBA_FORCE_STATIC__ = true` avant son exécution + force l'inverse : c'est ainsi qu'on relit l'édition statique depuis un poste de travail ; +- **l'hébergement ne pose pas ses règles de style.** Tout le code masque en posant l'attribut + `hidden` (`el.hidden = true`), jamais `style.display` : c'est l'hébergement d'édition en + ligne qui fournissait la règle `[hidden] { display: none }`. Une page statique ne l'a pas, + et une classe qui pose `display: flex` l'emporte alors sur le `display: none` du navigateur + (sélecteur d'attribut 0,1,0 contre sélecteur de type 0,0,1) — un panneau « fermé » restait + ouvert. `app.css` reprend donc la règle (`[hidden] { display: none !important; }`) : c'est + ce qui rend la fermeture du panneau de l'assistant effective en édition statique ; - **l'assistant n'a pas de moteur de langage** : `moteurDe` rend `{ type: "repli" }`, et Plume comme Publia répondent par **recherche documentaire** dans le guide (ou dans les actes publiés) — sans réseau, sans clé, rien à installer. Voir « Les assistants » plus haut. @@ -3177,10 +3081,10 @@ base, partagée. | `root.kv` (stockage clé/valeur local) | `server/web/host.js` — une base **IndexedDB** du navigateur | | `root.createServerSocket` (API du service) | appels `fetch` directs vers `/v1/…` (nginx → service Node) | -`server/web/index.html` est la coquille de cette édition (l'`index.html` d'origine ne sert -qu'à l'édition en ligne, comme `main.pjs`). `remote.js` choisit son transport : le canal de -l'environnement s'il existe, sinon HTTP si `window.__SCRIBA_SELF_HOSTED__` est posé par le -déploiement. Le mode de persistance par défaut y devient « serveur externe » sur la **même +`server/web/index.html` est la coquille de cette édition (l'`index.html` du dépôt sert la +démonstration statique). `remote.js` choisit son +transport : le canal de l'environnement s'il existe, sinon HTTP si +`window.__SCRIBA_SELF_HOSTED__` est posé par le déploiement. Le mode de persistance par défaut y devient « serveur externe » sur la **même origine**, et le jeton d'écriture est injecté au démarrage du conteneur (voir `server/web/config.js.template`). @@ -3190,10 +3094,8 @@ migration depuis la démonstration : `docs/ADMINISTRATION.md`. ## Architecture ``` -main.pjs $meta + les imports de la plateforme (stockage, canal du service, relais HTTP) index.html coquille : script serveur (LE SERVICE) + <link> + <div id="app"> + module src/SPEC.md spécification -src/docs/ATELIER.md TRAVAILLER DANS L'ATELIER : la méthode, les dix garde-fous, vérifier sans terminal (le harnais), ce qui compte comme « terminé » src/docs/ADMINISTRATION.md DOCUMENTATION D'ADMINISTRATION ET D'EXPLOITATION (auto-hébergement, sécurité, sauvegardes) src/docs/API.md RÉFÉRENCE DE L'API REST (engendrée par scripts/generer-api.mjs, lue par « Documentation technique ») src/docs/VARIABLES.md VARIABLES DE DÉPLOIEMENT (engendré par scripts/generer-variables.mjs) @@ -3202,7 +3104,6 @@ src/docs/REPRISE.md KIT DE REPRISE : points d'entrée, « un seul point de src/docs/UI-UX.md REVUE D'INTERFACE : constats chiffrés (hauteur des écrans, contrôles, pastilles, tailles de texte), huit propositions d'UI/UX et leur ordre de mise en œuvre src/docs/AUDIT-BUGS-2026-09-22.md AUDIT CIBLÉ : session, anti-CSRF, file d'attente, état de la base (constats corrigés et points ouverts) src/docs/PERFORMANCE.md PERFORMANCE MESURÉE : le gel des connexions simultanées, ce qui l'a corrigé, les scénarios et les chiffres avant/après -src/docs/GITHUB.md PAGE D'ACCUEIL DU DÉPÔT (recopiée en README.md à la racine par l'export GitHub) src/CHANGELOG.md JOURNAL DES VERSIONS (la première entrée datée = la version en service) src/TODO.md chantiers ouverts src/css/app.css feuille de style : l'ENTRÉE — elle importe dix parties (app-base.css, app-atelier.css, …), dans l'ordre d'origine @@ -3300,7 +3201,7 @@ src/server/ AUTO-HÉBERGEMENT : pile Docker complète nginx.conf façade : application servie, /v1/ en proxy vers api (cuit dans l'image de web) Dockerfile IMAGE AUTONOME : le service, la façade et le code en un conteneur env.example modèle du .env du déploiement - web/ édition web de l'application (chargée hors édition en ligne) + web/ édition web de l'application (coquille servie par nginx) Dockerfile la façade : nginx + nos réglages + la coquille + le code de l'application index.html coquille (remplace l'index.html d'origine) host.js hôtes d'exécution simulés : root.kv (IndexedDB) et transport HTTP @@ -3417,32 +3318,31 @@ src/audit/ CADRE ET LIVRABLES D'AUDIT (voir src/audit/README.md) REGISTRE-NON-CONFORMITES.md registre cumulatif (identifiants stables, statuts : Ouverte / En cours / Levée / Régression / Acceptée / Obsolète) rapports/ les rapports datés, un par campagne README.md mode d'emploi du cadre -src/tests/ TESTS QUI S'EXÉCUTENT SANS NAVIGATEUR — dans le dépôt livré : `tests/` (voir src/tests/README.md) - README.md pourquoi ces fichiers sont écrits pour la disposition LIVRÉE (le code y est `../src/…`), et ce que tient chacun +tests/ TESTS QUI S'EXÉCUTENT SANS NAVIGATEUR (voir tests/README.md) + README.md pourquoi les imports y disent `../src/…` (la disposition du dépôt), et ce que tient chaque fichier purs.test.mjs tests des modules purs : expressions, assainissement, numérotation, version — et, depuis la 1.6.1p, le contrat des champs que le client lit du service, ainsi que la règle des imports jamais employés (les modules exigeant un navigateur font SAUTER leurs tests au lieu de faire tomber la suite) industrialisation.test.mjs L'OUTILLAGE éprouvé lui-même : lance le contrôle de style tel qu'il est livré, sur l'arborescence livrée, exige le code de sortie 0 (ordinaire et strict) et vérifie que le parcours a bien vu tout le code — une faute de chemin ne se voit pas dans le code, elle se voit à l'exécution (il se saute là où aucun processus enfant n'est possible) publications-locales.test.mjs tests du repli local du recueil public : seuls les actes publiés entrent au recueil, les versions d'un même ELI sont rangées (la plus récente porte `latest`), un acte réservé reste caché, et les pièces se lisent aussi bien dans `formats` qu'à plat conformite-service.mjs LE JEU D'APPELS COMMUN aux deux services (démonstration et auto-hébergé) — le contrat, pas une épreuve ; conformite-service.test.mjs l'exécute contre la démonstration et le compare à une installation réelle (`SCRIBA_CONFORMITE_URL`) parcours.mjs LES PARCOURS joués dans le NAVIGATEUR, contre l'application vivante (`lancerParcours()`) — voir docs/INDUSTRIALISATION.md § 2 parcours.test.mjs abrogation-annexes.test.mjs amorcage-demo.test.mjs qualification-signature.test.mjs original-signe.test.mjs pilote-persistance.test.mjs reprise.test.mjs -src/scripts/ L'OUTILLAGE — dans le dépôt livré : `scripts/` - verifier-syntaxe.mjs `npm run syntaxe` : `node --check` sur tout le JavaScript du dépôt, sans aucune dépendance ; parcourt le dossier qui porte l'outillage (la racine du dépôt, ou tout l'arbre dans l'atelier) - verifier-style.mjs `npm run lint` (avertissements tolérés) et `npm run style` (`--strict` : ils font échouer) : analyse sans dépendance — REFUSE `debugger` hors outillage, SIGNALE `var` et `console.log` dans le code client, ET les imports jamais employés (partout, via `analyse-imports.mjs`) ; les exemptions sont écrites relativement à la RACINE DU CODE, ce qui les rend justes dans les deux dispositions +scripts/ L'OUTILLAGE + verifier-syntaxe.mjs `npm run syntaxe` : `node --check` sur tout le JavaScript du dépôt, sans aucune dépendance ; parcourt le dossier qui porte l'outillage (la racine du dépôt) + verifier-style.mjs `npm run lint` (avertissements tolérés) et `npm run style` (`--strict` : ils font échouer) : analyse sans dépendance — REFUSE `debugger` hors outillage, SIGNALE `var` et `console.log` dans le code client, ET les imports jamais employés (partout, via `analyse-imports.mjs`) ; les exemptions sont écrites relativement à la RACINE DU CODE analyse-imports.mjs MODULE PUR de la règle précédente : rend les noms importés que le reste du fichier n'emploie jamais, avec le numéro de ligne — éprouvé dans `tests/purs.test.mjs` - racine-code.mjs OÙ VIT LE CODE (`RACINE_CODE`, `RACINE_OUTILLAGE`) : le dossier qui porte `lib/version.js` — CONSTATÉ, jamais supposé (il est `src/` dans le dépôt, et la racine de l'outillage dans l'atelier) + racine-code.mjs OÙ VIT LE CODE (`RACINE_CODE`, `RACINE_OUTILLAGE`) : le dossier qui porte `lib/version.js` — CONSTATÉ, jamais supposé generer-variables.mjs engendre `src/docs/VARIABLES.md` depuis le registre des variables (`src/server/mysql/variables.mjs`) generer-api.mjs engendre `src/docs/API.md` depuis la description de l'API (`src/lib/api-reference.js`) — le document et l'écran « API REST » ne peuvent donc pas diverger generer-logiciel.mjs engendre `src/server/mysql/logiciel-engendre.mjs` depuis `src/lib/version.js` et `src/lib/logiciel.js` : l'image du service, qui ne contient que son dossier, peut ainsi annoncer la version RÉELLE dans sa bannière de démarrage - harnais-atelier.mjs LE BANC D'ÉPREUVES DE L'ATELIER : rejoue, sans terminal, la syntaxe, le style, les épreuves et les générateurs en chargeant LES SCRIPTS LIVRÉS eux-mêmes (doublures de `node:fs`/`node:path`/`node:url`/`node:crypto`, esbuild) — voir `docs/ATELIER.md` § 3 - annoncer-echecs.sh L'ÉPREUVE ROUGE DEVIENT LISIBLE : appelé par la chaîne d'intégration après un échec, il attache chaque épreuve rouge en ANNOTATION (« ::error:: », avec son message d'assertion) et résume le journal dans le récapitulatif du travail — le journal brut n'étant lisible que par un administrateur du dépôt, c'est ce qui permet de savoir CE QUI a lâché (voir `docs/INDUSTRIALISATION.md` § 3) -src/compose-exemple/ EXEMPLE DE PREMIER DÉPLOIEMENT — dans le dépôt livré : `compose-exemple/` : MariaDB et l'image publiée `aplds/scribae`, deux services, rien à construire (docker-compose.yml, env.example, README.md) -src/github/ SOURCES DES FICHIERS DE RACINE DU DÉPÔT (l'export les recopie, ils ne restent pas sous src/) : - ci.yml → `<racine>/.github/workflows/ci.yml` : syntaxe, style (strict) et épreuves, puis le service auto-hébergé - gitignore → `<racine>/.gitignore` : dépendances, secrets de déploiement (`.env*`) et dossier de données `data/` — voir la section « Exporter le dépôt GitHub » -src/AGENTS.md → `<racine>/AGENTS.md` : ce qu'un agent (Claude Code, Mistral Vibe…) doit savoir AVANT de toucher au dépôt -src/CLAUDE.md → `<racine>/CLAUDE.md` : le pointeur de Claude Code vers `AGENTS.md` -src/package.json MANIFESTE DU DÉPÔT (l'export le recopie à la racine) : `npm run lint`, `npm run style`, `npm test`, `npm run verifier` -src/package-lock.json VERROU DE DÉPENDANCES (l'export le recopie à la racine) : aucun paquet — Scribae n'a aucune dépendance — mais sa présence rend `npm ci` possible et fait taire les avertissements de la forge + annoncer-echecs.sh L'ÉPREUVE ROUGE DEVIENT LISIBLE : appelé par la chaîne d'intégration après un échec, il attache chaque épreuve rouge en ANNOTATION (`::error::`, avec son message d'assertion) et résume le journal dans le récapitulatif du travail — le journal brut n'étant lisible que par un administrateur du dépôt, c'est ce qui permet de savoir CE QUI a lâché (voir `docs/INDUSTRIALISATION.md` § 3) +compose-exemple/ EXEMPLE DE PREMIER DÉPLOIEMENT : MariaDB et l'image publiée `aplds/scribae`, deux services, rien à construire (docker-compose.yml, env.example, README.md) +package.json MANIFESTE DU DÉPÔT : `npm run lint`, `npm run style`, `npm test`, `npm run verifier` +package-lock.json VERROU DE DÉPENDANCES : aucun paquet — Scribae n'a aucune dépendance — mais sa présence rend `npm ci` possible et fait taire les avertissements de la forge +.github/workflows/ci.yml LA CHAÎNE D'INTÉGRATION : syntaxe, style (strict) et épreuves, puis le service auto-hébergé +.gitignore dépendances, secrets de déploiement (`.env*`), dossier de données `data/` +README.md PAGE D'ACCUEIL DU DÉPÔT +AGENTS.md ce qu'un agent (Claude Code, Mistral Vibe…) doit savoir AVANT de toucher au dépôt +CLAUDE.md le pointeur de Claude Code vers `AGENTS.md` src/LICENSE.md LICENCE, ARBITRÉE : logiciel sous GPL-3.0 (le fichier `LICENSE` vit à la racine du dépôt), réutilisation des actes sous Licence Ouverte 2.0, position sur le code produit par l'IA et table des dépendances ``` @@ -3656,7 +3556,7 @@ estimées à l'œil. Recette pour refaire une capture : ```js // dans la console de l'aperçu, après avoir mis l'application dans l'état voulu -const { capture } = await import("https://ai-agent.perchance.org/files/snapshot.js"); +const { capture } = await import("<adresse de snapshot.js>/files/snapshot.js"); // l'outil de capture de l'hébergement const root = document.querySelector("#app"); // cadre capturé const rect = (sel) => { // repère en % const r = root.getBoundingClientRect(), e = document.querySelector(sel).getBoundingClientRect(); @@ -3822,28 +3722,6 @@ Repères de vérification visuelle : pour l'aperçu, chaque `.paper` doit porter l'écran des feuilles de style se vérifie dans les deux vues (« Réglages » et « Édition directe »), en 390 × 844 et en 1600 × 950. -Vérifier l'outillage depuis l'atelier : l'aperçu du navigateur n'a ni système de -fichiers ni processus, il ne peut donc pas lancer `scripts/verifier-style.mjs` tel -qu'il est livré. C'est le rôle du **harnais de l'atelier** — -**`scripts/harnais-atelier.mjs`**, dans l'arborescence du dépôt depuis la 1.6.3b -(il n'est pas une épreuve : `npm test` ne le voit pas, `npm run syntaxe` le parse -comme un module). Il monte la disposition livrée dans un système de fichiers -virtuel (la table ci-dessus, la racine virtuelle étant `src/`), remplace les -modules Node (`node:fs`, `node:path`, `node:url`, `node:crypto`) par des -doublures, fait passer le tout par esbuild, et laisse les écritures revenir dans -`src/`. Il **charge les scripts du dépôt eux-mêmes** : le contrôle de style et -les trois générateurs s'exécutent tels quels, sans qu'aucune règle soit recopiée. -Les épreuves, elles, se construisent **un fichier à la fois** — comme `node --test` -les isole, une doublure qui fuit d'un fichier à l'autre fausserait le suivant. - -Ce que l'atelier ne peut pas faire, il le **dit** : `node:child_process` lève -(pas de processus) et un `import.meta.url` est servi sous une adresse `file://` -**virtuelle**, ancrée à la racine virtuelle, pour que `new URL("..", -import.meta.url)` calcule juste (`scripts/racine-code.mjs`). Il en résulte, à la -1.6.3s, **495/505** épreuves, **aucun échec** et **dix sauts** attendus — tous -dus à l'absence de Node et de réseau. Les recettes, les chiffres et la lecture -avant de travailler dans l'atelier. - ## Pièges connus - **Une épreuve qui lit un fichier ne doit pas dépendre du DOSSIER COURANT.** `node --test` peut @@ -3852,8 +3730,8 @@ avant de travailler dans l'atelier. `"src/server/env.example"` ne veut pas dire la même chose dans les deux cas, et ne trouve **rien** dans le second. Une épreuve qui échoue à cause de cela ne dit rien du code — elle ne dit que son propre dossier de départ. Ancrez le repère au fichier : `fileURLToPath(new URL("../env.example", - import.meta.url))` — et protégez ce calcul (`try`), car dans un harnais de navigateur l'adresse - d'un module n'est pas hiérarchique. Les chemins relatifs peuvent rester, **essayés ensuite** ; + import.meta.url))` — et protégez ce calcul (`try`), car sous le harnais de parcours + (`tests/parcours-navigateur.mjs`) l'adresse d'un module n'est pas hiérarchique. Les chemins relatifs peuvent rester, **essayés ensuite** ; c'est ainsi que la chaîne est restée rouge quatre livraisons durant (NC-I-008). - **Une feuille de style écrite dans un littéral gabarit ne peut pas contenir de backtick** — et `\00a0` (échappement octal) y est refusé aussi. `CSS_DOCUMENT_WEB` @@ -3920,8 +3798,7 @@ avant de travailler dans l'atelier. compter sur les marges du bloc, c'est `@page` qui fait la page. En test automatisé, `window.print()` **bloque** la page : ne pas appeler sans interaction utilisateur. -- **Captures d'écran** : utiliser l'aide de capture du harnais d'édition (`snapshot.js` de - `ai-agent` : `await capture()` pour la page entière, `await capture(el, {scale})` pour un sous-arbre). +- **Captures d'écran** : utiliser l'outil de capture (`snapshot.js` : `await capture()` pour la page entière, `await capture(el, {scale})` pour un sous-arbre). **Sauf si l'en-tête est dans le cadre** : snapdom y replie la barre d'outils (voir la section du guide) — prendre `html2canvas` à la place. Attention : un **logo distant** fait **traîner la capture** (le helper tente de le convertir en @@ -3968,12 +3845,12 @@ avant de travailler dans l'atelier. trouvé par une écriture de 120 000 caractères, qui revenait tronquée après rechargement. `src/pages/host.js` (`etatUtilise`, `VERSION_ETAT`, `STATE_UNITS`) décrit le même en-tête — les deux doivent rester d'accord, et `tests/persistance-double-tampon.test.mjs` le vérifie. -- **L'aperçu non enregistré utilise un serveur émulé** : son état (50 Mio) disparaît à - chaque rechargement de la page, et l'émulateur **met le service en quarantaine** sous une - salve d'appels trop dense (NC-II-012) — les parcours qui en dépendent se déclarent alors - « sans objet », et le relevé porte `service.joignable`. Espacez les appels, et refaites le - scénario proprement. La version publiée en ligne, elle, utilise le - vrai service et garde son état. Dans l'édition statique, l'état est conservé dans +- **L'édition en ligne non enregistrée utilisait un serveur émulé** : son état (50 Mio) + disparaissait à chaque rechargement de la page, et l'émulateur **mettait le service en + quarantaine** sous une salve d'appels trop dense (NC-II-012) — les parcours qui en + dépendaient se déclaraient alors « sans objet », et le relevé portait `service.joignable`. + Espacez les appels, et refaites le scénario proprement. La version publiée en ligne, elle, + utilise le vrai service et garde son état. Dans l'édition statique, l'état est conservé dans IndexedDB : il survit aux rechargements (voir `src/pages/host.js`). - **Taille des messages** : un message WebSocket est limité à 1 Mio. `remote.js` refuse au-delà de 900 000 caractères et le service au-delà de 400 000 caractères de document ; diff --git a/src/SPEC.md b/src/SPEC.md index 403a429..b632c2b 100644 --- a/src/SPEC.md +++ b/src/SPEC.md @@ -2744,7 +2744,7 @@ référentiel, applique par-dessus les variables du `.env`, et ne rend qu'une ** du navigateur : le service découvre le fournisseur, échange le code (avec le vérificateur PKCE que le navigateur a gardé le temps de l'aller-retour), vérifie le jeton d'identité, en tire le compte (règles de `src/lib/oidc.js` **reprises à l'identique** — la concordance des deux implémentations -est éprouvée, `src/tests/purs.test.mjs`) et ouvre **sa** session (`POST /v1/auth/annuaire`). Deux +est éprouvée, `tests/purs.test.mjs`) et ouvre **sa** session (`POST /v1/auth/annuaire`). Deux raisons, et deux conséquences : * **un fournisseur qui n'ouvre pas le CORS se branche** : aucun appel ne part d'une autre origine, @@ -3484,8 +3484,8 @@ le lui apporte — une épreuve (`logiciel-engendre.test.mjs`) refuse un miroir paraît que sur le **démarrage du service** : les commandes d'administration (`--reconcilier`, `--migrate`, `--mot-de-passe`) gardent un journal qui n'est que la trace du geste qu'on y a fait. -La coquille de cette édition est `server/web/index.html` : l'`index.html` et le `main.pjs` -d'origine ne servent qu'à l'édition en ligne. En auto-hébergement, le mode de persistance par +La coquille de cette édition est `server/web/index.html` : l'`index.html` du dépôt sert +la démonstration statique. En auto-hébergement, le mode de persistance par défaut est « serveur externe » sur la même origine, et le jeton d'écriture est injecté au démarrage du conteneur. diff --git a/src/TODO.md b/src/TODO.md index 9df275a..3c9a024 100644 --- a/src/TODO.md +++ b/src/TODO.md @@ -9,9 +9,9 @@ Demande : « assure toi que toute la doc soit à jour ». Le détail est au `CHA - [x] **`POST /v1/relais` entre dans la référence de l'API** (`src/lib/api-reference.js` + `docs/API.md` régénéré, écran « API REST » suivi) avec ses codes d'erreur. -- [x] **Chapeau du dépôt** (`docs/GITHUB.md`), **chiffres du banc** (`docs/ATELIER.md`, - `docs/INDUSTRIALISATION.md`, `src/README.md`), **tableau des épreuves** - (`src/tests/README.md`), **arborescence du service** (`src/server/README.md`), +- [x] **Chapeau du dépôt** (la page d'accueil du dépôt), **chiffres du banc** + (`docs/INDUSTRIALISATION.md`, `src/README.md`), **tableau des épreuves** + (`tests/README.md`), **arborescence du service** (`src/server/README.md`), **exemples Docker** (`docs/DOCKER.md`) : réalignés. **Ce qui reste ouvert** : inchangé — voir la section 1.6.3q ci-dessous (CI à voir @@ -285,7 +285,7 @@ contre registre, thèmes, etc.). Il a trouvé le défaut que la demande soupçon les décisions publiées des délégations, la résolution des liens ELI. - [x] **La cohérence en plus** : le recueil relit ses informations quand un billet **publié au poste** manque à la liste affichée — la rubrique se répare au redessin suivant. -- [x] **La régression est tenue** : `src/tests/relecture.test.mjs` (3 épreuves) et le parcours +- [x] **La régression est tenue** : `tests/relecture.test.mjs` (3 épreuves) et le parcours « Les informations du recueil ne s'effacent pas, et se réparent » (le trentième). - [x] **Le jeu d'épreuves** : **243 fichiers** sans faute de syntaxe, **451/462** épreuves (0 échec, **11 sauts** d'environnement), **30/30 parcours** verts, et les **18 adresses du @@ -407,10 +407,10 @@ Reste ouvert, et dit comme tel : parcours) et leurs réparations sont au `CHANGELOG.md` (1.6.3k). Reste à **voir la CI verte** sur un envoi : NC-I-008 reste ouverte jusque-là. - [x] **Rejouer les parcours du navigateur en intégration continue** (Playwright) — FAIT en 1.6.3d. - `src/tests/parcours-navigateur.mjs` rejoue les 29 parcours dans un vrai Chromium : serveur de + `tests/parcours-navigateur.mjs` rejoue les 29 parcours dans un vrai Chromium : serveur de fichiers local, applications montées en édition statique, session d'administration ouverte, verdict + annotations `::error::` + capture d'écran en artefact. Travail `parcours` ajouté à - `src/github/ci.yml` (avisant : `continue-on-error`). Vérifié dans l'aperçu : **29/29** sur un + `.github/workflows/ci.yml` (avisant : `continue-on-error`). Vérifié dans l'aperçu : **29/29** sur un profil vierge. NC-I-002 peut être levée. - [x] **`piece-depot` est instable sous l'émulateur de l'éditeur — traité (1.6.3l).** La cause était le **statut**, comme la fiche le pressentait : `supprimerPiece` rend `false` pour un @@ -510,12 +510,8 @@ limité. » Reste ouvert : -- [ ] **Rejouer les parcours en intégration continue** (Playwright ou équivalent) : ils demandent - aujourd'hui un navigateur, donc une main — voir `docs/INDUSTRIALISATION.md` § 4. Le harnais de - l'atelier ne les remplace pas. -- [ ] **Le harnais jette-t-il un écran de fumée ?** Il rejoue la CI, il ne la remplace pas : garder - la règle « une épreuve qui échoue ici et passe en Node est un écart à documenter », et tenir - la liste des écarts connus (`docs/ATELIER.md` § 3.3) à jour à chaque note. +- [x] **Rejouer les parcours en intégration continue** (Playwright ou équivalent) — FAIT en + 1.6.3d (`tests/parcours-navigateur.mjs`, travail « Parcours critiques » de la chaîne). ## Version 1.6.3a — la transmission au contrôle de légalité se règle en trois régimes (livré) @@ -1254,8 +1250,8 @@ publication → ELI) fonctionne sans serveur, y compris après rechargement. statique sur un **service distant** (`src/server/`, ou toute installation au même contrat) : le stockage local reste (réglages, session, file d'attente), les collections passent par l'API, et tous les postes voient le même référentiel. Le service doit autoriser l'origine - de la page (`CORS_ORIGINS`). Documenté dans `docs/GITHUB.md` (« Travailler à plusieurs tout - en restant sur GitHub Pages ») ; `__SCRIBA_STATIC_SHARED__` dit au bandeau qu'il ne doit + de la page (`CORS_ORIGINS`). Documenté dans `src/README.md` (« Démonstration statique ») ; + `__SCRIBA_STATIC_SHARED__` dit au bandeau qu'il ne doit pas annoncer « rien n'est partagé ». - [x] **Bandeau « démonstration statique ».** Le bandeau de tête (atelier **et** recueil public) dit maintenant, en édition statique, que la page héberge son propre service et que rien diff --git a/src/audit/PROMPT-AUDIT-SCRIBAE.md b/src/audit/PROMPT-AUDIT-SCRIBAE.md index b615599..7fd0fd5 100644 --- a/src/audit/PROMPT-AUDIT-SCRIBAE.md +++ b/src/audit/PROMPT-AUDIT-SCRIBAE.md @@ -2,7 +2,7 @@ titre: Prompt d'audit complet — Scribae version: 1.0 date: 2026-09-21 -cible: https://perchance.org/scribae +cible: https://demo.scribae.eu objet: Cadre d'audit et contrat de sortie pour produire le rapport d'audit de Scribae reutilisable: oui (voir §0) stockage: src/audit/PROMPT-AUDIT-SCRIBAE.md @@ -14,7 +14,7 @@ stockage: src/audit/PROMPT-AUDIT-SCRIBAE.md il le cadre, le rôle par rôle, section par section, et fixe le contrat de sortie du rapport. L'audit lui-même est produit par l'agent à qui ce prompt est donné. -**Cible auditée.** `https://perchance.org/scribae` — éditeur de trames d'actes +**Cible auditée.** `https://demo.scribae.eu` — éditeur de trames d'actes administratifs : préparation des modèles par les administrateurs, remplissage par les services, validation interne, signature, transmission au contrôle de légalité, publication au recueil public sous identifiant ELI, export Akoma Ntoso / Schematron / @@ -39,8 +39,8 @@ pour une collectivité fictive (mairie de Valmont-sur-Loire). 4. écrire le rapport dans `src/audit/rapports/AUDIT-SCRIBAE-AAAA-MM-JJ.md` (§8) ; 5. mettre à jour `src/audit/REGISTRE-NON-CONFORMITES.md` (§6.4) ; 6. **joindre le rapport à sa réponse finale** en markdown (`attach_file`) ; - 7. **ne rien corriger** : un audit constate et propose ; il ne modifie ni `main.pjs`, - ni `index.html`, ni le reste de `src/`. + 7. **ne rien corriger** : un audit constate et propose ; il ne modifie ni + `index.html`, ni le reste de `src/`. - **Ce que ce prompt n'est pas.** Ce n'est ni un référentiel de conformité exhaustif, ni un jugement pré-écrit. Les constats sont produits par l'agent, à partir des @@ -76,7 +76,7 @@ matière (l'outil), quatre regards irréductibles l'un à l'autre. ### §2.1 Dans le périmètre -- `main.pjs`, `index.html` (y compris le script `type="text/x-server-plugin"`), et +- `index.html` (y compris le script `type="text/x-server-plugin"`), et **l'intégralité** du dossier `src/` (code, documentation, données de démonstration) ; - l'application **à l'exécution** : parcours réels, écrans, workflows, exports, API, persistance, assistants ; @@ -86,8 +86,8 @@ matière (l'outil), quatre regards irréductibles l'un à l'autre. ### §2.2 Hors périmètre (à mentionner, sans en faire des non-conformités) -- les composants de la plateforme Perchance elle-même (moteur, hébergement, - plugins tiers) — mais **leur usage par Scribae** est dans le périmètre ; +- les composants de la plateforme d'hébergement d'origine (moteur de langage, + stockage, plugins tiers) — mais **leur usage par Scribae** est dans le périmètre ; - le prestataire de signature externe et les services tiers appelés — mais la **manière dont Scribae s'y branche** est dans le périmètre ; - l'infrastructure de la collectivité cliente. @@ -95,7 +95,7 @@ matière (l'outil), quatre regards irréductibles l'un à l'autre. ### §2.3 Distinction à tenir Le rapport distingue systématiquement : -**l'outil de démonstration** (ce qui tourne sur `perchance.org/scribae`, avec ses +**l'outil de démonstration** (ce qui tourne sur `demo.scribae.eu`, avec ses limites assumées : IndexedDB comme pilote local, pas de SMTP, etc.) et **le déploiement cible** (service auto-hébergé, base MySQL/MariaDB, SMTP, jeton d'API). La confusion entre les deux est elle-même un objet d'audit (§4.2). @@ -155,7 +155,7 @@ Chaque chapitre se termine par : les **points conformes**, les **non-conformité - **I.3 Environnements, tests, industrialisation** — séparation démo / recette / production ; tests automatisés ; intégration et déploiement continus ; reproductibilité des versions. - - **I.4 Dépendances et chaîne d'approvisionnement** — plugins Perchance, imports + - **I.4 Dépendances et chaîne d'approvisionnement** — imports CDN, dépendances implicites, épinglage de versions, vulnérabilités des composants. - **I.5 Gouvernance du code** — versionnage (`APP_VERSION`), changelog, revue, traçabilité des modifications, propriété intellectuelle du code produit par l'IA. diff --git a/src/audit/README.md b/src/audit/README.md index a1d4754..03dc166 100644 --- a/src/audit/README.md +++ b/src/audit/README.md @@ -64,7 +64,7 @@ L'agent doit, dans cet ordre : 4. écrire le rapport dans `rapports/AUDIT-SCRIBAE-AAAA-MM-JJ.md` ; 5. mettre à jour le registre (statuts, nouvelles entrées, historique des audits) ; 6. **joindre le rapport** à sa réponse finale (en markdown) ; -7. **ne rien corriger** : un audit constate et propose ; il ne modifie ni `main.pjs`, ni +7. **ne rien corriger** : un audit constate et propose ; il ne modifie ni `index.html`, ni le reste de `src/`. ## Règles diff --git a/src/docs/ADMINISTRATION.md b/src/docs/ADMINISTRATION.md index d121020..f9c3a65 100644 --- a/src/docs/ADMINISTRATION.md +++ b/src/docs/ADMINISTRATION.md @@ -1772,7 +1772,7 @@ administrateurs par la permission `docs.voir`, mais ses fichiers (`src/docs/ADMI l'entrée de l'écran aux autres rôles — c'est un confort d'interface, pas un secret : n'y écrivez jamais un identifiant, une clé ou une adresse à protéger (voir le paragraphe suivant). -Ne placez **jamais** dans `src/`, `main.pjs`, `index.html` ou `config.js` : mot de passe de +Ne placez **jamais** dans `src/`, `index.html` ou `config.js` : mot de passe de base, clé privée, jeton de production durable, URL de suppression. Les secrets vivent dans `.env` (non versionné) et dans la configuration du serveur. @@ -2152,7 +2152,7 @@ la migration consiste donc à déplacer les données, pas à les convertir. service) — la carte *Administration › Identité › Mention de démonstration* masque alors d'elle-même ses options, et aucun bandeau « Démonstration » ne s'affiche ; et retirer la dépendance à l'environnement d'édition — l'application auto-hébergée ne - charge plus `main.pjs` ni l'`index.html` d'origine (sa coquille est `src/server/web/index.html`). + charge plus l'`index.html` d'origine (sa coquille est `src/server/web/index.html`). > Les **certificats de signature** créés dans un navigateur de démonstration ne se > transportent pas : en service, ils sont créés par poste (ou remplacés par le certificat du diff --git a/src/docs/ATELIER.md b/src/docs/ATELIER.md deleted file mode 100644 index db1fd5e..0000000 --- a/src/docs/ATELIER.md +++ /dev/null @@ -1,450 +0,0 @@ -# Travailler dans l'atelier — la méthode, les règles, les garde-fous - -**À lire avant la première modification, et à relire à chaque session.** Ce document ne dit pas -ce que fait l'application (c'est `src/SPEC.md` et `src/README.md`) ni comment on la déploie -(c'est `src/docs/ADMINISTRATION.md`) : il dit **comment on travaille ici**, ce qu'on n'a pas le -droit de casser, et **comment on prouve** qu'on n'a rien cassé. - -Il s'adresse à n'importe qui reprend le dépôt — un humain, ou un agent dont la mémoire s'arrête -à chaque tour. **Il est écrit pour être suffisant à lui seul** : si vous ne lisez qu'un document -avant d'écrire une ligne, lisez celui-ci. - -> **L'atelier, c'est l'édition en ligne sur Perchance** (`main.pjs`, `index.html` et `src/`). -> C'est la **copie de travail** ; le **dépôt GitHub** est la copie **publiée** (voir -> `src/README.md` § « Flux de travail : Perchance ↔ GitHub »). Les deux ne diffèrent que par la -> **disposition des fichiers** : dans l'atelier, tout voisine sous `src/` (c'est le seul arbre que -> la plateforme conserve d'une séance à l'autre) ; dans le dépôt, l'outillage est remonté à la -> racine (`scripts/`, `tests/`). Un fichier ne vit qu'à **un** endroit, et l'export fait le -> rangement. - ---- - -## 0. Le résumé qu'on peut lire seul - -**Les dix garde-fous** (§ 1) : ne rien publier sans qu'on vous le demande · ne jamais écrire à la -main un document engendré · une règle, une seule implémentation · aucune dépendance nouvelle · -tout en français · le métier sans DOM ni réseau · recharger l'aperçu après chaque série -d'écritures · toute variable a sa ligne dans les deux `env.example` · une livraison laisse une -trace (changelog + version) · **ne jamais dire « terminé » sans preuve**. - -**La boucle** (§ 2) : lire le fichier canonique → écrire le minimum au bon endroit → **vérifier** -(§ 3 à § 6) → laisser la trace (§ 7) → relire l'écran (§ 6). - -**La vérification, en un appel** (§ 3) : - -```js -// dans l'outil `execute_js` de l'agent, avec un délai généreux (timeoutMs ≥ 1 500 000) -const src = await fs.readTextFile("src/scripts/harnais-atelier.mjs"); -const h = await import(URL.createObjectURL(new Blob([src], { type: "text/javascript" }))); -return h.texte(await h.verifier({ fs })); // syntaxe + style + épreuves, chiffres commentés -``` - ---- - -## 1. Les garde-fous - -Ce sont des règles **du projet**, pas des préférences. Chacune a été payée par un incident : la -colonne « pourquoi » n'est pas décorative. - -| Garde-fou | Pourquoi | Où c'est tenu | -|---|---|---| -| **Ne rien publier ni enregistrer sans demande explicite.** | L'atelier porte une version **non enregistrée** : l'enregistrer fige un nom, et un nom ne se défait pas — les données rangées sous l'ancien nom (base locale, galerie, commentaires) deviennent inaccessibles. Une livraison est un geste de l'utilisateur. | `src/README.md` § « Flux de travail » | -| **Ne jamais modifier à la main un fichier engendré.** | Le modifier à la main le fait diverger de sa source, et l'épreuve qui le garde échouera **plus tard**, loin de la cause. | `src/docs/VARIABLES.md`, `src/docs/API.md`, `src/server/mysql/logiciel-engendre.mjs` — régénérés par `scripts/generer-*.mjs` (§ 7) | -| **Une règle, une seule implémentation.** | Deux copies d'une même règle divergent en silence, et l'on corrige celle que personne ne lit. | Table des points de vérité : `src/docs/REPRISE.md` § 2 | -| **Aucune dépendance nouvelle.** | Le dépôt se vérifie avec Node seul : pas de chaîne d'approvisionnement à auditer, pas d'installation à recréer. La seule dépendance est `mysql2`, pour le service. | `src/AGENTS.md` § « Les règles du projet » | -| **Le métier ne touche ni au DOM ni au réseau.** | C'est ce qui rend le domaine éprouvable sans navigateur (et donc éprouvable **ici**). Les effets passent par des ports injectés. | `src/lib/**` pur, `src/ui/**` pour l'écran | -| **Tout est en français**, commentaires compris ; un commentaire dit le **pourquoi**. | Le dépôt est lu par des agents de collectivités ; un commentaire qui paraphrase le code est du bruit. | tout le dépôt | -| **Après une série d'écritures : recharger l'aperçu avant de conclure quoi que ce soit.** | Les fichiers ne sont **pas** appliqués à la page tant qu'on ne l'a pas rechargée : on croit voir la nouvelle version et l'on regarde l'ancienne. | § 3 et § 6 | -| **Toute variable de déploiement décrite a sa ligne dans les deux `env.example`.** | Une variable non documentée est une variable que personne ne réglera — et la panne sera silencieuse. | `src/server/env.example` **et** `src/server/mysql/env.example`, éprouvé par `src/server/mysql/variables.test.mjs` | -| **Une livraison laisse une trace** : une entrée datée dans `src/CHANGELOG.md`, le même numéro dans `APP_VERSION` (`src/lib/version.js`), et **les documents que le changement rend faux**. | Un numéro réutilisé, ou un document périmé, coûte plus cher que le changement lui-même. | `src/CHANGELOG.md` (son en-tête dit la convention) | -| **Ne jamais dire « terminé » sans preuve**, et ne jamais annoncer un chiffre qu'on n'a pas mesuré. | Un rapport sans preuve est une dette : quelqu'un d'autre la paiera. | § 3 à § 6, § 9 | - -**Deux interdits de forme**, qui se paient comptant : - -- **jamais deux fois le même numéro de version** (le changelog fait foi) ; -- **jamais `localeCompare` hors du service Node** : le moteur du service de démonstration n'a pas - d'`Intl`, et il ne se plaint qu'à partir du **deuxième** élément comparé — le défaut a donc vécu - sans se voir (voir « Pièges » dans `src/AGENTS.md`). - ---- - -## 2. La boucle de travail - -Six gestes, dans cet ordre. Sauter le geste 4 est la façon la plus sûre de livrer une régression. - -1. **Situer.** Lire `src/AGENTS.md` (il est court) puis, pour la règle visée, chercher son - **fichier canonique** dans `src/docs/REPRISE.md` § 2. Le registre - `src/audit/REGISTRE-NON-CONFORMITES.md` dit ce qui est **connu comme non conforme** : le - lire avant de promettre, et le mettre à jour si l'on lève une fiche. -2. **Décider du périmètre.** Un changement, un sujet. Ne pas renommer un fichier, ne pas - réorganiser « au passage » : ce qui n'est pas dans la demande n'est pas dans le diff. -3. **Écrire au bon endroit**, et de la façon dont le voisinage est écrit (mêmes conventions, - mêmes aides, même vocabulaire). Si deux implémentations d'une même règle existent (le service - de démonstration dans `index.html` et le service auto-hébergé), **les deux** doivent suivre, et - le jeu de conformité `src/tests/conformite-service.mjs` doit les couvrir. -4. **Vérifier** — dans l'ordre, et sans s'en dispenser : - - le **harnais** (syntaxe, style, épreuves) : § 3 ; - - si le **service** change : l'éprouver vivant, dans l'aperçu, sur une seule passe : § 4 ; - - si un **écran** change : le regarder, en mobile **et** en grand : § 6 ; - - si un **parcours** est touché : rejouer les parcours du navigateur : § 5. -5. **Laisser la trace** : la documentation que le changement rend fausse, l'entrée du changelog, - la version (§ 7). -6. **Recharger l'aperçu** et confirmer qu'il n'y a **ni erreur de console ni erreur de - plateforme** avant de rendre la main (§ 3, « ce que le harnais ne dit pas »). - ---- - -## 3. Vérifier sans terminal : le harnais de l'atelier - -L'atelier n'a **ni système de fichiers ni processus** : `npm test`, `node --check` et les -générateurs n'y sont pas lançables tels quels. Le harnais -**`src/scripts/harnais-atelier.mjs`** comble l'écart — et il le fait **sans dupliquer une seule -règle** : il charge les scripts du dépôt **eux-mêmes** et leur fournit des doublures de -`node:fs`, `node:path`, `node:url`, `node:crypto` (les écritures retombant dans `src/`). C'est le -même outillage que celui de la CI, avec d'autres fondations. - -### 3.1 Comment on s'en sert - -Depuis l'outil **`execute_js`** de l'agent (une passe, un délai généreux : le contrôle lit tout -l'arbre et construit quarante et un fichiers d'épreuves) : - -```js -const src = await fs.readTextFile("src/scripts/harnais-atelier.mjs"); -const h = await import(URL.createObjectURL(new Blob([src], { type: "text/javascript" }))); - -// tout : syntaxe, style, épreuves — dans l'ordre de `npm run verifier` -return h.texte(await h.verifier({ fs })); - -// ou ciblé (beaucoup plus rapide : c'est ce qu'on fait pendant un développement) -return h.texte(await h.verifier({ fs, epreuvesAussi: false })); -return await h.epreuves({ fs, cibles: ["src/server/mysql/actes.test.mjs", "src/tests/reprise.test.mjs"] }); -``` - -### 3.2 Ce que rend chaque service - -| Appel | Ce qu'il fait | Ce qu'on attend | -|---|---|---| -| `await h.verifier({ fs })` | syntaxe + style + épreuves | `ok: true` (voir § 3.3) | -| `await h.verifier({ fs, epreuvesAussi: false })` | syntaxe + style seuls | `ok: true` | -| `await h.verifierSyntaxe({ fs })` | parse chaque `.js`/`.mjs` de `src/` | `fautes: []` | -| `await h.verifierStyle({ fs })` | exécute **`scripts/verifier-style.mjs` tel que livré** (`--strict`) | `code: 0` | -| `await h.epreuves({ fs })` | toutes les épreuves (`tests/` + le service) | voir § 3.3 | -| `await h.epreuves({ fs, cibles: [...] })` | seulement celles-là | `totalOk === totalTests` sur les fichiers visés | -| `await h.regenerer({ fs })` | les trois générateurs livrés | `ok: true`, fichiers **identiques** s'il n'y avait rien à changer | -| `h.texte(rapport)` | le verdict, en quelques lignes | à rendre tel quel (petit, lisible) | - -Le **style** est en mode **strict** (comme la CI) : un `var`, un `console.log` de client, un -**import jamais employé** font échouer. Le message du contrôle nomme le fichier et la ligne. - -### 3.3 Les chiffres attendus dans l'atelier - -Le harnais **n'est pas Node** : certaines épreuves ne peuvent pas y être jouées. Elles ne mentent -pas pour autant — elles appellent `t.skip` —, et il vaut mieux les connaître que les redécouvrir. -À la version **1.6.3s**, la suite complète donne **49 fichiers, 495/505**, et les **10 sauts** -tiennent tous à l'environnement : - -| Fichier | Résultat ici | Ce qui manque | -|---|---|---| -| `server/mysql/jws.test.mjs` | 0/5 (**5 sautés**) | un vrai `node:crypto` (`generateKeyPairSync`, `createSign`) | -| `tests/industrialisation.test.mjs` | 3/6 (**3 sautés**) | un **processus** (`spawnSync`) — il n'y en a pas ici | -| `tests/conformite-service.test.mjs` | 4/5 (**1 sauté**) | une installation réelle à comparer (`SCRIBA_CONFORMITE_URL`) | -| `tests/purs.test.mjs` | 28/29 (**1 sauté**) | le **réseau** (l'échange OIDC d'un vrai fournisseur) | - -**Aucun échec n'est connu, et c'est le fait à retenir.** Jusqu'à la 1.6.3d, cinq fichiers étaient -rouges *dans l'atelier seulement*, et pour une raison qui n'existait que là : le harnais -construisait un bundle **par import dynamique**, si bien que deux modules chargés par une chaîne -calculée (`charger("../src/lib/store.js")` puis `charger("../src/lib/auth.js")`) étaient deux -copies — un test qui réglait `auth.js` ne se voyait pas dans `store.js`. Et ces modules survivaient -d'un fichier d'épreuves au suivant, là où `node --test` donne **un processus par fichier**. - -`uneEpreuve` répare les deux : elle fait entrer D'AVANCE, dans le bundle du fichier, tous les -modules que sa source cite en clair (esbuild les inline, donc ils sont partagés), et elle vide le -registre entre deux fichiers. **Un fichier = un bundle = un processus.** Conséquence à garder en -tête : un fichier rouge ici n'est plus un artefact du harnais, c'est une **régression**. - -**Regarder ses sauts de près.** Un saut n'est pas toujours un fait d'environnement. Jusqu'à la -1.6.3k, `tests/industrialisation.test.mjs` **sautait quatre fois** dans l'atelier : ses épreuves de -lecture se croyaient hors d'un processus alors qu'elles n'avaient besoin que du système de fichiers. -La cause était une faute du HARNAIS — il ne réécrivait `import.meta.url` que dans les modules -IMPORTÉS, jamais dans le fichier d'épreuve lui-même, dont l'URL restait un `blob:` : un -`new URL("..", import.meta.url)` y levait, et l'épreuve se sautait pour un motif qui n'existait -qu'ici (un **écran de fumée** : « tout est vert », alors que trois contrôles ne s'exécutaient pas). -Corollaire à garder : quand un saut apparaît là où l'environnement n'a rien à voir, ou qu'il -subsiste après un changement de harnais, c'est le harnais qu'il faut soupçonner — pas le test. - -**Deux chiffres à surveiller** quand on touche au service : `src/server/mysql/actes.test.mjs` -(**32/32**) et `src/server/mysql/controle-legalite.test.mjs` (**9/9**). Ce sont eux qui tiennent la -signature, la publication et la transmission. - -Et une épreuve qu'on ne touche pas sans y penser : `src/server/mysql/magasin-mysql.test.mjs` -(**7/7**), qui tient — entre autres — la **voie MySQL des pièces jointes** (`sb_piece`) et la forme -des résultats d'écriture rendus par la base en mémoire. - -> **Le reste doit être vert, et le total ne doit pas baisser.** Si un fichier qui était vert -> devient rouge, c'est une régression — la vôtre. Les quatre fichiers ci-dessus ne font pas -> exception : leurs sauts sont des faits d'environnement (pas de processus, pas de `node:crypto` -> complet, pas d'installation réelle à comparer, pas de réseau d'entreprise), et **la CI, elle, -> exécute la suite avec un vrai Node** — c'est elle qui fait foi, et depuis la 1.6.3d elle rejoue -> aussi les parcours critiques dans un vrai navigateur (§ 3.5). - -### 3.4 Ce que le harnais ne dit pas - -- **Il ne remplace pas la CI.** La CI exécute `npm run syntaxe` (`node --check`) et `npm test` - avec un vrai Node ; le harnais en est l'ombre portée dans l'atelier. -- **La syntaxe est vérifiée par le parseur d'esbuild**, pas par `node --check` : même service - rendu, autre outil (il n'y a pas de processus ici). -- **Il ne voit pas la page.** Une faute de rendu, un écouteur mal détaché, un débordement mobile : - tout cela se regarde (§ 5, § 6). - -### 3.5 Les parcours, rejoués par la CI dans un vrai navigateur - -Le harnais ci-dessus ne voit pas la page — et c'est précisément là que deux défauts sont passés en -production (le recueil qui reprenait la main sur l'atelier, la rubrique Informations qui -disparaissait) : **verts partout, cassés à l'écran**. La réponse est `tests/parcours.mjs` (les -parcours critiques, joués sur l'application vivante) — et, depuis la 1.6.3d, son exécution par la -CI : `tests/parcours-navigateur.mjs` ouvre un Chromium sans interface, sert le dépôt lui-même, -monte l'application en **édition statique** (celle de GitHub Pages : le service embarqué, l'état -dans IndexedDB), ouvre une session d'administration, puis rejoue la même suite. - -| Où | Comment | Ce que ça coûte | -|---|---|---| -| Ici, dans l'atelier | `import("src/tests/parcours.mjs")` puis `lancerParcours(await contexteDeLApercu())` | quelques secondes | -| En CI | le travail `parcours` (`.github/workflows/ci.yml`) | ~4 min, **avisant** (`continue-on-error`) | - -**Un service muet n'est pas jugé.** La suite interroge le **service** ; quand celui-ci ne répond -**rien** (statut 0 : canal fermé, service suspendu — l'aperçu met le sien en quarantaine sous une -salve trop dense, NC-II-012), les parcours qui en dépendent — le contrat du service, le dépôt d'une -pièce, la relecture des informations du recueil — se déclarent **sans objet** au lieu d'échouer : le -contrat n'est pas enfreint, il n'est pas éprouvable. Le relevé le **dit** (`service.joignable`), et -l'épreuve de la CI **échoue** si le service ne répond pas — l'édition statique l'héberge dans la -page, il doit donc répondre, et un « tout vert » servi par un service muet serait un théâtre. Un -service qui répond **mal** (5xx, mauvais statut) reste, lui, un échec : c'est tout l'objet de la -suite. - -Deux points à savoir si l'on touche à ce travail : - -- **Playwright est installé HORS VERROU** (`npm install --no-save --no-package-lock`), et son - navigateur avec. Rien n'entre dans le dépôt, `npm ci` reste reproductible, et `package-lock.json` - ne bouge pas. -- Le serveur de fichiers écoute sur **`127.0.0.1`** : c'est un « contexte sûr » pour un navigateur, - et sans cela `crypto.subtle` serait absent — tout ce qui calcule une empreinte tomberait. - -Le travail est **avisant** tant qu'il n'a pas été vu vert deux fois de suite : un échec s'y affiche -en annotation, avec une capture d'écran (`parcours-echec.png`, déposée en artefact), sans arrêter -la livraison. Le jour où il est vu vert, retirer son `continue-on-error` : un avertissement -permanent ne se lit plus. - ---- - -## 4. Éprouver le service de l'aperçu - -Dans l'atelier, le service est **émulé** par le script serveur de `index.html` (un moteur QuickJS -dans un fil séparé). Deux propriétés à retenir : - -- il est **éphémère** : son état ne survit pas d'un `page_eval` au suivant ; -- **tout un scénario doit donc tenir dans un seul `page_eval`**. - -```js -// une seule passe : provisionner (si besoin), puis appeler -const cles = await import("./src/lib/cles-service.js"); -const cs = await import("./src/lib/cle-service.js"); -const remote = await import("./src/lib/remote.js"); -const K = cles.tirerCle(); -cs.definirCleService(K); -await cles.provisionnerService({ cle: K }); // idempotent -const r = await remote.call("GET", "/v1/health", { token: K }); // comme le fait l'application -return { status: r.status, body: r.body }; -``` - -`remote.call(methode, chemin, { body, token })` est **le même transport que l'application** : ce -que vous éprouvez ici est le chemin réel (voir `src/lib/remote.js`). Le journal de la façade -(`remote.log`) garde la trace des appels. - -**Le piège.** Le service de démonstration n'est **pas conservatoire** (NC-II-012) et il se met en -**quarantaine** s'il est surchargé : un scénario qui harcèle le service perd son état, et la panne -ressemble à un défaut du code. Espacez les appels, et refaites le scénario proprement plutôt que -de l'enchaîner sur un état douteux. - ---- - -## 5. Les parcours, au navigateur - -Les épreuves de **parcours** (`src/tests/parcours.mjs`) traversent l'application vivante : dépôt -→ signature → publication → recueil. Elles ne s'exécutent **que** dans le navigateur, sur la page -de l'application. - -```js -// après un page_refresh (les écritures doivent être appliquées) -const s = await import("./src/ui/state.js"); -await s.login("u-dubois"); // ⚠ SESSION ADMINISTRATEUR (voir ci-dessous) -const p = await import("./src/tests/parcours.mjs"); -const ctx = await p.contexteDeLApercu(); -const r = await p.lancerParcours(ctx); // 32 parcours attendus -return r.ok + "/" + r.total; -``` - -- **Il faut une session d'administration** : `login("u-dubois")`. En session d'éditeur - (`u-roussel`), les quatre parcours d'administration échouent — ce n'est pas une régression, - c'est le contrôle qui fait son travail. -- Les autres comptes du jeu de démonstration : `u-roussel` (éditeur **et** réviseur, `p-roussel`). -- On peut n'en jouer qu'un : `lancerParcours(ctx, { seulement: ["recueil-public"] })`. -- Les parcours **attendent** ce qu'ils observent (une route rendue, un état chargé) : ne pas - supposer qu'un rendu est fini parce que l'appel est revenu. - -**L'import se fait dans le contexte de la page** : l'outil `page_eval` s'exécute **dans** l'iframe -du générateur, donc les chemins `./src/…` désignent les modules de l'application qui tourne. C'est -essentiel — importer ces modules depuis une **autre origine** éprouverait une **seconde** instance, -à l'état vide, dont les verdicts ne diraient rien. Depuis la console d'un onglet quelconque, il faut -au contraire viser l'origine des modules (voir `src/docs/INDUSTRIALISATION.md` § 2). - -Le contrat **commun aux deux services** se vérifie de la même façon, par le parcours -`service-contrat`, ou en Node (voir `src/docs/INDUSTRIALISATION.md` § 2). - ---- - -## 6. Regarder ce qui se voit - -Un console propre ne dit **pas** qu'un écran est juste : la mise en page, la lisibilité et le -responsive se regardent. - -1. **Appliquer les écritures** : `page_refresh`. Rien de ce qu'on écrit n'est dans la page avant - cela — et le rechargement est aussi ce qui révèle les **erreurs de plateforme** (celles que le - moteur Perchance signale et que personne ne voit dans la console). -2. **Cadrer** : `set_viewport_size({ width: 390, height: 844 })` pour un téléphone, - `{ width: 1920, height: 1080 }` pour un poste. Une application s'adapte : le vérifier dans les - deux, **avant** de dire terminé — un écran d'atelier doit tenir en 390 px de large comme en - grand. -3. **Regarder les pixels** : capturer la page — ou un sous-arbre — avec l'aide de capture de - l'atelier (`await capture()` pour la page, `await capture(el, { scale })` pour un élément), puis - **lire l'image avec l'outil de vision, en décrivant ce qu'on attend** (« l'en-tête tient sur une - ligne ; le bouton des notifications est une cloche ; aucun texte sous 13 px »). Une question - vague reçoit une réponse vague. -4. **Ce qui se mesure se mesure** : `getBoundingClientRect` (positions, débordements, - chevauchements) est plus sûr qu'une impression — et c'est vérifiable avant/après. - -Le projet a ses propres repères de vérification (formats d'export, charte des documents, écrans -sensibles) : ils sont dans `src/README.md` § « Repères de vérification ». Les **pièges de la -capture** (un logo distant fait traîner la capture ; l'en-tête est replié par l'outil sur certains -écrans) sont dans `src/README.md` § « Pièges connus », et il vaut mieux les lire **avant** de -conclure qu'un écran est cassé. - ---- - -## 7. La trace : les documents engendrés, la version, le changelog - -**Trois fichiers sont ENGENDRÉS** — on ne les écrit jamais à la main, on corrige **la source** puis -on régénère : - -| Engendré | Source de vérité | Générateur | -|---|---|---| -| `src/docs/VARIABLES.md` | le registre `src/server/mysql/variables.mjs` | `scripts/generer-variables.mjs` | -| `src/docs/API.md` | la description `src/lib/api-reference.js` | `scripts/generer-api.mjs` | -| `src/server/mysql/logiciel-engendre.mjs` | `src/lib/version.js` + `src/lib/logiciel.js` | `scripts/generer-logiciel.mjs` | - -Dans l'atelier, on les régénère **par le harnais** (les scripts livrés, tels quels) : - -```js -return await h.regenerer({ fs }); // → { ok: true, resultats: [...] } -``` - -Après une régénération, **vérifiez que seul ce qui devait changer a changé** : le harnais écrit -dans `src/`, et une régénération qui produit un fichier différent sans qu'aucune source n'ait -bougé signale une source désynchronisée. - -**La livraison, elle, se marque ainsi** (détail : `src/CHANGELOG.md`, en-tête) : - -1. une entrée **datée** en tête du changelog, avec un numéro — une **note intermédiaire** - (`1.6.3d`) suffit entre deux dépôts, et le numéro ne se réutilise jamais ; -2. `APP_VERSION` (`src/lib/version.js`) **dit la même chose** que le titre de la première entrée - datée ; -3. les documents que le changement rend faux (README, SPEC, TODO, registre d'audit) ; -4. régénérer `logiciel-engendre.mjs` (**il porte la version**) — sinon l'épreuve - `logiciel-engendre.test.mjs` refuse le miroir périmé. - ---- - -## 8. Le dépôt GitHub (hors atelier) - -- **Récupérer** : comparer **d'abord** les versions (lire `src/lib/version.js` et la première - entrée datée du changelog **du dépôt**), et si le dépôt est plus récent, **reporter son état ici - à l'identique** avant de toucher à quoi que ce soit. En cas de doute, **demander** : écraser une - version de l'atelier plus avancée fait perdre du travail. -- **Exporter** : la table complète (ce qui remonte à la racine, ce qui reste sous `src/`) et la - recette sont dans `src/README.md` § « Exporter le dépôt GitHub ». Un fichier déplacé doit être - **supprimé** de l'ancien emplacement, sinon on corrige un fichier que personne ne lit. -- **On ne publie pas de son propre chef.** Déposer une version est un geste de l'utilisateur. - ---- - -## 9. Définition de « terminé » - -On ne rend la main que lorsque **tout** ce qui suit est vrai. Chaque ligne a une preuve, et la -preuve est un **résultat**, pas une intention. - -- [ ] Le changement porte sur **ce qui était demandé**, et rien d'autre (pas de renommage, pas de - réorganisation « au passage »). -- [ ] La règle touchée n'existe **qu'une fois** ; si le service est concerné, **les deux** - implémentations suivent (et le jeu de conformité couvre la route). -- [ ] `h.verifier({ fs, epreuvesAussi: false })` → **syntaxe 0 faute**, **style code 0**. -- [ ] `h.epreuves({ fs })` → **aucun fichier autrefois vert n'est rouge** ; les quatre fichiers - non verts connus (§ 3.3) sont les seuls, et leurs **10 sauts** sont les seuls sauts. -- [ ] Si un écran a changé : il a été **regardé**, en **390 px** et en **grand**. -- [ ] Si le service a changé : il a été **éprouvé vivant** dans l'aperçu (§ 4), et le journal - `remote.log` ne porte rien d'inattendu. -- [ ] Si les parcours sont touchés : `lancerParcours` rend **32/32** en session d'administration. -- [ ] Les **documents engendrés** sont régénérés si leur source a bougé. -- [ ] Toute **variable** nouvelle a sa ligne dans les **deux** `env.example`. -- [ ] La **trace** est posée : changelog daté + `APP_VERSION` accordé + documents corrigés. -- [ ] L'aperçu a été **rechargé** et ne signale **ni erreur de console ni erreur de plateforme**. -- [ ] Le compte rendu **énonce les chiffres mesurés** et dit ce qui n'a pas été vérifié. - ---- - -## 10. Les pièges de l'atelier (ce qui se paie comptant) - -- **Une écriture n'est pas appliquée tant que la page n'est pas rechargée.** Conclure avant est la - faute la plus coûteuse, parce qu'elle est **invisible** : on regarde l'ancienne version. -- **Un accent grave dans un littéral gabarit le termine.** Les feuilles de style écrites en - littéraux gabarits (`src/lib/recueil.js`, `src/lib/eli.js`, et les doublures du harnais) ne - peuvent pas contenir d'accent grave — même dans un commentaire. Un accent grave de trop casse - le module entier (`Expected ";" but found …`). -- **Un `#!` (shebang) doit être neutralisé avant d'être importé** comme module : c'est ce que fait - le harnais. Le shebang en tête d'un script est normal, au milieu d'un module il est fatal. -- **Le service de l'aperçu est éphémère et peut se mettre en quarantaine** : un scénario = un - `page_eval` (§ 4). -- **Les dialogues natifs (`confirm`, `alert`, `prompt`) bloquent l'aperçu** : avant de piloter un - geste qui en ouvre un, le remplacer par une doublure dans la même page. -- **Un `import()` dynamique est PARTAGÉ depuis la 1.6.3d.** `uneEpreuve` fait entrer d'avance dans - le bundle du fichier tous les modules que sa source cite en clair, et vide son registre entre - deux fichiers (§ 3.3). Avant, deux modules chargés par une chaîne calculée étaient deux - instances : une épreuve qui réglait l'un ne se voyait pas dans l'autre. -- **Ne pas construire de correction sur `IntersectionObserver`** ni supposer qu'une animation est - passée : interroger le DOM (`getBoundingClientRect`, un texte, un attribut) — c'est observable. -- **Le vocabulaire.** « Atelier » = l'édition en ligne (ici) ; « dépôt » = GitHub ; « service » - = le service de signature et de publication, en deux implémentations ; « recueil » = la - publication publique. Employer les mots du projet, pas des synonymes. - ---- - -## 11. Toucher aux assistants (le moteur de langage) - -Les deux assistants (celui de l'atelier, celui du recueil) passent par **un seul point d'entrée** -— `src/lib/assistant.js` (`repondre({ …, onChunk })`) — qui résout lui-même le moteur : intégré -(le greffon de la plateforme), personnalisé (celui qu'une collectivité branche) ou repli sans -moteur. **On n'appelle jamais `generateText` directement depuis une vue** : cela contournerait le -choix du moteur, les connaissances et le repli. `src/lib/hosts.js` est le seul à le toucher. - -Quatre règles quand on écrit ou corrige une invite : - -1. **Jamais le contenu des actes dans une invite** : ce qui part chez un moteur tiers, ce sont - les connaissances et l'historique de la conversation, pas les documents de la collectivité - (voir l'en-tête de `main.pjs` et `src/docs/GITHUB.md` § « Vos données »). -2. **Ordonner l'invite pour le cache de préfixe** : d'abord ce qui est **identique** d'un appel à - l'autre (le rôle, les consignes, les connaissances), puis ce qui **s'ajoute** (l'historique), - et **en dernier** ce qui change à chaque appel (la question, l'écran, l'acte en cours). C'est - ce qui rend la réponse rapide — la partie commune est calculée une fois. -3. **Diffuser la réponse** (`onChunk`) plutôt que de l'attendre : le texte apparaît au fil de - l'eau, et l'on peut le lire avant la fin. -4. **Toujours un indicateur animé, visible, près de la fin du texte** : la génération peut durer - une minute, et peut **s'arrêter quelques secondes** en cours de route. Un texte qui cesse sans - indicateur a l'air terminé — et il ne l'est pas. diff --git a/src/docs/COLLABORATION.md b/src/docs/COLLABORATION.md index 9680673..e27a9af 100644 --- a/src/docs/COLLABORATION.md +++ b/src/docs/COLLABORATION.md @@ -179,11 +179,11 @@ apparaître chez l'autre avec la mention « untel écrit ». ## 8. Où c'est éprouvé -- `src/tests/fusion.test.mjs` — la fusion, hors navigateur : identifiants, ordre +- `tests/fusion.test.mjs` — la fusion, hors navigateur : identifiants, ordre des listes, suppressions symétriques, profondeur. - `src/server/mysql/flux.test.mjs` — le concentrateur : diffusion, poste lent (resync), abonné mort, plafond, battement, fermeture générale. -- `src/tests/parcours.mjs` — deux épreuves de plus : +- `tests/parcours.mjs` — deux épreuves de plus : `fusion-sans-perte` (deux postes qui écrivent le même document ne s'effacent plus) et `flux-temps-reel` (découpage des trames SSE, trame incomplète remise au paquet suivant, `resync` reconnu, service sans flux déclaré « absent »). diff --git a/src/docs/INDUSTRIALISATION.md b/src/docs/INDUSTRIALISATION.md index 4e74976..24851d1 100644 --- a/src/docs/INDUSTRIALISATION.md +++ b/src/docs/INDUSTRIALISATION.md @@ -2,23 +2,14 @@ Ce document s'adresse au service informatique qui **reprend le dépôt** : il décrit comment vérifier le code sans navigateur, ce qui est vérifié, et ce qui reste à -mettre en place. Il complète `src/README.md` (architecture) et `GITHUB.md` (mise en -ligne) — voir aussi `src/server/README.md` pour l'exploitation. +mettre en place. Il complète `src/README.md` (architecture) — voir aussi +`src/server/README.md` pour l'exploitation. > **Où vivent ces fichiers.** Le dépôt est servi **tel quel** : sa racine porte -> `index.html`, `main.pjs`, l'**outillage** (`scripts/`, `tests/`, `package.json`, +> `index.html`, l'**outillage** (`scripts/`, `tests/`, `package.json`, > `.github/workflows/ci.yml`) et le **code** sous `src/`. Rien n'est compilé, et -> un fichier ne vit qu'à UN endroit : l'export range chaque source à sa place et -> ne la recopie pas dans `src/` (la table complète est dans `src/README.md`, -> § « Exporter le dépôt GitHub »). -> -> **Dans l'atelier, l'outillage est sous `src/`** (`src/scripts/`, `src/tests/`) : -> c'est le seul arbre que la plateforme conserve d'une séance à l'autre. Il est -> pourtant écrit pour la **disposition livrée** — le code y est `../src/…`, -> l'outillage `../scripts/…` —, et `scripts/racine-code.mjs` constate la racine du -> code au lieu de la supposer. Le harnais de l'atelier (`scripts/harnais-atelier.mjs`, -> § 2 « Vérifier depuis l'atelier ») simule cette disposition avant de conclure -> quoi que ce soit. +> un fichier ne vit qu'à UN endroit (la disposition est dans `src/README.md`, +> § « Disposition des fichiers »). --- @@ -112,42 +103,6 @@ peut être lancé, l'épreuve se **saute** au lieu d'échouer. > **sautés**. Un test qu'on ne peut pas exécuter ici ne doit pas cacher ceux > qu'on peut exécuter. -### Vérifier depuis l'atelier : le harnais - -Dans l'atelier (Perchance), il n'y a **ni système de fichiers ni processus** : rien -de ce qui précède n'y est lançable tel quel. C'est le rôle de -**`scripts/harnais-atelier.mjs`** — l'outillage est ainsi rangé **dans le dépôt**, -à sa place attendue (il n'est pas une épreuve : `npm test` ne le voit pas, et -`npm run syntaxe` le parse comme un module). - -Il monte la disposition livrée dans un système de fichiers **virtuel** (la racine -virtuelle étant `src/`), remplace les modules Node (`node:fs`, `node:path`, -`node:url`, `node:crypto`) par des doublures, fait passer le tout par **esbuild**, -et laisse les écritures retomber dans `src/`. Surtout, il charge les scripts du -dépôt **eux-mêmes** : `verifier-style.mjs` et les trois générateurs s'exécutent -tels quels, sans qu'aucune règle ne soit recopiée. - -| Service | Ce qu'il fait | -|---|---| -| `h.verifier({ fs })` | la syntaxe, le style puis les épreuves — l'ordre de `npm run verifier` | -| `h.verifierSyntaxe({ fs })` | parse chaque `.js`/`.mjs` de `src/` (par le parseur d'esbuild : il n'y a pas de processus) | -| `h.verifierStyle({ fs })` | exécute `scripts/verifier-style.mjs` tel qu'il est livré, en strict | -| `h.epreuves({ fs, cibles })` | les épreuves, **un fichier à la fois** (comme `node --test` les isole) | -| `h.regenerer({ fs })` | les trois générateurs — les documents engendrés | -| `h.texte(rapport)` | le verdict en quelques lignes, à rendre tel quel | - -**Ce qu'il ne peut pas faire, il le dit** : `node:child_process` lève, et un -`import.meta.url` est servi sous une adresse `file://` virtuelle. Il en résulte des -écarts **connus** (**495/505**, aucun échec et **dix sauts** attendus à la 1.6.3s), -tous dus à l'absence de Node et de réseau : la liste, la lecture de chaque écart et -les chiffres à surveiller sont dans **`docs/ATELIER.md` § 3**. - -> **L'atelier n'est pas la CI, et le harnais n'est pas Node.** La CI exécute -> `npm run syntaxe` (`node --check`) et `npm test` avec un vrai Node : c'est elle -> qui fait foi. Le harnais est l'ombre portée de la CI dans l'atelier — il dit -> quand quelque chose a **régressé**, et il ne remplace jamais le regard sur -> l'écran (voir `docs/ATELIER.md` § 5 et § 6). - ### Les épreuves de parcours, et le contrat commun aux deux services Deux manques de l'audit sont comblés ici (NC-I-001, NC-I-010) — mais **hors de @@ -178,15 +133,11 @@ chargée, et son service. `lancerParcours(ctx)` les exécute tous (ou un seul, par `seulement`). **Comment les lancer.** Ils s'exécutent dans la console du navigateur, sur la page -de l'application (c'est là que les modules réels et le service vivant sont). Le -chemin à importer dépend de l'endroit d'où l'on parle : +de l'application (c'est là que les modules réels et le service vivant sont) : ```js -// dépôt livré (démonstration statique, auto-hébergement) : +// démonstration statique, ou auto-hébergement : const p = await import("tests/parcours.mjs"); -// atelier — la page est servie depuis le sous-domaine du générateur alors que ses -// modules viennent de l'origine de l'éditeur : il FAUT viser cette origine-là. -const p = await import("https://perchance.org/src/tests/parcours.mjs"); const ctx = await p.contexteDeLApercu(); // les modules de l'application, et son transport await p.lancerParcours(ctx); // tous les parcours @@ -209,8 +160,8 @@ s'installer (module absent) est **sauté**, jamais vert par accident. ## 3. L'intégration continue -`src/github/ci.yml` (recopié par l'export en `.github/workflows/ci.yml`) exécute -**trois** travaux à chaque `push`, chaque demande de fusion, et **sur demande** +`.github/workflows/ci.yml` exécute **trois** travaux à chaque `push`, chaque +demande de fusion, et **sur demande** (`workflow_dispatch` — de quoi rejouer la chaîne sans rien livrer, ce qui est la façon de relire un échec qu'on vient de corriger) : @@ -231,9 +182,8 @@ façon de relire un échec qu'on vient de corriger) : `GET /v1/health`, `GET /v1/db/health` (**schéma et migrations appliqués**, là où la santé ne prouve que le processus), la coquille servie, le code de l'application servi, `config.js` engendré depuis l'environnement. Aucune image - n'est poussée : aucun secret n'entre dans la chaîne. C'est la seule chose que - l'atelier ne peut pas faire, et c'est celle qui décide si une livraison - s'installe. + n'est poussée : aucun secret n'entre dans la chaîne. C'est elle qui décide si + une livraison s'installe. **QUAND ELLE ÉCHOUE, ELLE DIT MAINTENANT POURQUOI.** Le journal d'un travail n'est lisible que par un administrateur du dépôt : qui voit la chaîne rouge n'apprend pas @@ -272,7 +222,7 @@ qu'ils ne peuvent pas être produits hors de l'environnement d'installation : | Point | Pourquoi il n'est pas livré | Comment le faire | |---|---|---| -| **`package-lock.json`** | **livré** : le verrou du service (`src/server/mysql/package-lock.json`) est écrit à la main dans l'atelier, à partir du registre npm — chaque empreinte SHA-512 a été **vérifiée contre l'archive réellement retirée**, et la chaîne l'éprouve désormais à chaque envoi par `npm ci` (§3, 2e travail) | `cd src/server/mysql && npm ci` | +| **`package-lock.json`** | **livré** : le verrou du service (`src/server/mysql/package-lock.json`) est écrit à la main, à partir du registre npm — chaque empreinte SHA-512 a été **vérifiée contre l'archive réellement retirée**, et la chaîne l'éprouve désormais à chaque envoi par `npm ci` (§3, 2e travail) | `cd src/server/mysql && npm ci` | | **`package-lock.json` de la RACINE** | **livré** (1.6.3c, NC-I-015) : la racine du dépôt porte un verrou (lockfileVersion 3), si bien que `npm ci` y fonctionne même si le manifeste ne déclare aucune dépendance — la seule du dépôt (`mysql2`) reste verrouillée où elle vit | `npm ci` (racine), `cd src/server/mysql && npm ci` (service) | | **Images Docker épinglées** | livré : `node:20-alpine`, `mariadb:11`, `nginx:alpine` sont épinglées **par empreinte** dans `Dockerfile` et `docker-compose.yml` — et la chaîne **construit et met en service** l'image autonome à chaque envoi (§3, 3e travail), ce qui est la vraie preuve de l'épinglage | pour reprendre l'étiquette du jour : `docker buildx imagetools inspect node:20-alpine`, et reportez l'empreinte | | **Analyse statique** (linter) | **livrée** : `scripts/verifier-style.mjs` (sans dépendance) refuse `debugger`, les `var`, les traces de client, et — depuis la 1.6.1p — les **imports jamais employés** (`scripts/analyse-imports.mjs`, éprouvé). Aucun outil tiers n'est requis ; c'est un choix : un linter ajouterait une chaîne d'approvisionnement à auditer pour des règles que l'on écrit en quelques lignes. Reste à voir : la couverture des règles, à élargir au besoin | `npm run style` (les avertissements font échouer) ; en CI, `npm run verifier` | @@ -290,9 +240,9 @@ qu'ils ne peuvent pas être produits hors de l'environnement d'installation : - L'historique se tient dans **`src/CHANGELOG.md`** : une section **Non publié** décrit le travail en cours ; au moment de figer une version, on l'incrémente et on ouvre l'entrée datée. -- **Où chaque fichier vit** : l'export range l'outillage et les fichiers de racine - à leur place dans le dépôt (`scripts/`, `tests/`, `package.json`, - `.github/workflows/ci.yml`, `AGENTS.md`, `README.md`) et ne les recopie **pas** - dans `src/` — un fichier, un seul endroit. La table complète et la recette sont - dans `src/README.md` (§ « Exporter le dépôt GitHub »). +- **Où chaque fichier vit** : l'outillage et les fichiers de racine vivent à la + racine du dépôt (`scripts/`, `tests/`, `package.json`, `.github/workflows/ci.yml`, + `AGENTS.md`, `README.md`) et ne sont **pas** recopiés dans `src/` — un fichier, + un seul endroit. La disposition complète est dans `src/README.md` + (§ « Disposition des fichiers »). - Aucun artefact n'est construit : livrer, c'est **déposer le dépôt**. diff --git a/src/docs/PERFORMANCE.md b/src/docs/PERFORMANCE.md index 5e51132..203d24e 100644 --- a/src/docs/PERFORMANCE.md +++ b/src/docs/PERFORMANCE.md @@ -265,7 +265,7 @@ sur les nœuds rendus, eux, partent avec les nœuds — ceux-là sont sans risqu la nouvelle. - Vérifié : dix redessins n'ajoutent plus **rien** (0 / 0 / 0), contre +1 / +1 / +3 avant. L'épreuve de parcours `editeur-sans-fuite` - (`src/tests/parcours.mjs`) tient la règle : elle compte les écouteurs posés + (`tests/parcours.mjs`) tient la règle : elle compte les écouteurs posés MOINS ceux repris, et échoue si dix redessins en laissent derrière eux. > Le geste vaut pour tout l'atelier : la rédaction, l'organigramme et les diff --git a/src/docs/REPRISE.md b/src/docs/REPRISE.md index 9bbd25f..6c2ecd1 100644 --- a/src/docs/REPRISE.md +++ b/src/docs/REPRISE.md @@ -21,7 +21,6 @@ perdre plus de temps qu'il n'en fait gagner. | Où | Ce que c'est | Par où commencer la lecture | |---|---|---| -| `main.pjs` | Le **manifeste** : métadonnées de la page et imports de la plateforme (« Perchance »). Presque vide — c'est voulu. | en entier (court) | | `index.html` | La **coquille** : le script du service de démonstration (`<script type="text/x-server-plugin">`), le `<div id="app">`, et le module qui démarre l'application. | le bas du fichier (la ligne `import`) | | `src/ui/app.js` | Le **routeur** de l'application : lit l'adresse, choisit l'écran, gère la session. | `render()` | | `src/ui/views/` | Un fichier par **écran** (trames, rédaction, signature, recueil public…). | l'écran qui vous intéresse | @@ -29,7 +28,7 @@ perdre plus de temps qu'il n'en fait gagner. | `src/lib/db/index.js` | La **façade de persistance** : trois pilotes (navigateur, service partagé, MySQL), différences, miroir hors ligne, file d'écritures différées. | l'en-tête, puis `sync` | | `src/server/mysql/server.mjs` | Le **service auto-hébergé** (Node + son rangement — MariaDB, ou un dossier de fichiers) : routage HTTP, autorisation, magasin. | `handle()` | | `src/server/mysql/actes.mjs` | Le **domaine** signature/publication, **pur** (sans Node, sans réseau, sans base) — c'est le cœur métier du service. | en-tête, puis `hPublier` | -| `src/pages/host.js` · `src/server/web/host.js` | Les **hôtes d'exécution** : ils fournissent le stockage et le canal du service hors de l'environnement d'édition. | en-tête | +| `src/pages/host.js` · `src/server/web/host.js` | Les **hôtes d'exécution** : ils fournissent le stockage et le canal du service à la page servie en statique et au déploiement. | en-tête | **Deux modes de déploiement, un seul code d'application :** @@ -88,8 +87,8 @@ npm test # épreuves des modules purs + domaines du service npm run verifier # les deux, dans l'ordre ← c'est ce que la CI exécute ``` -À quoi s'ajoutent deux vérifications qui demandent un **navigateur** (l'aperçu de -l'éditeur, ou un poste) : +À quoi s'ajoutent deux vérifications qui demandent un **navigateur** (un poste de +travail, sur la page déployée) : - **les épreuves de parcours** — le trajet réel dépôt → signature → publication → recueil, et le contrat du service, exécutés **contre le service vivant** : @@ -98,11 +97,11 @@ l'éditeur, ou un poste) : - **l'apparence** — les écrans sensibles (recueil public, atelier) se relisent à l'écran, en mobile comme en grand. -> **Le piège à connaître.** L'édition en ligne recharge la page **à chaque -> modification** : une durée de vie d'état qui semblait stable peut se perdre. Les -> épreuves de parcours attendent explicitement ce qu'elles observent au lieu de -> supposer qu'un rendu est terminé. Voir `src/docs/INDUSTRIALISATION.md` (§ les -> parcours) pour la marche à suivre. +> **Le piège à connaître.** Un rechargement de la page remet l'état à zéro quand +> le service ne le garde pas : une durée de vie d'état qui semblait stable peut se +> perdre. Les épreuves de parcours attendent explicitement ce qu'elles observent au +> lieu de supposer qu'un rendu est terminé. Voir `src/docs/INDUSTRIALISATION.md` +> (§ les parcours) pour la marche à suivre. --- @@ -110,21 +109,11 @@ l'éditeur, ou un poste) : ### 4.1. Le dépôt GitHub (édition statique) -Le dépôt est servi **tel quel** : rien n'est compilé. L'outillage est rangé **à sa place attendue** — à la racine — et le code de -l'application sous `src/`. L'export range chaque source à sa place et ne la -recopie pas dans `src/` (une table complète, et la recette, sont dans -`src/README.md` § « Exporter le dépôt GitHub ») : - -| Élément | À la racine du dépôt | -|---|---| -| `src/docs/GITHUB.md` | `README.md` | -| `src/package.json` | `package.json` | -| `src/github/ci.yml` | `.github/workflows/ci.yml` | -| `src/github/gitignore` | `.gitignore` | -| `src/scripts/**`, `src/tests/**` | `scripts/**`, `tests/**` | -| `src/AGENTS.md`, `src/CLAUDE.md` | `AGENTS.md`, `CLAUDE.md` | - -Le site publié a besoin, à la racine, de `index.html`, `main.pjs`, `src/**`, et +Le dépôt est servi **tel quel** : rien n'est compilé. L'outillage est rangé **à sa +place attendue** — à la racine — et le code de l'application sous `src/` (la +disposition complète est dans `src/README.md`, § « Disposition des fichiers »). + +Le site publié a besoin, à la racine, de `index.html`, `src/**`, et d'un `.nojekyll` (pour que GitHub Pages serve `src/` sans transformation). Le domaine se règle par un fichier `CNAME`. diff --git a/src/docs/SORTIE-ATELIER.md b/src/docs/SORTIE-ATELIER.md index eba761f..5a58244 100644 --- a/src/docs/SORTIE-ATELIER.md +++ b/src/docs/SORTIE-ATELIER.md @@ -1,7 +1,7 @@ --- titre: Sortie d'atelier (Perchance) — le dépôt devient la seule source de vérité date: 2026-10-24 -statut: plan proposé, non démarré +statut: en cours — étapes 1-3 en relecture (PR #2, #3 et la suppression), étape 4 à trancher par Arno auteur: Vibe (Work) — validé par Arno pour exécution par une session Code --- diff --git a/src/docs/UI-UX.md b/src/docs/UI-UX.md index a972382..e3a9d27 100644 --- a/src/docs/UI-UX.md +++ b/src/docs/UI-UX.md @@ -361,7 +361,7 @@ est montré est réalisable dans le code actuel sans changer de socle. 4. **Lot 4 — le confort** (P4 ✅, P7 ✅, P8 ✅). Toutes les propositions ont été livrées. Chacune a reçu son **épreuve de parcours** -(`src/tests/parcours.mjs`) : la suite en compte aujourd'hui **vingt-cinq**, dont +(`tests/parcours.mjs`) : la suite en compte aujourd'hui **vingt-cinq**, dont dix nées de cette revue. Elles lisent les vraies classes des écrans et **remettent l'état** qu'elles ont touché (registre, chrono, tiroir, préférences du menu) pour ne pas perturber la suite. diff --git a/src/lib/logiciel.js b/src/lib/logiciel.js index 3744beb..49cb650 100644 --- a/src/lib/logiciel.js +++ b/src/lib/logiciel.js @@ -14,7 +14,7 @@ // au service de le lire, sans embarquer l'application. // // Pour changer de nom, de licence ou d'adresse de documentation, tout passe par -// ce fichier (et par `$meta` dans main.pjs pour le titre de la page). +// ce fichier (et par le `<title>` de `index.html` pour le titre de la page). // ============================================================================ // Le nom du logiciel, tel qu'il s'affiche : interface, pieds de page, bannière diff --git a/src/lib/publication-version.js b/src/lib/publication-version.js index 3cc6811..f586247 100644 --- a/src/lib/publication-version.js +++ b/src/lib/publication-version.js @@ -16,7 +16,7 @@ // Ces règles vivent ici, UNE SEULE FOIS : le client qui publie (voir `publier`, // src/ui/views/signature.js) et l'amorçage qui décide s'il doit republier (voir // `amorcerRecueil`, src/ui/demo-publications.js) les partagent — et l'épreuve -// les exerce sans navigateur (`src/tests/version-publication.test.mjs`). +// les exerce sans navigateur (`tests/version-publication.test.mjs`). // ============================================================================ // Le suffixe qui marque une publication comme celle d'une version donnée du jeu diff --git a/src/lib/version.js b/src/lib/version.js index 09cfbe2..00a65b0 100644 --- a/src/lib/version.js +++ b/src/lib/version.js @@ -9,8 +9,8 @@ // • `STATE_VERSION` (index.html) — le format de l'état durable du service ; // • `SEED_VERSION` (src/lib/store.js) — la version du jeu de démonstration. // -// Convention de versionnement (voir `src/CHANGELOG.md`, et « Flux de travail : -// Perchance ↔ GitHub » dans `src/README.md`) : +// Convention de versionnement (voir `src/CHANGELOG.md`, et « Flux de travail — +// le dépôt est la source de vérité » dans `src/README.md`) : // // • une version n'existe qu'une fois FIGÉE — c'est-à-dire déposée sur GitHub. // On ouvre et on date l'entrée du changelog à ce moment-là, jamais avant ; @@ -23,10 +23,10 @@ // dit la vérité, et il faut corriger `APP_VERSION`. // ============================================================================ -export const APP_VERSION = "1.6.3u"; +export const APP_VERSION = "1.6.3w"; // Date de la version courante (ISO, AAAA-MM-JJ). -export const APP_RELEASED = "2026-10-01"; +export const APP_RELEASED = "2026-10-24"; // Le changelog est un fichier du dépôt, pas une donnée recopiée dans le code : // l'écran « Documentation technique » le lit tel quel (comme README/SPEC/TODO). diff --git a/src/server/docker-compose.fichier.yml b/src/server/docker-compose.fichier.yml index 46b8609..10eac42 100644 --- a/src/server/docker-compose.fichier.yml +++ b/src/server/docker-compose.fichier.yml @@ -32,7 +32,7 @@ # LE DOSSIER DE DONNÉES (`./data`, à côté de ce fichier) est monté sur `/data` : # c'est là que TOUT est rangé — référentiel, trames, actes, comptes, sessions, # état de signature, journaux. SAUVEGARDER = copier ce dossier. Il est ignoré par -# git (voir src/github/gitignore), et n'a rien à faire dans un dépôt. +# git (voir .gitignore), et n'a rien à faire dans un dépôt. # ============================================================================ services: diff --git a/src/server/mysql/annuaire-service.test.mjs b/src/server/mysql/annuaire-service.test.mjs index 2d70649..df79e8a 100644 --- a/src/server/mysql/annuaire-service.test.mjs +++ b/src/server/mysql/annuaire-service.test.mjs @@ -16,7 +16,7 @@ // // Les règles « revendications → compte » sont une COPIE de celles du navigateur // (src/lib/oidc.js), que le service ne peut pas importer : son image ne contient -// que ce dossier. C'est `src/tests/purs.test.mjs` qui tient la concordance des +// que ce dossier. C'est `tests/purs.test.mjs` qui tient la concordance des // deux, en confrontant les deux implémentations sur les mêmes revendications. // ============================================================================ diff --git a/src/server/mysql/annuaire.test.mjs b/src/server/mysql/annuaire.test.mjs index 46c8dbf..710c6c3 100644 --- a/src/server/mysql/annuaire.test.mjs +++ b/src/server/mysql/annuaire.test.mjs @@ -87,7 +87,7 @@ test("ce qui n'est pas dans la liste blanche ne sort JAMAIS", () => { test("la liste blanche est celle des deux côtés", () => { // Le navigateur tient la même liste (ANNUAIRE_CLES, src/lib/auth.js) : les - // épreuves de src/tests/purs.test.mjs vérifient qu'elles ne divergent pas. + // épreuves de tests/purs.test.mjs vérifient qu'elles ne divergent pas. assert.equal(CHAMPS_PUBLICS.length, 19); assert.ok(CHAMPS_PUBLICS.includes("annuaire")); assert.ok(CHAMPS_PUBLICS.includes("roleMap")); diff --git a/src/server/mysql/logiciel-engendre.mjs b/src/server/mysql/logiciel-engendre.mjs index 9af1ec5..69e8b2d 100644 --- a/src/server/mysql/logiciel-engendre.mjs +++ b/src/server/mysql/logiciel-engendre.mjs @@ -7,8 +7,8 @@ // seul dossier `src/server/mysql/`, qui ne contient ni `src/lib/`, ni cette documentation. export const APP_NAME = "Scribae"; -export const APP_VERSION = "1.6.3u"; -export const VERSION_LABEL = "v1.6.3u"; -export const APP_RELEASED = "2026-10-01"; +export const APP_VERSION = "1.6.3w"; +export const VERSION_LABEL = "v1.6.3w"; +export const APP_RELEASED = "2026-10-24"; export const LICENCE = "GPLv3"; export const DOCUMENTATION = "https://doc.scribae.eu"; diff --git a/src/server/mysql/original-signe.mjs b/src/server/mysql/original-signe.mjs index f49bdc7..707f978 100644 --- a/src/server/mysql/original-signe.mjs +++ b/src/server/mysql/original-signe.mjs @@ -30,7 +30,7 @@ // Le service de DÉMONSTRATION (`index.html`, `<script type="text/x-server-plugin">`) // garde sa copie : son contexte d'exécution n'importe pas de modules. Un test la // tient, elle, pour équivalente au comportement de ce module (voir -// src/tests/original-signe.test.mjs) : la duplication ne peut plus dériver en +// tests/original-signe.test.mjs) : la duplication ne peut plus dériver en // silence. // ============================================================================ diff --git a/src/server/mysql/variables.test.mjs b/src/server/mysql/variables.test.mjs index 6e6ab74..208b8cd 100644 --- a/src/server/mysql/variables.test.mjs +++ b/src/server/mysql/variables.test.mjs @@ -172,7 +172,7 @@ test("une correspondance de groupes fautive est refusée ENTIÈREMENT", () => { test("les rôles de la correspondance sont ceux de l'application", () => { // La liste de référence est ROLE_ORDER (src/lib/users.js) ; les épreuves du - // navigateur (src/tests/purs.test.mjs) vérifient que les deux listes sont + // navigateur (tests/purs.test.mjs) vérifient que les deux listes sont // identiques — la copie locale tient ce module PUR, sans dépendance. assert.deepEqual(ROLES_CONNUS, ["visiteur", "redacteur", "reviseur", "signataire", "editeur", "administrateur"]); const r = lireVariables({ SCRIBA_ANNUAIRE_ROLE_DEFAUT: "prestataire" }); diff --git a/src/ui/brand.js b/src/ui/brand.js index 356c576..e8eee2c 100644 --- a/src/ui/brand.js +++ b/src/ui/brand.js @@ -12,8 +12,8 @@ // deux vérités pour un nom. On le réexporte pour que l'interface n'ait qu'un // module d'identité à connaître. // -// Pour changer de devise ou de marque, tout passe par ce fichier (et par -// `$meta` dans main.pjs pour le titre de la page). +// Pour changer de devise ou de marque, tout passe par ce fichier (et par le +// `<title>` de `index.html` pour le titre de la page). // ============================================================================ export { APP_NAME } from "../lib/logiciel.js"; export const APP_TAGLINE = "Trames, rédaction et publication des actes administratifs"; diff --git a/src/ui/views/docs.js b/src/ui/views/docs.js index e2ef5b9..521042c 100644 --- a/src/ui/views/docs.js +++ b/src/ui/views/docs.js @@ -72,11 +72,6 @@ const DOCUMENTS = [ path: "src/README.md", title: "Notes de développement", desc: "Architecture du code, rôle de chaque module, partis pris techniques.", }, - { - id: "atelier", group: "Conception", - path: "src/docs/ATELIER.md", title: "Travailler dans l'atelier", - desc: "La méthode, les dix garde-fous, la vérification sans terminal (le harnais d'épreuves), et ce qui compte comme « terminé ».", - }, { id: "todo", group: "Conception", path: "src/TODO.md", title: "Chantiers ouverts", diff --git a/tests/README.md b/tests/README.md index ffde237..a451e1e 100644 --- a/tests/README.md +++ b/tests/README.md @@ -42,22 +42,9 @@ node --test tests/ # ce dossier seul ## Pourquoi les imports disent `../src/…` Le dépôt range l'outillage à sa **racine** (`scripts/`, `tests/`) et le code de -l'application sous **`src/`** : c'est la disposition **livrée**, et c'est pour +l'application sous **`src/`** : c'est la disposition du dépôt, et c'est pour elle que ces fichiers sont écrits (le code est donc `../src/lib/…`, `../src/ui/…` -et l'outillage `../scripts/…`). - -Dans l'atelier, l'outillage vit sous `src/` — `src/tests/` est le seul arbre que -la plateforme conserve d'une séance à l'autre —, et le harnais d'essai y simule -la disposition livrée. `scripts/racine-code.mjs` **constate** la racine du code -au lieu de la supposer : c'est ce qui évite le piège de NC-I-009, où un décalage -entre l'outil et l'arborescence qu'il contrôle avait éteint toutes ses +et l'outillage `../scripts/…`). `scripts/racine-code.mjs` **constate** la racine +du code au lieu de la supposer : c'est ce qui évite le piège de NC-I-009, où un +décalage entre l'outil et l'arborescence qu'il contrôle avait éteint toutes ses exemptions. - -## Les jouer dans l'atelier - -Ces épreuves ne se lancent pas au terminal dans l'atelier : c'est -**`scripts/harnais-atelier.mjs`** qui les rejoue (un fichier à la fois, comme -`node --test` les isole), en même temps que la syntaxe, le style et les -générateurs. La recette, les **chiffres attendus** et la liste des écarts connus -(qui sont un artefact du harnais, jamais une excuse) sont dans -**`docs/ATELIER.md` § 3** — le document à lire avant de travailler dans l'atelier. diff --git a/tests/conformite-service.test.mjs b/tests/conformite-service.test.mjs index a08c310..8916dc3 100644 --- a/tests/conformite-service.test.mjs +++ b/tests/conformite-service.test.mjs @@ -29,8 +29,8 @@ async function lireIndex() { try { ({ readFile } = await import("node:fs/promises")); } catch (e) { readFile = null; } if (typeof readFile !== "function") return null; // La première adresse est ANCRÉE à ce fichier (`index.html` vit deux crans - // au-dessus de `tests/` : le dépôt livré comme l'atelier la trouvent), les - // autres couvrent une exécution lancée depuis un autre dossier. + // au-dessus de `tests/`), les autres couvrent une exécution lancée depuis un + // autre dossier. const chemins = []; try { const { fileURLToPath } = await import("node:url"); diff --git a/tests/file-attente.test.mjs b/tests/file-attente.test.mjs index 977a9cb..b5a8c9d 100644 --- a/tests/file-attente.test.mjs +++ b/tests/file-attente.test.mjs @@ -19,9 +19,9 @@ // // CE FICHIER N'IMPORTE QU'UN SEUL MODULE (`src/lib/db/index.js`) : le service de // données est doublé par `fetch`. C'est délibéré — une épreuve qui importe aussi -// `src/lib/auth.js` éprouve DEUX copies du module dans l'atelier (voir -// docs/ATELIER.md § 3.3), alors qu'ici le pilote se règle par son propre -// réglage (`setSettings`), sans dépendre de la découverte du mode. +// un second module du code épreuve DEUX copies de chaque module, alors qu'ici le +// pilote se règle par son propre réglage (`setSettings`), sans dépendre de la +// découverte du mode. // ============================================================================ import test from "node:test"; import assert from "node:assert/strict"; diff --git a/tests/industrialisation.test.mjs b/tests/industrialisation.test.mjs index f7f6ba4..1a0e459 100644 --- a/tests/industrialisation.test.mjs +++ b/tests/industrialisation.test.mjs @@ -89,9 +89,8 @@ const RACINE_TEST = (() => { catch (e) { return null; } })(); -// Le code vit sous `src/` à la racine du dépôt livré, et à la racine de -// l'outillage dans l'atelier (voir tests/README.md) : on essaie les deux -// préfixes plutôt que d'en figer un, et les appels n'ont pas à s'en soucier. +// Le code vit sous `src/` à la racine du dépôt : le préfixe est essayé d'abord +// ancré, puis tel quel, et les appels n'ont pas à s'en soucier. async function lire(chemin) { if (!RACINE_TEST) return null; for (const prefixe of ["src/", ""]) { @@ -147,12 +146,11 @@ test("la façade sert en JavaScript les extensions que le client importe", async // // Ces deux épreuves naissent de la même leçon que celle du contrôle de style // ci-dessus, et de deux pannes réelles de la chaîne d'intégration (2026-09-27) : -// un chemin JUSTE dans l'atelier et FAUX dans le dépôt. Elles lisent les -// fichiers, sans lancer de processus — elles tournent donc AUSSI dans l'atelier, -// là où le défaut se fabrique. +// un chemin juste dans un dossier, et faux dans un autre. Elles lisent les +// fichiers, sans lancer de processus. test("la chaîne cite le guetteur d'échecs par un chemin ancré à sa racine", async (ctx) => { - const ci = (await lire(".github/workflows/ci.yml")) ?? (await lire("github/ci.yml")); + const ci = await lire(".github/workflows/ci.yml"); if (!ci) return ctx.skip(SKIP); const lignes = ci.split("\n").filter((l) => /\bbash\s+\S*annoncer-echecs[.]sh/.test(l)); assert.ok(lignes.length, "la chaîne doit appeler le guetteur d'échecs (sinon cette épreuve ne prouve rien)"); @@ -160,19 +158,18 @@ test("la chaîne cite le guetteur d'échecs par un chemin ancré à sa racine", assert.match( ligne, /[$]GITHUB_WORKSPACE[/]scripts[/]annoncer-echecs[.]sh/, - "le guetteur doit être cité par un chemin ANCRÉ à la racine du dépôt : le travail « Service auto-hébergé » s'exécute depuis `src/server/mysql`, où un `../../scripts/…` désigne `src/scripts/…` — qui n'existe que dans l'atelier. La chaîne sortait donc en 127 (script introuvable) au lieu d'annoter l'épreuve rouge (NC-I-008) :\n" + ligne, + "le guetteur doit être cité par un chemin ANCRÉ à la racine du dépôt : le travail « Service auto-hébergé » s'exécute depuis `src/server/mysql`, où un `../../scripts/…` désigne un autre dossier. La chaîne sortait donc en 127 (script introuvable) au lieu d'annoter l'épreuve rouge (NC-I-008) :\n" + ligne, ); } }); -test("l'épreuve de navigateur cherche la suite de parcours dans les deux dispositions", async (ctx) => { +test("l'épreuve de navigateur cherche la suite de parcours à la racine", async (ctx) => { const source = await lire("tests/parcours-navigateur.mjs"); if (!source) return ctx.skip(SKIP); - // Dans l'atelier, la suite est `src/tests/parcours.mjs` ; dans le dépôt, `tests/` - // est remonté à la racine et la suite est `/tests/parcours.mjs`. Un import figé - // sur `/src/tests/` répond 404 dans la chaîne, et le navigateur lève + // La suite est `tests/parcours.mjs`, à la racine du dépôt. Un import figé sur + // un autre chemin répond 404 dans la chaîne, et le navigateur lève // « Failed to fetch dynamically imported module » : le travail `parcours` // échouait donc sans jamais jouer un seul parcours (2026-09-27). - assert.match(source, /['"]\/src\/tests\/parcours[.]mjs['"]/, "la disposition de l'atelier doit être citée"); - assert.match(source, /['"]\/tests\/parcours[.]mjs['"]/, "la disposition du dépôt (où `tests/` est à la racine) doit être citée"); + assert.match(source, /['"]\/tests\/parcours[.]mjs['"]/, "la suite de parcours doit être citée à la racine du dépôt"); + assert.doesNotMatch(source, /['"]\/src\/tests\//, "aucun chemin de l'ancienne double disposition ne doit rester"); }); diff --git a/tests/parcours-navigateur.mjs b/tests/parcours-navigateur.mjs index ffe530c..1e19db5 100644 --- a/tests/parcours-navigateur.mjs +++ b/tests/parcours-navigateur.mjs @@ -42,19 +42,14 @@ import { extname, join, normalize } from "node:path"; import { fileURLToPath } from "node:url"; // ------------------------------------------------------------------ la racine -// L'application est le dossier qui PORTE `index.html`. Deux dispositions -// existent, comme pour tout l'outillage de ce dépôt : le dépôt livré range le -// code sous `src/` et la page à sa racine (l'épreuve est alors `tests/…`) ; -// l'atelier range tout sous `src/` en laissant la page à la racine. On essaie -// les deux, et l'on garde celui qui porte la page — un chemin figé serait faux -// dans l'autre disposition (audit, NC-I-009). +// L'application est le dossier qui PORTE `index.html`. Le chemin est constaté, +// une seule fois : on garde le dossier qui porte la page — un chemin figé serait +// faux si l'arborescence changeait (audit, NC-I-009). const RACINE = await (async () => { - for (const relatif of ["..", "../.."]) { - const dossier = fileURLToPath(new URL(relatif + "/", import.meta.url)); - try { await stat(join(dossier, "index.html")); return dossier; } - catch (e) { /* disposition suivante */ } - } - throw new Error("index.html introuvable : l'épreuve ne sait pas où est l'application."); + const dossier = fileURLToPath(new URL("..", import.meta.url)); + try { await stat(join(dossier, "index.html")); return dossier; } + catch (e) { /* repli : le dossier courant */ } + return fileURLToPath(new URL(".", import.meta.url)); })(); // --------------------------------------------------------------- le serveur @@ -151,7 +146,7 @@ const INJECTION = [ " if (!m.state.user) throw new Error('aucune session ouverte apres login : les parcours seraient tous « sans objet »');", " for (let i = 0; i < 160 && !document.querySelector('.app-header'); i += 1) await pause(250);", " let p = null; let dernier = null;", - " for (const chemin of ['/src/tests/parcours.mjs', '/tests/parcours.mjs']) {", + " for (const chemin of ['/tests/parcours.mjs']) {", " try { p = await import(chemin); break; } catch (e) { dernier = e; }", " }", " if (!p) throw new Error('la suite de parcours est introuvable — ' + String((dernier && dernier.message) || dernier));", diff --git a/tests/parcours.mjs b/tests/parcours.mjs index a8c0203..4055f54 100644 --- a/tests/parcours.mjs +++ b/tests/parcours.mjs @@ -12,13 +12,13 @@ // quelques PARCOURS CRITIQUES, joués sur l'application réelle, dans l'ordre où un // agent les rencontre. // -// COMMENT ON S'EN SERT. Le module s'importe DANS la page de l'application (aperçu -// de l'éditeur, ou application déployée), ce qui lui donne accès aux mêmes -// modules que l'application — les mêmes instances, donc le même état : +// COMMENT ON S'EN SERT. Le module s'importe DANS la page de l'application +// (la démonstration statique, ou une application déployée), ce qui lui donne +// accès aux mêmes modules que l'application — les mêmes instances, donc le +// même état : // // import("tests/parcours.mjs").then((p) => p.lancerParcours()) -// (dans l'atelier : import("https://perchance.org/src/tests/parcours.mjs") — voir -// docs/INDUSTRIALISATION.md § 2) +// (voir docs/INDUSTRIALISATION.md § 2) // // Chaque parcours rend "" quand il passe, ou la RAISON de son échec. Aucun // parcours ne modifie durablement l'application : ce qui est changé (la route, @@ -30,17 +30,13 @@ // `tests/conformite-service.mjs`. // ============================================================================ -// OÙ VIT LE CODE. Ce module sert DEUX dispositions : dans le dépôt livré il est -// `tests/parcours.mjs` et le code est `../src/…` ; dans l'atelier il est -// `src/tests/parcours.mjs` et le code est `../…`. On essaie les deux, une seule -// fois, et l'on garde celui qui porte `lib/version.js` : un chemin d'import figé -// serait faux dans l'autre disposition — c'est le piège de NC-I-009. +// OÙ VIT LE CODE. Ce module est `tests/parcours.mjs` et le code est `../src/…`. +// Le chemin d'import est constaté, une seule fois : on garde celui qui porte +// `lib/version.js` — un chemin figé serait faux si l'arborescence changeait +// (c'est le piège de NC-I-009). const RACINE_CODE = await (async () => { - for (const essai of ["../", "../src/"]) { - try { await import(essai + "lib/version.js"); return essai; } - catch (e) { /* disposition suivante */ } - } - return "../"; + try { await import("../src/lib/version.js"); return "../src/"; } + catch (e) { return "../"; } })(); // Ce dont un parcours a besoin : de quoi lire et manœuvrer l'application. On diff --git a/tests/purs.test.mjs b/tests/purs.test.mjs index 533bc4b..96814fa 100644 --- a/tests/purs.test.mjs +++ b/tests/purs.test.mjs @@ -683,14 +683,13 @@ test("service : les champs que le client lit de `GET /v1/auth/config` lui sont t try { ({ readFile } = await import("node:fs/promises")); } catch (e) { readFile = null; } if (typeof readFile !== "function") return t.skip("lecture de fichier indisponible hors dépôt"); // Le dossier de CE fichier est le repère sûr : `node --test` peut partir de la - // racine du dépôt comme de `tests/`. Les deux dispositions sont essayées — le - // dépôt livré (`../src/…`) et l'atelier, où le code et l'outillage voisinent - // (`../…`) —, puis les chemins du dossier courant, qui couvrent les autres cas. + // racine du dépôt comme de `tests/`. Le chemin ancré au fichier couvre les deux + // cas ; les chemins du dossier courant servent de repli. let ici = () => null; try { const { fileURLToPath } = await import("node:url"); const base = import.meta.url; - // Le calcul est protégé ICI : dans un harnais de navigateur, l'adresse du + // Le calcul est protégé ICI : sous le harnais de parcours (`tests/parcours-navigateur.mjs`), l'adresse du // module n'est pas hiérarchique, et résoudre un chemin relatif contre elle // lève. L'essai rend alors `null`, et les chemins relatifs prennent la suite. ici = (rel) => { try { return fileURLToPath(new URL(rel, base)); } catch (e) { return null; } }; @@ -702,8 +701,8 @@ test("service : les champs que le client lit de `GET /v1/auth/config` lui sont t } return null; }; - const auth = await lire([ici("../src/lib/auth.js"), ici("../lib/auth.js"), "src/lib/auth.js", "lib/auth.js"]); - const etat = await lire([ici("../src/ui/state.js"), ici("../ui/state.js"), "src/ui/state.js", "ui/state.js"]); + const auth = await lire([ici("../src/lib/auth.js"), "src/lib/auth.js"]); + const etat = await lire([ici("../src/ui/state.js"), "src/ui/state.js"]); if (!auth || !etat) return t.skip("sources illisibles hors du dépôt"); const debut = auth.indexOf("export function setDeploiementAuth"); diff --git a/tests/version-publication.test.mjs b/tests/version-publication.test.mjs index adb14c6..b4d63dd 100644 --- a/tests/version-publication.test.mjs +++ b/tests/version-publication.test.mjs @@ -74,9 +74,9 @@ test("cleIdempotencePublication : la clé porte l'expression, et deux expression const SKIP = "index.html illisible ici"; -// L'adresse d'`index.html` : au-dessus de `tests/`, dans les deux dispositions -// (dépôt livré et atelier). Un module sans adresse hiérarchique ne doit pas -// faire échouer l'épreuve, seulement la faire changer de chemin. +// L'adresse d'`index.html` : au-dessus de `tests/`, ancrée à ce fichier. Un +// module sans adresse hiérarchique ne doit pas faire échouer l'épreuve, +// seulement la faire changer de chemin. async function lireIndex() { const noms = []; try {