Skip to content

structures

structures

Les cinq formes qu'un projet Succès peut prendre, et leurs règles.

Un projet n'est pas toujours une liste. Selon la forme choisie, les mêmes tâches se pensent autrement :

  • flat — la liste simple, l'existant.
  • tree — l'arbre généalogique : l'objectif engendre ses livrables, chaque étage porte un nom (levelLabels).
  • mindmap — la carte neuronale : mêmes données que l'arbre, rendu radial ; c'est la forme de la phase d'idéation.
  • pipeline — le couloir d'étapes : chaque tâche traverse les stages du projet, la vue est un kanban.
  • network — le réseau de dépendances : des tâches reliées par des arêtes « débloque → », sans hiérarchie ; le graphe doit rester sans boucle, sinon « que puis-je faire maintenant ? » n'a plus de réponse.
  • cycle — la roue des routines : un « nouveau tour » décoche TOUTES les tâches du projet, cadencées ou non. La cadence est un rappel affiché, pas un filtre : une tâche exceptionnelle terminée dans un projet cycle repart elle aussi au tour suivant, et c'est voulu — un tour est un tour.

Ce module ne touche pas à la base : il définit et VALIDE. Les écritures vivent dans store.py / workspace.py, la surface HTTP dans routes.py.

Classes

Functions:

normalize_structure

normalize_structure(value: Any, *, kit_id: str = '') -> str

La forme demandée, ou une erreur qui dit lesquelles existent.

Source code in src/diapason/succes/structures.py
def normalize_structure(value: Any, *, kit_id: str = "") -> str:
    """La forme demandée, ou une erreur qui dit lesquelles existent."""
    raw = str(value or "flat").strip().lower()
    if raw not in PROJECT_STRUCTURES:
        raise SuccesError(
            "La structure du projet doit être l'une de : "
            + ", ".join(PROJECT_STRUCTURES)
            + "."
        )
    if kit_id.strip() and raw == "flat":
        # Un gabarit matérialise un arbre : la forme plate ne peut pas le
        # porter. Le choix explicite d'une autre forme reste respecté.
        return "tree"
    return raw

normalize_structure_config

normalize_structure_config(
    structure: str, raw: Any
) -> dict[str, Any]

La configuration propre à une forme, validée et rien de plus.

On ne garde QUE les clés que la forme connaît : une configuration qui survivrait à un changement de forme transporterait des réglages morts, et le client mobile les re-synchroniserait à jamais.

Source code in src/diapason/succes/structures.py
def normalize_structure_config(structure: str, raw: Any) -> dict[str, Any]:
    """La configuration propre à une forme, validée et rien de plus.

    On ne garde QUE les clés que la forme connaît : une configuration qui
    survivrait à un changement de forme transporterait des réglages morts,
    et le client mobile les re-synchroniserait à jamais.
    """
    data: Mapping[str, Any]
    if raw is None:
        data = {}
    elif isinstance(raw, Mapping):
        data = raw
    else:
        raise SuccesError("La configuration de structure doit être un objet.")

    if structure in TREE_FAMILY:
        labels = _clean_labels(
            data.get("levelLabels"),
            field="levelLabels",
            maximum=_MAX_LEVEL_LABELS,
            allow_empty=True,
        )
        config: dict[str, Any] = {}
        if labels:
            config["levelLabels"] = labels
        # La progression séquentielle : dans une même fratrie, une tâche
        # attend que celle qui la précède soit cochée. Absente par défaut —
        # une clé toujours présente ferait diverger l'instantané de contrat
        # et les projets déjà en base.
        if _drapeau(data.get("sequential"), field="sequential"):
            config["sequential"] = True
        return config

    if structure == "pipeline":
        stages = _clean_labels(
            data.get("stages"),
            field="stages",
            maximum=_MAX_STAGES,
            allow_empty=True,
        ) or list(DEFAULT_STAGES)
        if len(stages) < 2:
            raise SuccesError("Un pipeline demande au moins deux étapes.")
        return {"stages": stages}

    # flat, network, cycle : aucune configuration aujourd'hui. Le réseau vit
    # dans ses arêtes, le cycle dans la cadence des tâches.
    return {}

decode_structure_config

decode_structure_config(raw: Any) -> dict[str, Any]

Relit la configuration stockée sans jamais faire échouer une lecture.

Source code in src/diapason/succes/structures.py
def decode_structure_config(raw: Any) -> dict[str, Any]:
    """Relit la configuration stockée sans jamais faire échouer une lecture."""
    text = str(raw or "").strip()
    if not text:
        return {}
    try:
        data = json.loads(text)
    except ValueError:
        return {}
    return data if isinstance(data, dict) else {}

stages_of

stages_of(
    structure: str, config: Mapping[str, Any]
) -> list[str]

Les étapes d'un projet pipeline ; vide pour toute autre forme.

Source code in src/diapason/succes/structures.py
def stages_of(structure: str, config: Mapping[str, Any]) -> list[str]:
    """Les étapes d'un projet pipeline ; vide pour toute autre forme."""
    if structure != "pipeline":
        return []
    stages = config.get("stages")
    if isinstance(stages, list) and len(stages) >= 2:
        return [str(s) for s in stages]
    return list(DEFAULT_STAGES)

normalize_cadence

normalize_cadence(raw: Any) -> str

La cadence d'une tâche de cycle, encodée pour la colonne.

"" = pas de cadence. Sinon {"every": "day"|"week"|"month", "day": int?}day vaut 0..6 (lundi=0) pour la semaine, 1..28 pour le mois, et n'a pas de sens pour le jour.

Source code in src/diapason/succes/structures.py
def normalize_cadence(raw: Any) -> str:
    """La cadence d'une tâche de cycle, encodée pour la colonne.

    ``""`` = pas de cadence. Sinon ``{"every": "day"|"week"|"month",
    "day": int?}`` — ``day`` vaut 0..6 (lundi=0) pour la semaine, 1..28 pour
    le mois, et n'a pas de sens pour le jour.
    """
    if raw is None or raw == "" or raw == {}:
        return ""
    if not isinstance(raw, Mapping):
        raise SuccesError("La cadence doit être un objet {every, day?}.")
    every = str(raw.get("every") or "").strip().lower()
    if every not in ("day", "week", "month"):
        raise SuccesError("La cadence doit être day, week ou month.")
    out: dict[str, Any] = {"every": every}
    if every != "day":
        day = raw.get("day")
        if day is None:
            raise SuccesError("Cette cadence demande un jour (day).")
        try:
            day = int(day)
        except (TypeError, ValueError) as exc:
            raise SuccesError("Le jour de la cadence doit être un entier.") from exc
        if every == "week" and not 0 <= day <= 6:
            raise SuccesError("Le jour hebdomadaire va de 0 (lundi) à 6.")
        if every == "month" and not 1 <= day <= 28:
            raise SuccesError(
                "Le jour mensuel va de 1 à 28, pour exister tous les mois."
            )
        out["day"] = day
    return json.dumps(out, ensure_ascii=False, sort_keys=True)