Second Brain documentation

ADR 0033 – Rattachement tâche-projet unique : lien dérivé d'un champ référence

Accepté (2026-10-16, mode autonome ; demande d'Aurélien, vague 1 du 2026-10-15)

← Toutes les décisions

ADR 0033 – Rattachement tâche-projet unique : lien dérivé d’un champ référence

Statut : Accepté (2026-10-16, mode autonome ; demande d’Aurélien, vague 1 du 2026-10-15)

Contexte

Une tâche pouvait être rattachée à un projet de deux façons :

  • le champ référence tache.projet (ADR 0005), saisi dans la fiche ;
  • le lien typé concerne-projet, ajouté depuis le panneau de contexte.

Les deux pouvaient diverger, et un projet listait alors ses tâches à deux endroits (« Éléments du projet » et « Tâche · Projet »). Aurélien veut une seule façon de faire : on garde le champ, le lien en est dérivé, affiché mais jamais saisi deux fois.

Décision

Format : derivedFrom sur un type de lien

name: concerne-projet
derivedFrom:
  - { aspect: tache, field: projet }

Pour un nœud source qui porte l’aspect tache, le lien concerne-projet est le champ tache.projet. Les autres nœuds (notes, factures, documents…) gardent le lien saisi. Au chargement, le chargeur vérifie :

  • l’aspect est connu ;
  • le champ est un champ référence de premier niveau (format: node-ref) ;
  • chaque aspect n’est cité qu’une fois.

Le moteur ne connaît que la clé derivedFrom. Aucun nom de domaine n’apparaît dans le code.

Comportement

  • Saisie : POST /api/nodes/{id}/links répond 400 type pour une source qui porte l’aspect (« se règle par le champ « Projet » de l’aspect Tâche »). Le panneau ne propose plus ce type pour un tel nœud.
  • Liens sortants : le lien dérivé vient en tête, en lecture seule. On y trouve kind = "field", ni id ni date, et field = "tache.projet". Une cible en corbeille est signalée, une cible purgée est omise.
  • Rétroliens : les sources du champ rejoignent le groupe du lien (« Éléments du projet »), après les liens saisis et une seule fois par source. Il n’y a plus de groupe « Tâche · Projet » séparé.
  • Vues : une colonne link:concerne-projet comprend les cibles dérivées, sans doublon, triées par titre.
  • Export : les liens dérivés n’ont pas de ligne node_link. L’export les donne déjà comme relation de champ (kind: field).

Reprise des liens saisis

Une même règle (LinkDerivation) sert à deux moments :

  • au démarrage, pour les données existantes, par un réconciliateur exécuté avant l’ouverture du port HTTP, idempotent, avec une transaction par source ;
  • quand on ajoute l’aspect à un nœud qui a déjà des liens saisis de ce type (PUT d’aspect, changement de type), dans la même transaction que l’ajout.

La règle :

  1. Le champ est résolu s’il contient l’identifiant d’un nœud existant hors corbeille.
  2. Champ résolu : il gagne et les liens saisis sont retirés. Un lien dont la cible diffère du champ est signalé dans le journal (WARN, avec la source et la cible) ; l’historique le garde.
  3. Champ non résolu (vide, pas un UUID, cible purgée ou en corbeille) : il reçoit la plus ancienne cible vivante des liens, par une écriture d’aspect validée (événement aspect.updated), puis les liens sont retirés.
  4. Aucune cible vivante : rien ne change, les liens restent et la source est signalée. Il n’y a aucune perte.

Chaque retrait écrit link.removed avec reason: derived et field. Une source qui échoue au démarrage, quelle qu’en soit la cause, est laissée telle quelle et signalée : le démarrage continue. Une source en corbeille ne peut pas être écrite ; elle est signalée à chaque démarrage.

Cohérence des définitions

  • Un champ ne porte qu’un seul type de lien : un second fichier qui cite le même aspect.field est refusé.
  • Au rechargement, un lien gardé dans sa version précédente est revalidé contre les définitions fusionnées. Si son champ a disparu ou n’est plus une référence, il est écarté et signalé.

La reprise passe par du Java plutôt que par une migration Flyway, car elle dépend des définitions de config/ : une migration SQL ne peut pas connaître tache.projet.

Alternatives écartées

  • Garder le lien et dériver le champ : le champ est déjà utilisé par les vues, l’expression where et la fiche. De plus, un lien many ne se ramène pas à un champ unique.
  • Restreindre source du lien pour exclure les tâches : les contraintes ne savent pas exprimer « sauf », et le rattachement d’une tâche n’apparaîtrait plus dans les liens.

Conséquences

  • Le champ référence étant unique, une tâche ne peut avoir qu’un projet. Une tâche qui avait plusieurs liens vers des projets garde la plus ancienne cible vivante dans le champ (ou son champ, s’il était déjà renseigné) ; les autres restent dans l’historique.
  • Lecture : les liens sortants montrent une ligne par champ dérivé porté. Les colonnes de vue relisent les identifiants comme la fiche (UUID en Java, cibles par clé primaire) et ne comptent une cible qu’une fois.
  • La même clé servira à d’autres paires champ/lien si config/ en déclare.