Skip to content

continuity

continuity

Recurring plans, quotes and year review for the native Succès domain.

This module translates the remaining Life OS business rules into Diapason's existing SQLite/operation-log architecture. It deliberately stays local; remote replication is a separate, explicitly configured phase.

Classes

SuccesContinuityStore

SuccesContinuityStore(db_path: str | Path | None = None)

Bases: SuccesWorkspaceStore

Succès store completed with recurrence and retrospective services.

Source code in src/diapason/succes/continuity.py
def __init__(self, db_path: str | Path | None = None) -> None:
    super().__init__(db_path)
    with self._connect() as conn:
        conn.executescript(_CONTINUITY_SCHEMA)
        conn.commit()
    self.materialize_continuity_archives()
Methods:
pastilles_planner
pastilles_planner(
    start_date: str, end_date: str
) -> dict[str, Any]

Ce que le petit calendrier doit montrer, jour par jour — sans rien créer.

Le calendrier du Planificateur peignait ses points depuis la liste des tâches déjà en base : une récurrence non encore matérialisée n'avait pas de point, puis en gagnait un après qu'on avait VISITÉ le jour (la matérialisation est à la demande) — « des points apparaissent où il n'y en avait pas » (13 septembre 2026). Ici, les gabarits actifs sont PROJETÉS sur la période, en lecture seule.

Une tâche « du jour D » : planifiée ce jour (quel que soit son état), ou sans date et terminée ce jour. La vue Jour applique la même règle : un point sans tâches, ou des tâches sans point, est un mensonge.

Source code in src/diapason/succes/continuity.py
def pastilles_planner(self, start_date: str, end_date: str) -> dict[str, Any]:
    """Ce que le petit calendrier doit montrer, jour par jour — sans rien créer.

    Le calendrier du Planificateur peignait ses points depuis la liste des
    tâches déjà en base : une récurrence non encore matérialisée n'avait
    pas de point, puis en gagnait un après qu'on avait VISITÉ le jour
    (la matérialisation est à la demande) — « des points apparaissent où
    il n'y en avait pas » (13 septembre 2026). Ici, les gabarits actifs
    sont PROJETÉS sur la période, en lecture seule.

    Une tâche « du jour D » : planifiée ce jour (quel que soit son état),
    ou sans date et terminée ce jour. La vue Jour applique la même règle :
    un point sans tâches, ou des tâches sans point, est un mensonge.
    """
    start_iso = _validate_iso_date(start_date, "La date de début")
    end_iso = _validate_iso_date(end_date, "La date de fin")
    if not start_iso or not end_iso or end_iso < start_iso:
        raise SuccesError("La période des pastilles n'est pas valide.")
    start, end = date.fromisoformat(start_iso), date.fromisoformat(end_iso)
    # Deux grilles de mois au plus : le calendrier n'en montre qu'une.
    if (end - start).days > 62:
        raise SuccesError("La période ne peut pas dépasser 62 jours.")
    days: dict[str, dict[str, int]] = {}

    def bucket(iso: str) -> dict[str, int]:
        return days.setdefault(iso, {"open": 0, "done": 0})

    with self._connect() as conn:
        for row in conn.execute(
            """SELECT scheduled_date AS d, done, COUNT(*) AS n
                 FROM succes_tasks
                WHERE deleted_at_ms IS NULL
                  AND scheduled_date >= ? AND scheduled_date <= ?
                GROUP BY scheduled_date, done""",
            (start_iso, end_iso),
        ):
            bucket(row["d"])["done" if row["done"] else "open"] += row["n"]
        # Les tâches sans date terminées ce jour-là appartiennent au jour
        # de leur achèvement — c'est là que la vue Jour les montre.
        for row in conn.execute(
            """SELECT completed_date AS d, COUNT(*) AS n
                 FROM succes_tasks
                WHERE deleted_at_ms IS NULL AND done = 1
                  AND scheduled_date = ''
                  AND completed_date >= ? AND completed_date <= ?
                GROUP BY completed_date""",
            (start_iso, end_iso),
        ):
            if row["d"]:
                bucket(row["d"])["done"] += row["n"]
        # Projection des gabarits : une occurrence pas encore matérialisée
        # compte comme ouverte. Une occurrence déjà en base est déjà
        # comptée ci-dessus — son id est déterministe, on l'écarte.
        templates = [
            item
            for item in self.list_templates(include_inactive=False)
            if item["templateKind"] == "task"
        ]
        cursor = start
        while cursor <= end:
            for template in templates:
                if not self._matches_template(template, cursor):
                    continue
                task_id = f"tpl_{template['id']}_{cursor.isoformat()}"
                if conn.execute(
                    "SELECT 1 FROM succes_tasks"
                    " WHERE id=? AND deleted_at_ms IS NULL",
                    (task_id,),
                ).fetchone():
                    continue
                bucket(cursor.isoformat())["open"] += 1
            cursor += timedelta(days=1)
    return {"startDate": start_iso, "endDate": end_iso, "days": days}