Skip to content

reseau

reseau

Le raisonnement du réseau côté serveur — statuts, voisinage, impact.

Miroir de frontend/src/features/succes/reseau.ts (chantier réseau, 18 septembre 2026). Jusque-là, aucun outil vocal ne connaissait les arêtes : « qu'est-ce que je peux faire dans AgriCulture ? » n'avait pas de réponse, et relier deux tâches passait par la souris seule — ce que le §82 refuse. Pur : (tâches, arêtes) en entrée, jamais la base. Une arête se lit « from débloque to » : to attend from.

Les deux modules doivent rendre la MÊME réponse aux mêmes questions ; tests/succes/test_reseau.py rejoue la fixture AgriCulture de reseau.test.ts pour le vérifier.

Functions:

cle_titre

cle_titre(titre: str) -> str

Un tri « à la française » sans dépendre de la locale du Mac.

localeCompare(..., 'fr') côté client place « âne » avant « zèbre » ; un tri par points de code met « â » (U+00E2) APRÈS « z ». On retire les accents et la casse pour comparer, sans toucher au titre affiché.

Source code in src/diapason/succes/reseau.py
def cle_titre(titre: str) -> str:
    """Un tri « à la française » sans dépendre de la locale du Mac.

    ``localeCompare(..., 'fr')`` côté client place « âne » avant « zèbre » ;
    un tri par points de code met « â » (U+00E2) APRÈS « z ». On retire les
    accents et la casse pour comparer, sans toucher au titre affiché.
    """
    sans_accents = "".join(
        c for c in unicodedata.normalize("NFKD", titre) if not unicodedata.combining(c)
    )
    return sans_accents.casefold()

construire_reseau

construire_reseau(
    taches: list[Mapping[str, Any]],
    aretes: list[Mapping[str, Any]],
) -> Reseau

Les arêtes dont un bout n'existe plus sont écartées — des fantômes.

Source code in src/diapason/succes/reseau.py
def construire_reseau(
    taches: list[Mapping[str, Any]], aretes: list[Mapping[str, Any]]
) -> Reseau:
    """Les arêtes dont un bout n'existe plus sont écartées — des fantômes."""
    reseau = Reseau(taches=[dict(t) for t in taches])
    for tache in reseau.taches:
        tid = str(tache["id"])
        reseau.par_id[tid] = tache
        reseau.amont[tid] = []
        reseau.aval[tid] = []
    for arete in aretes:
        de, vers = str(arete["fromTaskId"]), str(arete["toTaskId"])
        if de not in reseau.par_id or vers not in reseau.par_id:
            continue
        reseau.amont[vers].append(de)
        reseau.aval[de].append(vers)
    return reseau

chaine

chaine(
    reseau: Reseau, tid: str, sens: Sens
) -> list[tuple[str, int]]

Parcours en largeur, sans le départ, chaque tâche une seule fois.

« Une seule fois » est porteur : une boucle relayée par un pair (t1→t2 ET t2→t1, acceptée à la réception pour ne pas figer les appareils) tournerait sinon sans fin.

Source code in src/diapason/succes/reseau.py
def chaine(reseau: Reseau, tid: str, sens: Sens) -> list[tuple[str, int]]:
    """Parcours en largeur, sans le départ, chaque tâche une seule fois.

    « Une seule fois » est porteur : une boucle relayée par un pair (t1→t2
    ET t2→t1, acceptée à la réception pour ne pas figer les appareils)
    tournerait sinon sans fin.
    """
    voisins = reseau.amont if sens == "amont" else reseau.aval
    vus = {tid}
    resultat: list[tuple[str, int]] = []
    frontiere = [tid]
    profondeur = 0
    while frontiere:
        profondeur += 1
        suivante: list[str] = []
        for courant in frontiere:
            proches = sorted(
                (
                    reseau.par_id[v]
                    for v in voisins.get(courant, [])
                    if v in reseau.par_id
                ),
                key=lambda t: cle_titre(str(t["title"])),
            )
            for proche in proches:
                pid = str(proche["id"])
                if pid in vus:
                    continue
                vus.add(pid)
                resultat.append((pid, profondeur))
                suivante.append(pid)
        frontiere = suivante
    return resultat

voisines_ordonnees

voisines_ordonnees(
    reseau: Reseau, tid: str, sens: Sens
) -> list[str]

En amont, les ouvertes d'abord (les vraies raisons du blocage), puis par titre ; en aval, par titre.

Source code in src/diapason/succes/reseau.py
def voisines_ordonnees(reseau: Reseau, tid: str, sens: Sens) -> list[str]:
    """En amont, les ouvertes d'abord (les vraies raisons du blocage), puis
    par titre ; en aval, par titre."""
    ids = (reseau.amont if sens == "amont" else reseau.aval).get(tid, [])
    taches = [reseau.par_id[v] for v in ids if v in reseau.par_id]
    if sens == "amont":
        taches.sort(key=lambda t: (bool(t.get("done")), cle_titre(str(t["title"]))))
    else:
        taches.sort(key=lambda t: cle_titre(str(t["title"])))
    return [str(t["id"]) for t in taches]

ce_que_debloque

ce_que_debloque(reseau: Reseau, tid: str) -> list[str]

Les successeures qu'achever tid ouvre : celles dont toutes les autres attentes sont déjà faites. Le calcul ignore l'état de tid lui-même — lu avant la coche, il dit « terminer ceci ouvre… » ; lu sur l'état rechargé après, il dit ce qui vient de s'ouvrir (§100).

Source code in src/diapason/succes/reseau.py
def ce_que_debloque(reseau: Reseau, tid: str) -> list[str]:
    """Les successeures qu'achever ``tid`` ouvre : celles dont toutes les
    autres attentes sont déjà faites. Le calcul ignore l'état de ``tid``
    lui-même — lu avant la coche, il dit « terminer ceci ouvre… » ; lu sur
    l'état rechargé après, il dit ce qui vient de s'ouvrir (§100)."""
    resultat = []
    for sid in reseau.aval.get(tid, []):
        if _faite(reseau, sid):
            continue
        autres = [pid for pid in reseau.amont.get(sid, []) if pid != tid]
        if all(_faite(reseau, pid) for pid in autres):
            resultat.append(sid)
    return sorted(resultat, key=lambda s: cle_titre(str(reseau.par_id[s]["title"])))

impact

impact(reseau: Reseau, tid: str) -> int

Combien de tâches ouvertes attendent, de près ou de loin, tid.

Source code in src/diapason/succes/reseau.py
def impact(reseau: Reseau, tid: str) -> int:
    """Combien de tâches ouvertes attendent, de près ou de loin, ``tid``."""
    return sum(1 for mid, _ in chaine(reseau, tid, "aval") if not _faite(reseau, mid))

faisables

faisables(reseau: Reseau) -> list[str]

Les faisables maintenant, celles qui libèrent le plus d'abord, puis par titre : la première est la tâche à faire ce soir.

Source code in src/diapason/succes/reseau.py
def faisables(reseau: Reseau) -> list[str]:
    """Les faisables maintenant, celles qui libèrent le plus d'abord, puis
    par titre : la première est la tâche à faire ce soir."""
    ouvertes = [
        t for t in reseau.taches if statut_de(reseau, str(t["id"])) == "feasible"
    ]
    ouvertes.sort(
        key=lambda t: (-impact(reseau, str(t["id"])), cle_titre(str(t["title"])))
    )
    return [str(t["id"]) for t in ouvertes]