gestion_table_tournante_libre/src/stockage/depot.js

1218 lines
61 KiB
JavaScript
Raw Normal View History

[ADD] storage: event store — list, create, open, save, recover An event is a state file and a journal paired by its internal identifier, never by name (§ 8.6): a hand copy of a state does not take the original's journal, and a renamed journal stays paired. Each gesture writes its journal line first, then the .precedent and the state (§ 8.2); recovery compares revisions, never file dates, so a cut leaves the journal one entry ahead at most. Opening never writes (§ 8.4); an unreadable state names its readable secours instead of opening an empty event, and a newer format stays read-only. Writing elsewhere after a removed medium aligns the pair first. Checked: 60 tests, 100 % branches of depot.js, 1257 node tests on HEAD. --- FR --- [ADD] stockage : dépôt d'événements — lister, créer, ouvrir, reprendre Un événement est un fichier d'état et un journal appariés par l'identifiant interne, jamais par le nom (§ 8.6) : un état copié à la main ne prend pas le journal de l'original, et un journal renommé reste apparié. Chaque geste écrit sa ligne de journal d'abord, puis le .precedent et l'état (§ 8.2) ; la reprise compare les révisions, jamais la date des fichiers : une coupure laisse le journal en avance d'une entrée au plus. Ouvrir n'écrit jamais (§ 8.4) ; un état illisible nomme ses secours lisibles au lieu d'ouvrir un événement vide, et un format plus récent reste en lecture seule. Écrire ailleurs après un support retiré aligne d'abord la paire. Vérifié : 60 épreuves, 100 % des branches de depot.js, 1257 épreuves node. Assisted-by: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01EUXSGcwCLSC69FWEdCtWSb
2026-10-06 20:39:49 -04:00
// © 2026 TechnoLibre (http://www.technolibre.ca)
// License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
// Le dépôt des événements (§ 8.1, § 8.2, § 8.4, § 8.6, § 8.8, § 8.9) : lister
// un dossier de travail, créer un événement, l'ouvrir, enregistrer ses gestes,
// nommer ses instants, le reprendre après une panne, l'écrire ailleurs. Chaque
// fonction reçoit le système de fichiers (§ 13.4, § 14.10) ; aucune ne lit
// l'horloge ni un aléa : horodatage et version entrent en paramètre, et les
// libellés, textes figés, viennent de l'appelant (§ 14.6).
//
// Un événement est une paire de fichiers que l'identifiant interne apparie,
// jamais le nom (§ 8.6) : <base>.gtt.json, l'état, et le journal de la même
// base quand il porte cet identifiant, sinon le premier du dossier, par nom,
// qui le porte sans appartenir à sa propre base — dont l'état, ou faute
// d'état lisible le .precedent, porte le même identifiant. Un état copié à la
// main dans le même dossier ne s'apparie donc pas au journal de l'original.
// Un état illisible s'apparie par l'identifiant de son .precedent ; sans
// lui, au journal de sa base, quand aucun état lisible du dossier ne porte
// l'identifiant de ce journal. L'écriture suit la même règle : le journal
// d'un événement dont le journal de la base porte un autre événement s'écrit
// dans celui que son identifiant apparie, et ne se crée sous une base libre
// qu'à défaut. Lister, ouvrir et ouvrir un secours ne font que lire (§ 8.4) :
// ce qu'ils trouvent à réparer, le premier geste le répare.
//
// Un geste écrit le journal d'abord, l'état ensuite (§ 8.2). Le journal reçoit
// une ligne par ajout en fin quand il continue tel quel. Il se réécrit en
// entier, par une écriture atomique, quand il reçoit une entrée de reprise,
// seule ou avant la ligne d'un geste ou d'un jalon, quand sa lecture a écarté
// des lignes, que la lecture suivante ne dépasserait pas, quand il manque, et
// quand il dépasse PLAFOND_ENTREES. Le correctif d'une entrée part de la
// charge que restitue l'entrée qui la précède dans le journal, jamais de la
// charge ouverte : un correctif appliqué à une autre charge que la sienne
// s'applique sans lever et rend autre chose.
//
// La reprise à l'ouverture (§ 8.8) compare les révisions, jamais la date des
// fichiers (§ 14.10). Un journal en avance d'une entrée sur l'état se
// rattrape en mémoire. Un journal plus en avance, ou en retard, laisse l'état
// tel quel : la première écriture inscrit une entrée de reprise, qui
// restitue la charge ouverte à la suite du journal. La comparaison ne
// rattrape un geste interrompu que si l'état sur le disque porte la révision
// qui précède sa ligne. Quand il en porte une autre — après un rattrapage,
// une reprise, un secours, un état resté non écrit —, le geste pose d'abord
// la reprise, s'il en faut une, puis l'état à la révision de la dernière
// entrée, et seulement ensuite sa ligne : une coupure laisse le journal en
// avance d'une entrée au plus. Un état illisible lève ETAT_ILLISIBLE et nomme
// les secours lisibles, sans jamais ouvrir un événement vide.
//
// Écrire l'événement ailleurs (§ 8.6) pose une paire alignée, comme un
// geste : le journal en mémoire, en entier — suivi de l'entrée de reprise
// quand il finit sur une autre révision que la charge ouverte —, puis l'état
// que restitue sa dernière entrée, à sa révision. Rouvert là-bas,
[ADD] storage: copy, rename, import, delete events; local settings § 8.7 operations reuse the event store's pairing and alignment instead of copying them. Renaming writes the complete pair under the new name — the store now writes a gesture with the pair it writes elsewhere — before deleting the old one, so no cut leaves zero pairs; an unchanged name writes nothing. Deleting moves state, .precedent, journal, lock and leftovers into a dated trash, matching names as the case-insensitive disk does. creerDossier creates only the root it is aimed at, so a vanished working folder never comes back empty. reglages_locaux.json keeps exactly the two § 8.5 settings; its deferred writer always keeps the last demand. Checked: 1314 node, 35 browser, 27 node-long tests; depot.js at 100 %. --- FR --- [ADD] stockage : copier, renommer, importer, supprimer ; réglages locaux Les opérations du § 8.7 reprennent l'appariement et l'alignement du dépôt au lieu de les recopier. Renommer écrit la paire complète sous le nouveau nom — le dépôt écrit désormais un geste avec la paire qu'il pose ailleurs — avant de supprimer l'ancienne : aucune coupure ne laisse zéro paire ; un nom inchangé n'écrit rien. Supprimer range état, .precedent, journal, verrou et restes dans une corbeille datée, en comparant les noms comme le disque insensible à la casse. creerDossier ne crée que la racine visée : un dossier de travail disparu ne renaît pas vide. reglages_locaux.json garde les deux réglages du § 8.5 ; son écrivain différé garde la dernière demande. Vérifié : 1314 node, 35 navigateur, 27 node-long ; depot.js à 100 %. Assisted-by: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01EUXSGcwCLSC69FWEdCtWSb
2026-10-06 23:17:05 -04:00
// l'événement rend la charge écrite sans rien rattraper. Avec un geste, la
// paire porte l'entrée de ce geste à la suite du journal, et sa charge : le
// renommage (§ 8.7) écrit ainsi la paire complète sous le nouveau nom, dans
// le dossier même, sans toucher l'ancienne ni créer le dossier. Son
// .precedent, l'état d'avant le geste, s'écrit avant le journal : la
// nouvelle base appartient à l'événement dès que son journal existe, et
// l'ancienne paire ne s'apparie jamais ce journal.
[ADD] storage: event store — list, create, open, save, recover An event is a state file and a journal paired by its internal identifier, never by name (§ 8.6): a hand copy of a state does not take the original's journal, and a renamed journal stays paired. Each gesture writes its journal line first, then the .precedent and the state (§ 8.2); recovery compares revisions, never file dates, so a cut leaves the journal one entry ahead at most. Opening never writes (§ 8.4); an unreadable state names its readable secours instead of opening an empty event, and a newer format stays read-only. Writing elsewhere after a removed medium aligns the pair first. Checked: 60 tests, 100 % branches of depot.js, 1257 node tests on HEAD. --- FR --- [ADD] stockage : dépôt d'événements — lister, créer, ouvrir, reprendre Un événement est un fichier d'état et un journal appariés par l'identifiant interne, jamais par le nom (§ 8.6) : un état copié à la main ne prend pas le journal de l'original, et un journal renommé reste apparié. Chaque geste écrit sa ligne de journal d'abord, puis le .precedent et l'état (§ 8.2) ; la reprise compare les révisions, jamais la date des fichiers : une coupure laisse le journal en avance d'une entrée au plus. Ouvrir n'écrit jamais (§ 8.4) ; un état illisible nomme ses secours lisibles au lieu d'ouvrir un événement vide, et un format plus récent reste en lecture seule. Écrire ailleurs après un support retiré aligne d'abord la paire. Vérifié : 60 épreuves, 100 % des branches de depot.js, 1257 épreuves node. Assisted-by: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01EUXSGcwCLSC69FWEdCtWSb
2026-10-06 20:39:49 -04:00
//
// Un état sur le disque, ou un journal de la base, d'un format plus récent
// que FORMAT garde en lecture seule tout ce qui ouvre l'événement, secours
// compris : rien de ce qu'une construction plus récente a écrit dans l'état
// ou dans le journal de la base ne se remplace (§ 8.8). Un journal de la base
// plus récent, dont ce code ne lit pas l'identifiant, se reconnaît avant tout
// appariement, à l'ouverture comme dans la liste, et son secours se refuse au
// lieu de se dire absent. Un .precedent d'un format plus récent
// ne garde que l'Ouvert que son secours ouvre : un geste parti d'une autre
// ouverture le remplace, comme tout .precedent, quand l'état sur le disque
// se lit.
import { canoniser, serialiser } from './canonique.js';
import { FORMAT, analyser } from './document.js';
import { ErreurStockage } from './erreurs.js';
import {
PLAFOND_ENTREES,
elaguer,
estInstantane,
ligneEntree,
ligneJalon,
ligneOuverture,
lireJournal,
reconstruire,
versionDe,
} from './journal.js';
import { SUFFIXES, deriverBase } from './noms.js';
import { examiner } from './placements.js';
/**
* Un événement ouvert. Chaque fonction qui écrit rend l'Ouvert suivant, et ne
* modifie pas celui qu'elle reçoit.
*
* @typedef {Object} Ouvert
* @property {import('./systeme_fichiers.js').SystemeFichiers} fs
* @property {import('./systeme_fichiers.js').Racine} racine
* @property {string} base base du fichier d'état
* @property {import('./types.js').Charge} charge courante, que rend
* examiner : propositions fautives écartées, compteur relevé
* @property {number} revision révision de la charge courante
* @property {string} produitVersion construction qui a produit la charge
* courante : en-tête lu, version que le journal note pour sa
* révision, ou celle du dernier geste
* @property {string|null} texteEtat texte lisible du fichier d'état sur le
* disque ; null quand il ne l'est pas, ou qu'il manque
* @property {number|null} revisionEtat révision de ce texte, null avec lui :
* quand elle n'est pas celle de la dernière entrée du journal, le
* geste suivant écrit d'abord l'état à cette entrée
* @property {import('./journal.js').Journal|null} journal le journal de
* l'événement, lu jusqu'à son dernier instant restituable ; null
* quand il n'a aucune entrée à lui
* @property {string|null} baseJournal base du fichier du journal, ou de celui
* que la prochaine écriture crée ; null quand le fichier de la base
* porte un autre événement : la prochaine écriture prend le journal
* que l'identifiant de l'événement apparie (apparier), sinon une
* base libre que son nom dérive
* @property {import('./types.js').Charge|null} chargeJournal charge que
* restitue la dernière entrée du journal, d'où part le correctif
* de la suivante ; null sans journal
* @property {boolean} journalAReecrire le fichier du journal porte des
* lignes que la lecture a écartées : la prochaine écriture le
* réécrit sans elles
* @property {boolean} formatPlusRecent lecture seule (§ 8.8) : l'état sur
* le disque, le .precedent qu'un secours ouvre ou le journal de la
* base est d'un format plus récent que FORMAT
* @property {import('./placements.js').Examen} examen
* @property {Array<{code: string, details: Object}>} avertissements dans
* cet ordre, chacun au plus une fois : RETENU_FAUTIF {} ;
* JOURNAL_REPRIS {}, le journal en avance d'une entrée rattrapé en
* mémoire ; JOURNAL_EN_AVANCE_DE {n} ou JOURNAL_INCOMPLET {etat,
* journal}, le journal qui finit n révisions après la charge, ou
* avant elle ; JOURNAL_ECARTEES {n} ; JOURNAL_FORMAT_PLUS_RECENT
* {format}, le journal de la base, d'un format plus récent, que
* rien ne lit ni n'écrit ; JOURNAL_ETRANGER {evenement}, le journal
* de la base, d'un autre événement, que rien ne lit ;
* PROPOSITIONS_FAUTIVES {ids}, par rang, null pour un identifiant
* illisible ; DERIVE {ids, retenu}, les propositions périmées par
* identifiant, et vrai quand le retenu l'est (§ 9)
*/
/**
* Un événement du dossier, tel que lister le résume.
*
* Les champs qui viennent de l'état lu — nom, date, etat, participants —
* valent null quand il ne se lit pas ou qu'il manque ; id aussi, sauf pour un
* journal sans état, dont il est l'événement que nomme sa première ligne.
*
* @typedef {Object} Resume
* @property {string} base
* @property {string|null} id
* @property {string|null} nom
* @property {string|null} date
* @property {string|null} etat
* @property {number|null} participants exclus compris
* @property {number} modifie du fichier d'état, ou du journal sans
* état ; pour l'affichage seulement
* @property {string|null} journal base du journal apparié
* @property {string|null} illisible raison d'un état illisible, ABSENT
* pour un journal sans état
* @property {boolean} doublon un autre état lisible du dossier porte
* le même identifiant
*/
// Suffixes des fichiers d'un événement : une base que l'un d'eux porte dans
// un dossier n'est pas libre.
const SUFFIXES_EVENEMENT = [SUFFIXES.etat, SUFFIXES.precedent, SUFFIXES.journal, SUFFIXES.verrou];
// Deux chaînes, comparées unité UTF-16 par unité.
const comparerTextes = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
// Base d'un nom de fichier qui finit par suffixe, ou null.
const baseDe = (nom, suffixe) => (nom.endsWith(suffixe) ? nom.slice(0, nom.length - suffixe.length) : null);
// Bases que portent les fichiers d'événement d'une liste d'entrées de dossier.
const basesDe = (entrees) =>
entrees.flatMap(({ nom }) =>
SUFFIXES_EVENEMENT.map((suffixe) => baseDe(nom, suffixe)).filter((base) => base !== null),
);
// Bases des fichiers d'une liste d'entrées de dossier dont le nom finit par
// suffixe, dans son ordre.
const basesAuSuffixe = (entrees, suffixe) =>
entrees
.filter(({ type }) => type === 'fichier')
.map(({ nom }) => baseDe(nom, suffixe))
.filter((base) => base !== null);
// Texte du fichier chemin, ou null quand il manque ; toute autre erreur
// remonte telle quelle.
async function lireSiPresent(fs, racine, chemin) {
try {
return await fs.lireTexte(racine, chemin);
} catch (erreur) {
if (erreur.code === 'ABSENT') return null;
throw erreur;
}
}
// Lecture d'un texte d'état : { lecture } quand l'analyse l'admet, sinon
// { details } de son refus, raison et chemin. analyser ne lève que
// ETAT_ILLISIBLE.
function lireEtat(texte) {
try {
return { lecture: analyser(texte), details: null };
} catch (erreur) {
return { lecture: null, details: erreur.details };
}
}
// Ce que l'Ouvert tient de l'état sur le disque, d'après le texte et sa
// lecture (lireEtat), lecture et details null quand il manque : texteEtat et
// revisionEtat quand il se lit, null sinon ; formatPlusRecent quand il vient
// d'un format plus récent, qu'il se lise, réduit, ou que son refus porte ce
// format (analyser). Un tel état garde l'Ouvert en lecture seule, même ouvert
// d'un secours (§ 8.8).
function surDisque(texte, { lecture, details }) {
if (lecture !== null) {
return { texteEtat: texte, revisionEtat: lecture.entete.revision, formatPlusRecent: lecture.formatPlusRecent };
}
return { texteEtat: null, revisionEtat: null, formatPlusRecent: (details?.format ?? FORMAT) > FORMAT };
}
// Le texte d'état, que l'analyse doit relire : un état que ce module écrit se
// rouvre toujours. Une charge qu'elle refuserait lève TypeError, avant toute
// écriture.
function relisible(texte, fonction) {
const { details } = lireEtat(texte);
if (details !== null) {
throw new TypeError(`${fonction} : l'état ne se relirait pas, ${details.raison} ${details.chemin}`);
}
return texte;
}
// Événement que nomme la première ligne d'un texte de journal, null quand le
// texte manque ou que cette ligne ne se lit pas : la lecture du journal
// réduite à cette ligne.
const evenementDe = (texte) => (texte === null ? null : lireJournal(texte.slice(0, texte.indexOf('\n') + 1)).evenement);
// Format que déclare la première ligne d'un texte de journal quand c'est un
// entier plus grand que FORMAT, null sinon. lireJournal refuse une telle
// ouverture comme toute ligne illisible ; son format dit qu'elle vient d'une
// construction plus récente, dont le journal ne se remplace pas (§ 8.8). Une
// première ligne sans fin de ligne, qui n'est pas du JSON, ou dont la valeur
// est null, ne déclare rien.
function formatPlusRecentDe(texte) {
let format;
try {
format = JSON.parse(texte.slice(0, texte.indexOf('\n') + 1).replace(/^\u{FEFF}/u, '')).format;
} catch {
return null;
}
return Number.isSafeInteger(format) && format > FORMAT ? format : null;
}
[ADD] storage: copy, rename, import, delete events; local settings § 8.7 operations reuse the event store's pairing and alignment instead of copying them. Renaming writes the complete pair under the new name — the store now writes a gesture with the pair it writes elsewhere — before deleting the old one, so no cut leaves zero pairs; an unchanged name writes nothing. Deleting moves state, .precedent, journal, lock and leftovers into a dated trash, matching names as the case-insensitive disk does. creerDossier creates only the root it is aimed at, so a vanished working folder never comes back empty. reglages_locaux.json keeps exactly the two § 8.5 settings; its deferred writer always keeps the last demand. Checked: 1314 node, 35 browser, 27 node-long tests; depot.js at 100 %. --- FR --- [ADD] stockage : copier, renommer, importer, supprimer ; réglages locaux Les opérations du § 8.7 reprennent l'appariement et l'alignement du dépôt au lieu de les recopier. Renommer écrit la paire complète sous le nouveau nom — le dépôt écrit désormais un geste avec la paire qu'il pose ailleurs — avant de supprimer l'ancienne : aucune coupure ne laisse zéro paire ; un nom inchangé n'écrit rien. Supprimer range état, .precedent, journal, verrou et restes dans une corbeille datée, en comparant les noms comme le disque insensible à la casse. creerDossier ne crée que la racine visée : un dossier de travail disparu ne renaît pas vide. reglages_locaux.json garde les deux réglages du § 8.5 ; son écrivain différé garde la dernière demande. Vérifié : 1314 node, 35 navigateur, 27 node-long ; depot.js à 100 %. Assisted-by: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01EUXSGcwCLSC69FWEdCtWSb
2026-10-06 23:17:05 -04:00
/**
* Bases que portent les fichiers d'événement — état, .precedent, journal,
* verrou — à la racine de racine, dans l'ordre de sa liste, sans rien
* écrire : une base que l'un d'eux porte n'est pas libre (deriverBase). Une
* base revient une fois par fichier qui la porte.
*
* @param {import('./systeme_fichiers.js').SystemeFichiers} fs
* @param {import('./systeme_fichiers.js').Racine} racine
* @returns {Promise<string[]>}
* @throws {ErreurStockage} ABSENT quand la racine n'existe pas
*/
export async function basesPrises(fs, racine) {
return basesDe(await fs.lister(racine, ''));
}
[ADD] storage: event store — list, create, open, save, recover An event is a state file and a journal paired by its internal identifier, never by name (§ 8.6): a hand copy of a state does not take the original's journal, and a renamed journal stays paired. Each gesture writes its journal line first, then the .precedent and the state (§ 8.2); recovery compares revisions, never file dates, so a cut leaves the journal one entry ahead at most. Opening never writes (§ 8.4); an unreadable state names its readable secours instead of opening an empty event, and a newer format stays read-only. Writing elsewhere after a removed medium aligns the pair first. Checked: 60 tests, 100 % branches of depot.js, 1257 node tests on HEAD. --- FR --- [ADD] stockage : dépôt d'événements — lister, créer, ouvrir, reprendre Un événement est un fichier d'état et un journal appariés par l'identifiant interne, jamais par le nom (§ 8.6) : un état copié à la main ne prend pas le journal de l'original, et un journal renommé reste apparié. Chaque geste écrit sa ligne de journal d'abord, puis le .precedent et l'état (§ 8.2) ; la reprise compare les révisions, jamais la date des fichiers : une coupure laisse le journal en avance d'une entrée au plus. Ouvrir n'écrit jamais (§ 8.4) ; un état illisible nomme ses secours lisibles au lieu d'ouvrir un événement vide, et un format plus récent reste en lecture seule. Écrire ailleurs après un support retiré aligne d'abord la paire. Vérifié : 60 épreuves, 100 % des branches de depot.js, 1257 épreuves node. Assisted-by: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01EUXSGcwCLSC69FWEdCtWSb
2026-10-06 20:39:49 -04:00
// Base que prend l'événement nommé nom dans racine (deriverBase) : aucune des
// bases d'autres, ni de celles que portent les fichiers du dossier, à la casse
// près.
async function deriver(fs, racine, nom, autres) {
const { separateur } = await fs.emplacements();
[ADD] storage: copy, rename, import, delete events; local settings § 8.7 operations reuse the event store's pairing and alignment instead of copying them. Renaming writes the complete pair under the new name — the store now writes a gesture with the pair it writes elsewhere — before deleting the old one, so no cut leaves zero pairs; an unchanged name writes nothing. Deleting moves state, .precedent, journal, lock and leftovers into a dated trash, matching names as the case-insensitive disk does. creerDossier creates only the root it is aimed at, so a vanished working folder never comes back empty. reglages_locaux.json keeps exactly the two § 8.5 settings; its deferred writer always keeps the last demand. Checked: 1314 node, 35 browser, 27 node-long tests; depot.js at 100 %. --- FR --- [ADD] stockage : copier, renommer, importer, supprimer ; réglages locaux Les opérations du § 8.7 reprennent l'appariement et l'alignement du dépôt au lieu de les recopier. Renommer écrit la paire complète sous le nouveau nom — le dépôt écrit désormais un geste avec la paire qu'il pose ailleurs — avant de supprimer l'ancienne : aucune coupure ne laisse zéro paire ; un nom inchangé n'écrit rien. Supprimer range état, .precedent, journal, verrou et restes dans une corbeille datée, en comparant les noms comme le disque insensible à la casse. creerDossier ne crée que la racine visée : un dossier de travail disparu ne renaît pas vide. reglages_locaux.json garde les deux réglages du § 8.5 ; son écrivain différé garde la dernière demande. Vérifié : 1314 node, 35 navigateur, 27 node-long ; depot.js à 100 %. Assisted-by: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01EUXSGcwCLSC69FWEdCtWSb
2026-10-06 23:17:05 -04:00
const existantes = [...autres, ...(await basesPrises(fs, racine))];
[ADD] storage: event store — list, create, open, save, recover An event is a state file and a journal paired by its internal identifier, never by name (§ 8.6): a hand copy of a state does not take the original's journal, and a renamed journal stays paired. Each gesture writes its journal line first, then the .precedent and the state (§ 8.2); recovery compares revisions, never file dates, so a cut leaves the journal one entry ahead at most. Opening never writes (§ 8.4); an unreadable state names its readable secours instead of opening an empty event, and a newer format stays read-only. Writing elsewhere after a removed medium aligns the pair first. Checked: 60 tests, 100 % branches of depot.js, 1257 node tests on HEAD. --- FR --- [ADD] stockage : dépôt d'événements — lister, créer, ouvrir, reprendre Un événement est un fichier d'état et un journal appariés par l'identifiant interne, jamais par le nom (§ 8.6) : un état copié à la main ne prend pas le journal de l'original, et un journal renommé reste apparié. Chaque geste écrit sa ligne de journal d'abord, puis le .precedent et l'état (§ 8.2) ; la reprise compare les révisions, jamais la date des fichiers : une coupure laisse le journal en avance d'une entrée au plus. Ouvrir n'écrit jamais (§ 8.4) ; un état illisible nomme ses secours lisibles au lieu d'ouvrir un événement vide, et un format plus récent reste en lecture seule. Écrire ailleurs après un support retiré aligne d'abord la paire. Vérifié : 60 épreuves, 100 % des branches de depot.js, 1257 épreuves node. Assisted-by: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01EUXSGcwCLSC69FWEdCtWSb
2026-10-06 20:39:49 -04:00
return deriverBase(nom, { racine: racine.chemin, separateur, existantes });
}
// Les fichiers de racine tels qu'une lecture les trouve : la liste du
// dossier, le texte de chaque fichier — null quand il manque, toute autre
// erreur de lecture remontant telle quelle —, et la lecture de chaque texte
// d'état ou de .precedent (lireEtat), chacun lu une fois, à la demande. Les
// décisions d'appariement d'une même opération se prennent ainsi sur les
// mêmes fichiers, sans relire un journal. Une vue ne voit pas ce qui s'écrit
// après sa lecture : chaque ouverture en prend une, et l'écriture qui cherche
// le journal d'un événement en prend une neuve (baseCherchee).
function vueDe(fs, racine) {
let entrees = null;
const textes = new Map();
const lectures = new Map();
const texte = async (chemin) => {
if (!textes.has(chemin)) textes.set(chemin, await lireSiPresent(fs, racine, chemin));
return textes.get(chemin);
};
return {
entrees: async () => {
entrees ??= await fs.lister(racine, '');
return entrees;
},
texte,
// { texte, lecture, details } d'un fichier d'état ou d'un .precedent,
// les trois null quand il manque.
lecture: async (chemin) => {
if (!lectures.has(chemin)) {
const lu = await texte(chemin);
const absent = { texte: null, lecture: null, details: null };
lectures.set(chemin, lu === null ? absent : { texte: lu, ...lireEtat(lu) });
}
return lectures.get(chemin);
},
};
}
// L'événement auquel appartient base dans le dossier de la vue : celui que
// porte son état quand il se lit, sinon son .precedent quand il se lit ; null
// sinon. Le journal de base qui porte cet événement est le sien, et ne
// s'apparie à aucune autre base (apparier).
async function proprietaire(vue, base) {
const { lecture } = await vue.lecture(base + SUFFIXES.etat);
const lue = lecture ?? (await vue.lecture(base + SUFFIXES.precedent)).lecture;
return lue?.charge.evenement.id ?? null;
}
// Le journal de l'événement id pour base, dans le dossier de la vue (§ 8.6) :
// { base, texte } du journal de base quand sa première ligne nomme id ;
// sinon du premier journal du dossier, par nom, qui le nomme et n'appartient
// pas à sa propre base (proprietaire) ; null quand aucun ne s'apparie. Un
// journal renommé à la main, ou créé sous une base dérivée quand celui de la
// base portait un autre événement, n'a ni état ni .precedent à sa base : il
// s'apparie. Le journal d'un original reste à l'original, et un état copié à
// la main dans le même dossier ne le prend pas. C'est la seule règle
// d'appariement : lister, ouvrir, ouvrir un secours et l'écriture d'un
// journal sans base la suivent, après la préséance du journal de la base
// d'un format plus récent (formatDuJournalDeBase).
async function apparier(vue, base, id) {
const texte = await vue.texte(base + SUFFIXES.journal);
if (evenementDe(texte) === id) return { base, texte };
for (const autre of basesAuSuffixe(await vue.entrees(), SUFFIXES.journal)) {
const texteAutre = await vue.texte(autre + SUFFIXES.journal);
if (evenementDe(texteAutre) === id && (await proprietaire(vue, autre)) !== id) {
return { base: autre, texte: texteAutre };
}
}
return null;
}
// Identifiants que portent les états lisibles du dossier de la vue, un par
// état, dans l'ordre des noms : le même deux fois, l'événement est en double.
async function identifiantsLisibles(vue) {
const identifiants = [];
for (const base of basesAuSuffixe(await vue.entrees(), SUFFIXES.etat)) {
const { lecture } = await vue.lecture(base + SUFFIXES.etat);
if (lecture !== null) identifiants.push(lecture.charge.evenement.id);
}
return identifiants;
}
// L'identifiant par lequel une base dont l'état ne se lit pas, ou manque,
// s'apparie à un journal (§ 8.6) : celui de son .precedent quand il se lit ;
// sinon celui que nomme la première ligne du journal de la base, quand aucun
// état lisible du dossier ne le porte ; null sinon. Un état lisible de cet
// identifiant passe avant un état qui ne dit pas le sien, et un journal sans
// identifiant lisible n'apparie rien : l'état illisible ne s'apparie jamais
// par le seul nom.
async function identifiantSansEtat(vue, base) {
const { lecture } = await vue.lecture(base + SUFFIXES.precedent);
if (lecture !== null) return lecture.charge.evenement.id;
const id = evenementDe(await vue.texte(base + SUFFIXES.journal));
if (id === null || (await identifiantsLisibles(vue)).includes(id)) return null;
return id;
}
// Le format plus récent que FORMAT que déclare le journal de base dans le
// dossier de la vue, null quand il manque ou n'en déclare pas. Un tel journal
// passe avant tout appariement, à l'ouverture (journalDe) comme dans la liste
// (lister) : son identifiant ne se lit pas, et il reste celui de la base.
async function formatDuJournalDeBase(vue, base) {
const texte = await vue.texte(base + SUFFIXES.journal);
return texte === null ? null : formatPlusRecentDe(texte);
}
// Le journal de l'événement id pour base, dans le dossier de la vue (§ 8.6) :
// lu, sa base, etranger, l'événement que porte le journal de la base quand
// aucun ne s'apparie et qu'il en nomme un autre, et format. Le journal de la
// base qui déclare un format plus récent que FORMAT se reconnaît d'abord,
// avant tout appariement, puisque son identifiant ne se lit pas : lu vaut
// null et format le nomme, ce qui garde l'Ouvert en lecture seule, même
// quand une copie au format courant porte id ailleurs dans le dossier ; rien
// ne le lit ni ne le remplace (§ 8.8). Sinon, c'est le journal qu'apparie
// apparier, aucun quand id est null. Faute de lui, le journal de la base
// reste le sien quand il manque ou que sa première ligne ne se lit pas, une
// écriture le remplaçant. Quand il nomme un autre événement, lu et base
// valent null : rien ne le lit ni ne l'écrit, et l'écriture cherche le
// journal de l'événement (baseCherchee).
async function journalDe(vue, base, id) {
const format = await formatDuJournalDeBase(vue, base);
if (format !== null) return { lu: null, base, etranger: null, format };
const texte = await vue.texte(base + SUFFIXES.journal);
if (id === null) return { lu: null, base: null, etranger: null, format: null };
const apparie = await apparier(vue, base, id);
if (apparie !== null) return { lu: lireJournal(apparie.texte), base: apparie.base, etranger: null, format: null };
const etranger = evenementDe(texte);
if (etranger !== null) return { lu: null, base: null, etranger, format: null };
return { lu: texte === null ? null : lireJournal(texte), base, etranger: null, format: null };
}
// Le journal d'une base dont l'état ne se lit pas, ou manque : journalDe
// pour l'identifiant que rend identifiantSansEtat, null compris. Le journal
// de la base d'un format plus récent s'y reconnaît sans identifiant.
async function journalSansEtat(vue, base) {
return journalDe(vue, base, await identifiantSansEtat(vue, base));
}
// Le dernier instant restituable d'un journal lu qui a des entrées : la
// charge de sa dernière entrée ou, quand un correctif ne s'applique pas, de
// la dernière entrée qui le précède (§ 8.6). Les entrées qui suivent cet
// instant et leurs jalons s'écartent et se comptent, comme la lecture compte
// les lignes qu'elle écarte : le journal rendu finit sur lui. La première
// entrée porte un instantané, qui se restitue toujours : la recherche
// s'arrête au plus tard sur elle. Rend null pour un journal absent ou sans
// entrée.
function instantDe(lu) {
if (lu === null || lu.entrees.length === 0) return null;
for (let rang = lu.entrees.length - 1; ; rang -= 1) {
const { revision } = lu.entrees[rang];
let charge;
try {
charge = reconstruire(lu, revision);
} catch {
// Un correctif ne s'applique pas : l'entrée qui précède.
continue;
}
const entrees = lu.entrees.slice(0, rang + 1);
const jalons = lu.jalons.filter((jalon) => jalon.revision <= revision);
const ecartees = lu.ecartees + lu.entrees.length - entrees.length + lu.jalons.length - jalons.length;
return { journal: { ...lu, entrees, jalons, ecartees }, charge, revision };
}
}
// Ce qu'une lecture d'état — l'état ou le .precedent — apporte à l'Ouvert,
// avec ce qu'il tient de l'état sur le disque (surDisque) : la lecture seule
// de l'un ou de l'autre.
const depuisLecture = ({ entete, charge, formatPlusRecent }, disque) => ({
charge,
revision: entete.revision,
produitVersion: entete.produitVersion,
...disque,
formatPlusRecent: formatPlusRecent || disque.formatPlusRecent,
});
// Les avertissements d'un Ouvert, dans l'ordre que décrit le type Ouvert.
// derniere est la révision de la dernière entrée du journal, celle de la
// charge quand il n'y en a pas.
function avertir({ examen, journal, revision, rattrape, ecartees, format, etranger }) {
const avertissements = [];
const ajouter = (code, details = {}) => avertissements.push({ code, details });
if (examen.retenu.fautes.length > 0) ajouter('RETENU_FAUTIF');
if (rattrape) ajouter('JOURNAL_REPRIS');
const derniere = journal === null ? revision : journal.entrees.at(-1).revision;
if (derniere > revision) ajouter('JOURNAL_EN_AVANCE_DE', { n: derniere - revision });
if (derniere < revision) ajouter('JOURNAL_INCOMPLET', { etat: revision, journal: derniere });
if (ecartees > 0) ajouter('JOURNAL_ECARTEES', { n: ecartees });
if (format !== null) ajouter('JOURNAL_FORMAT_PLUS_RECENT', { format });
if (etranger !== null) ajouter('JOURNAL_ETRANGER', { evenement: etranger });
if (examen.fautives.length > 0) ajouter('PROPOSITIONS_FAUTIVES', { ids: examen.fautives.map(({ id }) => id) });
const retenu = examen.retenu.raisons.length > 0;
if (examen.derives.length > 0 || retenu) ajouter('DERIVE', { ids: examen.derives.map(({ id }) => id), retenu });
return avertissements;
}
// L'Ouvert d'une source — état, .precedent ou instant du journal — et du
// journal trouvé pour elle. rattraper : un journal en avance d'une seule
// entrée sur la source donne sa charge, sa révision et sa version. La source
// d'un format plus récent, ou un journal d'un format plus récent, gardent
// l'Ouvert en lecture seule.
function assembler({ fs, racine, base, source, trouve, instant, rattraper }) {
let { charge, revision, produitVersion } = source;
const rattrape = rattraper && instant !== null && instant.revision === revision + 1;
if (rattrape) {
charge = canoniser(instant.charge);
revision = instant.revision;
produitVersion = versionDe(instant.journal, revision);
}
const journal = instant?.journal ?? null;
const ecartees = journal?.ecartees ?? trouve.lu?.ecartees ?? 0;
const examen = examiner(charge);
return {
fs,
racine,
base,
charge: examen.charge,
revision,
produitVersion,
texteEtat: source.texteEtat,
revisionEtat: source.revisionEtat,
journal,
baseJournal: trouve.base,
chargeJournal: instant?.charge ?? null,
journalAReecrire: journal !== null && ecartees > 0,
formatPlusRecent: source.formatPlusRecent || trouve.format !== null,
examen,
avertissements: avertir({
examen,
journal,
revision,
rattrape,
ecartees,
format: trouve.format,
etranger: trouve.etranger,
}),
};
}
// L'erreur d'un état qui ne s'ouvre pas : ETAT_ILLISIBLE, que details décrit
// — raison, chemin, format —, complété de la base, du format du journal de la
// base quand il est plus récent et que details n'en nomme pas, et des
// secours lisibles : la révision du .precedent et le dernier instant du
// journal que l'identifiant apparie (journalSansEtat), jamais un journal
// pris par son seul nom ; ABSENT quand ni l'état, ni le .precedent, ni le
// journal de la base n'existent.
async function refuser(vue, base, details) {
const precedent = await vue.lecture(base + SUFFIXES.precedent);
const journal = await vue.texte(base + SUFFIXES.journal);
if (details.raison === 'ABSENT' && precedent.texte === null && journal === null) {
return new ErreurStockage('ABSENT', { chemin: base + SUFFIXES.etat });
}
const trouve = await journalSansEtat(vue, base);
const format = details.format ?? trouve.format;
const secours = {
precedent: precedent.lecture?.entete.revision ?? null,
journal: instantDe(trouve.lu)?.revision ?? null,
};
return new ErreurStockage('ETAT_ILLISIBLE', { base, ...details, ...(format === null ? {} : { format }), secours });
}
/**
* Événements de racine (§ 8.1, § 8.6), sans rien écrire : un résumé par
* fichier d'état, lisible ou non, et par journal sans état. Un état
* s'apparie à son journal par l'identifiant, jamais par le seul nom
* (apparier) : celui de l'état quand il se lit ; pour un état illisible,
* celui de son .precedent, ou de son journal quand aucun état lisible ne le
* porte (identifiantSansEtat). Un journal renommé à la main reste apparié ;
* le journal d'un original ne s'apparie pas à sa copie faite à la main. Un
* journal sans état dans sa base se résume quand aucun état ne se l'apparie
* et qu'aucun état lisible ne porte son identifiant, illisible ABSENT ; il
* ne se montre pas sinon : une copie d'un journal apparié, ou le journal
* qu'un état illisible s'apparie par son .precedent. Un identifiant que
* portent deux états lisibles les signale doublon. Résumés triés par nom,
* les noms inconnus après les autres, puis par base, unités UTF-16
* comparées.
*
* @param {import('./systeme_fichiers.js').SystemeFichiers} fs
* @param {import('./systeme_fichiers.js').Racine} racine
* @returns {Promise<Resume[]>}
*/
export async function lister(fs, racine) {
const vue = vueDe(fs, racine);
const fichiers = (await vue.entrees()).filter(({ type }) => type === 'fichier');
const identifiants = await identifiantsLisibles(vue);
const sansLecture = (base, id, modifie, journal, illisible) => ({
base,
id,
nom: null,
date: null,
etat: null,
participants: null,
modifie,
journal,
illisible,
doublon: false,
});
const apparies = new Set();
const resumes = [];
for (const { nom, modifie } of fichiers) {
const base = baseDe(nom, SUFFIXES.etat);
if (base === null) continue;
const { lecture, details } = await vue.lecture(nom);
const id = lecture === null ? await identifiantSansEtat(vue, base) : lecture.charge.evenement.id;
let journal = null;
if ((await formatDuJournalDeBase(vue, base)) !== null) journal = base;
else if (id !== null) journal = (await apparier(vue, base, id))?.base ?? null;
if (journal !== null) apparies.add(journal);
if (lecture === null) {
resumes.push(sansLecture(base, null, modifie, journal, details.raison));
continue;
}
const { evenement, participants } = lecture.charge;
resumes.push({
base,
id: evenement.id,
nom: evenement.nom,
date: evenement.date,
etat: evenement.etat,
participants: participants.length,
modifie,
journal,
illisible: null,
doublon: identifiants.indexOf(evenement.id) !== identifiants.lastIndexOf(evenement.id),
});
}
const basesDesEtats = new Set(basesAuSuffixe(fichiers, SUFFIXES.etat));
for (const { nom, modifie } of fichiers) {
const base = baseDe(nom, SUFFIXES.journal);
if (base === null || basesDesEtats.has(base) || apparies.has(base)) continue;
const evenement = evenementDe(await vue.texte(nom));
if (!identifiants.includes(evenement)) resumes.push(sansLecture(base, evenement, modifie, base, 'ABSENT'));
}
const nomsConnus = (a, b) => (a === null) - (b === null) || comparerTextes(a ?? '', b ?? '');
return resumes.sort((a, b) => nomsConnus(a.nom, b.nom) || comparerTextes(a.base, b.base));
}
/**
* Ouvre l'événement de base dans racine, sans rien écrire (§ 8.4, § 8.8) :
* la charge de son état, examinée (examiner), et son journal, apparié par
* l'identifiant. Un journal en avance d'une entrée donne sa charge, rattrapée
* en mémoire, JOURNAL_REPRIS ; l'état se réécrira au premier geste. Un journal
* plus en avance ou en retard laisse la charge de l'état, et le premier geste
* inscrit une entrée de reprise. Une proposition fautive s'écarte et se
* nomme ; une périmée se garde et se signale ; un retenu fautif se garde et
* se signale en premier (§ 8.9). Un format plus récent s'ouvre en lecture
* seule ; un format plus ancien se lit, et ne se convertit qu'au premier
* geste. Le journal de la base d'un format plus récent ne se lit pas, et
* garde l'Ouvert en lecture seule, JOURNAL_FORMAT_PLUS_RECENT, même quand une
* copie au format courant porte l'identifiant ailleurs dans le dossier.
*
* @param {import('./systeme_fichiers.js').SystemeFichiers} fs
* @param {import('./systeme_fichiers.js').Racine} racine
* @param {string} base
* @returns {Promise<Ouvert>}
* @throws {ErreurStockage} ETAT_ILLISIBLE {base, raison, chemin, [format],
* secours} : l'état ne se lit pas, ou manque à côté de son .precedent
* ou de son journal (raison ABSENT) ; format est celui de l'état
* quand son refus en nomme un, sinon celui du journal de la base
* quand il est plus récent ; secours nomme la révision lisible du
* .precedent et le dernier instant du journal que l'identifiant du
* .precedent apparie — ou, sans .precedent lisible, celui du journal
* de la base qu'aucun état lisible ne porte —, null pour ce qui ne se
* lit pas ou ne s'apparie pas. ABSENT {chemin} quand aucun fichier de
* l'événement n'existe
*/
export async function ouvrir(fs, racine, base) {
const vue = vueDe(fs, racine);
const etat = await vue.lecture(base + SUFFIXES.etat);
if (etat.texte === null) throw await refuser(vue, base, { raison: 'ABSENT', chemin: null });
if (etat.lecture === null) throw await refuser(vue, base, etat.details);
const trouve = await journalDe(vue, base, etat.lecture.charge.evenement.id);
const source = depuisLecture(etat.lecture, surDisque(etat.texte, etat));
return assembler({ fs, racine, base, source, trouve, instant: instantDe(trouve.lu), rattraper: true });
}
/**
* Ouvre l'événement de base depuis un secours, sans rien écrire (§ 8.8) :
* source 'precedent', la charge du .precedent, que le journal de son
* identifiant suit sans être rattrapé ; source 'journal', le dernier instant
* restituable du journal, apparié par l'identifiant de l'état quand il se lit,
* sinon par celui du .precedent, et sans .precedent lisible, le journal de
* la base quand aucun état lisible du dossier ne porte son identifiant
* (journalSansEtat) : jamais un journal pris par son seul nom, ni celui d'un
* autre événement. texteEtat est le texte de l'état quand il se lit : le
* premier geste le met au .precedent, et ne met jamais un état illisible.
* L'état sur le disque d'un format plus récent, qu'il se lise ou non, garde
* le secours en lecture seule, comme un journal de la base d'un format plus
* récent : aucun geste de ce secours ne remplace l'état ou le journal qu'une
* construction plus récente a écrits (§ 8.8). Un .precedent d'un format plus
* récent garde en lecture seule le secours 'precedent', qui le lit, et non le
* secours 'journal'. Un journal plus récent ne s'ouvre pas en secours, et ne
* se dit pas absent : il existe, et ce code ne le lit pas.
*
* @param {import('./systeme_fichiers.js').SystemeFichiers} fs
* @param {import('./systeme_fichiers.js').Racine} racine
* @param {string} base
* @param {'precedent'|'journal'} source
* @returns {Promise<Ouvert>}
* @throws {ErreurStockage} ABSENT {chemin} : le .precedent manque, ou aucun
* journal ne s'apparie, ou il n'a aucune entrée, chemin nommant alors
* le journal de la base ; ETAT_ILLISIBLE {raison, chemin, [format]} :
* celui de l'analyse d'un .precedent illisible, ou, source 'journal',
* {raison: 'FORMAT_PLUS_RECENT', chemin: le journal de la base,
* format} quand ce journal est d'un format plus récent
* @throws {RangeError} une autre source
*/
export async function ouvrirSecours(fs, racine, base, source) {
if (source !== 'precedent' && source !== 'journal') {
throw new RangeError(`ouvrirSecours : source precedent ou journal attendue, reçu ${JSON.stringify(source)}`);
}
const vue = vueDe(fs, racine);
const etat = await vue.lecture(base + SUFFIXES.etat);
const disque = surDisque(etat.texte, etat);
if (source === 'precedent') {
const lecture = analyser(await fs.lireTexte(racine, base + SUFFIXES.precedent));
const trouve = await journalDe(vue, base, lecture.charge.evenement.id);
const depuis = depuisLecture(lecture, disque);
return assembler({ fs, racine, base, source: depuis, trouve, instant: instantDe(trouve.lu), rattraper: false });
}
const trouve = await (etat.lecture === null
? journalSansEtat(vue, base)
: journalDe(vue, base, etat.lecture.charge.evenement.id));
if (trouve.format !== null) {
throw new ErreurStockage('ETAT_ILLISIBLE', {
raison: 'FORMAT_PLUS_RECENT',
chemin: base + SUFFIXES.journal,
format: trouve.format,
});
}
const instant = instantDe(trouve.lu);
if (instant === null) throw new ErreurStockage('ABSENT', { chemin: base + SUFFIXES.journal });
const depuis = {
charge: canoniser(instant.charge),
revision: instant.revision,
produitVersion: versionDe(instant.journal, instant.revision),
...disque,
};
return assembler({ fs, racine, base, source: depuis, trouve, instant, rattraper: false });
}
// Un Ouvert en lecture seule — un fichier qu'il a lu est d'un format plus
// récent — ne s'écrit jamais (§ 8.8).
function exigerEcriture({ formatPlusRecent }) {
if (formatPlusRecent) throw new ErreurStockage('LECTURE_SEULE', { raison: 'FORMAT_PLUS_RECENT' });
}
// version quand l'entrée qui suit revision la note, null sinon : à la première
// entrée, et quand la construction diffère de la précédente (§ 8.8).
const versionNotee = (version, journal, revision) => (version === versionDe(journal, revision) ? null : version);
// Le journal en mémoire, la ligne écrite ajoutée à ses entrées ou à ses jalons.
function avec(journal, ligne) {
const lue = JSON.parse(ligne);
if (lue.type === 'jalon') return { ...journal, jalons: [...journal.jalons, lue] };
return { ...journal, entrees: [...journal.entrees, lue] };
}
// Texte d'un journal en mémoire : son ouverture, puis chaque entrée suivie
// des jalons de sa révision, dans leur ordre. Une entrée lue se récrit par
// JSON.stringify, comme ligneEntree l'a écrite : ses clés dans l'ordre lu.
function texteDe({ evenement, entrees, jalons }) {
const lignes = [ligneOuverture(evenement)];
for (const entree of entrees) {
lignes.push(JSON.stringify(entree));
for (const jalon of jalons) {
if (jalon.revision === entree.revision) lignes.push(ligneJalon(jalon));
}
}
return `${lignes.join('\n')}\n`;
}
// Le journal de l'Ouvert prêt à recevoir une ligne, en mémoire : avant, le
// journal tel que le fichier le porte, vide quand il manque ; reprise, la
// ligne de l'entrée de reprise qui le fait finir sur la charge ouverte, ou
// null ; journal, avant suivi de la reprise ; charge, ce que restitue sa
// dernière entrée ; entier, vrai quand le fichier se réécrit en entier à sa
// prochaine écriture : il manque, sa lecture a écarté des lignes, ou il
// reçoit la reprise. Sans journal, la reprise le commence, instantané de la
// charge ouverte à sa révision et à sa version. Un journal qui finit sur une
// autre révision que la charge ouverte reçoit la reprise à la révision qui
// suit la sienne, qui restitue la charge ouverte. libelleReprise manque :
// ligneEntree lève TypeError, avant toute écriture.
function preparer(ouvert, { libelleReprise, horodatage }) {
const { journal, revision, charge, produitVersion } = ouvert;
if (journal === null) {
const reprise = ligneEntree({
revision,
libelle: libelleReprise,
horodatage,
produitVersion,
apres: charge,
instantane: true,
});
const vide = { evenement: charge.evenement.id, entrees: [], jalons: [], ecartees: 0 };
return { avant: vide, reprise, journal: avec(vide, reprise), charge: canoniser(charge), entier: true };
}
const derniere = journal.entrees.at(-1).revision;
if (derniere === revision) {
return { avant: journal, reprise: null, journal, charge: ouvert.chargeJournal, entier: ouvert.journalAReecrire };
}
const suivante = derniere + 1;
const reprise = ligneEntree({
revision: suivante,
libelle: libelleReprise,
horodatage,
produitVersion: versionNotee(produitVersion, journal, derniere),
avant: ouvert.chargeJournal,
apres: charge,
instantane: estInstantane(suivante, journal.entrees[0].revision),
});
return { avant: journal, reprise, journal: avec(journal, reprise), charge: canoniser(charge), entier: true };
}
// L'en-tête de l'état que restitue la dernière entrée d'un journal : la
// révision de cette entrée, et la version de la construction qui l'a
// produite (§ 8.8). L'état qui le porte, avec la charge de cette entrée,
// fait une paire avec le journal (§ 8.2).
function enteteDeLaDerniere(journal) {
const revision = journal.entrees.at(-1).revision;
return { revision, produitVersion: versionDe(journal, revision) };
}
// Le texte d'état que le disque doit porter avant la ligne d'un geste, ou
// null quand il le porte : un état lisible sur le disque, à une autre
// révision que la dernière entrée du journal préparé, reçoit d'abord la
// charge que restitue cette entrée, sous son en-tête (enteteDeLaDerniere).
// Sans ce pas, une coupure entre la ligne du geste et l'état laisserait le
// journal en avance de plus d'une entrée, ou à la révision de l'état avec
// une autre charge, et la comparaison des révisions ne rattraperait pas le
// geste (§ 8.2). Un état illisible ou absent n'a pas de révision : ouvrir le
// refuse et nomme le journal en secours. Une charge que l'analyse refuserait
// lève TypeError, avant toute écriture.
function etatAligne(ouvert, { journal, charge }) {
const entete = enteteDeLaDerniere(journal);
if (ouvert.revisionEtat === null || ouvert.revisionEtat === entete.revision) return null;
return relisible(serialiser(charge, entete), 'enregistrer');
}
// Pose texte, que etatAligne a calculé, avant la ligne d'un geste : la
// reprise seule d'abord, que preparer fait écrire en entier, puis l'état à la
// révision de la dernière entrée du journal. Rend l'Ouvert dont le disque
// porte cet état et la préparation de la ligne qui suit. Un refus d'écriture
// remonte tel quel : le geste n'est pas au journal, et l'Ouvert reçu reste
// celui d'où le réessayer. Le nouvel essai réécrit en entier le journal où la
// reprise est déjà, celui de la base de l'Ouvert ou, sans base, celui que
// l'identifiant de l'événement désigne (baseCherchee) : la reprise s'y
// remplace au lieu de s'y doubler.
async function aligner(ouvert, preparation, texte) {
let { journal, entier } = preparation;
let depart = ouvert;
if (preparation.reprise !== null) {
const ecrit = await ecrireJournal(ouvert, { journal: preparation.avant, entier }, preparation.reprise);
depart = { ...ouvert, ...ecrit };
journal = ecrit.journal;
entier = false;
}
const aligne = await poserEtat({ ...depart, revision: journal.entrees.at(-1).revision }, texte);
return { depart: aligne, preparation: { journal, entier } };
}
// Le texte entier d'un journal en mémoire, élagué au-delà de PLAFOND_ENTREES
// (elaguer), et le journal que ce texte porte, sans ligne écartée : ce que
// le fichier porte une fois écrit d'un bloc.
function textePlein(journal) {
const texte = texteDe(journal);
const elague = elaguer(texte);
if (elague === null) return { texte, journal: { ...journal, ecartees: 0 } };
return { texte: elague, journal: lireJournal(elague) };
}
// La base où s'écrit le journal d'un Ouvert sans base de journal, celui de
// sa base portant un autre événement : la base du journal que l'identifiant
// de l'événement apparie dans le dossier tel qu'il est (apparier), sinon une
// base libre que son nom dérive. L'essai repris après un refus réécrit
// ainsi, avec sa reprise, le journal que l'essai refusé a créé, au lieu d'en
// dériver un second que l'ouverture n'apparierait pas. Une recherche qui
// échoue — le dossier qui ne se liste plus, ou un journal qui ne se lit
// plus : ABSENT quand le dossier a disparu avec son support, LECTURE quand
// la lecture est refusée — lève ECRITURE, comme l'écriture qu'elle précède :
// chemin le journal de la base, dossier le chemin de la racine, qui porte
// les fichiers de l'événement, et cause celle de l'échec, ENOENT pour un
// dossier disparu. L'application nomme ainsi le fichier et son dossier, et
// propose d'écrire ailleurs (§ 8.6). Toute autre erreur remonte telle quelle.
async function baseCherchee(ouvert, evenement) {
const { fs, racine, base } = ouvert;
try {
return (
(await apparier(vueDe(fs, racine), base, evenement))?.base ??
(await deriver(fs, racine, ouvert.charge.evenement.nom, []))
);
} catch (erreur) {
if (erreur.code !== 'ABSENT' && erreur.code !== 'LECTURE') throw erreur;
const cause = erreur.code === 'ABSENT' ? 'ENOENT' : erreur.details.cause;
throw new ErreurStockage('ECRITURE', { chemin: base + SUFFIXES.journal, dossier: racine.chemin, cause });
}
}
// Écrit au journal la ligne à la suite de journal, le journal en mémoire que
// le fichier porte (§ 8.2, § 8.6) : par un ajout en fin, ou en entier par une
// écriture atomique, élaguée au-delà de PLAFOND_ENTREES (textePlein), quand
// entier le demande ou que le plafond est dépassé. Sans base, le journal
// prend celle que baseCherchee trouve. Rend le journal en mémoire, qui est
// le fichier, et sa base.
async function ecrireJournal(ouvert, preparation, ligne) {
let journal = avec(preparation.journal, ligne);
const { fs, racine } = ouvert;
const base = ouvert.baseJournal ?? (await baseCherchee(ouvert, journal.evenement));
if (preparation.entier || journal.entrees.length > PLAFOND_ENTREES) {
const plein = textePlein(journal);
journal = plein.journal;
await fs.ecrireAtomique(racine, base + SUFFIXES.journal, plein.texte);
} else {
await fs.ajouterLigne(racine, base + SUFFIXES.journal, ligne);
}
return { journal: { ...journal, ecartees: 0 }, baseJournal: base, journalAReecrire: false };
}
// Écrit texteEtat, l'état de l'Ouvert à sa révision : le .precedent d'abord,
// quand l'état sur le disque se lit, puis l'état (§ 8.8). Rend l'Ouvert dont
// le disque porte ce texte. Un refus remonte tel quel.
async function poserEtat(ouvert, texteEtat) {
const { fs, racine, base } = ouvert;
if (ouvert.texteEtat !== null) await fs.ecrireAtomique(racine, base + SUFFIXES.precedent, ouvert.texteEtat);
await fs.ecrireAtomique(racine, base + SUFFIXES.etat, texteEtat);
return { ...ouvert, texteEtat, revisionEtat: ouvert.revision };
}
// Écrit l'état de l'Ouvert avancé, dont le geste est déjà au journal
// (poserEtat). Un refus d'écriture lève ETAT_NON_ECRIT, ses détails — chemin,
// dossier, cause — complétés de l'Ouvert avancé, qui garde le texte de l'état
// resté sur le disque. L'Ouvert s'ajoute après la construction de l'erreur :
// son message, fait pour les traces, ne porte ni la charge ni le journal.
async function ecrireEtat(avance, texteEtat) {
try {
return await poserEtat(avance, texteEtat);
} catch (erreur) {
if (erreur.code !== 'ECRITURE') throw erreur;
const refus = new ErreurStockage('ETAT_NON_ECRIT', { ...erreur.details });
refus.details.ouvert = avance;
throw refus;
}
}
// L'Ouvert d'une charge écrite : examinée, ses avertissements ceux de ses
// placements et d'un journal qui finit ailleurs que sur elle.
function apresEcriture(ouvert, { charge, revision, journal }) {
const examen = examiner(charge);
return {
...ouvert,
charge: examen.charge,
revision,
examen,
avertissements: avertir({ examen, journal, revision, rattrape: false, ecartees: 0, format: null, etranger: null }),
};
}
[ADD] storage: copy, rename, import, delete events; local settings § 8.7 operations reuse the event store's pairing and alignment instead of copying them. Renaming writes the complete pair under the new name — the store now writes a gesture with the pair it writes elsewhere — before deleting the old one, so no cut leaves zero pairs; an unchanged name writes nothing. Deleting moves state, .precedent, journal, lock and leftovers into a dated trash, matching names as the case-insensitive disk does. creerDossier creates only the root it is aimed at, so a vanished working folder never comes back empty. reglages_locaux.json keeps exactly the two § 8.5 settings; its deferred writer always keeps the last demand. Checked: 1314 node, 35 browser, 27 node-long tests; depot.js at 100 %. --- FR --- [ADD] stockage : copier, renommer, importer, supprimer ; réglages locaux Les opérations du § 8.7 reprennent l'appariement et l'alignement du dépôt au lieu de les recopier. Renommer écrit la paire complète sous le nouveau nom — le dépôt écrit désormais un geste avec la paire qu'il pose ailleurs — avant de supprimer l'ancienne : aucune coupure ne laisse zéro paire ; un nom inchangé n'écrit rien. Supprimer range état, .precedent, journal, verrou et restes dans une corbeille datée, en comparant les noms comme le disque insensible à la casse. creerDossier ne crée que la racine visée : un dossier de travail disparu ne renaît pas vide. reglages_locaux.json garde les deux réglages du § 8.5 ; son écrivain différé garde la dernière demande. Vérifié : 1314 node, 35 navigateur, 27 node-long ; depot.js à 100 %. Assisted-by: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01EUXSGcwCLSC69FWEdCtWSb
2026-10-06 23:17:05 -04:00
// La ligne de l'entrée d'un geste à la suite du journal que preparer a
// préparé, et sa révision, celle qui suit la dernière entrée de ce journal :
// un correctif depuis la charge que restitue cette entrée, ou un instantané à
// la cadence du journal ; produitVersion n'y paraît que quand la construction
// change (versionNotee). Une ligne que la lecture refuserait lève TypeError
// (ligneEntree), avant toute écriture.
function ligneDuGeste(preparation, apres, { libelle, horodatage, produitVersion, sens, retour }) {
const derniere = preparation.journal.entrees.at(-1).revision;
const revision = derniere + 1;
const ligne = ligneEntree({
revision,
libelle,
horodatage,
sens,
retour,
produitVersion: versionNotee(produitVersion, preparation.journal, derniere),
avant: preparation.charge,
apres,
instantane: estInstantane(revision, preparation.journal.entrees[0].revision),
});
return { ligne, revision };
}
[ADD] storage: event store — list, create, open, save, recover An event is a state file and a journal paired by its internal identifier, never by name (§ 8.6): a hand copy of a state does not take the original's journal, and a renamed journal stays paired. Each gesture writes its journal line first, then the .precedent and the state (§ 8.2); recovery compares revisions, never file dates, so a cut leaves the journal one entry ahead at most. Opening never writes (§ 8.4); an unreadable state names its readable secours instead of opening an empty event, and a newer format stays read-only. Writing elsewhere after a removed medium aligns the pair first. Checked: 60 tests, 100 % branches of depot.js, 1257 node tests on HEAD. --- FR --- [ADD] stockage : dépôt d'événements — lister, créer, ouvrir, reprendre Un événement est un fichier d'état et un journal appariés par l'identifiant interne, jamais par le nom (§ 8.6) : un état copié à la main ne prend pas le journal de l'original, et un journal renommé reste apparié. Chaque geste écrit sa ligne de journal d'abord, puis le .precedent et l'état (§ 8.2) ; la reprise compare les révisions, jamais la date des fichiers : une coupure laisse le journal en avance d'une entrée au plus. Ouvrir n'écrit jamais (§ 8.4) ; un état illisible nomme ses secours lisibles au lieu d'ouvrir un événement vide, et un format plus récent reste en lecture seule. Écrire ailleurs après un support retiré aligne d'abord la paire. Vérifié : 60 épreuves, 100 % des branches de depot.js, 1257 épreuves node. Assisted-by: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01EUXSGcwCLSC69FWEdCtWSb
2026-10-06 20:39:49 -04:00
/**
* Crée l'événement de charge dans racine (§ 8.1, § 8.2) : sa base dérive de
* son nom (deriverBase), libre dans le dossier. Le journal d'abord, écrit
* d'un bloc — ouverture, entrée 1 en instantané, jalon nomJalon sur elle —,
* puis l'état, à la révision 1.
*
* @param {import('./systeme_fichiers.js').SystemeFichiers} fs
* @param {import('./systeme_fichiers.js').Racine} racine
* @param {import('./types.js').Charge} charge
* @param {{libelle: string, nomJalon: string, horodatage: string, produitVersion: string}} options
* @returns {Promise<Ouvert>}
* @throws {TypeError} une charge, un libellé, un nom ou un horodatage que la
* lecture refuserait, avant toute écriture
* @throws {ErreurStockage} ECRITURE : le journal n'a pas pu s'écrire, et rien
* n'est écrit ; ETAT_NON_ECRIT : le journal est écrit, l'état non ;
* CHEMIN_TROP_LONG (deriverBase) ; ABSENT : racine n'existe pas
*/
export async function creer(fs, racine, charge, { libelle, nomJalon, horodatage, produitVersion }) {
const base = await deriver(fs, racine, charge.evenement.nom, []);
const entree = ligneEntree({ revision: 1, libelle, horodatage, produitVersion, apres: charge, instantane: true });
const jalon = ligneJalon({ revision: 1, nom: nomJalon, horodatage });
const vide = { evenement: charge.evenement.id, entrees: [], jalons: [], ecartees: 0 };
const journal = avec(avec(vide, entree), jalon);
const texteEtat = relisible(serialiser(charge, { revision: 1, produitVersion }), 'creer');
await fs.ecrireAtomique(racine, base + SUFFIXES.journal, texteDe(journal));
const cree = {
fs,
racine,
base,
produitVersion,
texteEtat: null,
revisionEtat: null,
journal,
baseJournal: base,
chargeJournal: canoniser(charge),
journalAReecrire: false,
formatPlusRecent: false,
};
return ecrireEtat(apresEcriture(cree, { charge, revision: 1, journal }), texteEtat);
}
/**
* Enregistre un geste (§ 8.2) : apres devient la charge, à la révision qui
* suit la dernière entrée du journal. (1) Un journal qui manque, ou finit
* ailleurs que sur la charge ouverte, reçoit d'abord une entrée de reprise
* qui la restitue, au libellé libelleReprise ; (2) l'entrée du geste,
* correctif depuis la charge que restitue l'entrée qui la précède, ou
* instantané à la cadence du journal ; le journal s'écrit d'un ajout en fin,
* ou en entier quand (1) a lieu, quand sa lecture a écarté des lignes, et
* au-delà de PLAFOND_ENTREES, élagué ; (3) le .precedent reçoit texteEtat,
* s'il est lisible ; (4) l'état, son nouveau texte. Un format plus ancien
* s'écrit au format courant : la conversion tombe au premier geste.
*
* Quand l'état lisible sur le disque ne porte pas la révision de la dernière
* entrée du journal — reprise comprise —, la reprise s'écrit seule, puis le
* .precedent et l'état à la révision de cette entrée, et alors seulement (2),
* (3) et (4) : une coupure laisse le journal en avance d'une entrée au plus,
* que l'ouverture rattrape (§ 8.2).
*
* Un geste refusé n'écrit rien : chaque ligne et chaque texte d'état se
* calculent, et se relisent, avant la première écriture.
*
* @param {Ouvert} ouvert
* @param {import('./types.js').Charge} apres
* @param {Object} options
* @param {string} options.libelle texte figé de l'entrée
* @param {string} options.horodatage AAAA-MM-JJTHH:MM:SS±HH:MM
* @param {string} options.produitVersion la construction qui écrit
* @param {null|'defaire'|'refaire'|'revenir'} [options.sens]
* @param {number|null} [options.retour] révision visée par un retour
* @param {string} [options.libelleReprise] texte de l'entrée de reprise,
* requis quand elle a lieu
* @returns {Promise<Ouvert>}
* @throws {ErreurStockage} LECTURE_SEULE {raison: 'FORMAT_PLUS_RECENT'} ;
* ECRITURE {chemin, dossier, cause} en (1), (2) ou pendant
* l'alignement, la recherche du journal d'un Ouvert sans base de
* journal comprise (baseCherchee) : le geste n'est pas au journal, et
* l'Ouvert reçu reste celui d'où le réessayer ; ETAT_NON_ECRIT
* {chemin, dossier, cause, ouvert} en (3) ou (4), le geste au journal
* et ouvert l'Ouvert avancé ; CHEMIN_TROP_LONG pour un journal à créer
* sous une base libre
* @throws {TypeError} une ligne, une charge ou un état que la lecture
* refuserait, avant toute écriture
*/
export async function enregistrer(ouvert, apres, options) {
[ADD] storage: copy, rename, import, delete events; local settings § 8.7 operations reuse the event store's pairing and alignment instead of copying them. Renaming writes the complete pair under the new name — the store now writes a gesture with the pair it writes elsewhere — before deleting the old one, so no cut leaves zero pairs; an unchanged name writes nothing. Deleting moves state, .precedent, journal, lock and leftovers into a dated trash, matching names as the case-insensitive disk does. creerDossier creates only the root it is aimed at, so a vanished working folder never comes back empty. reglages_locaux.json keeps exactly the two § 8.5 settings; its deferred writer always keeps the last demand. Checked: 1314 node, 35 browser, 27 node-long tests; depot.js at 100 %. --- FR --- [ADD] stockage : copier, renommer, importer, supprimer ; réglages locaux Les opérations du § 8.7 reprennent l'appariement et l'alignement du dépôt au lieu de les recopier. Renommer écrit la paire complète sous le nouveau nom — le dépôt écrit désormais un geste avec la paire qu'il pose ailleurs — avant de supprimer l'ancienne : aucune coupure ne laisse zéro paire ; un nom inchangé n'écrit rien. Supprimer range état, .precedent, journal, verrou et restes dans une corbeille datée, en comparant les noms comme le disque insensible à la casse. creerDossier ne crée que la racine visée : un dossier de travail disparu ne renaît pas vide. reglages_locaux.json garde les deux réglages du § 8.5 ; son écrivain différé garde la dernière demande. Vérifié : 1314 node, 35 navigateur, 27 node-long ; depot.js à 100 %. Assisted-by: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01EUXSGcwCLSC69FWEdCtWSb
2026-10-06 23:17:05 -04:00
const { horodatage, produitVersion, libelleReprise } = options;
[ADD] storage: event store — list, create, open, save, recover An event is a state file and a journal paired by its internal identifier, never by name (§ 8.6): a hand copy of a state does not take the original's journal, and a renamed journal stays paired. Each gesture writes its journal line first, then the .precedent and the state (§ 8.2); recovery compares revisions, never file dates, so a cut leaves the journal one entry ahead at most. Opening never writes (§ 8.4); an unreadable state names its readable secours instead of opening an empty event, and a newer format stays read-only. Writing elsewhere after a removed medium aligns the pair first. Checked: 60 tests, 100 % branches of depot.js, 1257 node tests on HEAD. --- FR --- [ADD] stockage : dépôt d'événements — lister, créer, ouvrir, reprendre Un événement est un fichier d'état et un journal appariés par l'identifiant interne, jamais par le nom (§ 8.6) : un état copié à la main ne prend pas le journal de l'original, et un journal renommé reste apparié. Chaque geste écrit sa ligne de journal d'abord, puis le .precedent et l'état (§ 8.2) ; la reprise compare les révisions, jamais la date des fichiers : une coupure laisse le journal en avance d'une entrée au plus. Ouvrir n'écrit jamais (§ 8.4) ; un état illisible nomme ses secours lisibles au lieu d'ouvrir un événement vide, et un format plus récent reste en lecture seule. Écrire ailleurs après un support retiré aligne d'abord la paire. Vérifié : 60 épreuves, 100 % des branches de depot.js, 1257 épreuves node. Assisted-by: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01EUXSGcwCLSC69FWEdCtWSb
2026-10-06 20:39:49 -04:00
exigerEcriture(ouvert);
const preparation = preparer(ouvert, { libelleReprise, horodatage });
[ADD] storage: copy, rename, import, delete events; local settings § 8.7 operations reuse the event store's pairing and alignment instead of copying them. Renaming writes the complete pair under the new name — the store now writes a gesture with the pair it writes elsewhere — before deleting the old one, so no cut leaves zero pairs; an unchanged name writes nothing. Deleting moves state, .precedent, journal, lock and leftovers into a dated trash, matching names as the case-insensitive disk does. creerDossier creates only the root it is aimed at, so a vanished working folder never comes back empty. reglages_locaux.json keeps exactly the two § 8.5 settings; its deferred writer always keeps the last demand. Checked: 1314 node, 35 browser, 27 node-long tests; depot.js at 100 %. --- FR --- [ADD] stockage : copier, renommer, importer, supprimer ; réglages locaux Les opérations du § 8.7 reprennent l'appariement et l'alignement du dépôt au lieu de les recopier. Renommer écrit la paire complète sous le nouveau nom — le dépôt écrit désormais un geste avec la paire qu'il pose ailleurs — avant de supprimer l'ancienne : aucune coupure ne laisse zéro paire ; un nom inchangé n'écrit rien. Supprimer range état, .precedent, journal, verrou et restes dans une corbeille datée, en comparant les noms comme le disque insensible à la casse. creerDossier ne crée que la racine visée : un dossier de travail disparu ne renaît pas vide. reglages_locaux.json garde les deux réglages du § 8.5 ; son écrivain différé garde la dernière demande. Vérifié : 1314 node, 35 navigateur, 27 node-long ; depot.js à 100 %. Assisted-by: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01EUXSGcwCLSC69FWEdCtWSb
2026-10-06 23:17:05 -04:00
const { ligne, revision } = ligneDuGeste(preparation, apres, options);
[ADD] storage: event store — list, create, open, save, recover An event is a state file and a journal paired by its internal identifier, never by name (§ 8.6): a hand copy of a state does not take the original's journal, and a renamed journal stays paired. Each gesture writes its journal line first, then the .precedent and the state (§ 8.2); recovery compares revisions, never file dates, so a cut leaves the journal one entry ahead at most. Opening never writes (§ 8.4); an unreadable state names its readable secours instead of opening an empty event, and a newer format stays read-only. Writing elsewhere after a removed medium aligns the pair first. Checked: 60 tests, 100 % branches of depot.js, 1257 node tests on HEAD. --- FR --- [ADD] stockage : dépôt d'événements — lister, créer, ouvrir, reprendre Un événement est un fichier d'état et un journal appariés par l'identifiant interne, jamais par le nom (§ 8.6) : un état copié à la main ne prend pas le journal de l'original, et un journal renommé reste apparié. Chaque geste écrit sa ligne de journal d'abord, puis le .precedent et l'état (§ 8.2) ; la reprise compare les révisions, jamais la date des fichiers : une coupure laisse le journal en avance d'une entrée au plus. Ouvrir n'écrit jamais (§ 8.4) ; un état illisible nomme ses secours lisibles au lieu d'ouvrir un événement vide, et un format plus récent reste en lecture seule. Écrire ailleurs après un support retiré aligne d'abord la paire. Vérifié : 60 épreuves, 100 % des branches de depot.js, 1257 épreuves node. Assisted-by: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01EUXSGcwCLSC69FWEdCtWSb
2026-10-06 20:39:49 -04:00
const texteEtat = relisible(serialiser(apres, { revision, produitVersion }), 'enregistrer');
const aligne = etatAligne(ouvert, preparation);
const suite = aligne === null ? { depart: ouvert, preparation } : await aligner(ouvert, preparation, aligne);
const ecrit = await ecrireJournal(suite.depart, suite.preparation, ligne);
const avance = { ...suite.depart, ...ecrit, produitVersion, chargeJournal: canoniser(apres) };
return ecrireEtat(apresEcriture(avance, { charge: apres, revision, journal: ecrit.journal }), texteEtat);
}
/**
* Pose le jalon nom sur l'instant courant (§ 8.3) : une ligne du journal,
* qui ne prend pas de révision ; l'état ne s'écrit pas. Un journal qui manque
* ou finit ailleurs que sur la charge ouverte reçoit d'abord l'entrée de
* reprise, comme pour enregistrer, et le jalon nomme alors son instant.
*
* @param {Ouvert} ouvert
* @param {{nom: string, horodatage: string, libelleReprise?: string}} options
* @returns {Promise<Ouvert>}
* @throws {ErreurStockage} LECTURE_SEULE ; ECRITURE, et rien n'a changé
* @throws {TypeError} un nom vide, un horodatage hors de sa forme
*/
export async function nommer(ouvert, { nom, horodatage, libelleReprise }) {
exigerEcriture(ouvert);
const preparation = preparer(ouvert, { libelleReprise, horodatage });
const revision = preparation.journal.entrees.at(-1).revision;
const ecrit = await ecrireJournal(ouvert, preparation, ligneJalon({ revision, nom, horodatage }));
const nomme = { ...ouvert, ...ecrit, chargeJournal: preparation.charge };
return apresEcriture(nomme, { charge: ouvert.charge, revision, journal: ecrit.journal });
}
/**
* Prend le verrou de la séance sur l'événement (§ 8.8) : le Verrou que rend
* le système de fichiers, tel quel.
*
* @param {Ouvert} ouvert
* @param {string} seance
* @returns {Promise<import('./systeme_fichiers.js').Verrou>}
*/
export async function verrouiller({ fs, racine, base }, seance) {
return fs.verrouiller(racine, base + SUFFIXES.verrou, seance);
}
/**
* Rend le verrou de la séance : seul le sien s'efface.
*
* @param {Ouvert} ouvert
* @param {string} seance
* @returns {Promise<void>}
*/
export async function deverrouiller({ fs, racine, base }, seance) {
await fs.deverrouiller(racine, base + SUFFIXES.verrou, seance);
}
[ADD] storage: copy, rename, import, delete events; local settings § 8.7 operations reuse the event store's pairing and alignment instead of copying them. Renaming writes the complete pair under the new name — the store now writes a gesture with the pair it writes elsewhere — before deleting the old one, so no cut leaves zero pairs; an unchanged name writes nothing. Deleting moves state, .precedent, journal, lock and leftovers into a dated trash, matching names as the case-insensitive disk does. creerDossier creates only the root it is aimed at, so a vanished working folder never comes back empty. reglages_locaux.json keeps exactly the two § 8.5 settings; its deferred writer always keeps the last demand. Checked: 1314 node, 35 browser, 27 node-long tests; depot.js at 100 %. --- FR --- [ADD] stockage : copier, renommer, importer, supprimer ; réglages locaux Les opérations du § 8.7 reprennent l'appariement et l'alignement du dépôt au lieu de les recopier. Renommer écrit la paire complète sous le nouveau nom — le dépôt écrit désormais un geste avec la paire qu'il pose ailleurs — avant de supprimer l'ancienne : aucune coupure ne laisse zéro paire ; un nom inchangé n'écrit rien. Supprimer range état, .precedent, journal, verrou et restes dans une corbeille datée, en comparant les noms comme le disque insensible à la casse. creerDossier ne crée que la racine visée : un dossier de travail disparu ne renaît pas vide. reglages_locaux.json garde les deux réglages du § 8.5 ; son écrivain différé garde la dernière demande. Vérifié : 1314 node, 35 navigateur, 27 node-long ; depot.js à 100 %. Assisted-by: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01EUXSGcwCLSC69FWEdCtWSb
2026-10-06 23:17:05 -04:00
// L'état d'avant un geste, que le .precedent garde (§ 8.8) : le texte de
// l'état sur le disque quand il se lit ; sinon, pour un Ouvert de secours, la
// charge ouverte sous son en-tête, celle que l'entrée de reprise restitue.
// Une charge que l'analyse refuserait lève TypeError, avant toute écriture.
function etatDAvant({ texteEtat, charge, revision, produitVersion }) {
return texteEtat ?? relisible(serialiser(charge, { revision, produitVersion }), 'enregistrerAilleurs');
}
[ADD] storage: event store — list, create, open, save, recover An event is a state file and a journal paired by its internal identifier, never by name (§ 8.6): a hand copy of a state does not take the original's journal, and a renamed journal stays paired. Each gesture writes its journal line first, then the .precedent and the state (§ 8.2); recovery compares revisions, never file dates, so a cut leaves the journal one entry ahead at most. Opening never writes (§ 8.4); an unreadable state names its readable secours instead of opening an empty event, and a newer format stays read-only. Writing elsewhere after a removed medium aligns the pair first. Checked: 60 tests, 100 % branches of depot.js, 1257 node tests on HEAD. --- FR --- [ADD] stockage : dépôt d'événements — lister, créer, ouvrir, reprendre Un événement est un fichier d'état et un journal appariés par l'identifiant interne, jamais par le nom (§ 8.6) : un état copié à la main ne prend pas le journal de l'original, et un journal renommé reste apparié. Chaque geste écrit sa ligne de journal d'abord, puis le .precedent et l'état (§ 8.2) ; la reprise compare les révisions, jamais la date des fichiers : une coupure laisse le journal en avance d'une entrée au plus. Ouvrir n'écrit jamais (§ 8.4) ; un état illisible nomme ses secours lisibles au lieu d'ouvrir un événement vide, et un format plus récent reste en lecture seule. Écrire ailleurs après un support retiré aligne d'abord la paire. Vérifié : 60 épreuves, 100 % des branches de depot.js, 1257 épreuves node. Assisted-by: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01EUXSGcwCLSC69FWEdCtWSb
2026-10-06 20:39:49 -04:00
// La paire qu'enregistrerAilleurs écrit, calculée sans rien écrire. Sans
[ADD] storage: copy, rename, import, delete events; local settings § 8.7 operations reuse the event store's pairing and alignment instead of copying them. Renaming writes the complete pair under the new name — the store now writes a gesture with the pair it writes elsewhere — before deleting the old one, so no cut leaves zero pairs; an unchanged name writes nothing. Deleting moves state, .precedent, journal, lock and leftovers into a dated trash, matching names as the case-insensitive disk does. creerDossier creates only the root it is aimed at, so a vanished working folder never comes back empty. reglages_locaux.json keeps exactly the two § 8.5 settings; its deferred writer always keeps the last demand. Checked: 1314 node, 35 browser, 27 node-long tests; depot.js at 100 %. --- FR --- [ADD] stockage : copier, renommer, importer, supprimer ; réglages locaux Les opérations du § 8.7 reprennent l'appariement et l'alignement du dépôt au lieu de les recopier. Renommer écrit la paire complète sous le nouveau nom — le dépôt écrit désormais un geste avec la paire qu'il pose ailleurs — avant de supprimer l'ancienne : aucune coupure ne laisse zéro paire ; un nom inchangé n'écrit rien. Supprimer range état, .precedent, journal, verrou et restes dans une corbeille datée, en comparant les noms comme le disque insensible à la casse. creerDossier ne crée que la racine visée : un dossier de travail disparu ne renaît pas vide. reglages_locaux.json garde les deux réglages du § 8.5 ; son écrivain différé garde la dernière demande. Vérifié : 1314 node, 35 navigateur, 27 node-long ; depot.js à 100 %. Assisted-by: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01EUXSGcwCLSC69FWEdCtWSb
2026-10-06 23:17:05 -04:00
// journal ni geste, l'état seul : la charge ouverte, à sa révision et à sa
// version. Sinon le journal préparé (preparer), suivi de l'entrée de reprise
// quand il finit sur une autre révision que la charge ouverte, ou qu'il
// manque, puis de l'entrée du geste quand il y en a un (ligneDuGeste), en
// entier et élagué au-delà de PLAFOND_ENTREES (textePlein) ; puis l'état que
// restitue sa dernière entrée : la charge du geste, à sa révision et sous sa
// version, comme l'écrit enregistrer ; sans geste, la charge préparée sous
// l'en-tête de cette entrée (enteteDeLaDerniere), comme l'état qu'aligne un
// geste. La dernière entrée du journal écrit restitue l'état écrit (§ 8.2),
// et la réouverture n'a rien à rattraper. Une reprise sans libelleReprise, ou
// un geste dont la ligne ne se relirait pas, lève TypeError.
function paireAilleurs(ouvert, { libelleReprise, horodatage, geste }) {
if (ouvert.journal === null && geste === null) {
[ADD] storage: event store — list, create, open, save, recover An event is a state file and a journal paired by its internal identifier, never by name (§ 8.6): a hand copy of a state does not take the original's journal, and a renamed journal stays paired. Each gesture writes its journal line first, then the .precedent and the state (§ 8.2); recovery compares revisions, never file dates, so a cut leaves the journal one entry ahead at most. Opening never writes (§ 8.4); an unreadable state names its readable secours instead of opening an empty event, and a newer format stays read-only. Writing elsewhere after a removed medium aligns the pair first. Checked: 60 tests, 100 % branches of depot.js, 1257 node tests on HEAD. --- FR --- [ADD] stockage : dépôt d'événements — lister, créer, ouvrir, reprendre Un événement est un fichier d'état et un journal appariés par l'identifiant interne, jamais par le nom (§ 8.6) : un état copié à la main ne prend pas le journal de l'original, et un journal renommé reste apparié. Chaque geste écrit sa ligne de journal d'abord, puis le .precedent et l'état (§ 8.2) ; la reprise compare les révisions, jamais la date des fichiers : une coupure laisse le journal en avance d'une entrée au plus. Ouvrir n'écrit jamais (§ 8.4) ; un état illisible nomme ses secours lisibles au lieu d'ouvrir un événement vide, et un format plus récent reste en lecture seule. Écrire ailleurs après un support retiré aligne d'abord la paire. Vérifié : 60 épreuves, 100 % des branches de depot.js, 1257 épreuves node. Assisted-by: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01EUXSGcwCLSC69FWEdCtWSb
2026-10-06 20:39:49 -04:00
const { charge, revision, produitVersion } = ouvert;
return { journal: null, texteJournal: null, charge, chargeJournal: null, revision, produitVersion };
}
[ADD] storage: copy, rename, import, delete events; local settings § 8.7 operations reuse the event store's pairing and alignment instead of copying them. Renaming writes the complete pair under the new name — the store now writes a gesture with the pair it writes elsewhere — before deleting the old one, so no cut leaves zero pairs; an unchanged name writes nothing. Deleting moves state, .precedent, journal, lock and leftovers into a dated trash, matching names as the case-insensitive disk does. creerDossier creates only the root it is aimed at, so a vanished working folder never comes back empty. reglages_locaux.json keeps exactly the two § 8.5 settings; its deferred writer always keeps the last demand. Checked: 1314 node, 35 browser, 27 node-long tests; depot.js at 100 %. --- FR --- [ADD] stockage : copier, renommer, importer, supprimer ; réglages locaux Les opérations du § 8.7 reprennent l'appariement et l'alignement du dépôt au lieu de les recopier. Renommer écrit la paire complète sous le nouveau nom — le dépôt écrit désormais un geste avec la paire qu'il pose ailleurs — avant de supprimer l'ancienne : aucune coupure ne laisse zéro paire ; un nom inchangé n'écrit rien. Supprimer range état, .precedent, journal, verrou et restes dans une corbeille datée, en comparant les noms comme le disque insensible à la casse. creerDossier ne crée que la racine visée : un dossier de travail disparu ne renaît pas vide. reglages_locaux.json garde les deux réglages du § 8.5 ; son écrivain différé garde la dernière demande. Vérifié : 1314 node, 35 navigateur, 27 node-long ; depot.js à 100 %. Assisted-by: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01EUXSGcwCLSC69FWEdCtWSb
2026-10-06 23:17:05 -04:00
const preparation = preparer(ouvert, { libelleReprise, horodatage });
if (geste === null) {
const { texte, journal } = textePlein(preparation.journal);
const { charge } = preparation;
return { journal, texteJournal: texte, charge, chargeJournal: charge, ...enteteDeLaDerniere(journal) };
}
const { apres, produitVersion } = geste;
const { ligne, revision } = ligneDuGeste(preparation, apres, { ...geste, horodatage });
const { texte, journal } = textePlein(avec(preparation.journal, ligne));
return { journal, texteJournal: texte, charge: apres, chargeJournal: canoniser(apres), revision, produitVersion };
[ADD] storage: event store — list, create, open, save, recover An event is a state file and a journal paired by its internal identifier, never by name (§ 8.6): a hand copy of a state does not take the original's journal, and a renamed journal stays paired. Each gesture writes its journal line first, then the .precedent and the state (§ 8.2); recovery compares revisions, never file dates, so a cut leaves the journal one entry ahead at most. Opening never writes (§ 8.4); an unreadable state names its readable secours instead of opening an empty event, and a newer format stays read-only. Writing elsewhere after a removed medium aligns the pair first. Checked: 60 tests, 100 % branches of depot.js, 1257 node tests on HEAD. --- FR --- [ADD] stockage : dépôt d'événements — lister, créer, ouvrir, reprendre Un événement est un fichier d'état et un journal appariés par l'identifiant interne, jamais par le nom (§ 8.6) : un état copié à la main ne prend pas le journal de l'original, et un journal renommé reste apparié. Chaque geste écrit sa ligne de journal d'abord, puis le .precedent et l'état (§ 8.2) ; la reprise compare les révisions, jamais la date des fichiers : une coupure laisse le journal en avance d'une entrée au plus. Ouvrir n'écrit jamais (§ 8.4) ; un état illisible nomme ses secours lisibles au lieu d'ouvrir un événement vide, et un format plus récent reste en lecture seule. Écrire ailleurs après un support retiré aligne d'abord la paire. Vérifié : 60 épreuves, 100 % des branches de depot.js, 1257 épreuves node. Assisted-by: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01EUXSGcwCLSC69FWEdCtWSb
2026-10-06 20:39:49 -04:00
}
/**
[ADD] storage: copy, rename, import, delete events; local settings § 8.7 operations reuse the event store's pairing and alignment instead of copying them. Renaming writes the complete pair under the new name — the store now writes a gesture with the pair it writes elsewhere — before deleting the old one, so no cut leaves zero pairs; an unchanged name writes nothing. Deleting moves state, .precedent, journal, lock and leftovers into a dated trash, matching names as the case-insensitive disk does. creerDossier creates only the root it is aimed at, so a vanished working folder never comes back empty. reglages_locaux.json keeps exactly the two § 8.5 settings; its deferred writer always keeps the last demand. Checked: 1314 node, 35 browser, 27 node-long tests; depot.js at 100 %. --- FR --- [ADD] stockage : copier, renommer, importer, supprimer ; réglages locaux Les opérations du § 8.7 reprennent l'appariement et l'alignement du dépôt au lieu de les recopier. Renommer écrit la paire complète sous le nouveau nom — le dépôt écrit désormais un geste avec la paire qu'il pose ailleurs — avant de supprimer l'ancienne : aucune coupure ne laisse zéro paire ; un nom inchangé n'écrit rien. Supprimer range état, .precedent, journal, verrou et restes dans une corbeille datée, en comparant les noms comme le disque insensible à la casse. creerDossier ne crée que la racine visée : un dossier de travail disparu ne renaît pas vide. reglages_locaux.json garde les deux réglages du § 8.5 ; son écrivain différé garde la dernière demande. Vérifié : 1314 node, 35 navigateur, 27 node-long ; depot.js à 100 %. Assisted-by: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01EUXSGcwCLSC69FWEdCtWSb
2026-10-06 23:17:05 -04:00
* Écrit l'événement ouvert sous une base libre d'une racine : dans une autre
* racine (§ 8.6), la réponse à une écriture qui échoue, l'Ouvert reçu étant
* celui d'avant le geste refusé (ECRITURE) ou l'Ouvert avancé
* d'ETAT_NON_ECRIT ; ou, avec un geste, sous le nom que ce geste donne, dans
* le dossier même, ce qu'écrit le renommage (§ 8.7). Une racine que
* l'opérateur a choisie, que rien n'a sondée — documents en mode portable —,
* se crée d'abord, parents compris, un dossier existant restant tel quel ;
* creerRacine faux ne la crée pas : le dossier de travail qui disparaît en
* cours de séance ne renaît pas vide (§ 8.6). La base dérive du nom que porte
* la charge écrite (deriverBase), libre parmi existantes et les bases du
[ADD] storage: event store — list, create, open, save, recover An event is a state file and a journal paired by its internal identifier, never by name (§ 8.6): a hand copy of a state does not take the original's journal, and a renamed journal stays paired. Each gesture writes its journal line first, then the .precedent and the state (§ 8.2); recovery compares revisions, never file dates, so a cut leaves the journal one entry ahead at most. Opening never writes (§ 8.4); an unreadable state names its readable secours instead of opening an empty event, and a newer format stays read-only. Writing elsewhere after a removed medium aligns the pair first. Checked: 60 tests, 100 % branches of depot.js, 1257 node tests on HEAD. --- FR --- [ADD] stockage : dépôt d'événements — lister, créer, ouvrir, reprendre Un événement est un fichier d'état et un journal appariés par l'identifiant interne, jamais par le nom (§ 8.6) : un état copié à la main ne prend pas le journal de l'original, et un journal renommé reste apparié. Chaque geste écrit sa ligne de journal d'abord, puis le .precedent et l'état (§ 8.2) ; la reprise compare les révisions, jamais la date des fichiers : une coupure laisse le journal en avance d'une entrée au plus. Ouvrir n'écrit jamais (§ 8.4) ; un état illisible nomme ses secours lisibles au lieu d'ouvrir un événement vide, et un format plus récent reste en lecture seule. Écrire ailleurs après un support retiré aligne d'abord la paire. Vérifié : 60 épreuves, 100 % des branches de depot.js, 1257 épreuves node. Assisted-by: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01EUXSGcwCLSC69FWEdCtWSb
2026-10-06 20:39:49 -04:00
* dossier. La paire s'y écrit alignée, comme après un geste (§ 8.2) : le
* journal en mémoire, en entier — suivi d'une entrée de reprise, au libellé
* libelleReprise, quand il finit sur une autre révision que la charge
[ADD] storage: copy, rename, import, delete events; local settings § 8.7 operations reuse the event store's pairing and alignment instead of copying them. Renaming writes the complete pair under the new name — the store now writes a gesture with the pair it writes elsewhere — before deleting the old one, so no cut leaves zero pairs; an unchanged name writes nothing. Deleting moves state, .precedent, journal, lock and leftovers into a dated trash, matching names as the case-insensitive disk does. creerDossier creates only the root it is aimed at, so a vanished working folder never comes back empty. reglages_locaux.json keeps exactly the two § 8.5 settings; its deferred writer always keeps the last demand. Checked: 1314 node, 35 browser, 27 node-long tests; depot.js at 100 %. --- FR --- [ADD] stockage : copier, renommer, importer, supprimer ; réglages locaux Les opérations du § 8.7 reprennent l'appariement et l'alignement du dépôt au lieu de les recopier. Renommer écrit la paire complète sous le nouveau nom — le dépôt écrit désormais un geste avec la paire qu'il pose ailleurs — avant de supprimer l'ancienne : aucune coupure ne laisse zéro paire ; un nom inchangé n'écrit rien. Supprimer range état, .precedent, journal, verrou et restes dans une corbeille datée, en comparant les noms comme le disque insensible à la casse. creerDossier ne crée que la racine visée : un dossier de travail disparu ne renaît pas vide. reglages_locaux.json garde les deux réglages du § 8.5 ; son écrivain différé garde la dernière demande. Vérifié : 1314 node, 35 navigateur, 27 node-long ; depot.js à 100 %. Assisted-by: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01EUXSGcwCLSC69FWEdCtWSb
2026-10-06 23:17:05 -04:00
* ouverte, puis de l'entrée du geste, et élagué au-delà de PLAFOND_ENTREES —,
* puis l'état que restitue sa dernière entrée, à la révision et à la version
* de celle-ci. Rouvert là-bas, l'événement rend cette charge sans rien
* rattraper. Un événement sans journal ni geste n'écrit que son état : la
* charge ouverte, à sa révision ; avec un geste, l'entrée de reprise commence
* son journal. Le .precedent de l'Ouvert ne se recopie pas. Avec un geste, la
* nouvelle base reçoit d'abord le sien, l'état d'avant le geste (etatDAvant),
* comme enregistrer le pose, mais avant le journal : la base appartient alors
* à l'événement (proprietaire), si bien qu'aucun autre état de son
* identifiant — l'ancienne paire d'un renommage interrompu, dont le journal
* de la base n'est pas le sien — ne s'apparie le journal neuf (apparier). La
* paire va sous une base qu'aucun fichier du dossier ne porte : elle n'y
* remplace rien, pas même la paire de l'Ouvert quand ce dossier est le sien.
* Les lignes et les textes d'état se calculent, et se relisent, avant la
* première écriture, celle de la racine comprise.
[ADD] storage: event store — list, create, open, save, recover An event is a state file and a journal paired by its internal identifier, never by name (§ 8.6): a hand copy of a state does not take the original's journal, and a renamed journal stays paired. Each gesture writes its journal line first, then the .precedent and the state (§ 8.2); recovery compares revisions, never file dates, so a cut leaves the journal one entry ahead at most. Opening never writes (§ 8.4); an unreadable state names its readable secours instead of opening an empty event, and a newer format stays read-only. Writing elsewhere after a removed medium aligns the pair first. Checked: 60 tests, 100 % branches of depot.js, 1257 node tests on HEAD. --- FR --- [ADD] stockage : dépôt d'événements — lister, créer, ouvrir, reprendre Un événement est un fichier d'état et un journal appariés par l'identifiant interne, jamais par le nom (§ 8.6) : un état copié à la main ne prend pas le journal de l'original, et un journal renommé reste apparié. Chaque geste écrit sa ligne de journal d'abord, puis le .precedent et l'état (§ 8.2) ; la reprise compare les révisions, jamais la date des fichiers : une coupure laisse le journal en avance d'une entrée au plus. Ouvrir n'écrit jamais (§ 8.4) ; un état illisible nomme ses secours lisibles au lieu d'ouvrir un événement vide, et un format plus récent reste en lecture seule. Écrire ailleurs après un support retiré aligne d'abord la paire. Vérifié : 60 épreuves, 100 % des branches de depot.js, 1257 épreuves node. Assisted-by: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01EUXSGcwCLSC69FWEdCtWSb
2026-10-06 20:39:49 -04:00
*
* @param {Ouvert} ouvert
* @param {import('./systeme_fichiers.js').Racine} racine
* @param {Object} options
* @param {Iterable<string>} options.existantes bases à ne pas prendre
* là-bas, en plus de celles du dossier
* @param {string} [options.libelleReprise] texte de l'entrée de reprise,
* requis quand elle a lieu
* @param {string} [options.horodatage] AAAA-MM-JJTHH:MM:SS±HH:MM de
[ADD] storage: copy, rename, import, delete events; local settings § 8.7 operations reuse the event store's pairing and alignment instead of copying them. Renaming writes the complete pair under the new name — the store now writes a gesture with the pair it writes elsewhere — before deleting the old one, so no cut leaves zero pairs; an unchanged name writes nothing. Deleting moves state, .precedent, journal, lock and leftovers into a dated trash, matching names as the case-insensitive disk does. creerDossier creates only the root it is aimed at, so a vanished working folder never comes back empty. reglages_locaux.json keeps exactly the two § 8.5 settings; its deferred writer always keeps the last demand. Checked: 1314 node, 35 browser, 27 node-long tests; depot.js at 100 %. --- FR --- [ADD] stockage : copier, renommer, importer, supprimer ; réglages locaux Les opérations du § 8.7 reprennent l'appariement et l'alignement du dépôt au lieu de les recopier. Renommer écrit la paire complète sous le nouveau nom — le dépôt écrit désormais un geste avec la paire qu'il pose ailleurs — avant de supprimer l'ancienne : aucune coupure ne laisse zéro paire ; un nom inchangé n'écrit rien. Supprimer range état, .precedent, journal, verrou et restes dans une corbeille datée, en comparant les noms comme le disque insensible à la casse. creerDossier ne crée que la racine visée : un dossier de travail disparu ne renaît pas vide. reglages_locaux.json garde les deux réglages du § 8.5 ; son écrivain différé garde la dernière demande. Vérifié : 1314 node, 35 navigateur, 27 node-long ; depot.js à 100 %. Assisted-by: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01EUXSGcwCLSC69FWEdCtWSb
2026-10-06 23:17:05 -04:00
* l'entrée de reprise et de celle du geste, requis avec elles
* @param {{apres: import('./types.js').Charge, libelle: string, produitVersion: string}|null} [options.geste]
* un geste à la suite de la charge ouverte, comme enregistrer le
* reçoit : apres devient la charge écrite, à la révision qui suit la
* dernière entrée du journal, l'entrée au libellé libelle, l'état
* sous la version produitVersion ; null par défaut
* @param {boolean} [options.creerRacine] faux : la racine n'est pas créée,
* et doit exister ; vrai par défaut
[ADD] storage: event store — list, create, open, save, recover An event is a state file and a journal paired by its internal identifier, never by name (§ 8.6): a hand copy of a state does not take the original's journal, and a renamed journal stays paired. Each gesture writes its journal line first, then the .precedent and the state (§ 8.2); recovery compares revisions, never file dates, so a cut leaves the journal one entry ahead at most. Opening never writes (§ 8.4); an unreadable state names its readable secours instead of opening an empty event, and a newer format stays read-only. Writing elsewhere after a removed medium aligns the pair first. Checked: 60 tests, 100 % branches of depot.js, 1257 node tests on HEAD. --- FR --- [ADD] stockage : dépôt d'événements — lister, créer, ouvrir, reprendre Un événement est un fichier d'état et un journal appariés par l'identifiant interne, jamais par le nom (§ 8.6) : un état copié à la main ne prend pas le journal de l'original, et un journal renommé reste apparié. Chaque geste écrit sa ligne de journal d'abord, puis le .precedent et l'état (§ 8.2) ; la reprise compare les révisions, jamais la date des fichiers : une coupure laisse le journal en avance d'une entrée au plus. Ouvrir n'écrit jamais (§ 8.4) ; un état illisible nomme ses secours lisibles au lieu d'ouvrir un événement vide, et un format plus récent reste en lecture seule. Écrire ailleurs après un support retiré aligne d'abord la paire. Vérifié : 60 épreuves, 100 % des branches de depot.js, 1257 épreuves node. Assisted-by: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01EUXSGcwCLSC69FWEdCtWSb
2026-10-06 20:39:49 -04:00
* @returns {Promise<Ouvert>} l'Ouvert dans racine : le journal et la
* révision écrits, chargeJournal la charge de l'état écrit, et
* charge celle qu'en rend examiner
* @throws {ErreurStockage} LECTURE_SEULE ; ECRITURE quand la racine ne se
[ADD] storage: copy, rename, import, delete events; local settings § 8.7 operations reuse the event store's pairing and alignment instead of copying them. Renaming writes the complete pair under the new name — the store now writes a gesture with the pair it writes elsewhere — before deleting the old one, so no cut leaves zero pairs; an unchanged name writes nothing. Deleting moves state, .precedent, journal, lock and leftovers into a dated trash, matching names as the case-insensitive disk does. creerDossier creates only the root it is aimed at, so a vanished working folder never comes back empty. reglages_locaux.json keeps exactly the two § 8.5 settings; its deferred writer always keeps the last demand. Checked: 1314 node, 35 browser, 27 node-long tests; depot.js at 100 %. --- FR --- [ADD] stockage : copier, renommer, importer, supprimer ; réglages locaux Les opérations du § 8.7 reprennent l'appariement et l'alignement du dépôt au lieu de les recopier. Renommer écrit la paire complète sous le nouveau nom — le dépôt écrit désormais un geste avec la paire qu'il pose ailleurs — avant de supprimer l'ancienne : aucune coupure ne laisse zéro paire ; un nom inchangé n'écrit rien. Supprimer range état, .precedent, journal, verrou et restes dans une corbeille datée, en comparant les noms comme le disque insensible à la casse. creerDossier ne crée que la racine visée : un dossier de travail disparu ne renaît pas vide. reglages_locaux.json garde les deux réglages du § 8.5 ; son écrivain différé garde la dernière demande. Vérifié : 1314 node, 35 navigateur, 27 node-long ; depot.js à 100 %. Assisted-by: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01EUXSGcwCLSC69FWEdCtWSb
2026-10-06 23:17:05 -04:00
* crée pas, ou que le .precedent d'un geste n'a pas pu s'écrire, rien
* d'autre n'étant écrit, ou que le journal n'a pas pu s'écrire, le
* .precedent d'un geste restant écrit avant lui ; ABSENT {chemin: ''}
* quand creerRacine est faux et que la racine n'existe pas, rien
* n'étant écrit ; ETAT_NON_ECRIT quand l'état n'a pas pu s'écrire,
* ouvert étant l'Ouvert dans racine ; CHEMIN_TROP_LONG
* @throws {TypeError} une reprise sans libelleReprise ou horodatage, un geste
* ou un état que la lecture refuserait, avant toute écriture
[ADD] storage: event store — list, create, open, save, recover An event is a state file and a journal paired by its internal identifier, never by name (§ 8.6): a hand copy of a state does not take the original's journal, and a renamed journal stays paired. Each gesture writes its journal line first, then the .precedent and the state (§ 8.2); recovery compares revisions, never file dates, so a cut leaves the journal one entry ahead at most. Opening never writes (§ 8.4); an unreadable state names its readable secours instead of opening an empty event, and a newer format stays read-only. Writing elsewhere after a removed medium aligns the pair first. Checked: 60 tests, 100 % branches of depot.js, 1257 node tests on HEAD. --- FR --- [ADD] stockage : dépôt d'événements — lister, créer, ouvrir, reprendre Un événement est un fichier d'état et un journal appariés par l'identifiant interne, jamais par le nom (§ 8.6) : un état copié à la main ne prend pas le journal de l'original, et un journal renommé reste apparié. Chaque geste écrit sa ligne de journal d'abord, puis le .precedent et l'état (§ 8.2) ; la reprise compare les révisions, jamais la date des fichiers : une coupure laisse le journal en avance d'une entrée au plus. Ouvrir n'écrit jamais (§ 8.4) ; un état illisible nomme ses secours lisibles au lieu d'ouvrir un événement vide, et un format plus récent reste en lecture seule. Écrire ailleurs après un support retiré aligne d'abord la paire. Vérifié : 60 épreuves, 100 % des branches de depot.js, 1257 épreuves node. Assisted-by: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01EUXSGcwCLSC69FWEdCtWSb
2026-10-06 20:39:49 -04:00
*/
[ADD] storage: copy, rename, import, delete events; local settings § 8.7 operations reuse the event store's pairing and alignment instead of copying them. Renaming writes the complete pair under the new name — the store now writes a gesture with the pair it writes elsewhere — before deleting the old one, so no cut leaves zero pairs; an unchanged name writes nothing. Deleting moves state, .precedent, journal, lock and leftovers into a dated trash, matching names as the case-insensitive disk does. creerDossier creates only the root it is aimed at, so a vanished working folder never comes back empty. reglages_locaux.json keeps exactly the two § 8.5 settings; its deferred writer always keeps the last demand. Checked: 1314 node, 35 browser, 27 node-long tests; depot.js at 100 %. --- FR --- [ADD] stockage : copier, renommer, importer, supprimer ; réglages locaux Les opérations du § 8.7 reprennent l'appariement et l'alignement du dépôt au lieu de les recopier. Renommer écrit la paire complète sous le nouveau nom — le dépôt écrit désormais un geste avec la paire qu'il pose ailleurs — avant de supprimer l'ancienne : aucune coupure ne laisse zéro paire ; un nom inchangé n'écrit rien. Supprimer range état, .precedent, journal, verrou et restes dans une corbeille datée, en comparant les noms comme le disque insensible à la casse. creerDossier ne crée que la racine visée : un dossier de travail disparu ne renaît pas vide. reglages_locaux.json garde les deux réglages du § 8.5 ; son écrivain différé garde la dernière demande. Vérifié : 1314 node, 35 navigateur, 27 node-long ; depot.js à 100 %. Assisted-by: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01EUXSGcwCLSC69FWEdCtWSb
2026-10-06 23:17:05 -04:00
export async function enregistrerAilleurs(
ouvert,
racine,
{ existantes, libelleReprise, horodatage, geste = null, creerRacine = true },
) {
[ADD] storage: event store — list, create, open, save, recover An event is a state file and a journal paired by its internal identifier, never by name (§ 8.6): a hand copy of a state does not take the original's journal, and a renamed journal stays paired. Each gesture writes its journal line first, then the .precedent and the state (§ 8.2); recovery compares revisions, never file dates, so a cut leaves the journal one entry ahead at most. Opening never writes (§ 8.4); an unreadable state names its readable secours instead of opening an empty event, and a newer format stays read-only. Writing elsewhere after a removed medium aligns the pair first. Checked: 60 tests, 100 % branches of depot.js, 1257 node tests on HEAD. --- FR --- [ADD] stockage : dépôt d'événements — lister, créer, ouvrir, reprendre Un événement est un fichier d'état et un journal appariés par l'identifiant interne, jamais par le nom (§ 8.6) : un état copié à la main ne prend pas le journal de l'original, et un journal renommé reste apparié. Chaque geste écrit sa ligne de journal d'abord, puis le .precedent et l'état (§ 8.2) ; la reprise compare les révisions, jamais la date des fichiers : une coupure laisse le journal en avance d'une entrée au plus. Ouvrir n'écrit jamais (§ 8.4) ; un état illisible nomme ses secours lisibles au lieu d'ouvrir un événement vide, et un format plus récent reste en lecture seule. Écrire ailleurs après un support retiré aligne d'abord la paire. Vérifié : 60 épreuves, 100 % des branches de depot.js, 1257 épreuves node. Assisted-by: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01EUXSGcwCLSC69FWEdCtWSb
2026-10-06 20:39:49 -04:00
exigerEcriture(ouvert);
const { fs } = ouvert;
[ADD] storage: copy, rename, import, delete events; local settings § 8.7 operations reuse the event store's pairing and alignment instead of copying them. Renaming writes the complete pair under the new name — the store now writes a gesture with the pair it writes elsewhere — before deleting the old one, so no cut leaves zero pairs; an unchanged name writes nothing. Deleting moves state, .precedent, journal, lock and leftovers into a dated trash, matching names as the case-insensitive disk does. creerDossier creates only the root it is aimed at, so a vanished working folder never comes back empty. reglages_locaux.json keeps exactly the two § 8.5 settings; its deferred writer always keeps the last demand. Checked: 1314 node, 35 browser, 27 node-long tests; depot.js at 100 %. --- FR --- [ADD] stockage : copier, renommer, importer, supprimer ; réglages locaux Les opérations du § 8.7 reprennent l'appariement et l'alignement du dépôt au lieu de les recopier. Renommer écrit la paire complète sous le nouveau nom — le dépôt écrit désormais un geste avec la paire qu'il pose ailleurs — avant de supprimer l'ancienne : aucune coupure ne laisse zéro paire ; un nom inchangé n'écrit rien. Supprimer range état, .precedent, journal, verrou et restes dans une corbeille datée, en comparant les noms comme le disque insensible à la casse. creerDossier ne crée que la racine visée : un dossier de travail disparu ne renaît pas vide. reglages_locaux.json garde les deux réglages du § 8.5 ; son écrivain différé garde la dernière demande. Vérifié : 1314 node, 35 navigateur, 27 node-long ; depot.js à 100 %. Assisted-by: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01EUXSGcwCLSC69FWEdCtWSb
2026-10-06 23:17:05 -04:00
const paire = paireAilleurs(ouvert, { libelleReprise, horodatage, geste });
[ADD] storage: event store — list, create, open, save, recover An event is a state file and a journal paired by its internal identifier, never by name (§ 8.6): a hand copy of a state does not take the original's journal, and a renamed journal stays paired. Each gesture writes its journal line first, then the .precedent and the state (§ 8.2); recovery compares revisions, never file dates, so a cut leaves the journal one entry ahead at most. Opening never writes (§ 8.4); an unreadable state names its readable secours instead of opening an empty event, and a newer format stays read-only. Writing elsewhere after a removed medium aligns the pair first. Checked: 60 tests, 100 % branches of depot.js, 1257 node tests on HEAD. --- FR --- [ADD] stockage : dépôt d'événements — lister, créer, ouvrir, reprendre Un événement est un fichier d'état et un journal appariés par l'identifiant interne, jamais par le nom (§ 8.6) : un état copié à la main ne prend pas le journal de l'original, et un journal renommé reste apparié. Chaque geste écrit sa ligne de journal d'abord, puis le .precedent et l'état (§ 8.2) ; la reprise compare les révisions, jamais la date des fichiers : une coupure laisse le journal en avance d'une entrée au plus. Ouvrir n'écrit jamais (§ 8.4) ; un état illisible nomme ses secours lisibles au lieu d'ouvrir un événement vide, et un format plus récent reste en lecture seule. Écrire ailleurs après un support retiré aligne d'abord la paire. Vérifié : 60 épreuves, 100 % des branches de depot.js, 1257 épreuves node. Assisted-by: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01EUXSGcwCLSC69FWEdCtWSb
2026-10-06 20:39:49 -04:00
const { journal, charge, revision, produitVersion } = paire;
const texteEtat = relisible(serialiser(charge, { revision, produitVersion }), 'enregistrerAilleurs');
[ADD] storage: copy, rename, import, delete events; local settings § 8.7 operations reuse the event store's pairing and alignment instead of copying them. Renaming writes the complete pair under the new name — the store now writes a gesture with the pair it writes elsewhere — before deleting the old one, so no cut leaves zero pairs; an unchanged name writes nothing. Deleting moves state, .precedent, journal, lock and leftovers into a dated trash, matching names as the case-insensitive disk does. creerDossier creates only the root it is aimed at, so a vanished working folder never comes back empty. reglages_locaux.json keeps exactly the two § 8.5 settings; its deferred writer always keeps the last demand. Checked: 1314 node, 35 browser, 27 node-long tests; depot.js at 100 %. --- FR --- [ADD] stockage : copier, renommer, importer, supprimer ; réglages locaux Les opérations du § 8.7 reprennent l'appariement et l'alignement du dépôt au lieu de les recopier. Renommer écrit la paire complète sous le nouveau nom — le dépôt écrit désormais un geste avec la paire qu'il pose ailleurs — avant de supprimer l'ancienne : aucune coupure ne laisse zéro paire ; un nom inchangé n'écrit rien. Supprimer range état, .precedent, journal, verrou et restes dans une corbeille datée, en comparant les noms comme le disque insensible à la casse. creerDossier ne crée que la racine visée : un dossier de travail disparu ne renaît pas vide. reglages_locaux.json garde les deux réglages du § 8.5 ; son écrivain différé garde la dernière demande. Vérifié : 1314 node, 35 navigateur, 27 node-long ; depot.js à 100 %. Assisted-by: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01EUXSGcwCLSC69FWEdCtWSb
2026-10-06 23:17:05 -04:00
const precedent = geste === null ? null : etatDAvant(ouvert);
if (creerRacine) await fs.creerDossier(racine, '');
[ADD] storage: event store — list, create, open, save, recover An event is a state file and a journal paired by its internal identifier, never by name (§ 8.6): a hand copy of a state does not take the original's journal, and a renamed journal stays paired. Each gesture writes its journal line first, then the .precedent and the state (§ 8.2); recovery compares revisions, never file dates, so a cut leaves the journal one entry ahead at most. Opening never writes (§ 8.4); an unreadable state names its readable secours instead of opening an empty event, and a newer format stays read-only. Writing elsewhere after a removed medium aligns the pair first. Checked: 60 tests, 100 % branches of depot.js, 1257 node tests on HEAD. --- FR --- [ADD] stockage : dépôt d'événements — lister, créer, ouvrir, reprendre Un événement est un fichier d'état et un journal appariés par l'identifiant interne, jamais par le nom (§ 8.6) : un état copié à la main ne prend pas le journal de l'original, et un journal renommé reste apparié. Chaque geste écrit sa ligne de journal d'abord, puis le .precedent et l'état (§ 8.2) ; la reprise compare les révisions, jamais la date des fichiers : une coupure laisse le journal en avance d'une entrée au plus. Ouvrir n'écrit jamais (§ 8.4) ; un état illisible nomme ses secours lisibles au lieu d'ouvrir un événement vide, et un format plus récent reste en lecture seule. Écrire ailleurs après un support retiré aligne d'abord la paire. Vérifié : 60 épreuves, 100 % des branches de depot.js, 1257 épreuves node. Assisted-by: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01EUXSGcwCLSC69FWEdCtWSb
2026-10-06 20:39:49 -04:00
const base = await deriver(fs, racine, charge.evenement.nom, [...existantes]);
[ADD] storage: copy, rename, import, delete events; local settings § 8.7 operations reuse the event store's pairing and alignment instead of copying them. Renaming writes the complete pair under the new name — the store now writes a gesture with the pair it writes elsewhere — before deleting the old one, so no cut leaves zero pairs; an unchanged name writes nothing. Deleting moves state, .precedent, journal, lock and leftovers into a dated trash, matching names as the case-insensitive disk does. creerDossier creates only the root it is aimed at, so a vanished working folder never comes back empty. reglages_locaux.json keeps exactly the two § 8.5 settings; its deferred writer always keeps the last demand. Checked: 1314 node, 35 browser, 27 node-long tests; depot.js at 100 %. --- FR --- [ADD] stockage : copier, renommer, importer, supprimer ; réglages locaux Les opérations du § 8.7 reprennent l'appariement et l'alignement du dépôt au lieu de les recopier. Renommer écrit la paire complète sous le nouveau nom — le dépôt écrit désormais un geste avec la paire qu'il pose ailleurs — avant de supprimer l'ancienne : aucune coupure ne laisse zéro paire ; un nom inchangé n'écrit rien. Supprimer range état, .precedent, journal, verrou et restes dans une corbeille datée, en comparant les noms comme le disque insensible à la casse. creerDossier ne crée que la racine visée : un dossier de travail disparu ne renaît pas vide. reglages_locaux.json garde les deux réglages du § 8.5 ; son écrivain différé garde la dernière demande. Vérifié : 1314 node, 35 navigateur, 27 node-long ; depot.js à 100 %. Assisted-by: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01EUXSGcwCLSC69FWEdCtWSb
2026-10-06 23:17:05 -04:00
if (precedent !== null) await fs.ecrireAtomique(racine, base + SUFFIXES.precedent, precedent);
[ADD] storage: event store — list, create, open, save, recover An event is a state file and a journal paired by its internal identifier, never by name (§ 8.6): a hand copy of a state does not take the original's journal, and a renamed journal stays paired. Each gesture writes its journal line first, then the .precedent and the state (§ 8.2); recovery compares revisions, never file dates, so a cut leaves the journal one entry ahead at most. Opening never writes (§ 8.4); an unreadable state names its readable secours instead of opening an empty event, and a newer format stays read-only. Writing elsewhere after a removed medium aligns the pair first. Checked: 60 tests, 100 % branches of depot.js, 1257 node tests on HEAD. --- FR --- [ADD] stockage : dépôt d'événements — lister, créer, ouvrir, reprendre Un événement est un fichier d'état et un journal appariés par l'identifiant interne, jamais par le nom (§ 8.6) : un état copié à la main ne prend pas le journal de l'original, et un journal renommé reste apparié. Chaque geste écrit sa ligne de journal d'abord, puis le .precedent et l'état (§ 8.2) ; la reprise compare les révisions, jamais la date des fichiers : une coupure laisse le journal en avance d'une entrée au plus. Ouvrir n'écrit jamais (§ 8.4) ; un état illisible nomme ses secours lisibles au lieu d'ouvrir un événement vide, et un format plus récent reste en lecture seule. Écrire ailleurs après un support retiré aligne d'abord la paire. Vérifié : 60 épreuves, 100 % des branches de depot.js, 1257 épreuves node. Assisted-by: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01EUXSGcwCLSC69FWEdCtWSb
2026-10-06 20:39:49 -04:00
if (journal !== null) await fs.ecrireAtomique(racine, base + SUFFIXES.journal, paire.texteJournal);
const ailleurs = {
...ouvert,
racine,
base,
produitVersion,
texteEtat: null,
revisionEtat: null,
journal,
baseJournal: base,
chargeJournal: paire.chargeJournal,
journalAReecrire: false,
};
return ecrireEtat(apresEcriture(ailleurs, { charge, revision, journal }), texteEtat);
}