L'état n'est pas un champ : il se lit dans horizon, completedDate, discardedDate et la présence de phases. Les phases commandent, sauf quand on a prononcé quelque chose. L'emprise d'un projet retombe sur son horizon faute de phases, et une saisie n'est jamais réécrite par un calcul : un projet qui perd sa dernière phase retrouve l'horizon qu'il avait déclaré. Clore un projet termine aussi toutes ses phases, seule opération du cycle qui écrase des données. phasesParMois devient entreesParMois : elle émet aussi les projets sans phase, à leur horizon. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
280 lines
15 KiB
Markdown
280 lines
15 KiB
Markdown
# Modèle de données
|
|
|
|
Tout le planning tient dans un seul fichier JSON, par défaut `data/projets.json`.
|
|
|
|
Le fichier est écrit indenté sur 2 espaces, avec les clés dans un ordre stable, pour rester lisible
|
|
dans un diff git et éditable à la main.
|
|
|
|
## Structure
|
|
|
|
```json
|
|
{
|
|
"version": 5,
|
|
"projects": [
|
|
{
|
|
"id": "site-web",
|
|
"name": "Refonte du site web",
|
|
"color": "#3b82f6",
|
|
"tags": ["client", "web"],
|
|
"collapsed": false,
|
|
"hidden": false,
|
|
"notes": "## Contexte\n\nPiloté par **Marie D.**\n\n- [x] cadrage validé\n- [ ] recette",
|
|
"horizon": { "start": "2026-08", "end": "2026-11" },
|
|
"baselineDate": "2026-06-12",
|
|
"phases": [
|
|
{
|
|
"id": "cadrage",
|
|
"name": "Cadrage",
|
|
"start": "2026-08-01",
|
|
"end": "2026-08-20",
|
|
"status": "done",
|
|
"milestone": 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" }
|
|
},
|
|
{
|
|
"id": "livraison",
|
|
"name": "Mise en ligne",
|
|
"start": "2026-11-02",
|
|
"end": "2026-11-02",
|
|
"status": "todo",
|
|
"milestone": true,
|
|
"notes": ""
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
## Racine
|
|
|
|
| Champ | Type | Description |
|
|
|---|---|---|
|
|
| `version` | entier | Version du format. Vaut `5`. 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.
|
|
|
|
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.
|
|
|
|
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
|
|
pas, si bien qu'un binaire antérieur ouvrirait sans broncher un fichier plus riche que lui et en
|
|
effacerait les champs inconnus à la première sauvegarde.
|
|
|
|
## Projet
|
|
|
|
| Champ | Type | Description |
|
|
|---|---|---|
|
|
| `id` | chaîne | Identifiant parlant, dérivé du nom (`Refonte du site web` → `site-web`). Unique dans le fichier. |
|
|
| `name` | chaîne | Nom affiché. Non vide. |
|
|
| `color` | chaîne | Couleur du couloir, en hexadécimal `#rrggbb`. |
|
|
| `tags` | tableau | Étiquettes libres servant à filtrer la frise. Trié, sans doublon, éventuellement vide. |
|
|
| `collapsed` | booléen | `true` si le couloir est plié. |
|
|
| `hidden` | booléen | `true` si le projet est masqué de la frise. Ses données restent intactes. |
|
|
| `notes` | chaîne | Texte libre en markdown, éventuellement vide. Ce que la frise ne sait pas dire : contexte, interlocuteurs, décisions. |
|
|
| `horizon` | objet ou absent | Emprise déclarée, en mois : `{ "start": "2027-03", "end": "2027-06" }`. Fin **incluse**. C'est l'ancrage d'un projet qui n'a pas encore de phase. |
|
|
| `completedDate` | chaîne ou absent | Date à laquelle le projet a été déclaré terminé. Absent tant qu'il ne l'a pas été. |
|
|
| `discardedDate` | chaîne ou absent | Date à laquelle le projet a été écarté. Absent tant qu'il ne l'a pas été. |
|
|
| `baselineDate` | chaîne ou absent | Date à laquelle la référence a été figée. Absent si elle ne l'a jamais été. |
|
|
| `phases` | tableau | Les phases, triées par date de début croissante. |
|
|
|
|
## Phase
|
|
|
|
| Champ | Type | Description |
|
|
|---|---|---|
|
|
| `id` | chaîne | Identifiant parlant, unique **au sein de son projet**. |
|
|
| `name` | chaîne | Nom affiché. Non vide. |
|
|
| `start` | chaîne | Date de début, `AAAA-MM-JJ`. |
|
|
| `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. |
|
|
| `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. |
|
|
|
|
## Règles de validation
|
|
|
|
Appliquées par `js/model.js` et couvertes par `tests/model.test.js`.
|
|
|
|
- `end` est postérieure ou égale à `start`.
|
|
- Un jalon (`milestone: true`) a nécessairement `start === end`. Cocher « jalon » sur une phase de
|
|
plusieurs jours ramène `end` sur `start`.
|
|
- Les dates suivent strictement `AAAA-MM-JJ` et doivent exister réellement dans le calendrier
|
|
(`2026-02-30` est rejetée).
|
|
- `id` de projet unique dans le fichier ; `id` de phase unique dans son projet.
|
|
- Un `id` est engendré à partir du nom : minuscules, accents retirés, tout ce qui n'est ni lettre ni
|
|
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`.
|
|
- `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,
|
|
est normalisé sans erreur (voir ci-dessous).
|
|
- `notes`, de projet comme de phase, est ramenée à `""` si ce n'est pas une chaîne. Le champ n'engage
|
|
aucun calcul, contrairement aux dates : bloquer le chargement d'un planning entier pour lui serait
|
|
disproportionné.
|
|
- `horizon` est facultatif, mais s'il est présent il porte deux mois `AAAA-MM` réels, et sa fin
|
|
n'est pas antérieure à son début. Il est vérifié aussi strictement que les dates d'une phase, et
|
|
non réparé en silence : il décide d'une position sur la frise, et un horizon avalé laisserait un
|
|
projet envisagé invisible sans qu'on sache pourquoi.
|
|
- `completedDate` et `discardedDate` suivent les mêmes règles que `baselineDate` : absentes, ou
|
|
dates `AAAA-MM-JJ` réelles.
|
|
|
|
## Tags
|
|
|
|
Une étiquette libre, posée sur un projet, sans liste fermée à tenir à jour : les tags disponibles
|
|
sont simplement ceux que portent les projets.
|
|
|
|
À l'enregistrement, `normaliserTags()` nettoie la liste :
|
|
|
|
- espaces de bord retirés, espaces internes réduits à un seul ;
|
|
- tags vides écartés ;
|
|
- doublons fusionnés **à la casse près** — `Client` et `client` sont le même tag, et c'est la
|
|
première graphie rencontrée qui est conservée ;
|
|
- tag tronqué au-delà de **24 caractères** (`MAX_LONGUEUR_TAG`) ;
|
|
- tri alphabétique, pour que ressaisir les mêmes tags dans un autre ordre ne produise aucun diff git.
|
|
|
|
Les **accents comptent** : `éditeur` et `editeur` restent deux tags distincts. C'est la différence
|
|
avec `fabriquerId()`, qui les retire parce qu'un identifiant doit tenir dans une URL — un tag, lui,
|
|
n'est jamais qu'affiché.
|
|
|
|
La **couleur** d'un tag n'est pas stockée : elle se déduit du nom par un hachage
|
|
(`teinteTag()` → une teinte HSL entre 0 et 359). Elle est donc stable d'une session à l'autre et
|
|
identique partout où le tag apparaît, sans rien avoir à gérer. Deux tags peuvent tomber sur des
|
|
teintes voisines : la couleur aide à repérer, elle ne porte pas d'information à elle seule — le nom
|
|
est toujours écrit à côté.
|
|
|
|
Le **filtre**, lui, n'est pas dans le fichier : c'est un état de vue, au même titre que la fenêtre
|
|
temporelle. Voir [decisions.md](decisions.md), section 16.
|
|
|
|
Un fichier invalide n'est jamais réparé en silence : le chargement échoue avec un message qui pointe
|
|
le projet et la phase fautifs, pour que le fichier puisse être corrigé à la main.
|
|
|
|
## Notes
|
|
|
|
Un projet et une phase portent chacun un champ `notes`, en **markdown**. Rien n'est stocké d'autre
|
|
que le texte source : le rendu est refait à l'affichage par `js/markdown.js` (analyse) et
|
|
`js/notes.js` (mise en DOM). Un fichier reste donc lisible et modifiable à la main, et une note
|
|
écrite hors de l'outil s'affiche sans conversion.
|
|
|
|
Le markdown reconnu est un **sous-ensemble**, choisi pour ce qu'on écrit dans une note de suivi :
|
|
|
|
| Syntaxe | Rendu |
|
|
|---|---|
|
|
| `# Titre` à `###### Titre` | Titres, décalés de deux niveaux sous celui du panneau |
|
|
| `**gras**`, `*italique*`, `_italique_` | Emphase |
|
|
| `` `code` `` et blocs ```` ``` ```` | Code littéral |
|
|
| `- item`, `* item`, `1. item` | Listes, imbricables à l'indentation |
|
|
| `- [ ] item`, `- [x] item` | Cases à cocher, cliquables dans le panneau |
|
|
| `[texte](https://…)` | Lien |
|
|
| `> cité` | Citation, réanalysée (elle peut contenir une liste) |
|
|
| `---` | Séparateur |
|
|
| `\*` | Le caractère littéral qui suit la contre-oblique |
|
|
|
|
Trois écarts assumés au markdown standard :
|
|
|
|
- **Un retour à la ligne en est un.** Le standard recolle les lignes d'un même paragraphe et exige
|
|
deux espaces en fin de ligne pour un vrai retour ; c'est un piège invisible dans un champ de
|
|
saisie, où l'on écrit souvent une petite liste à la volée.
|
|
- **Ce qui n'est pas reconnu reste du texte.** `**gras` sans fermeture s'affiche tel quel plutôt que
|
|
d'avaler la suite : une note à moitié écrite est l'état normal d'une note.
|
|
- **Ni tableaux, ni images, ni HTML brut.** Les deux premiers ne tiennent pas dans un volet latéral ;
|
|
le troisième rouvrirait la porte à l'injection que la construction du DOM nœud par nœud ferme.
|
|
|
|
Les **liens** ne sont suivis que si leur schéma est `http`, `https` ou `mailto` — liste blanche, et
|
|
non liste noire. Un planning s'échange, et rien ne garantit que la note affichée a été écrite par
|
|
celui qui la lit. Un lien refusé n'est pas effacé : son libellé redevient du texte.
|
|
|
|
Voir [decisions.md](decisions.md), section 25.
|
|
|
|
## Cycle de vie et horizon
|
|
|
|
L'état d'un projet — envisagé, engagé, terminé, écarté — **n'est pas un champ**. Il se lit dans les
|
|
données, et `etatProjet()` applique une règle unique : *les phases commandent l'état, sauf quand on a
|
|
prononcé quelque chose.*
|
|
|
|
| État | Comment il se lit | Ce qu'il veut dire |
|
|
|---|---|---|
|
|
| `considered` | ni `discardedDate`, ni `completedDate`, ni phase | Envisagé : un horizon pour seul ancrage |
|
|
| `engaged` | au moins une phase | Engagé : il a des dates fermes |
|
|
| `completed` | `completedDate` présente | Terminé, et quelqu'un l'a prononcé |
|
|
| `discarded` | `discardedDate` présente | Écarté : on a décidé de ne pas le faire |
|
|
|
|
Terminer et écarter s'actent, ils ne se devinent pas : un projet dont toutes les phases sont `done`
|
|
reste *engagé* tant que sa clôture n'a pas été prononcée — livré n'est pas clos. Ces deux actes
|
|
laissent une date, et ces deux dates sont l'unique trace de l'état dans le fichier. Un projet ne
|
|
porte jamais les deux à la fois ; si un fichier retouché à la main le fait, l'écartement l'emporte.
|
|
|
|
**Clore un projet passe toutes ses phases à `done`**, jalons compris. C'est la seule opération du
|
|
cycle qui écrase des données : le statut d'une phase `blocked` ou `todo` est perdu, et
|
|
`rouvrirProjet()` ne le rétablit pas — il n'a aucun moyen de savoir ce qu'il valait.
|
|
`clotureEcraseDesStatuts()` dit s'il reste quelque chose à écraser, ce qui permet à l'interface de
|
|
ne demander confirmation que dans ce cas.
|
|
|
|
**Réanimer un projet écarté** ne demande donc rien d'autre que d'effacer `discardedDate` : il
|
|
retrouve seul l'état que ses données commandent, engagé s'il a des phases, envisagé sinon. Son
|
|
horizon est resté celui d'avant et se signalera aussitôt comme dépassé, ce qui est voulu.
|
|
|
|
### L'horizon
|
|
|
|
Un projet envisagé n'est jamais hors du temps : il déclare son emprise en **mois**, fin incluse. Un
|
|
horizon d'un seul mois a `start === end`.
|
|
|
|
Le grain est le mois et jamais l'année, et l'horizon s'exprime comme un intervalle plutôt que par des
|
|
crans nommés — ni trimestre ni semestre à apprendre. La largeur de l'intervalle *est* l'incertitude,
|
|
et elle se resserre à mesure que le projet mûrit, jusqu'aux dates fermes de l'engagement.
|
|
|
|
L'horizon **survit à l'engagement** : il n'est ni effacé ni réécrit quand des phases arrivent. C'est
|
|
ce qui permet à un projet ayant perdu sa dernière phase de retrouver l'horizon qu'il avait déclaré,
|
|
plutôt que l'enveloppe de phases disparues — **on ne réécrit jamais une saisie par un calcul**.
|
|
|
|
Il se manœuvre au **mois** partout : les crans du panneau, le glisser sur la frise et le
|
|
redimensionnement de ses bords passent tous par `ajouterMois()` et n'atteignent jamais le jour.
|
|
`entreesParMois()` rattache d'ailleurs un projet envisagé au premier mois de son horizon, pour qu'il
|
|
paraisse dans la vue par mois au même titre qu'une phase qui démarre.
|
|
|
|
Un projet envisagé **sans** horizon est légal : c'est l'état de tous les projets sans phase hérités
|
|
d'un fichier en version 4. Il réclame une date, exactement comme un projet dont l'horizon est
|
|
dépassé — `projetADater()` confond volontairement les deux cas en un seul signal.
|
|
|
|
Voir [decisions.md](decisions.md), section 27.
|
|
|
|
## Dates : conventions
|
|
|
|
Les dates sont manipulées comme des **chaînes `AAAA-MM-JJ`**, pas comme des objets `Date`. Cela évite
|
|
tous les pièges de fuseau horaire : `new Date("2026-08-01")` est interprétée en UTC alors que
|
|
`new Date(2026, 7, 1)` l'est en heure locale, ce qui décale d'un jour selon le fuseau. Les
|
|
conversions n'ont lieu que dans les helpers de calcul de `model.js`.
|
|
|
|
`end` est **incluse** : une phase du `2026-08-01` au `2026-08-01` dure un jour.
|
|
|
|
Tout se compte en **jours calendaires**, week-ends et jours fériés compris.
|
|
|
|
Les numéros de semaine affichés dans l'en-tête suivent l'**ISO 8601** : la semaine
|
|
appartient à l'année où tombe son jeudi. `semaineISO()` renvoie donc le numéro *et*
|
|
l'année correspondante, qui peut différer de celle de la date.
|
|
|
|
## Bornes et dérive
|
|
|
|
- Les **bornes d'un projet** (`bornesProjet()`) vont de la plus petite `start` à la plus grande `end`
|
|
de ses phases. Un projet sans phase n'a pas de bornes.
|
|
- Son **emprise** (`empriseProjet()`) est celle de ses phases, ou à défaut celle de son horizon,
|
|
ramenée du premier jour de son premier mois au dernier jour de son dernier. C'est elle qui cadre la
|
|
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.
|
|
- **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.
|
|
- La **dérive** d'un projet est l'écart, en jours, entre la plus grande `end` actuelle et la plus
|
|
grande `end` de référence. Elle est affichée arrondie en semaines. Une phase créée après le figeage
|
|
n'a pas de `baseline` et ne compte pas dans la référence, mais compte dans les dates actuelles :
|
|
ajouter une phase en fin de projet crée donc bien une dérive, ce qui est le comportement voulu.
|