fonzarely/regine-photos-archiverpublic⑂ Fork 0
⑂ faceb9d
Commits
⬇ Clone ▾
git clone https://git.rickub.com/fonzarely/regine-photos-archiver.git
git clone ssh://git@rickub.com/fonzarely/regine-photos-archiver.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

implement

Fabien Champigny committed 2026-09-19T14:54:03+02:00 Browse files
faceb9d parent: 26c1e45
modified .gitignore +10 -0
@@ -1,2 +1,12 @@
11 .claude/
22 .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_Store2 .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 @@
22
33 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).
44
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`
66
77 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).
88
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+
911 **Entrée** :
1012 - `date` : date EXIF (ou plage) du groupe d'import concerné.
1113 - `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`).
1216
1317 **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.
1418
@@ -16,24 +20,26 @@ Résout le répertoire racine (année ou catégorie) pour un dossier ou dossier
1620 - 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.
1721 - `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).
1822
19-## `regine_core.config.categories.list_known_categories() -> list[str]`
23+## `regine_core.config.categories.list_known_categories(conn: sqlite3.Connection) -> list[str]`
2024
2125 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).
2226
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]`
2430
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).
2632
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`
2834
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).
3036
3137 ## Point d'intégration côté façade (futur, hors périmètre de ce plan)
3238
3339 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.
3844
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/`
2222
2323 **Purpose**: Initialisation du monorepo — aucun paquet n'existe encore.
2424
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/`)
3131
3232 ---
3333
@@ -37,10 +37,10 @@ Aucun code n'existe encore dans ce dépôt (seuls `docs/`, `specs/`, `.specify/`
3737
3838 **⚠️ CRITICAL**: Aucune user story ne peut être implémentée avant la fin de cette phase.
3939
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)
4444
4545 **Checkpoint**: Fondations prêtes — les phases User Story peuvent commencer.
4646
@@ -54,12 +54,12 @@ Aucun code n'existe encore dans ce dépôt (seuls `docs/`, `specs/`, `.specify/`
5454
5555 ### Tests for User Story 1
5656
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`
5858
5959 ### Implementation for User Story 1
6060
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
6363
6464 **Checkpoint**: User Story 1 fonctionnelle et testable indépendamment (MVP).
6565
@@ -73,17 +73,17 @@ Aucun code n'existe encore dans ce dépôt (seuls `docs/`, `specs/`, `.specify/`
7373
7474 ### Tests for User Story 2
7575
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)
7878
7979 ### Implementation for User Story 2
8080
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`
8787
8888 **Checkpoint**: User Stories 1 ET 2 fonctionnelles indépendamment.
8989
@@ -97,11 +97,11 @@ Aucun code n'existe encore dans ce dépôt (seuls `docs/`, `specs/`, `.specify/`
9797
9898 ### Tests for User Story 3
9999
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`
101101
102102 ### Implementation for User Story 3
103103
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
105105
106106 **Checkpoint**: User Stories 1, 2 et 3 fonctionnelles indépendamment.
107107
@@ -113,8 +113,8 @@ Aucun code n'existe encore dans ce dépôt (seuls `docs/`, `specs/`, `.specify/`
113113
114114 **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).
115115
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**
118118
119119 **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.
120120
@@ -122,9 +122,9 @@ Aucun code n'existe encore dans ce dépôt (seuls `docs/`, `specs/`, `.specify/`
122122
123123 ## Phase 7: Polish & Cross-Cutting Concerns
124124
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)
128128
129129 ---
130130
@@ -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 workspace29+- [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 155 ### 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 159 ### 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 posteriori62+- [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 274 ### 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 279 ### 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 398 ### 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 3102 ### 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 ici104+- [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 ici116+- [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 Concerns123 ## 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ésultat125+- [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+]