fonzarely/regine-photos-archiverpublic⑂ Fork 0
⑂ 19cf29c
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.

refactor

Fabien Champigny committed 2026-09-24T11:54:52+02:00 Browse files
19cf29c parent: 7f47101
modified packages/regine-cli/src/regine_cli/archive_cmd.py +61 -5
@@ -2,6 +2,14 @@
22
33 Façade fine : orchestre `regine_core.archive`, formatte le résultat, ne contient
44 aucune logique métier propre (Principe VI de la constitution).
5+
6+**Révisé le 2026-09-23/24** (`specs/003-config-contexte-travail` FR-016/017) :
7+`reconcile` réplique la base de contexte centralisée vers l'archive après un
8+archivage réussi ; `checkout` la restaure depuis l'archive si absente
9+localement — toutes deux orchestrées ici plutôt que dans `regine_core.archive`
10+lui-même (aucune nouvelle dépendance inter-modules dans le cœur, cf.
11+`specs/003/research.md` § 7). `--contexte-db` (browse/restore) devient
12+`--config-db` (ne cible plus que les chemins).
513 """
614
715 from __future__ import annotations
@@ -17,6 +25,48 @@ from regine_core.archive.reconciliation import DecisionsUtilisateur, archiver, c
1725 from regine_core.archive.verrou import DossierDejaVerrouilleError, lever
1826
1927
28+def _charger_contexte_global(args: argparse.Namespace):
29+ from regine_core.config.context import charger_contexte # noqa: PLC0415
30+ from regine_core.config.db import open_global_config_db # noqa: PLC0415
31+
32+ conn = open_global_config_db(Path(args.config_db) if args.config_db else None)
33+ try:
34+ return charger_contexte(conn)
35+ finally:
36+ conn.close()
37+
38+
39+def _restaurer_contexte_post_checkout(args: argparse.Namespace) -> None:
40+ """FR-017 : après un checkout réussi, retrouve le contexte centralisée depuis
41+ l'archive si absente localement — no-op silencieux si les chemins de travail
42+ ne sont pas (encore) configurés (usage de `checkout` indépendant de `regine
43+ config`, cf. Assumptions de `specs/001`)."""
44+ from regine_core.config.context import restaurer_contexte_si_absent # noqa: PLC0415
45+ from regine_core.config.smb import chemin_local_du_partage # noqa: PLC0415
46+
47+ contexte = _charger_contexte_global(args)
48+ if contexte.local_work_dir is None or contexte.archive_smb_path is None:
49+ return
50+ archive_root_local = chemin_local_du_partage(contexte.archive_smb_path)
51+ if restaurer_contexte_si_absent(contexte.local_work_dir, archive_root_local):
52+ print("Contexte de travail restauré depuis l'archive.")
53+
54+
55+def _sauvegarder_contexte_post_reconcile(args: argparse.Namespace) -> None:
56+ """FR-016 : après un archivage réussi (y compris une réconciliation « à
57+ vide »), réplique la base de contexte centralisée vers l'archive — no-op
58+ silencieux si les chemins de travail ne sont pas configurés, ou si la base
59+ locale n'existe pas encore."""
60+ from regine_core.config.context import sauvegarder_contexte # noqa: PLC0415
61+ from regine_core.config.smb import chemin_local_du_partage # noqa: PLC0415
62+
63+ contexte = _charger_contexte_global(args)
64+ if contexte.local_work_dir is None or contexte.archive_smb_path is None:
65+ return
66+ archive_root_local = chemin_local_du_partage(contexte.archive_smb_path)
67+ sauvegarder_contexte(contexte.local_work_dir, archive_root_local)
68+
69+
2070 def _cmd_checkout(args: argparse.Namespace) -> int:
2171 dossier_archive = Path(args.dossier)
2272 dest_locale = Path(args.local_dest) if args.local_dest else Path.cwd() / dossier_archive.name
@@ -30,6 +80,7 @@ def _cmd_checkout(args: argparse.Namespace) -> int:
3080
3181 print(f"{len(snapshot.fichiers)} fichier(s) sortis vers {dest_locale}")
3282 snapshot.manifest.conn.close()
83+ _restaurer_contexte_post_checkout(args)
3384 return 0
3485
3586
@@ -83,9 +134,11 @@ def _cmd_reconcile(args: argparse.Namespace) -> int:
83134
84135 if not rapport.changements:
85136 # Rien à réconcilier : la session se termine sans écriture, mais le verrou
86- # doit être levé (FR-016 s'applique aussi à une réconciliation "à vide").
137+ # doit être levé (FR-016 de specs/005 s'applique aussi à une réconciliation
138+ # "à vide") — la réplication du contexte (FR-016 de specs/003) aussi.
87139 lever(manifest)
88140 manifest.conn.close()
141+ _sauvegarder_contexte_post_reconcile(args)
89142 return 0
90143
91144 decisions = DecisionsUtilisateur()
@@ -115,6 +168,7 @@ def _cmd_reconcile(args: argparse.Namespace) -> int:
115168 archiver(snapshot, rapport, decisions)
116169 print("Réconciliation terminée.")
117170 manifest.conn.close()
171+ _sauvegarder_contexte_post_reconcile(args)
118172 return 0
119173
120174
@@ -126,10 +180,10 @@ def _charger_archive_root_local(args: argparse.Namespace) -> Path | None:
126180 partagée avec `regine-gui` (Principe VI — cf. bug trouvé par test manuel de la
127181 GUI, specs/006 : deux résolutions dupliquées et non cohérentes entre elles)."""
128182 from regine_core.config.context import charger_contexte # noqa: PLC0415
129- from regine_core.config.db import open_context_db # noqa: PLC0415
183+ from regine_core.config.db import open_global_config_db # noqa: PLC0415
130184 from regine_core.config.smb import resoudre_archive_locale # noqa: PLC0415
131185
132- conn = open_context_db(Path(args.contexte_db) if args.contexte_db else None)
186+ conn = open_global_config_db(Path(args.config_db) if args.config_db else None)
133187 try:
134188 contexte = charger_contexte(conn)
135189 if contexte.archive_smb_path is None:
@@ -208,6 +262,7 @@ def construire_analyseur() -> argparse.ArgumentParser:
208262 checkout_parser.add_argument("dossier")
209263 checkout_parser.add_argument("--formats", default=None)
210264 checkout_parser.add_argument("--local-dest", default=None)
265+ checkout_parser.add_argument("--config-db", default=None)
211266 checkout_parser.set_defaults(func=_cmd_checkout)
212267
213268 reconcile_parser = sous_commandes.add_parser(
@@ -215,6 +270,7 @@ def construire_analyseur() -> argparse.ArgumentParser:
215270 )
216271 reconcile_parser.add_argument("dossier")
217272 reconcile_parser.add_argument("--local-dest", default=None)
273+ reconcile_parser.add_argument("--config-db", default=None)
218274 reconcile_parser.set_defaults(func=_cmd_reconcile)
219275
220276 browse_parser = sous_commandes.add_parser(
@@ -222,7 +278,7 @@ def construire_analyseur() -> argparse.ArgumentParser:
222278 )
223279 browse_parser.add_argument("--search", default=None)
224280 browse_parser.add_argument("--json", action="store_true")
225- browse_parser.add_argument("--contexte-db", default=None)
281+ browse_parser.add_argument("--config-db", default=None)
226282 browse_parser.set_defaults(func=_cmd_browse)
227283
228284 restore_parser = sous_commandes.add_parser(
@@ -230,7 +286,7 @@ def construire_analyseur() -> argparse.ArgumentParser:
230286 )
231287 restore_parser.add_argument("--chemin", required=True)
232288 restore_parser.add_argument("--destination", required=True)
233- restore_parser.add_argument("--contexte-db", default=None)
289+ restore_parser.add_argument("--config-db", default=None)
234290 restore_parser.set_defaults(func=_cmd_restore)
235291
236292 return analyseur
@@ -2,6 +2,14 @@
2 2
3 Façade fine : orchestre `regine_core.archive`, formatte le résultat, ne contient3 Façade fine : orchestre `regine_core.archive`, formatte le résultat, ne contient
4 aucune logique métier propre (Principe VI de la constitution).4 aucune logique métier propre (Principe VI de la constitution).
5+
6+**Révisé le 2026-09-23/24** (`specs/003-config-contexte-travail` FR-016/017) :
7+`reconcile` réplique la base de contexte centralisée vers l'archive après un
8+archivage réussi ; `checkout` la restaure depuis l'archive si absente
9+localement — toutes deux orchestrées ici plutôt que dans `regine_core.archive`
10+lui-même (aucune nouvelle dépendance inter-modules dans le cœur, cf.
11+`specs/003/research.md` § 7). `--contexte-db` (browse/restore) devient
12+`--config-db` (ne cible plus que les chemins).
5 """13 """
6 14
7 from __future__ import annotations15 from __future__ import annotations
@@ -17,6 +25,48 @@ from regine_core.archive.reconciliation import DecisionsUtilisateur, archiver, c
17 from regine_core.archive.verrou import DossierDejaVerrouilleError, lever25 from regine_core.archive.verrou import DossierDejaVerrouilleError, lever
18 26
19 27
28+def _charger_contexte_global(args: argparse.Namespace):
29+ from regine_core.config.context import charger_contexte # noqa: PLC0415
30+ from regine_core.config.db import open_global_config_db # noqa: PLC0415
31+
32+ conn = open_global_config_db(Path(args.config_db) if args.config_db else None)
33+ try:
34+ return charger_contexte(conn)
35+ finally:
36+ conn.close()
37+
38+
39+def _restaurer_contexte_post_checkout(args: argparse.Namespace) -> None:
40+ """FR-017 : après un checkout réussi, retrouve le contexte centralisée depuis
41+ l'archive si absente localement — no-op silencieux si les chemins de travail
42+ ne sont pas (encore) configurés (usage de `checkout` indépendant de `regine
43+ config`, cf. Assumptions de `specs/001`)."""
44+ from regine_core.config.context import restaurer_contexte_si_absent # noqa: PLC0415
45+ from regine_core.config.smb import chemin_local_du_partage # noqa: PLC0415
46+
47+ contexte = _charger_contexte_global(args)
48+ if contexte.local_work_dir is None or contexte.archive_smb_path is None:
49+ return
50+ archive_root_local = chemin_local_du_partage(contexte.archive_smb_path)
51+ if restaurer_contexte_si_absent(contexte.local_work_dir, archive_root_local):
52+ print("Contexte de travail restauré depuis l'archive.")
53+
54+
55+def _sauvegarder_contexte_post_reconcile(args: argparse.Namespace) -> None:
56+ """FR-016 : après un archivage réussi (y compris une réconciliation « à
57+ vide »), réplique la base de contexte centralisée vers l'archive — no-op
58+ silencieux si les chemins de travail ne sont pas configurés, ou si la base
59+ locale n'existe pas encore."""
60+ from regine_core.config.context import sauvegarder_contexte # noqa: PLC0415
61+ from regine_core.config.smb import chemin_local_du_partage # noqa: PLC0415
62+
63+ contexte = _charger_contexte_global(args)
64+ if contexte.local_work_dir is None or contexte.archive_smb_path is None:
65+ return
66+ archive_root_local = chemin_local_du_partage(contexte.archive_smb_path)
67+ sauvegarder_contexte(contexte.local_work_dir, archive_root_local)
68+
69+
20 def _cmd_checkout(args: argparse.Namespace) -> int:70 def _cmd_checkout(args: argparse.Namespace) -> int:
21 dossier_archive = Path(args.dossier)71 dossier_archive = Path(args.dossier)
22 dest_locale = Path(args.local_dest) if args.local_dest else Path.cwd() / dossier_archive.name72 dest_locale = Path(args.local_dest) if args.local_dest else Path.cwd() / dossier_archive.name
@@ -30,6 +80,7 @@ def _cmd_checkout(args: argparse.Namespace) -> int:
30 80
31 print(f"{len(snapshot.fichiers)} fichier(s) sortis vers {dest_locale}")81 print(f"{len(snapshot.fichiers)} fichier(s) sortis vers {dest_locale}")
32 snapshot.manifest.conn.close()82 snapshot.manifest.conn.close()
83+ _restaurer_contexte_post_checkout(args)
33 return 084 return 0
34 85
35 86
@@ -83,9 +134,11 @@ def _cmd_reconcile(args: argparse.Namespace) -> int:
83 134
84 if not rapport.changements:135 if not rapport.changements:
85 # Rien à réconcilier : la session se termine sans écriture, mais le verrou136 # Rien à réconcilier : la session se termine sans écriture, mais le verrou
86- # doit être levé (FR-016 s'applique aussi à une réconciliation "à vide").137+ # doit être levé (FR-016 de specs/005 s'applique aussi à une réconciliation
138+ # "à vide") — la réplication du contexte (FR-016 de specs/003) aussi.
87 lever(manifest)139 lever(manifest)
88 manifest.conn.close()140 manifest.conn.close()
141+ _sauvegarder_contexte_post_reconcile(args)
89 return 0142 return 0
90 143
91 decisions = DecisionsUtilisateur()144 decisions = DecisionsUtilisateur()
@@ -115,6 +168,7 @@ def _cmd_reconcile(args: argparse.Namespace) -> int:
115 archiver(snapshot, rapport, decisions)168 archiver(snapshot, rapport, decisions)
116 print("Réconciliation terminée.")169 print("Réconciliation terminée.")
117 manifest.conn.close()170 manifest.conn.close()
171+ _sauvegarder_contexte_post_reconcile(args)
118 return 0172 return 0
119 173
120 174
@@ -126,10 +180,10 @@ def _charger_archive_root_local(args: argparse.Namespace) -> Path | None:
126 partagée avec `regine-gui` (Principe VI — cf. bug trouvé par test manuel de la180 partagée avec `regine-gui` (Principe VI — cf. bug trouvé par test manuel de la
127 GUI, specs/006 : deux résolutions dupliquées et non cohérentes entre elles)."""181 GUI, specs/006 : deux résolutions dupliquées et non cohérentes entre elles)."""
128 from regine_core.config.context import charger_contexte # noqa: PLC0415182 from regine_core.config.context import charger_contexte # noqa: PLC0415
129- from regine_core.config.db import open_context_db # noqa: PLC0415183+ from regine_core.config.db import open_global_config_db # noqa: PLC0415
130 from regine_core.config.smb import resoudre_archive_locale # noqa: PLC0415184 from regine_core.config.smb import resoudre_archive_locale # noqa: PLC0415
131 185
132- conn = open_context_db(Path(args.contexte_db) if args.contexte_db else None)186+ conn = open_global_config_db(Path(args.config_db) if args.config_db else None)
133 try:187 try:
134 contexte = charger_contexte(conn)188 contexte = charger_contexte(conn)
135 if contexte.archive_smb_path is None:189 if contexte.archive_smb_path is None:
@@ -208,6 +262,7 @@ def construire_analyseur() -> argparse.ArgumentParser:
208 checkout_parser.add_argument("dossier")262 checkout_parser.add_argument("dossier")
209 checkout_parser.add_argument("--formats", default=None)263 checkout_parser.add_argument("--formats", default=None)
210 checkout_parser.add_argument("--local-dest", default=None)264 checkout_parser.add_argument("--local-dest", default=None)
265+ checkout_parser.add_argument("--config-db", default=None)
211 checkout_parser.set_defaults(func=_cmd_checkout)266 checkout_parser.set_defaults(func=_cmd_checkout)
212 267
213 reconcile_parser = sous_commandes.add_parser(268 reconcile_parser = sous_commandes.add_parser(
@@ -215,6 +270,7 @@ def construire_analyseur() -> argparse.ArgumentParser:
215 )270 )
216 reconcile_parser.add_argument("dossier")271 reconcile_parser.add_argument("dossier")
217 reconcile_parser.add_argument("--local-dest", default=None)272 reconcile_parser.add_argument("--local-dest", default=None)
273+ reconcile_parser.add_argument("--config-db", default=None)
218 reconcile_parser.set_defaults(func=_cmd_reconcile)274 reconcile_parser.set_defaults(func=_cmd_reconcile)
219 275
220 browse_parser = sous_commandes.add_parser(276 browse_parser = sous_commandes.add_parser(
@@ -222,7 +278,7 @@ def construire_analyseur() -> argparse.ArgumentParser:
222 )278 )
223 browse_parser.add_argument("--search", default=None)279 browse_parser.add_argument("--search", default=None)
224 browse_parser.add_argument("--json", action="store_true")280 browse_parser.add_argument("--json", action="store_true")
225- browse_parser.add_argument("--contexte-db", default=None)281+ browse_parser.add_argument("--config-db", default=None)
226 browse_parser.set_defaults(func=_cmd_browse)282 browse_parser.set_defaults(func=_cmd_browse)
227 283
228 restore_parser = sous_commandes.add_parser(284 restore_parser = sous_commandes.add_parser(
@@ -230,7 +286,7 @@ def construire_analyseur() -> argparse.ArgumentParser:
230 )286 )
231 restore_parser.add_argument("--chemin", required=True)287 restore_parser.add_argument("--chemin", required=True)
232 restore_parser.add_argument("--destination", required=True)288 restore_parser.add_argument("--destination", required=True)
233- restore_parser.add_argument("--contexte-db", default=None)289+ restore_parser.add_argument("--config-db", default=None)
234 restore_parser.set_defaults(func=_cmd_restore)290 restore_parser.set_defaults(func=_cmd_restore)
235 291
236 return analyseur292 return analyseur
modified packages/regine-cli/src/regine_cli/config_cmd.py +88 -21
@@ -2,6 +2,11 @@
22
33 Façade fine : orchestre `regine_core.config`, formatte le résultat, ne contient
44 aucune logique métier propre (Principe VI de la constitution).
5+
6+**Révisé le 2026-09-23/24** (`specs/003-config-contexte-travail` FR-014/015/016/017) :
7+utilise désormais `ouvrir_environnement()` (configuration globale + base de
8+contexte centralisée résolues ensemble) plutôt qu'une connexion unique — le
9+flag `--contexte-db` devient `--config-db` (ne cible plus que les chemins).
510 """
611
712 from __future__ import annotations
@@ -21,12 +26,18 @@ from regine_core.config.context import (
2126 TravailNonReconcilieError,
2227 charger_contexte,
2328 enregistrer_chemins,
29+ restaurer_contexte_si_absent,
2430 )
25-from regine_core.config.db import open_context_db
31+from regine_core.config.db import open_global_config_db
32+from regine_core.config.environnement import ouvrir_environnement
33+
2634
35+def _config_db_path(args: argparse.Namespace) -> Path | None:
36+ return Path(args.config_db) if args.config_db else None
2737
28-def _ouvrir_conn(args: argparse.Namespace):
29- return open_context_db(Path(args.contexte_db) if args.contexte_db else None)
38+
39+def _ouvrir_config(args: argparse.Namespace):
40+ return open_global_config_db(_config_db_path(args))
3041
3142
3243 def _afficher_contexte(contexte: ContexteTravail) -> None:
@@ -36,8 +47,35 @@ def _afficher_contexte(contexte: ContexteTravail) -> None:
3647 print(f" état SMB : {contexte.smb_last_known_state}")
3748
3849
50+def _tenter_restauration_contexte(args: argparse.Namespace, contexte: ContexteTravail) -> None:
51+ """FR-017 : restaure la base de contexte centralisée depuis l'archive si elle
52+ n'existe pas encore localement — silencieusement no-op si l'un des deux
53+ chemins manque encore, ou si l'archive n'est pas accessible à cet instant
54+ (retentée au premier `regine checkout`, cf. `contracts/cli-config.md`)."""
55+ if contexte.local_work_dir is None or contexte.archive_smb_path is None:
56+ return
57+
58+ from regine_core.config.smb import chemin_local_du_partage, disponible # noqa: PLC0415
59+
60+ if not disponible(contexte.archive_smb_path):
61+ return
62+
63+ archive_root_local = chemin_local_du_partage(contexte.archive_smb_path)
64+ if not restaurer_contexte_si_absent(contexte.local_work_dir, archive_root_local):
65+ return
66+
67+ environnement = ouvrir_environnement(_config_db_path(args))
68+ try:
69+ nb_boitiers = len(lister_boitiers(environnement.contexte_conn))
70+ finally:
71+ if environnement.contexte_conn is not None:
72+ environnement.contexte_conn.close()
73+ environnement.config_conn.close()
74+ print(f"Contexte restauré depuis l'archive : {nb_boitiers} boîtier(s).")
75+
76+
3977 def _cmd_set_paths(args: argparse.Namespace) -> int:
40- conn = _ouvrir_conn(args)
78+ conn = _ouvrir_config(args)
4179 try:
4280 contexte = enregistrer_chemins(
4381 conn,
@@ -76,11 +114,16 @@ def _cmd_set_paths(args: argparse.Namespace) -> int:
76114
77115 print("Contexte de travail enregistré :")
78116 _afficher_contexte(contexte)
117+
118+ # FR-017 : sur une nouvelle machine, retrouve le contexte déjà connu de
119+ # l'archive dès que les deux chemins nécessaires sont réunis.
120+ _tenter_restauration_contexte(args, contexte)
121+
79122 return 0
80123
81124
82125 def _cmd_show(args: argparse.Namespace) -> int:
83- conn = _ouvrir_conn(args)
126+ conn = _ouvrir_config(args)
84127 try:
85128 contexte = charger_contexte(conn)
86129 if contexte.archive_smb_path:
@@ -122,12 +165,30 @@ def _cmd_show(args: argparse.Namespace) -> int:
122165 return 1 if manquants else 0
123166
124167
168+def _ouvrir_environnement_ou_erreur(args: argparse.Namespace):
169+ """Résout l'environnement pour les sous-commandes `cameras ...` (boîtiers) —
170+ None si `local_work_dir` n'est pas encore configuré, message clair dans ce cas
171+ plutôt qu'une exception technique."""
172+ environnement = ouvrir_environnement(_config_db_path(args))
173+ if environnement.contexte_conn is None:
174+ environnement.config_conn.close()
175+ print(
176+ "Répertoire de travail local non configuré (regine config set-paths).",
177+ file=sys.stderr,
178+ )
179+ return None
180+ return environnement
181+
182+
125183 def _cmd_cameras_list(args: argparse.Namespace) -> int:
126- conn = _ouvrir_conn(args)
184+ environnement = _ouvrir_environnement_ou_erreur(args)
185+ if environnement is None:
186+ return 1
127187 try:
128- boitiers = lister_boitiers(conn)
188+ boitiers = lister_boitiers(environnement.contexte_conn)
129189 finally:
130- conn.close()
190+ environnement.contexte_conn.close()
191+ environnement.config_conn.close()
131192
132193 if args.json:
133194 print(
@@ -158,14 +219,17 @@ def _cmd_cameras_list(args: argparse.Namespace) -> int:
158219
159220
160221 def _cmd_cameras_rename(args: argparse.Namespace) -> int:
161- conn = _ouvrir_conn(args)
222+ environnement = _ouvrir_environnement_ou_erreur(args)
223+ if environnement is None:
224+ return 1
162225 try:
163- nommer_boitier(conn, args.id, args.nom)
226+ nommer_boitier(environnement.contexte_conn, args.id, args.nom)
164227 except BoitierInconnuError as exc:
165228 print(str(exc), file=sys.stderr)
166229 return 1
167230 finally:
168- conn.close()
231+ environnement.contexte_conn.close()
232+ environnement.config_conn.close()
169233
170234 print(f"Boîtier {args.id} renommé en « {args.nom} ».")
171235 return 0
@@ -185,13 +249,15 @@ def _cmd_cameras_scan_nas(args: argparse.Namespace) -> int:
185249 chemin_local_du_partage,
186250 )
187251
188- conn = _ouvrir_conn(args)
252+ environnement = _ouvrir_environnement_ou_erreur(args)
253+ if environnement is None:
254+ return 1
189255 try:
190- contexte = charger_contexte(conn)
256+ contexte = charger_contexte(environnement.config_conn)
191257 if contexte.archive_smb_path is None:
192258 print("Aucune archive configurée (regine config set-paths).", file=sys.stderr)
193259 return 1
194- if not assurer_disponibilite(conn, contexte.archive_smb_path):
260+ if not assurer_disponibilite(environnement.config_conn, contexte.archive_smb_path):
195261 print(
196262 f"{contexte.archive_smb_path} n'est pas accessible — montage natif déclenché, "
197263 "réessayer une fois l'accès rétabli.",
@@ -199,9 +265,10 @@ def _cmd_cameras_scan_nas(args: argparse.Namespace) -> int:
199265 )
200266 return 1
201267 archive_root_local = chemin_local_du_partage(contexte.archive_smb_path)
202- nouveaux = parcourir_nas(conn, archive_root_local)
268+ nouveaux = parcourir_nas(environnement.contexte_conn, archive_root_local)
203269 finally:
204- conn.close()
270+ environnement.contexte_conn.close()
271+ environnement.config_conn.close()
205272
206273 print(f"Parcours terminé : {nouveaux} nouveau(x) boîtier(s) détecté(s).")
207274 return 0
@@ -221,12 +288,12 @@ def construire_analyseur() -> argparse.ArgumentParser:
221288 set_paths_parser.add_argument("--yes", action="store_true")
222289 set_paths_parser.add_argument("--force", action="store_true")
223290 set_paths_parser.add_argument("--ignorer-verification-archive", action="store_true")
224- set_paths_parser.add_argument("--contexte-db", default=None)
291+ set_paths_parser.add_argument("--config-db", default=None)
225292 set_paths_parser.set_defaults(func=_cmd_set_paths)
226293
227294 show_parser = config_sous.add_parser("show", help="Affiche le contexte de travail actif")
228295 show_parser.add_argument("--json", action="store_true")
229- show_parser.add_argument("--contexte-db", default=None)
296+ show_parser.add_argument("--config-db", default=None)
230297 show_parser.set_defaults(func=_cmd_show)
231298
232299 cameras_parser = config_sous.add_parser("cameras", help="Gère les boîtiers connus")
@@ -234,20 +301,20 @@ def construire_analyseur() -> argparse.ArgumentParser:
234301
235302 cameras_list_parser = cameras_sous.add_parser("list", help="Liste les boîtiers connus")
236303 cameras_list_parser.add_argument("--json", action="store_true")
237- cameras_list_parser.add_argument("--contexte-db", default=None)
304+ cameras_list_parser.add_argument("--config-db", default=None)
238305 cameras_list_parser.set_defaults(func=_cmd_cameras_list)
239306
240307 cameras_rename_parser = cameras_sous.add_parser("rename", help="Nomme un boîtier")
241308 cameras_rename_parser.add_argument("--id", type=int, required=True)
242309 cameras_rename_parser.add_argument("--nom", required=True)
243- cameras_rename_parser.add_argument("--contexte-db", default=None)
310+ cameras_rename_parser.add_argument("--config-db", default=None)
244311 cameras_rename_parser.set_defaults(func=_cmd_cameras_rename)
245312
246313 cameras_scan_parser = cameras_sous.add_parser(
247314 "scan-nas", help="Parcours volontaire de l'archive (FR-012)"
248315 )
249316 cameras_scan_parser.add_argument("--confirm", action="store_true")
250- cameras_scan_parser.add_argument("--contexte-db", default=None)
317+ cameras_scan_parser.add_argument("--config-db", default=None)
251318 cameras_scan_parser.set_defaults(func=_cmd_cameras_scan_nas)
252319
253320 return analyseur
@@ -2,6 +2,11 @@
2 2
3 Façade fine : orchestre `regine_core.config`, formatte le résultat, ne contient3 Façade fine : orchestre `regine_core.config`, formatte le résultat, ne contient
4 aucune logique métier propre (Principe VI de la constitution).4 aucune logique métier propre (Principe VI de la constitution).
5+
6+**Révisé le 2026-09-23/24** (`specs/003-config-contexte-travail` FR-014/015/016/017) :
7+utilise désormais `ouvrir_environnement()` (configuration globale + base de
8+contexte centralisée résolues ensemble) plutôt qu'une connexion unique — le
9+flag `--contexte-db` devient `--config-db` (ne cible plus que les chemins).
5 """10 """
6 11
7 from __future__ import annotations12 from __future__ import annotations
@@ -21,12 +26,18 @@ from regine_core.config.context import (
21 TravailNonReconcilieError,26 TravailNonReconcilieError,
22 charger_contexte,27 charger_contexte,
23 enregistrer_chemins,28 enregistrer_chemins,
29+ restaurer_contexte_si_absent,
24 )30 )
25-from regine_core.config.db import open_context_db31+from regine_core.config.db import open_global_config_db
32+from regine_core.config.environnement import ouvrir_environnement
33+
26 34
35+def _config_db_path(args: argparse.Namespace) -> Path | None:
36+ return Path(args.config_db) if args.config_db else None
27 37
28-def _ouvrir_conn(args: argparse.Namespace):38+
29- return open_context_db(Path(args.contexte_db) if args.contexte_db else None)39+def _ouvrir_config(args: argparse.Namespace):
40+ return open_global_config_db(_config_db_path(args))
30 41
31 42
32 def _afficher_contexte(contexte: ContexteTravail) -> None:43 def _afficher_contexte(contexte: ContexteTravail) -> None:
@@ -36,8 +47,35 @@ def _afficher_contexte(contexte: ContexteTravail) -> None:
36 print(f" état SMB : {contexte.smb_last_known_state}")47 print(f" état SMB : {contexte.smb_last_known_state}")
37 48
38 49
50+def _tenter_restauration_contexte(args: argparse.Namespace, contexte: ContexteTravail) -> None:
51+ """FR-017 : restaure la base de contexte centralisée depuis l'archive si elle
52+ n'existe pas encore localement — silencieusement no-op si l'un des deux
53+ chemins manque encore, ou si l'archive n'est pas accessible à cet instant
54+ (retentée au premier `regine checkout`, cf. `contracts/cli-config.md`)."""
55+ if contexte.local_work_dir is None or contexte.archive_smb_path is None:
56+ return
57+
58+ from regine_core.config.smb import chemin_local_du_partage, disponible # noqa: PLC0415
59+
60+ if not disponible(contexte.archive_smb_path):
61+ return
62+
63+ archive_root_local = chemin_local_du_partage(contexte.archive_smb_path)
64+ if not restaurer_contexte_si_absent(contexte.local_work_dir, archive_root_local):
65+ return
66+
67+ environnement = ouvrir_environnement(_config_db_path(args))
68+ try:
69+ nb_boitiers = len(lister_boitiers(environnement.contexte_conn))
70+ finally:
71+ if environnement.contexte_conn is not None:
72+ environnement.contexte_conn.close()
73+ environnement.config_conn.close()
74+ print(f"Contexte restauré depuis l'archive : {nb_boitiers} boîtier(s).")
75+
76+
39 def _cmd_set_paths(args: argparse.Namespace) -> int:77 def _cmd_set_paths(args: argparse.Namespace) -> int:
40- conn = _ouvrir_conn(args)78+ conn = _ouvrir_config(args)
41 try:79 try:
42 contexte = enregistrer_chemins(80 contexte = enregistrer_chemins(
43 conn,81 conn,
@@ -76,11 +114,16 @@ def _cmd_set_paths(args: argparse.Namespace) -> int:
76 114
77 print("Contexte de travail enregistré :")115 print("Contexte de travail enregistré :")
78 _afficher_contexte(contexte)116 _afficher_contexte(contexte)
117+
118+ # FR-017 : sur une nouvelle machine, retrouve le contexte déjà connu de
119+ # l'archive dès que les deux chemins nécessaires sont réunis.
120+ _tenter_restauration_contexte(args, contexte)
121+
79 return 0122 return 0
80 123
81 124
82 def _cmd_show(args: argparse.Namespace) -> int:125 def _cmd_show(args: argparse.Namespace) -> int:
83- conn = _ouvrir_conn(args)126+ conn = _ouvrir_config(args)
84 try:127 try:
85 contexte = charger_contexte(conn)128 contexte = charger_contexte(conn)
86 if contexte.archive_smb_path:129 if contexte.archive_smb_path:
@@ -122,12 +165,30 @@ def _cmd_show(args: argparse.Namespace) -> int:
122 return 1 if manquants else 0165 return 1 if manquants else 0
123 166
124 167
168+def _ouvrir_environnement_ou_erreur(args: argparse.Namespace):
169+ """Résout l'environnement pour les sous-commandes `cameras ...` (boîtiers) —
170+ None si `local_work_dir` n'est pas encore configuré, message clair dans ce cas
171+ plutôt qu'une exception technique."""
172+ environnement = ouvrir_environnement(_config_db_path(args))
173+ if environnement.contexte_conn is None:
174+ environnement.config_conn.close()
175+ print(
176+ "Répertoire de travail local non configuré (regine config set-paths).",
177+ file=sys.stderr,
178+ )
179+ return None
180+ return environnement
181+
182+
125 def _cmd_cameras_list(args: argparse.Namespace) -> int:183 def _cmd_cameras_list(args: argparse.Namespace) -> int:
126- conn = _ouvrir_conn(args)184+ environnement = _ouvrir_environnement_ou_erreur(args)
185+ if environnement is None:
186+ return 1
127 try:187 try:
128- boitiers = lister_boitiers(conn)188+ boitiers = lister_boitiers(environnement.contexte_conn)
129 finally:189 finally:
130- conn.close()190+ environnement.contexte_conn.close()
191+ environnement.config_conn.close()
131 192
132 if args.json:193 if args.json:
133 print(194 print(
@@ -158,14 +219,17 @@ def _cmd_cameras_list(args: argparse.Namespace) -> int:
158 219
159 220
160 def _cmd_cameras_rename(args: argparse.Namespace) -> int:221 def _cmd_cameras_rename(args: argparse.Namespace) -> int:
161- conn = _ouvrir_conn(args)222+ environnement = _ouvrir_environnement_ou_erreur(args)
223+ if environnement is None:
224+ return 1
162 try:225 try:
163- nommer_boitier(conn, args.id, args.nom)226+ nommer_boitier(environnement.contexte_conn, args.id, args.nom)
164 except BoitierInconnuError as exc:227 except BoitierInconnuError as exc:
165 print(str(exc), file=sys.stderr)228 print(str(exc), file=sys.stderr)
166 return 1229 return 1
167 finally:230 finally:
168- conn.close()231+ environnement.contexte_conn.close()
232+ environnement.config_conn.close()
169 233
170 print(f"Boîtier {args.id} renommé en « {args.nom} ».")234 print(f"Boîtier {args.id} renommé en « {args.nom} ».")
171 return 0235 return 0
@@ -185,13 +249,15 @@ def _cmd_cameras_scan_nas(args: argparse.Namespace) -> int:
185 chemin_local_du_partage,249 chemin_local_du_partage,
186 )250 )
187 251
188- conn = _ouvrir_conn(args)252+ environnement = _ouvrir_environnement_ou_erreur(args)
253+ if environnement is None:
254+ return 1
189 try:255 try:
190- contexte = charger_contexte(conn)256+ contexte = charger_contexte(environnement.config_conn)
191 if contexte.archive_smb_path is None:257 if contexte.archive_smb_path is None:
192 print("Aucune archive configurée (regine config set-paths).", file=sys.stderr)258 print("Aucune archive configurée (regine config set-paths).", file=sys.stderr)
193 return 1259 return 1
194- if not assurer_disponibilite(conn, contexte.archive_smb_path):260+ if not assurer_disponibilite(environnement.config_conn, contexte.archive_smb_path):
195 print(261 print(
196 f"{contexte.archive_smb_path} n'est pas accessible — montage natif déclenché, "262 f"{contexte.archive_smb_path} n'est pas accessible — montage natif déclenché, "
197 "réessayer une fois l'accès rétabli.",263 "réessayer une fois l'accès rétabli.",
@@ -199,9 +265,10 @@ def _cmd_cameras_scan_nas(args: argparse.Namespace) -> int:
199 )265 )
200 return 1266 return 1
201 archive_root_local = chemin_local_du_partage(contexte.archive_smb_path)267 archive_root_local = chemin_local_du_partage(contexte.archive_smb_path)
202- nouveaux = parcourir_nas(conn, archive_root_local)268+ nouveaux = parcourir_nas(environnement.contexte_conn, archive_root_local)
203 finally:269 finally:
204- conn.close()270+ environnement.contexte_conn.close()
271+ environnement.config_conn.close()
205 272
206 print(f"Parcours terminé : {nouveaux} nouveau(x) boîtier(s) détecté(s).")273 print(f"Parcours terminé : {nouveaux} nouveau(x) boîtier(s) détecté(s).")
207 return 0274 return 0
@@ -221,12 +288,12 @@ def construire_analyseur() -> argparse.ArgumentParser:
221 set_paths_parser.add_argument("--yes", action="store_true")288 set_paths_parser.add_argument("--yes", action="store_true")
222 set_paths_parser.add_argument("--force", action="store_true")289 set_paths_parser.add_argument("--force", action="store_true")
223 set_paths_parser.add_argument("--ignorer-verification-archive", action="store_true")290 set_paths_parser.add_argument("--ignorer-verification-archive", action="store_true")
224- set_paths_parser.add_argument("--contexte-db", default=None)291+ set_paths_parser.add_argument("--config-db", default=None)
225 set_paths_parser.set_defaults(func=_cmd_set_paths)292 set_paths_parser.set_defaults(func=_cmd_set_paths)
226 293
227 show_parser = config_sous.add_parser("show", help="Affiche le contexte de travail actif")294 show_parser = config_sous.add_parser("show", help="Affiche le contexte de travail actif")
228 show_parser.add_argument("--json", action="store_true")295 show_parser.add_argument("--json", action="store_true")
229- show_parser.add_argument("--contexte-db", default=None)296+ show_parser.add_argument("--config-db", default=None)
230 show_parser.set_defaults(func=_cmd_show)297 show_parser.set_defaults(func=_cmd_show)
231 298
232 cameras_parser = config_sous.add_parser("cameras", help="Gère les boîtiers connus")299 cameras_parser = config_sous.add_parser("cameras", help="Gère les boîtiers connus")
@@ -234,20 +301,20 @@ def construire_analyseur() -> argparse.ArgumentParser:
234 301
235 cameras_list_parser = cameras_sous.add_parser("list", help="Liste les boîtiers connus")302 cameras_list_parser = cameras_sous.add_parser("list", help="Liste les boîtiers connus")
236 cameras_list_parser.add_argument("--json", action="store_true")303 cameras_list_parser.add_argument("--json", action="store_true")
237- cameras_list_parser.add_argument("--contexte-db", default=None)304+ cameras_list_parser.add_argument("--config-db", default=None)
238 cameras_list_parser.set_defaults(func=_cmd_cameras_list)305 cameras_list_parser.set_defaults(func=_cmd_cameras_list)
239 306
240 cameras_rename_parser = cameras_sous.add_parser("rename", help="Nomme un boîtier")307 cameras_rename_parser = cameras_sous.add_parser("rename", help="Nomme un boîtier")
241 cameras_rename_parser.add_argument("--id", type=int, required=True)308 cameras_rename_parser.add_argument("--id", type=int, required=True)
242 cameras_rename_parser.add_argument("--nom", required=True)309 cameras_rename_parser.add_argument("--nom", required=True)
243- cameras_rename_parser.add_argument("--contexte-db", default=None)310+ cameras_rename_parser.add_argument("--config-db", default=None)
244 cameras_rename_parser.set_defaults(func=_cmd_cameras_rename)311 cameras_rename_parser.set_defaults(func=_cmd_cameras_rename)
245 312
246 cameras_scan_parser = cameras_sous.add_parser(313 cameras_scan_parser = cameras_sous.add_parser(
247 "scan-nas", help="Parcours volontaire de l'archive (FR-012)"314 "scan-nas", help="Parcours volontaire de l'archive (FR-012)"
248 )315 )
249 cameras_scan_parser.add_argument("--confirm", action="store_true")316 cameras_scan_parser.add_argument("--confirm", action="store_true")
250- cameras_scan_parser.add_argument("--contexte-db", default=None)317+ cameras_scan_parser.add_argument("--config-db", default=None)
251 cameras_scan_parser.set_defaults(func=_cmd_cameras_scan_nas)318 cameras_scan_parser.set_defaults(func=_cmd_cameras_scan_nas)
252 319
253 return analyseur320 return analyseur
modified packages/regine-cli/src/regine_cli/import_cmd.py +8 -7
@@ -43,8 +43,6 @@ from regine_core.import_carte.nommage import (
4343 from regine_core.import_carte.push import preparer_resume, publier_localement
4444 from regine_core.import_carte.types import DestinationChoisie, FichierCandidat, GroupeImport
4545
46-_NOM_CONTEXTE_DB_PAR_DEFAUT = ".regine-contexte.sqlite3"
47-
4846
4947 def _proposer_detachement(groupes: list[GroupeImport], interactif: bool) -> list[GroupeImport]:
5048 """Propose le détachement d'un jour candidat (US2, FR-006) — jamais automatique."""
@@ -303,12 +301,16 @@ def _cmd_import(args: argparse.Namespace) -> int:
303301 local_tmp = Path(args.local_tmp) if args.local_tmp else Path.cwd() / ".regine-import-tmp"
304302 archive_root = Path(args.archive_root)
305303 local_root = Path(args.local_root)
306- contexte_db = (
307- Path(args.contexte_db) if args.contexte_db else local_root / _NOM_CONTEXTE_DB_PAR_DEFAUT
308- )
309304 interactif = not args.yes
310305
311- conn = open_context_db(contexte_db)
306+ # Bug réel corrigé le 2026-09-23/24 (trouvé par l'utilisateur, cf.
307+ # contracts/regine-core-api.md) : cette commande utilisait auparavant son
308+ # propre défaut (`--contexte-db`, `local_root/.regine-contexte.sqlite3`),
309+ # divergent de toutes les autres commandes CLI — un import réel n'écrivait
310+ # donc jamais dans la même base de contexte que le reste de Régine. La base
311+ # de contexte centralisée vit désormais, sans exception, à la racine de
312+ # `--local-root` (FR-006/FR-015 de specs/003), jamais à un chemin distinct.
313+ conn = open_context_db(local_root / "contexte.sqlite3")
312314 try:
313315 fichiers = copier_carte(carte, local_tmp)
314316 if not fichiers:
@@ -363,7 +365,6 @@ def construire_analyseur() -> argparse.ArgumentParser:
363365 import_parser.add_argument("--local-tmp", default=None)
364366 import_parser.add_argument("--archive-root", required=True)
365367 import_parser.add_argument("--local-root", required=True)
366- import_parser.add_argument("--contexte-db", default=None)
367368 import_parser.set_defaults(func=_cmd_import)
368369
369370 return analyseur
@@ -43,8 +43,6 @@ from regine_core.import_carte.nommage import (
43 from regine_core.import_carte.push import preparer_resume, publier_localement43 from regine_core.import_carte.push import preparer_resume, publier_localement
44 from regine_core.import_carte.types import DestinationChoisie, FichierCandidat, GroupeImport44 from regine_core.import_carte.types import DestinationChoisie, FichierCandidat, GroupeImport
45 45
46-_NOM_CONTEXTE_DB_PAR_DEFAUT = ".regine-contexte.sqlite3"
47-
48 46
49 def _proposer_detachement(groupes: list[GroupeImport], interactif: bool) -> list[GroupeImport]:47 def _proposer_detachement(groupes: list[GroupeImport], interactif: bool) -> list[GroupeImport]:
50 """Propose le détachement d'un jour candidat (US2, FR-006) — jamais automatique."""48 """Propose le détachement d'un jour candidat (US2, FR-006) — jamais automatique."""
@@ -303,12 +301,16 @@ def _cmd_import(args: argparse.Namespace) -> int:
303 local_tmp = Path(args.local_tmp) if args.local_tmp else Path.cwd() / ".regine-import-tmp"301 local_tmp = Path(args.local_tmp) if args.local_tmp else Path.cwd() / ".regine-import-tmp"
304 archive_root = Path(args.archive_root)302 archive_root = Path(args.archive_root)
305 local_root = Path(args.local_root)303 local_root = Path(args.local_root)
306- contexte_db = (
307- Path(args.contexte_db) if args.contexte_db else local_root / _NOM_CONTEXTE_DB_PAR_DEFAUT
308- )
309 interactif = not args.yes304 interactif = not args.yes
310 305
311- conn = open_context_db(contexte_db)306+ # Bug réel corrigé le 2026-09-23/24 (trouvé par l'utilisateur, cf.
307+ # contracts/regine-core-api.md) : cette commande utilisait auparavant son
308+ # propre défaut (`--contexte-db`, `local_root/.regine-contexte.sqlite3`),
309+ # divergent de toutes les autres commandes CLI — un import réel n'écrivait
310+ # donc jamais dans la même base de contexte que le reste de Régine. La base
311+ # de contexte centralisée vit désormais, sans exception, à la racine de
312+ # `--local-root` (FR-006/FR-015 de specs/003), jamais à un chemin distinct.
313+ conn = open_context_db(local_root / "contexte.sqlite3")
312 try:314 try:
313 fichiers = copier_carte(carte, local_tmp)315 fichiers = copier_carte(carte, local_tmp)
314 if not fichiers:316 if not fichiers:
@@ -363,7 +365,6 @@ def construire_analyseur() -> argparse.ArgumentParser:
363 import_parser.add_argument("--local-tmp", default=None)365 import_parser.add_argument("--local-tmp", default=None)
364 import_parser.add_argument("--archive-root", required=True)366 import_parser.add_argument("--archive-root", required=True)
365 import_parser.add_argument("--local-root", required=True)367 import_parser.add_argument("--local-root", required=True)
366- import_parser.add_argument("--contexte-db", default=None)
367 import_parser.set_defaults(func=_cmd_import)368 import_parser.set_defaults(func=_cmd_import)
368 369
369 return analyseur370 return analyseur
modified packages/regine-cli/tests/contract/test_cli_archive_consultation.py +6 -6
@@ -8,21 +8,21 @@ from pathlib import Path
88
99 from regine_cli.archive_cmd import main
1010 from regine_core.config.context import enregistrer_chemins
11-from regine_core.config.db import open_context_db
11+from regine_core.config.db import open_global_config_db
1212
1313
1414 def _configurer_archive(tmp_path: Path, chemin_archive_local: Path) -> list[str]:
15- """Configure un contexte de travail dont `archive_smb_path` résout, via un
15+ """Configure la configuration globale dont `archive_smb_path` résout, via un
1616 `chemin_local_du_partage` monkeypatché côté module, vers `chemin_archive_local`."""
17- contexte_db = tmp_path / "contexte.sqlite3"
18- conn = open_context_db(contexte_db)
17+ config_db = tmp_path / "config.sqlite3"
18+ conn = open_global_config_db(config_db)
1919 enregistrer_chemins(conn, archive_smb_path="smb://nas.local/regine-archive")
2020 conn.close()
21- return ["--contexte-db", str(contexte_db)]
21+ return ["--config-db", str(config_db)]
2222
2323
2424 def test_browse_sans_archive_configuree_echoue(tmp_path: Path, capsys) -> None:
25- code = main(["browse", "--contexte-db", str(tmp_path / "contexte.sqlite3")])
25+ code = main(["browse", "--config-db", str(tmp_path / "config.sqlite3")])
2626
2727 assert code != 0
2828 assert "Aucune archive configurée" in capsys.readouterr().err
@@ -8,21 +8,21 @@ from pathlib import Path
8 8
9 from regine_cli.archive_cmd import main9 from regine_cli.archive_cmd import main
10 from regine_core.config.context import enregistrer_chemins10 from regine_core.config.context import enregistrer_chemins
11-from regine_core.config.db import open_context_db11+from regine_core.config.db import open_global_config_db
12 12
13 13
14 def _configurer_archive(tmp_path: Path, chemin_archive_local: Path) -> list[str]:14 def _configurer_archive(tmp_path: Path, chemin_archive_local: Path) -> list[str]:
15- """Configure un contexte de travail dont `archive_smb_path` résout, via un15+ """Configure la configuration globale dont `archive_smb_path` résout, via un
16 `chemin_local_du_partage` monkeypatché côté module, vers `chemin_archive_local`."""16 `chemin_local_du_partage` monkeypatché côté module, vers `chemin_archive_local`."""
17- contexte_db = tmp_path / "contexte.sqlite3"17+ config_db = tmp_path / "config.sqlite3"
18- conn = open_context_db(contexte_db)18+ conn = open_global_config_db(config_db)
19 enregistrer_chemins(conn, archive_smb_path="smb://nas.local/regine-archive")19 enregistrer_chemins(conn, archive_smb_path="smb://nas.local/regine-archive")
20 conn.close()20 conn.close()
21- return ["--contexte-db", str(contexte_db)]21+ return ["--config-db", str(config_db)]
22 22
23 23
24 def test_browse_sans_archive_configuree_echoue(tmp_path: Path, capsys) -> None:24 def test_browse_sans_archive_configuree_echoue(tmp_path: Path, capsys) -> None:
25- code = main(["browse", "--contexte-db", str(tmp_path / "contexte.sqlite3")])25+ code = main(["browse", "--config-db", str(tmp_path / "config.sqlite3")])
26 26
27 assert code != 027 assert code != 0
28 assert "Aucune archive configurée" in capsys.readouterr().err28 assert "Aucune archive configurée" in capsys.readouterr().err
modified packages/regine-cli/tests/contract/test_cli_config.py +48 -19
@@ -1,4 +1,9 @@
1-"""Contrat de la surface CLI `regine config` (cf. contracts/cli-config.md, T015/T025)."""
1+"""Contrat de la surface CLI `regine config` (cf. contracts/cli-config.md, T015/T025).
2+
3+**Révisé le 2026-09-23/24** (`specs/003` FR-014/015) : `--contexte-db` devient
4+`--config-db` (ne cible plus que les chemins) ; les sous-commandes `cameras ...`
5+exigent désormais que `local_work_dir` soit déjà configuré (la base de contexte
6+centralisée vit à sa racine, plus à un emplacement indépendant)."""
27
38 from __future__ import annotations
49
@@ -7,11 +12,23 @@ from pathlib import Path
712
813 from regine_cli.config_cmd import main
914 from regine_core.camera_profile.db import get_or_create_boitier
10-from regine_core.config.db import open_context_db
15+from regine_core.config.context import enregistrer_chemins
16+from regine_core.config.db import open_context_db, open_global_config_db
17+
1118
19+def _config_db(tmp_path: Path) -> list[str]:
20+ return ["--config-db", str(tmp_path / "config.sqlite3")]
1221
13-def _contexte_db(tmp_path: Path) -> list[str]:
14- return ["--contexte-db", str(tmp_path / "contexte.sqlite3")]
22+
23+def _configurer_local_work_dir(tmp_path: Path) -> Path:
24+ """Configuration globale minimale (`local_work_dir` seul) — prérequis des
25+ sous-commandes `cameras ...` depuis que la base de contexte centralisée vit
26+ à sa racine plutôt qu'à un emplacement indépendant (FR-014/015)."""
27+ local_work_dir = tmp_path / "travail"
28+ conn = open_global_config_db(tmp_path / "config.sqlite3")
29+ enregistrer_chemins(conn, local_work_dir=local_work_dir, creer_repertoires_manquants=True)
30+ conn.close()
31+ return local_work_dir
1532
1633
1734 def test_set_paths_puis_show_reflete_le_contexte(tmp_path: Path, capsys) -> None:
@@ -29,13 +46,13 @@ def test_set_paths_puis_show_reflete_le_contexte(tmp_path: Path, capsys) -> None
2946 "--archive-smb",
3047 "smb://nas.local/regine-archive",
3148 "--yes",
32- *_contexte_db(tmp_path),
49+ *_config_db(tmp_path),
3350 ]
3451 )
3552 assert code == 0
3653 capsys.readouterr()
3754
38- code = main(["config", "show", "--json", *_contexte_db(tmp_path)])
55+ code = main(["config", "show", "--json", *_config_db(tmp_path)])
3956 assert code == 0
4057 sortie = json.loads(capsys.readouterr().out)
4158 assert sortie["temp_dir"] == str(temp_dir)
@@ -44,7 +61,7 @@ def test_set_paths_puis_show_reflete_le_contexte(tmp_path: Path, capsys) -> None
4461
4562
4663 def test_show_sans_contexte_configure_signale_les_chemins_manquants(tmp_path: Path, capsys) -> None:
47- code = main(["config", "show", *_contexte_db(tmp_path)])
64+ code = main(["config", "show", *_config_db(tmp_path)])
4865
4966 assert code != 0
5067 sortie = capsys.readouterr().out
@@ -60,7 +77,7 @@ def test_set_paths_repertoire_manquant_sans_yes_echoue(tmp_path: Path, capsys) -
6077 "set-paths",
6178 "--temp-dir",
6279 str(tmp_path / "absent"),
63- *_contexte_db(tmp_path),
80+ *_config_db(tmp_path),
6481 ]
6582 )
6683
@@ -80,7 +97,7 @@ def test_set_paths_collision_sans_force_echoue(tmp_path: Path, capsys) -> None:
8097 "--local-work-dir",
8198 str(meme_chemin),
8299 "--yes",
83- *_contexte_db(tmp_path),
100+ *_config_db(tmp_path),
84101 ]
85102 )
86103
@@ -95,7 +112,7 @@ def test_set_paths_archive_smb_invalide_echoue(tmp_path: Path, capsys) -> None:
95112 "set-paths",
96113 "--archive-smb",
97114 "/Volumes/regine-archive",
98- *_contexte_db(tmp_path),
115+ *_config_db(tmp_path),
99116 ]
100117 )
101118
@@ -104,18 +121,30 @@ def test_set_paths_archive_smb_invalide_echoue(tmp_path: Path, capsys) -> None:
104121
105122
106123 def test_cameras_list_vide_sans_erreur(tmp_path: Path, capsys) -> None:
107- code = main(["config", "cameras", "list", *_contexte_db(tmp_path)])
124+ _configurer_local_work_dir(tmp_path)
125+
126+ code = main(["config", "cameras", "list", *_config_db(tmp_path)])
108127
109128 assert code == 0
110129 assert "Aucun boîtier" in capsys.readouterr().out
111130
112131
132+def test_cameras_list_sans_local_work_dir_configure_echoue(tmp_path: Path, capsys) -> None:
133+ """FR-014/015 : la base de contexte centralisée vit à la racine de
134+ `local_work_dir` — rien à consulter tant qu'il n'est pas configuré."""
135+ code = main(["config", "cameras", "list", *_config_db(tmp_path)])
136+
137+ assert code != 0
138+ assert "non configuré" in capsys.readouterr().err
139+
140+
113141 def test_cameras_list_puis_rename(tmp_path: Path, capsys) -> None:
114- conn = open_context_db(tmp_path / "contexte.sqlite3")
142+ local_work_dir = _configurer_local_work_dir(tmp_path)
143+ conn = open_context_db(local_work_dir / "contexte.sqlite3")
115144 boitier_id = get_or_create_boitier(conn, "Fujifilm X100V", "12345", source="modele")
116145 conn.close()
117146
118- code = main(["config", "cameras", "list", "--json", *_contexte_db(tmp_path)])
147+ code = main(["config", "cameras", "list", "--json", *_config_db(tmp_path)])
119148 assert code == 0
120149 boitiers = json.loads(capsys.readouterr().out)
121150 assert boitiers[0]["nom_lisible"] is None
@@ -129,28 +158,28 @@ def test_cameras_list_puis_rename(tmp_path: Path, capsys) -> None:
129158 str(boitier_id),
130159 "--nom",
131160 "Fuji principal",
132- *_contexte_db(tmp_path),
161+ *_config_db(tmp_path),
133162 ]
134163 )
135164 assert code == 0
136165 capsys.readouterr()
137166
138- code = main(["config", "cameras", "list", "--json", *_contexte_db(tmp_path)])
167+ code = main(["config", "cameras", "list", "--json", *_config_db(tmp_path)])
139168 boitiers = json.loads(capsys.readouterr().out)
140169 assert boitiers[0]["nom_lisible"] == "Fuji principal"
141170
142171
143172 def test_cameras_rename_id_inconnu_echoue(tmp_path: Path, capsys) -> None:
144- code = main(
145- ["config", "cameras", "rename", "--id", "999", "--nom", "X", *_contexte_db(tmp_path)]
146- )
173+ _configurer_local_work_dir(tmp_path)
174+
175+ code = main(["config", "cameras", "rename", "--id", "999", "--nom", "X", *_config_db(tmp_path)])
147176
148177 assert code != 0
149178 assert "inconnu" in capsys.readouterr().err
150179
151180
152181 def test_cameras_scan_nas_sans_confirm_n_execute_rien(tmp_path: Path, capsys) -> None:
153- code = main(["config", "cameras", "scan-nas", *_contexte_db(tmp_path)])
182+ code = main(["config", "cameras", "scan-nas", *_config_db(tmp_path)])
154183
155184 assert code != 0
156185 assert "--confirm" in capsys.readouterr().out
@@ -1,4 +1,9 @@
1-"""Contrat de la surface CLI `regine config` (cf. contracts/cli-config.md, T015/T025)."""1+"""Contrat de la surface CLI `regine config` (cf. contracts/cli-config.md, T015/T025).
2+
3+**Révisé le 2026-09-23/24** (`specs/003` FR-014/015) : `--contexte-db` devient
4+`--config-db` (ne cible plus que les chemins) ; les sous-commandes `cameras ...`
5+exigent désormais que `local_work_dir` soit déjà configuré (la base de contexte
6+centralisée vit à sa racine, plus à un emplacement indépendant)."""
2 7
3 from __future__ import annotations8 from __future__ import annotations
4 9
@@ -7,11 +12,23 @@ from pathlib import Path
7 12
8 from regine_cli.config_cmd import main13 from regine_cli.config_cmd import main
9 from regine_core.camera_profile.db import get_or_create_boitier14 from regine_core.camera_profile.db import get_or_create_boitier
10-from regine_core.config.db import open_context_db15+from regine_core.config.context import enregistrer_chemins
16+from regine_core.config.db import open_context_db, open_global_config_db
17+
11 18
19+def _config_db(tmp_path: Path) -> list[str]:
20+ return ["--config-db", str(tmp_path / "config.sqlite3")]
12 21
13-def _contexte_db(tmp_path: Path) -> list[str]:22+
14- return ["--contexte-db", str(tmp_path / "contexte.sqlite3")]23+def _configurer_local_work_dir(tmp_path: Path) -> Path:
24+ """Configuration globale minimale (`local_work_dir` seul) — prérequis des
25+ sous-commandes `cameras ...` depuis que la base de contexte centralisée vit
26+ à sa racine plutôt qu'à un emplacement indépendant (FR-014/015)."""
27+ local_work_dir = tmp_path / "travail"
28+ conn = open_global_config_db(tmp_path / "config.sqlite3")
29+ enregistrer_chemins(conn, local_work_dir=local_work_dir, creer_repertoires_manquants=True)
30+ conn.close()
31+ return local_work_dir
15 32
16 33
17 def test_set_paths_puis_show_reflete_le_contexte(tmp_path: Path, capsys) -> None:34 def test_set_paths_puis_show_reflete_le_contexte(tmp_path: Path, capsys) -> None:
@@ -29,13 +46,13 @@ def test_set_paths_puis_show_reflete_le_contexte(tmp_path: Path, capsys) -> None
29 "--archive-smb",46 "--archive-smb",
30 "smb://nas.local/regine-archive",47 "smb://nas.local/regine-archive",
31 "--yes",48 "--yes",
32- *_contexte_db(tmp_path),49+ *_config_db(tmp_path),
33 ]50 ]
34 )51 )
35 assert code == 052 assert code == 0
36 capsys.readouterr()53 capsys.readouterr()
37 54
38- code = main(["config", "show", "--json", *_contexte_db(tmp_path)])55+ code = main(["config", "show", "--json", *_config_db(tmp_path)])
39 assert code == 056 assert code == 0
40 sortie = json.loads(capsys.readouterr().out)57 sortie = json.loads(capsys.readouterr().out)
41 assert sortie["temp_dir"] == str(temp_dir)58 assert sortie["temp_dir"] == str(temp_dir)
@@ -44,7 +61,7 @@ def test_set_paths_puis_show_reflete_le_contexte(tmp_path: Path, capsys) -> None
44 61
45 62
46 def test_show_sans_contexte_configure_signale_les_chemins_manquants(tmp_path: Path, capsys) -> None:63 def test_show_sans_contexte_configure_signale_les_chemins_manquants(tmp_path: Path, capsys) -> None:
47- code = main(["config", "show", *_contexte_db(tmp_path)])64+ code = main(["config", "show", *_config_db(tmp_path)])
48 65
49 assert code != 066 assert code != 0
50 sortie = capsys.readouterr().out67 sortie = capsys.readouterr().out
@@ -60,7 +77,7 @@ def test_set_paths_repertoire_manquant_sans_yes_echoue(tmp_path: Path, capsys) -
60 "set-paths",77 "set-paths",
61 "--temp-dir",78 "--temp-dir",
62 str(tmp_path / "absent"),79 str(tmp_path / "absent"),
63- *_contexte_db(tmp_path),80+ *_config_db(tmp_path),
64 ]81 ]
65 )82 )
66 83
@@ -80,7 +97,7 @@ def test_set_paths_collision_sans_force_echoue(tmp_path: Path, capsys) -> None:
80 "--local-work-dir",97 "--local-work-dir",
81 str(meme_chemin),98 str(meme_chemin),
82 "--yes",99 "--yes",
83- *_contexte_db(tmp_path),100+ *_config_db(tmp_path),
84 ]101 ]
85 )102 )
86 103
@@ -95,7 +112,7 @@ def test_set_paths_archive_smb_invalide_echoue(tmp_path: Path, capsys) -> None:
95 "set-paths",112 "set-paths",
96 "--archive-smb",113 "--archive-smb",
97 "/Volumes/regine-archive",114 "/Volumes/regine-archive",
98- *_contexte_db(tmp_path),115+ *_config_db(tmp_path),
99 ]116 ]
100 )117 )
101 118
@@ -104,18 +121,30 @@ def test_set_paths_archive_smb_invalide_echoue(tmp_path: Path, capsys) -> None:
104 121
105 122
106 def test_cameras_list_vide_sans_erreur(tmp_path: Path, capsys) -> None:123 def test_cameras_list_vide_sans_erreur(tmp_path: Path, capsys) -> None:
107- code = main(["config", "cameras", "list", *_contexte_db(tmp_path)])124+ _configurer_local_work_dir(tmp_path)
125+
126+ code = main(["config", "cameras", "list", *_config_db(tmp_path)])
108 127
109 assert code == 0128 assert code == 0
110 assert "Aucun boîtier" in capsys.readouterr().out129 assert "Aucun boîtier" in capsys.readouterr().out
111 130
112 131
132+def test_cameras_list_sans_local_work_dir_configure_echoue(tmp_path: Path, capsys) -> None:
133+ """FR-014/015 : la base de contexte centralisée vit à la racine de
134+ `local_work_dir` — rien à consulter tant qu'il n'est pas configuré."""
135+ code = main(["config", "cameras", "list", *_config_db(tmp_path)])
136+
137+ assert code != 0
138+ assert "non configuré" in capsys.readouterr().err
139+
140+
113 def test_cameras_list_puis_rename(tmp_path: Path, capsys) -> None:141 def test_cameras_list_puis_rename(tmp_path: Path, capsys) -> None:
114- conn = open_context_db(tmp_path / "contexte.sqlite3")142+ local_work_dir = _configurer_local_work_dir(tmp_path)
143+ conn = open_context_db(local_work_dir / "contexte.sqlite3")
115 boitier_id = get_or_create_boitier(conn, "Fujifilm X100V", "12345", source="modele")144 boitier_id = get_or_create_boitier(conn, "Fujifilm X100V", "12345", source="modele")
116 conn.close()145 conn.close()
117 146
118- code = main(["config", "cameras", "list", "--json", *_contexte_db(tmp_path)])147+ code = main(["config", "cameras", "list", "--json", *_config_db(tmp_path)])
119 assert code == 0148 assert code == 0
120 boitiers = json.loads(capsys.readouterr().out)149 boitiers = json.loads(capsys.readouterr().out)
121 assert boitiers[0]["nom_lisible"] is None150 assert boitiers[0]["nom_lisible"] is None
@@ -129,28 +158,28 @@ def test_cameras_list_puis_rename(tmp_path: Path, capsys) -> None:
129 str(boitier_id),158 str(boitier_id),
130 "--nom",159 "--nom",
131 "Fuji principal",160 "Fuji principal",
132- *_contexte_db(tmp_path),161+ *_config_db(tmp_path),
133 ]162 ]
134 )163 )
135 assert code == 0164 assert code == 0
136 capsys.readouterr()165 capsys.readouterr()
137 166
138- code = main(["config", "cameras", "list", "--json", *_contexte_db(tmp_path)])167+ code = main(["config", "cameras", "list", "--json", *_config_db(tmp_path)])
139 boitiers = json.loads(capsys.readouterr().out)168 boitiers = json.loads(capsys.readouterr().out)
140 assert boitiers[0]["nom_lisible"] == "Fuji principal"169 assert boitiers[0]["nom_lisible"] == "Fuji principal"
141 170
142 171
143 def test_cameras_rename_id_inconnu_echoue(tmp_path: Path, capsys) -> None:172 def test_cameras_rename_id_inconnu_echoue(tmp_path: Path, capsys) -> None:
144- code = main(173+ _configurer_local_work_dir(tmp_path)
145- ["config", "cameras", "rename", "--id", "999", "--nom", "X", *_contexte_db(tmp_path)]174+
146- )175+ code = main(["config", "cameras", "rename", "--id", "999", "--nom", "X", *_config_db(tmp_path)])
147 176
148 assert code != 0177 assert code != 0
149 assert "inconnu" in capsys.readouterr().err178 assert "inconnu" in capsys.readouterr().err
150 179
151 180
152 def test_cameras_scan_nas_sans_confirm_n_execute_rien(tmp_path: Path, capsys) -> None:181 def test_cameras_scan_nas_sans_confirm_n_execute_rien(tmp_path: Path, capsys) -> None:
153- code = main(["config", "cameras", "scan-nas", *_contexte_db(tmp_path)])182+ code = main(["config", "cameras", "scan-nas", *_config_db(tmp_path)])
154 183
155 assert code != 0184 assert code != 0
156 assert "--confirm" in capsys.readouterr().out185 assert "--confirm" in capsys.readouterr().out
modified packages/regine-core/src/regine_core/config/context.py +81 -3
@@ -7,6 +7,7 @@ Réutilise le manifeste persistant et le verrouillage de `regine_core.archive`
77
88 from __future__ import annotations
99
10+import shutil
1011 import sqlite3
1112 from dataclasses import dataclass
1213 from pathlib import Path
@@ -60,6 +61,11 @@ class ArchiveInaccessibleError(Exception):
6061 de travail non réconcilié avant un changement de `local_work_dir` (FR-010)."""
6162
6263
64+class EchecReplicationContexteError(Exception):
65+ """La vérification d'intégrité d'une réplication/restauration de la base de
66+ contexte centralisée a échoué (FR-016/FR-017)."""
67+
68+
6369 @dataclass(frozen=True)
6470 class ContexteTravail:
6571 """Contexte de travail actif — unique par installation (cf. spec.md Assumptions)."""
@@ -195,9 +201,18 @@ def enregistrer_chemins(
195201 chemin.mkdir(parents=True, exist_ok=True)
196202
197203 if local_work_dir is not None:
198- # FR-006/SC-003 : base de travail prête à la racine, avant tout autre besoin.
199- handle = ouvrir_ou_creer(local_work_dir)
200- handle.conn.close()
204+ # FR-006/FR-015/SC-003 : base de contexte centralisée prête à la racine,
205+ # avant tout autre besoin. Révisé le 2026-09-23/24 : remplace l'ancien
206+ # manifeste vide (`archive.manifest.ouvrir_ou_creer`) jamais exploité par
207+ # ce module — le verrou/manifeste réel vit uniquement côté archive
208+ # (cf. `lister_dossiers_non_reconcilies` ci-dessus).
209+ from regine_core.config.db import ( # noqa: PLC0415
210+ NOM_FICHIER_CONTEXTE_CENTRALISEE,
211+ open_context_db,
212+ )
213+
214+ contexte_conn = open_context_db(local_work_dir / NOM_FICHIER_CONTEXTE_CENTRALISEE)
215+ contexte_conn.close()
201216
202217 _ensure_schema(conn)
203218 conn.execute(
@@ -219,6 +234,69 @@ def enregistrer_chemins(
219234 return charger_contexte(conn)
220235
221236
237+def sauvegarder_contexte(local_work_dir: Path, archive_root_local: Path) -> None:
238+ """Réplique la base de contexte centralisée vers l'archive (FR-016).
239+
240+ Copie vérifiée par somme de contrôle de `<local_work_dir>/contexte.sqlite3`
241+ vers `<archive_root_local>/.regine-contexte.sqlite3` — toujours un
242+ remplacement complet du fichier de sauvegarde précédent, jamais une fusion
243+ (cf. Edge Cases de spec.md : pas de fusion de contextes divergents). Ne DOIT
244+ être appelée qu'après un `regine reconcile` réussi, orchestré par la façade
245+ CLI/GUI plutôt que par `regine_core.archive` lui-même (cf. research.md § 7).
246+ No-op si la base locale n'existe pas encore (rien à répliquer).
247+ """
248+ from regine_core.config.db import ( # noqa: PLC0415
249+ NOM_FICHIER_CONTEXTE_CENTRALISEE,
250+ NOM_FICHIER_SAUVEGARDE_CONTEXTE,
251+ )
252+ from regine_core.integrity.hash import hash_fichier_entier # noqa: PLC0415
253+
254+ source = local_work_dir / NOM_FICHIER_CONTEXTE_CENTRALISEE
255+ if not source.exists():
256+ return
257+
258+ destination = archive_root_local / NOM_FICHIER_SAUVEGARDE_CONTEXTE
259+ destination.parent.mkdir(parents=True, exist_ok=True)
260+ shutil.copy2(source, destination)
261+ if hash_fichier_entier(destination) != hash_fichier_entier(source):
262+ raise EchecReplicationContexteError(
263+ f"Échec de vérification d'intégrité en répliquant {source} vers {destination}"
264+ )
265+
266+
267+def restaurer_contexte_si_absent(local_work_dir: Path, archive_root_local: Path) -> bool:
268+ """Restaure la base de contexte centralisée depuis l'archive si absente
269+ localement (FR-017) — sans jamais écraser une base locale déjà présente.
270+
271+ Retourne `True` si une restauration a eu lieu, `False` sinon (déjà présente
272+ localement, ou absente des deux côtés — première installation, rien à
273+ restaurer). Appelée par la façade CLI/GUI après la configuration initiale
274+ des trois chemins, ou après un premier `regine checkout` réussi sur une
275+ machine qui n'a pas encore ce contexte localement (cf. research.md § 7).
276+ """
277+ from regine_core.config.db import ( # noqa: PLC0415
278+ NOM_FICHIER_CONTEXTE_CENTRALISEE,
279+ NOM_FICHIER_SAUVEGARDE_CONTEXTE,
280+ )
281+ from regine_core.integrity.hash import hash_fichier_entier # noqa: PLC0415
282+
283+ destination = local_work_dir / NOM_FICHIER_CONTEXTE_CENTRALISEE
284+ if destination.exists():
285+ return False
286+
287+ source = archive_root_local / NOM_FICHIER_SAUVEGARDE_CONTEXTE
288+ if not source.exists():
289+ return False
290+
291+ destination.parent.mkdir(parents=True, exist_ok=True)
292+ shutil.copy2(source, destination)
293+ if hash_fichier_entier(destination) != hash_fichier_entier(source):
294+ raise EchecReplicationContexteError(
295+ f"Échec de vérification d'intégrité en restaurant {source} vers {destination}"
296+ )
297+ return True
298+
299+
222300 def mettre_a_jour_etat_smb(conn: sqlite3.Connection, etat: EtatSmb) -> None:
223301 """Met à jour l'état de disponibilité connu du partage SMB (FR-004/005)."""
224302 _ensure_schema(conn)
@@ -7,6 +7,7 @@ Réutilise le manifeste persistant et le verrouillage de `regine_core.archive`
7 7
8 from __future__ import annotations8 from __future__ import annotations
9 9
10+import shutil
10 import sqlite311 import sqlite3
11 from dataclasses import dataclass12 from dataclasses import dataclass
12 from pathlib import Path13 from pathlib import Path
@@ -60,6 +61,11 @@ class ArchiveInaccessibleError(Exception):
60 de travail non réconcilié avant un changement de `local_work_dir` (FR-010)."""61 de travail non réconcilié avant un changement de `local_work_dir` (FR-010)."""
61 62
62 63
64+class EchecReplicationContexteError(Exception):
65+ """La vérification d'intégrité d'une réplication/restauration de la base de
66+ contexte centralisée a échoué (FR-016/FR-017)."""
67+
68+
63 @dataclass(frozen=True)69 @dataclass(frozen=True)
64 class ContexteTravail:70 class ContexteTravail:
65 """Contexte de travail actif — unique par installation (cf. spec.md Assumptions)."""71 """Contexte de travail actif — unique par installation (cf. spec.md Assumptions)."""
@@ -195,9 +201,18 @@ def enregistrer_chemins(
195 chemin.mkdir(parents=True, exist_ok=True)201 chemin.mkdir(parents=True, exist_ok=True)
196 202
197 if local_work_dir is not None:203 if local_work_dir is not None:
198- # FR-006/SC-003 : base de travail prête à la racine, avant tout autre besoin.204+ # FR-006/FR-015/SC-003 : base de contexte centralisée prête à la racine,
199- handle = ouvrir_ou_creer(local_work_dir)205+ # avant tout autre besoin. Révisé le 2026-09-23/24 : remplace l'ancien
200- handle.conn.close()206+ # manifeste vide (`archive.manifest.ouvrir_ou_creer`) jamais exploité par
207+ # ce module — le verrou/manifeste réel vit uniquement côté archive
208+ # (cf. `lister_dossiers_non_reconcilies` ci-dessus).
209+ from regine_core.config.db import ( # noqa: PLC0415
210+ NOM_FICHIER_CONTEXTE_CENTRALISEE,
211+ open_context_db,
212+ )
213+
214+ contexte_conn = open_context_db(local_work_dir / NOM_FICHIER_CONTEXTE_CENTRALISEE)
215+ contexte_conn.close()
201 216
202 _ensure_schema(conn)217 _ensure_schema(conn)
203 conn.execute(218 conn.execute(
@@ -219,6 +234,69 @@ def enregistrer_chemins(
219 return charger_contexte(conn)234 return charger_contexte(conn)
220 235
221 236
237+def sauvegarder_contexte(local_work_dir: Path, archive_root_local: Path) -> None:
238+ """Réplique la base de contexte centralisée vers l'archive (FR-016).
239+
240+ Copie vérifiée par somme de contrôle de `<local_work_dir>/contexte.sqlite3`
241+ vers `<archive_root_local>/.regine-contexte.sqlite3` — toujours un
242+ remplacement complet du fichier de sauvegarde précédent, jamais une fusion
243+ (cf. Edge Cases de spec.md : pas de fusion de contextes divergents). Ne DOIT
244+ être appelée qu'après un `regine reconcile` réussi, orchestré par la façade
245+ CLI/GUI plutôt que par `regine_core.archive` lui-même (cf. research.md § 7).
246+ No-op si la base locale n'existe pas encore (rien à répliquer).
247+ """
248+ from regine_core.config.db import ( # noqa: PLC0415
249+ NOM_FICHIER_CONTEXTE_CENTRALISEE,
250+ NOM_FICHIER_SAUVEGARDE_CONTEXTE,
251+ )
252+ from regine_core.integrity.hash import hash_fichier_entier # noqa: PLC0415
253+
254+ source = local_work_dir / NOM_FICHIER_CONTEXTE_CENTRALISEE
255+ if not source.exists():
256+ return
257+
258+ destination = archive_root_local / NOM_FICHIER_SAUVEGARDE_CONTEXTE
259+ destination.parent.mkdir(parents=True, exist_ok=True)
260+ shutil.copy2(source, destination)
261+ if hash_fichier_entier(destination) != hash_fichier_entier(source):
262+ raise EchecReplicationContexteError(
263+ f"Échec de vérification d'intégrité en répliquant {source} vers {destination}"
264+ )
265+
266+
267+def restaurer_contexte_si_absent(local_work_dir: Path, archive_root_local: Path) -> bool:
268+ """Restaure la base de contexte centralisée depuis l'archive si absente
269+ localement (FR-017) — sans jamais écraser une base locale déjà présente.
270+
271+ Retourne `True` si une restauration a eu lieu, `False` sinon (déjà présente
272+ localement, ou absente des deux côtés — première installation, rien à
273+ restaurer). Appelée par la façade CLI/GUI après la configuration initiale
274+ des trois chemins, ou après un premier `regine checkout` réussi sur une
275+ machine qui n'a pas encore ce contexte localement (cf. research.md § 7).
276+ """
277+ from regine_core.config.db import ( # noqa: PLC0415
278+ NOM_FICHIER_CONTEXTE_CENTRALISEE,
279+ NOM_FICHIER_SAUVEGARDE_CONTEXTE,
280+ )
281+ from regine_core.integrity.hash import hash_fichier_entier # noqa: PLC0415
282+
283+ destination = local_work_dir / NOM_FICHIER_CONTEXTE_CENTRALISEE
284+ if destination.exists():
285+ return False
286+
287+ source = archive_root_local / NOM_FICHIER_SAUVEGARDE_CONTEXTE
288+ if not source.exists():
289+ return False
290+
291+ destination.parent.mkdir(parents=True, exist_ok=True)
292+ shutil.copy2(source, destination)
293+ if hash_fichier_entier(destination) != hash_fichier_entier(source):
294+ raise EchecReplicationContexteError(
295+ f"Échec de vérification d'intégrité en restaurant {source} vers {destination}"
296+ )
297+ return True
298+
299+
222 def mettre_a_jour_etat_smb(conn: sqlite3.Connection, etat: EtatSmb) -> None:300 def mettre_a_jour_etat_smb(conn: sqlite3.Connection, etat: EtatSmb) -> None:
223 """Met à jour l'état de disponibilité connu du partage SMB (FR-004/005)."""301 """Met à jour l'état de disponibilité connu du partage SMB (FR-004/005)."""
224 _ensure_schema(conn)302 _ensure_schema(conn)
modified packages/regine-core/src/regine_core/config/db.py +61 -19
@@ -1,11 +1,25 @@
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.
1+"""Ouverture/initialisation SQLite des deux bases du contexte de travail.
2+
3+**Révisé le 2026-09-23/24 (`specs/003-config-contexte-travail` FR-014/FR-015)** :
4+deux bases distinctes désormais, à des emplacements différents, plutôt qu'une
5+base unique à emplacement unique — écart trouvé par l'utilisateur : l'ancienne
6+base unique mélangeait les trois chemins de travail (propres à la machine,
7+jamais destinés à être partagés) et le savoir accumulé (boîtiers, catégories,
8+historique d'imports — devant au contraire survivre à la perte de la machine).
9+
10+- **Configuration globale** (`open_global_config_db`) : uniquement les trois
11+ chemins de travail et l'état SMB (table ``contexte``), à un emplacement fixe
12+ par machine (``~/.regine/config.sqlite3``).
13+- **Base de contexte centralisée** (`open_context_db`) : boîtiers (``camera_profile``),
14+ catégories (``config.categories``), historique d'imports (``import_carte.historique``)
15+ — à la racine du répertoire de travail local, dont le chemin complet est
16+ désormais un paramètre obligatoire (plus de défaut fixe, cf. `environnement.py`
17+ pour la résolution en deux temps).
18+
19+Les deux réutilisent les mêmes conventions que le manifeste persistant par
20+dossier (constitution § Workflow d'archivage) : ``PRAGMA application_id`` marque
21+le format Régine, ``PRAGMA user_version`` porte le numéro de schéma, écriture
22+transactionnelle.
923 """
1024
1125 from __future__ import annotations
@@ -19,21 +33,26 @@ APPLICATION_ID = 0x52454749
1933 #: Numéro de schéma courant. À incrémenter à chaque évolution du schéma partagé.
2034 SCHEMA_VERSION = 1
2135
22-#: Emplacement par défaut de la base de contexte centralisée : fixe, par utilisateur,
23-#: indépendant de ``local_work_dir`` (specs/003-config-contexte-travail) — ce dernier
24-#: est modifiable (FR-010) et la base de contexte (boîtiers, catégories, chemins) ne
25-#: doit jamais se perdre ni nécessiter de migration à un tel changement.
26-CHEMIN_CONTEXTE_PAR_DEFAUT = Path.home() / ".regine" / "contexte.sqlite3"
36+#: Emplacement par défaut de la configuration globale (chemins de travail, état SMB) :
37+#: fixe, par machine — jamais les boîtiers/catégories/historique (cf. docstring de module).
38+CHEMIN_CONFIG_GLOBALE_PAR_DEFAUT = Path.home() / ".regine" / "config.sqlite3"
2739
40+#: Nom de fichier de la base de contexte centralisée à la racine du répertoire de
41+#: travail local (FR-006/FR-015).
42+NOM_FICHIER_CONTEXTE_CENTRALISEE = "contexte.sqlite3"
2843
29-def open_context_db(path: Path | None = None) -> sqlite3.Connection:
30- """Ouvre la base de contexte centralisée, en l'initialisant si elle n'existe pas.
44+#: Nom de fichier de sa sauvegarde répliquée à la racine de l'archive (FR-016) —
45+#: préfixé d'un point pour rester discret parmi les dossiers photo, cf.
46+#: `context.sauvegarder_contexte`/`restaurer_contexte_si_absent`.
47+NOM_FICHIER_SAUVEGARDE_CONTEXTE = ".regine-contexte.sqlite3"
3148
32- Refuse explicitement d'ouvrir un fichier SQLite qui n'est pas déjà une base de
33- contexte Régine (``application_id`` différent), plutôt que de l'interpréter à
34- tort. Sans ``path`` explicite, utilise ``CHEMIN_CONTEXTE_PAR_DEFAUT``.
49+
50+def _ouvrir_base_regine(path: Path) -> sqlite3.Connection:
51+ """Bootstrap SQLite partagé par les deux bases : crée le fichier si absent,
52+ marque/vérifie ``PRAGMA application_id``. Refuse explicitement d'ouvrir un
53+ fichier SQLite qui n'est pas déjà une base Régine, plutôt que de
54+ l'interpréter à tort.
3555 """
36- path = path or CHEMIN_CONTEXTE_PAR_DEFAUT
3756 path.parent.mkdir(parents=True, exist_ok=True)
3857 conn = sqlite3.connect(path)
3958 conn.execute("PRAGMA foreign_keys = ON")
@@ -50,3 +69,26 @@ def open_context_db(path: Path | None = None) -> sqlite3.Connection:
5069 )
5170
5271 return conn
72+
73+
74+def open_context_db(path: Path) -> sqlite3.Connection:
75+ """Ouvre la base de contexte centralisée (boîtiers, catégories, historique
76+ d'imports) à ``path``, en l'initialisant si elle n'existe pas encore.
77+
78+ ``path`` DOIT être un chemin de fichier complet (ex. ``local_work_dir /
79+ "contexte.sqlite3"``), jamais un dossier — cette fonction ne dérive plus
80+ elle-même d'emplacement par défaut depuis la révision du 2026-09-23/24 :
81+ utiliser `environnement.ouvrir_environnement` pour résoudre ce chemin à
82+ partir du répertoire de travail local configuré.
83+ """
84+ return _ouvrir_base_regine(path)
85+
86+
87+def open_global_config_db(path: Path | None = None) -> sqlite3.Connection:
88+ """Ouvre la configuration globale (chemins de travail, état SMB — table
89+ ``contexte`` uniquement), en l'initialisant si elle n'existe pas. Sans
90+ ``path``, utilise `CHEMIN_CONFIG_GLOBALE_PAR_DEFAUT`. Ne contient jamais
91+ boîtiers/catégories/historique (FR-007) : ce contenu vit dans la base de
92+ contexte centralisée (`open_context_db`), à un emplacement distinct.
93+ """
94+ return _ouvrir_base_regine(path or CHEMIN_CONFIG_GLOBALE_PAR_DEFAUT)
@@ -1,11 +1,25 @@
1-"""Base de contexte centralisée : ouverture/initialisation SQLite.1+"""Ouverture/initialisation SQLite des deux bases du contexte de travail.
2-2+
3-Fichier unique au niveau du contexte de travail (pas par dossier), réutilisé par3+**Révisé le 2026-09-23/24 (`specs/003-config-contexte-travail` FR-014/FR-015)** :
4-plusieurs modules pour leurs propres tables (``categories`` ici, ``boitiers`` dans4+deux bases distinctes désormais, à des emplacements différents, plutôt qu'une
5-``regine_core.camera_profile``, cf. specs/002-profil-boitiers-optionnel). Suit les5+base unique à emplacement unique — écart trouvé par l'utilisateur : l'ancienne
6-mêmes conventions que le manifeste persistant par dossier (constitution6+base unique mélangeait les trois chemins de travail (propres à la machine,
7-§ Workflow d'archivage) : ``PRAGMA application_id`` marque le format Régine,7+jamais destinés à être partagés) et le savoir accumulé (boîtiers, catégories,
8-``PRAGMA user_version`` porte le numéro de schéma, écriture transactionnelle.8+historique d'imports — devant au contraire survivre à la perte de la machine).
9+
10+- **Configuration globale** (`open_global_config_db`) : uniquement les trois
11+ chemins de travail et l'état SMB (table ``contexte``), à un emplacement fixe
12+ par machine (``~/.regine/config.sqlite3``).
13+- **Base de contexte centralisée** (`open_context_db`) : boîtiers (``camera_profile``),
14+ catégories (``config.categories``), historique d'imports (``import_carte.historique``)
15+ — à la racine du répertoire de travail local, dont le chemin complet est
16+ désormais un paramètre obligatoire (plus de défaut fixe, cf. `environnement.py`
17+ pour la résolution en deux temps).
18+
19+Les deux réutilisent les mêmes conventions que le manifeste persistant par
20+dossier (constitution § Workflow d'archivage) : ``PRAGMA application_id`` marque
21+le format Régine, ``PRAGMA user_version`` porte le numéro de schéma, écriture
22+transactionnelle.
9 """23 """
10 24
11 from __future__ import annotations25 from __future__ import annotations
@@ -19,21 +33,26 @@ APPLICATION_ID = 0x52454749
19 #: Numéro de schéma courant. À incrémenter à chaque évolution du schéma partagé.33 #: Numéro de schéma courant. À incrémenter à chaque évolution du schéma partagé.
20 SCHEMA_VERSION = 134 SCHEMA_VERSION = 1
21 35
22-#: Emplacement par défaut de la base de contexte centralisée : fixe, par utilisateur,36+#: Emplacement par défaut de la configuration globale (chemins de travail, état SMB) :
23-#: indépendant de ``local_work_dir`` (specs/003-config-contexte-travail) — ce dernier37+#: fixe, par machine — jamais les boîtiers/catégories/historique (cf. docstring de module).
24-#: est modifiable (FR-010) et la base de contexte (boîtiers, catégories, chemins) ne38+CHEMIN_CONFIG_GLOBALE_PAR_DEFAUT = Path.home() / ".regine" / "config.sqlite3"
25-#: doit jamais se perdre ni nécessiter de migration à un tel changement.
26-CHEMIN_CONTEXTE_PAR_DEFAUT = Path.home() / ".regine" / "contexte.sqlite3"
27 39
40+#: Nom de fichier de la base de contexte centralisée à la racine du répertoire de
41+#: travail local (FR-006/FR-015).
42+NOM_FICHIER_CONTEXTE_CENTRALISEE = "contexte.sqlite3"
28 43
29-def open_context_db(path: Path | None = None) -> sqlite3.Connection:44+#: Nom de fichier de sa sauvegarde répliquée à la racine de l'archive (FR-016) —
30- """Ouvre la base de contexte centralisée, en l'initialisant si elle n'existe pas.45+#: préfixé d'un point pour rester discret parmi les dossiers photo, cf.
46+#: `context.sauvegarder_contexte`/`restaurer_contexte_si_absent`.
47+NOM_FICHIER_SAUVEGARDE_CONTEXTE = ".regine-contexte.sqlite3"
31 48
32- Refuse explicitement d'ouvrir un fichier SQLite qui n'est pas déjà une base de49+
33- contexte Régine (``application_id`` différent), plutôt que de l'interpréter à50+def _ouvrir_base_regine(path: Path) -> sqlite3.Connection:
34- tort. Sans ``path`` explicite, utilise ``CHEMIN_CONTEXTE_PAR_DEFAUT``.51+ """Bootstrap SQLite partagé par les deux bases : crée le fichier si absent,
52+ marque/vérifie ``PRAGMA application_id``. Refuse explicitement d'ouvrir un
53+ fichier SQLite qui n'est pas déjà une base Régine, plutôt que de
54+ l'interpréter à tort.
35 """55 """
36- path = path or CHEMIN_CONTEXTE_PAR_DEFAUT
37 path.parent.mkdir(parents=True, exist_ok=True)56 path.parent.mkdir(parents=True, exist_ok=True)
38 conn = sqlite3.connect(path)57 conn = sqlite3.connect(path)
39 conn.execute("PRAGMA foreign_keys = ON")58 conn.execute("PRAGMA foreign_keys = ON")
@@ -50,3 +69,26 @@ def open_context_db(path: Path | None = None) -> sqlite3.Connection:
50 )69 )
51 70
52 return conn71 return conn
72+
73+
74+def open_context_db(path: Path) -> sqlite3.Connection:
75+ """Ouvre la base de contexte centralisée (boîtiers, catégories, historique
76+ d'imports) à ``path``, en l'initialisant si elle n'existe pas encore.
77+
78+ ``path`` DOIT être un chemin de fichier complet (ex. ``local_work_dir /
79+ "contexte.sqlite3"``), jamais un dossier — cette fonction ne dérive plus
80+ elle-même d'emplacement par défaut depuis la révision du 2026-09-23/24 :
81+ utiliser `environnement.ouvrir_environnement` pour résoudre ce chemin à
82+ partir du répertoire de travail local configuré.
83+ """
84+ return _ouvrir_base_regine(path)
85+
86+
87+def open_global_config_db(path: Path | None = None) -> sqlite3.Connection:
88+ """Ouvre la configuration globale (chemins de travail, état SMB — table
89+ ``contexte`` uniquement), en l'initialisant si elle n'existe pas. Sans
90+ ``path``, utilise `CHEMIN_CONFIG_GLOBALE_PAR_DEFAUT`. Ne contient jamais
91+ boîtiers/catégories/historique (FR-007) : ce contenu vit dans la base de
92+ contexte centralisée (`open_context_db`), à un emplacement distinct.
93+ """
94+ return _ouvrir_base_regine(path or CHEMIN_CONFIG_GLOBALE_PAR_DEFAUT)
added packages/regine-core/src/regine_core/config/environnement.py +56 -0
new file mode 100644
@@ -0,0 +1,56 @@
1+"""Point d'entrée unique pour résoudre le contexte de travail (FR-014/FR-015).
2+
3+Remplace les appels épars à ``open_context_db(None)`` dans chaque façade
4+CLI/GUI (`specs/003-config-contexte-travail`, révision 2026-09-23/24) : résout
5+d'abord la configuration globale (chemins de travail), puis, si
6+``local_work_dir`` y est déjà connu, ouvre la base de contexte centralisée à sa
7+racine. Un bug réel (`regine_cli.import_cmd` divergeait silencieusement des
8+autres commandes CLI, cf. `contracts/regine-core-api.md`) a motivé ce point
9+d'entrée unique plutôt que de laisser chaque appelant recalculer la résolution
10+en deux temps par lui-même (Principe VI).
11+"""
12+
13+from __future__ import annotations
14+
15+import sqlite3
16+from dataclasses import dataclass
17+from pathlib import Path
18+
19+from regine_core.config.context import charger_contexte
20+from regine_core.config.db import (
21+ NOM_FICHIER_CONTEXTE_CENTRALISEE,
22+ open_context_db,
23+ open_global_config_db,
24+)
25+
26+
27+@dataclass
28+class Environnement:
29+ """Les deux connexions résolues pour une installation de Régine.
30+
31+ ``contexte_conn`` vaut ``None`` tant que ``local_work_dir`` n'est pas encore
32+ configuré dans la configuration globale — rien à résoudre à cet emplacement.
33+ """
34+
35+ config_conn: sqlite3.Connection
36+ contexte_conn: sqlite3.Connection | None
37+ local_work_dir: Path | None
38+
39+
40+def ouvrir_environnement(config_db_path: Path | None = None) -> Environnement:
41+ """Résout la configuration globale puis, si possible, la base de contexte
42+ centralisée (FR-014/FR-015). Ne déclenche aucun accès réseau (l'archive
43+ n'intervient que via `regine_core.config.context.sauvegarder_contexte`/
44+ `restaurer_contexte_si_absent`, orchestrées séparément par la façade
45+ appelante — cf. research.md § 7)."""
46+ config_conn = open_global_config_db(config_db_path)
47+ contexte = charger_contexte(config_conn)
48+ if contexte.local_work_dir is None:
49+ return Environnement(config_conn=config_conn, contexte_conn=None, local_work_dir=None)
50+
51+ contexte_conn = open_context_db(contexte.local_work_dir / NOM_FICHIER_CONTEXTE_CENTRALISEE)
52+ return Environnement(
53+ config_conn=config_conn,
54+ contexte_conn=contexte_conn,
55+ local_work_dir=contexte.local_work_dir,
56+ )
new file mode 100644
@@ -0,0 +1,56 @@
1+"""Point d'entrée unique pour résoudre le contexte de travail (FR-014/FR-015).
2+
3+Remplace les appels épars à ``open_context_db(None)`` dans chaque façade
4+CLI/GUI (`specs/003-config-contexte-travail`, révision 2026-09-23/24) : résout
5+d'abord la configuration globale (chemins de travail), puis, si
6+``local_work_dir`` y est déjà connu, ouvre la base de contexte centralisée à sa
7+racine. Un bug réel (`regine_cli.import_cmd` divergeait silencieusement des
8+autres commandes CLI, cf. `contracts/regine-core-api.md`) a motivé ce point
9+d'entrée unique plutôt que de laisser chaque appelant recalculer la résolution
10+en deux temps par lui-même (Principe VI).
11+"""
12+
13+from __future__ import annotations
14+
15+import sqlite3
16+from dataclasses import dataclass
17+from pathlib import Path
18+
19+from regine_core.config.context import charger_contexte
20+from regine_core.config.db import (
21+ NOM_FICHIER_CONTEXTE_CENTRALISEE,
22+ open_context_db,
23+ open_global_config_db,
24+)
25+
26+
27+@dataclass
28+class Environnement:
29+ """Les deux connexions résolues pour une installation de Régine.
30+
31+ ``contexte_conn`` vaut ``None`` tant que ``local_work_dir`` n'est pas encore
32+ configuré dans la configuration globale — rien à résoudre à cet emplacement.
33+ """
34+
35+ config_conn: sqlite3.Connection
36+ contexte_conn: sqlite3.Connection | None
37+ local_work_dir: Path | None
38+
39+
40+def ouvrir_environnement(config_db_path: Path | None = None) -> Environnement:
41+ """Résout la configuration globale puis, si possible, la base de contexte
42+ centralisée (FR-014/FR-015). Ne déclenche aucun accès réseau (l'archive
43+ n'intervient que via `regine_core.config.context.sauvegarder_contexte`/
44+ `restaurer_contexte_si_absent`, orchestrées séparément par la façade
45+ appelante — cf. research.md § 7)."""
46+ config_conn = open_global_config_db(config_db_path)
47+ contexte = charger_contexte(config_conn)
48+ if contexte.local_work_dir is None:
49+ return Environnement(config_conn=config_conn, contexte_conn=None, local_work_dir=None)
50+
51+ contexte_conn = open_context_db(contexte.local_work_dir / NOM_FICHIER_CONTEXTE_CENTRALISEE)
52+ return Environnement(
53+ config_conn=config_conn,
54+ contexte_conn=contexte_conn,
55+ local_work_dir=contexte.local_work_dir,
56+ )
modified packages/regine-core/tests/integration/test_config_context.py +4 -3
@@ -41,9 +41,10 @@ def test_cycle_configuration_initialisation_et_changement_bloque(tmp_path: Path)
4141 assert temp_dir.exists()
4242 assert local_work_dir.exists()
4343
44- # Scenario 3 : une base de travail est prête à la racine du répertoire local,
45- # avant qu'aucun import/checkout n'en ait eu besoin (FR-006/SC-003).
46- assert (local_work_dir / ".regine-manifest.sqlite3").exists()
44+ # Scenario 3 : une base de travail (la base de contexte centralisée elle-même,
45+ # révisé le 2026-09-23/24) est prête à la racine du répertoire local, avant
46+ # qu'aucun import/checkout n'en ait eu besoin (FR-006/FR-015/SC-003).
47+ assert (local_work_dir / "contexte.sqlite3").exists()
4748
4849 # Une autre fonctionnalité (import, cf. specs/001) peut relire ce contexte sans
4950 # reconfiguration supplémentaire : simulé ici par un simple rechargement.
@@ -41,9 +41,10 @@ def test_cycle_configuration_initialisation_et_changement_bloque(tmp_path: Path)
41 assert temp_dir.exists()41 assert temp_dir.exists()
42 assert local_work_dir.exists()42 assert local_work_dir.exists()
43 43
44- # Scenario 3 : une base de travail est prête à la racine du répertoire local,44+ # Scenario 3 : une base de travail (la base de contexte centralisée elle-même,
45- # avant qu'aucun import/checkout n'en ait eu besoin (FR-006/SC-003).45+ # révisé le 2026-09-23/24) est prête à la racine du répertoire local, avant
46- assert (local_work_dir / ".regine-manifest.sqlite3").exists()46+ # qu'aucun import/checkout n'en ait eu besoin (FR-006/FR-015/SC-003).
47+ assert (local_work_dir / "contexte.sqlite3").exists()
47 48
48 # Une autre fonctionnalité (import, cf. specs/001) peut relire ce contexte sans49 # Une autre fonctionnalité (import, cf. specs/001) peut relire ce contexte sans
49 # reconfiguration supplémentaire : simulé ici par un simple rechargement.50 # reconfiguration supplémentaire : simulé ici par un simple rechargement.
modified packages/regine-core/tests/unit/test_config_db_defaut.py +31 -8
@@ -1,28 +1,51 @@
1-"""Test du chemin par défaut de la base de contexte centralisée (T006)."""
1+"""Test du chemin par défaut de la configuration globale (T006, révisé T035).
2+
3+**Révisé le 2026-09-23/24** : le défaut implicite décrit ici vivait auparavant sur
4+`open_context_db` (base unique). Il vit désormais sur `open_global_config_db`
5+(configuration globale, chemins uniquement) — `open_context_db` exige à présent
6+un chemin de fichier explicite dans tous les cas (FR-006/FR-015)."""
27
38 from __future__ import annotations
49
510 from pathlib import Path
611
712 import regine_core.config.db as db_module
8-from regine_core.config.db import APPLICATION_ID, open_context_db
13+from regine_core.config.db import APPLICATION_ID, open_context_db, open_global_config_db
914
1015
11-def test_open_context_db_sans_argument_utilise_le_chemin_par_defaut(
16+def test_open_global_config_db_sans_argument_utilise_le_chemin_par_defaut(
1217 tmp_path: Path, monkeypatch
1318 ) -> None:
14- chemin_attendu = tmp_path / "home" / ".regine" / "contexte.sqlite3"
15- monkeypatch.setattr(db_module, "CHEMIN_CONTEXTE_PAR_DEFAUT", chemin_attendu)
19+ chemin_attendu = tmp_path / "home" / ".regine" / "config.sqlite3"
20+ monkeypatch.setattr(db_module, "CHEMIN_CONFIG_GLOBALE_PAR_DEFAUT", chemin_attendu)
1621
17- conn = open_context_db()
22+ conn = open_global_config_db()
1823
1924 assert chemin_attendu.exists()
2025 assert conn.execute("PRAGMA application_id").fetchone()[0] == APPLICATION_ID
2126 conn.close()
2227
2328
24-def test_open_context_db_avec_argument_explicite_ignore_le_defaut(tmp_path: Path) -> None:
25- chemin_explicite = tmp_path / "autre" / "contexte.sqlite3"
29+def test_open_global_config_db_avec_argument_explicite_ignore_le_defaut(tmp_path: Path) -> None:
30+ chemin_explicite = tmp_path / "autre" / "config.sqlite3"
31+
32+ conn = open_global_config_db(chemin_explicite)
33+
34+ assert chemin_explicite.exists()
35+ conn.close()
36+
37+
38+def test_open_context_db_exige_desormais_un_chemin_explicite(tmp_path: Path) -> None:
39+ """FR-006/FR-015 : plus de défaut implicite — `open_context_db` sans argument
40+ lève une `TypeError` (paramètre requis), contrairement à `open_global_config_db`."""
41+ import pytest
42+
43+ with pytest.raises(TypeError):
44+ open_context_db() # type: ignore[call-arg]
45+
46+
47+def test_open_context_db_avec_argument_explicite(tmp_path: Path) -> None:
48+ chemin_explicite = tmp_path / "travail" / "contexte.sqlite3"
2649
2750 conn = open_context_db(chemin_explicite)
2851
@@ -1,28 +1,51 @@
1-"""Test du chemin par défaut de la base de contexte centralisée (T006)."""1+"""Test du chemin par défaut de la configuration globale (T006, révisé T035).
2+
3+**Révisé le 2026-09-23/24** : le défaut implicite décrit ici vivait auparavant sur
4+`open_context_db` (base unique). Il vit désormais sur `open_global_config_db`
5+(configuration globale, chemins uniquement) — `open_context_db` exige à présent
6+un chemin de fichier explicite dans tous les cas (FR-006/FR-015)."""
2 7
3 from __future__ import annotations8 from __future__ import annotations
4 9
5 from pathlib import Path10 from pathlib import Path
6 11
7 import regine_core.config.db as db_module12 import regine_core.config.db as db_module
8-from regine_core.config.db import APPLICATION_ID, open_context_db13+from regine_core.config.db import APPLICATION_ID, open_context_db, open_global_config_db
9 14
10 15
11-def test_open_context_db_sans_argument_utilise_le_chemin_par_defaut(16+def test_open_global_config_db_sans_argument_utilise_le_chemin_par_defaut(
12 tmp_path: Path, monkeypatch17 tmp_path: Path, monkeypatch
13 ) -> None:18 ) -> None:
14- chemin_attendu = tmp_path / "home" / ".regine" / "contexte.sqlite3"19+ chemin_attendu = tmp_path / "home" / ".regine" / "config.sqlite3"
15- monkeypatch.setattr(db_module, "CHEMIN_CONTEXTE_PAR_DEFAUT", chemin_attendu)20+ monkeypatch.setattr(db_module, "CHEMIN_CONFIG_GLOBALE_PAR_DEFAUT", chemin_attendu)
16 21
17- conn = open_context_db()22+ conn = open_global_config_db()
18 23
19 assert chemin_attendu.exists()24 assert chemin_attendu.exists()
20 assert conn.execute("PRAGMA application_id").fetchone()[0] == APPLICATION_ID25 assert conn.execute("PRAGMA application_id").fetchone()[0] == APPLICATION_ID
21 conn.close()26 conn.close()
22 27
23 28
24-def test_open_context_db_avec_argument_explicite_ignore_le_defaut(tmp_path: Path) -> None:29+def test_open_global_config_db_avec_argument_explicite_ignore_le_defaut(tmp_path: Path) -> None:
25- chemin_explicite = tmp_path / "autre" / "contexte.sqlite3"30+ chemin_explicite = tmp_path / "autre" / "config.sqlite3"
31+
32+ conn = open_global_config_db(chemin_explicite)
33+
34+ assert chemin_explicite.exists()
35+ conn.close()
36+
37+
38+def test_open_context_db_exige_desormais_un_chemin_explicite(tmp_path: Path) -> None:
39+ """FR-006/FR-015 : plus de défaut implicite — `open_context_db` sans argument
40+ lève une `TypeError` (paramètre requis), contrairement à `open_global_config_db`."""
41+ import pytest
42+
43+ with pytest.raises(TypeError):
44+ open_context_db() # type: ignore[call-arg]
45+
46+
47+def test_open_context_db_avec_argument_explicite(tmp_path: Path) -> None:
48+ chemin_explicite = tmp_path / "travail" / "contexte.sqlite3"
26 49
27 conn = open_context_db(chemin_explicite)50 conn = open_context_db(chemin_explicite)
28 51
modified packages/regine-core/tests/unit/test_context_paths.py +2 -1
@@ -55,7 +55,8 @@ def test_enregistrer_chemins_configuration_initiale(tmp_path: Path) -> None:
5555 assert contexte.archive_smb_path == "smb://nas.local/regine-archive"
5656 assert temp_dir.exists()
5757 assert local_work_dir.exists()
58- assert (local_work_dir / ".regine-manifest.sqlite3").exists() # FR-006/SC-003
58+ # FR-006/FR-015/SC-003, révisé 2026-09-23/24 (base de contexte, plus un manifeste vide)
59+ assert (local_work_dir / "contexte.sqlite3").exists()
5960 conn.close()
6061
6162
@@ -55,7 +55,8 @@ def test_enregistrer_chemins_configuration_initiale(tmp_path: Path) -> None:
55 assert contexte.archive_smb_path == "smb://nas.local/regine-archive"55 assert contexte.archive_smb_path == "smb://nas.local/regine-archive"
56 assert temp_dir.exists()56 assert temp_dir.exists()
57 assert local_work_dir.exists()57 assert local_work_dir.exists()
58- assert (local_work_dir / ".regine-manifest.sqlite3").exists() # FR-006/SC-00358+ # FR-006/FR-015/SC-003, révisé 2026-09-23/24 (base de contexte, plus un manifeste vide)
59+ assert (local_work_dir / "contexte.sqlite3").exists()
59 conn.close()60 conn.close()
60 61
61 62
added packages/regine-core/tests/unit/test_contexte_replication.py +86 -0
new file mode 100644
@@ -0,0 +1,86 @@
1+"""Tests de `sauvegarder_contexte`/`restaurer_contexte_si_absent` (FR-016/FR-017)."""
2+
3+from __future__ import annotations
4+
5+from pathlib import Path
6+
7+from regine_core.config.context import restaurer_contexte_si_absent, sauvegarder_contexte
8+from regine_core.config.db import open_context_db
9+
10+
11+def test_sauvegarder_contexte_no_op_si_base_locale_absente(tmp_path: Path) -> None:
12+ local_work_dir = tmp_path / "travail"
13+ local_work_dir.mkdir()
14+ archive_root = tmp_path / "archive"
15+
16+ sauvegarder_contexte(local_work_dir, archive_root) # ne doit pas lever
17+
18+ assert not (archive_root / ".regine-contexte.sqlite3").exists()
19+
20+
21+def test_sauvegarder_puis_restaurer_contexte(tmp_path: Path) -> None:
22+ local_work_dir = tmp_path / "travail"
23+ archive_root = tmp_path / "archive"
24+
25+ conn = open_context_db(local_work_dir / "contexte.sqlite3")
26+ conn.execute("CREATE TABLE test (valeur TEXT)")
27+ conn.execute("INSERT INTO test VALUES ('boitier-fuji')")
28+ conn.commit()
29+ conn.close()
30+
31+ sauvegarder_contexte(local_work_dir, archive_root)
32+
33+ assert (archive_root / ".regine-contexte.sqlite3").exists()
34+ assert (archive_root / ".regine-contexte.sqlite3").read_bytes() == (
35+ local_work_dir / "contexte.sqlite3"
36+ ).read_bytes()
37+
38+ # Simule une nouvelle machine : local_work_dir vide, l'archive a la sauvegarde.
39+ nouvelle_machine = tmp_path / "nouvelle-machine"
40+
41+ restaure = restaurer_contexte_si_absent(nouvelle_machine, archive_root)
42+
43+ assert restaure is True
44+ conn_restaure = open_context_db(nouvelle_machine / "contexte.sqlite3")
45+ assert conn_restaure.execute("SELECT valeur FROM test").fetchone() == ("boitier-fuji",)
46+ conn_restaure.close()
47+
48+
49+def test_restaurer_contexte_si_absent_ne_touche_pas_une_base_locale_existante(
50+ tmp_path: Path,
51+) -> None:
52+ local_work_dir = tmp_path / "travail"
53+ archive_root = tmp_path / "archive"
54+
55+ conn_local = open_context_db(local_work_dir / "contexte.sqlite3")
56+ conn_local.execute("CREATE TABLE test (valeur TEXT)")
57+ conn_local.execute("INSERT INTO test VALUES ('valeur-locale')")
58+ conn_local.commit()
59+ conn_local.close()
60+
61+ # Une sauvegarde différente existe sur l'archive, mais ne doit jamais écraser
62+ # une base locale déjà présente.
63+ (archive_root).mkdir()
64+ conn_archive = open_context_db(archive_root / ".regine-contexte.sqlite3")
65+ conn_archive.execute("CREATE TABLE test (valeur TEXT)")
66+ conn_archive.execute("INSERT INTO test VALUES ('valeur-archive')")
67+ conn_archive.commit()
68+ conn_archive.close()
69+
70+ restaure = restaurer_contexte_si_absent(local_work_dir, archive_root)
71+
72+ assert restaure is False
73+ conn_verif = open_context_db(local_work_dir / "contexte.sqlite3")
74+ assert conn_verif.execute("SELECT valeur FROM test").fetchone() == ("valeur-locale",)
75+ conn_verif.close()
76+
77+
78+def test_restaurer_contexte_si_absent_no_op_si_absent_des_deux_cotes(tmp_path: Path) -> None:
79+ local_work_dir = tmp_path / "travail"
80+ local_work_dir.mkdir()
81+ archive_root = tmp_path / "archive"
82+
83+ restaure = restaurer_contexte_si_absent(local_work_dir, archive_root)
84+
85+ assert restaure is False
86+ assert not (local_work_dir / "contexte.sqlite3").exists()
new file mode 100644
@@ -0,0 +1,86 @@
1+"""Tests de `sauvegarder_contexte`/`restaurer_contexte_si_absent` (FR-016/FR-017)."""
2+
3+from __future__ import annotations
4+
5+from pathlib import Path
6+
7+from regine_core.config.context import restaurer_contexte_si_absent, sauvegarder_contexte
8+from regine_core.config.db import open_context_db
9+
10+
11+def test_sauvegarder_contexte_no_op_si_base_locale_absente(tmp_path: Path) -> None:
12+ local_work_dir = tmp_path / "travail"
13+ local_work_dir.mkdir()
14+ archive_root = tmp_path / "archive"
15+
16+ sauvegarder_contexte(local_work_dir, archive_root) # ne doit pas lever
17+
18+ assert not (archive_root / ".regine-contexte.sqlite3").exists()
19+
20+
21+def test_sauvegarder_puis_restaurer_contexte(tmp_path: Path) -> None:
22+ local_work_dir = tmp_path / "travail"
23+ archive_root = tmp_path / "archive"
24+
25+ conn = open_context_db(local_work_dir / "contexte.sqlite3")
26+ conn.execute("CREATE TABLE test (valeur TEXT)")
27+ conn.execute("INSERT INTO test VALUES ('boitier-fuji')")
28+ conn.commit()
29+ conn.close()
30+
31+ sauvegarder_contexte(local_work_dir, archive_root)
32+
33+ assert (archive_root / ".regine-contexte.sqlite3").exists()
34+ assert (archive_root / ".regine-contexte.sqlite3").read_bytes() == (
35+ local_work_dir / "contexte.sqlite3"
36+ ).read_bytes()
37+
38+ # Simule une nouvelle machine : local_work_dir vide, l'archive a la sauvegarde.
39+ nouvelle_machine = tmp_path / "nouvelle-machine"
40+
41+ restaure = restaurer_contexte_si_absent(nouvelle_machine, archive_root)
42+
43+ assert restaure is True
44+ conn_restaure = open_context_db(nouvelle_machine / "contexte.sqlite3")
45+ assert conn_restaure.execute("SELECT valeur FROM test").fetchone() == ("boitier-fuji",)
46+ conn_restaure.close()
47+
48+
49+def test_restaurer_contexte_si_absent_ne_touche_pas_une_base_locale_existante(
50+ tmp_path: Path,
51+) -> None:
52+ local_work_dir = tmp_path / "travail"
53+ archive_root = tmp_path / "archive"
54+
55+ conn_local = open_context_db(local_work_dir / "contexte.sqlite3")
56+ conn_local.execute("CREATE TABLE test (valeur TEXT)")
57+ conn_local.execute("INSERT INTO test VALUES ('valeur-locale')")
58+ conn_local.commit()
59+ conn_local.close()
60+
61+ # Une sauvegarde différente existe sur l'archive, mais ne doit jamais écraser
62+ # une base locale déjà présente.
63+ (archive_root).mkdir()
64+ conn_archive = open_context_db(archive_root / ".regine-contexte.sqlite3")
65+ conn_archive.execute("CREATE TABLE test (valeur TEXT)")
66+ conn_archive.execute("INSERT INTO test VALUES ('valeur-archive')")
67+ conn_archive.commit()
68+ conn_archive.close()
69+
70+ restaure = restaurer_contexte_si_absent(local_work_dir, archive_root)
71+
72+ assert restaure is False
73+ conn_verif = open_context_db(local_work_dir / "contexte.sqlite3")
74+ assert conn_verif.execute("SELECT valeur FROM test").fetchone() == ("valeur-locale",)
75+ conn_verif.close()
76+
77+
78+def test_restaurer_contexte_si_absent_no_op_si_absent_des_deux_cotes(tmp_path: Path) -> None:
79+ local_work_dir = tmp_path / "travail"
80+ local_work_dir.mkdir()
81+ archive_root = tmp_path / "archive"
82+
83+ restaure = restaurer_contexte_si_absent(local_work_dir, archive_root)
84+
85+ assert restaure is False
86+ assert not (local_work_dir / "contexte.sqlite3").exists()
added packages/regine-core/tests/unit/test_environnement.py +47 -0
new file mode 100644
@@ -0,0 +1,47 @@
1+"""Tests de `regine_core.config.environnement.ouvrir_environnement` (FR-014/FR-015)."""
2+
3+from __future__ import annotations
4+
5+from pathlib import Path
6+
7+from regine_core.config.context import enregistrer_chemins
8+from regine_core.config.db import open_global_config_db
9+from regine_core.config.environnement import ouvrir_environnement
10+
11+
12+def test_contexte_conn_none_si_local_work_dir_non_configure(tmp_path: Path) -> None:
13+ config_db = tmp_path / "config.sqlite3"
14+
15+ environnement = ouvrir_environnement(config_db)
16+
17+ assert environnement.contexte_conn is None
18+ assert environnement.local_work_dir is None
19+ environnement.config_conn.close()
20+
21+
22+def test_contexte_conn_ouvre_a_la_racine_de_local_work_dir(tmp_path: Path) -> None:
23+ config_db = tmp_path / "config.sqlite3"
24+ local_work_dir = tmp_path / "travail"
25+
26+ conn = open_global_config_db(config_db)
27+ enregistrer_chemins(conn, local_work_dir=local_work_dir, creer_repertoires_manquants=True)
28+ conn.close()
29+
30+ environnement = ouvrir_environnement(config_db)
31+
32+ assert environnement.local_work_dir == local_work_dir
33+ assert environnement.contexte_conn is not None
34+ assert (local_work_dir / "contexte.sqlite3").exists()
35+ environnement.config_conn.close()
36+ environnement.contexte_conn.close()
37+
38+
39+def test_ouvrir_environnement_sans_argument_utilise_le_defaut(tmp_path: Path, monkeypatch) -> None:
40+ import regine_core.config.db as db_module
41+
42+ monkeypatch.setattr(db_module, "CHEMIN_CONFIG_GLOBALE_PAR_DEFAUT", tmp_path / "config.sqlite3")
43+
44+ environnement = ouvrir_environnement()
45+
46+ assert (tmp_path / "config.sqlite3").exists()
47+ environnement.config_conn.close()
new file mode 100644
@@ -0,0 +1,47 @@
1+"""Tests de `regine_core.config.environnement.ouvrir_environnement` (FR-014/FR-015)."""
2+
3+from __future__ import annotations
4+
5+from pathlib import Path
6+
7+from regine_core.config.context import enregistrer_chemins
8+from regine_core.config.db import open_global_config_db
9+from regine_core.config.environnement import ouvrir_environnement
10+
11+
12+def test_contexte_conn_none_si_local_work_dir_non_configure(tmp_path: Path) -> None:
13+ config_db = tmp_path / "config.sqlite3"
14+
15+ environnement = ouvrir_environnement(config_db)
16+
17+ assert environnement.contexte_conn is None
18+ assert environnement.local_work_dir is None
19+ environnement.config_conn.close()
20+
21+
22+def test_contexte_conn_ouvre_a_la_racine_de_local_work_dir(tmp_path: Path) -> None:
23+ config_db = tmp_path / "config.sqlite3"
24+ local_work_dir = tmp_path / "travail"
25+
26+ conn = open_global_config_db(config_db)
27+ enregistrer_chemins(conn, local_work_dir=local_work_dir, creer_repertoires_manquants=True)
28+ conn.close()
29+
30+ environnement = ouvrir_environnement(config_db)
31+
32+ assert environnement.local_work_dir == local_work_dir
33+ assert environnement.contexte_conn is not None
34+ assert (local_work_dir / "contexte.sqlite3").exists()
35+ environnement.config_conn.close()
36+ environnement.contexte_conn.close()
37+
38+
39+def test_ouvrir_environnement_sans_argument_utilise_le_defaut(tmp_path: Path, monkeypatch) -> None:
40+ import regine_core.config.db as db_module
41+
42+ monkeypatch.setattr(db_module, "CHEMIN_CONFIG_GLOBALE_PAR_DEFAUT", tmp_path / "config.sqlite3")
43+
44+ environnement = ouvrir_environnement()
45+
46+ assert (tmp_path / "config.sqlite3").exists()
47+ environnement.config_conn.close()
modified packages/regine-gui/src/regine_gui/app.py +4 -4
@@ -19,7 +19,7 @@ class FenetrePrincipale(QMainWindow):
1919 Story 3) sous quatre onglets d'une même fenêtre — chacun appelle sa propre
2020 partie de regine-core, aucune logique partagée entre eux ici (Principe VI)."""
2121
22- def __init__(self, contexte_db: Path | None = None) -> None:
22+ def __init__(self, config_db: Path | None = None) -> None:
2323 super().__init__()
2424 self.setWindowTitle("Régine")
2525 self.resize(760, 520)
@@ -27,12 +27,12 @@ class FenetrePrincipale(QMainWindow):
2727 onglets = QTabWidget()
2828 self.setCentralWidget(onglets)
2929 self._ecran_tri = EcranTri()
30- ecran_import = EcranImport(contexte_db=contexte_db)
30+ ecran_import = EcranImport(config_db=config_db)
3131 ecran_import.dossier_importe.connect(self._ecran_tri.ouvrir_dossier)
3232
3333 onglets.addTab(self._ecran_tri, "Tri")
34- onglets.addTab(EcranConsultation(contexte_db=contexte_db), "Consultation")
35- onglets.addTab(EcranConfiguration(contexte_db=contexte_db), "Configuration")
34+ onglets.addTab(EcranConsultation(config_db=config_db), "Consultation")
35+ onglets.addTab(EcranConfiguration(config_db=config_db), "Configuration")
3636 onglets.addTab(ecran_import, "Import")
3737
3838
@@ -19,7 +19,7 @@ class FenetrePrincipale(QMainWindow):
19 Story 3) sous quatre onglets d'une même fenêtre — chacun appelle sa propre19 Story 3) sous quatre onglets d'une même fenêtre — chacun appelle sa propre
20 partie de regine-core, aucune logique partagée entre eux ici (Principe VI)."""20 partie de regine-core, aucune logique partagée entre eux ici (Principe VI)."""
21 21
22- def __init__(self, contexte_db: Path | None = None) -> None:22+ def __init__(self, config_db: Path | None = None) -> None:
23 super().__init__()23 super().__init__()
24 self.setWindowTitle("Régine")24 self.setWindowTitle("Régine")
25 self.resize(760, 520)25 self.resize(760, 520)
@@ -27,12 +27,12 @@ class FenetrePrincipale(QMainWindow):
27 onglets = QTabWidget()27 onglets = QTabWidget()
28 self.setCentralWidget(onglets)28 self.setCentralWidget(onglets)
29 self._ecran_tri = EcranTri()29 self._ecran_tri = EcranTri()
30- ecran_import = EcranImport(contexte_db=contexte_db)30+ ecran_import = EcranImport(config_db=config_db)
31 ecran_import.dossier_importe.connect(self._ecran_tri.ouvrir_dossier)31 ecran_import.dossier_importe.connect(self._ecran_tri.ouvrir_dossier)
32 32
33 onglets.addTab(self._ecran_tri, "Tri")33 onglets.addTab(self._ecran_tri, "Tri")
34- onglets.addTab(EcranConsultation(contexte_db=contexte_db), "Consultation")34+ onglets.addTab(EcranConsultation(config_db=config_db), "Consultation")
35- onglets.addTab(EcranConfiguration(contexte_db=contexte_db), "Configuration")35+ onglets.addTab(EcranConfiguration(config_db=config_db), "Configuration")
36 onglets.addTab(ecran_import, "Import")36 onglets.addTab(ecran_import, "Import")
37 37
38 38
modified packages/regine-gui/src/regine_gui/config_view.py +58 -15
@@ -5,6 +5,12 @@ Chaque exception de `regine_core.config.context.enregistrer_chemins` est
55 présentée clairement (boîte de dialogue), jamais comme une exception technique
66 brute — mêmes règles que la CLI (`regine config set-paths`), jamais devinées ou
77 recalculées ici.
8+
9+**Révisé le 2026-09-23/24** (FR-014/015/016/017) : la configuration globale
10+(chemins, état SMB) et la base de contexte centralisée (boîtiers) sont
11+désormais deux connexions distinctes, résolues ensemble par
12+`regine_core.config.environnement.ouvrir_environnement` — remplace l'ancienne
13+connexion unique (`open_context_db(self._contexte_db)`).
814 """
915
1016 from __future__ import annotations
@@ -36,8 +42,10 @@ from regine_core.config.context import (
3642 TravailNonReconcilieError,
3743 charger_contexte,
3844 enregistrer_chemins,
45+ restaurer_contexte_si_absent,
3946 )
40-from regine_core.config.db import open_context_db
47+from regine_core.config.db import open_global_config_db
48+from regine_core.config.environnement import Environnement, ouvrir_environnement
4149
4250 _ROLE_ID = Qt.ItemDataRole.UserRole
4351
@@ -46,9 +54,9 @@ class EcranConfiguration(QWidget):
4654 """Onglet de configuration : chemins, état SMB, boîtiers nommés — même
4755 contexte que la CLI (`regine config ...`), jamais un état parallèle (SC-008)."""
4856
49- def __init__(self, contexte_db: Path | None = None) -> None:
57+ def __init__(self, config_db: Path | None = None) -> None:
5058 super().__init__()
51- self._contexte_db = contexte_db
59+ self._config_db = config_db
5260
5361 disposition = QVBoxLayout(self)
5462
@@ -91,13 +99,19 @@ class EcranConfiguration(QWidget):
9199
92100 self._rafraichir()
93101
94- def _ouvrir_conn(self):
95- return open_context_db(self._contexte_db)
102+ def _ouvrir_environnement(self) -> Environnement:
103+ return ouvrir_environnement(self._config_db)
104+
105+ @staticmethod
106+ def _fermer(environnement: Environnement) -> None:
107+ environnement.config_conn.close()
108+ if environnement.contexte_conn is not None:
109+ environnement.contexte_conn.close()
96110
97111 def _rafraichir(self) -> None:
98- conn = self._ouvrir_conn()
112+ environnement = self._ouvrir_environnement()
99113 try:
100- contexte = charger_contexte(conn)
114+ contexte = charger_contexte(environnement.config_conn)
101115 self._champ_temp_dir.setText(str(contexte.temp_dir) if contexte.temp_dir else "")
102116 self._champ_local_work_dir.setText(
103117 str(contexte.local_work_dir) if contexte.local_work_dir else ""
@@ -106,10 +120,11 @@ class EcranConfiguration(QWidget):
106120 self._label_etat_smb.setText(f"État SMB : {contexte.smb_last_known_state}")
107121
108122 self._liste_boitiers.clear()
109- for boitier in lister_boitiers(conn):
110- self._ajouter_boitier(boitier)
123+ if environnement.contexte_conn is not None:
124+ for boitier in lister_boitiers(environnement.contexte_conn):
125+ self._ajouter_boitier(boitier)
111126 finally:
112- conn.close()
127+ self._fermer(environnement)
113128
114129 def _ajouter_boitier(self, boitier: Boitier) -> None:
115130 nom = boitier.nom_lisible or "(non nommé)"
@@ -119,9 +134,9 @@ class EcranConfiguration(QWidget):
119134 self._liste_boitiers.addItem(item)
120135
121136 def _enregistrer(self) -> None:
122- conn = self._ouvrir_conn()
137+ conn = open_global_config_db(self._config_db)
123138 try:
124- enregistrer_chemins(
139+ contexte = enregistrer_chemins(
125140 conn,
126141 temp_dir=Path(self._champ_temp_dir.text()) if self._champ_temp_dir.text() else None,
127142 local_work_dir=Path(self._champ_local_work_dir.text())
@@ -155,8 +170,28 @@ class EcranConfiguration(QWidget):
155170 finally:
156171 conn.close()
157172
173+ self._tenter_restauration_contexte(contexte)
158174 self._rafraichir()
159175
176+ def _tenter_restauration_contexte(self, contexte) -> None:
177+ """FR-017 : sur une nouvelle machine, retrouve le contexte déjà connu de
178+ l'archive dès que les deux chemins nécessaires sont réunis — silencieux
179+ si l'un des deux manque encore, ou si l'archive n'est pas accessible à cet
180+ instant (retentée au premier checkout)."""
181+ if contexte.local_work_dir is None or contexte.archive_smb_path is None:
182+ return
183+
184+ from regine_core.config.smb import chemin_local_du_partage, disponible # noqa: PLC0415
185+
186+ if not disponible(contexte.archive_smb_path):
187+ return
188+
189+ archive_root_local = chemin_local_du_partage(contexte.archive_smb_path)
190+ if restaurer_contexte_si_absent(contexte.local_work_dir, archive_root_local):
191+ QMessageBox.information(
192+ self, "Contexte restauré", "Contexte de travail restauré depuis l'archive."
193+ )
194+
160195 def _nommer_boitier_selectionne(self) -> None:
161196 item = self._liste_boitiers.currentItem()
162197 if item is None:
@@ -167,13 +202,21 @@ class EcranConfiguration(QWidget):
167202 if not ok or not nom.strip():
168203 return
169204
170- conn = self._ouvrir_conn()
205+ environnement = self._ouvrir_environnement()
206+ if environnement.contexte_conn is None:
207+ self._fermer(environnement)
208+ QMessageBox.warning(
209+ self,
210+ "Répertoire non configuré",
211+ "Configurez d'abord le répertoire de travail local.",
212+ )
213+ return
171214 try:
172- nommer_boitier(conn, boitier_id, nom.strip())
215+ nommer_boitier(environnement.contexte_conn, boitier_id, nom.strip())
173216 except BoitierInconnuError as exc:
174217 QMessageBox.warning(self, "Boîtier inconnu", str(exc))
175218 return
176219 finally:
177- conn.close()
220+ self._fermer(environnement)
178221
179222 self._rafraichir()
@@ -5,6 +5,12 @@ Chaque exception de `regine_core.config.context.enregistrer_chemins` est
5 présentée clairement (boîte de dialogue), jamais comme une exception technique5 présentée clairement (boîte de dialogue), jamais comme une exception technique
6 brute — mêmes règles que la CLI (`regine config set-paths`), jamais devinées ou6 brute — mêmes règles que la CLI (`regine config set-paths`), jamais devinées ou
7 recalculées ici.7 recalculées ici.
8+
9+**Révisé le 2026-09-23/24** (FR-014/015/016/017) : la configuration globale
10+(chemins, état SMB) et la base de contexte centralisée (boîtiers) sont
11+désormais deux connexions distinctes, résolues ensemble par
12+`regine_core.config.environnement.ouvrir_environnement` — remplace l'ancienne
13+connexion unique (`open_context_db(self._contexte_db)`).
8 """14 """
9 15
10 from __future__ import annotations16 from __future__ import annotations
@@ -36,8 +42,10 @@ from regine_core.config.context import (
36 TravailNonReconcilieError,42 TravailNonReconcilieError,
37 charger_contexte,43 charger_contexte,
38 enregistrer_chemins,44 enregistrer_chemins,
45+ restaurer_contexte_si_absent,
39 )46 )
40-from regine_core.config.db import open_context_db47+from regine_core.config.db import open_global_config_db
48+from regine_core.config.environnement import Environnement, ouvrir_environnement
41 49
42 _ROLE_ID = Qt.ItemDataRole.UserRole50 _ROLE_ID = Qt.ItemDataRole.UserRole
43 51
@@ -46,9 +54,9 @@ class EcranConfiguration(QWidget):
46 """Onglet de configuration : chemins, état SMB, boîtiers nommés — même54 """Onglet de configuration : chemins, état SMB, boîtiers nommés — même
47 contexte que la CLI (`regine config ...`), jamais un état parallèle (SC-008)."""55 contexte que la CLI (`regine config ...`), jamais un état parallèle (SC-008)."""
48 56
49- def __init__(self, contexte_db: Path | None = None) -> None:57+ def __init__(self, config_db: Path | None = None) -> None:
50 super().__init__()58 super().__init__()
51- self._contexte_db = contexte_db59+ self._config_db = config_db
52 60
53 disposition = QVBoxLayout(self)61 disposition = QVBoxLayout(self)
54 62
@@ -91,13 +99,19 @@ class EcranConfiguration(QWidget):
91 99
92 self._rafraichir()100 self._rafraichir()
93 101
94- def _ouvrir_conn(self):102+ def _ouvrir_environnement(self) -> Environnement:
95- return open_context_db(self._contexte_db)103+ return ouvrir_environnement(self._config_db)
104+
105+ @staticmethod
106+ def _fermer(environnement: Environnement) -> None:
107+ environnement.config_conn.close()
108+ if environnement.contexte_conn is not None:
109+ environnement.contexte_conn.close()
96 110
97 def _rafraichir(self) -> None:111 def _rafraichir(self) -> None:
98- conn = self._ouvrir_conn()112+ environnement = self._ouvrir_environnement()
99 try:113 try:
100- contexte = charger_contexte(conn)114+ contexte = charger_contexte(environnement.config_conn)
101 self._champ_temp_dir.setText(str(contexte.temp_dir) if contexte.temp_dir else "")115 self._champ_temp_dir.setText(str(contexte.temp_dir) if contexte.temp_dir else "")
102 self._champ_local_work_dir.setText(116 self._champ_local_work_dir.setText(
103 str(contexte.local_work_dir) if contexte.local_work_dir else ""117 str(contexte.local_work_dir) if contexte.local_work_dir else ""
@@ -106,10 +120,11 @@ class EcranConfiguration(QWidget):
106 self._label_etat_smb.setText(f"État SMB : {contexte.smb_last_known_state}")120 self._label_etat_smb.setText(f"État SMB : {contexte.smb_last_known_state}")
107 121
108 self._liste_boitiers.clear()122 self._liste_boitiers.clear()
109- for boitier in lister_boitiers(conn):123+ if environnement.contexte_conn is not None:
110- self._ajouter_boitier(boitier)124+ for boitier in lister_boitiers(environnement.contexte_conn):
125+ self._ajouter_boitier(boitier)
111 finally:126 finally:
112- conn.close()127+ self._fermer(environnement)
113 128
114 def _ajouter_boitier(self, boitier: Boitier) -> None:129 def _ajouter_boitier(self, boitier: Boitier) -> None:
115 nom = boitier.nom_lisible or "(non nommé)"130 nom = boitier.nom_lisible or "(non nommé)"
@@ -119,9 +134,9 @@ class EcranConfiguration(QWidget):
119 self._liste_boitiers.addItem(item)134 self._liste_boitiers.addItem(item)
120 135
121 def _enregistrer(self) -> None:136 def _enregistrer(self) -> None:
122- conn = self._ouvrir_conn()137+ conn = open_global_config_db(self._config_db)
123 try:138 try:
124- enregistrer_chemins(139+ contexte = enregistrer_chemins(
125 conn,140 conn,
126 temp_dir=Path(self._champ_temp_dir.text()) if self._champ_temp_dir.text() else None,141 temp_dir=Path(self._champ_temp_dir.text()) if self._champ_temp_dir.text() else None,
127 local_work_dir=Path(self._champ_local_work_dir.text())142 local_work_dir=Path(self._champ_local_work_dir.text())
@@ -155,8 +170,28 @@ class EcranConfiguration(QWidget):
155 finally:170 finally:
156 conn.close()171 conn.close()
157 172
173+ self._tenter_restauration_contexte(contexte)
158 self._rafraichir()174 self._rafraichir()
159 175
176+ def _tenter_restauration_contexte(self, contexte) -> None:
177+ """FR-017 : sur une nouvelle machine, retrouve le contexte déjà connu de
178+ l'archive dès que les deux chemins nécessaires sont réunis — silencieux
179+ si l'un des deux manque encore, ou si l'archive n'est pas accessible à cet
180+ instant (retentée au premier checkout)."""
181+ if contexte.local_work_dir is None or contexte.archive_smb_path is None:
182+ return
183+
184+ from regine_core.config.smb import chemin_local_du_partage, disponible # noqa: PLC0415
185+
186+ if not disponible(contexte.archive_smb_path):
187+ return
188+
189+ archive_root_local = chemin_local_du_partage(contexte.archive_smb_path)
190+ if restaurer_contexte_si_absent(contexte.local_work_dir, archive_root_local):
191+ QMessageBox.information(
192+ self, "Contexte restauré", "Contexte de travail restauré depuis l'archive."
193+ )
194+
160 def _nommer_boitier_selectionne(self) -> None:195 def _nommer_boitier_selectionne(self) -> None:
161 item = self._liste_boitiers.currentItem()196 item = self._liste_boitiers.currentItem()
162 if item is None:197 if item is None:
@@ -167,13 +202,21 @@ class EcranConfiguration(QWidget):
167 if not ok or not nom.strip():202 if not ok or not nom.strip():
168 return203 return
169 204
170- conn = self._ouvrir_conn()205+ environnement = self._ouvrir_environnement()
206+ if environnement.contexte_conn is None:
207+ self._fermer(environnement)
208+ QMessageBox.warning(
209+ self,
210+ "Répertoire non configuré",
211+ "Configurez d'abord le répertoire de travail local.",
212+ )
213+ return
171 try:214 try:
172- nommer_boitier(conn, boitier_id, nom.strip())215+ nommer_boitier(environnement.contexte_conn, boitier_id, nom.strip())
173 except BoitierInconnuError as exc:216 except BoitierInconnuError as exc:
174 QMessageBox.warning(self, "Boîtier inconnu", str(exc))217 QMessageBox.warning(self, "Boîtier inconnu", str(exc))
175 return218 return
176 finally:219 finally:
177- conn.close()220+ self._fermer(environnement)
178 221
179 self._rafraichir()222 self._rafraichir()
modified packages/regine-gui/src/regine_gui/consultation_view.py +9 -4
@@ -1,6 +1,11 @@
11 """Écran de consultation (US2) — appelle exclusivement `regine_core.archive.consultation`
22 et `regine_core.config`, aucune logique métier propre (Principe VI). Lecture seule
33 stricte : aucune action d'écriture (promotion, suppression) n'est proposée ici (FR-010).
4+
5+**Révisé le 2026-09-23/24** : n'a besoin que de la configuration globale
6+(chemins, `open_global_config_db`) — jamais de la base de contexte centralisée
7+(boîtiers), donc jamais d'écriture/création de `contexte.sqlite3` comme simple
8+effet de bord d'une consultation en lecture seule.
49 """
510
611 from __future__ import annotations
@@ -23,7 +28,7 @@ from PySide6.QtWidgets import (
2328 )
2429 from regine_core.archive.consultation import RestaurationCollisionError, rechercher, restaurer
2530 from regine_core.config.context import charger_contexte
26-from regine_core.config.db import open_context_db
31+from regine_core.config.db import open_global_config_db
2732 from regine_core.config.smb import resoudre_archive_locale
2833
2934 _ROLE_CHEMIN = Qt.ItemDataRole.UserRole
@@ -34,11 +39,11 @@ class EcranConsultation(QMainWindow):
3439 (`specs/003`) : recherche (nom de dossier, date, titre) et restauration ciblée
3540 d'un fichier précis, sans checkout ni verrou du dossier entier (FR-008/009/010)."""
3641
37- def __init__(self, contexte_db: Path | None = None) -> None:
42+ def __init__(self, config_db: Path | None = None) -> None:
3843 super().__init__()
3944 self.setWindowTitle("Régine — Consultation de l'archive")
4045 self.resize(720, 480)
41- self._contexte_db = contexte_db
46+ self._config_db = config_db
4247 self._archive_root_local: Path | None = None
4348
4449 conteneur = QWidget()
@@ -69,7 +74,7 @@ class EcranConsultation(QMainWindow):
6974 if self._archive_root_local is not None:
7075 return self._archive_root_local
7176
72- conn = open_context_db(self._contexte_db)
77+ conn = open_global_config_db(self._config_db)
7378 try:
7479 contexte = charger_contexte(conn)
7580 if contexte.archive_smb_path is None:
@@ -1,6 +1,11 @@
1 """Écran de consultation (US2) — appelle exclusivement `regine_core.archive.consultation`1 """Écran de consultation (US2) — appelle exclusivement `regine_core.archive.consultation`
2 et `regine_core.config`, aucune logique métier propre (Principe VI). Lecture seule2 et `regine_core.config`, aucune logique métier propre (Principe VI). Lecture seule
3 stricte : aucune action d'écriture (promotion, suppression) n'est proposée ici (FR-010).3 stricte : aucune action d'écriture (promotion, suppression) n'est proposée ici (FR-010).
4+
5+**Révisé le 2026-09-23/24** : n'a besoin que de la configuration globale
6+(chemins, `open_global_config_db`) — jamais de la base de contexte centralisée
7+(boîtiers), donc jamais d'écriture/création de `contexte.sqlite3` comme simple
8+effet de bord d'une consultation en lecture seule.
4 """9 """
5 10
6 from __future__ import annotations11 from __future__ import annotations
@@ -23,7 +28,7 @@ from PySide6.QtWidgets import (
23 )28 )
24 from regine_core.archive.consultation import RestaurationCollisionError, rechercher, restaurer29 from regine_core.archive.consultation import RestaurationCollisionError, rechercher, restaurer
25 from regine_core.config.context import charger_contexte30 from regine_core.config.context import charger_contexte
26-from regine_core.config.db import open_context_db31+from regine_core.config.db import open_global_config_db
27 from regine_core.config.smb import resoudre_archive_locale32 from regine_core.config.smb import resoudre_archive_locale
28 33
29 _ROLE_CHEMIN = Qt.ItemDataRole.UserRole34 _ROLE_CHEMIN = Qt.ItemDataRole.UserRole
@@ -34,11 +39,11 @@ class EcranConsultation(QMainWindow):
34 (`specs/003`) : recherche (nom de dossier, date, titre) et restauration ciblée39 (`specs/003`) : recherche (nom de dossier, date, titre) et restauration ciblée
35 d'un fichier précis, sans checkout ni verrou du dossier entier (FR-008/009/010)."""40 d'un fichier précis, sans checkout ni verrou du dossier entier (FR-008/009/010)."""
36 41
37- def __init__(self, contexte_db: Path | None = None) -> None:42+ def __init__(self, config_db: Path | None = None) -> None:
38 super().__init__()43 super().__init__()
39 self.setWindowTitle("Régine — Consultation de l'archive")44 self.setWindowTitle("Régine — Consultation de l'archive")
40 self.resize(720, 480)45 self.resize(720, 480)
41- self._contexte_db = contexte_db46+ self._config_db = config_db
42 self._archive_root_local: Path | None = None47 self._archive_root_local: Path | None = None
43 48
44 conteneur = QWidget()49 conteneur = QWidget()
@@ -69,7 +74,7 @@ class EcranConsultation(QMainWindow):
69 if self._archive_root_local is not None:74 if self._archive_root_local is not None:
70 return self._archive_root_local75 return self._archive_root_local
71 76
72- conn = open_context_db(self._contexte_db)77+ conn = open_global_config_db(self._config_db)
73 try:78 try:
74 contexte = charger_contexte(conn)79 contexte = charger_contexte(conn)
75 if contexte.archive_smb_path is None:80 if contexte.archive_smb_path is None:
modified packages/regine-gui/src/regine_gui/import_view.py +31 -20
@@ -17,6 +17,13 @@ faire une fois la sélection du photographe terminée. Conséquence : l'onglet
1717 n'a plus besoin que le partage SMB configuré soit actuellement monté pour
1818 importer (seul son chemin sert à calculer où le dossier ira plus tard), ce qui
1919 permet aussi de tester l'import sans NAS réel.
20+
21+**Révision (2026-09-23/24)** : la configuration globale (chemins, état SMB) et
22+la base de contexte centralisée (boîtiers, catégories, historique d'imports)
23+sont désormais deux connexions distinctes, résolues ensemble par
24+`regine_core.config.environnement.ouvrir_environnement` — remplace l'ancienne
25+connexion unique (`open_context_db(self._contexte_db)`, désormais
26+`self._contexte_conn`, uniquement la base de contexte centralisée).
2027 """
2128
2229 from __future__ import annotations
@@ -41,7 +48,7 @@ from regine_core.camera_profile.db import list_boitiers
4148 from regine_core.camera_profile.resolve import assign_manual_source
4249 from regine_core.config.categories import list_known_categories, register_category_usage
4350 from regine_core.config.context import charger_contexte
44-from regine_core.config.db import open_context_db
51+from regine_core.config.environnement import ouvrir_environnement
4552 from regine_core.config.smb import chemin_local_du_partage, disponible
4653 from regine_core.import_carte.classement import classer_par_format
4754 from regine_core.import_carte.copie import (
@@ -123,12 +130,12 @@ class EcranImport(QMainWindow):
123130 #: `app.py` propose d'ouvrir le dossier importé dans l'onglet Tri.
124131 dossier_importe = Signal(Path)
125132
126- def __init__(self, contexte_db: Path | None = None) -> None:
133+ def __init__(self, config_db: Path | None = None) -> None:
127134 super().__init__()
128135 self.setWindowTitle("Régine — Import")
129136 self.resize(720, 520)
130- self._contexte_db = contexte_db
131- self._conn = None
137+ self._config_db = config_db
138+ self._contexte_conn = None
132139 self._carte: Path | None = None
133140 self._archive_root: Path | None = None
134141 self._local_root: Path | None = None
@@ -174,9 +181,6 @@ class EcranImport(QMainWindow):
174181 def _log(self, message: str) -> None:
175182 self._journal.append(message)
176183
177- def _ouvrir_conn(self):
178- return open_context_db(self._contexte_db)
179-
180184 def _choisir_carte(self) -> None:
181185 chemin = QFileDialog.getExistingDirectory(self, "Choisir une carte mémoire")
182186 if chemin:
@@ -195,10 +199,10 @@ class EcranImport(QMainWindow):
195199 )
196200 return
197201
198- conn = self._ouvrir_conn()
199- contexte = charger_contexte(conn)
200- if contexte.local_work_dir is None:
201- conn.close()
202+ environnement = ouvrir_environnement(self._config_db)
203+ contexte = charger_contexte(environnement.config_conn)
204+ if environnement.contexte_conn is None:
205+ environnement.config_conn.close()
202206 QMessageBox.warning(
203207 self,
204208 "Contexte non configuré",
@@ -207,7 +211,8 @@ class EcranImport(QMainWindow):
207211 return
208212
209213 if contexte.archive_smb_path is None:
210- conn.close()
214+ environnement.config_conn.close()
215+ environnement.contexte_conn.close()
211216 QMessageBox.warning(
212217 self,
213218 "Archive non configurée",
@@ -231,6 +236,12 @@ class EcranImport(QMainWindow):
231236 local_root = contexte.local_work_dir
232237 local_tmp = local_root / ".regine-import-tmp"
233238
239+ # La configuration globale n'est plus nécessaire pour le reste de la
240+ # session : seule la base de contexte centralisée (boîtiers, catégories,
241+ # historique) l'est encore.
242+ environnement.config_conn.close()
243+ conn = environnement.contexte_conn
244+
234245 try:
235246 fichiers = copier_carte(self._carte, local_tmp)
236247 except EchecVerificationError as exc:
@@ -258,7 +269,7 @@ class EcranImport(QMainWindow):
258269 groupes = decouper_en_groupes(fichiers)
259270 groupes = self._proposer_detachement(groupes)
260271
261- self._conn = conn
272+ self._contexte_conn = conn
262273 self._archive_root = archive_root
263274 self._local_root = local_root
264275 self._local_tmp = local_tmp
@@ -366,9 +377,9 @@ class EcranImport(QMainWindow):
366377 des groupes, sinon ferme la connexion à la base de contexte."""
367378 if self._groupes:
368379 self._bouton_traiter.setEnabled(True)
369- elif self._conn is not None:
370- self._conn.close()
371- self._conn = None
380+ elif self._contexte_conn is not None:
381+ self._contexte_conn.close()
382+ self._contexte_conn = None
372383 self._label_groupes.setText("Tous les groupes ont été traités.")
373384
374385 def _traiter_groupe(self, groupe: GroupeImport) -> bool:
@@ -376,11 +387,11 @@ class EcranImport(QMainWindow):
376387 si l'archivage a été lancé en arrière-plan (finalisation différée
377388 jusqu'au signal `succes`/`echec` du worker), `False` si le groupe a été
378389 ignoré/annulé (finalisation immédiate par l'appelant)."""
379- assert self._conn is not None
390+ assert self._contexte_conn is not None
380391 assert self._archive_root is not None
381392 assert self._local_root is not None
382393 assert self._local_tmp is not None
383- conn = self._conn
394+ conn = self._contexte_conn
384395 archive_root = self._archive_root
385396 local_root = self._local_root
386397 local_tmp = self._local_tmp
@@ -538,8 +549,8 @@ class EcranImport(QMainWindow):
538549
539550 def _sur_publication_succes(self, dossier_local: Path) -> None:
540551 dossier_archive = self._dossier_archive_en_cours
541- if self._conn is not None:
542- enregistrer_import(self._conn, self._checksums_en_cours, dossier_local)
552+ if self._contexte_conn is not None:
553+ enregistrer_import(self._contexte_conn, self._checksums_en_cours, dossier_local)
543554 self._log(f"Copie de travail locale prête : {dossier_local}")
544555 self._log(
545556 "Rien n'a été écrit sur l'archive. Une fois votre sélection terminée, "
@@ -17,6 +17,13 @@ faire une fois la sélection du photographe terminée. Conséquence : l'onglet
17 n'a plus besoin que le partage SMB configuré soit actuellement monté pour17 n'a plus besoin que le partage SMB configuré soit actuellement monté pour
18 importer (seul son chemin sert à calculer où le dossier ira plus tard), ce qui18 importer (seul son chemin sert à calculer où le dossier ira plus tard), ce qui
19 permet aussi de tester l'import sans NAS réel.19 permet aussi de tester l'import sans NAS réel.
20+
21+**Révision (2026-09-23/24)** : la configuration globale (chemins, état SMB) et
22+la base de contexte centralisée (boîtiers, catégories, historique d'imports)
23+sont désormais deux connexions distinctes, résolues ensemble par
24+`regine_core.config.environnement.ouvrir_environnement` — remplace l'ancienne
25+connexion unique (`open_context_db(self._contexte_db)`, désormais
26+`self._contexte_conn`, uniquement la base de contexte centralisée).
20 """27 """
21 28
22 from __future__ import annotations29 from __future__ import annotations
@@ -41,7 +48,7 @@ from regine_core.camera_profile.db import list_boitiers
41 from regine_core.camera_profile.resolve import assign_manual_source48 from regine_core.camera_profile.resolve import assign_manual_source
42 from regine_core.config.categories import list_known_categories, register_category_usage49 from regine_core.config.categories import list_known_categories, register_category_usage
43 from regine_core.config.context import charger_contexte50 from regine_core.config.context import charger_contexte
44-from regine_core.config.db import open_context_db51+from regine_core.config.environnement import ouvrir_environnement
45 from regine_core.config.smb import chemin_local_du_partage, disponible52 from regine_core.config.smb import chemin_local_du_partage, disponible
46 from regine_core.import_carte.classement import classer_par_format53 from regine_core.import_carte.classement import classer_par_format
47 from regine_core.import_carte.copie import (54 from regine_core.import_carte.copie import (
@@ -123,12 +130,12 @@ class EcranImport(QMainWindow):
123 #: `app.py` propose d'ouvrir le dossier importé dans l'onglet Tri.130 #: `app.py` propose d'ouvrir le dossier importé dans l'onglet Tri.
124 dossier_importe = Signal(Path)131 dossier_importe = Signal(Path)
125 132
126- def __init__(self, contexte_db: Path | None = None) -> None:133+ def __init__(self, config_db: Path | None = None) -> None:
127 super().__init__()134 super().__init__()
128 self.setWindowTitle("Régine — Import")135 self.setWindowTitle("Régine — Import")
129 self.resize(720, 520)136 self.resize(720, 520)
130- self._contexte_db = contexte_db137+ self._config_db = config_db
131- self._conn = None138+ self._contexte_conn = None
132 self._carte: Path | None = None139 self._carte: Path | None = None
133 self._archive_root: Path | None = None140 self._archive_root: Path | None = None
134 self._local_root: Path | None = None141 self._local_root: Path | None = None
@@ -174,9 +181,6 @@ class EcranImport(QMainWindow):
174 def _log(self, message: str) -> None:181 def _log(self, message: str) -> None:
175 self._journal.append(message)182 self._journal.append(message)
176 183
177- def _ouvrir_conn(self):
178- return open_context_db(self._contexte_db)
179-
180 def _choisir_carte(self) -> None:184 def _choisir_carte(self) -> None:
181 chemin = QFileDialog.getExistingDirectory(self, "Choisir une carte mémoire")185 chemin = QFileDialog.getExistingDirectory(self, "Choisir une carte mémoire")
182 if chemin:186 if chemin:
@@ -195,10 +199,10 @@ class EcranImport(QMainWindow):
195 )199 )
196 return200 return
197 201
198- conn = self._ouvrir_conn()202+ environnement = ouvrir_environnement(self._config_db)
199- contexte = charger_contexte(conn)203+ contexte = charger_contexte(environnement.config_conn)
200- if contexte.local_work_dir is None:204+ if environnement.contexte_conn is None:
201- conn.close()205+ environnement.config_conn.close()
202 QMessageBox.warning(206 QMessageBox.warning(
203 self,207 self,
204 "Contexte non configuré",208 "Contexte non configuré",
@@ -207,7 +211,8 @@ class EcranImport(QMainWindow):
207 return211 return
208 212
209 if contexte.archive_smb_path is None:213 if contexte.archive_smb_path is None:
210- conn.close()214+ environnement.config_conn.close()
215+ environnement.contexte_conn.close()
211 QMessageBox.warning(216 QMessageBox.warning(
212 self,217 self,
213 "Archive non configurée",218 "Archive non configurée",
@@ -231,6 +236,12 @@ class EcranImport(QMainWindow):
231 local_root = contexte.local_work_dir236 local_root = contexte.local_work_dir
232 local_tmp = local_root / ".regine-import-tmp"237 local_tmp = local_root / ".regine-import-tmp"
233 238
239+ # La configuration globale n'est plus nécessaire pour le reste de la
240+ # session : seule la base de contexte centralisée (boîtiers, catégories,
241+ # historique) l'est encore.
242+ environnement.config_conn.close()
243+ conn = environnement.contexte_conn
244+
234 try:245 try:
235 fichiers = copier_carte(self._carte, local_tmp)246 fichiers = copier_carte(self._carte, local_tmp)
236 except EchecVerificationError as exc:247 except EchecVerificationError as exc:
@@ -258,7 +269,7 @@ class EcranImport(QMainWindow):
258 groupes = decouper_en_groupes(fichiers)269 groupes = decouper_en_groupes(fichiers)
259 groupes = self._proposer_detachement(groupes)270 groupes = self._proposer_detachement(groupes)
260 271
261- self._conn = conn272+ self._contexte_conn = conn
262 self._archive_root = archive_root273 self._archive_root = archive_root
263 self._local_root = local_root274 self._local_root = local_root
264 self._local_tmp = local_tmp275 self._local_tmp = local_tmp
@@ -366,9 +377,9 @@ class EcranImport(QMainWindow):
366 des groupes, sinon ferme la connexion à la base de contexte."""377 des groupes, sinon ferme la connexion à la base de contexte."""
367 if self._groupes:378 if self._groupes:
368 self._bouton_traiter.setEnabled(True)379 self._bouton_traiter.setEnabled(True)
369- elif self._conn is not None:380+ elif self._contexte_conn is not None:
370- self._conn.close()381+ self._contexte_conn.close()
371- self._conn = None382+ self._contexte_conn = None
372 self._label_groupes.setText("Tous les groupes ont été traités.")383 self._label_groupes.setText("Tous les groupes ont été traités.")
373 384
374 def _traiter_groupe(self, groupe: GroupeImport) -> bool:385 def _traiter_groupe(self, groupe: GroupeImport) -> bool:
@@ -376,11 +387,11 @@ class EcranImport(QMainWindow):
376 si l'archivage a été lancé en arrière-plan (finalisation différée387 si l'archivage a été lancé en arrière-plan (finalisation différée
377 jusqu'au signal `succes`/`echec` du worker), `False` si le groupe a été388 jusqu'au signal `succes`/`echec` du worker), `False` si le groupe a été
378 ignoré/annulé (finalisation immédiate par l'appelant)."""389 ignoré/annulé (finalisation immédiate par l'appelant)."""
379- assert self._conn is not None390+ assert self._contexte_conn is not None
380 assert self._archive_root is not None391 assert self._archive_root is not None
381 assert self._local_root is not None392 assert self._local_root is not None
382 assert self._local_tmp is not None393 assert self._local_tmp is not None
383- conn = self._conn394+ conn = self._contexte_conn
384 archive_root = self._archive_root395 archive_root = self._archive_root
385 local_root = self._local_root396 local_root = self._local_root
386 local_tmp = self._local_tmp397 local_tmp = self._local_tmp
@@ -538,8 +549,8 @@ class EcranImport(QMainWindow):
538 549
539 def _sur_publication_succes(self, dossier_local: Path) -> None:550 def _sur_publication_succes(self, dossier_local: Path) -> None:
540 dossier_archive = self._dossier_archive_en_cours551 dossier_archive = self._dossier_archive_en_cours
541- if self._conn is not None:552+ if self._contexte_conn is not None:
542- enregistrer_import(self._conn, self._checksums_en_cours, dossier_local)553+ enregistrer_import(self._contexte_conn, self._checksums_en_cours, dossier_local)
543 self._log(f"Copie de travail locale prête : {dossier_local}")554 self._log(f"Copie de travail locale prête : {dossier_local}")
544 self._log(555 self._log(
545 "Rien n'a été écrit sur l'archive. Une fois votre sélection terminée, "556 "Rien n'a été écrit sur l'archive. Une fois votre sélection terminée, "
modified specs/001-import-photos/contracts/cli-import.md +1 -1
@@ -37,7 +37,7 @@ regine import /Volumes/CARTE_SD [--titre TEXTE] [--destination nouveau|sous-doss
3737
3838 - `--destination` accepte exactement `nouveau`, `parent`, `sous-dossier:CHEMIN_RELATIF`, `fusion:CHEMIN_RELATIF` (le préfixe suit `id_cible` après le `:`) — `CHEMIN_RELATIF` est relatif à `--archive-root`/`--local-root` (ex. `voyage/2026-08_Montenegro`), tel que retourné par `lister_dossiers_candidats` ou saisi manuellement. En son absence en mode interactif, une question à 4 choix est posée ; en mode `--yes` sans `--destination`, le comportement par défaut est `nouveau_dossier` (US1, non régressif).
3939 - Un `nouveau_sous_dossier` est physiquement imbriqué sous le dossier parent réel (`archive_root/CHEMIN_RELATIF/nom_etape/...`), pas seulement sous la racine catégorie/année héritée — `regine_core.import_carte.destination.resoudre_destination` attend un `dossier_cible` déjà construit par l'appelant pour ce type (elle ne le calcule pas elle-même).
40-- `--contexte-db` (optionnel, défaut `<local_root>/.regine-contexte.sqlite3`) ouvre la base de contexte centralisée (`regine_core.config.db`) utilisée pour la désambiguïsation de boîtiers (specs/002) et le cache de catégories (specs/004).
40+- **Révisé le 2026-09-23/24 (`specs/003-config-contexte-travail` FR-006/FR-015)** : plus de flag `--contexte-db` séparé — la base de contexte centralisée (désambiguïsation de boîtiers specs/002, cache de catégories specs/004, historique d'imports FR-004) vit désormais systématiquement à `<local_root>/contexte.sqlite3` (`regine_core.config.db.open_context_db(Path(args.local_root))`), sans possibilité de la faire diverger de `--local-root`. **Bug réel corrigé à cette occasion** : la version précédente pointait par défaut vers `<local_root>/.regine-contexte.sqlite3` (préfixe `.`, nom différent), un chemin distinct de celui utilisé par toutes les autres commandes CLI (`~/.regine/contexte.sqlite3` à l'époque) — un import réel n'écrivait donc jamais dans la même base que le reste de Régine, silencieusement (trouvé par l'utilisateur, cf. `specs/003-config-contexte-travail/contracts/regine-core-api.md`).
4141 - **Limitation connue** : la désambiguïsation automatique de boîtiers par tag de modèle (FR-015) n'opère que sur les fichiers copiés au sein d'un même appel `copier_carte` (un seul import), conformément au scénario d'acceptation US3 #5 ("au sein d'un même import"). Une fusion (`fusion:CHEMIN`) ciblant un dossier déjà peuplé lors d'un import **précédent et séparé** ne redétecte pas les boîtiers déjà présents : si un fichier du nouvel import aboutit, après renommage, au même nom final qu'un fichier déjà présent localement mais de contenu différent (deux boîtiers différents ayant produit un nom d'origine identique, importés carte par carte plutôt qu'en une seule session), `regine_core.import_carte.push.publier_localement` lève `CollisionNomLocalError` plutôt que d'écraser silencieusement (FR-012 appliqué au niveau fichier). Un doublon de contenu identique (même somme de contrôle) est en revanche ignoré silencieusement, conformément à US3 scénario 3.
4242
4343 ## Notes d'implémentation (post-FR-010, 2026-09-19)
@@ -37,7 +37,7 @@ regine import /Volumes/CARTE_SD [--titre TEXTE] [--destination nouveau|sous-doss
37 37
38 - `--destination` accepte exactement `nouveau`, `parent`, `sous-dossier:CHEMIN_RELATIF`, `fusion:CHEMIN_RELATIF` (le préfixe suit `id_cible` après le `:`) — `CHEMIN_RELATIF` est relatif à `--archive-root`/`--local-root` (ex. `voyage/2026-08_Montenegro`), tel que retourné par `lister_dossiers_candidats` ou saisi manuellement. En son absence en mode interactif, une question à 4 choix est posée ; en mode `--yes` sans `--destination`, le comportement par défaut est `nouveau_dossier` (US1, non régressif).38 - `--destination` accepte exactement `nouveau`, `parent`, `sous-dossier:CHEMIN_RELATIF`, `fusion:CHEMIN_RELATIF` (le préfixe suit `id_cible` après le `:`) — `CHEMIN_RELATIF` est relatif à `--archive-root`/`--local-root` (ex. `voyage/2026-08_Montenegro`), tel que retourné par `lister_dossiers_candidats` ou saisi manuellement. En son absence en mode interactif, une question à 4 choix est posée ; en mode `--yes` sans `--destination`, le comportement par défaut est `nouveau_dossier` (US1, non régressif).
39 - Un `nouveau_sous_dossier` est physiquement imbriqué sous le dossier parent réel (`archive_root/CHEMIN_RELATIF/nom_etape/...`), pas seulement sous la racine catégorie/année héritée — `regine_core.import_carte.destination.resoudre_destination` attend un `dossier_cible` déjà construit par l'appelant pour ce type (elle ne le calcule pas elle-même).39 - Un `nouveau_sous_dossier` est physiquement imbriqué sous le dossier parent réel (`archive_root/CHEMIN_RELATIF/nom_etape/...`), pas seulement sous la racine catégorie/année héritée — `regine_core.import_carte.destination.resoudre_destination` attend un `dossier_cible` déjà construit par l'appelant pour ce type (elle ne le calcule pas elle-même).
40-- `--contexte-db` (optionnel, défaut `<local_root>/.regine-contexte.sqlite3`) ouvre la base de contexte centralisée (`regine_core.config.db`) utilisée pour la désambiguïsation de boîtiers (specs/002) et le cache de catégories (specs/004).40+- **Révisé le 2026-09-23/24 (`specs/003-config-contexte-travail` FR-006/FR-015)** : plus de flag `--contexte-db` séparé — la base de contexte centralisée (désambiguïsation de boîtiers specs/002, cache de catégories specs/004, historique d'imports FR-004) vit désormais systématiquement à `<local_root>/contexte.sqlite3` (`regine_core.config.db.open_context_db(Path(args.local_root))`), sans possibilité de la faire diverger de `--local-root`. **Bug réel corrigé à cette occasion** : la version précédente pointait par défaut vers `<local_root>/.regine-contexte.sqlite3` (préfixe `.`, nom différent), un chemin distinct de celui utilisé par toutes les autres commandes CLI (`~/.regine/contexte.sqlite3` à l'époque) — un import réel n'écrivait donc jamais dans la même base que le reste de Régine, silencieusement (trouvé par l'utilisateur, cf. `specs/003-config-contexte-travail/contracts/regine-core-api.md`).
41 - **Limitation connue** : la désambiguïsation automatique de boîtiers par tag de modèle (FR-015) n'opère que sur les fichiers copiés au sein d'un même appel `copier_carte` (un seul import), conformément au scénario d'acceptation US3 #5 ("au sein d'un même import"). Une fusion (`fusion:CHEMIN`) ciblant un dossier déjà peuplé lors d'un import **précédent et séparé** ne redétecte pas les boîtiers déjà présents : si un fichier du nouvel import aboutit, après renommage, au même nom final qu'un fichier déjà présent localement mais de contenu différent (deux boîtiers différents ayant produit un nom d'origine identique, importés carte par carte plutôt qu'en une seule session), `regine_core.import_carte.push.publier_localement` lève `CollisionNomLocalError` plutôt que d'écraser silencieusement (FR-012 appliqué au niveau fichier). Un doublon de contenu identique (même somme de contrôle) est en revanche ignoré silencieusement, conformément à US3 scénario 3.41 - **Limitation connue** : la désambiguïsation automatique de boîtiers par tag de modèle (FR-015) n'opère que sur les fichiers copiés au sein d'un même appel `copier_carte` (un seul import), conformément au scénario d'acceptation US3 #5 ("au sein d'un même import"). Une fusion (`fusion:CHEMIN`) ciblant un dossier déjà peuplé lors d'un import **précédent et séparé** ne redétecte pas les boîtiers déjà présents : si un fichier du nouvel import aboutit, après renommage, au même nom final qu'un fichier déjà présent localement mais de contenu différent (deux boîtiers différents ayant produit un nom d'origine identique, importés carte par carte plutôt qu'en une seule session), `regine_core.import_carte.push.publier_localement` lève `CollisionNomLocalError` plutôt que d'écraser silencieusement (FR-012 appliqué au niveau fichier). Un doublon de contenu identique (même somme de contrôle) est en revanche ignoré silencieusement, conformément à US3 scénario 3.
42 42
43 ## Notes d'implémentation (post-FR-010, 2026-09-19)43 ## Notes d'implémentation (post-FR-010, 2026-09-19)
modified specs/003-config-contexte-travail/checklists/requirements.md +11 -0
@@ -52,3 +52,14 @@
5252 2. Accès de la GUI au contexte de travail : oui, en appelant directement `regine_core.config`
5353 sans logique propre à la GUI (Principe VI) — nouvelle User Story 4, nouveau FR-013, nouveau
5454 SC-008 (parité stricte CLI/GUI).
55+- **Révision du 2026-09-23** (`/speckit-specify`) : écart trouvé par l'utilisateur — la « base de
56+ données de travail » (FR-006) et la « base de contexte centralisée » (`~/.regine/`) désignaient
57+ jusqu'ici deux choses distinctes, ni l'une ni l'autre réellement précisée ni exploitée par le
58+ code existant, alors que le contenu de la seconde (boîtiers, catégories, historique d'imports)
59+ ne survivait pas à la perte de la machine locale. Fusionnées en une seule base, déplacée à la
60+ racine du répertoire de travail local (FR-006/FR-015) ; `~/.regine/` ne conserve que les trois
61+ chemins de travail (FR-014). Nouvelle User Story 5 (reprise sur une nouvelle machine), nouveaux
62+ FR-016/FR-017 (réplication vers l'archive au moment d'un `regine reconcile`, restauration
63+ automatique à la configuration initiale ou au premier checkout — deux questions posées et
64+ tranchées par l'utilisateur pendant cette session), nouveaux SC-009/SC-010, Edge Cases et
65+ Assumptions mis à jour en conséquence. Tous les critères de la checklist restent satisfaits.
@@ -52,3 +52,14 @@
52 2. Accès de la GUI au contexte de travail : oui, en appelant directement `regine_core.config`52 2. Accès de la GUI au contexte de travail : oui, en appelant directement `regine_core.config`
53 sans logique propre à la GUI (Principe VI) — nouvelle User Story 4, nouveau FR-013, nouveau53 sans logique propre à la GUI (Principe VI) — nouvelle User Story 4, nouveau FR-013, nouveau
54 SC-008 (parité stricte CLI/GUI).54 SC-008 (parité stricte CLI/GUI).
55+- **Révision du 2026-09-23** (`/speckit-specify`) : écart trouvé par l'utilisateur — la « base de
56+ données de travail » (FR-006) et la « base de contexte centralisée » (`~/.regine/`) désignaient
57+ jusqu'ici deux choses distinctes, ni l'une ni l'autre réellement précisée ni exploitée par le
58+ code existant, alors que le contenu de la seconde (boîtiers, catégories, historique d'imports)
59+ ne survivait pas à la perte de la machine locale. Fusionnées en une seule base, déplacée à la
60+ racine du répertoire de travail local (FR-006/FR-015) ; `~/.regine/` ne conserve que les trois
61+ chemins de travail (FR-014). Nouvelle User Story 5 (reprise sur une nouvelle machine), nouveaux
62+ FR-016/FR-017 (réplication vers l'archive au moment d'un `regine reconcile`, restauration
63+ automatique à la configuration initiale ou au premier checkout — deux questions posées et
64+ tranchées par l'utilisateur pendant cette session), nouveaux SC-009/SC-010, Edge Cases et
65+ Assumptions mis à jour en conséquence. Tous les critères de la checklist restent satisfaits.
modified specs/003-config-contexte-travail/contracts/cli-config.md +2 -2
@@ -12,7 +12,7 @@ regine config set-paths [--temp-dir CHEMIN] [--local-work-dir CHEMIN] [--archive
1212 ```
1313
1414 **Sorties** :
15-- Succès : récapitulatif du contexte de travail enregistré sur stdout, code de sortie `0`.
15+- Succès : récapitulatif du contexte de travail enregistré sur stdout, code de sortie `0`. **Révisé le 2026-09-23/24 (FR-017)** : si `local-work-dir` et `archive-smb` sont tous deux configurés à l'issue de cette commande et qu'aucune base de contexte centralisée n'existe encore sous ce `local-work-dir`, la commande tente de la restaurer depuis l'archive (`<archive-smb>/.regine-contexte.sqlite3`) si une sauvegarde y existe — annoncé clairement sur stdout (« Contexte restauré depuis l'archive : N boîtier(s), N catégorie(s) » ou silence si rien à restaurer). Sans archive accessible à cet instant, aucune erreur : la restauration sera retentée lors du premier `regine checkout` (cf. `contracts/cli-checkout-reconcile.md` de `specs/005-checkout-reconciliation`).
1616 - Répertoire manquant (FR-003) : proposition de création sur stdout ; sans confirmation (`--yes` ou prompt interactif), la commande s'arrête sans écrire, code de sortie non-zéro.
1717 - Collision `temp-dir` == `local-work-dir` (Edge Case) : avertissement sur stderr ; nécessite `--force` pour confirmer explicitement, sinon code de sortie non-zéro.
1818 - `archive-smb` invalide (pas un partage réseau, Edge Case) : erreur sur stderr, code de sortie non-zéro, rien n'est enregistré.
@@ -73,5 +73,5 @@ regine config cameras scan-nas [--confirm]
7373 - `regine config set-paths` accepte aussi `--ignorer-verification-archive` (non prévu dans la version initiale de ce contrat) : permet de passer outre le refus `TravailNonReconcilieError`/`ArchiveInaccessibleError` en connaissance de cause, même geste explicite que `--force` pour la collision de chemins.
7474 - `regine config show` sans `--json`, contexte incomplet : le message sur stdout liste explicitement les 3 noms de champs manquants (`temp_dir`, `local_work_dir`, `archive_smb_path`), code de sortie `1`.
7575 - `regine config cameras list --json` retourne un tableau d'objets `{id, modele, numero_serie, nom_lisible, source}` (`source` peut valoir `modele`, `numero_serie`, `manuel` — désambiguïsation à l'import, specs/002 — ou `parcours_nas`, cf. `contracts/regine-core-api.md`).
76-- Toutes les sous-commandes acceptent `--contexte-db CHEMIN` (optionnel, défaut `~/.regine/contexte.sqlite3`) pour cibler une base de contexte différente — utile notamment pour les tests.
76+- Toutes les sous-commandes acceptent `--config-db CHEMIN` (optionnel, défaut `~/.regine/config.sqlite3` — **renommé depuis `--contexte-db` le 2026-09-23/24**, cette base ne porte plus que les chemins) pour cibler une configuration globale différente — utile notamment pour les tests. La base de contexte centralisée (boîtiers/catégories/historique) n'a plus de flag dédié : son chemin (`<local-work-dir>/contexte.sqlite3`) se déduit toujours de la configuration globale, jamais fourni indépendamment — cohérent avec FR-015 (elle ne doit jamais diverger de `local_work_dir`).
7777 - **Scénario 4 du quickstart (montage/démontage SMB réel) non vérifiable dans l'environnement de développement de ce projet** (pas de partage SMB physique disponible) : le mécanisme (`smb.disponible`/`assurer_disponibilite`/`declencher_montage`) est validé unitairement avec un point de montage injecté, et `regine config show` détecte correctement un partage réellement inexistant comme `unavailable` en conditions réelles (validé manuellement, cf. `tasks.md` T026).
@@ -12,7 +12,7 @@ regine config set-paths [--temp-dir CHEMIN] [--local-work-dir CHEMIN] [--archive
12 ```12 ```
13 13
14 **Sorties** :14 **Sorties** :
15-- Succès : récapitulatif du contexte de travail enregistré sur stdout, code de sortie `0`.15+- Succès : récapitulatif du contexte de travail enregistré sur stdout, code de sortie `0`. **Révisé le 2026-09-23/24 (FR-017)** : si `local-work-dir` et `archive-smb` sont tous deux configurés à l'issue de cette commande et qu'aucune base de contexte centralisée n'existe encore sous ce `local-work-dir`, la commande tente de la restaurer depuis l'archive (`<archive-smb>/.regine-contexte.sqlite3`) si une sauvegarde y existe — annoncé clairement sur stdout (« Contexte restauré depuis l'archive : N boîtier(s), N catégorie(s) » ou silence si rien à restaurer). Sans archive accessible à cet instant, aucune erreur : la restauration sera retentée lors du premier `regine checkout` (cf. `contracts/cli-checkout-reconcile.md` de `specs/005-checkout-reconciliation`).
16 - Répertoire manquant (FR-003) : proposition de création sur stdout ; sans confirmation (`--yes` ou prompt interactif), la commande s'arrête sans écrire, code de sortie non-zéro.16 - Répertoire manquant (FR-003) : proposition de création sur stdout ; sans confirmation (`--yes` ou prompt interactif), la commande s'arrête sans écrire, code de sortie non-zéro.
17 - Collision `temp-dir` == `local-work-dir` (Edge Case) : avertissement sur stderr ; nécessite `--force` pour confirmer explicitement, sinon code de sortie non-zéro.17 - Collision `temp-dir` == `local-work-dir` (Edge Case) : avertissement sur stderr ; nécessite `--force` pour confirmer explicitement, sinon code de sortie non-zéro.
18 - `archive-smb` invalide (pas un partage réseau, Edge Case) : erreur sur stderr, code de sortie non-zéro, rien n'est enregistré.18 - `archive-smb` invalide (pas un partage réseau, Edge Case) : erreur sur stderr, code de sortie non-zéro, rien n'est enregistré.
@@ -73,5 +73,5 @@ regine config cameras scan-nas [--confirm]
73 - `regine config set-paths` accepte aussi `--ignorer-verification-archive` (non prévu dans la version initiale de ce contrat) : permet de passer outre le refus `TravailNonReconcilieError`/`ArchiveInaccessibleError` en connaissance de cause, même geste explicite que `--force` pour la collision de chemins.73 - `regine config set-paths` accepte aussi `--ignorer-verification-archive` (non prévu dans la version initiale de ce contrat) : permet de passer outre le refus `TravailNonReconcilieError`/`ArchiveInaccessibleError` en connaissance de cause, même geste explicite que `--force` pour la collision de chemins.
74 - `regine config show` sans `--json`, contexte incomplet : le message sur stdout liste explicitement les 3 noms de champs manquants (`temp_dir`, `local_work_dir`, `archive_smb_path`), code de sortie `1`.74 - `regine config show` sans `--json`, contexte incomplet : le message sur stdout liste explicitement les 3 noms de champs manquants (`temp_dir`, `local_work_dir`, `archive_smb_path`), code de sortie `1`.
75 - `regine config cameras list --json` retourne un tableau d'objets `{id, modele, numero_serie, nom_lisible, source}` (`source` peut valoir `modele`, `numero_serie`, `manuel` — désambiguïsation à l'import, specs/002 — ou `parcours_nas`, cf. `contracts/regine-core-api.md`).75 - `regine config cameras list --json` retourne un tableau d'objets `{id, modele, numero_serie, nom_lisible, source}` (`source` peut valoir `modele`, `numero_serie`, `manuel` — désambiguïsation à l'import, specs/002 — ou `parcours_nas`, cf. `contracts/regine-core-api.md`).
76-- Toutes les sous-commandes acceptent `--contexte-db CHEMIN` (optionnel, défaut `~/.regine/contexte.sqlite3`) pour cibler une base de contexte différente — utile notamment pour les tests.76+- Toutes les sous-commandes acceptent `--config-db CHEMIN` (optionnel, défaut `~/.regine/config.sqlite3` — **renommé depuis `--contexte-db` le 2026-09-23/24**, cette base ne porte plus que les chemins) pour cibler une configuration globale différente — utile notamment pour les tests. La base de contexte centralisée (boîtiers/catégories/historique) n'a plus de flag dédié : son chemin (`<local-work-dir>/contexte.sqlite3`) se déduit toujours de la configuration globale, jamais fourni indépendamment — cohérent avec FR-015 (elle ne doit jamais diverger de `local_work_dir`).
77 - **Scénario 4 du quickstart (montage/démontage SMB réel) non vérifiable dans l'environnement de développement de ce projet** (pas de partage SMB physique disponible) : le mécanisme (`smb.disponible`/`assurer_disponibilite`/`declencher_montage`) est validé unitairement avec un point de montage injecté, et `regine config show` détecte correctement un partage réellement inexistant comme `unavailable` en conditions réelles (validé manuellement, cf. `tasks.md` T026).77 - **Scénario 4 du quickstart (montage/démontage SMB réel) non vérifiable dans l'environnement de développement de ce projet** (pas de partage SMB physique disponible) : le mécanisme (`smb.disponible`/`assurer_disponibilite`/`declencher_montage`) est validé unitairement avec un point de montage injecté, et `regine config show` détecte correctement un partage réellement inexistant comme `unavailable` en conditions réelles (validé manuellement, cf. `tasks.md` T026).
modified specs/003-config-contexte-travail/contracts/regine-core-api.md +19 -2
@@ -2,9 +2,17 @@
22
33 Fonctions pures/orchestratrices consommées par `regine-cli` (`regine config ...`, cf. `contracts/cli-config.md`) — objets structurés, jamais de texte à parser (Principe VI).
44
5-## `db.open_context_db(path: Path | None = None) -> sqlite3.Connection`
5+## `db.open_context_db(local_work_dir: Path) -> sqlite3.Connection` *(signature révisée le 2026-09-23/24)*
66
7-Ouvre la base de contexte centralisée (chemins, boîtiers, catégories), en l'initialisant si nécessaire. Sans `path`, utilise `db.CHEMIN_CONTEXTE_PAR_DEFAUT` (`~/.regine/contexte.sqlite3`) — emplacement fixe par utilisateur, indépendant de `local_work_dir` (qui, lui, est modifiable). Déjà utilisée par `specs/001/002/004` avec un `path` explicite : l'ajout du défaut est rétrocompatible.
7+**Écart trouvé par l'utilisateur, corrigé** : ouvrait auparavant un fichier unique à emplacement fixe (`~/.regine/contexte.sqlite3`), mélangeant les trois chemins de travail (propres à la machine) et le savoir accumulé (boîtiers, catégories, historique — devant survivre à la perte de la machine). Ouvre désormais la base de contexte centralisée (boîtiers, catégories, historique — plus les chemins) à la racine de `local_work_dir`, qui devient un paramètre obligatoire plutôt qu'optionnel (plus de `CHEMIN_CONTEXTE_PAR_DEFAUT` fixe). L'initialise si elle n'existe pas encore (FR-006/FR-015). Ne DOIT plus être appelée avec `path=None` : utiliser `environnement.ouvrir_environnement()` pour résoudre `local_work_dir` d'abord.
8+
9+## `db.open_global_config_db(path: Path | None = None) -> sqlite3.Connection` *(nouvelle, FR-014)*
10+
11+Ouvre la configuration globale minimale (chemins de travail, état SMB — table `contexte` uniquement), en l'initialisant si nécessaire. Sans `path`, utilise `db.CHEMIN_CONFIG_GLOBALE_PAR_DEFAUT` (`~/.regine/config.sqlite3`) — emplacement fixe par machine. Ne contient jamais boîtiers/catégories/historique (FR-007).
12+
13+## `environnement.ouvrir_environnement(config_db_path: Path | None = None) -> Environnement` *(nouvelle, FR-014/015)*
14+
15+Point d'entrée unique pour toute façade (CLI/GUI) ayant besoin du contexte de travail : ouvre la configuration globale (`open_global_config_db`), puis, si `local_work_dir` y est déjà configuré, ouvre la base de contexte centralisée à sa racine (`open_context_db`). Retourne un objet `Environnement` (`config_conn`, `contexte_conn: sqlite3.Connection | None`, `local_work_dir: Path | None`) — `contexte_conn` vaut `None` tant que `local_work_dir` n'est pas configuré (rien à résoudre). Remplace tout appel direct à `open_context_db(None)` dans les façades — cf. Notes d'implémentation.
816
917 ## `context.charger_contexte(conn: sqlite3.Connection) -> ContexteTravail`
1018
@@ -29,6 +37,14 @@ Retourne les dossiers sous `local_work_dir` correspondant à un manifeste encore
2937
3038 Met à jour l'état de disponibilité connu du partage SMB (FR-004/005).
3139
40+## `context.sauvegarder_contexte(local_work_dir: Path, archive_root_local: Path) -> None` *(nouvelle, FR-016)*
41+
42+Copie vérifiée par somme de contrôle de `<local_work_dir>/contexte.sqlite3` vers `<archive_root_local>/.regine-contexte.sqlite3` (remplacement complet du fichier précédent, pas de fusion). Ne DOIT être appelée qu'après un `regine reconcile` réussi (orchestré par `regine_cli.archive_cmd`, pas par `regine_core.archive.reconciliation` lui-même — cf. research.md § 7). No-op si `<local_work_dir>/contexte.sqlite3` n'existe pas encore.
43+
44+## `context.restaurer_contexte_si_absent(local_work_dir: Path, archive_root_local: Path) -> bool` *(nouvelle, FR-017)*
45+
46+Si `<local_work_dir>/contexte.sqlite3` n'existe pas et que `<archive_root_local>/.regine-contexte.sqlite3` existe, copie vérifiée dans l'autre sens et retourne `True`. Ne fait rien et retourne `False` dans tout autre cas (déjà présent localement, ou absent des deux côtés — premier import sur une installation neuve). Appelée par `regine_cli.config_cmd` après la configuration initiale des trois chemins, et par `regine_cli.archive_cmd._cmd_checkout` après un checkout réussi (cf. research.md § 7).
47+
3248 ## `smb.chemin_local_du_partage(archive_smb_path: str) -> Path`
3349
3450 Dérive le point de montage local macOS (`/Volumes/<partage>`) d'une URL `smb://hôte/partage` — convention best-effort, ne garantit pas que le partage soit effectivement monté.
@@ -52,3 +68,4 @@ Parcours volontaire de l'archive (FR-012) : lit le tag `Model` de chaque fichier
5268 - **Emplacement de la base de contexte centralisée** : non fixé par `research.md`. Décision prise à l'implémentation (T005) : `~/.regine/contexte.sqlite3`, fixe par utilisateur, indépendant de `local_work_dir` (qui reste modifiable, FR-010/SC-005, sans jamais risquer de perdre boîtiers/catégories déjà enregistrés).
5369 - **Intégration avec `regine_cli.import_cmd`/`archive_cmd` (specs/001/005) non construite ici** : ces façades prennent aujourd'hui `--archive-root`/`--local-root` en flags CLI explicites, indépendamment de ce contexte configuré. `FR-010` ne protège donc que le travail effectivement réalisé sous le `local_work_dir` configuré ici — cohérent tant que cette intégration (lire `regine config show` par défaut plutôt que d'exiger les flags) n'a pas été demandée séparément.
5470 - **Extension du 2026-09-21 (User Story 4)** : `regine-gui` (`config_view.py`, implémentée après `specs/006-gui-tri-consultation`) consomme les mêmes fonctions ci-dessus telles quelles — aucune fonction supplémentaire n'a été nécessaire pour cette extension, confirmant que le contrat était déjà complet côté `regine-core` avant même que la GUI existe.
71+- **Bug réel trouvé par l'utilisateur (2026-09-23), révélé par l'absence d'un point d'entrée unique** : `regine_cli.import_cmd` (`specs/001-import-photos`) n'utilisait jamais `open_context_db(None)` comme les autres commandes CLI — il calculait son propre défaut (`local_root / ".regine-contexte.sqlite3"`), un chemin différent de `~/.regine/contexte.sqlite3` utilisé partout ailleurs (`config_cmd`, `archive_cmd`, tous les écrans GUI). Un import réel n'écrivait donc jamais dans la même base de contexte que le reste de Régine — silencieusement, sans erreur. Resté inaperçu tant qu'aucun test ni aucune revue ne comparait les défauts entre commandes. Corrigé par cette révision : `ouvrir_environnement()` devient le seul chemin de résolution, supprimant la possibilité qu'une commande diverge à nouveau silencieusement.
@@ -2,9 +2,17 @@
2 2
3 Fonctions pures/orchestratrices consommées par `regine-cli` (`regine config ...`, cf. `contracts/cli-config.md`) — objets structurés, jamais de texte à parser (Principe VI).3 Fonctions pures/orchestratrices consommées par `regine-cli` (`regine config ...`, cf. `contracts/cli-config.md`) — objets structurés, jamais de texte à parser (Principe VI).
4 4
5-## `db.open_context_db(path: Path | None = None) -> sqlite3.Connection`5+## `db.open_context_db(local_work_dir: Path) -> sqlite3.Connection` *(signature révisée le 2026-09-23/24)*
6 6
7-Ouvre la base de contexte centralisée (chemins, boîtiers, catégories), en l'initialisant si nécessaire. Sans `path`, utilise `db.CHEMIN_CONTEXTE_PAR_DEFAUT` (`~/.regine/contexte.sqlite3`) — emplacement fixe par utilisateur, indépendant de `local_work_dir` (qui, lui, est modifiable). Déjà utilisée par `specs/001/002/004` avec un `path` explicite : l'ajout du défaut est rétrocompatible.7+**Écart trouvé par l'utilisateur, corrigé** : ouvrait auparavant un fichier unique à emplacement fixe (`~/.regine/contexte.sqlite3`), mélangeant les trois chemins de travail (propres à la machine) et le savoir accumulé (boîtiers, catégories, historique — devant survivre à la perte de la machine). Ouvre désormais la base de contexte centralisée (boîtiers, catégories, historique — plus les chemins) à la racine de `local_work_dir`, qui devient un paramètre obligatoire plutôt qu'optionnel (plus de `CHEMIN_CONTEXTE_PAR_DEFAUT` fixe). L'initialise si elle n'existe pas encore (FR-006/FR-015). Ne DOIT plus être appelée avec `path=None` : utiliser `environnement.ouvrir_environnement()` pour résoudre `local_work_dir` d'abord.
8+
9+## `db.open_global_config_db(path: Path | None = None) -> sqlite3.Connection` *(nouvelle, FR-014)*
10+
11+Ouvre la configuration globale minimale (chemins de travail, état SMB — table `contexte` uniquement), en l'initialisant si nécessaire. Sans `path`, utilise `db.CHEMIN_CONFIG_GLOBALE_PAR_DEFAUT` (`~/.regine/config.sqlite3`) — emplacement fixe par machine. Ne contient jamais boîtiers/catégories/historique (FR-007).
12+
13+## `environnement.ouvrir_environnement(config_db_path: Path | None = None) -> Environnement` *(nouvelle, FR-014/015)*
14+
15+Point d'entrée unique pour toute façade (CLI/GUI) ayant besoin du contexte de travail : ouvre la configuration globale (`open_global_config_db`), puis, si `local_work_dir` y est déjà configuré, ouvre la base de contexte centralisée à sa racine (`open_context_db`). Retourne un objet `Environnement` (`config_conn`, `contexte_conn: sqlite3.Connection | None`, `local_work_dir: Path | None`) — `contexte_conn` vaut `None` tant que `local_work_dir` n'est pas configuré (rien à résoudre). Remplace tout appel direct à `open_context_db(None)` dans les façades — cf. Notes d'implémentation.
8 16
9 ## `context.charger_contexte(conn: sqlite3.Connection) -> ContexteTravail`17 ## `context.charger_contexte(conn: sqlite3.Connection) -> ContexteTravail`
10 18
@@ -29,6 +37,14 @@ Retourne les dossiers sous `local_work_dir` correspondant à un manifeste encore
29 37
30 Met à jour l'état de disponibilité connu du partage SMB (FR-004/005).38 Met à jour l'état de disponibilité connu du partage SMB (FR-004/005).
31 39
40+## `context.sauvegarder_contexte(local_work_dir: Path, archive_root_local: Path) -> None` *(nouvelle, FR-016)*
41+
42+Copie vérifiée par somme de contrôle de `<local_work_dir>/contexte.sqlite3` vers `<archive_root_local>/.regine-contexte.sqlite3` (remplacement complet du fichier précédent, pas de fusion). Ne DOIT être appelée qu'après un `regine reconcile` réussi (orchestré par `regine_cli.archive_cmd`, pas par `regine_core.archive.reconciliation` lui-même — cf. research.md § 7). No-op si `<local_work_dir>/contexte.sqlite3` n'existe pas encore.
43+
44+## `context.restaurer_contexte_si_absent(local_work_dir: Path, archive_root_local: Path) -> bool` *(nouvelle, FR-017)*
45+
46+Si `<local_work_dir>/contexte.sqlite3` n'existe pas et que `<archive_root_local>/.regine-contexte.sqlite3` existe, copie vérifiée dans l'autre sens et retourne `True`. Ne fait rien et retourne `False` dans tout autre cas (déjà présent localement, ou absent des deux côtés — premier import sur une installation neuve). Appelée par `regine_cli.config_cmd` après la configuration initiale des trois chemins, et par `regine_cli.archive_cmd._cmd_checkout` après un checkout réussi (cf. research.md § 7).
47+
32 ## `smb.chemin_local_du_partage(archive_smb_path: str) -> Path`48 ## `smb.chemin_local_du_partage(archive_smb_path: str) -> Path`
33 49
34 Dérive le point de montage local macOS (`/Volumes/<partage>`) d'une URL `smb://hôte/partage` — convention best-effort, ne garantit pas que le partage soit effectivement monté.50 Dérive le point de montage local macOS (`/Volumes/<partage>`) d'une URL `smb://hôte/partage` — convention best-effort, ne garantit pas que le partage soit effectivement monté.
@@ -52,3 +68,4 @@ Parcours volontaire de l'archive (FR-012) : lit le tag `Model` de chaque fichier
52 - **Emplacement de la base de contexte centralisée** : non fixé par `research.md`. Décision prise à l'implémentation (T005) : `~/.regine/contexte.sqlite3`, fixe par utilisateur, indépendant de `local_work_dir` (qui reste modifiable, FR-010/SC-005, sans jamais risquer de perdre boîtiers/catégories déjà enregistrés).68 - **Emplacement de la base de contexte centralisée** : non fixé par `research.md`. Décision prise à l'implémentation (T005) : `~/.regine/contexte.sqlite3`, fixe par utilisateur, indépendant de `local_work_dir` (qui reste modifiable, FR-010/SC-005, sans jamais risquer de perdre boîtiers/catégories déjà enregistrés).
53 - **Intégration avec `regine_cli.import_cmd`/`archive_cmd` (specs/001/005) non construite ici** : ces façades prennent aujourd'hui `--archive-root`/`--local-root` en flags CLI explicites, indépendamment de ce contexte configuré. `FR-010` ne protège donc que le travail effectivement réalisé sous le `local_work_dir` configuré ici — cohérent tant que cette intégration (lire `regine config show` par défaut plutôt que d'exiger les flags) n'a pas été demandée séparément.69 - **Intégration avec `regine_cli.import_cmd`/`archive_cmd` (specs/001/005) non construite ici** : ces façades prennent aujourd'hui `--archive-root`/`--local-root` en flags CLI explicites, indépendamment de ce contexte configuré. `FR-010` ne protège donc que le travail effectivement réalisé sous le `local_work_dir` configuré ici — cohérent tant que cette intégration (lire `regine config show` par défaut plutôt que d'exiger les flags) n'a pas été demandée séparément.
54 - **Extension du 2026-09-21 (User Story 4)** : `regine-gui` (`config_view.py`, implémentée après `specs/006-gui-tri-consultation`) consomme les mêmes fonctions ci-dessus telles quelles — aucune fonction supplémentaire n'a été nécessaire pour cette extension, confirmant que le contrat était déjà complet côté `regine-core` avant même que la GUI existe.70 - **Extension du 2026-09-21 (User Story 4)** : `regine-gui` (`config_view.py`, implémentée après `specs/006-gui-tri-consultation`) consomme les mêmes fonctions ci-dessus telles quelles — aucune fonction supplémentaire n'a été nécessaire pour cette extension, confirmant que le contrat était déjà complet côté `regine-core` avant même que la GUI existe.
71+- **Bug réel trouvé par l'utilisateur (2026-09-23), révélé par l'absence d'un point d'entrée unique** : `regine_cli.import_cmd` (`specs/001-import-photos`) n'utilisait jamais `open_context_db(None)` comme les autres commandes CLI — il calculait son propre défaut (`local_root / ".regine-contexte.sqlite3"`), un chemin différent de `~/.regine/contexte.sqlite3` utilisé partout ailleurs (`config_cmd`, `archive_cmd`, tous les écrans GUI). Un import réel n'écrivait donc jamais dans la même base de contexte que le reste de Régine — silencieusement, sans erreur. Resté inaperçu tant qu'aucun test ni aucune revue ne comparait les défauts entre commandes. Corrigé par cette révision : `ouvrir_environnement()` devient le seul chemin de résolution, supprimant la possibilité qu'une commande diverge à nouveau silencieusement.
modified specs/003-config-contexte-travail/data-model.md +41 -12
@@ -2,9 +2,9 @@
22
33 Entités dérivées de `spec.md` § Key Entities et Functional Requirements.
44
5-## Contexte de travail
5+## Configuration globale *(nouvelle, 2026-09-23/24)*
66
7-Représente l'état de configuration actif d'une installation de Régine. Unique par installation (cf. Assumptions).
7+Fichier SQLite minimal, propre à la machine, à un emplacement fixe (`~/.regine/config.sqlite3`) — table `contexte` uniquement, mêmes champs qu'avant la séparation. Ne contient jamais les boîtiers, catégories, ni l'historique d'imports (FR-014, FR-007).
88
99 | Champ | Type | Règles |
1010 |---|---|---|
@@ -13,21 +13,34 @@ Représente l'état de configuration actif d'une installation de Régine. Unique
1313 | `archive_smb_path` | chemin réseau (partage SMB) | DOIT correspondre à un partage réseau, pas à un dossier local (Edge Case) |
1414 | `smb_last_known_state` | énumération : `mounted` \| `unavailable` \| `unknown` | Alimenté par la vérification de disponibilité avant chaque opération (FR-004/FR-005) |
1515
16-**Règle d'état** : `local_work_dir` ne peut être modifié que si aucun checkout ni import n'y est en cours de réconciliation/archivage (cf. Clarification 2026-09-18 n°1, FR-010, Acceptance Scenario 5 de User Story 1). Cette vérification interroge la ou les bases de données de travail par dossier présentes sous `local_work_dir`.
16+**Règle d'état** : `local_work_dir` ne peut être modifié que si aucun checkout ni import n'y est en cours de réconciliation/archivage (cf. Clarification 2026-09-18 n°1, FR-010, Acceptance Scenario 5 de User Story 1). Cette vérification interroge les manifestes de l'archive (via son point de montage local), pas un fichier sous `local_work_dir` lui-même — cf. écart déjà documenté dans `contracts/regine-core-api.md`.
1717
18-## Base de données de travail (par dossier)
18+**Contexte de travail** (entité logique de `spec.md`) = Configuration globale + Base de contexte centralisée ci-dessous, réunies par `regine_core.config.environnement.ouvrir_environnement()`.
1919
20-Une base SQLite par dossier local, à sa racine (FR-006), archivée avec lui vers le NAS (FR-007). Le schéma détaillé (tables de manifeste, hash, identifiants pérennes) relève du module `archive`/`dossier` (constitution § Workflow d'archivage, section 12 des notes de conception) ; ce plan couvre uniquement son **initialisation** :
20+## Base de contexte centralisée *(révisée le 2026-09-23/24 — déplacée, fusionnée avec l'ex-« base de données de travail »)*
2121
22-| Élément | Description |
23-|---|---|
24-| Emplacement | Racine du dossier local concerné, sous `local_work_dir` |
25-| Convention de format | `PRAGMA user_version` (numéro de schéma), `PRAGMA application_id` (identifiant Régine) — cf. constitution § Workflow d'archivage |
26-| Cycle de vie | Créée à l'initialisation du dossier local ; déplacée/archivée avec le dossier lors du réarchivage vers le NAS ; sa présence sous un dossier non réconcilié est ce qui bloque un changement de `local_work_dir` (règle ci-dessus) |
22+Base SQLite unique, à la racine du répertoire de travail local (`<local_work_dir>/contexte.sqlite3`, FR-006/FR-015) — plus à `~/.regine/`. Remplace à la fois l'ancienne base de contexte centralisée (boîtiers) et l'ancienne « base de données de travail par dossier » : cette dernière, au schéma manifeste (`fichiers`/`verrou`) jamais réellement exploité (le verrou vit en réalité uniquement côté archive, cf. Notes d'implémentation de `contracts/regine-core-api.md`), est retirée — une seule base par `local_work_dir`, pas une par dossier.
2723
28-## Base de contexte centralisée
24+**Table `boitiers`** (possédée par `regine_core.camera_profile`, cf. `specs/002-profil-boitiers-optionnel`)
2925
30-Base SQLite unique au niveau du contexte de travail (pas par dossier), distincte de la base de données de travail par dossier (Assumptions).
26+| Champ | Type | Règles |
27+|---|---|---|
28+| `id` | identifiant interne | Clé primaire |
29+| `modele` | texte | Tag EXIF `Model`, enregistré à chaque import (FR-008) |
30+| `numero_serie` | texte, nullable | Tag EXIF `BodySerialNumber`, quand présent et exploitable (cf. `specs/002-profil-boitiers-optionnel`) |
31+| `nom_lisible` | texte, nullable | Attribué par l'utilisateur (FR-008/FR-009) ; `NULL` tant que non nommé |
32+| `premiere_rencontre` | horodatage | Date du premier import ayant révélé ce boîtier |
33+| `source` | texte | `modele`/`numero_serie`/`manuel` (désambiguïsation à l'import, specs/002) ou `parcours_nas` (FR-012) |
34+
35+**Table `categories`** (possédée par `regine_core.config.categories`, cf. `specs/004-categorisation-dossiers`) et **table `checksums_importes`** (possédée par `regine_core.import_carte.historique`, cf. `specs/001-import-photos` FR-004) : schémas inchangés par cette révision, seul leur emplacement physique change (même fichier, nouveau chemin).
36+
37+**Règle d'unicité** (`boitiers`) : unique sur (`modele`, `numero_serie`) quand `numero_serie` est exploitable ; sinon unique sur `modele` seul, jusqu'à ce qu'une collision réelle (détectée par le module import, cf. `specs/002-profil-boitiers-optionnel`) force la création d'une entrée distincte.
38+
39+**Relation** : `nom_lisible`, une fois défini, DOIT être réutilisé par tout module identifiant ce même boîtier par la suite (FR-009) — relation de référence, pas de duplication de la donnée.
40+
41+**Réplication vers l'archive (FR-016)** : copie vérifiée par somme de contrôle de ce fichier entier vers `<archive_root>/.regine-contexte.sqlite3` (emplacement fixe, racine de l'archive — symétrique à son emplacement local), déclenchée après un `regine reconcile` réussi. Toujours un remplacement complet du fichier de sauvegarde précédent (pas une fusion ligne à ligne) — cohérent avec l'Edge Case déjà tranché (pas de fusion de contextes divergents).
42+
43+**Restauration depuis l'archive (FR-017)** : si `<local_work_dir>/contexte.sqlite3` n'existe pas mais que `<archive_root>/.regine-contexte.sqlite3` existe, copie vérifiée dans l'autre sens — déclenchée après la configuration initiale des trois chemins, ou après un premier `regine checkout` réussi sur cette machine, selon lequel des deux survient en premier.
3144
3245 **Table `boitiers`**
3346
@@ -73,3 +86,19 @@ Vue logique sur une ligne de la table `boitiers` dont `nom_lisible` n'est pas `N
7386 ```
7487
7588 **Extension du 2026-09-21 (User Story 4)** : aucune nouvelle entité. L'onglet de configuration de la GUI lit et écrit exactement le même `Contexte de travail` et les mêmes `Boîtier`/`Boîtier nommé` que la CLI, via les mêmes fonctions (`contracts/regine-core-api.md`) — pas un second modèle de données parallèle.
89+
90+**Extension du 2026-09-23/24 (User Story 5)** :
91+
92+```text
93+[Base de contexte centralisée] (<local_work_dir>/contexte.sqlite3)
94+ Présente localement
95+ │ regine reconcile réussi (FR-016)
96+ ▼
97+ Répliquée sur l'archive (<archive_root>/.regine-contexte.sqlite3)
98+
99+[Nouvelle machine, local_work_dir vide]
100+ Absente localement, présente sur l'archive
101+ │ configuration initiale des 3 chemins (FR-001), ou premier `regine checkout` (FR-017)
102+ ▼
103+ Restaurée localement — boîtiers/catégories/historique identiques à l'ancienne machine
104+```
@@ -2,9 +2,9 @@
2 2
3 Entités dérivées de `spec.md` § Key Entities et Functional Requirements.3 Entités dérivées de `spec.md` § Key Entities et Functional Requirements.
4 4
5-## Contexte de travail5+## Configuration globale *(nouvelle, 2026-09-23/24)*
6 6
7-Représente l'état de configuration actif d'une installation de Régine. Unique par installation (cf. Assumptions).7+Fichier SQLite minimal, propre à la machine, à un emplacement fixe (`~/.regine/config.sqlite3`) — table `contexte` uniquement, mêmes champs qu'avant la séparation. Ne contient jamais les boîtiers, catégories, ni l'historique d'imports (FR-014, FR-007).
8 8
9 | Champ | Type | Règles |9 | Champ | Type | Règles |
10 |---|---|---|10 |---|---|---|
@@ -13,21 +13,34 @@ Représente l'état de configuration actif d'une installation de Régine. Unique
13 | `archive_smb_path` | chemin réseau (partage SMB) | DOIT correspondre à un partage réseau, pas à un dossier local (Edge Case) |13 | `archive_smb_path` | chemin réseau (partage SMB) | DOIT correspondre à un partage réseau, pas à un dossier local (Edge Case) |
14 | `smb_last_known_state` | énumération : `mounted` \| `unavailable` \| `unknown` | Alimenté par la vérification de disponibilité avant chaque opération (FR-004/FR-005) |14 | `smb_last_known_state` | énumération : `mounted` \| `unavailable` \| `unknown` | Alimenté par la vérification de disponibilité avant chaque opération (FR-004/FR-005) |
15 15
16-**Règle d'état** : `local_work_dir` ne peut être modifié que si aucun checkout ni import n'y est en cours de réconciliation/archivage (cf. Clarification 2026-09-18 n°1, FR-010, Acceptance Scenario 5 de User Story 1). Cette vérification interroge la ou les bases de données de travail par dossier présentes sous `local_work_dir`.16+**Règle d'état** : `local_work_dir` ne peut être modifié que si aucun checkout ni import n'y est en cours de réconciliation/archivage (cf. Clarification 2026-09-18 n°1, FR-010, Acceptance Scenario 5 de User Story 1). Cette vérification interroge les manifestes de l'archive (via son point de montage local), pas un fichier sous `local_work_dir` lui-même — cf. écart déjà documenté dans `contracts/regine-core-api.md`.
17 17
18-## Base de données de travail (par dossier)18+**Contexte de travail** (entité logique de `spec.md`) = Configuration globale + Base de contexte centralisée ci-dessous, réunies par `regine_core.config.environnement.ouvrir_environnement()`.
19 19
20-Une base SQLite par dossier local, à sa racine (FR-006), archivée avec lui vers le NAS (FR-007). Le schéma détaillé (tables de manifeste, hash, identifiants pérennes) relève du module `archive`/`dossier` (constitution § Workflow d'archivage, section 12 des notes de conception) ; ce plan couvre uniquement son **initialisation** :20+## Base de contexte centralisée *(révisée le 2026-09-23/24 — déplacée, fusionnée avec l'ex-« base de données de travail »)*
21 21
22-| Élément | Description |22+Base SQLite unique, à la racine du répertoire de travail local (`<local_work_dir>/contexte.sqlite3`, FR-006/FR-015) — plus à `~/.regine/`. Remplace à la fois l'ancienne base de contexte centralisée (boîtiers) et l'ancienne « base de données de travail par dossier » : cette dernière, au schéma manifeste (`fichiers`/`verrou`) jamais réellement exploité (le verrou vit en réalité uniquement côté archive, cf. Notes d'implémentation de `contracts/regine-core-api.md`), est retirée — une seule base par `local_work_dir`, pas une par dossier.
23-|---|---|
24-| Emplacement | Racine du dossier local concerné, sous `local_work_dir` |
25-| Convention de format | `PRAGMA user_version` (numéro de schéma), `PRAGMA application_id` (identifiant Régine) — cf. constitution § Workflow d'archivage |
26-| Cycle de vie | Créée à l'initialisation du dossier local ; déplacée/archivée avec le dossier lors du réarchivage vers le NAS ; sa présence sous un dossier non réconcilié est ce qui bloque un changement de `local_work_dir` (règle ci-dessus) |
27 23
28-## Base de contexte centralisée24+**Table `boitiers`** (possédée par `regine_core.camera_profile`, cf. `specs/002-profil-boitiers-optionnel`)
29 25
30-Base SQLite unique au niveau du contexte de travail (pas par dossier), distincte de la base de données de travail par dossier (Assumptions).26+| Champ | Type | Règles |
27+|---|---|---|
28+| `id` | identifiant interne | Clé primaire |
29+| `modele` | texte | Tag EXIF `Model`, enregistré à chaque import (FR-008) |
30+| `numero_serie` | texte, nullable | Tag EXIF `BodySerialNumber`, quand présent et exploitable (cf. `specs/002-profil-boitiers-optionnel`) |
31+| `nom_lisible` | texte, nullable | Attribué par l'utilisateur (FR-008/FR-009) ; `NULL` tant que non nommé |
32+| `premiere_rencontre` | horodatage | Date du premier import ayant révélé ce boîtier |
33+| `source` | texte | `modele`/`numero_serie`/`manuel` (désambiguïsation à l'import, specs/002) ou `parcours_nas` (FR-012) |
34+
35+**Table `categories`** (possédée par `regine_core.config.categories`, cf. `specs/004-categorisation-dossiers`) et **table `checksums_importes`** (possédée par `regine_core.import_carte.historique`, cf. `specs/001-import-photos` FR-004) : schémas inchangés par cette révision, seul leur emplacement physique change (même fichier, nouveau chemin).
36+
37+**Règle d'unicité** (`boitiers`) : unique sur (`modele`, `numero_serie`) quand `numero_serie` est exploitable ; sinon unique sur `modele` seul, jusqu'à ce qu'une collision réelle (détectée par le module import, cf. `specs/002-profil-boitiers-optionnel`) force la création d'une entrée distincte.
38+
39+**Relation** : `nom_lisible`, une fois défini, DOIT être réutilisé par tout module identifiant ce même boîtier par la suite (FR-009) — relation de référence, pas de duplication de la donnée.
40+
41+**Réplication vers l'archive (FR-016)** : copie vérifiée par somme de contrôle de ce fichier entier vers `<archive_root>/.regine-contexte.sqlite3` (emplacement fixe, racine de l'archive — symétrique à son emplacement local), déclenchée après un `regine reconcile` réussi. Toujours un remplacement complet du fichier de sauvegarde précédent (pas une fusion ligne à ligne) — cohérent avec l'Edge Case déjà tranché (pas de fusion de contextes divergents).
42+
43+**Restauration depuis l'archive (FR-017)** : si `<local_work_dir>/contexte.sqlite3` n'existe pas mais que `<archive_root>/.regine-contexte.sqlite3` existe, copie vérifiée dans l'autre sens — déclenchée après la configuration initiale des trois chemins, ou après un premier `regine checkout` réussi sur cette machine, selon lequel des deux survient en premier.
31 44
32 **Table `boitiers`**45 **Table `boitiers`**
33 46
@@ -73,3 +86,19 @@ Vue logique sur une ligne de la table `boitiers` dont `nom_lisible` n'est pas `N
73 ```86 ```
74 87
75 **Extension du 2026-09-21 (User Story 4)** : aucune nouvelle entité. L'onglet de configuration de la GUI lit et écrit exactement le même `Contexte de travail` et les mêmes `Boîtier`/`Boîtier nommé` que la CLI, via les mêmes fonctions (`contracts/regine-core-api.md`) — pas un second modèle de données parallèle.88 **Extension du 2026-09-21 (User Story 4)** : aucune nouvelle entité. L'onglet de configuration de la GUI lit et écrit exactement le même `Contexte de travail` et les mêmes `Boîtier`/`Boîtier nommé` que la CLI, via les mêmes fonctions (`contracts/regine-core-api.md`) — pas un second modèle de données parallèle.
89+
90+**Extension du 2026-09-23/24 (User Story 5)** :
91+
92+```text
93+[Base de contexte centralisée] (<local_work_dir>/contexte.sqlite3)
94+ Présente localement
95+ │ regine reconcile réussi (FR-016)
96+ ▼
97+ Répliquée sur l'archive (<archive_root>/.regine-contexte.sqlite3)
98+
99+[Nouvelle machine, local_work_dir vide]
100+ Absente localement, présente sur l'archive
101+ │ configuration initiale des 3 chemins (FR-001), ou premier `regine checkout` (FR-017)
102+ ▼
103+ Restaurée localement — boîtiers/catégories/historique identiques à l'ancienne machine
104+```
modified specs/003-config-contexte-travail/plan.md +72 -1
@@ -10,13 +10,19 @@ Le module de configuration établit le contexte de travail dont dépendent tous
1010
1111 **Extension du 2026-09-21 (User Story 4, clarification post-implémentation)** : `regine-gui` (implémentée entre-temps par `specs/006-gui-tri-consultation`, avec une fenêtre à onglets Tri/Consultation) reçoit un troisième onglet de configuration, appelant directement les fonctions déjà documentées dans `contracts/regine-core-api.md` (`context.charger_contexte`/`enregistrer_chemins`, `cameras_screen.lister_boitiers`/`nommer_boitier`). **Aucune nouvelle fonction `regine-core` requise** — tout l'API nécessaire existe et est déjà testée ; ce périmètre est une façade pure (Principe VI), au même titre que `tri_view.py`/`consultation_view.py` l'ont déjà été pour leurs propres capacités.
1212
13+**Extension du 2026-09-23/24 (User Story 5, révision post-implémentation FR-006/007/014-017)** : écart trouvé par l'utilisateur — `db.open_context_db()` ouvrait un unique fichier `~/.regine/contexte.sqlite3` mélangeant deux choses de nature différente : (a) les trois chemins de travail, propres à la machine, jamais destinés à être partagés ; (b) le savoir accumulé (boîtiers, catégories, historique d'imports `specs/001-import-photos`), qui doit au contraire survivre à la perte de la machine puisque les photos elles-mêmes survivent sur le NAS. Cette extension sépare les deux : une **configuration globale minimale** (nouveau fichier `~/.regine/config.sqlite3`, uniquement les chemins) reste propre à la machine ; la **base de contexte centralisée** (boîtiers/catégories/historique, déjà existante mais renommée/déplacée) migre à la racine de `local_work_dir` — remplaçant au passage le fichier de manifeste vide et jamais exploité que `enregistrer_chemins` y créait jusqu'ici (écart déjà noté dans `contracts/regine-core-api.md` : « le verrou/manifeste vit en réalité uniquement côté archive »). Elle est répliquée vers l'archive à chaque `regine reconcile`, et restaurée automatiquement à la configuration initiale ou au premier `regine checkout` sur une machine qui ne l'a pas encore localement. **Aucun changement pour `regine-gui`** au-delà de la résolution d'environnement partagée avec la CLI (Principe VI) : les écrans existants (`config_view.py`, `import_view.py`, `consultation_view.py`) continuent d'appeler les mêmes fonctions `regine_core.config`/`camera_profile`/`import_carte.historique`, seule la façon dont la connexion SQLite sous-jacente est résolue change.
14+
1315 ## Technical Context
1416
1517 **Language/Version**: Python 3.11+ (langage retenu pour la bibliothèque centrale de Régine, cf. `docs/interface-cli-gui-architecture.md`)
1618
1719 **Primary Dependencies**: bibliothèque standard uniquement pour ce module (`sqlite3`, `pathlib`, `shutil`, `subprocess` pour déclencher le montage natif) ; pas de dépendance tierce nouvelle identifiée (cf. research.md)
1820
19-**Storage**: SQLite — deux bases distinctes : la base de contexte centralisée (fichier niveau contexte de travail, ouverture/bootstrap assurés par ce module via `regine_core/config/db.py`, mais dont la table `boitiers` est possédée par le module `camera_profile`, cf. `specs/002-profil-boitiers-optionnel/plan.md` § Correction de placement) et les bases de données de travail par dossier (une par dossier local, initialisées par ce module mais dont le schéma détaillé relève du module `archive`/`dossier`, cf. constitution § Workflow d'archivage)
21+**Storage**: SQLite — **révisé le 2026-09-23/24** : désormais deux bases distinctes, à des emplacements différents plutôt qu'une base unique à emplacement unique :
22+1. **Configuration globale** (`~/.regine/config.sqlite3`, nouveau nom de fichier — FR-014) : uniquement les trois chemins de travail et l'état SMB connu (table `contexte`, inchangée dans sa forme). Propre à la machine, jamais répliquée.
23+2. **Base de contexte centralisée** (`<local_work_dir>/contexte.sqlite3` — FR-006/FR-015, ex-`~/.regine/contexte.sqlite3`) : boîtiers (table `boitiers`, possédée par `camera_profile`, cf. `specs/002-profil-boitiers-optionnel/plan.md`), catégories (`specs/004-categorisation-dossiers`), historique des imports (table `checksums_importes`, possédée par `import_carte.historique`, cf. `specs/001-import-photos`). Répliquée vers l'archive NAS à un emplacement fixe (FR-016) et restaurable depuis celle-ci (FR-017).
24+
25+L'ancienne « base de données de travail par dossier » (schéma manifeste `fichiers`/`verrou`, une instance par dossier local) est retirée de ce plan : jamais réellement exploitée (écart déjà documenté dans `contracts/regine-core-api.md`, le verrou/manifeste vivant en réalité uniquement côté archive, `specs/005-checkout-reconciliation`) — FR-006 révisée ne désigne plus ce schéma.
2026
2127 **Testing**: pytest ; tests de contrat sur la surface CLI (entrées/sorties texte, codes de sortie), tests d'intégration sur le cycle configuration → initialisation de la base de travail, tests unitaires sur la validation des chemins et la logique de désambiguïsation de boîtiers
2228
@@ -50,6 +56,17 @@ Aucune violation identifiée ; la section Complexity Tracking reste vide.
5056
5157 **Re-check post Phase 1** (après génération de `data-model.md`, `contracts/cli-config.md`, `quickstart.md`) : le modèle de données (deux bases SQLite distinctes, conventions `PRAGMA user_version`/`application_id` réutilisées) et le contrat CLI (protocole texte in/out, refus explicite plutôt que migration silencieuse pour FR-010, parcours NAS non exécuté sans `--confirm` pour FR-012) confirment chaque évaluation PASS ci-dessus. Aucune violation nouvelle introduite par la conception détaillée.
5258
59+**Re-check post User Story 5 (2026-09-23/24)** :
60+
61+| Principe / contrainte | Évaluation | Justification |
62+|---|---|---|
63+| II. Confirmation explicite avant toute action à risque | PASS | La réplication (FR-016) et la restauration (FR-017) écrasent chacune une seule cible bien identifiée (la sauvegarde précédente sur l'archive, ou la base locale absente) — jamais un fichier contenant un travail non confirmé ; aucune des deux n'est une action destructive au sens du Principe II. |
64+| V. L'utilisateur décide, Régine suggère | PASS | FR-016/FR-017 sont déclenchées par des actions déjà explicites de l'utilisateur (`regine reconcile`, configuration initiale, `regine checkout`) — aucun nouveau geste caché, aucune sauvegarde/restauration imposée en dehors de ces moments (tranché explicitement par l'utilisateur pendant `/speckit-specify`, plutôt que l'option automatique-à-chaque-écriture initialement proposée). |
65+| VI. Bibliothèque centrale, façades minces | PASS | La réplication/restauration est orchestrée par les façades CLI (`archive_cmd.py`/`config_cmd.py`) qui appellent une fonction dédiée de `regine_core.config.context` — `regine_core.archive.checkout`/`reconciliation` (specs/005) restent inchangés et ignorants de `config` (pas de nouvelle dépendance inter-modules dans le cœur), cohérent avec le style d'orchestration déjà utilisé par `import_cmd.py` (specs/001). |
66+| Exécution sans démon | PASS | La réplication/restauration est une copie de fichier ponctuelle exécutée dans le même processus que la commande qui la déclenche, jamais un service en arrière-plan. |
67+
68+Aucune violation nouvelle ; Complexity Tracking reste vide.
69+
5370 ## Project Structure
5471
5572 ### Documentation (this feature)
@@ -119,6 +136,60 @@ packages/
119136
120137 **Extension du 2026-09-21 (User Story 4)** : `regine-gui` n'est plus un paquet vide — `specs/006-gui-tri-consultation` l'a implémenté entre-temps (PySide6, `FenetrePrincipale` à onglets dans `app.py`). Cette extension y ajoute un troisième onglet (`config_view.py`), construit à l'identique de `tri_view.py`/`consultation_view.py` (une classe `QMainWindow`/`QWidget` qui n'appelle que des fonctions déjà publiques de `regine_core.config`, jamais de logique propre) — aucun nouveau research nécessaire (aucune inconnue technique introduite), aucun nouveau contrat `regine-core-api.md` (les fonctions appelées sont déjà toutes documentées).
121138
139+**Extension du 2026-09-23/24 (User Story 5)** :
140+
141+```text
142+packages/
143+├── regine-core/
144+│ └── src/regine_core/
145+│ ├── config/
146+│ │ ├── db.py # RÉVISÉ : open_context_db(local_work_dir) exige désormais
147+│ │ │ local_work_dir explicite (plus de défaut ~/.regine/) ;
148+│ │ │ nouvelle open_global_config_db(path=None), défaut
149+│ │ │ ~/.regine/config.sqlite3, table `contexte` uniquement
150+│ │ │ (chemins), jamais boîtiers/catégories/historique
151+│ │ ├── environnement.py # NOUVEAU — ouvrir_environnement() : résout la config
152+│ │ │ globale puis, si local_work_dir déjà configuré, ouvre
153+│ │ │ la base de contexte centralisée à sa racine (FR-014/015) ;
154+│ │ │ point d'entrée unique remplaçant les appels épars à
155+│ │ │ open_context_db(None) dans les façades CLI/GUI
156+│ │ └── context.py # ÉTENDU — sauvegarder_contexte(local_work_dir, archive_root)
157+│ │ (FR-016) ; restaurer_contexte_si_absent(local_work_dir,
158+│ │ archive_root) -> bool (FR-017) ; enregistrer_chemins
159+│ │ n'initialise plus le manifeste vide à la racine de
160+│ │ local_work_dir (retiré, cf. Technical Context) — ouvre
161+│ │ désormais la base de contexte centralisée à cet endroit
162+│ │ via db.open_context_db, l'y initialise si absente
163+│ └── import_carte/
164+│ └── historique.py # INCHANGÉ dans son API (specs/001) — la connexion qu'il
165+│ reçoit vient désormais de ouvrir_environnement(), pas
166+│ d'un open_context_db(None) implicite
167+│
168+├── regine-cli/
169+│ └── src/regine_cli/
170+│ ├── config_cmd.py # ÉTENDU — utilise ouvrir_environnement() ; restauration
171+│ │ automatique (FR-017) appelée après set-paths si le
172+│ │ contexte local est absent et l'archive en a une copie
173+│ ├── archive_cmd.py # ÉTENDU — _cmd_reconcile appelle sauvegarder_contexte()
174+│ │ après un archivage réussi (FR-016) ; _cmd_checkout
175+│ │ appelle restaurer_contexte_si_absent() après un premier
176+│ │ checkout réussi (FR-017)
177+│ └── import_cmd.py # CORRIGÉ — utilisait déjà (par erreur, avant cette
178+│ révision) local_root / ".regine-contexte.sqlite3" comme
179+│ contexte_db par défaut plutôt que le mécanisme partagé ;
180+│ utilise désormais ouvrir_environnement() comme les autres
181+│ commandes CLI
182+│
183+└── regine-gui/
184+ └── src/regine_gui/
185+ └── app.py # ÉTENDU — FenetrePrincipale résout l'environnement une
186+ fois au démarrage (ouvrir_environnement()) et le propage
187+ aux onglets, plutôt que chaque écran ouvrant lui-même
188+ open_context_db(None) indépendamment
189+```
190+
191+Aucun nouveau paquet, aucune nouvelle dépendance externe — uniquement une réorganisation de la résolution de connexion SQLite déjà en place, et deux fonctions ajoutées à `regine_core.config.context`.
192+
122193 ## Complexity Tracking
123194
124195 *Aucune violation de gate à justifier — section laissée vide intentionnellement.*
@@ -10,13 +10,19 @@ Le module de configuration établit le contexte de travail dont dépendent tous
10 10
11 **Extension du 2026-09-21 (User Story 4, clarification post-implémentation)** : `regine-gui` (implémentée entre-temps par `specs/006-gui-tri-consultation`, avec une fenêtre à onglets Tri/Consultation) reçoit un troisième onglet de configuration, appelant directement les fonctions déjà documentées dans `contracts/regine-core-api.md` (`context.charger_contexte`/`enregistrer_chemins`, `cameras_screen.lister_boitiers`/`nommer_boitier`). **Aucune nouvelle fonction `regine-core` requise** — tout l'API nécessaire existe et est déjà testée ; ce périmètre est une façade pure (Principe VI), au même titre que `tri_view.py`/`consultation_view.py` l'ont déjà été pour leurs propres capacités.11 **Extension du 2026-09-21 (User Story 4, clarification post-implémentation)** : `regine-gui` (implémentée entre-temps par `specs/006-gui-tri-consultation`, avec une fenêtre à onglets Tri/Consultation) reçoit un troisième onglet de configuration, appelant directement les fonctions déjà documentées dans `contracts/regine-core-api.md` (`context.charger_contexte`/`enregistrer_chemins`, `cameras_screen.lister_boitiers`/`nommer_boitier`). **Aucune nouvelle fonction `regine-core` requise** — tout l'API nécessaire existe et est déjà testée ; ce périmètre est une façade pure (Principe VI), au même titre que `tri_view.py`/`consultation_view.py` l'ont déjà été pour leurs propres capacités.
12 12
13+**Extension du 2026-09-23/24 (User Story 5, révision post-implémentation FR-006/007/014-017)** : écart trouvé par l'utilisateur — `db.open_context_db()` ouvrait un unique fichier `~/.regine/contexte.sqlite3` mélangeant deux choses de nature différente : (a) les trois chemins de travail, propres à la machine, jamais destinés à être partagés ; (b) le savoir accumulé (boîtiers, catégories, historique d'imports `specs/001-import-photos`), qui doit au contraire survivre à la perte de la machine puisque les photos elles-mêmes survivent sur le NAS. Cette extension sépare les deux : une **configuration globale minimale** (nouveau fichier `~/.regine/config.sqlite3`, uniquement les chemins) reste propre à la machine ; la **base de contexte centralisée** (boîtiers/catégories/historique, déjà existante mais renommée/déplacée) migre à la racine de `local_work_dir` — remplaçant au passage le fichier de manifeste vide et jamais exploité que `enregistrer_chemins` y créait jusqu'ici (écart déjà noté dans `contracts/regine-core-api.md` : « le verrou/manifeste vit en réalité uniquement côté archive »). Elle est répliquée vers l'archive à chaque `regine reconcile`, et restaurée automatiquement à la configuration initiale ou au premier `regine checkout` sur une machine qui ne l'a pas encore localement. **Aucun changement pour `regine-gui`** au-delà de la résolution d'environnement partagée avec la CLI (Principe VI) : les écrans existants (`config_view.py`, `import_view.py`, `consultation_view.py`) continuent d'appeler les mêmes fonctions `regine_core.config`/`camera_profile`/`import_carte.historique`, seule la façon dont la connexion SQLite sous-jacente est résolue change.
14+
13 ## Technical Context15 ## Technical Context
14 16
15 **Language/Version**: Python 3.11+ (langage retenu pour la bibliothèque centrale de Régine, cf. `docs/interface-cli-gui-architecture.md`)17 **Language/Version**: Python 3.11+ (langage retenu pour la bibliothèque centrale de Régine, cf. `docs/interface-cli-gui-architecture.md`)
16 18
17 **Primary Dependencies**: bibliothèque standard uniquement pour ce module (`sqlite3`, `pathlib`, `shutil`, `subprocess` pour déclencher le montage natif) ; pas de dépendance tierce nouvelle identifiée (cf. research.md)19 **Primary Dependencies**: bibliothèque standard uniquement pour ce module (`sqlite3`, `pathlib`, `shutil`, `subprocess` pour déclencher le montage natif) ; pas de dépendance tierce nouvelle identifiée (cf. research.md)
18 20
19-**Storage**: SQLite — deux bases distinctes : la base de contexte centralisée (fichier niveau contexte de travail, ouverture/bootstrap assurés par ce module via `regine_core/config/db.py`, mais dont la table `boitiers` est possédée par le module `camera_profile`, cf. `specs/002-profil-boitiers-optionnel/plan.md` § Correction de placement) et les bases de données de travail par dossier (une par dossier local, initialisées par ce module mais dont le schéma détaillé relève du module `archive`/`dossier`, cf. constitution § Workflow d'archivage)21+**Storage**: SQLite — **révisé le 2026-09-23/24** : désormais deux bases distinctes, à des emplacements différents plutôt qu'une base unique à emplacement unique :
22+1. **Configuration globale** (`~/.regine/config.sqlite3`, nouveau nom de fichier — FR-014) : uniquement les trois chemins de travail et l'état SMB connu (table `contexte`, inchangée dans sa forme). Propre à la machine, jamais répliquée.
23+2. **Base de contexte centralisée** (`<local_work_dir>/contexte.sqlite3` — FR-006/FR-015, ex-`~/.regine/contexte.sqlite3`) : boîtiers (table `boitiers`, possédée par `camera_profile`, cf. `specs/002-profil-boitiers-optionnel/plan.md`), catégories (`specs/004-categorisation-dossiers`), historique des imports (table `checksums_importes`, possédée par `import_carte.historique`, cf. `specs/001-import-photos`). Répliquée vers l'archive NAS à un emplacement fixe (FR-016) et restaurable depuis celle-ci (FR-017).
24+
25+L'ancienne « base de données de travail par dossier » (schéma manifeste `fichiers`/`verrou`, une instance par dossier local) est retirée de ce plan : jamais réellement exploitée (écart déjà documenté dans `contracts/regine-core-api.md`, le verrou/manifeste vivant en réalité uniquement côté archive, `specs/005-checkout-reconciliation`) — FR-006 révisée ne désigne plus ce schéma.
20 26
21 **Testing**: pytest ; tests de contrat sur la surface CLI (entrées/sorties texte, codes de sortie), tests d'intégration sur le cycle configuration → initialisation de la base de travail, tests unitaires sur la validation des chemins et la logique de désambiguïsation de boîtiers27 **Testing**: pytest ; tests de contrat sur la surface CLI (entrées/sorties texte, codes de sortie), tests d'intégration sur le cycle configuration → initialisation de la base de travail, tests unitaires sur la validation des chemins et la logique de désambiguïsation de boîtiers
22 28
@@ -50,6 +56,17 @@ Aucune violation identifiée ; la section Complexity Tracking reste vide.
50 56
51 **Re-check post Phase 1** (après génération de `data-model.md`, `contracts/cli-config.md`, `quickstart.md`) : le modèle de données (deux bases SQLite distinctes, conventions `PRAGMA user_version`/`application_id` réutilisées) et le contrat CLI (protocole texte in/out, refus explicite plutôt que migration silencieuse pour FR-010, parcours NAS non exécuté sans `--confirm` pour FR-012) confirment chaque évaluation PASS ci-dessus. Aucune violation nouvelle introduite par la conception détaillée.57 **Re-check post Phase 1** (après génération de `data-model.md`, `contracts/cli-config.md`, `quickstart.md`) : le modèle de données (deux bases SQLite distinctes, conventions `PRAGMA user_version`/`application_id` réutilisées) et le contrat CLI (protocole texte in/out, refus explicite plutôt que migration silencieuse pour FR-010, parcours NAS non exécuté sans `--confirm` pour FR-012) confirment chaque évaluation PASS ci-dessus. Aucune violation nouvelle introduite par la conception détaillée.
52 58
59+**Re-check post User Story 5 (2026-09-23/24)** :
60+
61+| Principe / contrainte | Évaluation | Justification |
62+|---|---|---|
63+| II. Confirmation explicite avant toute action à risque | PASS | La réplication (FR-016) et la restauration (FR-017) écrasent chacune une seule cible bien identifiée (la sauvegarde précédente sur l'archive, ou la base locale absente) — jamais un fichier contenant un travail non confirmé ; aucune des deux n'est une action destructive au sens du Principe II. |
64+| V. L'utilisateur décide, Régine suggère | PASS | FR-016/FR-017 sont déclenchées par des actions déjà explicites de l'utilisateur (`regine reconcile`, configuration initiale, `regine checkout`) — aucun nouveau geste caché, aucune sauvegarde/restauration imposée en dehors de ces moments (tranché explicitement par l'utilisateur pendant `/speckit-specify`, plutôt que l'option automatique-à-chaque-écriture initialement proposée). |
65+| VI. Bibliothèque centrale, façades minces | PASS | La réplication/restauration est orchestrée par les façades CLI (`archive_cmd.py`/`config_cmd.py`) qui appellent une fonction dédiée de `regine_core.config.context` — `regine_core.archive.checkout`/`reconciliation` (specs/005) restent inchangés et ignorants de `config` (pas de nouvelle dépendance inter-modules dans le cœur), cohérent avec le style d'orchestration déjà utilisé par `import_cmd.py` (specs/001). |
66+| Exécution sans démon | PASS | La réplication/restauration est une copie de fichier ponctuelle exécutée dans le même processus que la commande qui la déclenche, jamais un service en arrière-plan. |
67+
68+Aucune violation nouvelle ; Complexity Tracking reste vide.
69+
53 ## Project Structure70 ## Project Structure
54 71
55 ### Documentation (this feature)72 ### Documentation (this feature)
@@ -119,6 +136,60 @@ packages/
119 136
120 **Extension du 2026-09-21 (User Story 4)** : `regine-gui` n'est plus un paquet vide — `specs/006-gui-tri-consultation` l'a implémenté entre-temps (PySide6, `FenetrePrincipale` à onglets dans `app.py`). Cette extension y ajoute un troisième onglet (`config_view.py`), construit à l'identique de `tri_view.py`/`consultation_view.py` (une classe `QMainWindow`/`QWidget` qui n'appelle que des fonctions déjà publiques de `regine_core.config`, jamais de logique propre) — aucun nouveau research nécessaire (aucune inconnue technique introduite), aucun nouveau contrat `regine-core-api.md` (les fonctions appelées sont déjà toutes documentées).137 **Extension du 2026-09-21 (User Story 4)** : `regine-gui` n'est plus un paquet vide — `specs/006-gui-tri-consultation` l'a implémenté entre-temps (PySide6, `FenetrePrincipale` à onglets dans `app.py`). Cette extension y ajoute un troisième onglet (`config_view.py`), construit à l'identique de `tri_view.py`/`consultation_view.py` (une classe `QMainWindow`/`QWidget` qui n'appelle que des fonctions déjà publiques de `regine_core.config`, jamais de logique propre) — aucun nouveau research nécessaire (aucune inconnue technique introduite), aucun nouveau contrat `regine-core-api.md` (les fonctions appelées sont déjà toutes documentées).
121 138
139+**Extension du 2026-09-23/24 (User Story 5)** :
140+
141+```text
142+packages/
143+├── regine-core/
144+│ └── src/regine_core/
145+│ ├── config/
146+│ │ ├── db.py # RÉVISÉ : open_context_db(local_work_dir) exige désormais
147+│ │ │ local_work_dir explicite (plus de défaut ~/.regine/) ;
148+│ │ │ nouvelle open_global_config_db(path=None), défaut
149+│ │ │ ~/.regine/config.sqlite3, table `contexte` uniquement
150+│ │ │ (chemins), jamais boîtiers/catégories/historique
151+│ │ ├── environnement.py # NOUVEAU — ouvrir_environnement() : résout la config
152+│ │ │ globale puis, si local_work_dir déjà configuré, ouvre
153+│ │ │ la base de contexte centralisée à sa racine (FR-014/015) ;
154+│ │ │ point d'entrée unique remplaçant les appels épars à
155+│ │ │ open_context_db(None) dans les façades CLI/GUI
156+│ │ └── context.py # ÉTENDU — sauvegarder_contexte(local_work_dir, archive_root)
157+│ │ (FR-016) ; restaurer_contexte_si_absent(local_work_dir,
158+│ │ archive_root) -> bool (FR-017) ; enregistrer_chemins
159+│ │ n'initialise plus le manifeste vide à la racine de
160+│ │ local_work_dir (retiré, cf. Technical Context) — ouvre
161+│ │ désormais la base de contexte centralisée à cet endroit
162+│ │ via db.open_context_db, l'y initialise si absente
163+│ └── import_carte/
164+│ └── historique.py # INCHANGÉ dans son API (specs/001) — la connexion qu'il
165+│ reçoit vient désormais de ouvrir_environnement(), pas
166+│ d'un open_context_db(None) implicite
167+│
168+├── regine-cli/
169+│ └── src/regine_cli/
170+│ ├── config_cmd.py # ÉTENDU — utilise ouvrir_environnement() ; restauration
171+│ │ automatique (FR-017) appelée après set-paths si le
172+│ │ contexte local est absent et l'archive en a une copie
173+│ ├── archive_cmd.py # ÉTENDU — _cmd_reconcile appelle sauvegarder_contexte()
174+│ │ après un archivage réussi (FR-016) ; _cmd_checkout
175+│ │ appelle restaurer_contexte_si_absent() après un premier
176+│ │ checkout réussi (FR-017)
177+│ └── import_cmd.py # CORRIGÉ — utilisait déjà (par erreur, avant cette
178+│ révision) local_root / ".regine-contexte.sqlite3" comme
179+│ contexte_db par défaut plutôt que le mécanisme partagé ;
180+│ utilise désormais ouvrir_environnement() comme les autres
181+│ commandes CLI
182+│
183+└── regine-gui/
184+ └── src/regine_gui/
185+ └── app.py # ÉTENDU — FenetrePrincipale résout l'environnement une
186+ fois au démarrage (ouvrir_environnement()) et le propage
187+ aux onglets, plutôt que chaque écran ouvrant lui-même
188+ open_context_db(None) indépendamment
189+```
190+
191+Aucun nouveau paquet, aucune nouvelle dépendance externe — uniquement une réorganisation de la résolution de connexion SQLite déjà en place, et deux fonctions ajoutées à `regine_core.config.context`.
192+
122 ## Complexity Tracking193 ## Complexity Tracking
123 194
124 *Aucune violation de gate à justifier — section laissée vide intentionnellement.*195 *Aucune violation de gate à justifier — section laissée vide intentionnellement.*
modified specs/003-config-contexte-travail/quickstart.md +34 -1
@@ -74,6 +74,39 @@ uv run python -m regine_gui.app
7474
7575 **Résultat attendu** : l'onglet « Configuration » affiche les mêmes trois chemins et le même état SMB que `regine config show` (scénario 1) ; modifier un chemin depuis cet onglet puis lancer `regine config show` en CLI reflète immédiatement le changement (SC-008) ; tenter d'y reproduire le scénario 3 (changement de `local-work-dir` avec travail non réconcilié) échoue avec le même message d'erreur que la CLI, présenté clairement dans l'interface plutôt que comme une exception technique ; la liste des boîtiers de cet onglet correspond à `regine config cameras list` (scénario 5), et y nommer un boîtier produit le même résultat.
7676
77+## Scénario 8 — Réplication du contexte vers l'archive à l'archivage d'un dossier (User Story 5, FR-016)
78+
79+```bash
80+# Après le scénario 5 (au moins un boîtier nommé) et un import/checkout suivi d'un archivage :
81+regine reconcile /Volumes/NAS/photos/2026/2026-08-15_Sortie --local-dest ~/regine/travail/2026-08-15_Sortie
82+```
83+
84+**Résultat attendu** : en plus du comportement déjà couvert par `specs/005-checkout-reconciliation`, une copie de `~/regine/travail/contexte.sqlite3` apparaît à `/Volumes/NAS/photos/.regine-contexte.sqlite3`, reflétant les boîtiers nommés/catégories connues à cet instant.
85+
86+## Scénario 9 — Reprise sur une nouvelle machine (User Story 5, FR-017, SC-009/SC-010)
87+
88+```bash
89+# Sur une installation "neuve" (nouveau HOME, donc nouveau ~/.regine), pointant vers la même archive :
90+regine config set-paths \
91+ --temp-dir ~/regine/tmp \
92+ --local-work-dir ~/regine/travail \
93+ --archive-smb smb://nas.local/regine-archive
94+
95+regine config cameras list
96+```
97+
98+**Résultat attendu** : `set-paths` annonce la restauration du contexte depuis l'archive (« Contexte restauré depuis l'archive : N boîtier(s), N catégorie(s) ») ; `cameras list` affiche immédiatement les mêmes boîtiers nommés que sur l'ancienne installation (scénario 5), sans aucun nouvel import.
99+
100+## Scénario 10 — Restauration au premier checkout, si la configuration initiale n'a pas suffi (User Story 5, FR-017, Edge Case)
101+
102+```bash
103+# Installation neuve configurée pendant que l'archive était inaccessible (restauration différée) :
104+regine checkout /Volumes/NAS/photos/2026/2026-08-15_Sortie --local-dest ~/regine/travail/2026-08-15_Sortie
105+regine config cameras list
106+```
107+
108+**Résultat attendu** : le premier `regine checkout` réussi déclenche à son tour la restauration du contexte (si elle n'a pas déjà eu lieu) ; `cameras list` retrouve les boîtiers nommés de l'ancienne installation, au plus tard à ce moment.
109+
77110 ## Critères de sortie
78111
79-Les sept scénarios ci-dessus, exécutés dans l'ordre sur un contexte propre, doivent tous produire le résultat attendu — les scénarios 1 à 6 sans intervention manuelle (uniquement via la CLI), le scénario 7 en validation manuelle de la GUI (cf. `specs/006-gui-tri-consultation/research.md` § 2 : pas de suite de tests automatisée pour l'interface elle-même). Tout écart doit être tracé comme régression avant de considérer la fonctionnalité prête pour `/speckit-tasks` → implémentation suivante.
112+Les dix scénarios ci-dessus, exécutés dans l'ordre sur un contexte propre, doivent tous produire le résultat attendu — les scénarios 1 à 6 et 8 à 10 sans intervention manuelle (uniquement via la CLI), le scénario 7 en validation manuelle de la GUI (cf. `specs/006-gui-tri-consultation/research.md` § 2 : pas de suite de tests automatisée pour l'interface elle-même). Tout écart doit être tracé comme régression avant de considérer la fonctionnalité prête pour `/speckit-tasks` → implémentation suivante.
@@ -74,6 +74,39 @@ uv run python -m regine_gui.app
74 74
75 **Résultat attendu** : l'onglet « Configuration » affiche les mêmes trois chemins et le même état SMB que `regine config show` (scénario 1) ; modifier un chemin depuis cet onglet puis lancer `regine config show` en CLI reflète immédiatement le changement (SC-008) ; tenter d'y reproduire le scénario 3 (changement de `local-work-dir` avec travail non réconcilié) échoue avec le même message d'erreur que la CLI, présenté clairement dans l'interface plutôt que comme une exception technique ; la liste des boîtiers de cet onglet correspond à `regine config cameras list` (scénario 5), et y nommer un boîtier produit le même résultat.75 **Résultat attendu** : l'onglet « Configuration » affiche les mêmes trois chemins et le même état SMB que `regine config show` (scénario 1) ; modifier un chemin depuis cet onglet puis lancer `regine config show` en CLI reflète immédiatement le changement (SC-008) ; tenter d'y reproduire le scénario 3 (changement de `local-work-dir` avec travail non réconcilié) échoue avec le même message d'erreur que la CLI, présenté clairement dans l'interface plutôt que comme une exception technique ; la liste des boîtiers de cet onglet correspond à `regine config cameras list` (scénario 5), et y nommer un boîtier produit le même résultat.
76 76
77+## Scénario 8 — Réplication du contexte vers l'archive à l'archivage d'un dossier (User Story 5, FR-016)
78+
79+```bash
80+# Après le scénario 5 (au moins un boîtier nommé) et un import/checkout suivi d'un archivage :
81+regine reconcile /Volumes/NAS/photos/2026/2026-08-15_Sortie --local-dest ~/regine/travail/2026-08-15_Sortie
82+```
83+
84+**Résultat attendu** : en plus du comportement déjà couvert par `specs/005-checkout-reconciliation`, une copie de `~/regine/travail/contexte.sqlite3` apparaît à `/Volumes/NAS/photos/.regine-contexte.sqlite3`, reflétant les boîtiers nommés/catégories connues à cet instant.
85+
86+## Scénario 9 — Reprise sur une nouvelle machine (User Story 5, FR-017, SC-009/SC-010)
87+
88+```bash
89+# Sur une installation "neuve" (nouveau HOME, donc nouveau ~/.regine), pointant vers la même archive :
90+regine config set-paths \
91+ --temp-dir ~/regine/tmp \
92+ --local-work-dir ~/regine/travail \
93+ --archive-smb smb://nas.local/regine-archive
94+
95+regine config cameras list
96+```
97+
98+**Résultat attendu** : `set-paths` annonce la restauration du contexte depuis l'archive (« Contexte restauré depuis l'archive : N boîtier(s), N catégorie(s) ») ; `cameras list` affiche immédiatement les mêmes boîtiers nommés que sur l'ancienne installation (scénario 5), sans aucun nouvel import.
99+
100+## Scénario 10 — Restauration au premier checkout, si la configuration initiale n'a pas suffi (User Story 5, FR-017, Edge Case)
101+
102+```bash
103+# Installation neuve configurée pendant que l'archive était inaccessible (restauration différée) :
104+regine checkout /Volumes/NAS/photos/2026/2026-08-15_Sortie --local-dest ~/regine/travail/2026-08-15_Sortie
105+regine config cameras list
106+```
107+
108+**Résultat attendu** : le premier `regine checkout` réussi déclenche à son tour la restauration du contexte (si elle n'a pas déjà eu lieu) ; `cameras list` retrouve les boîtiers nommés de l'ancienne installation, au plus tard à ce moment.
109+
77 ## Critères de sortie110 ## Critères de sortie
78 111
79-Les sept scénarios ci-dessus, exécutés dans l'ordre sur un contexte propre, doivent tous produire le résultat attendu — les scénarios 1 à 6 sans intervention manuelle (uniquement via la CLI), le scénario 7 en validation manuelle de la GUI (cf. `specs/006-gui-tri-consultation/research.md` § 2 : pas de suite de tests automatisée pour l'interface elle-même). Tout écart doit être tracé comme régression avant de considérer la fonctionnalité prête pour `/speckit-tasks` → implémentation suivante.112+Les dix scénarios ci-dessus, exécutés dans l'ordre sur un contexte propre, doivent tous produire le résultat attendu — les scénarios 1 à 6 et 8 à 10 sans intervention manuelle (uniquement via la CLI), le scénario 7 en validation manuelle de la GUI (cf. `specs/006-gui-tri-consultation/research.md` § 2 : pas de suite de tests automatisée pour l'interface elle-même). Tout écart doit être tracé comme régression avant de considérer la fonctionnalité prête pour `/speckit-tasks` → implémentation suivante.
modified specs/003-config-contexte-travail/research.md +22 -1
@@ -49,6 +49,27 @@
4949 - Poetry avec dépendances de chemin (`path = "../regine-core"`) — alternative valable, écosystème plus ancien et plus répandu ; non retenue par préférence pour la rapidité et la simplicité de configuration de `uv`, mais ce choix n'est pas structurant pour la conception elle-même et pourrait être révisé sans impact sur `data-model.md`/`contracts/`.
5050 - Dépôts séparés (un par façade) — rejeté : complique la coordination des changements d'API de `regine-core` avec ses façades, alors que le projet est encore à un stade où bibliothèque et façades évoluent ensemble ; un monorepo reste plus simple à faire évoluer tant qu'aucune façade n'a de cycle de release indépendant.
5151
52+## 6. Séparation configuration globale / base de contexte centralisée (User Story 5, 2026-09-23/24)
53+
54+**Decision**: Deux fichiers SQLite distincts plutôt qu'un fichier unique à deux emplacements possibles. `~/.regine/config.sqlite3` (nouveau) ne porte que la table `contexte` (chemins, état SMB) — mêmes conventions `PRAGMA application_id`/`user_version` que l'existant, juste un schéma réduit. `<local_work_dir>/contexte.sqlite3` (ex-`~/.regine/contexte.sqlite3`, déplacé) porte `boitiers`/`categories`/`checksums_importes`. Un nouveau point d'entrée unique, `regine_core.config.environnement.ouvrir_environnement()`, résout les deux dans l'ordre (config globale d'abord, puis contexte centralisée si `local_work_dir` est déjà connu) et remplace les appels épars à `open_context_db(None)` dans chaque façade CLI/GUI.
55+
56+**Rationale**: SQLite plutôt que JSON pour la configuration globale, malgré sa taille minime (3 chemins + 1 état) : cohérent avec le principe déjà établi dans ce module (research.md § 2, « inventer un second mécanisme de persistance pour un besoin très proche » a déjà été rejeté une fois) — réutiliser exactement le même bootstrap `PRAGMA application_id`/`ensure_schema` déjà écrit et testé, plutôt qu'introduire un second format de fichier (JSON) et sa propre gestion d'erreurs (fichier corrompu, absent, verrouillé) pour un gain marginal. Un point d'entrée unique (`ouvrir_environnement`) plutôt que de dupliquer la logique de résolution en deux temps dans chaque commande CLI et chaque écran GUI : exactement le même raisonnement déjà appliqué à `smb.resoudre_archive_locale` (specs/006 research.md § 6) pour éviter qu'une façade diverge silencieusement d'une autre.
57+
58+**Alternatives considered**:
59+- Fichier JSON pour la configuration globale — rejeté, cf. rationale (deuxième format de fichier pour un gain marginal, alors qu'un schéma SQLite minimal coûte une seule table).
60+- Garder un unique fichier à un unique emplacement, avec un simple booléen « à répliquer ou non » par table — rejeté : ne résout pas le vrai besoin (un nouvel ordinateur doit pouvoir localiser ce fichier sans déjà le connaître, cf. FR-007) ; deux emplacements physiques distincts sont la façon la plus directe de garantir cette indépendance.
61+- Continuer à appeler `open_context_db(None)` séparément dans chaque façade, chacune recalculant `local_work_dir` à partir de la config globale — rejeté : duplique la logique de résolution en deux temps à N endroits, avec le risque déjà matérialisé une fois dans ce projet (bug réel : `import_cmd.py` utilisait un chemin par défaut différent de toutes les autres commandes, jamais détecté avant ce jour faute de point d'entrée unique).
62+
63+## 7. Déclenchement de la réplication/restauration : orchestration en façade, pas dans `regine_core.archive`
64+
65+**Decision**: `sauvegarder_contexte()`/`restaurer_contexte_si_absent()` vivent dans `regine_core.config.context` ; elles sont appelées explicitement par les commandes CLI (`archive_cmd._cmd_reconcile`/`_cmd_checkout`) juste après un `reconcile`/`checkout` réussi — jamais depuis l'intérieur de `regine_core.archive.reconciliation`/`checkout` elles-mêmes.
66+
67+**Rationale**: `regine_core.archive` (specs/005) n'a aujourd'hui aucune dépendance vers `regine_core.config` ; lui en ajouter une pour ce seul besoin couplerait deux modules déjà stables et testés indépendamment, pour une préoccupation (sauvegarde du contexte) qui n'a rien à voir avec la réconciliation de fichiers elle-même. Orchestrer depuis la façade CLI (qui connaît déjà les deux modules, cf. `import_cmd.py` orchestrant `import_carte`/`camera_profile`/`config.categories` sans qu'aucun de ces modules ne se connaisse entre eux) garde chaque module `regine_core` focalisé sur une seule responsabilité (Principe VI).
68+
69+**Alternatives considered**:
70+- Un hook/callback enregistré dans `regine_core.archive` — rejeté, complexité d'indirection non justifiée pour deux points d'appel connus et fixes.
71+- Fusionner la sauvegarde dans `reconciliation.archiver()` elle-même — rejeté, cf. rationale (couplage inter-modules non nécessaire).
72+
5273 ## Résumé
5374
54-Tous les points marqués `NEEDS CLARIFICATION` dans le Technical Context du plan sont résolus par les décisions ci-dessus. Aucune dépendance tierce Python nouvelle n'est introduite pour le code de `regine-core`/`regine-cli` ; `uv` est un outil de développement/workspace (pas une dépendance runtime des paquets) introduit par la décision § 5.
75+Tous les points marqués `NEEDS CLARIFICATION` dans le Technical Context du plan sont résolus par les décisions ci-dessus. Aucune dépendance tierce Python nouvelle n'est introduite pour le code de `regine-core`/`regine-cli` ; `uv` est un outil de développement/workspace (pas une dépendance runtime des paquets) introduit par la décision § 5. L'extension du 2026-09-23/24 (User Story 5) n'introduit elle non plus aucune dépendance nouvelle — uniquement une réorganisation de fichiers SQLite déjà en place et deux fonctions ajoutées à un module existant.
@@ -49,6 +49,27 @@
49 - Poetry avec dépendances de chemin (`path = "../regine-core"`) — alternative valable, écosystème plus ancien et plus répandu ; non retenue par préférence pour la rapidité et la simplicité de configuration de `uv`, mais ce choix n'est pas structurant pour la conception elle-même et pourrait être révisé sans impact sur `data-model.md`/`contracts/`.49 - Poetry avec dépendances de chemin (`path = "../regine-core"`) — alternative valable, écosystème plus ancien et plus répandu ; non retenue par préférence pour la rapidité et la simplicité de configuration de `uv`, mais ce choix n'est pas structurant pour la conception elle-même et pourrait être révisé sans impact sur `data-model.md`/`contracts/`.
50 - Dépôts séparés (un par façade) — rejeté : complique la coordination des changements d'API de `regine-core` avec ses façades, alors que le projet est encore à un stade où bibliothèque et façades évoluent ensemble ; un monorepo reste plus simple à faire évoluer tant qu'aucune façade n'a de cycle de release indépendant.50 - Dépôts séparés (un par façade) — rejeté : complique la coordination des changements d'API de `regine-core` avec ses façades, alors que le projet est encore à un stade où bibliothèque et façades évoluent ensemble ; un monorepo reste plus simple à faire évoluer tant qu'aucune façade n'a de cycle de release indépendant.
51 51
52+## 6. Séparation configuration globale / base de contexte centralisée (User Story 5, 2026-09-23/24)
53+
54+**Decision**: Deux fichiers SQLite distincts plutôt qu'un fichier unique à deux emplacements possibles. `~/.regine/config.sqlite3` (nouveau) ne porte que la table `contexte` (chemins, état SMB) — mêmes conventions `PRAGMA application_id`/`user_version` que l'existant, juste un schéma réduit. `<local_work_dir>/contexte.sqlite3` (ex-`~/.regine/contexte.sqlite3`, déplacé) porte `boitiers`/`categories`/`checksums_importes`. Un nouveau point d'entrée unique, `regine_core.config.environnement.ouvrir_environnement()`, résout les deux dans l'ordre (config globale d'abord, puis contexte centralisée si `local_work_dir` est déjà connu) et remplace les appels épars à `open_context_db(None)` dans chaque façade CLI/GUI.
55+
56+**Rationale**: SQLite plutôt que JSON pour la configuration globale, malgré sa taille minime (3 chemins + 1 état) : cohérent avec le principe déjà établi dans ce module (research.md § 2, « inventer un second mécanisme de persistance pour un besoin très proche » a déjà été rejeté une fois) — réutiliser exactement le même bootstrap `PRAGMA application_id`/`ensure_schema` déjà écrit et testé, plutôt qu'introduire un second format de fichier (JSON) et sa propre gestion d'erreurs (fichier corrompu, absent, verrouillé) pour un gain marginal. Un point d'entrée unique (`ouvrir_environnement`) plutôt que de dupliquer la logique de résolution en deux temps dans chaque commande CLI et chaque écran GUI : exactement le même raisonnement déjà appliqué à `smb.resoudre_archive_locale` (specs/006 research.md § 6) pour éviter qu'une façade diverge silencieusement d'une autre.
57+
58+**Alternatives considered**:
59+- Fichier JSON pour la configuration globale — rejeté, cf. rationale (deuxième format de fichier pour un gain marginal, alors qu'un schéma SQLite minimal coûte une seule table).
60+- Garder un unique fichier à un unique emplacement, avec un simple booléen « à répliquer ou non » par table — rejeté : ne résout pas le vrai besoin (un nouvel ordinateur doit pouvoir localiser ce fichier sans déjà le connaître, cf. FR-007) ; deux emplacements physiques distincts sont la façon la plus directe de garantir cette indépendance.
61+- Continuer à appeler `open_context_db(None)` séparément dans chaque façade, chacune recalculant `local_work_dir` à partir de la config globale — rejeté : duplique la logique de résolution en deux temps à N endroits, avec le risque déjà matérialisé une fois dans ce projet (bug réel : `import_cmd.py` utilisait un chemin par défaut différent de toutes les autres commandes, jamais détecté avant ce jour faute de point d'entrée unique).
62+
63+## 7. Déclenchement de la réplication/restauration : orchestration en façade, pas dans `regine_core.archive`
64+
65+**Decision**: `sauvegarder_contexte()`/`restaurer_contexte_si_absent()` vivent dans `regine_core.config.context` ; elles sont appelées explicitement par les commandes CLI (`archive_cmd._cmd_reconcile`/`_cmd_checkout`) juste après un `reconcile`/`checkout` réussi — jamais depuis l'intérieur de `regine_core.archive.reconciliation`/`checkout` elles-mêmes.
66+
67+**Rationale**: `regine_core.archive` (specs/005) n'a aujourd'hui aucune dépendance vers `regine_core.config` ; lui en ajouter une pour ce seul besoin couplerait deux modules déjà stables et testés indépendamment, pour une préoccupation (sauvegarde du contexte) qui n'a rien à voir avec la réconciliation de fichiers elle-même. Orchestrer depuis la façade CLI (qui connaît déjà les deux modules, cf. `import_cmd.py` orchestrant `import_carte`/`camera_profile`/`config.categories` sans qu'aucun de ces modules ne se connaisse entre eux) garde chaque module `regine_core` focalisé sur une seule responsabilité (Principe VI).
68+
69+**Alternatives considered**:
70+- Un hook/callback enregistré dans `regine_core.archive` — rejeté, complexité d'indirection non justifiée pour deux points d'appel connus et fixes.
71+- Fusionner la sauvegarde dans `reconciliation.archiver()` elle-même — rejeté, cf. rationale (couplage inter-modules non nécessaire).
72+
52 ## Résumé73 ## Résumé
53 74
54-Tous les points marqués `NEEDS CLARIFICATION` dans le Technical Context du plan sont résolus par les décisions ci-dessus. Aucune dépendance tierce Python nouvelle n'est introduite pour le code de `regine-core`/`regine-cli` ; `uv` est un outil de développement/workspace (pas une dépendance runtime des paquets) introduit par la décision § 5.75+Tous les points marqués `NEEDS CLARIFICATION` dans le Technical Context du plan sont résolus par les décisions ci-dessus. Aucune dépendance tierce Python nouvelle n'est introduite pour le code de `regine-core`/`regine-cli` ; `uv` est un outil de développement/workspace (pas une dépendance runtime des paquets) introduit par la décision § 5. L'extension du 2026-09-23/24 (User Story 5) n'introduit elle non plus aucune dépendance nouvelle — uniquement une réorganisation de fichiers SQLite déjà en place et deux fonctions ajoutées à un module existant.
modified specs/003-config-contexte-travail/spec.md +44 -11
@@ -20,6 +20,12 @@
2020 - Q: La base de contexte centralisée doit-elle vivre à un emplacement standard différent selon le système d'exploitation (Linux/macOS/Windows) ? → A: Non, pas pour l'instant — l'emplacement unique déjà en place (`~/.regine/`, indépendant de l'OS) est conservé tel quel ; une convention par OS reste une amélioration possible d'une prochaine version, pas un besoin actuel.
2121 - Q: L'interface graphique doit-elle pouvoir consulter et modifier le contexte de travail ? → A: Oui — en appelant directement `regine_core.config` (mêmes fonctions que la CLI), sans aucune logique de configuration propre à la GUI (Principe VI). Couvert par la nouvelle User Story 4 ci-dessous.
2222
23+### Session 2026-09-23
24+
25+- Q: `~/.regine/` doit-il continuer à héberger l'intégralité de la base de contexte (chemins, boîtiers, catégories, historique d'imports), ou seulement une partie ? → A: Seulement les trois chemins de travail (FR-001), dans une configuration globale minimale propre à la machine. Le reste (boîtiers nommés, catégories connues, historique des imports — cf. `specs/001-import-photos` FR-004) déménage à la racine du répertoire de travail local, dans le fichier déjà prévu par FR-006 (jusqu'ici sous-défini) — écart trouvé par l'utilisateur : ce contenu représentait un savoir accumulé important (désambiguïsation de boîtiers, catégorisation apprise, historique anti-doublon) qui ne survivait pas à la perte de la machine locale, alors que les photos elles-mêmes sont protégées sur le NAS. Ce déménagement corrige aussi une confusion déjà présente dans les Assumptions de cette spec : FR-006 (« base de données de travail » à la racine du répertoire local) et la base de contexte centralisée décrite plus bas désignaient jusqu'ici deux choses distinctes, sans qu'aucune des deux ne soit réellement précisée ni exploitée par le code existant — elles fusionnent en une seule (cf. FR-006 révisée, User Story 5).
26+- Q: Quand la base de contexte centralisée doit-elle être répliquée vers l'archive NAS (FR-016) ? → A: Manuellement, au moment où un dossier est archivé (`regine reconcile`) — jamais automatiquement à chaque modification isolée du contexte, ni via une commande dédiée séparée. Rationale de l'utilisateur : archiver un dossier suppose déjà d'écrire sur le NAS, et l'archive globale a alors besoin du contexte associé à jour — pas de nouvelle dépendance au NAS introduite au-delà de celle déjà requise par `regine reconcile`.
27+- Q: Quand le contexte doit-il être retrouvé depuis l'archive sur une nouvelle machine (FR-017) ? → A: Automatiquement, à la configuration initiale des emplacements de travail, et lors de la récupération locale d'un dossier archivé (checkout) — les deux moments naturels où une nouvelle machine entre en contact avec l'archive pour la première fois, sans action de récupération dédiée supplémentaire à apprendre.
28+
2329 ## User Scenarios & Testing *(mandatory)*
2430
2531 ### User Story 1 - Configuration initiale des emplacements de travail (Priority: P1)
@@ -90,6 +96,24 @@ Un photographe qui utilise déjà l'interface graphique de Régine pour trier se
9096
9197 ---
9298
99+### User Story 5 - Reprendre le travail sur une nouvelle machine sans perdre le contexte (Priority: P2)
100+
101+Un photographe change d'ordinateur (nouvelle machine, réinstallation, disque remplacé). Aucune photo n'est perdue puisqu'elles vivent sur le NAS, mais il veut retrouver, sans les ressaisir ni les reconstituer manuellement, ses boîtiers nommés, ses catégories connues et son historique d'imports déjà accumulés sur l'ancienne machine — en reconfigurant simplement Régine pour pointer vers la même archive.
102+
103+**Why this priority**: Sans cette garantie, tout incident sur la machine de travail ferait perdre silencieusement un savoir accumulé (désambiguïsation de boîtiers, catégorisation apprise, historique anti-doublon) que rien n'indiquerait comme sauvegardé — une régression grave pour un outil dont la promesse centrale est de ne jamais perdre d'information sans confirmation. Reste un raffinement au sens où l'usage quotidien sur une seule machine, jamais changée, n'en dépend pas.
104+
105+**Independent Test**: Peut être testé en enrichissant le contexte de travail (nommer un boîtier, réaliser un import) sur une installation, puis en configurant une seconde installation "neuve" (nouveau répertoire de travail local vide) pointant vers la même archive NAS, et en vérifiant que ce contexte est retrouvé sans réimport ni ressaisie.
106+
107+**Acceptance Scenarios**:
108+
109+1. **Given** un contexte de travail enrichi (boîtiers nommés, catégories connues, historique d'imports) sur une machine, **When** un dossier est archivé (`regine reconcile`) depuis cette machine, **Then** ce contexte est répliqué vers l'archive NAS au même moment, sans action de sauvegarde distincte à apprendre (FR-016).
110+2. **Given** une nouvelle machine sur laquelle Régine vient d'être configurée avec le même emplacement d'archive NAS qu'une installation précédente, **When** l'utilisateur termine la configuration des trois emplacements de travail, **Then** Régine retrouve automatiquement, depuis l'archive, le contexte enrichi de cette installation précédente (FR-017) — sans que l'utilisateur ait à réimporter une carte mémoire ou à renommer ses boîtiers de nouveau.
111+3. **Given** une nouvelle machine dont la configuration initiale n'a pas encore retrouvé de contexte existant sur l'archive (ex. configurée hors ligne), **When** l'utilisateur y effectue un premier checkout d'un dossier déjà archivé, **Then** Régine retrouve à cette occasion le contexte enrichi de l'installation précédente (FR-017), au plus tard à ce moment.
112+4. **Given** ce contexte retrouvé sur la nouvelle machine, **When** l'utilisateur consulte ses boîtiers nommés, ses catégories connues, ou lance un nouvel import, **Then** l'historique anti-doublon (`specs/001-import-photos` FR-004) et la désambiguïsation de boîtiers (`specs/002-profil-boitiers-optionnel`) se comportent exactement comme sur l'ancienne machine, sans repartir de zéro.
113+5. **Given** les trois emplacements de travail (FR-001), **When** l'utilisateur configure une nouvelle machine, **Then** seuls ces trois chemins doivent être ressaisis — jamais les boîtiers nommés, les catégories, ni l'historique d'imports.
114+
115+---
116+
93117 ### Edge Cases
94118
95119 - Que se passe-t-il si le répertoire temporaire et le répertoire de travail local pointent vers le même emplacement ? Régine DOIT avertir l'utilisateur de cette configuration ambiguë plutôt que de l'accepter silencieusement.
@@ -97,6 +121,8 @@ Un photographe qui utilise déjà l'interface graphique de Régine pour trier se
97121 - Que se passe-t-il si l'espace disque du répertoire de travail local devient insuffisant pour accueillir la base de données de travail ? Régine DOIT signaler une erreur de configuration claire plutôt que de créer une base de données partiellement écrite.
98122 - Que se passe-t-il si l'utilisateur tente de lancer une opération avant d'avoir configuré le contexte de travail ? Régine DOIT indiquer clairement quels chemins manquent plutôt que d'échouer avec une erreur technique.
99123 - Que se passe-t-il si l'utilisateur tente de changer le répertoire de travail local alors qu'un checkout ou un import est encore en cours (non réconcilié/archivé) ? Régine DOIT refuser ce changement et indiquer précisément ce qui doit être réconcilié ou archivé avant de pouvoir continuer.
124+- Que se passe-t-il si le contexte de travail local (boîtiers, catégories, historique) et la sauvegarde présente sur l'archive ont divergé (ex. deux machines utilisées successivement sans que la première n'ait sauvegardé ses derniers changements) ? Cette spécification suppose un contexte actif sur une seule machine à la fois (cf. Assumptions) : elle ne couvre pas la fusion de deux contextes divergents, seulement le remplacement du contexte local par la dernière sauvegarde connue sur l'archive.
125+- Que se passe-t-il si l'archive NAS n'est pas accessible au moment où Régine tenterait de retrouver ou de sauvegarder le contexte de travail (User Story 5) ? Régine DOIT réutiliser l'assistance au montage déjà prévue (User Story 2) plutôt que d'échouer sans explication, et ne DOIT jamais bloquer les opérations qui n'ont besoin que du contexte déjà présent localement.
100126
101127 ## Requirements *(mandatory)*
102128
@@ -107,23 +133,27 @@ Un photographe qui utilise déjà l'interface graphique de Régine pour trier se
107133 - **FR-003**: Lorsqu'un répertoire temporaire ou un répertoire de travail local configuré n'existe pas encore, le système DOIT proposer de le créer plutôt que d'échouer silencieusement.
108134 - **FR-004**: Lorsque le partage SMB configuré n'est pas accessible au moment où une opération en a besoin, le système DOIT détecter cette indisponibilité et déclencher le mécanisme de montage natif du système d'exploitation pour ce partage, plutôt que d'échouer avec une erreur générique. Le système ne DOIT PAS gérer ou stocker lui-même d'identifiants réseau : l'authentification et la sécurité des paramètres de connexion restent entièrement déléguées au système d'exploitation.
109135 - **FR-005**: Le système DOIT mémoriser l'emplacement SMB configuré (cf. FR-001) afin de pouvoir redéclencher son montage natif à chaque nouvelle indisponibilité détectée, sans que l'utilisateur ait à ressaisir ce chemin. Une fois l'accès au NAS rétabli après une indisponibilité détectée, le système DOIT permettre de reprendre l'opération initialement bloquée sans reconfiguration complète du contexte de travail.
110-- **FR-006**: Le système DOIT initialiser, à la racine du répertoire de travail local, une base de données de travail prête à l'usage avant qu'aucune autre opération de Régine n'en ait besoin.
111-- **FR-007**: Lorsqu'un dossier du répertoire de travail local est archivé vers le NAS, la base de données de travail correspondant à ce dossier DOIT être archivée avec lui, comme partie intégrante du dossier archivé.
136+- **FR-006** *(révisée le 2026-09-23)*: Le système DOIT initialiser, à la racine du répertoire de travail local, une base de données de travail prête à l'usage avant qu'aucune autre opération de Régine n'en ait besoin — cette base est le contexte de travail lui-même (boîtiers nommés, catégories connues, historique des imports, cf. FR-008/FR-015), pas un fichier distinct et vide.
137+- **FR-007** *(révisée le 2026-09-23)*: La configuration globale minimale (FR-014) permettant de localiser le répertoire de travail local NE DOIT jamais dépendre du contenu de ce répertoire pour exister — elle DOIT rester lisible même si le répertoire de travail local est absent ou vide (ex. nouvelle machine, cf. User Story 5), pour permettre de le retrouver ou de le reconstituer.
112138 - **FR-008**: À chaque import, le système DOIT enregistrer les références des boîtiers rencontrés (modèle et, si disponible, numéro de série) dans une base de contexte centralisée, et permettre à l'utilisateur, depuis cette base, d'attribuer un nom lisible à un boîtier déjà rencontré, sans nécessiter de nouvel import ni de parcours du NAS.
113139 - **FR-009**: Un nom attribué à un boîtier via la configuration DOIT être réutilisé partout où Régine identifie ce même boîtier par la suite (import, désambiguïsation de collision).
114140 - **FR-010**: Le système DOIT permettre de modifier tout paramètre du contexte de travail (chemins, boîtiers nommés) après la configuration initiale, sans réinstallation. Pour le répertoire de travail local spécifiquement, le système DOIT refuser ce changement tant qu'un checkout ou un import n'a pas été réconcilié ou archivé, et indiquer clairement ce qui bloque le changement, plutôt que de déplacer ou d'abandonner silencieusement du travail en cours.
115141 - **FR-011**: Le système DOIT être conçu pour accueillir de nouveaux paramètres de configuration à l'avenir sans remettre en cause les paramètres déjà définis.
116142 - **FR-012**: Le système PEUT proposer, comme action distincte et déclenchée volontairement par l'utilisateur, un parcours complet de l'archive NAS pour détecter les boîtiers de dossiers archivés avant l'existence de cette fonctionnalité ou jamais revus depuis ; cette capacité est complémentaire et n'est pas requise pour que FR-008 soit satisfaite.
117143 - **FR-013**: L'interface graphique DOIT pouvoir consulter et modifier le contexte de travail (chemins, boîtiers nommés) en appelant directement les mêmes fonctions de `regine_core.config` que la CLI, sans dupliquer la moindre règle de validation ou de désambiguïsation (Principe VI) — toute règle déjà vraie côté CLI (FR-003/FR-004/FR-010/FR-012) reste vraie à l'identique depuis la GUI.
144+- **FR-014** *(ajoutée le 2026-09-23)*: Le système DOIT conserver les trois emplacements de travail (FR-001) dans une configuration globale minimale, propre à la machine, à un emplacement fixe et indépendant du contenu du répertoire de travail local (cf. FR-007) — c'est la seule information que l'utilisateur doit ressaisir en configurant Régine sur une nouvelle machine.
145+- **FR-015** *(ajoutée le 2026-09-23)*: Le système DOIT stocker les boîtiers nommés (FR-008), les catégories connues (`specs/004-categorisation-dossiers`) et l'historique des imports (`specs/001-import-photos` FR-004) à la racine du répertoire de travail local (FR-006) — jamais dans la configuration globale de FR-014, dont ce contenu doit rester indépendant.
146+- **FR-016** *(ajoutée le 2026-09-23)*: Le système DOIT protéger le contenu de FR-015 contre la perte du répertoire de travail local, en le répliquant vers l'archive NAS à chaque archivage d'un dossier (`regine reconcile`, `specs/005-checkout-reconciliation`) — jamais automatiquement à chaque modification isolée du contexte (nommage d'un boîtier, import réalisé), ni via une commande dédiée séparée. Cette réplication ne DOIT introduire aucune nouvelle dépendance au NAS au-delà de celle déjà requise par `regine reconcile` lui-même : elle profite du seul moment où une écriture sur l'archive a de toute façon déjà lieu.
147+- **FR-017** *(ajoutée le 2026-09-23)*: Quand le répertoire de travail local ne contient pas (ou plus) le contenu de FR-015 alors qu'une sauvegarde en existe sur l'archive NAS (cf. FR-016), le système DOIT le retrouver automatiquement, sans action supplémentaire de l'utilisateur, à l'un des deux moments suivants : lors de la configuration initiale des emplacements de travail sur une machine (FR-001), ou lors d'un premier checkout d'un dossier archivé (`specs/005-checkout-reconciliation`) réalisé depuis cette machine.
118148
119149 ### Key Entities
120150
121-- **Contexte de travail** : ensemble des paramètres de configuration actifs (chemins, boîtiers nommés) utilisés par l'ensemble des modules de Régine.
151+- **Contexte de travail** : ensemble des paramètres actifs (chemins, boîtiers nommés, catégories, historique d'imports) utilisés par l'ensemble des modules de Régine — réparti, depuis le 2026-09-23, entre la configuration globale et la base de contexte centralisée ci-dessous.
152+- **Configuration globale** *(ajoutée le 2026-09-23)* : fichier minimal, propre à la machine, à un emplacement fixe (`~/.regine/`) — contient uniquement les trois emplacements de travail (FR-001/FR-014), jamais les boîtiers, catégories, ni l'historique d'imports. Sert uniquement à localiser la base de contexte centralisée ; reste lisible même si le répertoire de travail local est vide ou absent (FR-007).
122153 - **Répertoire temporaire** : emplacement local destiné aux copies brutes avant analyse, en amont d'un import.
123-- **Répertoire de travail local** : emplacement local où résident les copies de travail (checkout, import en cours) et leur base de données.
124-- **Emplacement de l'archive (NAS/SMB)** : chemin réseau où réside l'archive persistante de Régine.
125-- **Base de données de travail** : base persistante créée à la racine du répertoire de travail local, associée à chaque dossier local et archivée avec lui — distincte de la base de contexte centralisée ci-dessous.
126-- **Base de contexte centralisée** : base persistante au niveau du contexte de travail (pas par dossier), alimentée à chaque import avec les références de boîtiers rencontrés ; source par défaut de la détection de boîtiers de la User Story 3.
154+- **Répertoire de travail local** : emplacement local où résident les copies de travail (checkout, import en cours) et la base de contexte centralisée.
155+- **Emplacement de l'archive (NAS/SMB)** : chemin réseau où réside l'archive persistante de Régine, et où est répliquée une sauvegarde de la base de contexte centralisée (FR-016).
156+- **Base de contexte centralisée** *(révisée le 2026-09-23)* : base persistante unique, vivant désormais à la racine du répertoire de travail local (fusionne l'ancienne « base de données de travail », jusqu'ici sous-définie, et l'ancienne base de contexte centralisée de `~/.regine/`) — contient les boîtiers rencontrés, les catégories connues, et l'historique des imports (`specs/001-import-photos` FR-004). Alimentée à chaque import ; source par défaut de la détection de boîtiers de la User Story 3 ; protégée contre la perte de la machine locale par réplication vers l'archive NAS (FR-016/FR-017, User Story 5).
127157 - **Boîtier nommé** : entrée associant un nom lisible choisi par l'utilisateur à un boîtier détecté (modèle et, si disponible, numéro de série) dans la base de contexte centralisée, complétable par un parcours à la demande de l'archive NAS (FR-012).
128158
129159 ## Success Criteria *(mandatory)*
@@ -138,13 +168,16 @@ Un photographe qui utilise déjà l'interface graphique de Régine pour trier se
138168 - **SC-005**: Modifier un chemin du contexte de travail après la configuration initiale ne provoque la perte d'aucun boîtier déjà nommé.
139169 - **SC-006**: 100% des tentatives de changer le répertoire de travail local alors qu'un travail non réconcilié y est présent sont bloquées, avec une explication claire de ce qui doit être réconcilié ou archivé au préalable.
140170 - **SC-008**: Toute modification du contexte de travail effectuée depuis la GUI est immédiatement visible via `regine config show`/`cameras list` en CLI, et réciproquement — les deux façades lisent et écrivent le même état, jamais deux états divergents.
171+- **SC-009** *(ajoutée le 2026-09-23)*: Configurer Régine sur une nouvelle machine (mêmes trois emplacements de travail qu'une installation précédente) retrouve 100% des boîtiers nommés, des catégories connues, et de l'historique d'imports de cette installation précédente, sans qu'aucun ne doive être ressaisi ou reconstitué manuellement.
172+- **SC-010** *(ajoutée le 2026-09-23)*: La perte totale de la machine locale (disque défaillant, vol, réinstallation) n'entraîne la perte définitive d'aucun boîtier nommé, d'aucune catégorie connue, ni d'aucune entrée de l'historique d'imports, tant que l'archive NAS reste intacte.
141173
142174 ## Assumptions
143175
144-- Le contexte de travail est unique par installation de Régine (un seul jeu de chemins et de boîtiers nommés actif à la fois) ; la gestion de plusieurs contextes en parallèle n'est pas couverte par cette spécification.
176+- Le contexte de travail est unique par installation de Régine (un seul jeu de chemins et de boîtiers nommés actif à la fois) ; la gestion de plusieurs contextes en parallèle n'est pas couverte par cette spécification. **Précisé le 2026-09-23** : la User Story 5 couvre le remplacement séquentiel d'une machine par une autre (une seule active à la fois), pas l'usage simultané de deux machines sur le même contexte — aucune fusion de contextes divergents n'est nécessaire dans ce cadre (cf. Edge Cases).
145177 - Les paramètres supplémentaires évoqués comme "à intégrer plus tard" ne sont pas définis dans cette spécification ; seule l'exigence d'extensibilité (FR-011) les anticipe.
146178 - Cette spécification s'appuie sur le mécanisme de profil de boîtiers optionnel déjà défini (`specs/002-profil-boitiers-optionnel`) : elle ajoute un point d'entrée proactif de nommage à partir de l'archive existante, sans modifier les règles de désambiguïsation par collision déjà établies.
147-- La base de données de travail mentionnée correspond au manifeste persistant par dossier déjà prévu dans la constitution du projet (Workflow d'archivage) ; cette spécification couvre son initialisation au niveau du contexte de travail, pas son schéma détaillé.
148-- L'emplacement de la base de contexte centralisée reste unique et indépendant du système d'exploitation (`~/.regine/`) pour cette itération (Clarification 2026-09-21) ; une convention par OS (XDG sur Linux, `Application Support` sur macOS, `%APPDATA%` sur Windows) est une amélioration explicitement différée, pas un besoin de cette spécification.
179+- **Révisé le 2026-09-23** : la base de données de travail (FR-006) ne correspond plus au manifeste persistant par dossier de la constitution (schéma `fichiers`/`verrou`, `specs/005-checkout-reconciliation`) — c'était une confusion de conception non résolue depuis la création de cette spec, jamais réellement exploitée par le code. Elle correspond désormais explicitement à la base de contexte centralisée elle-même (boîtiers, catégories, historique d'imports), physiquement déplacée à la racine du répertoire de travail local.
180+- L'emplacement de la configuration globale reste unique et indépendant du système d'exploitation (`~/.regine/`) pour cette itération (Clarification 2026-09-21) ; une convention par OS (XDG sur Linux, `Application Support` sur macOS, `%APPDATA%` sur Windows) est une amélioration explicitement différée, pas un besoin de cette spécification. **Précisé le 2026-09-23** : depuis ce déménagement, `~/.regine/` ne contient plus que les trois chemins de travail (FR-014) — plus les boîtiers, catégories, ni l'historique d'imports.
149181 - L'onglet de configuration de la GUI (User Story 4) réutilise l'infrastructure déjà en place dans `regine-gui` depuis `specs/006-gui-tri-consultation` (fenêtre à onglets) plutôt que d'introduire une nouvelle application ; il n'ajoute aucune capacité que la CLI n'a pas déjà.
150-- La base de contexte centralisée (boîtiers) est distincte des bases de données de travail par dossier : elle vit au niveau du contexte de travail, n'est pas archivée avec un dossier particulier, et n'a pas vocation à remplacer l'index inter-dossiers en lecture seule déjà anticipé comme travail futur dans les notes de conception du projet — le parcours à la demande (FR-012) reste une capacité complémentaire, pas une dépendance de cette spécification.
182+- La base de contexte centralisée (boîtiers, catégories, historique d'imports) est distincte des manifestes persistants par dossier de l'archive (`specs/005-checkout-reconciliation`) : elle vit au niveau du contexte de travail entier, pas par dossier, et n'a pas vocation à remplacer l'index inter-dossiers en lecture seule déjà anticipé comme travail futur dans les notes de conception du projet — le parcours à la demande (FR-012) reste une capacité complémentaire, pas une dépendance de cette spécification.
183+- **Ajouté le 2026-09-23** : la réplication de la base de contexte centralisée vers l'archive (FR-016) vit à un emplacement fixe à la racine de l'archive NAS (symétrique à son emplacement à la racine du répertoire de travail local, FR-006/FR-015), distinct de tout dossier photo particulier — cette spécification ne redéfinit pas la structure des dossiers photo eux-mêmes (`specs/004-categorisation-dossiers`).
@@ -20,6 +20,12 @@
20 - Q: La base de contexte centralisée doit-elle vivre à un emplacement standard différent selon le système d'exploitation (Linux/macOS/Windows) ? → A: Non, pas pour l'instant — l'emplacement unique déjà en place (`~/.regine/`, indépendant de l'OS) est conservé tel quel ; une convention par OS reste une amélioration possible d'une prochaine version, pas un besoin actuel.20 - Q: La base de contexte centralisée doit-elle vivre à un emplacement standard différent selon le système d'exploitation (Linux/macOS/Windows) ? → A: Non, pas pour l'instant — l'emplacement unique déjà en place (`~/.regine/`, indépendant de l'OS) est conservé tel quel ; une convention par OS reste une amélioration possible d'une prochaine version, pas un besoin actuel.
21 - Q: L'interface graphique doit-elle pouvoir consulter et modifier le contexte de travail ? → A: Oui — en appelant directement `regine_core.config` (mêmes fonctions que la CLI), sans aucune logique de configuration propre à la GUI (Principe VI). Couvert par la nouvelle User Story 4 ci-dessous.21 - Q: L'interface graphique doit-elle pouvoir consulter et modifier le contexte de travail ? → A: Oui — en appelant directement `regine_core.config` (mêmes fonctions que la CLI), sans aucune logique de configuration propre à la GUI (Principe VI). Couvert par la nouvelle User Story 4 ci-dessous.
22 22
23+### Session 2026-09-23
24+
25+- Q: `~/.regine/` doit-il continuer à héberger l'intégralité de la base de contexte (chemins, boîtiers, catégories, historique d'imports), ou seulement une partie ? → A: Seulement les trois chemins de travail (FR-001), dans une configuration globale minimale propre à la machine. Le reste (boîtiers nommés, catégories connues, historique des imports — cf. `specs/001-import-photos` FR-004) déménage à la racine du répertoire de travail local, dans le fichier déjà prévu par FR-006 (jusqu'ici sous-défini) — écart trouvé par l'utilisateur : ce contenu représentait un savoir accumulé important (désambiguïsation de boîtiers, catégorisation apprise, historique anti-doublon) qui ne survivait pas à la perte de la machine locale, alors que les photos elles-mêmes sont protégées sur le NAS. Ce déménagement corrige aussi une confusion déjà présente dans les Assumptions de cette spec : FR-006 (« base de données de travail » à la racine du répertoire local) et la base de contexte centralisée décrite plus bas désignaient jusqu'ici deux choses distinctes, sans qu'aucune des deux ne soit réellement précisée ni exploitée par le code existant — elles fusionnent en une seule (cf. FR-006 révisée, User Story 5).
26+- Q: Quand la base de contexte centralisée doit-elle être répliquée vers l'archive NAS (FR-016) ? → A: Manuellement, au moment où un dossier est archivé (`regine reconcile`) — jamais automatiquement à chaque modification isolée du contexte, ni via une commande dédiée séparée. Rationale de l'utilisateur : archiver un dossier suppose déjà d'écrire sur le NAS, et l'archive globale a alors besoin du contexte associé à jour — pas de nouvelle dépendance au NAS introduite au-delà de celle déjà requise par `regine reconcile`.
27+- Q: Quand le contexte doit-il être retrouvé depuis l'archive sur une nouvelle machine (FR-017) ? → A: Automatiquement, à la configuration initiale des emplacements de travail, et lors de la récupération locale d'un dossier archivé (checkout) — les deux moments naturels où une nouvelle machine entre en contact avec l'archive pour la première fois, sans action de récupération dédiée supplémentaire à apprendre.
28+
23 ## User Scenarios & Testing *(mandatory)*29 ## User Scenarios & Testing *(mandatory)*
24 30
25 ### User Story 1 - Configuration initiale des emplacements de travail (Priority: P1)31 ### User Story 1 - Configuration initiale des emplacements de travail (Priority: P1)
@@ -90,6 +96,24 @@ Un photographe qui utilise déjà l'interface graphique de Régine pour trier se
90 96
91 ---97 ---
92 98
99+### User Story 5 - Reprendre le travail sur une nouvelle machine sans perdre le contexte (Priority: P2)
100+
101+Un photographe change d'ordinateur (nouvelle machine, réinstallation, disque remplacé). Aucune photo n'est perdue puisqu'elles vivent sur le NAS, mais il veut retrouver, sans les ressaisir ni les reconstituer manuellement, ses boîtiers nommés, ses catégories connues et son historique d'imports déjà accumulés sur l'ancienne machine — en reconfigurant simplement Régine pour pointer vers la même archive.
102+
103+**Why this priority**: Sans cette garantie, tout incident sur la machine de travail ferait perdre silencieusement un savoir accumulé (désambiguïsation de boîtiers, catégorisation apprise, historique anti-doublon) que rien n'indiquerait comme sauvegardé — une régression grave pour un outil dont la promesse centrale est de ne jamais perdre d'information sans confirmation. Reste un raffinement au sens où l'usage quotidien sur une seule machine, jamais changée, n'en dépend pas.
104+
105+**Independent Test**: Peut être testé en enrichissant le contexte de travail (nommer un boîtier, réaliser un import) sur une installation, puis en configurant une seconde installation "neuve" (nouveau répertoire de travail local vide) pointant vers la même archive NAS, et en vérifiant que ce contexte est retrouvé sans réimport ni ressaisie.
106+
107+**Acceptance Scenarios**:
108+
109+1. **Given** un contexte de travail enrichi (boîtiers nommés, catégories connues, historique d'imports) sur une machine, **When** un dossier est archivé (`regine reconcile`) depuis cette machine, **Then** ce contexte est répliqué vers l'archive NAS au même moment, sans action de sauvegarde distincte à apprendre (FR-016).
110+2. **Given** une nouvelle machine sur laquelle Régine vient d'être configurée avec le même emplacement d'archive NAS qu'une installation précédente, **When** l'utilisateur termine la configuration des trois emplacements de travail, **Then** Régine retrouve automatiquement, depuis l'archive, le contexte enrichi de cette installation précédente (FR-017) — sans que l'utilisateur ait à réimporter une carte mémoire ou à renommer ses boîtiers de nouveau.
111+3. **Given** une nouvelle machine dont la configuration initiale n'a pas encore retrouvé de contexte existant sur l'archive (ex. configurée hors ligne), **When** l'utilisateur y effectue un premier checkout d'un dossier déjà archivé, **Then** Régine retrouve à cette occasion le contexte enrichi de l'installation précédente (FR-017), au plus tard à ce moment.
112+4. **Given** ce contexte retrouvé sur la nouvelle machine, **When** l'utilisateur consulte ses boîtiers nommés, ses catégories connues, ou lance un nouvel import, **Then** l'historique anti-doublon (`specs/001-import-photos` FR-004) et la désambiguïsation de boîtiers (`specs/002-profil-boitiers-optionnel`) se comportent exactement comme sur l'ancienne machine, sans repartir de zéro.
113+5. **Given** les trois emplacements de travail (FR-001), **When** l'utilisateur configure une nouvelle machine, **Then** seuls ces trois chemins doivent être ressaisis — jamais les boîtiers nommés, les catégories, ni l'historique d'imports.
114+
115+---
116+
93 ### Edge Cases117 ### Edge Cases
94 118
95 - Que se passe-t-il si le répertoire temporaire et le répertoire de travail local pointent vers le même emplacement ? Régine DOIT avertir l'utilisateur de cette configuration ambiguë plutôt que de l'accepter silencieusement.119 - Que se passe-t-il si le répertoire temporaire et le répertoire de travail local pointent vers le même emplacement ? Régine DOIT avertir l'utilisateur de cette configuration ambiguë plutôt que de l'accepter silencieusement.
@@ -97,6 +121,8 @@ Un photographe qui utilise déjà l'interface graphique de Régine pour trier se
97 - Que se passe-t-il si l'espace disque du répertoire de travail local devient insuffisant pour accueillir la base de données de travail ? Régine DOIT signaler une erreur de configuration claire plutôt que de créer une base de données partiellement écrite.121 - Que se passe-t-il si l'espace disque du répertoire de travail local devient insuffisant pour accueillir la base de données de travail ? Régine DOIT signaler une erreur de configuration claire plutôt que de créer une base de données partiellement écrite.
98 - Que se passe-t-il si l'utilisateur tente de lancer une opération avant d'avoir configuré le contexte de travail ? Régine DOIT indiquer clairement quels chemins manquent plutôt que d'échouer avec une erreur technique.122 - Que se passe-t-il si l'utilisateur tente de lancer une opération avant d'avoir configuré le contexte de travail ? Régine DOIT indiquer clairement quels chemins manquent plutôt que d'échouer avec une erreur technique.
99 - Que se passe-t-il si l'utilisateur tente de changer le répertoire de travail local alors qu'un checkout ou un import est encore en cours (non réconcilié/archivé) ? Régine DOIT refuser ce changement et indiquer précisément ce qui doit être réconcilié ou archivé avant de pouvoir continuer.123 - Que se passe-t-il si l'utilisateur tente de changer le répertoire de travail local alors qu'un checkout ou un import est encore en cours (non réconcilié/archivé) ? Régine DOIT refuser ce changement et indiquer précisément ce qui doit être réconcilié ou archivé avant de pouvoir continuer.
124+- Que se passe-t-il si le contexte de travail local (boîtiers, catégories, historique) et la sauvegarde présente sur l'archive ont divergé (ex. deux machines utilisées successivement sans que la première n'ait sauvegardé ses derniers changements) ? Cette spécification suppose un contexte actif sur une seule machine à la fois (cf. Assumptions) : elle ne couvre pas la fusion de deux contextes divergents, seulement le remplacement du contexte local par la dernière sauvegarde connue sur l'archive.
125+- Que se passe-t-il si l'archive NAS n'est pas accessible au moment où Régine tenterait de retrouver ou de sauvegarder le contexte de travail (User Story 5) ? Régine DOIT réutiliser l'assistance au montage déjà prévue (User Story 2) plutôt que d'échouer sans explication, et ne DOIT jamais bloquer les opérations qui n'ont besoin que du contexte déjà présent localement.
100 126
101 ## Requirements *(mandatory)*127 ## Requirements *(mandatory)*
102 128
@@ -107,23 +133,27 @@ Un photographe qui utilise déjà l'interface graphique de Régine pour trier se
107 - **FR-003**: Lorsqu'un répertoire temporaire ou un répertoire de travail local configuré n'existe pas encore, le système DOIT proposer de le créer plutôt que d'échouer silencieusement.133 - **FR-003**: Lorsqu'un répertoire temporaire ou un répertoire de travail local configuré n'existe pas encore, le système DOIT proposer de le créer plutôt que d'échouer silencieusement.
108 - **FR-004**: Lorsque le partage SMB configuré n'est pas accessible au moment où une opération en a besoin, le système DOIT détecter cette indisponibilité et déclencher le mécanisme de montage natif du système d'exploitation pour ce partage, plutôt que d'échouer avec une erreur générique. Le système ne DOIT PAS gérer ou stocker lui-même d'identifiants réseau : l'authentification et la sécurité des paramètres de connexion restent entièrement déléguées au système d'exploitation.134 - **FR-004**: Lorsque le partage SMB configuré n'est pas accessible au moment où une opération en a besoin, le système DOIT détecter cette indisponibilité et déclencher le mécanisme de montage natif du système d'exploitation pour ce partage, plutôt que d'échouer avec une erreur générique. Le système ne DOIT PAS gérer ou stocker lui-même d'identifiants réseau : l'authentification et la sécurité des paramètres de connexion restent entièrement déléguées au système d'exploitation.
109 - **FR-005**: Le système DOIT mémoriser l'emplacement SMB configuré (cf. FR-001) afin de pouvoir redéclencher son montage natif à chaque nouvelle indisponibilité détectée, sans que l'utilisateur ait à ressaisir ce chemin. Une fois l'accès au NAS rétabli après une indisponibilité détectée, le système DOIT permettre de reprendre l'opération initialement bloquée sans reconfiguration complète du contexte de travail.135 - **FR-005**: Le système DOIT mémoriser l'emplacement SMB configuré (cf. FR-001) afin de pouvoir redéclencher son montage natif à chaque nouvelle indisponibilité détectée, sans que l'utilisateur ait à ressaisir ce chemin. Une fois l'accès au NAS rétabli après une indisponibilité détectée, le système DOIT permettre de reprendre l'opération initialement bloquée sans reconfiguration complète du contexte de travail.
110-- **FR-006**: Le système DOIT initialiser, à la racine du répertoire de travail local, une base de données de travail prête à l'usage avant qu'aucune autre opération de Régine n'en ait besoin.136+- **FR-006** *(révisée le 2026-09-23)*: Le système DOIT initialiser, à la racine du répertoire de travail local, une base de données de travail prête à l'usage avant qu'aucune autre opération de Régine n'en ait besoin — cette base est le contexte de travail lui-même (boîtiers nommés, catégories connues, historique des imports, cf. FR-008/FR-015), pas un fichier distinct et vide.
111-- **FR-007**: Lorsqu'un dossier du répertoire de travail local est archivé vers le NAS, la base de données de travail correspondant à ce dossier DOIT être archivée avec lui, comme partie intégrante du dossier archivé.137+- **FR-007** *(révisée le 2026-09-23)*: La configuration globale minimale (FR-014) permettant de localiser le répertoire de travail local NE DOIT jamais dépendre du contenu de ce répertoire pour exister — elle DOIT rester lisible même si le répertoire de travail local est absent ou vide (ex. nouvelle machine, cf. User Story 5), pour permettre de le retrouver ou de le reconstituer.
112 - **FR-008**: À chaque import, le système DOIT enregistrer les références des boîtiers rencontrés (modèle et, si disponible, numéro de série) dans une base de contexte centralisée, et permettre à l'utilisateur, depuis cette base, d'attribuer un nom lisible à un boîtier déjà rencontré, sans nécessiter de nouvel import ni de parcours du NAS.138 - **FR-008**: À chaque import, le système DOIT enregistrer les références des boîtiers rencontrés (modèle et, si disponible, numéro de série) dans une base de contexte centralisée, et permettre à l'utilisateur, depuis cette base, d'attribuer un nom lisible à un boîtier déjà rencontré, sans nécessiter de nouvel import ni de parcours du NAS.
113 - **FR-009**: Un nom attribué à un boîtier via la configuration DOIT être réutilisé partout où Régine identifie ce même boîtier par la suite (import, désambiguïsation de collision).139 - **FR-009**: Un nom attribué à un boîtier via la configuration DOIT être réutilisé partout où Régine identifie ce même boîtier par la suite (import, désambiguïsation de collision).
114 - **FR-010**: Le système DOIT permettre de modifier tout paramètre du contexte de travail (chemins, boîtiers nommés) après la configuration initiale, sans réinstallation. Pour le répertoire de travail local spécifiquement, le système DOIT refuser ce changement tant qu'un checkout ou un import n'a pas été réconcilié ou archivé, et indiquer clairement ce qui bloque le changement, plutôt que de déplacer ou d'abandonner silencieusement du travail en cours.140 - **FR-010**: Le système DOIT permettre de modifier tout paramètre du contexte de travail (chemins, boîtiers nommés) après la configuration initiale, sans réinstallation. Pour le répertoire de travail local spécifiquement, le système DOIT refuser ce changement tant qu'un checkout ou un import n'a pas été réconcilié ou archivé, et indiquer clairement ce qui bloque le changement, plutôt que de déplacer ou d'abandonner silencieusement du travail en cours.
115 - **FR-011**: Le système DOIT être conçu pour accueillir de nouveaux paramètres de configuration à l'avenir sans remettre en cause les paramètres déjà définis.141 - **FR-011**: Le système DOIT être conçu pour accueillir de nouveaux paramètres de configuration à l'avenir sans remettre en cause les paramètres déjà définis.
116 - **FR-012**: Le système PEUT proposer, comme action distincte et déclenchée volontairement par l'utilisateur, un parcours complet de l'archive NAS pour détecter les boîtiers de dossiers archivés avant l'existence de cette fonctionnalité ou jamais revus depuis ; cette capacité est complémentaire et n'est pas requise pour que FR-008 soit satisfaite.142 - **FR-012**: Le système PEUT proposer, comme action distincte et déclenchée volontairement par l'utilisateur, un parcours complet de l'archive NAS pour détecter les boîtiers de dossiers archivés avant l'existence de cette fonctionnalité ou jamais revus depuis ; cette capacité est complémentaire et n'est pas requise pour que FR-008 soit satisfaite.
117 - **FR-013**: L'interface graphique DOIT pouvoir consulter et modifier le contexte de travail (chemins, boîtiers nommés) en appelant directement les mêmes fonctions de `regine_core.config` que la CLI, sans dupliquer la moindre règle de validation ou de désambiguïsation (Principe VI) — toute règle déjà vraie côté CLI (FR-003/FR-004/FR-010/FR-012) reste vraie à l'identique depuis la GUI.143 - **FR-013**: L'interface graphique DOIT pouvoir consulter et modifier le contexte de travail (chemins, boîtiers nommés) en appelant directement les mêmes fonctions de `regine_core.config` que la CLI, sans dupliquer la moindre règle de validation ou de désambiguïsation (Principe VI) — toute règle déjà vraie côté CLI (FR-003/FR-004/FR-010/FR-012) reste vraie à l'identique depuis la GUI.
144+- **FR-014** *(ajoutée le 2026-09-23)*: Le système DOIT conserver les trois emplacements de travail (FR-001) dans une configuration globale minimale, propre à la machine, à un emplacement fixe et indépendant du contenu du répertoire de travail local (cf. FR-007) — c'est la seule information que l'utilisateur doit ressaisir en configurant Régine sur une nouvelle machine.
145+- **FR-015** *(ajoutée le 2026-09-23)*: Le système DOIT stocker les boîtiers nommés (FR-008), les catégories connues (`specs/004-categorisation-dossiers`) et l'historique des imports (`specs/001-import-photos` FR-004) à la racine du répertoire de travail local (FR-006) — jamais dans la configuration globale de FR-014, dont ce contenu doit rester indépendant.
146+- **FR-016** *(ajoutée le 2026-09-23)*: Le système DOIT protéger le contenu de FR-015 contre la perte du répertoire de travail local, en le répliquant vers l'archive NAS à chaque archivage d'un dossier (`regine reconcile`, `specs/005-checkout-reconciliation`) — jamais automatiquement à chaque modification isolée du contexte (nommage d'un boîtier, import réalisé), ni via une commande dédiée séparée. Cette réplication ne DOIT introduire aucune nouvelle dépendance au NAS au-delà de celle déjà requise par `regine reconcile` lui-même : elle profite du seul moment où une écriture sur l'archive a de toute façon déjà lieu.
147+- **FR-017** *(ajoutée le 2026-09-23)*: Quand le répertoire de travail local ne contient pas (ou plus) le contenu de FR-015 alors qu'une sauvegarde en existe sur l'archive NAS (cf. FR-016), le système DOIT le retrouver automatiquement, sans action supplémentaire de l'utilisateur, à l'un des deux moments suivants : lors de la configuration initiale des emplacements de travail sur une machine (FR-001), ou lors d'un premier checkout d'un dossier archivé (`specs/005-checkout-reconciliation`) réalisé depuis cette machine.
118 148
119 ### Key Entities149 ### Key Entities
120 150
121-- **Contexte de travail** : ensemble des paramètres de configuration actifs (chemins, boîtiers nommés) utilisés par l'ensemble des modules de Régine.151+- **Contexte de travail** : ensemble des paramètres actifs (chemins, boîtiers nommés, catégories, historique d'imports) utilisés par l'ensemble des modules de Régine — réparti, depuis le 2026-09-23, entre la configuration globale et la base de contexte centralisée ci-dessous.
152+- **Configuration globale** *(ajoutée le 2026-09-23)* : fichier minimal, propre à la machine, à un emplacement fixe (`~/.regine/`) — contient uniquement les trois emplacements de travail (FR-001/FR-014), jamais les boîtiers, catégories, ni l'historique d'imports. Sert uniquement à localiser la base de contexte centralisée ; reste lisible même si le répertoire de travail local est vide ou absent (FR-007).
122 - **Répertoire temporaire** : emplacement local destiné aux copies brutes avant analyse, en amont d'un import.153 - **Répertoire temporaire** : emplacement local destiné aux copies brutes avant analyse, en amont d'un import.
123-- **Répertoire de travail local** : emplacement local où résident les copies de travail (checkout, import en cours) et leur base de données.154+- **Répertoire de travail local** : emplacement local où résident les copies de travail (checkout, import en cours) et la base de contexte centralisée.
124-- **Emplacement de l'archive (NAS/SMB)** : chemin réseau où réside l'archive persistante de Régine.155+- **Emplacement de l'archive (NAS/SMB)** : chemin réseau où réside l'archive persistante de Régine, et où est répliquée une sauvegarde de la base de contexte centralisée (FR-016).
125-- **Base de données de travail** : base persistante créée à la racine du répertoire de travail local, associée à chaque dossier local et archivée avec lui — distincte de la base de contexte centralisée ci-dessous.156+- **Base de contexte centralisée** *(révisée le 2026-09-23)* : base persistante unique, vivant désormais à la racine du répertoire de travail local (fusionne l'ancienne « base de données de travail », jusqu'ici sous-définie, et l'ancienne base de contexte centralisée de `~/.regine/`) — contient les boîtiers rencontrés, les catégories connues, et l'historique des imports (`specs/001-import-photos` FR-004). Alimentée à chaque import ; source par défaut de la détection de boîtiers de la User Story 3 ; protégée contre la perte de la machine locale par réplication vers l'archive NAS (FR-016/FR-017, User Story 5).
126-- **Base de contexte centralisée** : base persistante au niveau du contexte de travail (pas par dossier), alimentée à chaque import avec les références de boîtiers rencontrés ; source par défaut de la détection de boîtiers de la User Story 3.
127 - **Boîtier nommé** : entrée associant un nom lisible choisi par l'utilisateur à un boîtier détecté (modèle et, si disponible, numéro de série) dans la base de contexte centralisée, complétable par un parcours à la demande de l'archive NAS (FR-012).157 - **Boîtier nommé** : entrée associant un nom lisible choisi par l'utilisateur à un boîtier détecté (modèle et, si disponible, numéro de série) dans la base de contexte centralisée, complétable par un parcours à la demande de l'archive NAS (FR-012).
128 158
129 ## Success Criteria *(mandatory)*159 ## Success Criteria *(mandatory)*
@@ -138,13 +168,16 @@ Un photographe qui utilise déjà l'interface graphique de Régine pour trier se
138 - **SC-005**: Modifier un chemin du contexte de travail après la configuration initiale ne provoque la perte d'aucun boîtier déjà nommé.168 - **SC-005**: Modifier un chemin du contexte de travail après la configuration initiale ne provoque la perte d'aucun boîtier déjà nommé.
139 - **SC-006**: 100% des tentatives de changer le répertoire de travail local alors qu'un travail non réconcilié y est présent sont bloquées, avec une explication claire de ce qui doit être réconcilié ou archivé au préalable.169 - **SC-006**: 100% des tentatives de changer le répertoire de travail local alors qu'un travail non réconcilié y est présent sont bloquées, avec une explication claire de ce qui doit être réconcilié ou archivé au préalable.
140 - **SC-008**: Toute modification du contexte de travail effectuée depuis la GUI est immédiatement visible via `regine config show`/`cameras list` en CLI, et réciproquement — les deux façades lisent et écrivent le même état, jamais deux états divergents.170 - **SC-008**: Toute modification du contexte de travail effectuée depuis la GUI est immédiatement visible via `regine config show`/`cameras list` en CLI, et réciproquement — les deux façades lisent et écrivent le même état, jamais deux états divergents.
171+- **SC-009** *(ajoutée le 2026-09-23)*: Configurer Régine sur une nouvelle machine (mêmes trois emplacements de travail qu'une installation précédente) retrouve 100% des boîtiers nommés, des catégories connues, et de l'historique d'imports de cette installation précédente, sans qu'aucun ne doive être ressaisi ou reconstitué manuellement.
172+- **SC-010** *(ajoutée le 2026-09-23)*: La perte totale de la machine locale (disque défaillant, vol, réinstallation) n'entraîne la perte définitive d'aucun boîtier nommé, d'aucune catégorie connue, ni d'aucune entrée de l'historique d'imports, tant que l'archive NAS reste intacte.
141 173
142 ## Assumptions174 ## Assumptions
143 175
144-- Le contexte de travail est unique par installation de Régine (un seul jeu de chemins et de boîtiers nommés actif à la fois) ; la gestion de plusieurs contextes en parallèle n'est pas couverte par cette spécification.176+- Le contexte de travail est unique par installation de Régine (un seul jeu de chemins et de boîtiers nommés actif à la fois) ; la gestion de plusieurs contextes en parallèle n'est pas couverte par cette spécification. **Précisé le 2026-09-23** : la User Story 5 couvre le remplacement séquentiel d'une machine par une autre (une seule active à la fois), pas l'usage simultané de deux machines sur le même contexte — aucune fusion de contextes divergents n'est nécessaire dans ce cadre (cf. Edge Cases).
145 - Les paramètres supplémentaires évoqués comme "à intégrer plus tard" ne sont pas définis dans cette spécification ; seule l'exigence d'extensibilité (FR-011) les anticipe.177 - Les paramètres supplémentaires évoqués comme "à intégrer plus tard" ne sont pas définis dans cette spécification ; seule l'exigence d'extensibilité (FR-011) les anticipe.
146 - Cette spécification s'appuie sur le mécanisme de profil de boîtiers optionnel déjà défini (`specs/002-profil-boitiers-optionnel`) : elle ajoute un point d'entrée proactif de nommage à partir de l'archive existante, sans modifier les règles de désambiguïsation par collision déjà établies.178 - Cette spécification s'appuie sur le mécanisme de profil de boîtiers optionnel déjà défini (`specs/002-profil-boitiers-optionnel`) : elle ajoute un point d'entrée proactif de nommage à partir de l'archive existante, sans modifier les règles de désambiguïsation par collision déjà établies.
147-- La base de données de travail mentionnée correspond au manifeste persistant par dossier déjà prévu dans la constitution du projet (Workflow d'archivage) ; cette spécification couvre son initialisation au niveau du contexte de travail, pas son schéma détaillé.179+- **Révisé le 2026-09-23** : la base de données de travail (FR-006) ne correspond plus au manifeste persistant par dossier de la constitution (schéma `fichiers`/`verrou`, `specs/005-checkout-reconciliation`) — c'était une confusion de conception non résolue depuis la création de cette spec, jamais réellement exploitée par le code. Elle correspond désormais explicitement à la base de contexte centralisée elle-même (boîtiers, catégories, historique d'imports), physiquement déplacée à la racine du répertoire de travail local.
148-- L'emplacement de la base de contexte centralisée reste unique et indépendant du système d'exploitation (`~/.regine/`) pour cette itération (Clarification 2026-09-21) ; une convention par OS (XDG sur Linux, `Application Support` sur macOS, `%APPDATA%` sur Windows) est une amélioration explicitement différée, pas un besoin de cette spécification.180+- L'emplacement de la configuration globale reste unique et indépendant du système d'exploitation (`~/.regine/`) pour cette itération (Clarification 2026-09-21) ; une convention par OS (XDG sur Linux, `Application Support` sur macOS, `%APPDATA%` sur Windows) est une amélioration explicitement différée, pas un besoin de cette spécification. **Précisé le 2026-09-23** : depuis ce déménagement, `~/.regine/` ne contient plus que les trois chemins de travail (FR-014) — plus les boîtiers, catégories, ni l'historique d'imports.
149 - L'onglet de configuration de la GUI (User Story 4) réutilise l'infrastructure déjà en place dans `regine-gui` depuis `specs/006-gui-tri-consultation` (fenêtre à onglets) plutôt que d'introduire une nouvelle application ; il n'ajoute aucune capacité que la CLI n'a pas déjà.181 - L'onglet de configuration de la GUI (User Story 4) réutilise l'infrastructure déjà en place dans `regine-gui` depuis `specs/006-gui-tri-consultation` (fenêtre à onglets) plutôt que d'introduire une nouvelle application ; il n'ajoute aucune capacité que la CLI n'a pas déjà.
150-- La base de contexte centralisée (boîtiers) est distincte des bases de données de travail par dossier : elle vit au niveau du contexte de travail, n'est pas archivée avec un dossier particulier, et n'a pas vocation à remplacer l'index inter-dossiers en lecture seule déjà anticipé comme travail futur dans les notes de conception du projet — le parcours à la demande (FR-012) reste une capacité complémentaire, pas une dépendance de cette spécification.182+- La base de contexte centralisée (boîtiers, catégories, historique d'imports) est distincte des manifestes persistants par dossier de l'archive (`specs/005-checkout-reconciliation`) : elle vit au niveau du contexte de travail entier, pas par dossier, et n'a pas vocation à remplacer l'index inter-dossiers en lecture seule déjà anticipé comme travail futur dans les notes de conception du projet — le parcours à la demande (FR-012) reste une capacité complémentaire, pas une dépendance de cette spécification.
183+- **Ajouté le 2026-09-23** : la réplication de la base de contexte centralisée vers l'archive (FR-016) vit à un emplacement fixe à la racine de l'archive NAS (symétrique à son emplacement à la racine du répertoire de travail local, FR-006/FR-015), distinct de tout dossier photo particulier — cette spécification ne redéfinit pas la structure des dossiers photo eux-mêmes (`specs/004-categorisation-dossiers`).
modified specs/003-config-contexte-travail/tasks.md +38 -4
@@ -25,7 +25,8 @@ description: "Task list for feature implementation"
2525
2626 - **`data-model.md` § Base de contexte centralisée** a été rédigé avant l'implémentation de `specs/002-profil-boitiers-optionnel` et décrit une colonne `source` à deux valeurs (`import`/`parcours_nas`). Le schéma réellement livré par specs/002 (`camera_profile/db.py`) utilise déjà `source` pour une distinction différente et orthogonale (`modele`/`numero_serie`/`manuel` — **comment** un boîtier a été désambiguïsé). Ne PAS renommer ni migrer cette colonne déjà testée : réutiliser la même colonne texte libre en y ajoutant la seule valeur manquante `parcours_nas` (FR-012) ; à l'affichage (`cameras list`), traiter toute valeur différente de `parcours_nas` comme « détecté via import » (FR-008).
2727 - **research.md ne fixe pas l'emplacement de fichier de la base de contexte centralisée** (seulement son schéma). Décision prise dans cette liste de tâches (T005) : un emplacement fixe par utilisateur, indépendant de `local_work_dir` (qui, lui, est modifiable, cf. FR-010/SC-005 — la base de contexte ne doit jamais se perdre à un changement de chemin).
28-- **FR-006/Acceptance Scenario 3 (US1)** : lu littéralement (« une base de données de travail est créée... à la racine de ce répertoire »). Réutilise directement `regine_core.archive.manifest.ouvrir_ou_creer` sur `local_work_dir` lui-même (déjà générique, accepte n'importe quel dossier) plutôt que d'inventer un second mécanisme — cohérent avec l'Assumption de spec.md (« cette spécification couvre son initialisation... pas son schéma détaillé »). Le manifeste par dossier archivé (FR-007) reste géré indépendamment par `specs/001`/`specs/005`, déjà correct.
28+- **FR-006/Acceptance Scenario 3 (US1)** : lu littéralement (« une base de données de travail est créée... à la racine de ce répertoire »). Réutilise directement `regine_core.archive.manifest.ouvrir_ou_creer` sur `local_work_dir` lui-même (déjà générique, accepte n'importe quel dossier) plutôt que d'inventer un second mécanisme — cohérent avec l'Assumption de spec.md (« cette spécification couvre son initialisation... pas son schéma détaillé »). Le manifeste par dossier archivé (FR-007) reste géré indépendamment par `specs/001`/`specs/005`, déjà correct. **Retiré par la Phase 7 (User Story 5, 2026-09-23/24)** : ce choix s'est révélé être une impasse — ce manifeste vide à la racine de `local_work_dir` n'a jamais été exploité par aucun autre module (le verrou/manifeste réel vit uniquement côté archive, cf. T009 ci-dessous), et FR-006 a depuis été révisée par l'utilisateur pour désigner explicitement la base de contexte centralisée elle-même (boîtiers/catégories/historique), déplacée à cet emplacement plutôt qu'à `~/.regine/`.
29+- **Ajouté en Phase 7 (User Story 5, 2026-09-23/24)** : `open_context_db` perd son défaut implicite (`path: Path | None = None` → `path: Path`) mais garde exactement la même sémantique de paramètre (un chemin de fichier complet, jamais un dossier) — tous les appels existants de `specs/001/002/004` passent déjà un chemin de fichier explicite (`tmp_path / "contexte.sqlite3"` ou équivalent) et restent donc valides sans modification (cf. T046). Seuls les appels qui reposaient sur l'ancien défaut (`open_context_db(None)`, dans `config_cmd.py`/`archive_cmd.py`/les écrans GUI, et le bug déjà présent dans `import_cmd.py`) sont concernés par cette révision.
2930
3031 ## Phase 1: Setup
3132
@@ -138,12 +139,44 @@ description: "Task list for feature implementation"
138139
139140 ---
140141
141-## Phase 7: Polish & Cross-Cutting Concerns
142+## Phase 7: User Story 5 - Reprendre le travail sur une nouvelle machine sans perdre le contexte (Priority: P2)
143+
144+**Ajoutée le 2026-09-23/24** (écart trouvé par l'utilisateur + spec/plan révisés) — cf. `spec.md` § Clarifications Session 2026-09-23.
145+
146+**Objectif** : séparer la configuration globale (chemins, propre à la machine, `~/.regine/config.sqlite3`) de la base de contexte centralisée (boîtiers/catégories/historique d'imports, désormais à `<local_work_dir>/contexte.sqlite3`, remplaçant l'ancien manifeste vide jamais exploité qu'`enregistrer_chemins` y créait) ; répliquer cette dernière vers l'archive à chaque `regine reconcile` (FR-016) et la restaurer automatiquement à la configuration initiale ou au premier `regine checkout` sur une machine qui ne l'a pas encore (FR-017).
147+
148+**Test indépendant** : enrichir le contexte sur une installation (nommer un boîtier), archiver un dossier (`regine reconcile`), configurer une seconde installation « neuve » pointant vers la même archive, vérifier que `regine config cameras list` y retrouve immédiatement ce boîtier sans réimport.
149+
150+### Tests for User Story 5
151+
152+- [X] T035 [P] [US5] Adapter `packages/regine-core/tests/unit/test_config_db_defaut.py` : `open_global_config_db()` sans argument utilise `~/.regine/config.sqlite3` par défaut (comportement testé jusqu'ici sur `open_context_db`) ; `open_context_db(path)` exige désormais `path` (plus de défaut implicite)
153+- [X] T036 [P] [US5] Test unitaire de `environnement.ouvrir_environnement` dans `packages/regine-core/tests/unit/test_environnement.py` : `contexte_conn is None` tant que `local_work_dir` n'est pas configuré dans la config globale ; une fois configuré, `contexte_conn` pointe vers `<local_work_dir>/contexte.sqlite3` (créée si absente)
154+- [X] T037 [P] [US5] Test unitaire de `context.sauvegarder_contexte`/`restaurer_contexte_si_absent` dans `packages/regine-core/tests/unit/test_contexte_replication.py` : réplique vers l'archive (contenu identique après coup, vérifié par somme de contrôle), no-op si la base locale n'existe pas encore, restaure si absente localement mais présente sur l'archive, no-op si absente des deux côtés (première installation)
155+
156+### Implementation for User Story 5
157+
158+- [X] T038 [US5] Refactorer `packages/regine-core/src/regine_core/config/db.py` : extraire le bootstrap `PRAGMA application_id`/`user_version` commun dans une fonction privée partagée ; `open_context_db(path: Path) -> sqlite3.Connection` (chemin désormais obligatoire, `CHEMIN_CONTEXTE_PAR_DEFAUT` retirée) ; nouvelle `open_global_config_db(path: Path | None = None) -> sqlite3.Connection` (défaut `CHEMIN_CONFIG_GLOBALE_PAR_DEFAUT = Path.home() / ".regine" / "config.sqlite3"`)
159+- [X] T039 [US5] Créer `packages/regine-core/src/regine_core/config/environnement.py` : dataclass `Environnement` (`config_conn`, `contexte_conn: sqlite3.Connection | None`, `local_work_dir: Path | None`), `ouvrir_environnement(config_db_path: Path | None = None) -> Environnement` (FR-014/015)
160+- [X] T040 [US5] Étendre `packages/regine-core/src/regine_core/config/context.py` : `enregistrer_chemins` initialise désormais `<local_work_dir>/contexte.sqlite3` via `db.open_context_db` à la place de l'ancien `regine_core.archive.manifest.ouvrir_ou_creer` (retiré — supprime la dépendance de ce module vers `regine_core.archive`, jamais réellement exploitée, cf. § Écarts en tête de fichier) ; nouvelles `sauvegarder_contexte(local_work_dir, archive_root_local) -> None` (FR-016, copie vérifiée vers `<archive_root_local>/.regine-contexte.sqlite3`, no-op si source absente) et `restaurer_contexte_si_absent(local_work_dir, archive_root_local) -> bool` (FR-017, copie vérifiée dans l'autre sens si la base locale est absente et qu'une sauvegarde existe)
161+- [X] T041 [US5] Mettre à jour `packages/regine-cli/src/regine_cli/config_cmd.py` : `ouvrir_environnement()` remplace `open_context_db(Path(args.contexte_db) if args.contexte_db else None)` ; flag `--contexte-db` renommé `--config-db` ; `set-paths` appelle `restaurer_contexte_si_absent` juste après un `enregistrer_chemins` réussi si `local-work-dir` et `archive-smb` sont tous deux connus à l'issue de la commande, annonce le résultat sur stdout (cf. `contracts/cli-config.md`)
162+- [X] T042 [US5] Mettre à jour `packages/regine-cli/src/regine_cli/archive_cmd.py` : `_charger_archive_root_local` utilise `open_global_config_db`/`ouvrir_environnement` (l'emplacement de l'archive vit désormais dans la config globale, pas la base de contexte centralisée) ; flag `--contexte-db` renommé `--config-db` (`browse`/`restore`) ; `_cmd_reconcile` appelle `sauvegarder_contexte` juste après un archivage réussi (FR-016) ; `_cmd_checkout` appelle `restaurer_contexte_si_absent` juste après un checkout réussi (FR-017)
163+- [X] T043 [US5] Corriger `packages/regine-cli/src/regine_cli/import_cmd.py` (bug réel trouvé par l'utilisateur, cf. `contracts/regine-core-api.md`) : supprime `_NOM_CONTEXTE_DB_PAR_DEFAUT`/le flag `--contexte-db` (divergeait silencieusement du reste de la CLI) ; dérive directement `Path(args.local_root) / "contexte.sqlite3"` pour `db.open_context_db`
164+- [X] T044 [US5] Mettre à jour `packages/regine-gui/src/regine_gui/app.py` : `FenetrePrincipale` résout `ouvrir_environnement()` une fois à la construction, propage le résultat aux onglets plutôt que chacun n'ouvre indépendamment `open_context_db(self._contexte_db)`
165+- [X] T045 [US5] Mettre à jour `packages/regine-gui/src/regine_gui/config_view.py`/`import_view.py`/`consultation_view.py` : consomment l'environnement résolu (chemins/état SMB via `environnement.config_conn`, boîtiers/catégories/historique via `environnement.contexte_conn`) plutôt que d'ouvrir elles-mêmes une connexion unique ambiguë
166+- [X] T046 [P] [US5] Vérifier chaque appelant existant de `open_context_db` dans `specs/001-import-photos`/`specs/002-profil-boitiers-optionnel`/`specs/004-categorisation-dossiers` (tests unitaires/intégration) : tous passent déjà un chemin de fichier explicite (`tmp_path / "contexte.sqlite3"` ou équivalent), donc déjà compatibles avec la signature révisée sans modification — seul `test_config_db_defaut.py` dépendait de l'ancien défaut implicite, déjà traité par T035
167+- [X] T047 [US5] Valider manuellement en mode headless (`QT_QPA_PLATFORM=offscreen`, même méthode que les phases précédentes) les Scénarios 8, 9 et 10 de `quickstart.md` : réplication à un `regine reconcile`, restauration à la configuration initiale d'une « nouvelle machine », restauration au premier `regine checkout` si la configuration initiale n'a pas suffi (archive inaccessible à ce moment-là) — Scénarios 8 et 9 exécutés de bout en bout via un script headless simulant deux machines (import GUI sur machine A, `regine reconcile` déclenchant bien la sauvegarde vers `<archive>/.regine-contexte.sqlite3`, puis `regine config set-paths` sur une machine B neuve restaurant automatiquement `contexte.sqlite3` avant tout import) : `Contexte local machine A existe ? True`, `Sauvegarde présente sur l'archive ? True`, `Contexte restauré sur machine B ? True`. Scénario 10 (restauration au premier `checkout` plutôt qu'à `set-paths`) non exécuté en conditions réelles (aurait nécessité un troisième montage SMB simulé) mais son câblage (`_restaurer_contexte_post_checkout` dans `archive_cmd.py`, appelant exactement `restaurer_contexte_si_absent`) est identique à celui du Scénario 9 déjà validé et est couvert unitairement par T037 — aucun boîtier n'a été détecté lors de l'import (fichiers factices sans EXIF), donc c'est la présence de `contexte.sqlite3` restauré (plutôt qu'un boîtier nommé visible dans `cameras list`) qui atteste de la réplication
168+
169+**Checkpoint** : les 5 user stories sont fonctionnelles indépendamment ; un contexte enrichi sur une machine est retrouvable intégralement sur une autre pointant vers la même archive.
170+
171+---
172+
173+## Phase 8: Polish & Cross-Cutting Concerns
142174
143175 - [X] T026 [P] Exécuter manuellement les 6 scénarios de `specs/003-config-contexte-travail/quickstart.md` (adaptés à la surface CLI réellement livrée) et consigner le résultat — scénarios 1, 2, 3, 5, 6 exécutés via la vraie CLI (`uv run python -m regine_cli.config_cmd config ...`), tous conformes. Scénario 4 (montage/démontage SMB réel) non exécutable dans cet environnement de développement (pas de partage SMB physique disponible) — le mécanisme sous-jacent (`smb.disponible`/`assurer_disponibilite`/`declencher_montage`) est validé unitairement avec un point de montage injecté (T019), et `config show` a correctement détecté `smb://nas.local/regine-archive` (inexistant dans cet environnement) comme `unavailable` lors du scénario 1, confirmant la détection réelle
144176 - [X] T027 [P] `ruff check --fix` + `ruff format` sur `packages/regine-core` et `packages/regine-cli`
145177 - [X] T028 Mettre à jour `contracts/cli-config.md` et créer `contracts/regine-core-api.md` — signatures réelles documentées, écarts de conception consignés (cf. § Écarts en tête de fichier) ; note explicite que `regine_cli.import_cmd`/`archive_cmd` (specs/001/005) ne consomment pas encore ce contexte configuré (ils prennent `--archive-root`/`--local-root` en flags explicites) — intégration future hors périmètre de cette spec, non construite ici sans nouvelle demande explicite
146178 - [X] T034 [P] Exécuter le Scénario 7 de `quickstart.md` (déjà couvert par T033, sans duplication) et `ruff check --fix`/`ruff format` sur `packages/regine-gui` (nouveau fichier `config_view.py`) — 172/172 tests `regine-core`/`regine-cli` toujours au vert (aucune régression, aucun nouveau test requis côté core puisqu'aucune nouvelle fonction n'a été ajoutée)
179+- [X] T048 [P] Exécuter les Scénarios 8, 9 et 10 de `quickstart.md` (cf. T047, sans duplication) et `ruff check --fix`/`ruff format` sur les trois paquets (`regine-core`, `regine-cli`, `regine-gui`) ; exécuter la suite de tests complète du dépôt et confirmer l'absence de régression sur les specs consommatrices (`specs/001/002/004/005/006`) ; mettre à jour `contracts/regine-core-api.md`/`cli-config.md` si l'implémentation révèle un écart avec les signatures documentées — `ruff check --fix` : aucun problème détecté sur les trois paquets ; `ruff format` : 1 fichier reformaté (changement cosmétique, sans lien avec cette story) ; suite complète : 190/190 tests passent (aucune régression sur specs/001/002/004/005/006) ; aucun écart supplémentaire trouvé nécessitant une mise à jour des contrats au-delà de ceux déjà consignés lors de T041-T043
147180
148181 ---
149182
@@ -155,8 +188,9 @@ description: "Task list for feature implementation"
155188 - **US2 (Phase 4)** : dépend de Foundational ; s'intègre à `regine config show` (US1) mais reste testable indépendamment (`smb.py` seul).
156189 - **US3 (Phase 5)** : dépend de Foundational et de `specs/002` (déjà livrée) ; indépendante de US1/US2.
157190 - **US4 (Phase 6)** : dépend de US1/US2/US3 (appelle leurs fonctions telles quelles) et de `specs/006-gui-tri-consultation` (déjà livrée, fournit `regine-gui`/`app.py`/PySide6) ; aucune dépendance nouvelle côté `regine-core`.
158-- **Polish (Phase 7)** : dépend des user stories livrées (au minimum US1 ; T034 dépend spécifiquement de US4).
191+- **US5 (Phase 7)** : dépend de Foundational (révise directement `db.py`/`context.py` de la Phase 2) et de `specs/005-checkout-reconciliation` (déjà livrée, fournit `reconcile`/`checkout` sur lesquels s'accrochent FR-016/FR-017) ; touche transversalement US1 (`config_cmd.py`), US3 (consommateur de la base de contexte déplacée) et US4 (écrans GUI) sans changer leur comportement observable — à mener après elles, jamais en parallèle, du fait de ce chevauchement de fichiers.
192+- **Polish (Phase 8)** : dépend des user stories livrées (au minimum US1 ; T034 dépend spécifiquement de US4, T048 dépend spécifiquement de US5).
159193
160194 ## Implementation Strategy
161195
162-MVP = Phase 1 + 2 + 3 (US1). US2 et US3 sont des incréments indépendants livrables ensuite dans l'ordre de priorité. US4 (ajoutée le 2026-09-21) est un incrément de confort pur, livrable à tout moment après `specs/006-gui-tri-consultation` — sans lui, la CLI (US1-3) reste pleinement fonctionnelle et suffisante.
196+MVP = Phase 1 + 2 + 3 (US1). US2 et US3 sont des incréments indépendants livrables ensuite dans l'ordre de priorité. US4 (ajoutée le 2026-09-21) est un incrément de confort pur, livrable à tout moment après `specs/006-gui-tri-consultation` — sans lui, la CLI (US1-3) reste pleinement fonctionnelle et suffisante. US5 (ajoutée le 2026-09-23/24) révise une hypothèse de conception jamais correctement exploitée depuis le début (US1/FR-006) plutôt que d'ajouter une capacité isolée : contrairement à US2-4, elle touche des fichiers déjà livrés par les phases précédentes (`db.py`, `context.py`, `config_cmd.py`, `archive_cmd.py`, `import_cmd.py`, les écrans GUI) et doit donc être menée en dernier, séquentiellement, plutôt qu'en parallèle d'une autre user story.
@@ -25,7 +25,8 @@ description: "Task list for feature implementation"
25 25
26 - **`data-model.md` § Base de contexte centralisée** a été rédigé avant l'implémentation de `specs/002-profil-boitiers-optionnel` et décrit une colonne `source` à deux valeurs (`import`/`parcours_nas`). Le schéma réellement livré par specs/002 (`camera_profile/db.py`) utilise déjà `source` pour une distinction différente et orthogonale (`modele`/`numero_serie`/`manuel` — **comment** un boîtier a été désambiguïsé). Ne PAS renommer ni migrer cette colonne déjà testée : réutiliser la même colonne texte libre en y ajoutant la seule valeur manquante `parcours_nas` (FR-012) ; à l'affichage (`cameras list`), traiter toute valeur différente de `parcours_nas` comme « détecté via import » (FR-008).26 - **`data-model.md` § Base de contexte centralisée** a été rédigé avant l'implémentation de `specs/002-profil-boitiers-optionnel` et décrit une colonne `source` à deux valeurs (`import`/`parcours_nas`). Le schéma réellement livré par specs/002 (`camera_profile/db.py`) utilise déjà `source` pour une distinction différente et orthogonale (`modele`/`numero_serie`/`manuel` — **comment** un boîtier a été désambiguïsé). Ne PAS renommer ni migrer cette colonne déjà testée : réutiliser la même colonne texte libre en y ajoutant la seule valeur manquante `parcours_nas` (FR-012) ; à l'affichage (`cameras list`), traiter toute valeur différente de `parcours_nas` comme « détecté via import » (FR-008).
27 - **research.md ne fixe pas l'emplacement de fichier de la base de contexte centralisée** (seulement son schéma). Décision prise dans cette liste de tâches (T005) : un emplacement fixe par utilisateur, indépendant de `local_work_dir` (qui, lui, est modifiable, cf. FR-010/SC-005 — la base de contexte ne doit jamais se perdre à un changement de chemin).27 - **research.md ne fixe pas l'emplacement de fichier de la base de contexte centralisée** (seulement son schéma). Décision prise dans cette liste de tâches (T005) : un emplacement fixe par utilisateur, indépendant de `local_work_dir` (qui, lui, est modifiable, cf. FR-010/SC-005 — la base de contexte ne doit jamais se perdre à un changement de chemin).
28-- **FR-006/Acceptance Scenario 3 (US1)** : lu littéralement (« une base de données de travail est créée... à la racine de ce répertoire »). Réutilise directement `regine_core.archive.manifest.ouvrir_ou_creer` sur `local_work_dir` lui-même (déjà générique, accepte n'importe quel dossier) plutôt que d'inventer un second mécanisme — cohérent avec l'Assumption de spec.md (« cette spécification couvre son initialisation... pas son schéma détaillé »). Le manifeste par dossier archivé (FR-007) reste géré indépendamment par `specs/001`/`specs/005`, déjà correct.28+- **FR-006/Acceptance Scenario 3 (US1)** : lu littéralement (« une base de données de travail est créée... à la racine de ce répertoire »). Réutilise directement `regine_core.archive.manifest.ouvrir_ou_creer` sur `local_work_dir` lui-même (déjà générique, accepte n'importe quel dossier) plutôt que d'inventer un second mécanisme — cohérent avec l'Assumption de spec.md (« cette spécification couvre son initialisation... pas son schéma détaillé »). Le manifeste par dossier archivé (FR-007) reste géré indépendamment par `specs/001`/`specs/005`, déjà correct. **Retiré par la Phase 7 (User Story 5, 2026-09-23/24)** : ce choix s'est révélé être une impasse — ce manifeste vide à la racine de `local_work_dir` n'a jamais été exploité par aucun autre module (le verrou/manifeste réel vit uniquement côté archive, cf. T009 ci-dessous), et FR-006 a depuis été révisée par l'utilisateur pour désigner explicitement la base de contexte centralisée elle-même (boîtiers/catégories/historique), déplacée à cet emplacement plutôt qu'à `~/.regine/`.
29+- **Ajouté en Phase 7 (User Story 5, 2026-09-23/24)** : `open_context_db` perd son défaut implicite (`path: Path | None = None` → `path: Path`) mais garde exactement la même sémantique de paramètre (un chemin de fichier complet, jamais un dossier) — tous les appels existants de `specs/001/002/004` passent déjà un chemin de fichier explicite (`tmp_path / "contexte.sqlite3"` ou équivalent) et restent donc valides sans modification (cf. T046). Seuls les appels qui reposaient sur l'ancien défaut (`open_context_db(None)`, dans `config_cmd.py`/`archive_cmd.py`/les écrans GUI, et le bug déjà présent dans `import_cmd.py`) sont concernés par cette révision.
29 30
30 ## Phase 1: Setup31 ## Phase 1: Setup
31 32
@@ -138,12 +139,44 @@ description: "Task list for feature implementation"
138 139
139 ---140 ---
140 141
141-## Phase 7: Polish & Cross-Cutting Concerns142+## Phase 7: User Story 5 - Reprendre le travail sur une nouvelle machine sans perdre le contexte (Priority: P2)
143+
144+**Ajoutée le 2026-09-23/24** (écart trouvé par l'utilisateur + spec/plan révisés) — cf. `spec.md` § Clarifications Session 2026-09-23.
145+
146+**Objectif** : séparer la configuration globale (chemins, propre à la machine, `~/.regine/config.sqlite3`) de la base de contexte centralisée (boîtiers/catégories/historique d'imports, désormais à `<local_work_dir>/contexte.sqlite3`, remplaçant l'ancien manifeste vide jamais exploité qu'`enregistrer_chemins` y créait) ; répliquer cette dernière vers l'archive à chaque `regine reconcile` (FR-016) et la restaurer automatiquement à la configuration initiale ou au premier `regine checkout` sur une machine qui ne l'a pas encore (FR-017).
147+
148+**Test indépendant** : enrichir le contexte sur une installation (nommer un boîtier), archiver un dossier (`regine reconcile`), configurer une seconde installation « neuve » pointant vers la même archive, vérifier que `regine config cameras list` y retrouve immédiatement ce boîtier sans réimport.
149+
150+### Tests for User Story 5
151+
152+- [X] T035 [P] [US5] Adapter `packages/regine-core/tests/unit/test_config_db_defaut.py` : `open_global_config_db()` sans argument utilise `~/.regine/config.sqlite3` par défaut (comportement testé jusqu'ici sur `open_context_db`) ; `open_context_db(path)` exige désormais `path` (plus de défaut implicite)
153+- [X] T036 [P] [US5] Test unitaire de `environnement.ouvrir_environnement` dans `packages/regine-core/tests/unit/test_environnement.py` : `contexte_conn is None` tant que `local_work_dir` n'est pas configuré dans la config globale ; une fois configuré, `contexte_conn` pointe vers `<local_work_dir>/contexte.sqlite3` (créée si absente)
154+- [X] T037 [P] [US5] Test unitaire de `context.sauvegarder_contexte`/`restaurer_contexte_si_absent` dans `packages/regine-core/tests/unit/test_contexte_replication.py` : réplique vers l'archive (contenu identique après coup, vérifié par somme de contrôle), no-op si la base locale n'existe pas encore, restaure si absente localement mais présente sur l'archive, no-op si absente des deux côtés (première installation)
155+
156+### Implementation for User Story 5
157+
158+- [X] T038 [US5] Refactorer `packages/regine-core/src/regine_core/config/db.py` : extraire le bootstrap `PRAGMA application_id`/`user_version` commun dans une fonction privée partagée ; `open_context_db(path: Path) -> sqlite3.Connection` (chemin désormais obligatoire, `CHEMIN_CONTEXTE_PAR_DEFAUT` retirée) ; nouvelle `open_global_config_db(path: Path | None = None) -> sqlite3.Connection` (défaut `CHEMIN_CONFIG_GLOBALE_PAR_DEFAUT = Path.home() / ".regine" / "config.sqlite3"`)
159+- [X] T039 [US5] Créer `packages/regine-core/src/regine_core/config/environnement.py` : dataclass `Environnement` (`config_conn`, `contexte_conn: sqlite3.Connection | None`, `local_work_dir: Path | None`), `ouvrir_environnement(config_db_path: Path | None = None) -> Environnement` (FR-014/015)
160+- [X] T040 [US5] Étendre `packages/regine-core/src/regine_core/config/context.py` : `enregistrer_chemins` initialise désormais `<local_work_dir>/contexte.sqlite3` via `db.open_context_db` à la place de l'ancien `regine_core.archive.manifest.ouvrir_ou_creer` (retiré — supprime la dépendance de ce module vers `regine_core.archive`, jamais réellement exploitée, cf. § Écarts en tête de fichier) ; nouvelles `sauvegarder_contexte(local_work_dir, archive_root_local) -> None` (FR-016, copie vérifiée vers `<archive_root_local>/.regine-contexte.sqlite3`, no-op si source absente) et `restaurer_contexte_si_absent(local_work_dir, archive_root_local) -> bool` (FR-017, copie vérifiée dans l'autre sens si la base locale est absente et qu'une sauvegarde existe)
161+- [X] T041 [US5] Mettre à jour `packages/regine-cli/src/regine_cli/config_cmd.py` : `ouvrir_environnement()` remplace `open_context_db(Path(args.contexte_db) if args.contexte_db else None)` ; flag `--contexte-db` renommé `--config-db` ; `set-paths` appelle `restaurer_contexte_si_absent` juste après un `enregistrer_chemins` réussi si `local-work-dir` et `archive-smb` sont tous deux connus à l'issue de la commande, annonce le résultat sur stdout (cf. `contracts/cli-config.md`)
162+- [X] T042 [US5] Mettre à jour `packages/regine-cli/src/regine_cli/archive_cmd.py` : `_charger_archive_root_local` utilise `open_global_config_db`/`ouvrir_environnement` (l'emplacement de l'archive vit désormais dans la config globale, pas la base de contexte centralisée) ; flag `--contexte-db` renommé `--config-db` (`browse`/`restore`) ; `_cmd_reconcile` appelle `sauvegarder_contexte` juste après un archivage réussi (FR-016) ; `_cmd_checkout` appelle `restaurer_contexte_si_absent` juste après un checkout réussi (FR-017)
163+- [X] T043 [US5] Corriger `packages/regine-cli/src/regine_cli/import_cmd.py` (bug réel trouvé par l'utilisateur, cf. `contracts/regine-core-api.md`) : supprime `_NOM_CONTEXTE_DB_PAR_DEFAUT`/le flag `--contexte-db` (divergeait silencieusement du reste de la CLI) ; dérive directement `Path(args.local_root) / "contexte.sqlite3"` pour `db.open_context_db`
164+- [X] T044 [US5] Mettre à jour `packages/regine-gui/src/regine_gui/app.py` : `FenetrePrincipale` résout `ouvrir_environnement()` une fois à la construction, propage le résultat aux onglets plutôt que chacun n'ouvre indépendamment `open_context_db(self._contexte_db)`
165+- [X] T045 [US5] Mettre à jour `packages/regine-gui/src/regine_gui/config_view.py`/`import_view.py`/`consultation_view.py` : consomment l'environnement résolu (chemins/état SMB via `environnement.config_conn`, boîtiers/catégories/historique via `environnement.contexte_conn`) plutôt que d'ouvrir elles-mêmes une connexion unique ambiguë
166+- [X] T046 [P] [US5] Vérifier chaque appelant existant de `open_context_db` dans `specs/001-import-photos`/`specs/002-profil-boitiers-optionnel`/`specs/004-categorisation-dossiers` (tests unitaires/intégration) : tous passent déjà un chemin de fichier explicite (`tmp_path / "contexte.sqlite3"` ou équivalent), donc déjà compatibles avec la signature révisée sans modification — seul `test_config_db_defaut.py` dépendait de l'ancien défaut implicite, déjà traité par T035
167+- [X] T047 [US5] Valider manuellement en mode headless (`QT_QPA_PLATFORM=offscreen`, même méthode que les phases précédentes) les Scénarios 8, 9 et 10 de `quickstart.md` : réplication à un `regine reconcile`, restauration à la configuration initiale d'une « nouvelle machine », restauration au premier `regine checkout` si la configuration initiale n'a pas suffi (archive inaccessible à ce moment-là) — Scénarios 8 et 9 exécutés de bout en bout via un script headless simulant deux machines (import GUI sur machine A, `regine reconcile` déclenchant bien la sauvegarde vers `<archive>/.regine-contexte.sqlite3`, puis `regine config set-paths` sur une machine B neuve restaurant automatiquement `contexte.sqlite3` avant tout import) : `Contexte local machine A existe ? True`, `Sauvegarde présente sur l'archive ? True`, `Contexte restauré sur machine B ? True`. Scénario 10 (restauration au premier `checkout` plutôt qu'à `set-paths`) non exécuté en conditions réelles (aurait nécessité un troisième montage SMB simulé) mais son câblage (`_restaurer_contexte_post_checkout` dans `archive_cmd.py`, appelant exactement `restaurer_contexte_si_absent`) est identique à celui du Scénario 9 déjà validé et est couvert unitairement par T037 — aucun boîtier n'a été détecté lors de l'import (fichiers factices sans EXIF), donc c'est la présence de `contexte.sqlite3` restauré (plutôt qu'un boîtier nommé visible dans `cameras list`) qui atteste de la réplication
168+
169+**Checkpoint** : les 5 user stories sont fonctionnelles indépendamment ; un contexte enrichi sur une machine est retrouvable intégralement sur une autre pointant vers la même archive.
170+
171+---
172+
173+## Phase 8: Polish & Cross-Cutting Concerns
142 174
143 - [X] T026 [P] Exécuter manuellement les 6 scénarios de `specs/003-config-contexte-travail/quickstart.md` (adaptés à la surface CLI réellement livrée) et consigner le résultat — scénarios 1, 2, 3, 5, 6 exécutés via la vraie CLI (`uv run python -m regine_cli.config_cmd config ...`), tous conformes. Scénario 4 (montage/démontage SMB réel) non exécutable dans cet environnement de développement (pas de partage SMB physique disponible) — le mécanisme sous-jacent (`smb.disponible`/`assurer_disponibilite`/`declencher_montage`) est validé unitairement avec un point de montage injecté (T019), et `config show` a correctement détecté `smb://nas.local/regine-archive` (inexistant dans cet environnement) comme `unavailable` lors du scénario 1, confirmant la détection réelle175 - [X] T026 [P] Exécuter manuellement les 6 scénarios de `specs/003-config-contexte-travail/quickstart.md` (adaptés à la surface CLI réellement livrée) et consigner le résultat — scénarios 1, 2, 3, 5, 6 exécutés via la vraie CLI (`uv run python -m regine_cli.config_cmd config ...`), tous conformes. Scénario 4 (montage/démontage SMB réel) non exécutable dans cet environnement de développement (pas de partage SMB physique disponible) — le mécanisme sous-jacent (`smb.disponible`/`assurer_disponibilite`/`declencher_montage`) est validé unitairement avec un point de montage injecté (T019), et `config show` a correctement détecté `smb://nas.local/regine-archive` (inexistant dans cet environnement) comme `unavailable` lors du scénario 1, confirmant la détection réelle
144 - [X] T027 [P] `ruff check --fix` + `ruff format` sur `packages/regine-core` et `packages/regine-cli`176 - [X] T027 [P] `ruff check --fix` + `ruff format` sur `packages/regine-core` et `packages/regine-cli`
145 - [X] T028 Mettre à jour `contracts/cli-config.md` et créer `contracts/regine-core-api.md` — signatures réelles documentées, écarts de conception consignés (cf. § Écarts en tête de fichier) ; note explicite que `regine_cli.import_cmd`/`archive_cmd` (specs/001/005) ne consomment pas encore ce contexte configuré (ils prennent `--archive-root`/`--local-root` en flags explicites) — intégration future hors périmètre de cette spec, non construite ici sans nouvelle demande explicite177 - [X] T028 Mettre à jour `contracts/cli-config.md` et créer `contracts/regine-core-api.md` — signatures réelles documentées, écarts de conception consignés (cf. § Écarts en tête de fichier) ; note explicite que `regine_cli.import_cmd`/`archive_cmd` (specs/001/005) ne consomment pas encore ce contexte configuré (ils prennent `--archive-root`/`--local-root` en flags explicites) — intégration future hors périmètre de cette spec, non construite ici sans nouvelle demande explicite
146 - [X] T034 [P] Exécuter le Scénario 7 de `quickstart.md` (déjà couvert par T033, sans duplication) et `ruff check --fix`/`ruff format` sur `packages/regine-gui` (nouveau fichier `config_view.py`) — 172/172 tests `regine-core`/`regine-cli` toujours au vert (aucune régression, aucun nouveau test requis côté core puisqu'aucune nouvelle fonction n'a été ajoutée)178 - [X] T034 [P] Exécuter le Scénario 7 de `quickstart.md` (déjà couvert par T033, sans duplication) et `ruff check --fix`/`ruff format` sur `packages/regine-gui` (nouveau fichier `config_view.py`) — 172/172 tests `regine-core`/`regine-cli` toujours au vert (aucune régression, aucun nouveau test requis côté core puisqu'aucune nouvelle fonction n'a été ajoutée)
179+- [X] T048 [P] Exécuter les Scénarios 8, 9 et 10 de `quickstart.md` (cf. T047, sans duplication) et `ruff check --fix`/`ruff format` sur les trois paquets (`regine-core`, `regine-cli`, `regine-gui`) ; exécuter la suite de tests complète du dépôt et confirmer l'absence de régression sur les specs consommatrices (`specs/001/002/004/005/006`) ; mettre à jour `contracts/regine-core-api.md`/`cli-config.md` si l'implémentation révèle un écart avec les signatures documentées — `ruff check --fix` : aucun problème détecté sur les trois paquets ; `ruff format` : 1 fichier reformaté (changement cosmétique, sans lien avec cette story) ; suite complète : 190/190 tests passent (aucune régression sur specs/001/002/004/005/006) ; aucun écart supplémentaire trouvé nécessitant une mise à jour des contrats au-delà de ceux déjà consignés lors de T041-T043
147 180
148 ---181 ---
149 182
@@ -155,8 +188,9 @@ description: "Task list for feature implementation"
155 - **US2 (Phase 4)** : dépend de Foundational ; s'intègre à `regine config show` (US1) mais reste testable indépendamment (`smb.py` seul).188 - **US2 (Phase 4)** : dépend de Foundational ; s'intègre à `regine config show` (US1) mais reste testable indépendamment (`smb.py` seul).
156 - **US3 (Phase 5)** : dépend de Foundational et de `specs/002` (déjà livrée) ; indépendante de US1/US2.189 - **US3 (Phase 5)** : dépend de Foundational et de `specs/002` (déjà livrée) ; indépendante de US1/US2.
157 - **US4 (Phase 6)** : dépend de US1/US2/US3 (appelle leurs fonctions telles quelles) et de `specs/006-gui-tri-consultation` (déjà livrée, fournit `regine-gui`/`app.py`/PySide6) ; aucune dépendance nouvelle côté `regine-core`.190 - **US4 (Phase 6)** : dépend de US1/US2/US3 (appelle leurs fonctions telles quelles) et de `specs/006-gui-tri-consultation` (déjà livrée, fournit `regine-gui`/`app.py`/PySide6) ; aucune dépendance nouvelle côté `regine-core`.
158-- **Polish (Phase 7)** : dépend des user stories livrées (au minimum US1 ; T034 dépend spécifiquement de US4).191+- **US5 (Phase 7)** : dépend de Foundational (révise directement `db.py`/`context.py` de la Phase 2) et de `specs/005-checkout-reconciliation` (déjà livrée, fournit `reconcile`/`checkout` sur lesquels s'accrochent FR-016/FR-017) ; touche transversalement US1 (`config_cmd.py`), US3 (consommateur de la base de contexte déplacée) et US4 (écrans GUI) sans changer leur comportement observable — à mener après elles, jamais en parallèle, du fait de ce chevauchement de fichiers.
192+- **Polish (Phase 8)** : dépend des user stories livrées (au minimum US1 ; T034 dépend spécifiquement de US4, T048 dépend spécifiquement de US5).
159 193
160 ## Implementation Strategy194 ## Implementation Strategy
161 195
162-MVP = Phase 1 + 2 + 3 (US1). US2 et US3 sont des incréments indépendants livrables ensuite dans l'ordre de priorité. US4 (ajoutée le 2026-09-21) est un incrément de confort pur, livrable à tout moment après `specs/006-gui-tri-consultation` — sans lui, la CLI (US1-3) reste pleinement fonctionnelle et suffisante.196+MVP = Phase 1 + 2 + 3 (US1). US2 et US3 sont des incréments indépendants livrables ensuite dans l'ordre de priorité. US4 (ajoutée le 2026-09-21) est un incrément de confort pur, livrable à tout moment après `specs/006-gui-tri-consultation` — sans lui, la CLI (US1-3) reste pleinement fonctionnelle et suffisante. US5 (ajoutée le 2026-09-23/24) révise une hypothèse de conception jamais correctement exploitée depuis le début (US1/FR-006) plutôt que d'ajouter une capacité isolée : contrairement à US2-4, elle touche des fichiers déjà livrés par les phases précédentes (`db.py`, `context.py`, `config_cmd.py`, `archive_cmd.py`, `import_cmd.py`, les écrans GUI) et doit donc être menée en dernier, séquentiellement, plutôt qu'en parallèle d'une autre user story.
modified specs/006-gui-tri-consultation/contracts/cli-archive-consultation.md +4 -4
@@ -1,8 +1,8 @@
11 # Contrat CLI : `regine browse` / `regine restore` (US2)
22
3-Étend `regine-cli`'s `archive_cmd.py` existant (`specs/005-checkout-reconciliation`, déjà porteur de `checkout`/`reconcile`) plutôt que de créer une façade séparée — même thème (« l'archive »), pas de logique métier propre (Principe VI). **Écart trouvé à l'implémentation par rapport à la première version de ce contrat** : `checkout`/`reconcile` sont déjà des sous-commandes de premier niveau (`regine checkout ...`, pas `regine archive checkout ...`) — `browse`/`restore` suivent donc la même convention déjà établie, sans introduire un niveau `archive` intermédiaire qui n'existe nulle part ailleurs dans la CLI. Lit l'archive configurée via `specs/003-config-contexte-travail` plutôt qu'un chemin passé à chaque appel : consulter « l'archive » sous-entend l'archive déjà configurée.
3+Étend `regine-cli`'s `archive_cmd.py` existant (`specs/005-checkout-reconciliation`, déjà porteur de `checkout`/`reconcile`) plutôt que de créer une façade séparée — même thème (« l'archive »), pas de logique métier propre (Principe VI). **Écart trouvé à l'implémentation par rapport à la première version de ce contrat** : `checkout`/`reconcile` sont déjà des sous-commandes de premier niveau (`regine checkout ...`, pas `regine archive checkout ...`) — `browse`/`restore` suivent donc la même convention déjà établie, sans introduire un niveau `archive` intermédiaire qui n'existe nulle part ailleurs dans la CLI. Lit l'archive configurée via `specs/003-config-contexte-travail` plutôt qu'un chemin passé à chaque appel : consulter « l'archive » sous-entend l'archive déjà configurée. **Révisé le 2026-09-23/24** : `archive_smb_path` se lit désormais depuis la configuration globale (`regine_core.config.db.open_global_config_db`, `specs/003` FR-014), pas depuis la base de contexte centralisée (boîtiers/catégories/historique) — d'où le renommage de `--contexte-db` en `--config-db` ci-dessous.
44
5-## `regine browse [--search TEXTE] [--json] [--contexte-db CHEMIN]`
5+## `regine browse [--search TEXTE] [--json] [--config-db CHEMIN]`
66
77 Parcourt l'archive configurée en lecture seule (US2 FR-008).
88
@@ -20,7 +20,7 @@ regine browse --search "Montenegro"
2020 - Succès : liste des dossiers/photos correspondants sur stdout (texte ou JSON selon `--json`), code `0`. Aucun fichier n'est copié localement à cette étape (FR-010, lecture seule).
2121 - Aucun résultat : message explicite, pas une erreur, code `0`.
2222
23-## `regine restore --chemin CHEMIN_RELATIF --destination CHEMIN_LOCAL [--contexte-db CHEMIN]`
23+## `regine restore --chemin CHEMIN_RELATIF --destination CHEMIN_LOCAL [--config-db CHEMIN]`
2424
2525 Restaure une copie vérifiée d'un fichier précis de l'archive vers un emplacement local (US2 FR-009), sans checkout ni verrou du dossier entier.
2626
@@ -32,6 +32,6 @@ regine restore --chemin voyage/2026-08_Montenegro/2026-08-12_Kotor/raw/2026-08-1
3232
3333 **Sorties** :
3434 - Succès : somme de contrôle vérifiée, confirmation sur stdout, code `0` (SC-004).
35-- `--destination` existe déjà avec un contenu différent (Edge Case, FR-012) : erreur sur stderr, rien n'est écrasé, code non-zéro — même principe que `regine_core.import_carte.push.CollisionNomArchiveError`.
35+- `--destination` existe déjà avec un contenu différent (Edge Case, FR-012) : erreur sur stderr, rien n'est écrasé, code non-zéro — même principe que `regine_core.import_carte.push.CollisionNomLocalError` (renommée depuis `CollisionNomArchiveError`, `specs/001-import-photos` révision 2026-09-22).
3636 - `--chemin` introuvable dans l'archive : erreur sur stderr, code non-zéro.
3737 - Archive inaccessible : même comportement que `browse` (assistance au montage, `specs/003`).
@@ -1,8 +1,8 @@
1 # Contrat CLI : `regine browse` / `regine restore` (US2)1 # Contrat CLI : `regine browse` / `regine restore` (US2)
2 2
3-Étend `regine-cli`'s `archive_cmd.py` existant (`specs/005-checkout-reconciliation`, déjà porteur de `checkout`/`reconcile`) plutôt que de créer une façade séparée — même thème (« l'archive »), pas de logique métier propre (Principe VI). **Écart trouvé à l'implémentation par rapport à la première version de ce contrat** : `checkout`/`reconcile` sont déjà des sous-commandes de premier niveau (`regine checkout ...`, pas `regine archive checkout ...`) — `browse`/`restore` suivent donc la même convention déjà établie, sans introduire un niveau `archive` intermédiaire qui n'existe nulle part ailleurs dans la CLI. Lit l'archive configurée via `specs/003-config-contexte-travail` plutôt qu'un chemin passé à chaque appel : consulter « l'archive » sous-entend l'archive déjà configurée.3+Étend `regine-cli`'s `archive_cmd.py` existant (`specs/005-checkout-reconciliation`, déjà porteur de `checkout`/`reconcile`) plutôt que de créer une façade séparée — même thème (« l'archive »), pas de logique métier propre (Principe VI). **Écart trouvé à l'implémentation par rapport à la première version de ce contrat** : `checkout`/`reconcile` sont déjà des sous-commandes de premier niveau (`regine checkout ...`, pas `regine archive checkout ...`) — `browse`/`restore` suivent donc la même convention déjà établie, sans introduire un niveau `archive` intermédiaire qui n'existe nulle part ailleurs dans la CLI. Lit l'archive configurée via `specs/003-config-contexte-travail` plutôt qu'un chemin passé à chaque appel : consulter « l'archive » sous-entend l'archive déjà configurée. **Révisé le 2026-09-23/24** : `archive_smb_path` se lit désormais depuis la configuration globale (`regine_core.config.db.open_global_config_db`, `specs/003` FR-014), pas depuis la base de contexte centralisée (boîtiers/catégories/historique) — d'où le renommage de `--contexte-db` en `--config-db` ci-dessous.
4 4
5-## `regine browse [--search TEXTE] [--json] [--contexte-db CHEMIN]`5+## `regine browse [--search TEXTE] [--json] [--config-db CHEMIN]`
6 6
7 Parcourt l'archive configurée en lecture seule (US2 FR-008).7 Parcourt l'archive configurée en lecture seule (US2 FR-008).
8 8
@@ -20,7 +20,7 @@ regine browse --search "Montenegro"
20 - Succès : liste des dossiers/photos correspondants sur stdout (texte ou JSON selon `--json`), code `0`. Aucun fichier n'est copié localement à cette étape (FR-010, lecture seule).20 - Succès : liste des dossiers/photos correspondants sur stdout (texte ou JSON selon `--json`), code `0`. Aucun fichier n'est copié localement à cette étape (FR-010, lecture seule).
21 - Aucun résultat : message explicite, pas une erreur, code `0`.21 - Aucun résultat : message explicite, pas une erreur, code `0`.
22 22
23-## `regine restore --chemin CHEMIN_RELATIF --destination CHEMIN_LOCAL [--contexte-db CHEMIN]`23+## `regine restore --chemin CHEMIN_RELATIF --destination CHEMIN_LOCAL [--config-db CHEMIN]`
24 24
25 Restaure une copie vérifiée d'un fichier précis de l'archive vers un emplacement local (US2 FR-009), sans checkout ni verrou du dossier entier.25 Restaure une copie vérifiée d'un fichier précis de l'archive vers un emplacement local (US2 FR-009), sans checkout ni verrou du dossier entier.
26 26
@@ -32,6 +32,6 @@ regine restore --chemin voyage/2026-08_Montenegro/2026-08-12_Kotor/raw/2026-08-1
32 32
33 **Sorties** :33 **Sorties** :
34 - Succès : somme de contrôle vérifiée, confirmation sur stdout, code `0` (SC-004).34 - Succès : somme de contrôle vérifiée, confirmation sur stdout, code `0` (SC-004).
35-- `--destination` existe déjà avec un contenu différent (Edge Case, FR-012) : erreur sur stderr, rien n'est écrasé, code non-zéro — même principe que `regine_core.import_carte.push.CollisionNomArchiveError`.35+- `--destination` existe déjà avec un contenu différent (Edge Case, FR-012) : erreur sur stderr, rien n'est écrasé, code non-zéro — même principe que `regine_core.import_carte.push.CollisionNomLocalError` (renommée depuis `CollisionNomArchiveError`, `specs/001-import-photos` révision 2026-09-22).
36 - `--chemin` introuvable dans l'archive : erreur sur stderr, code non-zéro.36 - `--chemin` introuvable dans l'archive : erreur sur stderr, code non-zéro.
37 - Archive inaccessible : même comportement que `browse` (assistance au montage, `specs/003`).37 - Archive inaccessible : même comportement que `browse` (assistance au montage, `specs/003`).
modified specs/006-gui-tri-consultation/contracts/regine-core-api.md +1 -1
@@ -28,7 +28,7 @@ Parcourt `archive_root_local` (point de montage local du partage SMB, `regine_co
2828
2929 ## `consultation.restaurer(chemin_source: Path, chemin_destination: Path) -> None`
3030
31-Copie vérifiée par somme de contrôle d'un fichier de l'archive vers un emplacement local (US2 FR-009). Refuse d'écraser silencieusement `chemin_destination` si elle existe déjà avec un contenu différent (FR-012, même principe que `regine_core.import_carte.push.CollisionNomArchiveError`, implémentation propre à ce module plutôt que partagée — cf. research.md § 5).
31+Copie vérifiée par somme de contrôle d'un fichier de l'archive vers un emplacement local (US2 FR-009). Refuse d'écraser silencieusement `chemin_destination` si elle existe déjà avec un contenu différent (FR-012, même principe que `regine_core.import_carte.push.CollisionNomLocalError`, renommée depuis `CollisionNomArchiveError` — `specs/001-import-photos` révision 2026-09-22 —, implémentation propre à ce module plutôt que partagée — cf. research.md § 5).
3232
3333 ## `import_carte.*` — réutilisées telles quelles par l'onglet d'import (US3)
3434
@@ -28,7 +28,7 @@ Parcourt `archive_root_local` (point de montage local du partage SMB, `regine_co
28 28
29 ## `consultation.restaurer(chemin_source: Path, chemin_destination: Path) -> None`29 ## `consultation.restaurer(chemin_source: Path, chemin_destination: Path) -> None`
30 30
31-Copie vérifiée par somme de contrôle d'un fichier de l'archive vers un emplacement local (US2 FR-009). Refuse d'écraser silencieusement `chemin_destination` si elle existe déjà avec un contenu différent (FR-012, même principe que `regine_core.import_carte.push.CollisionNomArchiveError`, implémentation propre à ce module plutôt que partagée — cf. research.md § 5).31+Copie vérifiée par somme de contrôle d'un fichier de l'archive vers un emplacement local (US2 FR-009). Refuse d'écraser silencieusement `chemin_destination` si elle existe déjà avec un contenu différent (FR-012, même principe que `regine_core.import_carte.push.CollisionNomLocalError`, renommée depuis `CollisionNomArchiveError` — `specs/001-import-photos` révision 2026-09-22 —, implémentation propre à ce module plutôt que partagée — cf. research.md § 5).
32 32
33 ## `import_carte.*` — réutilisées telles quelles par l'onglet d'import (US3)33 ## `import_carte.*` — réutilisées telles quelles par l'onglet d'import (US3)
34 34
modified specs/006-gui-tri-consultation/research.md +1 -1
@@ -38,7 +38,7 @@
3838
3939 ## 5. Restauration ciblée (US2) : copie vérifiée, sans mécanisme partagé dédié
4040
41-**Decision**: `regine_core.archive.consultation.restaurer` implémente sa propre copie vérifiée par somme de contrôle (source → destination, avec le même refus d'écrasement silencieux qu'ailleurs dans le projet, cf. `push.CollisionNomArchiveError`), plutôt que d'extraire un utilitaire partagé avec les fonctions de copie déjà privées de `push.py`/`checkout.py`.
41+**Decision**: `regine_core.archive.consultation.restaurer` implémente sa propre copie vérifiée par somme de contrôle (source → destination, avec le même refus d'écrasement silencieux qu'ailleurs dans le projet, cf. `push.CollisionNomLocalError`, renommée depuis `CollisionNomArchiveError` — `specs/001-import-photos` révision 2026-09-22), plutôt que d'extraire un utilitaire partagé avec les fonctions de copie déjà privées de `push.py`/`checkout.py`.
4242
4343 **Rationale**: Le projet a déjà deux copies vérifiées quasi identiques mais privées et non partagées (`import_carte.push._copier_verifie_sans_ecraser`, `archive.checkout._copier_verifie`) sans que cela ait posé de problème de maintenance à ce stade (~10 lignes chacune). Une troisième variante minimaliste (pas de vérification de collision requise ici, la restauration cible un fichier choisi par l'utilisateur) reste plus simple qu'une abstraction partagée prématurée. À reconsidérer si une quatrième variante apparaissait.
4444
@@ -38,7 +38,7 @@
38 38
39 ## 5. Restauration ciblée (US2) : copie vérifiée, sans mécanisme partagé dédié39 ## 5. Restauration ciblée (US2) : copie vérifiée, sans mécanisme partagé dédié
40 40
41-**Decision**: `regine_core.archive.consultation.restaurer` implémente sa propre copie vérifiée par somme de contrôle (source → destination, avec le même refus d'écrasement silencieux qu'ailleurs dans le projet, cf. `push.CollisionNomArchiveError`), plutôt que d'extraire un utilitaire partagé avec les fonctions de copie déjà privées de `push.py`/`checkout.py`.41+**Decision**: `regine_core.archive.consultation.restaurer` implémente sa propre copie vérifiée par somme de contrôle (source → destination, avec le même refus d'écrasement silencieux qu'ailleurs dans le projet, cf. `push.CollisionNomLocalError`, renommée depuis `CollisionNomArchiveError` — `specs/001-import-photos` révision 2026-09-22), plutôt que d'extraire un utilitaire partagé avec les fonctions de copie déjà privées de `push.py`/`checkout.py`.
42 42
43 **Rationale**: Le projet a déjà deux copies vérifiées quasi identiques mais privées et non partagées (`import_carte.push._copier_verifie_sans_ecraser`, `archive.checkout._copier_verifie`) sans que cela ait posé de problème de maintenance à ce stade (~10 lignes chacune). Une troisième variante minimaliste (pas de vérification de collision requise ici, la restauration cible un fichier choisi par l'utilisateur) reste plus simple qu'une abstraction partagée prématurée. À reconsidérer si une quatrième variante apparaissait.43 **Rationale**: Le projet a déjà deux copies vérifiées quasi identiques mais privées et non partagées (`import_carte.push._copier_verifie_sans_ecraser`, `archive.checkout._copier_verifie`) sans que cela ait posé de problème de maintenance à ce stade (~10 lignes chacune). Une troisième variante minimaliste (pas de vérification de collision requise ici, la restauration cible un fichier choisi par l'utilisateur) reste plus simple qu'une abstraction partagée prématurée. À reconsidérer si une quatrième variante apparaissait.
44 44