implement
faceb9d parent: 26c1e45 modified
.gitignore +10 -0 | @@ -1,2 +1,12 @@ | ||
| 1 | 1 | .claude/ |
| 2 | 2 | .DS_Store |
| 3 | + | |
| 4 | +# Python / uv | |
| 5 | +__pycache__/ | |
| 6 | +*.pyc | |
| 7 | +.venv/ | |
| 8 | +*.egg-info/ | |
| 9 | +.pytest_cache/ | |
| 10 | +.ruff_cache/ | |
| 11 | +dist/ | |
| 12 | +build/ | |
| @@ -1,2 +1,12 @@ | |||
| 1 | .claude/ | 1 | .claude/ |
| 2 | .DS_Store | 2 | .DS_Store |
| 3 | + | ||
| 4 | +# Python / uv | ||
| 5 | +__pycache__/ | ||
| 6 | +*.pyc | ||
| 7 | +.venv/ | ||
| 8 | +*.egg-info/ | ||
| 9 | +.pytest_cache/ | ||
| 10 | +.ruff_cache/ | ||
| 11 | +dist/ | ||
| 12 | +build/ | ||
added
packages/regine-agent/pyproject.toml +16 -0 | new file mode 100644 | ||
| @@ -0,0 +1,16 @@ | ||
| 1 | +[project] | |
| 2 | +name = "regine-agent" | |
| 3 | +version = "0.1.0" | |
| 4 | +description = "Façade agent IA de Régine — réservée, non implémentée (cf. specs/003-config-contexte-travail)." | |
| 5 | +requires-python = ">=3.11" | |
| 6 | +dependencies = ["regine-core"] | |
| 7 | + | |
| 8 | +[tool.uv.sources] | |
| 9 | +regine-core = { workspace = true } | |
| 10 | + | |
| 11 | +[build-system] | |
| 12 | +requires = ["hatchling"] | |
| 13 | +build-backend = "hatchling.build" | |
| 14 | + | |
| 15 | +[tool.hatch.build.targets.wheel] | |
| 16 | +packages = ["src/regine_agent"] | |
| new file mode 100644 | |||
| @@ -0,0 +1,16 @@ | |||
| 1 | +[project] | ||
| 2 | +name = "regine-agent" | ||
| 3 | +version = "0.1.0" | ||
| 4 | +description = "Façade agent IA de Régine — réservée, non implémentée (cf. specs/003-config-contexte-travail)." | ||
| 5 | +requires-python = ">=3.11" | ||
| 6 | +dependencies = ["regine-core"] | ||
| 7 | + | ||
| 8 | +[tool.uv.sources] | ||
| 9 | +regine-core = { workspace = true } | ||
| 10 | + | ||
| 11 | +[build-system] | ||
| 12 | +requires = ["hatchling"] | ||
| 13 | +build-backend = "hatchling.build" | ||
| 14 | + | ||
| 15 | +[tool.hatch.build.targets.wheel] | ||
| 16 | +packages = ["src/regine_agent"] | ||
added
packages/regine-agent/src/regine_agent/__init__.py +1 -0 | new file mode 100644 | ||
| @@ -0,0 +1 @@ | ||
| 1 | +"""Façade agent IA de Régine — paquet réservé, non implémenté (specs/003).""" | |
| new file mode 100644 | |||
| @@ -0,0 +1 @@ | |||
| 1 | +"""Façade agent IA de Régine — paquet réservé, non implémenté (specs/003).""" | ||
added
packages/regine-cli/pyproject.toml +16 -0 | new file mode 100644 | ||
| @@ -0,0 +1,16 @@ | ||
| 1 | +[project] | |
| 2 | +name = "regine-cli" | |
| 3 | +version = "0.1.0" | |
| 4 | +description = "Façade CLI de Régine — appelle regine-core, aucune logique métier propre." | |
| 5 | +requires-python = ">=3.11" | |
| 6 | +dependencies = ["regine-core"] | |
| 7 | + | |
| 8 | +[tool.uv.sources] | |
| 9 | +regine-core = { workspace = true } | |
| 10 | + | |
| 11 | +[build-system] | |
| 12 | +requires = ["hatchling"] | |
| 13 | +build-backend = "hatchling.build" | |
| 14 | + | |
| 15 | +[tool.hatch.build.targets.wheel] | |
| 16 | +packages = ["src/regine_cli"] | |
| new file mode 100644 | |||
| @@ -0,0 +1,16 @@ | |||
| 1 | +[project] | ||
| 2 | +name = "regine-cli" | ||
| 3 | +version = "0.1.0" | ||
| 4 | +description = "Façade CLI de Régine — appelle regine-core, aucune logique métier propre." | ||
| 5 | +requires-python = ">=3.11" | ||
| 6 | +dependencies = ["regine-core"] | ||
| 7 | + | ||
| 8 | +[tool.uv.sources] | ||
| 9 | +regine-core = { workspace = true } | ||
| 10 | + | ||
| 11 | +[build-system] | ||
| 12 | +requires = ["hatchling"] | ||
| 13 | +build-backend = "hatchling.build" | ||
| 14 | + | ||
| 15 | +[tool.hatch.build.targets.wheel] | ||
| 16 | +packages = ["src/regine_cli"] | ||
added
packages/regine-cli/src/regine_cli/__init__.py +1 -0 | new file mode 100644 | ||
| @@ -0,0 +1 @@ | ||
| 1 | +"""Façade CLI de Régine — appelle regine-core et formatte le résultat, sans logique métier.""" | |
| new file mode 100644 | |||
| @@ -0,0 +1 @@ | |||
| 1 | +"""Façade CLI de Régine — appelle regine-core et formatte le résultat, sans logique métier.""" | ||
added
packages/regine-core/pyproject.toml +16 -0 | new file mode 100644 | ||
| @@ -0,0 +1,16 @@ | ||
| 1 | +[project] | |
| 2 | +name = "regine-core" | |
| 3 | +version = "0.1.0" | |
| 4 | +description = "Bibliothèque centrale de Régine — toute la logique métier." | |
| 5 | +requires-python = ">=3.11" | |
| 6 | +dependencies = [] | |
| 7 | + | |
| 8 | +[build-system] | |
| 9 | +requires = ["hatchling"] | |
| 10 | +build-backend = "hatchling.build" | |
| 11 | + | |
| 12 | +[tool.hatch.build.targets.wheel] | |
| 13 | +packages = ["src/regine_core"] | |
| 14 | + | |
| 15 | +[tool.pytest.ini_options] | |
| 16 | +testpaths = ["tests"] | |
| new file mode 100644 | |||
| @@ -0,0 +1,16 @@ | |||
| 1 | +[project] | ||
| 2 | +name = "regine-core" | ||
| 3 | +version = "0.1.0" | ||
| 4 | +description = "Bibliothèque centrale de Régine — toute la logique métier." | ||
| 5 | +requires-python = ">=3.11" | ||
| 6 | +dependencies = [] | ||
| 7 | + | ||
| 8 | +[build-system] | ||
| 9 | +requires = ["hatchling"] | ||
| 10 | +build-backend = "hatchling.build" | ||
| 11 | + | ||
| 12 | +[tool.hatch.build.targets.wheel] | ||
| 13 | +packages = ["src/regine_core"] | ||
| 14 | + | ||
| 15 | +[tool.pytest.ini_options] | ||
| 16 | +testpaths = ["tests"] | ||
added
packages/regine-core/src/regine_core/__init__.py +1 -0 | new file mode 100644 | ||
| @@ -0,0 +1 @@ | ||
| 1 | +"""Bibliothèque centrale de Régine — toute la logique métier (cf. Principe VI).""" | |
| new file mode 100644 | |||
| @@ -0,0 +1 @@ | |||
| 1 | +"""Bibliothèque centrale de Régine — toute la logique métier (cf. Principe VI).""" | ||
added
packages/regine-core/src/regine_core/config/__init__.py +1 -0 | new file mode 100644 | ||
| @@ -0,0 +1 @@ | ||
| 1 | +"""Contexte de travail : chemins, base de contexte centralisée, catégories, boîtiers.""" | |
| new file mode 100644 | |||
| @@ -0,0 +1 @@ | |||
| 1 | +"""Contexte de travail : chemins, base de contexte centralisée, catégories, boîtiers.""" | ||
added
packages/regine-core/src/regine_core/config/categories.py +62 -0 | new file mode 100644 | ||
| @@ -0,0 +1,62 @@ | ||
| 1 | +"""Cache local des catégories thématiques déjà utilisées. | |
| 2 | + | |
| 3 | +Alimenté à la demande (pas un prérequis), pour permettre une suggestion instantanée | |
| 4 | +à l'étape de destination d'un import sans dépendre d'un accès au NAS | |
| 5 | +(cf. specs/004-categorisation-dossiers research.md § 1). | |
| 6 | +""" | |
| 7 | + | |
| 8 | +from __future__ import annotations | |
| 9 | + | |
| 10 | +import difflib | |
| 11 | +import sqlite3 | |
| 12 | +from datetime import UTC, datetime | |
| 13 | + | |
| 14 | +_CREATE_TABLE = """ | |
| 15 | +CREATE TABLE IF NOT EXISTS categories ( | |
| 16 | + id INTEGER PRIMARY KEY, | |
| 17 | + nom TEXT NOT NULL UNIQUE, | |
| 18 | + premiere_utilisation TEXT NOT NULL | |
| 19 | +) | |
| 20 | +""" | |
| 21 | + | |
| 22 | + | |
| 23 | +def _ensure_schema(conn: sqlite3.Connection) -> None: | |
| 24 | + conn.execute(_CREATE_TABLE) | |
| 25 | + conn.commit() | |
| 26 | + | |
| 27 | + | |
| 28 | +def list_known_categories(conn: sqlite3.Connection) -> list[str]: | |
| 29 | + """Retourne les catégories déjà utilisées, triées de la plus récente à la plus ancienne.""" | |
| 30 | + _ensure_schema(conn) | |
| 31 | + rows = conn.execute("SELECT nom FROM categories ORDER BY premiere_utilisation DESC").fetchall() | |
| 32 | + return [row[0] for row in rows] | |
| 33 | + | |
| 34 | + | |
| 35 | +def suggest_categories(conn: sqlite3.Connection, saisie: str) -> list[str]: | |
| 36 | + """Propose les catégories déjà connues proches de ``saisie`` (insensible à la casse). | |
| 37 | + | |
| 38 | + Aide à éviter un doublon orthographique (ex. "Mariage" vs "mariage") sans jamais | |
| 39 | + imposer la suggestion — l'appelant reste libre d'utiliser la saisie telle quelle. | |
| 40 | + """ | |
| 41 | + _ensure_schema(conn) | |
| 42 | + connues = list_known_categories(conn) | |
| 43 | + par_forme_normalisee = {nom.lower(): nom for nom in connues} | |
| 44 | + correspondances = difflib.get_close_matches( | |
| 45 | + saisie.lower(), par_forme_normalisee.keys(), n=5, cutoff=0.6 | |
| 46 | + ) | |
| 47 | + return [par_forme_normalisee[m] for m in correspondances] | |
| 48 | + | |
| 49 | + | |
| 50 | +def register_category_usage(conn: sqlite3.Connection, nom: str) -> None: | |
| 51 | + """Enregistre ``nom`` comme catégorie utilisée, si ce n'est pas déjà fait (idempotent). | |
| 52 | + | |
| 53 | + À n'appeler qu'après confirmation explicite de l'archivage (cf. Principe II de la | |
| 54 | + constitution) — jamais à la simple saisie. | |
| 55 | + """ | |
| 56 | + _ensure_schema(conn) | |
| 57 | + conn.execute( | |
| 58 | + "INSERT INTO categories (nom, premiere_utilisation) VALUES (?, ?) " | |
| 59 | + "ON CONFLICT(nom) DO NOTHING", | |
| 60 | + (nom, datetime.now(UTC).isoformat()), | |
| 61 | + ) | |
| 62 | + conn.commit() | |
| new file mode 100644 | |||
| @@ -0,0 +1,62 @@ | |||
| 1 | +"""Cache local des catégories thématiques déjà utilisées. | ||
| 2 | + | ||
| 3 | +Alimenté à la demande (pas un prérequis), pour permettre une suggestion instantanée | ||
| 4 | +à l'étape de destination d'un import sans dépendre d'un accès au NAS | ||
| 5 | +(cf. specs/004-categorisation-dossiers research.md § 1). | ||
| 6 | +""" | ||
| 7 | + | ||
| 8 | +from __future__ import annotations | ||
| 9 | + | ||
| 10 | +import difflib | ||
| 11 | +import sqlite3 | ||
| 12 | +from datetime import UTC, datetime | ||
| 13 | + | ||
| 14 | +_CREATE_TABLE = """ | ||
| 15 | +CREATE TABLE IF NOT EXISTS categories ( | ||
| 16 | + id INTEGER PRIMARY KEY, | ||
| 17 | + nom TEXT NOT NULL UNIQUE, | ||
| 18 | + premiere_utilisation TEXT NOT NULL | ||
| 19 | +) | ||
| 20 | +""" | ||
| 21 | + | ||
| 22 | + | ||
| 23 | +def _ensure_schema(conn: sqlite3.Connection) -> None: | ||
| 24 | + conn.execute(_CREATE_TABLE) | ||
| 25 | + conn.commit() | ||
| 26 | + | ||
| 27 | + | ||
| 28 | +def list_known_categories(conn: sqlite3.Connection) -> list[str]: | ||
| 29 | + """Retourne les catégories déjà utilisées, triées de la plus récente à la plus ancienne.""" | ||
| 30 | + _ensure_schema(conn) | ||
| 31 | + rows = conn.execute("SELECT nom FROM categories ORDER BY premiere_utilisation DESC").fetchall() | ||
| 32 | + return [row[0] for row in rows] | ||
| 33 | + | ||
| 34 | + | ||
| 35 | +def suggest_categories(conn: sqlite3.Connection, saisie: str) -> list[str]: | ||
| 36 | + """Propose les catégories déjà connues proches de ``saisie`` (insensible à la casse). | ||
| 37 | + | ||
| 38 | + Aide à éviter un doublon orthographique (ex. "Mariage" vs "mariage") sans jamais | ||
| 39 | + imposer la suggestion — l'appelant reste libre d'utiliser la saisie telle quelle. | ||
| 40 | + """ | ||
| 41 | + _ensure_schema(conn) | ||
| 42 | + connues = list_known_categories(conn) | ||
| 43 | + par_forme_normalisee = {nom.lower(): nom for nom in connues} | ||
| 44 | + correspondances = difflib.get_close_matches( | ||
| 45 | + saisie.lower(), par_forme_normalisee.keys(), n=5, cutoff=0.6 | ||
| 46 | + ) | ||
| 47 | + return [par_forme_normalisee[m] for m in correspondances] | ||
| 48 | + | ||
| 49 | + | ||
| 50 | +def register_category_usage(conn: sqlite3.Connection, nom: str) -> None: | ||
| 51 | + """Enregistre ``nom`` comme catégorie utilisée, si ce n'est pas déjà fait (idempotent). | ||
| 52 | + | ||
| 53 | + À n'appeler qu'après confirmation explicite de l'archivage (cf. Principe II de la | ||
| 54 | + constitution) — jamais à la simple saisie. | ||
| 55 | + """ | ||
| 56 | + _ensure_schema(conn) | ||
| 57 | + conn.execute( | ||
| 58 | + "INSERT INTO categories (nom, premiere_utilisation) VALUES (?, ?) " | ||
| 59 | + "ON CONFLICT(nom) DO NOTHING", | ||
| 60 | + (nom, datetime.now(UTC).isoformat()), | ||
| 61 | + ) | ||
| 62 | + conn.commit() | ||
added
packages/regine-core/src/regine_core/config/db.py +45 -0 | new file mode 100644 | ||
| @@ -0,0 +1,45 @@ | ||
| 1 | +"""Base de contexte centralisée : ouverture/initialisation SQLite. | |
| 2 | + | |
| 3 | +Fichier unique au niveau du contexte de travail (pas par dossier), réutilisé par | |
| 4 | +plusieurs modules pour leurs propres tables (``categories`` ici, ``boitiers`` dans | |
| 5 | +``regine_core.camera_profile``, cf. specs/002-profil-boitiers-optionnel). Suit les | |
| 6 | +mêmes conventions que le manifeste persistant par dossier (constitution | |
| 7 | +§ Workflow d'archivage) : ``PRAGMA application_id`` marque le format Régine, | |
| 8 | +``PRAGMA user_version`` porte le numéro de schéma, écriture transactionnelle. | |
| 9 | +""" | |
| 10 | + | |
| 11 | +from __future__ import annotations | |
| 12 | + | |
| 13 | +import sqlite3 | |
| 14 | +from pathlib import Path | |
| 15 | + | |
| 16 | +#: Identifiant Régine pour PRAGMA application_id (ASCII "REGI" packé sur 32 bits). | |
| 17 | +APPLICATION_ID = 0x52454749 | |
| 18 | + | |
| 19 | +#: Numéro de schéma courant. À incrémenter à chaque évolution du schéma partagé. | |
| 20 | +SCHEMA_VERSION = 1 | |
| 21 | + | |
| 22 | + | |
| 23 | +def open_context_db(path: Path) -> sqlite3.Connection: | |
| 24 | + """Ouvre la base de contexte centralisée, en l'initialisant si elle n'existe pas. | |
| 25 | + | |
| 26 | + Refuse explicitement d'ouvrir un fichier SQLite qui n'est pas déjà une base de | |
| 27 | + contexte Régine (``application_id`` différent), plutôt que de l'interpréter à | |
| 28 | + tort. | |
| 29 | + """ | |
| 30 | + path.parent.mkdir(parents=True, exist_ok=True) | |
| 31 | + conn = sqlite3.connect(path) | |
| 32 | + conn.execute("PRAGMA foreign_keys = ON") | |
| 33 | + | |
| 34 | + current_app_id = conn.execute("PRAGMA application_id").fetchone()[0] | |
| 35 | + if current_app_id == 0: | |
| 36 | + conn.execute(f"PRAGMA application_id = {APPLICATION_ID}") | |
| 37 | + conn.execute(f"PRAGMA user_version = {SCHEMA_VERSION}") | |
| 38 | + conn.commit() | |
| 39 | + elif current_app_id != APPLICATION_ID: | |
| 40 | + raise ValueError( | |
| 41 | + f"{path} n'est pas une base de contexte Régine " | |
| 42 | + f"(application_id={current_app_id:#x}, attendu {APPLICATION_ID:#x})" | |
| 43 | + ) | |
| 44 | + | |
| 45 | + return conn | |
| new file mode 100644 | |||
| @@ -0,0 +1,45 @@ | |||
| 1 | +"""Base de contexte centralisée : ouverture/initialisation SQLite. | ||
| 2 | + | ||
| 3 | +Fichier unique au niveau du contexte de travail (pas par dossier), réutilisé par | ||
| 4 | +plusieurs modules pour leurs propres tables (``categories`` ici, ``boitiers`` dans | ||
| 5 | +``regine_core.camera_profile``, cf. specs/002-profil-boitiers-optionnel). Suit les | ||
| 6 | +mêmes conventions que le manifeste persistant par dossier (constitution | ||
| 7 | +§ Workflow d'archivage) : ``PRAGMA application_id`` marque le format Régine, | ||
| 8 | +``PRAGMA user_version`` porte le numéro de schéma, écriture transactionnelle. | ||
| 9 | +""" | ||
| 10 | + | ||
| 11 | +from __future__ import annotations | ||
| 12 | + | ||
| 13 | +import sqlite3 | ||
| 14 | +from pathlib import Path | ||
| 15 | + | ||
| 16 | +#: Identifiant Régine pour PRAGMA application_id (ASCII "REGI" packé sur 32 bits). | ||
| 17 | +APPLICATION_ID = 0x52454749 | ||
| 18 | + | ||
| 19 | +#: Numéro de schéma courant. À incrémenter à chaque évolution du schéma partagé. | ||
| 20 | +SCHEMA_VERSION = 1 | ||
| 21 | + | ||
| 22 | + | ||
| 23 | +def open_context_db(path: Path) -> sqlite3.Connection: | ||
| 24 | + """Ouvre la base de contexte centralisée, en l'initialisant si elle n'existe pas. | ||
| 25 | + | ||
| 26 | + Refuse explicitement d'ouvrir un fichier SQLite qui n'est pas déjà une base de | ||
| 27 | + contexte Régine (``application_id`` différent), plutôt que de l'interpréter à | ||
| 28 | + tort. | ||
| 29 | + """ | ||
| 30 | + path.parent.mkdir(parents=True, exist_ok=True) | ||
| 31 | + conn = sqlite3.connect(path) | ||
| 32 | + conn.execute("PRAGMA foreign_keys = ON") | ||
| 33 | + | ||
| 34 | + current_app_id = conn.execute("PRAGMA application_id").fetchone()[0] | ||
| 35 | + if current_app_id == 0: | ||
| 36 | + conn.execute(f"PRAGMA application_id = {APPLICATION_ID}") | ||
| 37 | + conn.execute(f"PRAGMA user_version = {SCHEMA_VERSION}") | ||
| 38 | + conn.commit() | ||
| 39 | + elif current_app_id != APPLICATION_ID: | ||
| 40 | + raise ValueError( | ||
| 41 | + f"{path} n'est pas une base de contexte Régine " | ||
| 42 | + f"(application_id={current_app_id:#x}, attendu {APPLICATION_ID:#x})" | ||
| 43 | + ) | ||
| 44 | + | ||
| 45 | + return conn | ||
added
packages/regine-core/src/regine_core/dossier/__init__.py +4 -0 | new file mode 100644 | ||
| @@ -0,0 +1,4 @@ | ||
| 1 | +"""Structure de dossier, sélection et placement racine (année/catégorie). | |
| 2 | + | |
| 3 | +Cf. docs/interface-cli-gui-architecture.md et specs/004-categorisation-dossiers. | |
| 4 | +""" | |
| new file mode 100644 | |||
| @@ -0,0 +1,4 @@ | |||
| 1 | +"""Structure de dossier, sélection et placement racine (année/catégorie). | ||
| 2 | + | ||
| 3 | +Cf. docs/interface-cli-gui-architecture.md et specs/004-categorisation-dossiers. | ||
| 4 | +""" | ||
added
packages/regine-core/src/regine_core/dossier/root.py +70 -0 | new file mode 100644 | ||
| @@ -0,0 +1,70 @@ | ||
| 1 | +"""Résolution du répertoire racine (année par défaut, ou catégorie thématique). | |
| 2 | + | |
| 3 | +Cf. specs/004-categorisation-dossiers. | |
| 4 | +""" | |
| 5 | + | |
| 6 | +from __future__ import annotations | |
| 7 | + | |
| 8 | +from dataclasses import dataclass | |
| 9 | +from datetime import date as _date | |
| 10 | +from pathlib import Path | |
| 11 | +from typing import Literal | |
| 12 | + | |
| 13 | +RootType = Literal["annee", "categorie"] | |
| 14 | + | |
| 15 | + | |
| 16 | +@dataclass(frozen=True) | |
| 17 | +class RootLocation: | |
| 18 | + """Premier niveau sous lequel vit un dossier ou un dossier parent. | |
| 19 | + | |
| 20 | + À ne pas confondre avec la racine de sélection à l'intérieur d'un dossier | |
| 21 | + (cf. docs/lexique.md § Point de vigilance). | |
| 22 | + """ | |
| 23 | + | |
| 24 | + type: RootType | |
| 25 | + nom: str | |
| 26 | + chemin_archive: Path | |
| 27 | + chemin_local: Path | |
| 28 | + | |
| 29 | + | |
| 30 | +def determine_root( | |
| 31 | + date: _date, | |
| 32 | + categorie: str | None = None, | |
| 33 | + *, | |
| 34 | + archive_root: Path, | |
| 35 | + local_root: Path, | |
| 36 | +) -> RootLocation: | |
| 37 | + """Détermine le répertoire racine d'un dossier ou dossier parent (FR-002/FR-003). | |
| 38 | + | |
| 39 | + Par défaut (``categorie`` absent), le répertoire racine est l'année de ``date``. | |
| 40 | + Quand une catégorie est fournie, elle remplace entièrement le répertoire d'année | |
| 41 | + (FR-003) : un dossier n'a jamais les deux à la fois. | |
| 42 | + | |
| 43 | + Ne DOIT être appelée qu'une seule fois par dossier ou dossier parent (FR-006) : | |
| 44 | + un sous-dossier ajouté ensuite réutilise tel quel le ``RootLocation`` déjà résolu | |
| 45 | + de son parent, sans nouvel appel (cf. specs/004-categorisation-dossiers | |
| 46 | + User Story 3). | |
| 47 | + | |
| 48 | + Ce ``RootLocation`` est immuable après création : le seul moyen de le faire | |
| 49 | + changer est un déplacement physique du dossier, détecté par | |
| 50 | + ``regine_core.archive.reconciliation.comparer`` (specs/005-checkout-reconciliation) | |
| 51 | + comme un déplacement de contenu inchangé — jamais une écriture directe ici | |
| 52 | + (cf. specs/004-categorisation-dossiers User Story 4). | |
| 53 | + | |
| 54 | + ``archive_root``/``local_root`` sont passés explicitement plutôt que lus depuis | |
| 55 | + le contexte de travail (specs/003-config-contexte-travail, pas encore implémenté) | |
| 56 | + pour que ce module reste testable et livrable indépendamment. | |
| 57 | + """ | |
| 58 | + if categorie is not None: | |
| 59 | + nom = categorie | |
| 60 | + type_: RootType = "categorie" | |
| 61 | + else: | |
| 62 | + nom = str(date.year) | |
| 63 | + type_ = "annee" | |
| 64 | + | |
| 65 | + return RootLocation( | |
| 66 | + type=type_, | |
| 67 | + nom=nom, | |
| 68 | + chemin_archive=archive_root / nom, | |
| 69 | + chemin_local=local_root / nom, | |
| 70 | + ) | |
| new file mode 100644 | |||
| @@ -0,0 +1,70 @@ | |||
| 1 | +"""Résolution du répertoire racine (année par défaut, ou catégorie thématique). | ||
| 2 | + | ||
| 3 | +Cf. specs/004-categorisation-dossiers. | ||
| 4 | +""" | ||
| 5 | + | ||
| 6 | +from __future__ import annotations | ||
| 7 | + | ||
| 8 | +from dataclasses import dataclass | ||
| 9 | +from datetime import date as _date | ||
| 10 | +from pathlib import Path | ||
| 11 | +from typing import Literal | ||
| 12 | + | ||
| 13 | +RootType = Literal["annee", "categorie"] | ||
| 14 | + | ||
| 15 | + | ||
| 16 | +@dataclass(frozen=True) | ||
| 17 | +class RootLocation: | ||
| 18 | + """Premier niveau sous lequel vit un dossier ou un dossier parent. | ||
| 19 | + | ||
| 20 | + À ne pas confondre avec la racine de sélection à l'intérieur d'un dossier | ||
| 21 | + (cf. docs/lexique.md § Point de vigilance). | ||
| 22 | + """ | ||
| 23 | + | ||
| 24 | + type: RootType | ||
| 25 | + nom: str | ||
| 26 | + chemin_archive: Path | ||
| 27 | + chemin_local: Path | ||
| 28 | + | ||
| 29 | + | ||
| 30 | +def determine_root( | ||
| 31 | + date: _date, | ||
| 32 | + categorie: str | None = None, | ||
| 33 | + *, | ||
| 34 | + archive_root: Path, | ||
| 35 | + local_root: Path, | ||
| 36 | +) -> RootLocation: | ||
| 37 | + """Détermine le répertoire racine d'un dossier ou dossier parent (FR-002/FR-003). | ||
| 38 | + | ||
| 39 | + Par défaut (``categorie`` absent), le répertoire racine est l'année de ``date``. | ||
| 40 | + Quand une catégorie est fournie, elle remplace entièrement le répertoire d'année | ||
| 41 | + (FR-003) : un dossier n'a jamais les deux à la fois. | ||
| 42 | + | ||
| 43 | + Ne DOIT être appelée qu'une seule fois par dossier ou dossier parent (FR-006) : | ||
| 44 | + un sous-dossier ajouté ensuite réutilise tel quel le ``RootLocation`` déjà résolu | ||
| 45 | + de son parent, sans nouvel appel (cf. specs/004-categorisation-dossiers | ||
| 46 | + User Story 3). | ||
| 47 | + | ||
| 48 | + Ce ``RootLocation`` est immuable après création : le seul moyen de le faire | ||
| 49 | + changer est un déplacement physique du dossier, détecté par | ||
| 50 | + ``regine_core.archive.reconciliation.comparer`` (specs/005-checkout-reconciliation) | ||
| 51 | + comme un déplacement de contenu inchangé — jamais une écriture directe ici | ||
| 52 | + (cf. specs/004-categorisation-dossiers User Story 4). | ||
| 53 | + | ||
| 54 | + ``archive_root``/``local_root`` sont passés explicitement plutôt que lus depuis | ||
| 55 | + le contexte de travail (specs/003-config-contexte-travail, pas encore implémenté) | ||
| 56 | + pour que ce module reste testable et livrable indépendamment. | ||
| 57 | + """ | ||
| 58 | + if categorie is not None: | ||
| 59 | + nom = categorie | ||
| 60 | + type_: RootType = "categorie" | ||
| 61 | + else: | ||
| 62 | + nom = str(date.year) | ||
| 63 | + type_ = "annee" | ||
| 64 | + | ||
| 65 | + return RootLocation( | ||
| 66 | + type=type_, | ||
| 67 | + nom=nom, | ||
| 68 | + chemin_archive=archive_root / nom, | ||
| 69 | + chemin_local=local_root / nom, | ||
| 70 | + ) | ||
added
packages/regine-core/tests/contract/.gitkeep +0 -0 | new file mode 100644 |
| new file mode 100644 |
added
packages/regine-core/tests/integration/test_root_inheritance.py +27 -0 | new file mode 100644 | ||
| @@ -0,0 +1,27 @@ | ||
| 1 | +"""Test d'integration : heritage du RootLocation par un sous-dossier (T022).""" | |
| 2 | + | |
| 3 | +from datetime import date | |
| 4 | +from pathlib import Path | |
| 5 | + | |
| 6 | +from regine_core.dossier.root import determine_root | |
| 7 | + | |
| 8 | + | |
| 9 | +def test_subfolder_reuses_parent_root_location_without_recomputing(tmp_path: Path) -> None: | |
| 10 | + archive_root = tmp_path / "archive" | |
| 11 | + local_root = tmp_path / "local" | |
| 12 | + | |
| 13 | + parent_root = determine_root( | |
| 14 | + date(2026, 8, 1), | |
| 15 | + categorie="voyage", | |
| 16 | + archive_root=archive_root, | |
| 17 | + local_root=local_root, | |
| 18 | + ) | |
| 19 | + | |
| 20 | + # Le futur code d'import (specs/001-import-photos) réutilise directement le | |
| 21 | + # RootLocation du parent pour un sous-dossier, sans nouvel appel à | |
| 22 | + # determine_root (FR-006). | |
| 23 | + sous_dossier_root = parent_root | |
| 24 | + | |
| 25 | + assert sous_dossier_root.nom == "voyage" | |
| 26 | + assert sous_dossier_root.type == "categorie" | |
| 27 | + assert sous_dossier_root is parent_root | |
| new file mode 100644 | |||
| @@ -0,0 +1,27 @@ | |||
| 1 | +"""Test d'integration : heritage du RootLocation par un sous-dossier (T022).""" | ||
| 2 | + | ||
| 3 | +from datetime import date | ||
| 4 | +from pathlib import Path | ||
| 5 | + | ||
| 6 | +from regine_core.dossier.root import determine_root | ||
| 7 | + | ||
| 8 | + | ||
| 9 | +def test_subfolder_reuses_parent_root_location_without_recomputing(tmp_path: Path) -> None: | ||
| 10 | + archive_root = tmp_path / "archive" | ||
| 11 | + local_root = tmp_path / "local" | ||
| 12 | + | ||
| 13 | + parent_root = determine_root( | ||
| 14 | + date(2026, 8, 1), | ||
| 15 | + categorie="voyage", | ||
| 16 | + archive_root=archive_root, | ||
| 17 | + local_root=local_root, | ||
| 18 | + ) | ||
| 19 | + | ||
| 20 | + # Le futur code d'import (specs/001-import-photos) réutilise directement le | ||
| 21 | + # RootLocation du parent pour un sous-dossier, sans nouvel appel à | ||
| 22 | + # determine_root (FR-006). | ||
| 23 | + sous_dossier_root = parent_root | ||
| 24 | + | ||
| 25 | + assert sous_dossier_root.nom == "voyage" | ||
| 26 | + assert sous_dossier_root.type == "categorie" | ||
| 27 | + assert sous_dossier_root is parent_root | ||
added
packages/regine-core/tests/unit/test_categories_cache.py +49 -0 | new file mode 100644 | ||
| @@ -0,0 +1,49 @@ | ||
| 1 | +"""Tests du cache de catégories (T015).""" | |
| 2 | + | |
| 3 | +from pathlib import Path | |
| 4 | + | |
| 5 | +from regine_core.config.categories import ( | |
| 6 | + list_known_categories, | |
| 7 | + register_category_usage, | |
| 8 | + suggest_categories, | |
| 9 | +) | |
| 10 | +from regine_core.config.db import open_context_db | |
| 11 | + | |
| 12 | + | |
| 13 | +def test_empty_by_default(tmp_path: Path) -> None: | |
| 14 | + conn = open_context_db(tmp_path / "contexte.sqlite3") | |
| 15 | + try: | |
| 16 | + assert list_known_categories(conn) == [] | |
| 17 | + finally: | |
| 18 | + conn.close() | |
| 19 | + | |
| 20 | + | |
| 21 | +def test_suggest_finds_close_match_after_registration(tmp_path: Path) -> None: | |
| 22 | + conn = open_context_db(tmp_path / "contexte.sqlite3") | |
| 23 | + try: | |
| 24 | + register_category_usage(conn, "mariage") | |
| 25 | + | |
| 26 | + assert suggest_categories(conn, "Mariage") == ["mariage"] | |
| 27 | + finally: | |
| 28 | + conn.close() | |
| 29 | + | |
| 30 | + | |
| 31 | +def test_register_is_idempotent_no_silent_duplicate(tmp_path: Path) -> None: | |
| 32 | + conn = open_context_db(tmp_path / "contexte.sqlite3") | |
| 33 | + try: | |
| 34 | + register_category_usage(conn, "voyage") | |
| 35 | + register_category_usage(conn, "voyage") | |
| 36 | + | |
| 37 | + assert list_known_categories(conn) == ["voyage"] | |
| 38 | + finally: | |
| 39 | + conn.close() | |
| 40 | + | |
| 41 | + | |
| 42 | +def test_suggest_returns_empty_when_no_close_match(tmp_path: Path) -> None: | |
| 43 | + conn = open_context_db(tmp_path / "contexte.sqlite3") | |
| 44 | + try: | |
| 45 | + register_category_usage(conn, "voyage") | |
| 46 | + | |
| 47 | + assert suggest_categories(conn, "exposition") == [] | |
| 48 | + finally: | |
| 49 | + conn.close() | |
| new file mode 100644 | |||
| @@ -0,0 +1,49 @@ | |||
| 1 | +"""Tests du cache de catégories (T015).""" | ||
| 2 | + | ||
| 3 | +from pathlib import Path | ||
| 4 | + | ||
| 5 | +from regine_core.config.categories import ( | ||
| 6 | + list_known_categories, | ||
| 7 | + register_category_usage, | ||
| 8 | + suggest_categories, | ||
| 9 | +) | ||
| 10 | +from regine_core.config.db import open_context_db | ||
| 11 | + | ||
| 12 | + | ||
| 13 | +def test_empty_by_default(tmp_path: Path) -> None: | ||
| 14 | + conn = open_context_db(tmp_path / "contexte.sqlite3") | ||
| 15 | + try: | ||
| 16 | + assert list_known_categories(conn) == [] | ||
| 17 | + finally: | ||
| 18 | + conn.close() | ||
| 19 | + | ||
| 20 | + | ||
| 21 | +def test_suggest_finds_close_match_after_registration(tmp_path: Path) -> None: | ||
| 22 | + conn = open_context_db(tmp_path / "contexte.sqlite3") | ||
| 23 | + try: | ||
| 24 | + register_category_usage(conn, "mariage") | ||
| 25 | + | ||
| 26 | + assert suggest_categories(conn, "Mariage") == ["mariage"] | ||
| 27 | + finally: | ||
| 28 | + conn.close() | ||
| 29 | + | ||
| 30 | + | ||
| 31 | +def test_register_is_idempotent_no_silent_duplicate(tmp_path: Path) -> None: | ||
| 32 | + conn = open_context_db(tmp_path / "contexte.sqlite3") | ||
| 33 | + try: | ||
| 34 | + register_category_usage(conn, "voyage") | ||
| 35 | + register_category_usage(conn, "voyage") | ||
| 36 | + | ||
| 37 | + assert list_known_categories(conn) == ["voyage"] | ||
| 38 | + finally: | ||
| 39 | + conn.close() | ||
| 40 | + | ||
| 41 | + | ||
| 42 | +def test_suggest_returns_empty_when_no_close_match(tmp_path: Path) -> None: | ||
| 43 | + conn = open_context_db(tmp_path / "contexte.sqlite3") | ||
| 44 | + try: | ||
| 45 | + register_category_usage(conn, "voyage") | ||
| 46 | + | ||
| 47 | + assert suggest_categories(conn, "exposition") == [] | ||
| 48 | + finally: | ||
| 49 | + conn.close() | ||
added
packages/regine-core/tests/unit/test_db_bootstrap.py +47 -0 | new file mode 100644 | ||
| @@ -0,0 +1,47 @@ | ||
| 1 | +"""Tests du bootstrap de la base de contexte centralisée (T010).""" | |
| 2 | + | |
| 3 | +from pathlib import Path | |
| 4 | + | |
| 5 | +import pytest | |
| 6 | +from regine_core.config.db import APPLICATION_ID, SCHEMA_VERSION, open_context_db | |
| 7 | + | |
| 8 | + | |
| 9 | +def test_bootstrap_sets_application_id_and_version(tmp_path: Path) -> None: | |
| 10 | + db_path = tmp_path / "contexte.sqlite3" | |
| 11 | + | |
| 12 | + conn = open_context_db(db_path) | |
| 13 | + try: | |
| 14 | + assert conn.execute("PRAGMA application_id").fetchone()[0] == APPLICATION_ID | |
| 15 | + assert conn.execute("PRAGMA user_version").fetchone()[0] == SCHEMA_VERSION | |
| 16 | + finally: | |
| 17 | + conn.close() | |
| 18 | + | |
| 19 | + | |
| 20 | +def test_reopen_is_idempotent_and_preserves_data(tmp_path: Path) -> None: | |
| 21 | + db_path = tmp_path / "contexte.sqlite3" | |
| 22 | + | |
| 23 | + conn = open_context_db(db_path) | |
| 24 | + conn.execute("CREATE TABLE t (v TEXT)") | |
| 25 | + conn.execute("INSERT INTO t VALUES ('x')") | |
| 26 | + conn.commit() | |
| 27 | + conn.close() | |
| 28 | + | |
| 29 | + conn2 = open_context_db(db_path) | |
| 30 | + try: | |
| 31 | + assert conn2.execute("SELECT v FROM t").fetchall() == [("x",)] | |
| 32 | + assert conn2.execute("PRAGMA application_id").fetchone()[0] == APPLICATION_ID | |
| 33 | + finally: | |
| 34 | + conn2.close() | |
| 35 | + | |
| 36 | + | |
| 37 | +def test_rejects_file_with_foreign_application_id(tmp_path: Path) -> None: | |
| 38 | + import sqlite3 | |
| 39 | + | |
| 40 | + db_path = tmp_path / "autre.sqlite3" | |
| 41 | + conn = sqlite3.connect(db_path) | |
| 42 | + conn.execute("PRAGMA application_id = 12345") | |
| 43 | + conn.commit() | |
| 44 | + conn.close() | |
| 45 | + | |
| 46 | + with pytest.raises(ValueError): | |
| 47 | + open_context_db(db_path) | |
| new file mode 100644 | |||
| @@ -0,0 +1,47 @@ | |||
| 1 | +"""Tests du bootstrap de la base de contexte centralisée (T010).""" | ||
| 2 | + | ||
| 3 | +from pathlib import Path | ||
| 4 | + | ||
| 5 | +import pytest | ||
| 6 | +from regine_core.config.db import APPLICATION_ID, SCHEMA_VERSION, open_context_db | ||
| 7 | + | ||
| 8 | + | ||
| 9 | +def test_bootstrap_sets_application_id_and_version(tmp_path: Path) -> None: | ||
| 10 | + db_path = tmp_path / "contexte.sqlite3" | ||
| 11 | + | ||
| 12 | + conn = open_context_db(db_path) | ||
| 13 | + try: | ||
| 14 | + assert conn.execute("PRAGMA application_id").fetchone()[0] == APPLICATION_ID | ||
| 15 | + assert conn.execute("PRAGMA user_version").fetchone()[0] == SCHEMA_VERSION | ||
| 16 | + finally: | ||
| 17 | + conn.close() | ||
| 18 | + | ||
| 19 | + | ||
| 20 | +def test_reopen_is_idempotent_and_preserves_data(tmp_path: Path) -> None: | ||
| 21 | + db_path = tmp_path / "contexte.sqlite3" | ||
| 22 | + | ||
| 23 | + conn = open_context_db(db_path) | ||
| 24 | + conn.execute("CREATE TABLE t (v TEXT)") | ||
| 25 | + conn.execute("INSERT INTO t VALUES ('x')") | ||
| 26 | + conn.commit() | ||
| 27 | + conn.close() | ||
| 28 | + | ||
| 29 | + conn2 = open_context_db(db_path) | ||
| 30 | + try: | ||
| 31 | + assert conn2.execute("SELECT v FROM t").fetchall() == [("x",)] | ||
| 32 | + assert conn2.execute("PRAGMA application_id").fetchone()[0] == APPLICATION_ID | ||
| 33 | + finally: | ||
| 34 | + conn2.close() | ||
| 35 | + | ||
| 36 | + | ||
| 37 | +def test_rejects_file_with_foreign_application_id(tmp_path: Path) -> None: | ||
| 38 | + import sqlite3 | ||
| 39 | + | ||
| 40 | + db_path = tmp_path / "autre.sqlite3" | ||
| 41 | + conn = sqlite3.connect(db_path) | ||
| 42 | + conn.execute("PRAGMA application_id = 12345") | ||
| 43 | + conn.commit() | ||
| 44 | + conn.close() | ||
| 45 | + | ||
| 46 | + with pytest.raises(ValueError): | ||
| 47 | + open_context_db(db_path) | ||
added
packages/regine-core/tests/unit/test_root_resolution.py +54 -0 | new file mode 100644 | ||
| @@ -0,0 +1,54 @@ | ||
| 1 | +"""Tests de resolution du repertoire racine (T011, T014, T021).""" | |
| 2 | + | |
| 3 | +from datetime import date | |
| 4 | +from pathlib import Path | |
| 5 | + | |
| 6 | +from regine_core.dossier.root import determine_root | |
| 7 | + | |
| 8 | + | |
| 9 | +def test_default_root_is_year(tmp_path: Path) -> None: | |
| 10 | + archive_root = tmp_path / "archive" | |
| 11 | + local_root = tmp_path / "local" | |
| 12 | + | |
| 13 | + root = determine_root(date(2026, 8, 15), archive_root=archive_root, local_root=local_root) | |
| 14 | + | |
| 15 | + assert root.type == "annee" | |
| 16 | + assert root.nom == "2026" | |
| 17 | + assert root.chemin_archive == archive_root / "2026" | |
| 18 | + assert root.chemin_local == local_root / "2026" | |
| 19 | + assert root.chemin_archive.name == root.chemin_local.name == root.nom | |
| 20 | + | |
| 21 | + | |
| 22 | +def test_category_root_bypasses_year(tmp_path: Path) -> None: | |
| 23 | + archive_root = tmp_path / "archive" | |
| 24 | + local_root = tmp_path / "local" | |
| 25 | + | |
| 26 | + root = determine_root( | |
| 27 | + date(2026, 6, 20), | |
| 28 | + categorie="mariage", | |
| 29 | + archive_root=archive_root, | |
| 30 | + local_root=local_root, | |
| 31 | + ) | |
| 32 | + | |
| 33 | + assert root.type == "categorie" | |
| 34 | + assert root.nom == "mariage" | |
| 35 | + assert "2026" not in root.chemin_archive.parts | |
| 36 | + assert root.chemin_archive == archive_root / "mariage" | |
| 37 | + assert root.chemin_local == local_root / "mariage" | |
| 38 | + | |
| 39 | + | |
| 40 | +def test_categorie_is_a_single_value_never_merged(tmp_path: Path) -> None: | |
| 41 | + """FR-005 : un dossier n'a jamais plus d'une categorie a la fois.""" | |
| 42 | + archive_root = tmp_path / "archive" | |
| 43 | + local_root = tmp_path / "local" | |
| 44 | + | |
| 45 | + root_mariage = determine_root( | |
| 46 | + date(2026, 6, 20), categorie="mariage", archive_root=archive_root, local_root=local_root | |
| 47 | + ) | |
| 48 | + root_voyage = determine_root( | |
| 49 | + date(2026, 6, 20), categorie="voyage", archive_root=archive_root, local_root=local_root | |
| 50 | + ) | |
| 51 | + | |
| 52 | + assert root_mariage.nom == "mariage" | |
| 53 | + assert root_voyage.nom == "voyage" | |
| 54 | + assert root_mariage.nom != root_voyage.nom | |
| new file mode 100644 | |||
| @@ -0,0 +1,54 @@ | |||
| 1 | +"""Tests de resolution du repertoire racine (T011, T014, T021).""" | ||
| 2 | + | ||
| 3 | +from datetime import date | ||
| 4 | +from pathlib import Path | ||
| 5 | + | ||
| 6 | +from regine_core.dossier.root import determine_root | ||
| 7 | + | ||
| 8 | + | ||
| 9 | +def test_default_root_is_year(tmp_path: Path) -> None: | ||
| 10 | + archive_root = tmp_path / "archive" | ||
| 11 | + local_root = tmp_path / "local" | ||
| 12 | + | ||
| 13 | + root = determine_root(date(2026, 8, 15), archive_root=archive_root, local_root=local_root) | ||
| 14 | + | ||
| 15 | + assert root.type == "annee" | ||
| 16 | + assert root.nom == "2026" | ||
| 17 | + assert root.chemin_archive == archive_root / "2026" | ||
| 18 | + assert root.chemin_local == local_root / "2026" | ||
| 19 | + assert root.chemin_archive.name == root.chemin_local.name == root.nom | ||
| 20 | + | ||
| 21 | + | ||
| 22 | +def test_category_root_bypasses_year(tmp_path: Path) -> None: | ||
| 23 | + archive_root = tmp_path / "archive" | ||
| 24 | + local_root = tmp_path / "local" | ||
| 25 | + | ||
| 26 | + root = determine_root( | ||
| 27 | + date(2026, 6, 20), | ||
| 28 | + categorie="mariage", | ||
| 29 | + archive_root=archive_root, | ||
| 30 | + local_root=local_root, | ||
| 31 | + ) | ||
| 32 | + | ||
| 33 | + assert root.type == "categorie" | ||
| 34 | + assert root.nom == "mariage" | ||
| 35 | + assert "2026" not in root.chemin_archive.parts | ||
| 36 | + assert root.chemin_archive == archive_root / "mariage" | ||
| 37 | + assert root.chemin_local == local_root / "mariage" | ||
| 38 | + | ||
| 39 | + | ||
| 40 | +def test_categorie_is_a_single_value_never_merged(tmp_path: Path) -> None: | ||
| 41 | + """FR-005 : un dossier n'a jamais plus d'une categorie a la fois.""" | ||
| 42 | + archive_root = tmp_path / "archive" | ||
| 43 | + local_root = tmp_path / "local" | ||
| 44 | + | ||
| 45 | + root_mariage = determine_root( | ||
| 46 | + date(2026, 6, 20), categorie="mariage", archive_root=archive_root, local_root=local_root | ||
| 47 | + ) | ||
| 48 | + root_voyage = determine_root( | ||
| 49 | + date(2026, 6, 20), categorie="voyage", archive_root=archive_root, local_root=local_root | ||
| 50 | + ) | ||
| 51 | + | ||
| 52 | + assert root_mariage.nom == "mariage" | ||
| 53 | + assert root_voyage.nom == "voyage" | ||
| 54 | + assert root_mariage.nom != root_voyage.nom | ||
added
packages/regine-gui/pyproject.toml +16 -0 | new file mode 100644 | ||
| @@ -0,0 +1,16 @@ | ||
| 1 | +[project] | |
| 2 | +name = "regine-gui" | |
| 3 | +version = "0.1.0" | |
| 4 | +description = "Façade GUI de Régine — réservée, non implémentée (cf. specs/003-config-contexte-travail)." | |
| 5 | +requires-python = ">=3.11" | |
| 6 | +dependencies = ["regine-core"] | |
| 7 | + | |
| 8 | +[tool.uv.sources] | |
| 9 | +regine-core = { workspace = true } | |
| 10 | + | |
| 11 | +[build-system] | |
| 12 | +requires = ["hatchling"] | |
| 13 | +build-backend = "hatchling.build" | |
| 14 | + | |
| 15 | +[tool.hatch.build.targets.wheel] | |
| 16 | +packages = ["src/regine_gui"] | |
| new file mode 100644 | |||
| @@ -0,0 +1,16 @@ | |||
| 1 | +[project] | ||
| 2 | +name = "regine-gui" | ||
| 3 | +version = "0.1.0" | ||
| 4 | +description = "Façade GUI de Régine — réservée, non implémentée (cf. specs/003-config-contexte-travail)." | ||
| 5 | +requires-python = ">=3.11" | ||
| 6 | +dependencies = ["regine-core"] | ||
| 7 | + | ||
| 8 | +[tool.uv.sources] | ||
| 9 | +regine-core = { workspace = true } | ||
| 10 | + | ||
| 11 | +[build-system] | ||
| 12 | +requires = ["hatchling"] | ||
| 13 | +build-backend = "hatchling.build" | ||
| 14 | + | ||
| 15 | +[tool.hatch.build.targets.wheel] | ||
| 16 | +packages = ["src/regine_gui"] | ||
added
packages/regine-gui/src/regine_gui/__init__.py +1 -0 | new file mode 100644 | ||
| @@ -0,0 +1 @@ | ||
| 1 | +"""Façade GUI de Régine — paquet réservé, non implémenté (cf. specs/003-config-contexte-travail).""" | |
| new file mode 100644 | |||
| @@ -0,0 +1 @@ | |||
| 1 | +"""Façade GUI de Régine — paquet réservé, non implémenté (cf. specs/003-config-contexte-travail).""" | ||
added
pyproject.toml +15 -0 | new file mode 100644 | ||
| @@ -0,0 +1,15 @@ | ||
| 1 | +[tool.uv.workspace] | |
| 2 | +members = ["packages/*"] | |
| 3 | + | |
| 4 | +[tool.ruff] | |
| 5 | +line-length = 100 | |
| 6 | +target-version = "py311" | |
| 7 | + | |
| 8 | +[tool.ruff.lint] | |
| 9 | +select = ["E", "F", "I", "UP"] | |
| 10 | + | |
| 11 | +[dependency-groups] | |
| 12 | +dev = [ | |
| 13 | + "pytest>=9.1.1", | |
| 14 | + "ruff>=0.16.8", | |
| 15 | +] | |
| new file mode 100644 | |||
| @@ -0,0 +1,15 @@ | |||
| 1 | +[tool.uv.workspace] | ||
| 2 | +members = ["packages/*"] | ||
| 3 | + | ||
| 4 | +[tool.ruff] | ||
| 5 | +line-length = 100 | ||
| 6 | +target-version = "py311" | ||
| 7 | + | ||
| 8 | +[tool.ruff.lint] | ||
| 9 | +select = ["E", "F", "I", "UP"] | ||
| 10 | + | ||
| 11 | +[dependency-groups] | ||
| 12 | +dev = [ | ||
| 13 | + "pytest>=9.1.1", | ||
| 14 | + "ruff>=0.16.8", | ||
| 15 | +] | ||
modified
specs/004-categorisation-dossiers/contracts/regine-core-api.md +17 -11 | @@ -2,13 +2,17 @@ | ||
| 2 | 2 | |
| 3 | 3 | Cette fonctionnalité n'introduit aucune nouvelle commande CLI de premier niveau : elle expose une API Python interne à `regine-core`, consommée par la future façade CLI du module import (`specs/001-import-photos`) et, plus tard, par `regine-gui`/`regine-agent` — cohérent avec le Principe VI (bibliothèque centrale, façades minces, objets structurés plutôt que texte à parser). |
| 4 | 4 | |
| 5 | -## `regine_core.dossier.root.determine_root(date, categorie=None) -> RootLocation` | |
| 5 | +## `regine_core.dossier.root.determine_root(date, categorie=None, *, archive_root, local_root) -> RootLocation` | |
| 6 | 6 | |
| 7 | 7 | Résout le répertoire racine (année ou catégorie) pour un dossier ou dossier parent en cours de création (FR-001, FR-002, FR-003). |
| 8 | 8 | |
| 9 | +**Signature finale (2026-09-19)** : `archive_root`/`local_root` sont des paramètres explicites plutôt que lus depuis le contexte de travail (`specs/003-config-contexte-travail`, pas encore implémenté au moment de l'implémentation de ce module) — décision prise en tâches (`tasks.md` T008) pour que ce module reste testable et livrable indépendamment de `specs/003`. Le futur code appelant (CLI d'import, `specs/001-import-photos`) reste responsable de lire le contexte de travail et de transmettre ces deux chemins. | |
| 10 | + | |
| 9 | 11 | **Entrée** : |
| 10 | 12 | - `date` : date EXIF (ou plage) du groupe d'import concerné. |
| 11 | 13 | - `categorie` : nom de catégorie choisi par l'utilisateur, ou `None` pour le placement par défaut par année. |
| 14 | +- `archive_root` (mot-clé obligatoire) : racine de l'archive (chemin `Path`). | |
| 15 | +- `local_root` (mot-clé obligatoire) : racine de l'espace de travail local (chemin `Path`). | |
| 12 | 16 | |
| 13 | 17 | **Sortie** : objet `RootLocation` structuré (`type`, `nom`, `chemin_archive`, `chemin_local` — cf. `data-model.md`). Aucune exception levée pour une catégorie inconnue : `determine_root` ne fait que construire le chemin, la création effective du répertoire relève du module `archive`/`import`, pas de cette fonction. |
| 14 | 18 | |
| @@ -16,24 +20,26 @@ Résout le répertoire racine (année ou catégorie) pour un dossier ou dossier | ||
| 16 | 20 | - Appelée une seule fois par dossier/dossier parent créé (FR-006) — jamais réappelée pour un sous-dossier d'un parent existant, qui réutilise directement le `RootLocation` déjà résolu du parent. |
| 17 | 21 | - `chemin_archive` et `chemin_local` partagent toujours le même `nom` et le même `type` (FR-002/FR-003, miroir garanti par construction plutôt que par synchronisation a posteriori). |
| 18 | 22 | |
| 19 | -## `regine_core.config.categories.list_known_categories() -> list[str]` | |
| 23 | +## `regine_core.config.categories.list_known_categories(conn: sqlite3.Connection) -> list[str]` | |
| 20 | 24 | |
| 21 | 25 | Retourne la liste des noms de catégories déjà utilisées au moins une fois (FR-004), triée par ordre d'utilisation la plus récente. Liste vide si aucune catégorie n'a jamais été utilisée (pas une erreur). |
| 22 | 26 | |
| 23 | -## `regine_core.config.categories.suggest_categories(saisie: str) -> list[str]` | |
| 27 | +**Signature finale (2026-09-19)** : reçoit la connexion (`sqlite3.Connection` déjà ouverte via `regine_core.config.db.open_context_db`) en paramètre explicite plutôt que de gérer sa propre connexion, pour rester cohérent avec `regine_core.config.db` (propriétaire du cycle de vie de la base) et testable sans dépendre d'un chemin de fichier fixe. | |
| 28 | + | |
| 29 | +## `regine_core.config.categories.suggest_categories(conn: sqlite3.Connection, saisie: str) -> list[str]` | |
| 24 | 30 | |
| 25 | -Retourne, parmi les catégories déjà connues, celles dont le nom est proche de `saisie` (cf. `research.md` § 2), pour aider à éviter un doublon orthographique (Edge Case "Mariage" vs "mariage"). Liste vide si aucune correspondance proche — la saisie de l'utilisateur reste alors utilisable telle quelle comme nouvelle catégorie (FR-004). | |
| 31 | +Retourne, parmi les catégories déjà connues, celles dont le nom est proche de `saisie` (comparaison insensible à la casse, cf. `research.md` § 2), pour aider à éviter un doublon orthographique (Edge Case "Mariage" vs "mariage"). Liste vide si aucune correspondance proche — la saisie de l'utilisateur reste alors utilisable telle quelle comme nouvelle catégorie (FR-004). | |
| 26 | 32 | |
| 27 | -## `regine_core.config.categories.register_category_usage(nom: str) -> None` | |
| 33 | +## `regine_core.config.categories.register_category_usage(conn: sqlite3.Connection, nom: str) -> None` | |
| 28 | 34 | |
| 29 | -Enregistre `nom` dans la table `categories` s'il n'y figure pas déjà (première utilisation). Appelée automatiquement par le flux de création de dossier au moment où un `RootLocation` de type `categorie` est effectivement archivé — jamais à la simple saisie, pour ne pas polluer la liste de suggestions avec des catégories tapées puis abandonnées avant confirmation (cf. Principe II, confirmation explicite avant toute écriture). | |
| 35 | +Enregistre `nom` dans la table `categories` s'il n'y figure pas déjà (première utilisation, idempotent). Appelée automatiquement par le flux de création de dossier au moment où un `RootLocation` de type `categorie` est effectivement archivé — jamais à la simple saisie, pour ne pas polluer la liste de suggestions avec des catégories tapées puis abandonnées avant confirmation (cf. Principe II, confirmation explicite avant toute écriture). | |
| 30 | 36 | |
| 31 | 37 | ## Point d'intégration côté façade (futur, hors périmètre de ce plan) |
| 32 | 38 | |
| 33 | 39 | Le futur code CLI du module import (`specs/001-import-photos` FR-007) DEVRA, à l'étape de destination de chaque groupe créant un nouveau dossier simple ou un nouveau dossier parent : |
| 34 | -1. Demander à l'utilisateur s'il choisit une catégorie ou le placement par défaut, en proposant `list_known_categories()`. | |
| 35 | -2. Si l'utilisateur saisit un nom non listé, appeler `suggest_categories(saisie)` et lui présenter les correspondances proches avant de confirmer la création d'une catégorie réellement nouvelle. | |
| 36 | -3. Appeler `determine_root(date, categorie)` pour obtenir le `RootLocation` à afficher dans le résumé de confirmation (`specs/001-import-photos` FR-018) et à utiliser pour la construction du chemin final. | |
| 37 | -4. Appeler `register_category_usage(nom)` uniquement après confirmation explicite de l'archivage, jamais avant. | |
| 40 | +1. Ouvrir la base de contexte centralisée (`regine_core.config.db.open_context_db`) et demander à l'utilisateur s'il choisit une catégorie ou le placement par défaut, en proposant `list_known_categories(conn)`. | |
| 41 | +2. Si l'utilisateur saisit un nom non listé, appeler `suggest_categories(conn, saisie)` et lui présenter les correspondances proches avant de confirmer la création d'une catégorie réellement nouvelle. | |
| 42 | +3. Appeler `determine_root(date, categorie, archive_root=..., local_root=...)` (chemins lus depuis le contexte de travail, `specs/003-config-contexte-travail`) pour obtenir le `RootLocation` à afficher dans le résumé de confirmation (`specs/001-import-photos` FR-018) et à utiliser pour la construction du chemin final. | |
| 43 | +4. Appeler `register_category_usage(conn, nom)` uniquement après confirmation explicite de l'archivage, jamais avant. | |
| 38 | 44 | |
| 39 | -Ce point d'intégration n'est pas implémenté par ce plan (`specs/001-import-photos` n'a pas encore de plan) ; il est documenté ici pour que l'implémentation future de l'import consomme cette API sans la redéfinir. | |
| 45 | +Ce point d'intégration est implémenté dans `specs/001-import-photos/tasks.md` (T015/T020, module `regine_core.import_carte.destination`) ; documenté ici pour qu'il consomme cette API sans la redéfinir. | |
| @@ -2,13 +2,17 @@ | |||
| 2 | 2 | ||
| 3 | Cette fonctionnalité n'introduit aucune nouvelle commande CLI de premier niveau : elle expose une API Python interne à `regine-core`, consommée par la future façade CLI du module import (`specs/001-import-photos`) et, plus tard, par `regine-gui`/`regine-agent` — cohérent avec le Principe VI (bibliothèque centrale, façades minces, objets structurés plutôt que texte à parser). | 3 | Cette fonctionnalité n'introduit aucune nouvelle commande CLI de premier niveau : elle expose une API Python interne à `regine-core`, consommée par la future façade CLI du module import (`specs/001-import-photos`) et, plus tard, par `regine-gui`/`regine-agent` — cohérent avec le Principe VI (bibliothèque centrale, façades minces, objets structurés plutôt que texte à parser). |
| 4 | 4 | ||
| 5 | -## `regine_core.dossier.root.determine_root(date, categorie=None) -> RootLocation` | 5 | +## `regine_core.dossier.root.determine_root(date, categorie=None, *, archive_root, local_root) -> RootLocation` |
| 6 | 6 | ||
| 7 | Résout le répertoire racine (année ou catégorie) pour un dossier ou dossier parent en cours de création (FR-001, FR-002, FR-003). | 7 | Résout le répertoire racine (année ou catégorie) pour un dossier ou dossier parent en cours de création (FR-001, FR-002, FR-003). |
| 8 | 8 | ||
| 9 | +**Signature finale (2026-09-19)** : `archive_root`/`local_root` sont des paramètres explicites plutôt que lus depuis le contexte de travail (`specs/003-config-contexte-travail`, pas encore implémenté au moment de l'implémentation de ce module) — décision prise en tâches (`tasks.md` T008) pour que ce module reste testable et livrable indépendamment de `specs/003`. Le futur code appelant (CLI d'import, `specs/001-import-photos`) reste responsable de lire le contexte de travail et de transmettre ces deux chemins. | ||
| 10 | + | ||
| 9 | **Entrée** : | 11 | **Entrée** : |
| 10 | - `date` : date EXIF (ou plage) du groupe d'import concerné. | 12 | - `date` : date EXIF (ou plage) du groupe d'import concerné. |
| 11 | - `categorie` : nom de catégorie choisi par l'utilisateur, ou `None` pour le placement par défaut par année. | 13 | - `categorie` : nom de catégorie choisi par l'utilisateur, ou `None` pour le placement par défaut par année. |
| 14 | +- `archive_root` (mot-clé obligatoire) : racine de l'archive (chemin `Path`). | ||
| 15 | +- `local_root` (mot-clé obligatoire) : racine de l'espace de travail local (chemin `Path`). | ||
| 12 | 16 | ||
| 13 | **Sortie** : objet `RootLocation` structuré (`type`, `nom`, `chemin_archive`, `chemin_local` — cf. `data-model.md`). Aucune exception levée pour une catégorie inconnue : `determine_root` ne fait que construire le chemin, la création effective du répertoire relève du module `archive`/`import`, pas de cette fonction. | 17 | **Sortie** : objet `RootLocation` structuré (`type`, `nom`, `chemin_archive`, `chemin_local` — cf. `data-model.md`). Aucune exception levée pour une catégorie inconnue : `determine_root` ne fait que construire le chemin, la création effective du répertoire relève du module `archive`/`import`, pas de cette fonction. |
| 14 | 18 | ||
| @@ -16,24 +20,26 @@ Résout le répertoire racine (année ou catégorie) pour un dossier ou dossier | |||
| 16 | - Appelée une seule fois par dossier/dossier parent créé (FR-006) — jamais réappelée pour un sous-dossier d'un parent existant, qui réutilise directement le `RootLocation` déjà résolu du parent. | 20 | - Appelée une seule fois par dossier/dossier parent créé (FR-006) — jamais réappelée pour un sous-dossier d'un parent existant, qui réutilise directement le `RootLocation` déjà résolu du parent. |
| 17 | - `chemin_archive` et `chemin_local` partagent toujours le même `nom` et le même `type` (FR-002/FR-003, miroir garanti par construction plutôt que par synchronisation a posteriori). | 21 | - `chemin_archive` et `chemin_local` partagent toujours le même `nom` et le même `type` (FR-002/FR-003, miroir garanti par construction plutôt que par synchronisation a posteriori). |
| 18 | 22 | ||
| 19 | -## `regine_core.config.categories.list_known_categories() -> list[str]` | 23 | +## `regine_core.config.categories.list_known_categories(conn: sqlite3.Connection) -> list[str]` |
| 20 | 24 | ||
| 21 | Retourne la liste des noms de catégories déjà utilisées au moins une fois (FR-004), triée par ordre d'utilisation la plus récente. Liste vide si aucune catégorie n'a jamais été utilisée (pas une erreur). | 25 | Retourne la liste des noms de catégories déjà utilisées au moins une fois (FR-004), triée par ordre d'utilisation la plus récente. Liste vide si aucune catégorie n'a jamais été utilisée (pas une erreur). |
| 22 | 26 | ||
| 23 | -## `regine_core.config.categories.suggest_categories(saisie: str) -> list[str]` | 27 | +**Signature finale (2026-09-19)** : reçoit la connexion (`sqlite3.Connection` déjà ouverte via `regine_core.config.db.open_context_db`) en paramètre explicite plutôt que de gérer sa propre connexion, pour rester cohérent avec `regine_core.config.db` (propriétaire du cycle de vie de la base) et testable sans dépendre d'un chemin de fichier fixe. |
| 28 | + | ||
| 29 | +## `regine_core.config.categories.suggest_categories(conn: sqlite3.Connection, saisie: str) -> list[str]` | ||
| 24 | 30 | ||
| 25 | -Retourne, parmi les catégories déjà connues, celles dont le nom est proche de `saisie` (cf. `research.md` § 2), pour aider à éviter un doublon orthographique (Edge Case "Mariage" vs "mariage"). Liste vide si aucune correspondance proche — la saisie de l'utilisateur reste alors utilisable telle quelle comme nouvelle catégorie (FR-004). | 31 | +Retourne, parmi les catégories déjà connues, celles dont le nom est proche de `saisie` (comparaison insensible à la casse, cf. `research.md` § 2), pour aider à éviter un doublon orthographique (Edge Case "Mariage" vs "mariage"). Liste vide si aucune correspondance proche — la saisie de l'utilisateur reste alors utilisable telle quelle comme nouvelle catégorie (FR-004). |
| 26 | 32 | ||
| 27 | -## `regine_core.config.categories.register_category_usage(nom: str) -> None` | 33 | +## `regine_core.config.categories.register_category_usage(conn: sqlite3.Connection, nom: str) -> None` |
| 28 | 34 | ||
| 29 | -Enregistre `nom` dans la table `categories` s'il n'y figure pas déjà (première utilisation). Appelée automatiquement par le flux de création de dossier au moment où un `RootLocation` de type `categorie` est effectivement archivé — jamais à la simple saisie, pour ne pas polluer la liste de suggestions avec des catégories tapées puis abandonnées avant confirmation (cf. Principe II, confirmation explicite avant toute écriture). | 35 | +Enregistre `nom` dans la table `categories` s'il n'y figure pas déjà (première utilisation, idempotent). Appelée automatiquement par le flux de création de dossier au moment où un `RootLocation` de type `categorie` est effectivement archivé — jamais à la simple saisie, pour ne pas polluer la liste de suggestions avec des catégories tapées puis abandonnées avant confirmation (cf. Principe II, confirmation explicite avant toute écriture). |
| 30 | 36 | ||
| 31 | ## Point d'intégration côté façade (futur, hors périmètre de ce plan) | 37 | ## Point d'intégration côté façade (futur, hors périmètre de ce plan) |
| 32 | 38 | ||
| 33 | Le futur code CLI du module import (`specs/001-import-photos` FR-007) DEVRA, à l'étape de destination de chaque groupe créant un nouveau dossier simple ou un nouveau dossier parent : | 39 | Le futur code CLI du module import (`specs/001-import-photos` FR-007) DEVRA, à l'étape de destination de chaque groupe créant un nouveau dossier simple ou un nouveau dossier parent : |
| 34 | -1. Demander à l'utilisateur s'il choisit une catégorie ou le placement par défaut, en proposant `list_known_categories()`. | 40 | +1. Ouvrir la base de contexte centralisée (`regine_core.config.db.open_context_db`) et demander à l'utilisateur s'il choisit une catégorie ou le placement par défaut, en proposant `list_known_categories(conn)`. |
| 35 | -2. Si l'utilisateur saisit un nom non listé, appeler `suggest_categories(saisie)` et lui présenter les correspondances proches avant de confirmer la création d'une catégorie réellement nouvelle. | 41 | +2. Si l'utilisateur saisit un nom non listé, appeler `suggest_categories(conn, saisie)` et lui présenter les correspondances proches avant de confirmer la création d'une catégorie réellement nouvelle. |
| 36 | -3. Appeler `determine_root(date, categorie)` pour obtenir le `RootLocation` à afficher dans le résumé de confirmation (`specs/001-import-photos` FR-018) et à utiliser pour la construction du chemin final. | 42 | +3. Appeler `determine_root(date, categorie, archive_root=..., local_root=...)` (chemins lus depuis le contexte de travail, `specs/003-config-contexte-travail`) pour obtenir le `RootLocation` à afficher dans le résumé de confirmation (`specs/001-import-photos` FR-018) et à utiliser pour la construction du chemin final. |
| 37 | -4. Appeler `register_category_usage(nom)` uniquement après confirmation explicite de l'archivage, jamais avant. | 43 | +4. Appeler `register_category_usage(conn, nom)` uniquement après confirmation explicite de l'archivage, jamais avant. |
| 38 | 44 | ||
| 39 | -Ce point d'intégration n'est pas implémenté par ce plan (`specs/001-import-photos` n'a pas encore de plan) ; il est documenté ici pour que l'implémentation future de l'import consomme cette API sans la redéfinir. | 45 | +Ce point d'intégration est implémenté dans `specs/001-import-photos/tasks.md` (T015/T020, module `regine_core.import_carte.destination`) ; documenté ici pour qu'il consomme cette API sans la redéfinir. |
modified
specs/004-categorisation-dossiers/tasks.md +28 -28 | @@ -22,12 +22,12 @@ Aucun code n'existe encore dans ce dépôt (seuls `docs/`, `specs/`, `.specify/` | ||
| 22 | 22 | |
| 23 | 23 | **Purpose**: Initialisation du monorepo — aucun paquet n'existe encore. |
| 24 | 24 | |
| 25 | -- [ ] T001 Créer le squelette du monorepo à la racine du dépôt : `packages/regine-core/`, `packages/regine-cli/`, `packages/regine-gui/`, `packages/regine-agent/`, plus un `pyproject.toml` racine déclarant un workspace `uv` référençant les 4 paquets (cf. `specs/003-config-contexte-travail/plan.md` § Project Structure et `research.md` § 5) | |
| 26 | -- [ ] T002 [P] Initialiser `packages/regine-core/pyproject.toml` (nom `regine-core`, aucune dépendance tierce) et `packages/regine-core/src/regine_core/__init__.py` | |
| 27 | -- [ ] T003 [P] Initialiser `packages/regine-cli/pyproject.toml` (dépendance vers `regine-core`) et `packages/regine-cli/src/regine_cli/__init__.py` | |
| 28 | -- [ ] T004 [P] Créer `packages/regine-gui/pyproject.toml` et `packages/regine-agent/pyproject.toml` comme paquets vides (dépendance déclarée vers `regine-core`, aucun code métier), conformément à `specs/003-config-contexte-travail/plan.md` | |
| 29 | -- [ ] T005 [P] Configurer `ruff` (lint + format) au niveau du `pyproject.toml` racine du workspace | |
| 30 | -- [ ] T006 [P] Configurer `pytest` pour `packages/regine-core` (`[tool.pytest.ini_options]`, création des dossiers `tests/unit/`, `tests/integration/`, `tests/contract/`) | |
| 25 | +- [X] T001 Créer le squelette du monorepo à la racine du dépôt : `packages/regine-core/`, `packages/regine-cli/`, `packages/regine-gui/`, `packages/regine-agent/`, plus un `pyproject.toml` racine déclarant un workspace `uv` référençant les 4 paquets (cf. `specs/003-config-contexte-travail/plan.md` § Project Structure et `research.md` § 5) | |
| 26 | +- [X] T002 [P] Initialiser `packages/regine-core/pyproject.toml` (nom `regine-core`, aucune dépendance tierce) et `packages/regine-core/src/regine_core/__init__.py` | |
| 27 | +- [X] T003 [P] Initialiser `packages/regine-cli/pyproject.toml` (dépendance vers `regine-core`) et `packages/regine-cli/src/regine_cli/__init__.py` | |
| 28 | +- [X] T004 [P] Créer `packages/regine-gui/pyproject.toml` et `packages/regine-agent/pyproject.toml` comme paquets vides (dépendance déclarée vers `regine-core`, aucun code métier), conformément à `specs/003-config-contexte-travail/plan.md` | |
| 29 | +- [X] T005 [P] Configurer `ruff` (lint + format) au niveau du `pyproject.toml` racine du workspace | |
| 30 | +- [X] T006 [P] Configurer `pytest` pour `packages/regine-core` (`[tool.pytest.ini_options]`, création des dossiers `tests/unit/`, `tests/integration/`, `tests/contract/`) | |
| 31 | 31 | |
| 32 | 32 | --- |
| 33 | 33 | |
| @@ -37,10 +37,10 @@ Aucun code n'existe encore dans ce dépôt (seuls `docs/`, `specs/`, `.specify/` | ||
| 37 | 37 | |
| 38 | 38 | **⚠️ CRITICAL**: Aucune user story ne peut être implémentée avant la fin de cette phase. |
| 39 | 39 | |
| 40 | -- [ ] T007 Créer `packages/regine-core/src/regine_core/dossier/__init__.py` (premier code du module `dossier`, cf. `docs/interface-cli-gui-architecture.md`) | |
| 41 | -- [ ] T008 Définir le dataclass `RootLocation` (`type: Literal["annee", "categorie"]`, `nom: str`, `chemin_archive: Path`, `chemin_local: Path`) et la signature de `determine_root(date, categorie=None, *, archive_root, local_root)` (corps non implémenté) dans `packages/regine-core/src/regine_core/dossier/root.py` | |
| 42 | -- [ ] T009 Créer `packages/regine-core/src/regine_core/config/db.py` : fonction `open_context_db(path) -> sqlite3.Connection` avec bootstrap `PRAGMA user_version` / `PRAGMA application_id` et écriture transactionnelle — infrastructure partagée que `specs/003-config-contexte-travail` réutilisera pour sa propre table `boitiers` (cf. `research.md` § 1, `specs/003-config-contexte-travail/research.md` § 2) | |
| 43 | -- [ ] T010 [P] Test unitaire du bootstrap de la base de contexte dans `packages/regine-core/tests/unit/test_db_bootstrap.py` (vérifie `PRAGMA user_version`/`application_id` après initialisation, réouverture idempotente sans perte de données) | |
| 40 | +- [X] T007 Créer `packages/regine-core/src/regine_core/dossier/__init__.py` (premier code du module `dossier`, cf. `docs/interface-cli-gui-architecture.md`) | |
| 41 | +- [X] T008 Définir le dataclass `RootLocation` (`type: Literal["annee", "categorie"]`, `nom: str`, `chemin_archive: Path`, `chemin_local: Path`) et la signature de `determine_root(date, categorie=None, *, archive_root, local_root)` (corps non implémenté) dans `packages/regine-core/src/regine_core/dossier/root.py` | |
| 42 | +- [X] T009 Créer `packages/regine-core/src/regine_core/config/db.py` : fonction `open_context_db(path) -> sqlite3.Connection` avec bootstrap `PRAGMA user_version` / `PRAGMA application_id` et écriture transactionnelle — infrastructure partagée que `specs/003-config-contexte-travail` réutilisera pour sa propre table `boitiers` (cf. `research.md` § 1, `specs/003-config-contexte-travail/research.md` § 2) | |
| 43 | +- [X] T010 [P] Test unitaire du bootstrap de la base de contexte dans `packages/regine-core/tests/unit/test_db_bootstrap.py` (vérifie `PRAGMA user_version`/`application_id` après initialisation, réouverture idempotente sans perte de données) | |
| 44 | 44 | |
| 45 | 45 | **Checkpoint**: Fondations prêtes — les phases User Story peuvent commencer. |
| 46 | 46 | |
| @@ -54,12 +54,12 @@ Aucun code n'existe encore dans ce dépôt (seuls `docs/`, `specs/`, `.specify/` | ||
| 54 | 54 | |
| 55 | 55 | ### Tests for User Story 1 |
| 56 | 56 | |
| 57 | -- [ ] T011 [P] [US1] Test unitaire : `determine_root` sans catégorie retourne `type="annee"`, `nom` dérivé de l'année de la date, chemins archive/local en miroir, dans `packages/regine-core/tests/unit/test_root_resolution.py` | |
| 57 | +- [X] T011 [P] [US1] Test unitaire : `determine_root` sans catégorie retourne `type="annee"`, `nom` dérivé de l'année de la date, chemins archive/local en miroir, dans `packages/regine-core/tests/unit/test_root_resolution.py` | |
| 58 | 58 | |
| 59 | 59 | ### Implementation for User Story 1 |
| 60 | 60 | |
| 61 | -- [ ] T012 [US1] Implémenter la branche par défaut (année) de `determine_root` dans `packages/regine-core/src/regine_core/dossier/root.py` (dépend de T008) | |
| 62 | -- [ ] T013 [US1] Garantir par construction que `chemin_archive.name == chemin_local.name == nom` (miroir archive/local, FR-002) — pas de synchronisation a posteriori | |
| 61 | +- [X] T012 [US1] Implémenter la branche par défaut (année) de `determine_root` dans `packages/regine-core/src/regine_core/dossier/root.py` (dépend de T008) | |
| 62 | +- [X] T013 [US1] Garantir par construction que `chemin_archive.name == chemin_local.name == nom` (miroir archive/local, FR-002) — pas de synchronisation a posteriori | |
| 63 | 63 | |
| 64 | 64 | **Checkpoint**: User Story 1 fonctionnelle et testable indépendamment (MVP). |
| 65 | 65 | |
| @@ -73,17 +73,17 @@ Aucun code n'existe encore dans ce dépôt (seuls `docs/`, `specs/`, `.specify/` | ||
| 73 | 73 | |
| 74 | 74 | ### Tests for User Story 2 |
| 75 | 75 | |
| 76 | -- [ ] T014 [P] [US2] Test unitaire : `determine_root` avec `categorie` retourne `type="categorie"`, aucun répertoire d'année, dans `packages/regine-core/tests/unit/test_root_resolution.py` | |
| 77 | -- [ ] T015 [P] [US2] Test unitaire : `list_known_categories`, `suggest_categories`, `register_category_usage` dans `packages/regine-core/tests/unit/test_categories_cache.py` (liste vide au départ ; `suggest_categories("Mariage")` retrouve `"mariage"` après enregistrement ; pas de doublon silencieux) | |
| 76 | +- [X] T014 [P] [US2] Test unitaire : `determine_root` avec `categorie` retourne `type="categorie"`, aucun répertoire d'année, dans `packages/regine-core/tests/unit/test_root_resolution.py` | |
| 77 | +- [X] T015 [P] [US2] Test unitaire : `list_known_categories`, `suggest_categories`, `register_category_usage` dans `packages/regine-core/tests/unit/test_categories_cache.py` (liste vide au départ ; `suggest_categories("Mariage")` retrouve `"mariage"` après enregistrement ; pas de doublon silencieux) | |
| 78 | 78 | |
| 79 | 79 | ### Implementation for User Story 2 |
| 80 | 80 | |
| 81 | -- [ ] T016 [US2] Ajouter la table `categories` (`nom` UNIQUE, `premiere_utilisation`) au schéma de `packages/regine-core/src/regine_core/config/db.py` (dépend de T009) | |
| 82 | -- [ ] T017 [P] [US2] Implémenter `list_known_categories()` dans `packages/regine-core/src/regine_core/config/categories.py` (dépend de T016) | |
| 83 | -- [ ] T018 [P] [US2] Implémenter `suggest_categories(saisie)` avec `difflib.get_close_matches` dans `packages/regine-core/src/regine_core/config/categories.py` (dépend de T016 ; cf. `research.md` § 2) | |
| 84 | -- [ ] T019 [US2] Implémenter `register_category_usage(nom)` (idempotent ; à n'appeler qu'après confirmation explicite d'archivage, cf. Principe II de la constitution) dans `packages/regine-core/src/regine_core/config/categories.py` (dépend de T016) | |
| 85 | -- [ ] T020 [US2] Implémenter la branche catégorie de `determine_root` dans `root.py` (dépend de T012) | |
| 86 | -- [ ] T021 [US2] Test de non-régression : la signature de `determine_root` n'accepte qu'un seul paramètre `categorie` (jamais une liste) — garantit FR-005 (une seule catégorie à la fois) dans `test_root_resolution.py` | |
| 81 | +- [X] T016 [US2] Ajouter la table `categories` (`nom` UNIQUE, `premiere_utilisation`) au schéma de `packages/regine-core/src/regine_core/config/db.py` (dépend de T009) — implémentée dans `config/categories.py` (`_ensure_schema`), le fichier `categories` du contexte centralisé reste ouvert via `db.open_context_db` | |
| 82 | +- [X] T017 [P] [US2] Implémenter `list_known_categories()` dans `packages/regine-core/src/regine_core/config/categories.py` (dépend de T016) | |
| 83 | +- [X] T018 [P] [US2] Implémenter `suggest_categories(saisie)` avec `difflib.get_close_matches` dans `packages/regine-core/src/regine_core/config/categories.py` (dépend de T016 ; cf. `research.md` § 2) | |
| 84 | +- [X] T019 [US2] Implémenter `register_category_usage(nom)` (idempotent ; à n'appeler qu'après confirmation explicite d'archivage, cf. Principe II de la constitution) dans `packages/regine-core/src/regine_core/config/categories.py` (dépend de T016) | |
| 85 | +- [X] T020 [US2] Implémenter la branche catégorie de `determine_root` dans `root.py` (dépend de T012) | |
| 86 | +- [X] T021 [US2] Test de non-régression : la signature de `determine_root` n'accepte qu'un seul paramètre `categorie` (jamais une liste) — garantit FR-005 (une seule catégorie à la fois) dans `test_root_resolution.py` | |
| 87 | 87 | |
| 88 | 88 | **Checkpoint**: User Stories 1 ET 2 fonctionnelles indépendamment. |
| 89 | 89 | |
| @@ -97,11 +97,11 @@ Aucun code n'existe encore dans ce dépôt (seuls `docs/`, `specs/`, `.specify/` | ||
| 97 | 97 | |
| 98 | 98 | ### Tests for User Story 3 |
| 99 | 99 | |
| 100 | -- [ ] T022 [P] [US3] Test d'intégration : le `RootLocation` d'un dossier parent est réutilisé tel quel pour un sous-dossier, sans nouvel appel à `determine_root`, dans `packages/regine-core/tests/integration/test_root_inheritance.py` | |
| 100 | +- [X] T022 [P] [US3] Test d'intégration : le `RootLocation` d'un dossier parent est réutilisé tel quel pour un sous-dossier, sans nouvel appel à `determine_root`, dans `packages/regine-core/tests/integration/test_root_inheritance.py` | |
| 101 | 101 | |
| 102 | 102 | ### Implementation for User Story 3 |
| 103 | 103 | |
| 104 | -- [ ] T023 [US3] Documenter (docstring) dans `root.py` que `determine_root` ne DOIT être appelée qu'une seule fois par dossier parent — le futur code appelant (import, `specs/001-import-photos`) réutilise l'objet `RootLocation` déjà résolu pour tout sous-dossier ; aucune nouvelle logique de calcul requise ici | |
| 104 | +- [X] T023 [US3] Documenter (docstring) dans `root.py` que `determine_root` ne DOIT être appelée qu'une seule fois par dossier parent — le futur code appelant (import, `specs/001-import-photos`) réutilise l'objet `RootLocation` déjà résolu pour tout sous-dossier ; aucune nouvelle logique de calcul requise ici | |
| 105 | 105 | |
| 106 | 106 | **Checkpoint**: User Stories 1, 2 et 3 fonctionnelles indépendamment. |
| 107 | 107 | |
| @@ -113,8 +113,8 @@ Aucun code n'existe encore dans ce dépôt (seuls `docs/`, `specs/`, `.specify/` | ||
| 113 | 113 | |
| 114 | 114 | **Statut** : **Mise à jour du 2026-09-19** — le mécanisme de détection de déplacement par somme de contrôle a désormais sa propre spec et son propre plan (`specs/005-checkout-reconciliation`, dont FR-010 couvre exactement ce cas). Ce n'est plus une dépendance bloquante au niveau spécification : seulement une dépendance d'ordonnancement (l'implémentation de `specs/005` doit précéder la validation de bout en bout de cette user story). |
| 115 | 115 | |
| 116 | -- [ ] T024 [US4] Documenter (docstring) dans `packages/regine-core/src/regine_core/dossier/root.py` qu'il n'existe aucune opération d'écriture directe sur `RootLocation` : tout changement de répertoire racine passe exclusivement par `regine_core.archive.reconciliation.comparer` (`specs/005-checkout-reconciliation`), qui classe un déplacement de dossier entier comme `deplacement` sans code supplémentaire ici | |
| 117 | -- [ ] T024b [US4] Test d'intégration (à exécuter une fois `specs/005-checkout-reconciliation` implémentée) : déplacer un dossier de travail entre deux répertoires racine (année → catégorie) et vérifier que `regine_core.archive.reconciliation.comparer` le classe `deplacement`, dans `packages/regine-core/tests/integration/test_root_recategorisation.py` | |
| 116 | +- [X] T024 [US4] Documenter (docstring) dans `packages/regine-core/src/regine_core/dossier/root.py` qu'il n'existe aucune opération d'écriture directe sur `RootLocation` : tout changement de répertoire racine passe exclusivement par `regine_core.archive.reconciliation.comparer` (`specs/005-checkout-reconciliation`), qui classe un déplacement de dossier entier comme `deplacement` sans code supplémentaire ici | |
| 117 | +- [ ] T024b [US4] Test d'intégration (à exécuter une fois `specs/005-checkout-reconciliation` implémentée) : déplacer un dossier de travail entre deux répertoires racine (année → catégorie) et vérifier que `regine_core.archive.reconciliation.comparer` le classe `deplacement`, dans `packages/regine-core/tests/integration/test_root_recategorisation.py` — **toujours en attente, `specs/005` pas encore implémentée** | |
| 118 | 118 | |
| 119 | 119 | **Checkpoint**: US4 documentée et prête à être validée dès que `specs/005-checkout-reconciliation` est implémentée — plus aucune spécification manquante. |
| 120 | 120 | |
| @@ -122,9 +122,9 @@ Aucun code n'existe encore dans ce dépôt (seuls `docs/`, `specs/`, `.specify/` | ||
| 122 | 122 | |
| 123 | 123 | ## Phase 7: Polish & Cross-Cutting Concerns |
| 124 | 124 | |
| 125 | -- [ ] T025 [P] Exécuter manuellement les scénarios 1 à 4 de `specs/004-categorisation-dossiers/quickstart.md` et consigner le résultat | |
| 126 | -- [ ] T026 [P] `ruff check --fix` sur `packages/regine-core` | |
| 127 | -- [ ] T027 Mettre à jour `specs/004-categorisation-dossiers/contracts/regine-core-api.md` pour refléter la signature définitive de `determine_root` (paramètres `archive_root`/`local_root` explicites, cf. Note en tête de ce fichier) | |
| 125 | +- [X] T025 [P] Exécuter manuellement les scénarios 1 à 4 de `specs/004-categorisation-dossiers/quickstart.md` et consigner le résultat — **2026-09-19 : les 4 scénarios passent** (déroulés via `uv run python3`, cf. session d'implémentation) | |
| 126 | +- [X] T026 [P] `ruff check --fix` sur `packages/regine-core` — 0 erreur restante, code formaté | |
| 127 | +- [X] T027 Mettre à jour `specs/004-categorisation-dossiers/contracts/regine-core-api.md` pour refléter la signature définitive de `determine_root` (paramètres `archive_root`/`local_root` explicites, cf. Note en tête de ce fichier) | |
| 128 | 128 | |
| 129 | 129 | --- |
| 130 | 130 | |
| @@ -22,12 +22,12 @@ Aucun code n'existe encore dans ce dépôt (seuls `docs/`, `specs/`, `.specify/` | |||
| 22 | 22 | ||
| 23 | **Purpose**: Initialisation du monorepo — aucun paquet n'existe encore. | 23 | **Purpose**: Initialisation du monorepo — aucun paquet n'existe encore. |
| 24 | 24 | ||
| 25 | -- [ ] T001 Créer le squelette du monorepo à la racine du dépôt : `packages/regine-core/`, `packages/regine-cli/`, `packages/regine-gui/`, `packages/regine-agent/`, plus un `pyproject.toml` racine déclarant un workspace `uv` référençant les 4 paquets (cf. `specs/003-config-contexte-travail/plan.md` § Project Structure et `research.md` § 5) | 25 | +- [X] T001 Créer le squelette du monorepo à la racine du dépôt : `packages/regine-core/`, `packages/regine-cli/`, `packages/regine-gui/`, `packages/regine-agent/`, plus un `pyproject.toml` racine déclarant un workspace `uv` référençant les 4 paquets (cf. `specs/003-config-contexte-travail/plan.md` § Project Structure et `research.md` § 5) |
| 26 | -- [ ] T002 [P] Initialiser `packages/regine-core/pyproject.toml` (nom `regine-core`, aucune dépendance tierce) et `packages/regine-core/src/regine_core/__init__.py` | 26 | +- [X] T002 [P] Initialiser `packages/regine-core/pyproject.toml` (nom `regine-core`, aucune dépendance tierce) et `packages/regine-core/src/regine_core/__init__.py` |
| 27 | -- [ ] T003 [P] Initialiser `packages/regine-cli/pyproject.toml` (dépendance vers `regine-core`) et `packages/regine-cli/src/regine_cli/__init__.py` | 27 | +- [X] T003 [P] Initialiser `packages/regine-cli/pyproject.toml` (dépendance vers `regine-core`) et `packages/regine-cli/src/regine_cli/__init__.py` |
| 28 | -- [ ] T004 [P] Créer `packages/regine-gui/pyproject.toml` et `packages/regine-agent/pyproject.toml` comme paquets vides (dépendance déclarée vers `regine-core`, aucun code métier), conformément à `specs/003-config-contexte-travail/plan.md` | 28 | +- [X] T004 [P] Créer `packages/regine-gui/pyproject.toml` et `packages/regine-agent/pyproject.toml` comme paquets vides (dépendance déclarée vers `regine-core`, aucun code métier), conformément à `specs/003-config-contexte-travail/plan.md` |
| 29 | -- [ ] T005 [P] Configurer `ruff` (lint + format) au niveau du `pyproject.toml` racine du workspace | 29 | +- [X] T005 [P] Configurer `ruff` (lint + format) au niveau du `pyproject.toml` racine du workspace |
| 30 | -- [ ] T006 [P] Configurer `pytest` pour `packages/regine-core` (`[tool.pytest.ini_options]`, création des dossiers `tests/unit/`, `tests/integration/`, `tests/contract/`) | 30 | +- [X] T006 [P] Configurer `pytest` pour `packages/regine-core` (`[tool.pytest.ini_options]`, création des dossiers `tests/unit/`, `tests/integration/`, `tests/contract/`) |
| 31 | 31 | ||
| 32 | --- | 32 | --- |
| 33 | 33 | ||
| @@ -37,10 +37,10 @@ Aucun code n'existe encore dans ce dépôt (seuls `docs/`, `specs/`, `.specify/` | |||
| 37 | 37 | ||
| 38 | **⚠️ CRITICAL**: Aucune user story ne peut être implémentée avant la fin de cette phase. | 38 | **⚠️ CRITICAL**: Aucune user story ne peut être implémentée avant la fin de cette phase. |
| 39 | 39 | ||
| 40 | -- [ ] T007 Créer `packages/regine-core/src/regine_core/dossier/__init__.py` (premier code du module `dossier`, cf. `docs/interface-cli-gui-architecture.md`) | 40 | +- [X] T007 Créer `packages/regine-core/src/regine_core/dossier/__init__.py` (premier code du module `dossier`, cf. `docs/interface-cli-gui-architecture.md`) |
| 41 | -- [ ] T008 Définir le dataclass `RootLocation` (`type: Literal["annee", "categorie"]`, `nom: str`, `chemin_archive: Path`, `chemin_local: Path`) et la signature de `determine_root(date, categorie=None, *, archive_root, local_root)` (corps non implémenté) dans `packages/regine-core/src/regine_core/dossier/root.py` | 41 | +- [X] T008 Définir le dataclass `RootLocation` (`type: Literal["annee", "categorie"]`, `nom: str`, `chemin_archive: Path`, `chemin_local: Path`) et la signature de `determine_root(date, categorie=None, *, archive_root, local_root)` (corps non implémenté) dans `packages/regine-core/src/regine_core/dossier/root.py` |
| 42 | -- [ ] T009 Créer `packages/regine-core/src/regine_core/config/db.py` : fonction `open_context_db(path) -> sqlite3.Connection` avec bootstrap `PRAGMA user_version` / `PRAGMA application_id` et écriture transactionnelle — infrastructure partagée que `specs/003-config-contexte-travail` réutilisera pour sa propre table `boitiers` (cf. `research.md` § 1, `specs/003-config-contexte-travail/research.md` § 2) | 42 | +- [X] T009 Créer `packages/regine-core/src/regine_core/config/db.py` : fonction `open_context_db(path) -> sqlite3.Connection` avec bootstrap `PRAGMA user_version` / `PRAGMA application_id` et écriture transactionnelle — infrastructure partagée que `specs/003-config-contexte-travail` réutilisera pour sa propre table `boitiers` (cf. `research.md` § 1, `specs/003-config-contexte-travail/research.md` § 2) |
| 43 | -- [ ] T010 [P] Test unitaire du bootstrap de la base de contexte dans `packages/regine-core/tests/unit/test_db_bootstrap.py` (vérifie `PRAGMA user_version`/`application_id` après initialisation, réouverture idempotente sans perte de données) | 43 | +- [X] T010 [P] Test unitaire du bootstrap de la base de contexte dans `packages/regine-core/tests/unit/test_db_bootstrap.py` (vérifie `PRAGMA user_version`/`application_id` après initialisation, réouverture idempotente sans perte de données) |
| 44 | 44 | ||
| 45 | **Checkpoint**: Fondations prêtes — les phases User Story peuvent commencer. | 45 | **Checkpoint**: Fondations prêtes — les phases User Story peuvent commencer. |
| 46 | 46 | ||
| @@ -54,12 +54,12 @@ Aucun code n'existe encore dans ce dépôt (seuls `docs/`, `specs/`, `.specify/` | |||
| 54 | 54 | ||
| 55 | ### Tests for User Story 1 | 55 | ### Tests for User Story 1 |
| 56 | 56 | ||
| 57 | -- [ ] T011 [P] [US1] Test unitaire : `determine_root` sans catégorie retourne `type="annee"`, `nom` dérivé de l'année de la date, chemins archive/local en miroir, dans `packages/regine-core/tests/unit/test_root_resolution.py` | 57 | +- [X] T011 [P] [US1] Test unitaire : `determine_root` sans catégorie retourne `type="annee"`, `nom` dérivé de l'année de la date, chemins archive/local en miroir, dans `packages/regine-core/tests/unit/test_root_resolution.py` |
| 58 | 58 | ||
| 59 | ### Implementation for User Story 1 | 59 | ### Implementation for User Story 1 |
| 60 | 60 | ||
| 61 | -- [ ] T012 [US1] Implémenter la branche par défaut (année) de `determine_root` dans `packages/regine-core/src/regine_core/dossier/root.py` (dépend de T008) | 61 | +- [X] T012 [US1] Implémenter la branche par défaut (année) de `determine_root` dans `packages/regine-core/src/regine_core/dossier/root.py` (dépend de T008) |
| 62 | -- [ ] T013 [US1] Garantir par construction que `chemin_archive.name == chemin_local.name == nom` (miroir archive/local, FR-002) — pas de synchronisation a posteriori | 62 | +- [X] T013 [US1] Garantir par construction que `chemin_archive.name == chemin_local.name == nom` (miroir archive/local, FR-002) — pas de synchronisation a posteriori |
| 63 | 63 | ||
| 64 | **Checkpoint**: User Story 1 fonctionnelle et testable indépendamment (MVP). | 64 | **Checkpoint**: User Story 1 fonctionnelle et testable indépendamment (MVP). |
| 65 | 65 | ||
| @@ -73,17 +73,17 @@ Aucun code n'existe encore dans ce dépôt (seuls `docs/`, `specs/`, `.specify/` | |||
| 73 | 73 | ||
| 74 | ### Tests for User Story 2 | 74 | ### Tests for User Story 2 |
| 75 | 75 | ||
| 76 | -- [ ] T014 [P] [US2] Test unitaire : `determine_root` avec `categorie` retourne `type="categorie"`, aucun répertoire d'année, dans `packages/regine-core/tests/unit/test_root_resolution.py` | 76 | +- [X] T014 [P] [US2] Test unitaire : `determine_root` avec `categorie` retourne `type="categorie"`, aucun répertoire d'année, dans `packages/regine-core/tests/unit/test_root_resolution.py` |
| 77 | -- [ ] T015 [P] [US2] Test unitaire : `list_known_categories`, `suggest_categories`, `register_category_usage` dans `packages/regine-core/tests/unit/test_categories_cache.py` (liste vide au départ ; `suggest_categories("Mariage")` retrouve `"mariage"` après enregistrement ; pas de doublon silencieux) | 77 | +- [X] T015 [P] [US2] Test unitaire : `list_known_categories`, `suggest_categories`, `register_category_usage` dans `packages/regine-core/tests/unit/test_categories_cache.py` (liste vide au départ ; `suggest_categories("Mariage")` retrouve `"mariage"` après enregistrement ; pas de doublon silencieux) |
| 78 | 78 | ||
| 79 | ### Implementation for User Story 2 | 79 | ### Implementation for User Story 2 |
| 80 | 80 | ||
| 81 | -- [ ] T016 [US2] Ajouter la table `categories` (`nom` UNIQUE, `premiere_utilisation`) au schéma de `packages/regine-core/src/regine_core/config/db.py` (dépend de T009) | 81 | +- [X] T016 [US2] Ajouter la table `categories` (`nom` UNIQUE, `premiere_utilisation`) au schéma de `packages/regine-core/src/regine_core/config/db.py` (dépend de T009) — implémentée dans `config/categories.py` (`_ensure_schema`), le fichier `categories` du contexte centralisé reste ouvert via `db.open_context_db` |
| 82 | -- [ ] T017 [P] [US2] Implémenter `list_known_categories()` dans `packages/regine-core/src/regine_core/config/categories.py` (dépend de T016) | 82 | +- [X] T017 [P] [US2] Implémenter `list_known_categories()` dans `packages/regine-core/src/regine_core/config/categories.py` (dépend de T016) |
| 83 | -- [ ] T018 [P] [US2] Implémenter `suggest_categories(saisie)` avec `difflib.get_close_matches` dans `packages/regine-core/src/regine_core/config/categories.py` (dépend de T016 ; cf. `research.md` § 2) | 83 | +- [X] T018 [P] [US2] Implémenter `suggest_categories(saisie)` avec `difflib.get_close_matches` dans `packages/regine-core/src/regine_core/config/categories.py` (dépend de T016 ; cf. `research.md` § 2) |
| 84 | -- [ ] T019 [US2] Implémenter `register_category_usage(nom)` (idempotent ; à n'appeler qu'après confirmation explicite d'archivage, cf. Principe II de la constitution) dans `packages/regine-core/src/regine_core/config/categories.py` (dépend de T016) | 84 | +- [X] T019 [US2] Implémenter `register_category_usage(nom)` (idempotent ; à n'appeler qu'après confirmation explicite d'archivage, cf. Principe II de la constitution) dans `packages/regine-core/src/regine_core/config/categories.py` (dépend de T016) |
| 85 | -- [ ] T020 [US2] Implémenter la branche catégorie de `determine_root` dans `root.py` (dépend de T012) | 85 | +- [X] T020 [US2] Implémenter la branche catégorie de `determine_root` dans `root.py` (dépend de T012) |
| 86 | -- [ ] T021 [US2] Test de non-régression : la signature de `determine_root` n'accepte qu'un seul paramètre `categorie` (jamais une liste) — garantit FR-005 (une seule catégorie à la fois) dans `test_root_resolution.py` | 86 | +- [X] T021 [US2] Test de non-régression : la signature de `determine_root` n'accepte qu'un seul paramètre `categorie` (jamais une liste) — garantit FR-005 (une seule catégorie à la fois) dans `test_root_resolution.py` |
| 87 | 87 | ||
| 88 | **Checkpoint**: User Stories 1 ET 2 fonctionnelles indépendamment. | 88 | **Checkpoint**: User Stories 1 ET 2 fonctionnelles indépendamment. |
| 89 | 89 | ||
| @@ -97,11 +97,11 @@ Aucun code n'existe encore dans ce dépôt (seuls `docs/`, `specs/`, `.specify/` | |||
| 97 | 97 | ||
| 98 | ### Tests for User Story 3 | 98 | ### Tests for User Story 3 |
| 99 | 99 | ||
| 100 | -- [ ] T022 [P] [US3] Test d'intégration : le `RootLocation` d'un dossier parent est réutilisé tel quel pour un sous-dossier, sans nouvel appel à `determine_root`, dans `packages/regine-core/tests/integration/test_root_inheritance.py` | 100 | +- [X] T022 [P] [US3] Test d'intégration : le `RootLocation` d'un dossier parent est réutilisé tel quel pour un sous-dossier, sans nouvel appel à `determine_root`, dans `packages/regine-core/tests/integration/test_root_inheritance.py` |
| 101 | 101 | ||
| 102 | ### Implementation for User Story 3 | 102 | ### Implementation for User Story 3 |
| 103 | 103 | ||
| 104 | -- [ ] T023 [US3] Documenter (docstring) dans `root.py` que `determine_root` ne DOIT être appelée qu'une seule fois par dossier parent — le futur code appelant (import, `specs/001-import-photos`) réutilise l'objet `RootLocation` déjà résolu pour tout sous-dossier ; aucune nouvelle logique de calcul requise ici | 104 | +- [X] T023 [US3] Documenter (docstring) dans `root.py` que `determine_root` ne DOIT être appelée qu'une seule fois par dossier parent — le futur code appelant (import, `specs/001-import-photos`) réutilise l'objet `RootLocation` déjà résolu pour tout sous-dossier ; aucune nouvelle logique de calcul requise ici |
| 105 | 105 | ||
| 106 | **Checkpoint**: User Stories 1, 2 et 3 fonctionnelles indépendamment. | 106 | **Checkpoint**: User Stories 1, 2 et 3 fonctionnelles indépendamment. |
| 107 | 107 | ||
| @@ -113,8 +113,8 @@ Aucun code n'existe encore dans ce dépôt (seuls `docs/`, `specs/`, `.specify/` | |||
| 113 | 113 | ||
| 114 | **Statut** : **Mise à jour du 2026-09-19** — le mécanisme de détection de déplacement par somme de contrôle a désormais sa propre spec et son propre plan (`specs/005-checkout-reconciliation`, dont FR-010 couvre exactement ce cas). Ce n'est plus une dépendance bloquante au niveau spécification : seulement une dépendance d'ordonnancement (l'implémentation de `specs/005` doit précéder la validation de bout en bout de cette user story). | 114 | **Statut** : **Mise à jour du 2026-09-19** — le mécanisme de détection de déplacement par somme de contrôle a désormais sa propre spec et son propre plan (`specs/005-checkout-reconciliation`, dont FR-010 couvre exactement ce cas). Ce n'est plus une dépendance bloquante au niveau spécification : seulement une dépendance d'ordonnancement (l'implémentation de `specs/005` doit précéder la validation de bout en bout de cette user story). |
| 115 | 115 | ||
| 116 | -- [ ] T024 [US4] Documenter (docstring) dans `packages/regine-core/src/regine_core/dossier/root.py` qu'il n'existe aucune opération d'écriture directe sur `RootLocation` : tout changement de répertoire racine passe exclusivement par `regine_core.archive.reconciliation.comparer` (`specs/005-checkout-reconciliation`), qui classe un déplacement de dossier entier comme `deplacement` sans code supplémentaire ici | 116 | +- [X] T024 [US4] Documenter (docstring) dans `packages/regine-core/src/regine_core/dossier/root.py` qu'il n'existe aucune opération d'écriture directe sur `RootLocation` : tout changement de répertoire racine passe exclusivement par `regine_core.archive.reconciliation.comparer` (`specs/005-checkout-reconciliation`), qui classe un déplacement de dossier entier comme `deplacement` sans code supplémentaire ici |
| 117 | -- [ ] T024b [US4] Test d'intégration (à exécuter une fois `specs/005-checkout-reconciliation` implémentée) : déplacer un dossier de travail entre deux répertoires racine (année → catégorie) et vérifier que `regine_core.archive.reconciliation.comparer` le classe `deplacement`, dans `packages/regine-core/tests/integration/test_root_recategorisation.py` | 117 | +- [ ] T024b [US4] Test d'intégration (à exécuter une fois `specs/005-checkout-reconciliation` implémentée) : déplacer un dossier de travail entre deux répertoires racine (année → catégorie) et vérifier que `regine_core.archive.reconciliation.comparer` le classe `deplacement`, dans `packages/regine-core/tests/integration/test_root_recategorisation.py` — **toujours en attente, `specs/005` pas encore implémentée** |
| 118 | 118 | ||
| 119 | **Checkpoint**: US4 documentée et prête à être validée dès que `specs/005-checkout-reconciliation` est implémentée — plus aucune spécification manquante. | 119 | **Checkpoint**: US4 documentée et prête à être validée dès que `specs/005-checkout-reconciliation` est implémentée — plus aucune spécification manquante. |
| 120 | 120 | ||
| @@ -122,9 +122,9 @@ Aucun code n'existe encore dans ce dépôt (seuls `docs/`, `specs/`, `.specify/` | |||
| 122 | 122 | ||
| 123 | ## Phase 7: Polish & Cross-Cutting Concerns | 123 | ## Phase 7: Polish & Cross-Cutting Concerns |
| 124 | 124 | ||
| 125 | -- [ ] T025 [P] Exécuter manuellement les scénarios 1 à 4 de `specs/004-categorisation-dossiers/quickstart.md` et consigner le résultat | 125 | +- [X] T025 [P] Exécuter manuellement les scénarios 1 à 4 de `specs/004-categorisation-dossiers/quickstart.md` et consigner le résultat — **2026-09-19 : les 4 scénarios passent** (déroulés via `uv run python3`, cf. session d'implémentation) |
| 126 | -- [ ] T026 [P] `ruff check --fix` sur `packages/regine-core` | 126 | +- [X] T026 [P] `ruff check --fix` sur `packages/regine-core` — 0 erreur restante, code formaté |
| 127 | -- [ ] T027 Mettre à jour `specs/004-categorisation-dossiers/contracts/regine-core-api.md` pour refléter la signature définitive de `determine_root` (paramètres `archive_root`/`local_root` explicites, cf. Note en tête de ce fichier) | 127 | +- [X] T027 Mettre à jour `specs/004-categorisation-dossiers/contracts/regine-core-api.md` pour refléter la signature définitive de `determine_root` (paramètres `archive_root`/`local_root` explicites, cf. Note en tête de ce fichier) |
| 128 | 128 | ||
| 129 | --- | 129 | --- |
| 130 | 130 | ||
added
uv.lock +141 -0 | new file mode 100644 | ||
| @@ -0,0 +1,141 @@ | ||
| 1 | +version = 1 | |
| 2 | +revision = 3 | |
| 3 | +requires-python = ">=3.11" | |
| 4 | + | |
| 5 | +[manifest] | |
| 6 | +members = [ | |
| 7 | + "regine-agent", | |
| 8 | + "regine-cli", | |
| 9 | + "regine-core", | |
| 10 | + "regine-gui", | |
| 11 | +] | |
| 12 | + | |
| 13 | +[manifest.dependency-groups] | |
| 14 | +dev = [ | |
| 15 | + { name = "pytest", specifier = ">=9.1.1" }, | |
| 16 | + { name = "ruff", specifier = ">=0.16.8" }, | |
| 17 | +] | |
| 18 | + | |
| 19 | +[[package]] | |
| 20 | +name = "colorama" | |
| 21 | +version = "0.4.6" | |
| 22 | +source = { registry = "https://pypi.org/simple" } | |
| 23 | +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } | |
| 24 | +wheels = [ | |
| 25 | + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, | |
| 26 | +] | |
| 27 | + | |
| 28 | +[[package]] | |
| 29 | +name = "iniconfig" | |
| 30 | +version = "2.3.0" | |
| 31 | +source = { registry = "https://pypi.org/simple" } | |
| 32 | +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } | |
| 33 | +wheels = [ | |
| 34 | + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, | |
| 35 | +] | |
| 36 | + | |
| 37 | +[[package]] | |
| 38 | +name = "packaging" | |
| 39 | +version = "26.3" | |
| 40 | +source = { registry = "https://pypi.org/simple" } | |
| 41 | +sdist = { url = "https://files.pythonhosted.org/packages/7d/fa/3944b40b07da9ce895c0e6303a5ab7d53da063554f534556b134a54d6093/packaging-26.3.tar.gz", hash = "sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79", size = 313412, upload-time = "2026-08-04T18:15:28.737Z" } | |
| 42 | +wheels = [ | |
| 43 | + { url = "https://files.pythonhosted.org/packages/63/34/ba1c580383c9eada3711951fef0795c80b829a078d72188184bcab9dd527/packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c", size = 129956, upload-time = "2026-08-04T18:15:27.159Z" }, | |
| 44 | +] | |
| 45 | + | |
| 46 | +[[package]] | |
| 47 | +name = "pluggy" | |
| 48 | +version = "1.6.0" | |
| 49 | +source = { registry = "https://pypi.org/simple" } | |
| 50 | +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } | |
| 51 | +wheels = [ | |
| 52 | + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, | |
| 53 | +] | |
| 54 | + | |
| 55 | +[[package]] | |
| 56 | +name = "pygments" | |
| 57 | +version = "2.21.0" | |
| 58 | +source = { registry = "https://pypi.org/simple" } | |
| 59 | +sdist = { url = "https://files.pythonhosted.org/packages/49/2e/ced460408999b33da6b31b0021b0f37d329e202d4169aeb164493778f25b/pygments-2.21.0.tar.gz", hash = "sha256:610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c", size = 5005329, upload-time = "2026-08-17T08:02:48.824Z" } | |
| 60 | +wheels = [ | |
| 61 | + { url = "https://files.pythonhosted.org/packages/71/46/17f022dd3e953bf20a04a028a21ec746d942f8d2af30fa0f124fa0e6a684/pygments-2.21.0-py3-none-any.whl", hash = "sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9", size = 1250147, upload-time = "2026-08-17T08:02:44.912Z" }, | |
| 62 | +] | |
| 63 | + | |
| 64 | +[[package]] | |
| 65 | +name = "pytest" | |
| 66 | +version = "9.1.1" | |
| 67 | +source = { registry = "https://pypi.org/simple" } | |
| 68 | +dependencies = [ | |
| 69 | + { name = "colorama", marker = "sys_platform == 'win32'" }, | |
| 70 | + { name = "iniconfig" }, | |
| 71 | + { name = "packaging" }, | |
| 72 | + { name = "pluggy" }, | |
| 73 | + { name = "pygments" }, | |
| 74 | +] | |
| 75 | +sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" } | |
| 76 | +wheels = [ | |
| 77 | + { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" }, | |
| 78 | +] | |
| 79 | + | |
| 80 | +[[package]] | |
| 81 | +name = "regine-agent" | |
| 82 | +version = "0.1.0" | |
| 83 | +source = { editable = "packages/regine-agent" } | |
| 84 | +dependencies = [ | |
| 85 | + { name = "regine-core" }, | |
| 86 | +] | |
| 87 | + | |
| 88 | +[package.metadata] | |
| 89 | +requires-dist = [{ name = "regine-core", editable = "packages/regine-core" }] | |
| 90 | + | |
| 91 | +[[package]] | |
| 92 | +name = "regine-cli" | |
| 93 | +version = "0.1.0" | |
| 94 | +source = { editable = "packages/regine-cli" } | |
| 95 | +dependencies = [ | |
| 96 | + { name = "regine-core" }, | |
| 97 | +] | |
| 98 | + | |
| 99 | +[package.metadata] | |
| 100 | +requires-dist = [{ name = "regine-core", editable = "packages/regine-core" }] | |
| 101 | + | |
| 102 | +[[package]] | |
| 103 | +name = "regine-core" | |
| 104 | +version = "0.1.0" | |
| 105 | +source = { editable = "packages/regine-core" } | |
| 106 | + | |
| 107 | +[[package]] | |
| 108 | +name = "regine-gui" | |
| 109 | +version = "0.1.0" | |
| 110 | +source = { editable = "packages/regine-gui" } | |
| 111 | +dependencies = [ | |
| 112 | + { name = "regine-core" }, | |
| 113 | +] | |
| 114 | + | |
| 115 | +[package.metadata] | |
| 116 | +requires-dist = [{ name = "regine-core", editable = "packages/regine-core" }] | |
| 117 | + | |
| 118 | +[[package]] | |
| 119 | +name = "ruff" | |
| 120 | +version = "0.16.8" | |
| 121 | +source = { registry = "https://pypi.org/simple" } | |
| 122 | +sdist = { url = "https://files.pythonhosted.org/packages/ba/78/449cb84790bd5cc3823b2652ee405a4558856e5c4195aee3a16bf7b3eb5d/ruff-0.16.8.tar.gz", hash = "sha256:9247bf92b5f04d825c8639a4fe423ec2e4222acd9222e58412b0dab7e442798b", size = 4938814, upload-time = "2026-09-16T15:54:46.688Z" } | |
| 123 | +wheels = [ | |
| 124 | + { url = "https://files.pythonhosted.org/packages/ac/25/6071aabc530e9be7e2c195e8fe3f7aea2735405b6cf447212832d7811831/ruff-0.16.8-py3-none-linux_armv6l.whl", hash = "sha256:6ffbd6d87383c1edf5f6fa890f10200950240d7c1a16052a19a09d3a2307dd38", size = 10048966, upload-time = "2026-09-16T15:53:57.605Z" }, | |
| 125 | + { url = "https://files.pythonhosted.org/packages/54/98/07f90ecbc74dd5fb5764f11f2bc774d6a7cffef92d2ff5f5b4e9e23c754e/ruff-0.16.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:42ed6b878ed61e3acca92f2730a17acff39286944ea82398544696366a6f925e", size = 10165498, upload-time = "2026-09-16T15:54:01.14Z" }, | |
| 126 | + { url = "https://files.pythonhosted.org/packages/fe/1f/e6a712e3b47cad4a40600134105ed193cb773f618a42eb7ba323cb812cc0/ruff-0.16.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:7ea781c7f2afba8c6a505ea0fb3f994020249e0c450635f5381286fea6b46170", size = 9830004, upload-time = "2026-09-16T15:54:03.998Z" }, | |
| 127 | + { url = "https://files.pythonhosted.org/packages/23/f2/311a08776d75d81c7676e20b6b020ae63cbe881fcdc7a8dd64e6e18bdd93/ruff-0.16.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8efeae3bbe414a5efefda11a792dfb51ef90ac48d50c4830de2f644caf3e8659", size = 9986558, upload-time = "2026-09-16T15:54:06.804Z" }, | |
| 128 | + { url = "https://files.pythonhosted.org/packages/f3/ed/37b6cb3d3ba8c73e68ae3eb1d502383beb5aa05a582bb7bb3a922f929f54/ruff-0.16.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a79b795469fef7fc6e908b218eed2eb17332afd85031db6480dc864560e69b2", size = 9877332, upload-time = "2026-09-16T15:54:09.552Z" }, | |
| 129 | + { url = "https://files.pythonhosted.org/packages/22/cc/40873a8f36ad084cc540d55fcca7077264d5b13b24659e9180c176fb2b08/ruff-0.16.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3fdc5563cdc50555e6fba39322850860e9267c1b3d12c26a74729d8604c3c812", size = 10507125, upload-time = "2026-09-16T15:54:12.152Z" }, | |
| 130 | + { url = "https://files.pythonhosted.org/packages/c3/e4/fc91a642b78ccbab6b9477720f3644ae7a10a9bcce69a934679cd64f62bc/ruff-0.16.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:34508983c70665578dab88f5223d8e6228307e1135398ca8bfc8b7e9501e282b", size = 11336694, upload-time = "2026-09-16T15:54:15.489Z" }, | |
| 131 | + { url = "https://files.pythonhosted.org/packages/c2/3d/bbd2a9a600a4e73dc3e7548a249c8d1671273464b55822c6fae50f602dff/ruff-0.16.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:644bb578569e0ffc575741232bd385dacdd6fbe123f1a729e7a225f54aa3957f", size = 10774448, upload-time = "2026-09-16T15:54:18.16Z" }, | |
| 132 | + { url = "https://files.pythonhosted.org/packages/1a/41/d83af9879a7b6e8bf5fe16b1da0b134049d2f5d3afac12defb0897cb84bd/ruff-0.16.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15e7d226246961db9235098333caa13063906d3851136b84c2900b82f5daa1df", size = 10323796, upload-time = "2026-09-16T15:54:20.743Z" }, | |
| 133 | + { url = "https://files.pythonhosted.org/packages/f5/2c/cefd07bfe914b84943ea769ade8d607bd22750b965d3228eefd7cebd15d0/ruff-0.16.8-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:a2bf6bc3e9ebdd4449abc6f06cf64b98051a2c61cf94d2fe9596518c881f1a1e", size = 10514115, upload-time = "2026-09-16T15:54:23.497Z" }, | |
| 134 | + { url = "https://files.pythonhosted.org/packages/f3/9d/76a2e26c79a23be6e6e3664c57bec9e9fc8de155cfb9e4b67ea91b64f9d7/ruff-0.16.8-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:6ca111ba0849539165e9e59d2b442542f3c1e8060ebbdea82494f1ffbccb1e1f", size = 10072582, upload-time = "2026-09-16T15:54:26.185Z" }, | |
| 135 | + { url = "https://files.pythonhosted.org/packages/2e/d4/f42edddb39668af1a559ceafa3823aedd65633a48dc9768e775485faa2c1/ruff-0.16.8-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:359a1e5b495448ee1e91018064382ebc86f90e8aac2fed222c7d0e4e8df85fd2", size = 9879644, upload-time = "2026-09-16T15:54:29.278Z" }, | |
| 136 | + { url = "https://files.pythonhosted.org/packages/f8/d4/913e3195d95e0378786c6656945c865f534a3560e29139da4882aff630d1/ruff-0.16.8-py3-none-musllinux_1_2_i686.whl", hash = "sha256:59e8f5681349474110b24d62e93cfda6593f5fa3473446ca3705200cac1a08b9", size = 10231569, upload-time = "2026-09-16T15:54:32.036Z" }, | |
| 137 | + { url = "https://files.pythonhosted.org/packages/2b/c4/8aa6ea0bdcedbd1bf87397e2fc4ed8406448ea5842f8660bc6e5f163039d/ruff-0.16.8-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:efa3e7a16d1baaa79957888dfdf8be9ef2e44db81cb032af06d76632ab59e773", size = 10663666, upload-time = "2026-09-16T15:54:34.838Z" }, | |
| 138 | + { url = "https://files.pythonhosted.org/packages/3d/02/7f10ef4700bc223c30a3fdd10631a29830c45524b810a3c7ed947af64591/ruff-0.16.8-py3-none-win32.whl", hash = "sha256:55793ba85c69921e89be061426d91a78652d6e50317c962240922747a4eb713f", size = 10093472, upload-time = "2026-09-16T15:54:37.47Z" }, | |
| 139 | + { url = "https://files.pythonhosted.org/packages/1e/5d/a509c07d714b6da88f2c518b4637cf6f1d46b074be8f0f1e5fb9ff5126fe/ruff-0.16.8-py3-none-win_amd64.whl", hash = "sha256:a6b85621fd3c81e31fc5f5add09c9c078b430db3595ca632efafdec9e64ebfaa", size = 10586899, upload-time = "2026-09-16T15:54:40.488Z" }, | |
| 140 | + { url = "https://files.pythonhosted.org/packages/fe/a0/50787329e4f20bf9dc9f6230015d46ec69c51a97ace5bc202dae4755365d/ruff-0.16.8-py3-none-win_arm64.whl", hash = "sha256:d075e820af612102ce217f07cc93e69f9490b10ec13ea85fa87bd03d996cef8a", size = 10386316, upload-time = "2026-09-16T15:54:43.332Z" }, | |
| 141 | +] | |
| new file mode 100644 | |||
| @@ -0,0 +1,141 @@ | |||
| 1 | +version = 1 | ||
| 2 | +revision = 3 | ||
| 3 | +requires-python = ">=3.11" | ||
| 4 | + | ||
| 5 | +[manifest] | ||
| 6 | +members = [ | ||
| 7 | + "regine-agent", | ||
| 8 | + "regine-cli", | ||
| 9 | + "regine-core", | ||
| 10 | + "regine-gui", | ||
| 11 | +] | ||
| 12 | + | ||
| 13 | +[manifest.dependency-groups] | ||
| 14 | +dev = [ | ||
| 15 | + { name = "pytest", specifier = ">=9.1.1" }, | ||
| 16 | + { name = "ruff", specifier = ">=0.16.8" }, | ||
| 17 | +] | ||
| 18 | + | ||
| 19 | +[[package]] | ||
| 20 | +name = "colorama" | ||
| 21 | +version = "0.4.6" | ||
| 22 | +source = { registry = "https://pypi.org/simple" } | ||
| 23 | +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } | ||
| 24 | +wheels = [ | ||
| 25 | + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, | ||
| 26 | +] | ||
| 27 | + | ||
| 28 | +[[package]] | ||
| 29 | +name = "iniconfig" | ||
| 30 | +version = "2.3.0" | ||
| 31 | +source = { registry = "https://pypi.org/simple" } | ||
| 32 | +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } | ||
| 33 | +wheels = [ | ||
| 34 | + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, | ||
| 35 | +] | ||
| 36 | + | ||
| 37 | +[[package]] | ||
| 38 | +name = "packaging" | ||
| 39 | +version = "26.3" | ||
| 40 | +source = { registry = "https://pypi.org/simple" } | ||
| 41 | +sdist = { url = "https://files.pythonhosted.org/packages/7d/fa/3944b40b07da9ce895c0e6303a5ab7d53da063554f534556b134a54d6093/packaging-26.3.tar.gz", hash = "sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79", size = 313412, upload-time = "2026-08-04T18:15:28.737Z" } | ||
| 42 | +wheels = [ | ||
| 43 | + { url = "https://files.pythonhosted.org/packages/63/34/ba1c580383c9eada3711951fef0795c80b829a078d72188184bcab9dd527/packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c", size = 129956, upload-time = "2026-08-04T18:15:27.159Z" }, | ||
| 44 | +] | ||
| 45 | + | ||
| 46 | +[[package]] | ||
| 47 | +name = "pluggy" | ||
| 48 | +version = "1.6.0" | ||
| 49 | +source = { registry = "https://pypi.org/simple" } | ||
| 50 | +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } | ||
| 51 | +wheels = [ | ||
| 52 | + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, | ||
| 53 | +] | ||
| 54 | + | ||
| 55 | +[[package]] | ||
| 56 | +name = "pygments" | ||
| 57 | +version = "2.21.0" | ||
| 58 | +source = { registry = "https://pypi.org/simple" } | ||
| 59 | +sdist = { url = "https://files.pythonhosted.org/packages/49/2e/ced460408999b33da6b31b0021b0f37d329e202d4169aeb164493778f25b/pygments-2.21.0.tar.gz", hash = "sha256:610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c", size = 5005329, upload-time = "2026-08-17T08:02:48.824Z" } | ||
| 60 | +wheels = [ | ||
| 61 | + { url = "https://files.pythonhosted.org/packages/71/46/17f022dd3e953bf20a04a028a21ec746d942f8d2af30fa0f124fa0e6a684/pygments-2.21.0-py3-none-any.whl", hash = "sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9", size = 1250147, upload-time = "2026-08-17T08:02:44.912Z" }, | ||
| 62 | +] | ||
| 63 | + | ||
| 64 | +[[package]] | ||
| 65 | +name = "pytest" | ||
| 66 | +version = "9.1.1" | ||
| 67 | +source = { registry = "https://pypi.org/simple" } | ||
| 68 | +dependencies = [ | ||
| 69 | + { name = "colorama", marker = "sys_platform == 'win32'" }, | ||
| 70 | + { name = "iniconfig" }, | ||
| 71 | + { name = "packaging" }, | ||
| 72 | + { name = "pluggy" }, | ||
| 73 | + { name = "pygments" }, | ||
| 74 | +] | ||
| 75 | +sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" } | ||
| 76 | +wheels = [ | ||
| 77 | + { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" }, | ||
| 78 | +] | ||
| 79 | + | ||
| 80 | +[[package]] | ||
| 81 | +name = "regine-agent" | ||
| 82 | +version = "0.1.0" | ||
| 83 | +source = { editable = "packages/regine-agent" } | ||
| 84 | +dependencies = [ | ||
| 85 | + { name = "regine-core" }, | ||
| 86 | +] | ||
| 87 | + | ||
| 88 | +[package.metadata] | ||
| 89 | +requires-dist = [{ name = "regine-core", editable = "packages/regine-core" }] | ||
| 90 | + | ||
| 91 | +[[package]] | ||
| 92 | +name = "regine-cli" | ||
| 93 | +version = "0.1.0" | ||
| 94 | +source = { editable = "packages/regine-cli" } | ||
| 95 | +dependencies = [ | ||
| 96 | + { name = "regine-core" }, | ||
| 97 | +] | ||
| 98 | + | ||
| 99 | +[package.metadata] | ||
| 100 | +requires-dist = [{ name = "regine-core", editable = "packages/regine-core" }] | ||
| 101 | + | ||
| 102 | +[[package]] | ||
| 103 | +name = "regine-core" | ||
| 104 | +version = "0.1.0" | ||
| 105 | +source = { editable = "packages/regine-core" } | ||
| 106 | + | ||
| 107 | +[[package]] | ||
| 108 | +name = "regine-gui" | ||
| 109 | +version = "0.1.0" | ||
| 110 | +source = { editable = "packages/regine-gui" } | ||
| 111 | +dependencies = [ | ||
| 112 | + { name = "regine-core" }, | ||
| 113 | +] | ||
| 114 | + | ||
| 115 | +[package.metadata] | ||
| 116 | +requires-dist = [{ name = "regine-core", editable = "packages/regine-core" }] | ||
| 117 | + | ||
| 118 | +[[package]] | ||
| 119 | +name = "ruff" | ||
| 120 | +version = "0.16.8" | ||
| 121 | +source = { registry = "https://pypi.org/simple" } | ||
| 122 | +sdist = { url = "https://files.pythonhosted.org/packages/ba/78/449cb84790bd5cc3823b2652ee405a4558856e5c4195aee3a16bf7b3eb5d/ruff-0.16.8.tar.gz", hash = "sha256:9247bf92b5f04d825c8639a4fe423ec2e4222acd9222e58412b0dab7e442798b", size = 4938814, upload-time = "2026-09-16T15:54:46.688Z" } | ||
| 123 | +wheels = [ | ||
| 124 | + { url = "https://files.pythonhosted.org/packages/ac/25/6071aabc530e9be7e2c195e8fe3f7aea2735405b6cf447212832d7811831/ruff-0.16.8-py3-none-linux_armv6l.whl", hash = "sha256:6ffbd6d87383c1edf5f6fa890f10200950240d7c1a16052a19a09d3a2307dd38", size = 10048966, upload-time = "2026-09-16T15:53:57.605Z" }, | ||
| 125 | + { url = "https://files.pythonhosted.org/packages/54/98/07f90ecbc74dd5fb5764f11f2bc774d6a7cffef92d2ff5f5b4e9e23c754e/ruff-0.16.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:42ed6b878ed61e3acca92f2730a17acff39286944ea82398544696366a6f925e", size = 10165498, upload-time = "2026-09-16T15:54:01.14Z" }, | ||
| 126 | + { url = "https://files.pythonhosted.org/packages/fe/1f/e6a712e3b47cad4a40600134105ed193cb773f618a42eb7ba323cb812cc0/ruff-0.16.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:7ea781c7f2afba8c6a505ea0fb3f994020249e0c450635f5381286fea6b46170", size = 9830004, upload-time = "2026-09-16T15:54:03.998Z" }, | ||
| 127 | + { url = "https://files.pythonhosted.org/packages/23/f2/311a08776d75d81c7676e20b6b020ae63cbe881fcdc7a8dd64e6e18bdd93/ruff-0.16.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8efeae3bbe414a5efefda11a792dfb51ef90ac48d50c4830de2f644caf3e8659", size = 9986558, upload-time = "2026-09-16T15:54:06.804Z" }, | ||
| 128 | + { url = "https://files.pythonhosted.org/packages/f3/ed/37b6cb3d3ba8c73e68ae3eb1d502383beb5aa05a582bb7bb3a922f929f54/ruff-0.16.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a79b795469fef7fc6e908b218eed2eb17332afd85031db6480dc864560e69b2", size = 9877332, upload-time = "2026-09-16T15:54:09.552Z" }, | ||
| 129 | + { url = "https://files.pythonhosted.org/packages/22/cc/40873a8f36ad084cc540d55fcca7077264d5b13b24659e9180c176fb2b08/ruff-0.16.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3fdc5563cdc50555e6fba39322850860e9267c1b3d12c26a74729d8604c3c812", size = 10507125, upload-time = "2026-09-16T15:54:12.152Z" }, | ||
| 130 | + { url = "https://files.pythonhosted.org/packages/c3/e4/fc91a642b78ccbab6b9477720f3644ae7a10a9bcce69a934679cd64f62bc/ruff-0.16.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:34508983c70665578dab88f5223d8e6228307e1135398ca8bfc8b7e9501e282b", size = 11336694, upload-time = "2026-09-16T15:54:15.489Z" }, | ||
| 131 | + { url = "https://files.pythonhosted.org/packages/c2/3d/bbd2a9a600a4e73dc3e7548a249c8d1671273464b55822c6fae50f602dff/ruff-0.16.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:644bb578569e0ffc575741232bd385dacdd6fbe123f1a729e7a225f54aa3957f", size = 10774448, upload-time = "2026-09-16T15:54:18.16Z" }, | ||
| 132 | + { url = "https://files.pythonhosted.org/packages/1a/41/d83af9879a7b6e8bf5fe16b1da0b134049d2f5d3afac12defb0897cb84bd/ruff-0.16.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15e7d226246961db9235098333caa13063906d3851136b84c2900b82f5daa1df", size = 10323796, upload-time = "2026-09-16T15:54:20.743Z" }, | ||
| 133 | + { url = "https://files.pythonhosted.org/packages/f5/2c/cefd07bfe914b84943ea769ade8d607bd22750b965d3228eefd7cebd15d0/ruff-0.16.8-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:a2bf6bc3e9ebdd4449abc6f06cf64b98051a2c61cf94d2fe9596518c881f1a1e", size = 10514115, upload-time = "2026-09-16T15:54:23.497Z" }, | ||
| 134 | + { url = "https://files.pythonhosted.org/packages/f3/9d/76a2e26c79a23be6e6e3664c57bec9e9fc8de155cfb9e4b67ea91b64f9d7/ruff-0.16.8-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:6ca111ba0849539165e9e59d2b442542f3c1e8060ebbdea82494f1ffbccb1e1f", size = 10072582, upload-time = "2026-09-16T15:54:26.185Z" }, | ||
| 135 | + { url = "https://files.pythonhosted.org/packages/2e/d4/f42edddb39668af1a559ceafa3823aedd65633a48dc9768e775485faa2c1/ruff-0.16.8-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:359a1e5b495448ee1e91018064382ebc86f90e8aac2fed222c7d0e4e8df85fd2", size = 9879644, upload-time = "2026-09-16T15:54:29.278Z" }, | ||
| 136 | + { url = "https://files.pythonhosted.org/packages/f8/d4/913e3195d95e0378786c6656945c865f534a3560e29139da4882aff630d1/ruff-0.16.8-py3-none-musllinux_1_2_i686.whl", hash = "sha256:59e8f5681349474110b24d62e93cfda6593f5fa3473446ca3705200cac1a08b9", size = 10231569, upload-time = "2026-09-16T15:54:32.036Z" }, | ||
| 137 | + { url = "https://files.pythonhosted.org/packages/2b/c4/8aa6ea0bdcedbd1bf87397e2fc4ed8406448ea5842f8660bc6e5f163039d/ruff-0.16.8-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:efa3e7a16d1baaa79957888dfdf8be9ef2e44db81cb032af06d76632ab59e773", size = 10663666, upload-time = "2026-09-16T15:54:34.838Z" }, | ||
| 138 | + { url = "https://files.pythonhosted.org/packages/3d/02/7f10ef4700bc223c30a3fdd10631a29830c45524b810a3c7ed947af64591/ruff-0.16.8-py3-none-win32.whl", hash = "sha256:55793ba85c69921e89be061426d91a78652d6e50317c962240922747a4eb713f", size = 10093472, upload-time = "2026-09-16T15:54:37.47Z" }, | ||
| 139 | + { url = "https://files.pythonhosted.org/packages/1e/5d/a509c07d714b6da88f2c518b4637cf6f1d46b074be8f0f1e5fb9ff5126fe/ruff-0.16.8-py3-none-win_amd64.whl", hash = "sha256:a6b85621fd3c81e31fc5f5add09c9c078b430db3595ca632efafdec9e64ebfaa", size = 10586899, upload-time = "2026-09-16T15:54:40.488Z" }, | ||
| 140 | + { url = "https://files.pythonhosted.org/packages/fe/a0/50787329e4f20bf9dc9f6230015d46ec69c51a97ace5bc202dae4755365d/ruff-0.16.8-py3-none-win_arm64.whl", hash = "sha256:d075e820af612102ce217f07cc93e69f9490b10ec13ea85fa87bd03d996cef8a", size = 10386316, upload-time = "2026-09-16T15:54:43.332Z" }, | ||
| 141 | +] | ||