Repousser un projet en bloc, sauf ce dont on ne décide pas

Glisser la barre résumé décale toutes les phases du même nombre de jours,
écarts conservés : un projet engagé glisse souvent, et le faire phase par
phase les déformait. Restent en place ce qui est terminé — on ne réécrit
pas le passé — et les phases à date imposée, nouveau booléen `fixed` pour
ce qui est délégué ou tenu du dehors.

D'où le format en version 6 : sans l'incrément, un binaire antérieur
effacerait ces dates imposées à la première sauvegarde.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-10 18:07:31 +02:00
parent 55128fd0d4
commit 4a513f9a59
15 changed files with 788 additions and 74 deletions

View File

@@ -102,15 +102,62 @@ Gérer les jours ouvrés impose de faire passer chaque calcul de date par un hel
aussitôt la question des congés et des jours fériés — donc un calendrier à tenir à jour. Sur des
phases qui se comptent en semaines et en mois, l'écart est dans le bruit de l'estimation.
## 8. La barre cumulative est en lecture seule
## 8. La barre cumulative repousse le projet
La ligne d'un projet porte une barre unique segmentée par phase, qui donne sa forme d'ensemble. On
aurait pu la rendre glissable pour décaler tout le projet en bloc.
La ligne d'un projet porte une barre unique segmentée par phase, qui donne sa forme d'ensemble. Elle
se glisse, et le projet entier se décale avec elle.
Écarté pour une raison de lisibilité du geste : la même barre représenterait tantôt une phase, tantôt
un projet entier, et un glissement de quelques pixels déplacerait alors plusieurs mois de travail
d'un coup, sans que l'utilisateur voie ce qui bouge. Elle sert à regarder ; pour modifier, on agit
sur les phases.
**Ce n'était pas le cas au départ, et le motif du refus mérite d'être conservé** : la même barre
représenterait tantôt une phase, tantôt un projet entier, et un glissement de quelques pixels
déplacerait alors plusieurs mois de travail d'un coup, sans qu'on voie ce qui bouge. L'argument était
juste ; c'est le premier usage réel de l'outil qui l'a renversé. Un projet engagé glisse dans le
temps — c'est même la chose qui arrive le plus souvent —, et le faire glisser phase par phase oblige
à répéter le même geste cinq fois en priant pour que les écarts se conservent. Or ils ne se
conservent pas : chaque phase s'accroche à son propre lundi, et un projet ainsi déplacé se
déforme un peu à chaque passage.
L'objection d'origine n'a pas été balayée, elle a été traitée :
- **Les barres de phase suivent le résumé pendant le geste.** On voit exactement ce qu'on déplace
pendant qu'on le déplace, y compris plié — où les segments colorés de la barre suffisent à le
montrer. C'était le cœur du reproche, et il tombe.
- **Le delta est calculé une fois, sur la première phase qui bouge, puis appliqué tel quel
partout.** Les écarts entre phases sont conservés au jour près : le projet est repoussé, il n'est
pas replanifié. C'est précisément ce que le geste phase par phase ne savait pas faire.
- **La référence figée ne suit pas.** Repousser de trois semaines affiche aussitôt une dérive de
trois semaines. Le geste ne cache pas ce qu'il coûte, il l'inscrit.
### Ce qui est fait est fait
Une phase `done` ne bouge pas. Décaler un projet, c'est reconnaître qu'il prendra plus de temps que
prévu, et emporter les phases terminées réécrirait un passé qu'on a vécu — en effaçant du même coup
la dérive qu'on cherche à lire.
Le geste ne « déplace » donc pas toujours : sur un projet à moitié fait, la barre s'**étire**, son
bord gauche tient et le reste s'en va. C'est assumé, et c'est même ce qu'on veut voir. Le résumé se
repose alors depuis le modèle à chaque mouvement de souris plutôt que d'être translaté, faute de quoi
l'aperçu et le résultat divergeraient.
Le critère est le **statut**, jamais la position dans le calendrier : une phase terminée en avance
reste où elle est, une phase `blocked` ou `doing` déjà commencée se décale avec le reste — c'est bien
elle qui glisse. Un projet dont toutes les phases sont closes n'a rien à repousser : sa barre perd
son `data-role`, garde le curseur par défaut, et le dit dans son infobulle plutôt que d'offrir une
prise qui ne mènerait nulle part.
Rien n'empêche un décalage vers l'arrière de faire chevaucher une phase avec une phase terminée. Il
n'y a pas de dépendances entre phases dans ce modèle (section 2) et poser ici une butée en
inventerait une par la bande.
### Pas de poignées
La barre cumulative se déplace, elle ne se redimensionne pas. Étirer un projet supposerait de dilater
chaque phase au prorata, avec des arrondis qui déforment les durées courtes et un résultat qu'on ne
peut pas prévoir avant de l'avoir vu. C'est un autre besoin, et il attendra de se présenter.
Le même geste existe au clavier : sur la ligne d'un projet, `←` et `→` le repoussent d'un jour, là où
elles ne faisaient rien. `Maj` n'y ajoute rien, pour la raison qui précède.
### Ce qui n'a pas changé
**Elle est affichée que le projet soit plié ou déplié.** Elle ne l'était d'abord qu'en mode plié, ce
qui obligeait à replier pour savoir où en était le projet entier — donc à perdre la vue qu'on était
@@ -120,8 +167,10 @@ en train d'éditer.
plié, au motif qu'elle y était seule sur sa ligne. C'était une erreur : plier un couloir ne change
rien à ce que cette barre représente, et la voir changer d'aspect au pli laissait croire à deux
objets différents. Elle garde donc partout la même hauteur, plus mince qu'une barre de phase —
puisqu'elle voisine avec elles dès que le projet est déplié, et qu'elle ne se glisse pas. Le fantôme
de référence se cale dessous, d'où sa variante `--cumulatif`.
puisqu'elle voisine avec elles dès que le projet est déplié. Le fantôme de référence se cale dessous,
d'où sa variante `--cumulatif`. Étant mince, elle se repère mal une fois saisie : un anneau à sa
couleur la décolle du couloir le temps du geste, sans toucher à sa géométrie, ce qui décalerait ce
qu'on est en train de viser.
## 9. Accroche à la semaine
@@ -1052,3 +1101,78 @@ urgence en début de mandat.
**Un retour arrière déclaré**, d'engagé vers envisagé. On déplace les dates, cela suffit. Le seul
cas où il survient — retirer la dernière phase — se résout tout seul par la règle des bornes.
## 28. Une date qu'on ne décide pas
Toutes les phases d'un planning ne sont pas nôtres. Certaines sont **déléguées** — un prestataire les
tient, on n'a que le résultat —, d'autres portent une **contrainte extérieure** : une fenêtre de
maintenance chez l'hébergeur, une échéance réglementaire, une date de salon. Une phase reçoit donc un
booléen `fixed`, et le décalage d'un projet (section 8) ne l'emporte pas.
Sans elle, le geste ajouté en section 8 était activement nuisible : repousser un projet de trois
semaines repoussait aussi l'audit du commissaire aux comptes, ce qui est faux et ce que rien ne
signalait.
### Pourquoi ce n'est pas un cinquième statut
C'était la formulation spontanée — « ajouter un état ». Elle a été écartée, parce que les quatre
statuts existants disent tous **l'avancement** (section 6), et qu'une date imposée n'est pas une
étape du travail : une phase déléguée est tour à tour à venir, en cours, terminée, et elle reste
déléguée tout du long. En faire une cinquième valeur aurait créé une exclusion mutuelle fausse — on
ne pourrait plus dire d'une phase déléguée qu'elle est bloquée, ce qui est précisément la situation
qu'on veut voir venir de loin.
Le modèle avait déjà le patron : **`milestone` est un booléen à côté de `status`**, pas un statut. Un
jalon peut être `todo` ou `done` sans que cela pose la moindre question. `fixed` suit exactement le
même chemin, et les deux se combinent librement — un jalon à date imposée est le cas le plus courant
des deux.
**À ne pas confondre avec `blocked`**, dont il est le voisin immédiat dans un formulaire :
`blocked` dit *ça n'avance pas*, `fixed` dit *cette date ne m'appartient pas*. Une phase peut être
les deux, l'une ou l'autre, ou aucune. Le libellé retenu — « date imposée » plutôt que « fixe » —
sert d'abord à cela : il nomme la cause, là où « fixe » n'aurait nommé que l'effet, et six mois plus
tard c'est la cause qu'on cherche en relisant.
### Le verrou est souple
Une phase à date imposée **se glisse toujours seule**, à la souris comme aux flèches. Seul le
décalage de son projet l'épargne. « Imposée » veut donc dire *indépendante du projet*, et non
*immuable* : quand l'hébergeur déplace son créneau, on déplace la barre, sans rien avoir à décocher.
Le verrou dur avait été envisagé — barre non glissable, dates saisies au panneau — au motif qu'on
marque une phase pour la protéger d'un geste accidentel. Il ajoutait une friction permanente pour un
risque occasionnel, et il aurait fait diverger cette case de son modèle, `milestone`, qui n'empêche
rien non plus.
Corollaire : un projet dont toutes les phases sont terminées **ou** imposées n'offre aucune prise, et
sa barre cumulative le dit dans son infobulle. C'est le comportement déjà écrit pour les projets
entièrement clos ; il n'a pas eu à changer, seule la liste des phases emportées s'est resserrée.
### Ce que ça donne à l'œil
Deux **taquets** verticaux aux extrémités de la barre, comme les butées d'une pièce qui ne coulisse
plus ; un **cercle** pour un jalon, qui n'a pas d'extrémités où en poser. La marque se cumule avec
les quatre textures de statut au lieu de leur disputer la place — c'est bien une phase à venir, en
cours ou bloquée, qui se trouve en plus tenue par le dehors.
Elle se pose en `box-shadow` interne, donc sans toucher à la géométrie : la barre garde exactement la
largeur de ses dates. Sur une phase de deux jours, les taquets occupent presque toute la barre et la
texture de statut disparaît ; c'est accepté, une fenêtre de maintenance de deux jours se lit de toute
façon à son infobulle.
### Le format passe en version 6
Un champ ajouté n'y obligerait pas — la v5 se relit sans encombre. Ce qui y oblige, c'est le sens du
numéro : le validateur reconstruit chaque phase champ par champ et laisse tomber ce qu'il ne connaît
pas. Sans incrément, un binaire antérieur ouvrirait un fichier v6 sans broncher et en effacerait
toutes les dates imposées à la première sauvegarde — les phases concernées se remettant alors à
suivre le décalage de leur projet, en silence. C'est exactement le genre de perte que ce numéro
existe pour empêcher.
La migration ne devine rien : toute phase héritée reçoit `false`. Rien dans les données ne permettrait
d'inférer laquelle était déléguée, et un faux positif ici immobiliserait une phase sans qu'on
comprenne pourquoi.
Au passage, `donneesInitiales` dans `main.go` a été remis d'aplomb : le serveur écrivait encore
`version 4` pour un planning vide, deux versions en retard. Sans conséquence — le validateur accepte
un fichier plus ancien et le remonte — mais un fichier neuf se déclarait d'avant le cycle de vie.

View File

@@ -9,7 +9,7 @@ dans un diff git et éditable à la main.
```json
{
"version": 5,
"version": 6,
"projects": [
{
"id": "site-web",
@@ -29,6 +29,7 @@ dans un diff git et éditable à la main.
"end": "2026-08-20",
"status": "done",
"milestone": false,
"fixed": false,
"notes": "Ateliers avec les trois pôles.\n\nPérimètre arrêté le **8 juillet**.",
"baseline": { "start": "2026-07-25", "end": "2026-08-10" }
},
@@ -39,7 +40,8 @@ dans un diff git et éditable à la main.
"end": "2026-11-02",
"status": "todo",
"milestone": true,
"notes": ""
"fixed": true,
"notes": "Date annoncée au client."
}
]
}
@@ -51,18 +53,21 @@ dans un diff git et éditable à la main.
| Champ | Type | Description |
|---|---|---|
| `version` | entier | Version du format. Vaut `5`. Sert à détecter un fichier trop ancien au chargement. |
| `version` | entier | Version du format. Vaut `6`. Sert à détecter un fichier trop ancien au chargement. |
| `projects` | tableau | Les projets, dans l'ordre d'affichage des couloirs. |
Un fichier plus ancien se charge sans rien demander et est réécrit au format courant à la première
sauvegarde : une version `2` — celle d'avant les tags — voit ses projets recevoir une liste de tags
vide, une version `3` — celle d'avant les notes de projet — des notes vides, une version `4` — celle
d'avant le cycle de vie — des projets sans horizon ni date d'acte.
d'avant le cycle de vie — des projets sans horizon ni date d'acte, une version `5` — celle d'avant
les dates imposées — des phases à `fixed: false`.
Cette dernière migration ne demande rien et ne devine rien : un projet qui a des phases est *engagé*,
un projet qui n'en a pas est *envisagé sans horizon*, et il se signalera comme réclamant une date.
Aucun projet n'est déclaré terminé ni écarté au chargement — ces deux états s'actent, ils ne se
devinent pas, fût-ce d'un planning dont toutes les phases sont finies.
Aucune de ces migrations ne demande ni ne devine quoi que ce soit. Un projet qui a des phases est
*engagé*, un projet qui n'en a pas est *envisagé sans horizon*, et il se signalera comme réclamant une
date. Aucun projet n'est déclaré terminé ni écarté au chargement — ces deux états s'actent, ils ne se
devinent pas, fût-ce d'un planning dont toutes les phases sont finies. Et aucune phase n'est déclarée
à date imposée : rien dans les données ne dirait laquelle est déléguée, et un faux positif
immobiliserait une phase sans qu'on comprenne pourquoi.
Un fichier écrit par une version **plus récente** est en revanche refusé. C'est la raison d'être du
numéro : le validateur reconstruit chaque projet champ par champ et laisse tomber ce qu'il ne connaît
@@ -96,6 +101,7 @@ effacerait les champs inconnus à la première sauvegarde.
| `end` | chaîne | Date de fin **incluse**, `AAAA-MM-JJ`. |
| `status` | chaîne | `todo`, `doing`, `done` ou `blocked`. |
| `milestone` | booléen | `true` pour un jalon. |
| `fixed` | booléen | `true` si la date est imposée du dehors — phase déléguée, contrainte contractuelle ou réglementaire. Un décalage du projet ne l'emporte pas. |
| `notes` | chaîne | Texte libre en markdown, éventuellement vide. |
| `baseline` | objet ou absent | Dates de référence : `{ "start": …, "end": … }`. Absent tant que la référence n'a pas été figée. |
@@ -113,6 +119,10 @@ Appliquées par `js/model.js` et couvertes par `tests/model.test.js`.
chiffre remplacé par un tiret. En cas de collision, un suffixe numérique est ajouté (`cadrage-2`).
Un `id` ne change jamais si le nom est modifié ensuite — il identifie, il ne décrit pas.
- `status` fait partie des quatre valeurs autorisées ; toute autre valeur est ramenée à `todo`.
- `fixed` est orthogonal au reste : aucune combinaison n'est interdite. Une phase peut être à la fois
jalon et à date imposée — c'est même le cas le plus courant des deux —, ou `blocked` et imposée, ce
qui n'est pas une contradiction : `blocked` dit que ça n'avance pas, `fixed` que la date ne nous
appartient pas.
- `color` est un hexadécimal `#rrggbb` valide.
- `tags` est un tableau de chaînes. Le tableau lui-même et le type de ses éléments sont vérifiés —
un tag mal typé serait un tag qu'on croit poser et qui ne filtre rien. Leur *contenu*, en revanche,
@@ -270,6 +280,12 @@ l'année correspondante, qui peut différer de celle de la date.
fenêtre du planning au premier affichage — sans quoi un planning fait de projets encore tous
envisagés s'ouvrirait sur rien. Les projets écartés en sont exclus : ils ne s'affichent pas
d'ordinaire, et un projet abandonné il y a trois ans n'a pas à tirer la vue en arrière.
- **Repousser un projet** (`decalerProjet()`) ajoute le même nombre de jours aux `start` et `end` des
phases qu'il emporte — `phasesDecalables()` les désigne, `ancreDecalage()` donne la date sur
laquelle le geste s'accroche. Deux sortes en sont exclues, pour des raisons sans rapport : les
phases **terminées**, parce qu'on ne réécrit pas le passé, et les phases à **date imposée**, parce
qu'on n'en décide pas. Les `baseline` ne bougent pas non plus : le décalage se lit alors
intégralement dans la dérive.
- **Figer la référence** copie, pour chaque phase du projet, ses `start` et `end` courantes dans son
champ `baseline`, et inscrit la date du jour dans `baselineDate`. L'opération est rejouable : la
refiger après un arbitrage assumé repart d'une base propre.