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

@@ -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.