refactor
19cf29c parent: 7f47101 modified
packages/regine-cli/src/regine_cli/archive_cmd.py +61 -5 | @@ -2,6 +2,14 @@ | ||
| 2 | 2 | |
| 3 | 3 | Façade fine : orchestre `regine_core.archive`, formatte le résultat, ne contient |
| 4 | 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 | 15 | from __future__ import annotations |
| @@ -17,6 +25,48 @@ from regine_core.archive.reconciliation import DecisionsUtilisateur, archiver, c | ||
| 17 | 25 | 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 | 70 | def _cmd_checkout(args: argparse.Namespace) -> int: |
| 21 | 71 | dossier_archive = Path(args.dossier) |
| 22 | 72 | 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 | 81 | print(f"{len(snapshot.fichiers)} fichier(s) sortis vers {dest_locale}") |
| 32 | 82 | snapshot.manifest.conn.close() |
| 83 | + _restaurer_contexte_post_checkout(args) | |
| 33 | 84 | return 0 |
| 34 | 85 | |
| 35 | 86 | |
| @@ -83,9 +134,11 @@ def _cmd_reconcile(args: argparse.Namespace) -> int: | ||
| 83 | 134 | |
| 84 | 135 | if not rapport.changements: |
| 85 | 136 | # 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 | 139 | lever(manifest) |
| 88 | 140 | manifest.conn.close() |
| 141 | + _sauvegarder_contexte_post_reconcile(args) | |
| 89 | 142 | return 0 |
| 90 | 143 | |
| 91 | 144 | decisions = DecisionsUtilisateur() |
| @@ -115,6 +168,7 @@ def _cmd_reconcile(args: argparse.Namespace) -> int: | ||
| 115 | 168 | archiver(snapshot, rapport, decisions) |
| 116 | 169 | print("Réconciliation terminée.") |
| 117 | 170 | manifest.conn.close() |
| 171 | + _sauvegarder_contexte_post_reconcile(args) | |
| 118 | 172 | return 0 |
| 119 | 173 | |
| 120 | 174 | |
| @@ -126,10 +180,10 @@ def _charger_archive_root_local(args: argparse.Namespace) -> Path | None: | ||
| 126 | 180 | partagée avec `regine-gui` (Principe VI — cf. bug trouvé par test manuel de la |
| 127 | 181 | GUI, specs/006 : deux résolutions dupliquées et non cohérentes entre elles).""" |
| 128 | 182 | 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 | |
| 130 | 184 | 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 | 187 | try: |
| 134 | 188 | contexte = charger_contexte(conn) |
| 135 | 189 | if contexte.archive_smb_path is None: |
| @@ -208,6 +262,7 @@ def construire_analyseur() -> argparse.ArgumentParser: | ||
| 208 | 262 | checkout_parser.add_argument("dossier") |
| 209 | 263 | checkout_parser.add_argument("--formats", default=None) |
| 210 | 264 | checkout_parser.add_argument("--local-dest", default=None) |
| 265 | + checkout_parser.add_argument("--config-db", default=None) | |
| 211 | 266 | checkout_parser.set_defaults(func=_cmd_checkout) |
| 212 | 267 | |
| 213 | 268 | reconcile_parser = sous_commandes.add_parser( |
| @@ -215,6 +270,7 @@ def construire_analyseur() -> argparse.ArgumentParser: | ||
| 215 | 270 | ) |
| 216 | 271 | reconcile_parser.add_argument("dossier") |
| 217 | 272 | reconcile_parser.add_argument("--local-dest", default=None) |
| 273 | + reconcile_parser.add_argument("--config-db", default=None) | |
| 218 | 274 | reconcile_parser.set_defaults(func=_cmd_reconcile) |
| 219 | 275 | |
| 220 | 276 | browse_parser = sous_commandes.add_parser( |
| @@ -222,7 +278,7 @@ def construire_analyseur() -> argparse.ArgumentParser: | ||
| 222 | 278 | ) |
| 223 | 279 | browse_parser.add_argument("--search", default=None) |
| 224 | 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 | 282 | browse_parser.set_defaults(func=_cmd_browse) |
| 227 | 283 | |
| 228 | 284 | restore_parser = sous_commandes.add_parser( |
| @@ -230,7 +286,7 @@ def construire_analyseur() -> argparse.ArgumentParser: | ||
| 230 | 286 | ) |
| 231 | 287 | restore_parser.add_argument("--chemin", required=True) |
| 232 | 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 | 290 | restore_parser.set_defaults(func=_cmd_restore) |
| 235 | 291 | |
| 236 | 292 | return analyseur |
| @@ -2,6 +2,14 @@ | |||
| 2 | 2 | ||
| 3 | Façade fine : orchestre `regine_core.archive`, formatte le résultat, ne contient | 3 | 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 annotations | 15 | 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, lever | 25 | 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.name | 72 | 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 0 | 84 | 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 verrou | 136 | # 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 0 | 142 | 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 0 | 172 | 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 la | 180 | 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: PLC0415 | 182 | 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 |
| 130 | from regine_core.config.smb import resoudre_archive_locale # noqa: PLC0415 | 184 | 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 analyseur | 292 | return analyseur |
modified
packages/regine-cli/src/regine_cli/config_cmd.py +88 -21 | @@ -2,6 +2,11 @@ | ||
| 2 | 2 | |
| 3 | 3 | Façade fine : orchestre `regine_core.config`, formatte le résultat, ne contient |
| 4 | 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 | 12 | from __future__ import annotations |
| @@ -21,12 +26,18 @@ from regine_core.config.context import ( | ||
| 21 | 26 | TravailNonReconcilieError, |
| 22 | 27 | charger_contexte, |
| 23 | 28 | enregistrer_chemins, |
| 29 | + restaurer_contexte_si_absent, | |
| 24 | 30 | ) |
| 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 | + | |
| 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): | |
| 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)) | |
| 30 | 41 | |
| 31 | 42 | |
| 32 | 43 | def _afficher_contexte(contexte: ContexteTravail) -> None: |
| @@ -36,8 +47,35 @@ def _afficher_contexte(contexte: ContexteTravail) -> None: | ||
| 36 | 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 | 77 | def _cmd_set_paths(args: argparse.Namespace) -> int: |
| 40 | - conn = _ouvrir_conn(args) | |
| 78 | + conn = _ouvrir_config(args) | |
| 41 | 79 | try: |
| 42 | 80 | contexte = enregistrer_chemins( |
| 43 | 81 | conn, |
| @@ -76,11 +114,16 @@ def _cmd_set_paths(args: argparse.Namespace) -> int: | ||
| 76 | 114 | |
| 77 | 115 | print("Contexte de travail enregistré :") |
| 78 | 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 | 122 | return 0 |
| 80 | 123 | |
| 81 | 124 | |
| 82 | 125 | def _cmd_show(args: argparse.Namespace) -> int: |
| 83 | - conn = _ouvrir_conn(args) | |
| 126 | + conn = _ouvrir_config(args) | |
| 84 | 127 | try: |
| 85 | 128 | contexte = charger_contexte(conn) |
| 86 | 129 | if contexte.archive_smb_path: |
| @@ -122,12 +165,30 @@ def _cmd_show(args: argparse.Namespace) -> int: | ||
| 122 | 165 | 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 | 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 | 187 | try: |
| 128 | - boitiers = lister_boitiers(conn) | |
| 188 | + boitiers = lister_boitiers(environnement.contexte_conn) | |
| 129 | 189 | finally: |
| 130 | - conn.close() | |
| 190 | + environnement.contexte_conn.close() | |
| 191 | + environnement.config_conn.close() | |
| 131 | 192 | |
| 132 | 193 | if args.json: |
| 133 | 194 | print( |
| @@ -158,14 +219,17 @@ def _cmd_cameras_list(args: argparse.Namespace) -> int: | ||
| 158 | 219 | |
| 159 | 220 | |
| 160 | 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 | 225 | try: |
| 163 | - nommer_boitier(conn, args.id, args.nom) | |
| 226 | + nommer_boitier(environnement.contexte_conn, args.id, args.nom) | |
| 164 | 227 | except BoitierInconnuError as exc: |
| 165 | 228 | print(str(exc), file=sys.stderr) |
| 166 | 229 | return 1 |
| 167 | 230 | finally: |
| 168 | - conn.close() | |
| 231 | + environnement.contexte_conn.close() | |
| 232 | + environnement.config_conn.close() | |
| 169 | 233 | |
| 170 | 234 | print(f"Boîtier {args.id} renommé en « {args.nom} ».") |
| 171 | 235 | return 0 |
| @@ -185,13 +249,15 @@ def _cmd_cameras_scan_nas(args: argparse.Namespace) -> int: | ||
| 185 | 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 | 255 | try: |
| 190 | - contexte = charger_contexte(conn) | |
| 256 | + contexte = charger_contexte(environnement.config_conn) | |
| 191 | 257 | if contexte.archive_smb_path is None: |
| 192 | 258 | print("Aucune archive configurée (regine config set-paths).", file=sys.stderr) |
| 193 | 259 | 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 | 261 | print( |
| 196 | 262 | f"{contexte.archive_smb_path} n'est pas accessible — montage natif déclenché, " |
| 197 | 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 | 266 | return 1 |
| 201 | 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 | 269 | finally: |
| 204 | - conn.close() | |
| 270 | + environnement.contexte_conn.close() | |
| 271 | + environnement.config_conn.close() | |
| 205 | 272 | |
| 206 | 273 | print(f"Parcours terminé : {nouveaux} nouveau(x) boîtier(s) détecté(s).") |
| 207 | 274 | return 0 |
| @@ -221,12 +288,12 @@ def construire_analyseur() -> argparse.ArgumentParser: | ||
| 221 | 288 | set_paths_parser.add_argument("--yes", action="store_true") |
| 222 | 289 | set_paths_parser.add_argument("--force", action="store_true") |
| 223 | 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 | 292 | set_paths_parser.set_defaults(func=_cmd_set_paths) |
| 226 | 293 | |
| 227 | 294 | show_parser = config_sous.add_parser("show", help="Affiche le contexte de travail actif") |
| 228 | 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 | 297 | show_parser.set_defaults(func=_cmd_show) |
| 231 | 298 | |
| 232 | 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 | 302 | cameras_list_parser = cameras_sous.add_parser("list", help="Liste les boîtiers connus") |
| 236 | 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 | 305 | cameras_list_parser.set_defaults(func=_cmd_cameras_list) |
| 239 | 306 | |
| 240 | 307 | cameras_rename_parser = cameras_sous.add_parser("rename", help="Nomme un boîtier") |
| 241 | 308 | cameras_rename_parser.add_argument("--id", type=int, required=True) |
| 242 | 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 | 311 | cameras_rename_parser.set_defaults(func=_cmd_cameras_rename) |
| 245 | 312 | |
| 246 | 313 | cameras_scan_parser = cameras_sous.add_parser( |
| 247 | 314 | "scan-nas", help="Parcours volontaire de l'archive (FR-012)" |
| 248 | 315 | ) |
| 249 | 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 | 318 | cameras_scan_parser.set_defaults(func=_cmd_cameras_scan_nas) |
| 252 | 319 | |
| 253 | 320 | return analyseur |
| @@ -2,6 +2,11 @@ | |||
| 2 | 2 | ||
| 3 | Façade fine : orchestre `regine_core.config`, formatte le résultat, ne contient | 3 | 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 annotations | 12 | 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_db | 31 | +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 0 | 122 | 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 0 | 165 | 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 1 | 229 | 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 0 | 235 | 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 1 | 259 | 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 1 | 266 | 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 0 | 274 | 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 analyseur | 320 | 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 ( | ||
| 43 | 43 | from regine_core.import_carte.push import preparer_resume, publier_localement |
| 44 | 44 | 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 | 47 | def _proposer_detachement(groupes: list[GroupeImport], interactif: bool) -> list[GroupeImport]: |
| 50 | 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 | 301 | local_tmp = Path(args.local_tmp) if args.local_tmp else Path.cwd() / ".regine-import-tmp" |
| 304 | 302 | archive_root = Path(args.archive_root) |
| 305 | 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 | 304 | 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 | 314 | try: |
| 313 | 315 | fichiers = copier_carte(carte, local_tmp) |
| 314 | 316 | if not fichiers: |
| @@ -363,7 +365,6 @@ def construire_analyseur() -> argparse.ArgumentParser: | ||
| 363 | 365 | import_parser.add_argument("--local-tmp", default=None) |
| 364 | 366 | import_parser.add_argument("--archive-root", required=True) |
| 365 | 367 | import_parser.add_argument("--local-root", required=True) |
| 366 | - import_parser.add_argument("--contexte-db", default=None) | |
| 367 | 368 | import_parser.set_defaults(func=_cmd_import) |
| 368 | 369 | |
| 369 | 370 | return analyseur |
| @@ -43,8 +43,6 @@ from regine_core.import_carte.nommage import ( | |||
| 43 | from regine_core.import_carte.push import preparer_resume, publier_localement | 43 | from regine_core.import_carte.push import preparer_resume, publier_localement |
| 44 | from regine_core.import_carte.types import DestinationChoisie, FichierCandidat, GroupeImport | 44 | 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.yes | 304 | 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 analyseur | 370 | return analyseur |
modified
packages/regine-cli/tests/contract/test_cli_archive_consultation.py +6 -6 | @@ -8,21 +8,21 @@ from pathlib import Path | ||
| 8 | 8 | |
| 9 | 9 | from regine_cli.archive_cmd import main |
| 10 | 10 | 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 | |
| 12 | 12 | |
| 13 | 13 | |
| 14 | 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 un | |
| 15 | + """Configure la configuration globale dont `archive_smb_path` résout, via un | |
| 16 | 16 | `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) | |
| 19 | 19 | enregistrer_chemins(conn, archive_smb_path="smb://nas.local/regine-archive") |
| 20 | 20 | conn.close() |
| 21 | - return ["--contexte-db", str(contexte_db)] | |
| 21 | + return ["--config-db", str(config_db)] | |
| 22 | 22 | |
| 23 | 23 | |
| 24 | 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 | 27 | assert code != 0 |
| 28 | 28 | 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 main | 9 | from regine_cli.archive_cmd import main |
| 10 | from regine_core.config.context import enregistrer_chemins | 10 | 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 |
| 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 un | 15 | + """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 != 0 | 27 | assert code != 0 |
| 28 | assert "Aucune archive configurée" in capsys.readouterr().err | 28 | 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).""" | |
| 2 | 7 | |
| 3 | 8 | from __future__ import annotations |
| 4 | 9 | |
| @@ -7,11 +12,23 @@ from pathlib import Path | ||
| 7 | 12 | |
| 8 | 13 | from regine_cli.config_cmd import main |
| 9 | 14 | 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 | + | |
| 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]: | |
| 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 | |
| 15 | 32 | |
| 16 | 33 | |
| 17 | 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 | 46 | "--archive-smb", |
| 30 | 47 | "smb://nas.local/regine-archive", |
| 31 | 48 | "--yes", |
| 32 | - *_contexte_db(tmp_path), | |
| 49 | + *_config_db(tmp_path), | |
| 33 | 50 | ] |
| 34 | 51 | ) |
| 35 | 52 | assert code == 0 |
| 36 | 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 | 56 | assert code == 0 |
| 40 | 57 | sortie = json.loads(capsys.readouterr().out) |
| 41 | 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 | 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 | 66 | assert code != 0 |
| 50 | 67 | sortie = capsys.readouterr().out |
| @@ -60,7 +77,7 @@ def test_set_paths_repertoire_manquant_sans_yes_echoue(tmp_path: Path, capsys) - | ||
| 60 | 77 | "set-paths", |
| 61 | 78 | "--temp-dir", |
| 62 | 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 | 97 | "--local-work-dir", |
| 81 | 98 | str(meme_chemin), |
| 82 | 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 | 112 | "set-paths", |
| 96 | 113 | "--archive-smb", |
| 97 | 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 | 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 | 128 | assert code == 0 |
| 110 | 129 | 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 | 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 | 144 | boitier_id = get_or_create_boitier(conn, "Fujifilm X100V", "12345", source="modele") |
| 116 | 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 | 148 | assert code == 0 |
| 120 | 149 | boitiers = json.loads(capsys.readouterr().out) |
| 121 | 150 | assert boitiers[0]["nom_lisible"] is None |
| @@ -129,28 +158,28 @@ def test_cameras_list_puis_rename(tmp_path: Path, capsys) -> None: | ||
| 129 | 158 | str(boitier_id), |
| 130 | 159 | "--nom", |
| 131 | 160 | "Fuji principal", |
| 132 | - *_contexte_db(tmp_path), | |
| 161 | + *_config_db(tmp_path), | |
| 133 | 162 | ] |
| 134 | 163 | ) |
| 135 | 164 | assert code == 0 |
| 136 | 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 | 168 | boitiers = json.loads(capsys.readouterr().out) |
| 140 | 169 | assert boitiers[0]["nom_lisible"] == "Fuji principal" |
| 141 | 170 | |
| 142 | 171 | |
| 143 | 172 | 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)]) | |
| 147 | 176 | |
| 148 | 177 | assert code != 0 |
| 149 | 178 | assert "inconnu" in capsys.readouterr().err |
| 150 | 179 | |
| 151 | 180 | |
| 152 | 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 | 184 | assert code != 0 |
| 156 | 185 | 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 annotations | 8 | from __future__ import annotations |
| 4 | 9 | ||
| @@ -7,11 +12,23 @@ from pathlib import Path | |||
| 7 | 12 | ||
| 8 | from regine_cli.config_cmd import main | 13 | from regine_cli.config_cmd import main |
| 9 | from regine_core.camera_profile.db import get_or_create_boitier | 14 | 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 | + | ||
| 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 == 0 | 52 | 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 == 0 | 56 | 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 != 0 | 66 | assert code != 0 |
| 50 | sortie = capsys.readouterr().out | 67 | 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 == 0 | 128 | assert code == 0 |
| 110 | assert "Aucun boîtier" in capsys.readouterr().out | 129 | 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 == 0 | 148 | assert code == 0 |
| 120 | boitiers = json.loads(capsys.readouterr().out) | 149 | boitiers = json.loads(capsys.readouterr().out) |
| 121 | assert boitiers[0]["nom_lisible"] is None | 150 | 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 == 0 | 164 | 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 != 0 | 177 | assert code != 0 |
| 149 | assert "inconnu" in capsys.readouterr().err | 178 | 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 != 0 | 184 | assert code != 0 |
| 156 | assert "--confirm" in capsys.readouterr().out | 185 | 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` | ||
| 7 | 7 | |
| 8 | 8 | from __future__ import annotations |
| 9 | 9 | |
| 10 | +import shutil | |
| 10 | 11 | import sqlite3 |
| 11 | 12 | from dataclasses import dataclass |
| 12 | 13 | from pathlib import Path |
| @@ -60,6 +61,11 @@ class ArchiveInaccessibleError(Exception): | ||
| 60 | 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 | 69 | @dataclass(frozen=True) |
| 64 | 70 | class ContexteTravail: |
| 65 | 71 | """Contexte de travail actif — unique par installation (cf. spec.md Assumptions).""" |
| @@ -195,9 +201,18 @@ def enregistrer_chemins( | ||
| 195 | 201 | chemin.mkdir(parents=True, exist_ok=True) |
| 196 | 202 | |
| 197 | 203 | 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() | |
| 201 | 216 | |
| 202 | 217 | _ensure_schema(conn) |
| 203 | 218 | conn.execute( |
| @@ -219,6 +234,69 @@ def enregistrer_chemins( | ||
| 219 | 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 | 300 | def mettre_a_jour_etat_smb(conn: sqlite3.Connection, etat: EtatSmb) -> None: |
| 223 | 301 | """Met à jour l'état de disponibilité connu du partage SMB (FR-004/005).""" |
| 224 | 302 | _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 annotations | 8 | from __future__ import annotations |
| 9 | 9 | ||
| 10 | +import shutil | ||
| 10 | import sqlite3 | 11 | import sqlite3 |
| 11 | from dataclasses import dataclass | 12 | from dataclasses import dataclass |
| 12 | from pathlib import Path | 13 | 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. | |
| 9 | 23 | """ |
| 10 | 24 | |
| 11 | 25 | from __future__ import annotations |
| @@ -19,21 +33,26 @@ APPLICATION_ID = 0x52454749 | ||
| 19 | 33 | #: Numéro de schéma courant. À incrémenter à chaque évolution du schéma partagé. |
| 20 | 34 | SCHEMA_VERSION = 1 |
| 21 | 35 | |
| 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" | |
| 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: | |
| 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" | |
| 31 | 48 | |
| 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. | |
| 35 | 55 | """ |
| 36 | - path = path or CHEMIN_CONTEXTE_PAR_DEFAUT | |
| 37 | 56 | path.parent.mkdir(parents=True, exist_ok=True) |
| 38 | 57 | conn = sqlite3.connect(path) |
| 39 | 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 | 71 | 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é par | 3 | +**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`` dans | 4 | +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 les | 5 | +base unique à emplacement unique — écart trouvé par l'utilisateur : l'ancienne |
| 6 | -mêmes conventions que le manifeste persistant par dossier (constitution | 6 | +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 annotations | 25 | 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 = 1 | 34 | 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 dernier | 37 | +#: 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) ne | 38 | +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 de | 49 | + |
| 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 conn | 71 | 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) | ||
| 41 | 41 | assert temp_dir.exists() |
| 42 | 42 | assert local_work_dir.exists() |
| 43 | 43 | |
| 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() | |
| 47 | 48 | |
| 48 | 49 | # Une autre fonctionnalité (import, cf. specs/001) peut relire ce contexte sans |
| 49 | 50 | # 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 sans | 49 | # 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).""" | |
| 2 | 7 | |
| 3 | 8 | from __future__ import annotations |
| 4 | 9 | |
| 5 | 10 | from pathlib import Path |
| 6 | 11 | |
| 7 | 12 | 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 | |
| 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 | 17 | tmp_path: Path, monkeypatch |
| 13 | 18 | ) -> 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) | |
| 16 | 21 | |
| 17 | - conn = open_context_db() | |
| 22 | + conn = open_global_config_db() | |
| 18 | 23 | |
| 19 | 24 | assert chemin_attendu.exists() |
| 20 | 25 | assert conn.execute("PRAGMA application_id").fetchone()[0] == APPLICATION_ID |
| 21 | 26 | conn.close() |
| 22 | 27 | |
| 23 | 28 | |
| 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" | |
| 26 | 49 | |
| 27 | 50 | conn = open_context_db(chemin_explicite) |
| 28 | 51 | |
| @@ -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 annotations | 8 | from __future__ import annotations |
| 4 | 9 | ||
| 5 | from pathlib import Path | 10 | from pathlib import Path |
| 6 | 11 | ||
| 7 | import regine_core.config.db as db_module | 12 | 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 |
| 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, monkeypatch | 17 | 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_ID | 25 | 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: | ||
| 55 | 55 | assert contexte.archive_smb_path == "smb://nas.local/regine-archive" |
| 56 | 56 | assert temp_dir.exists() |
| 57 | 57 | 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() | |
| 59 | 60 | conn.close() |
| 60 | 61 | |
| 61 | 62 | |
| @@ -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-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() | ||
| 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): | ||
| 19 | 19 | Story 3) sous quatre onglets d'une même fenêtre — chacun appelle sa propre |
| 20 | 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 | 23 | super().__init__() |
| 24 | 24 | self.setWindowTitle("Régine") |
| 25 | 25 | self.resize(760, 520) |
| @@ -27,12 +27,12 @@ class FenetrePrincipale(QMainWindow): | ||
| 27 | 27 | onglets = QTabWidget() |
| 28 | 28 | self.setCentralWidget(onglets) |
| 29 | 29 | self._ecran_tri = EcranTri() |
| 30 | - ecran_import = EcranImport(contexte_db=contexte_db) | |
| 30 | + ecran_import = EcranImport(config_db=config_db) | |
| 31 | 31 | ecran_import.dossier_importe.connect(self._ecran_tri.ouvrir_dossier) |
| 32 | 32 | |
| 33 | 33 | 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") | |
| 36 | 36 | onglets.addTab(ecran_import, "Import") |
| 37 | 37 | |
| 38 | 38 | |
| @@ -19,7 +19,7 @@ class FenetrePrincipale(QMainWindow): | |||
| 19 | Story 3) sous quatre onglets d'une même fenêtre — chacun appelle sa propre | 19 | 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 | ||
| 5 | 5 | présentée clairement (boîte de dialogue), jamais comme une exception technique |
| 6 | 6 | brute — mêmes règles que la CLI (`regine config set-paths`), jamais devinées ou |
| 7 | 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 | 16 | from __future__ import annotations |
| @@ -36,8 +42,10 @@ from regine_core.config.context import ( | ||
| 36 | 42 | TravailNonReconcilieError, |
| 37 | 43 | charger_contexte, |
| 38 | 44 | enregistrer_chemins, |
| 45 | + restaurer_contexte_si_absent, | |
| 39 | 46 | ) |
| 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 | |
| 41 | 49 | |
| 42 | 50 | _ROLE_ID = Qt.ItemDataRole.UserRole |
| 43 | 51 | |
| @@ -46,9 +54,9 @@ class EcranConfiguration(QWidget): | ||
| 46 | 54 | """Onglet de configuration : chemins, état SMB, boîtiers nommés — même |
| 47 | 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 | 58 | super().__init__() |
| 51 | - self._contexte_db = contexte_db | |
| 59 | + self._config_db = config_db | |
| 52 | 60 | |
| 53 | 61 | disposition = QVBoxLayout(self) |
| 54 | 62 | |
| @@ -91,13 +99,19 @@ class EcranConfiguration(QWidget): | ||
| 91 | 99 | |
| 92 | 100 | self._rafraichir() |
| 93 | 101 | |
| 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() | |
| 96 | 110 | |
| 97 | 111 | def _rafraichir(self) -> None: |
| 98 | - conn = self._ouvrir_conn() | |
| 112 | + environnement = self._ouvrir_environnement() | |
| 99 | 113 | try: |
| 100 | - contexte = charger_contexte(conn) | |
| 114 | + contexte = charger_contexte(environnement.config_conn) | |
| 101 | 115 | self._champ_temp_dir.setText(str(contexte.temp_dir) if contexte.temp_dir else "") |
| 102 | 116 | self._champ_local_work_dir.setText( |
| 103 | 117 | str(contexte.local_work_dir) if contexte.local_work_dir else "" |
| @@ -106,10 +120,11 @@ class EcranConfiguration(QWidget): | ||
| 106 | 120 | self._label_etat_smb.setText(f"État SMB : {contexte.smb_last_known_state}") |
| 107 | 121 | |
| 108 | 122 | 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) | |
| 111 | 126 | finally: |
| 112 | - conn.close() | |
| 127 | + self._fermer(environnement) | |
| 113 | 128 | |
| 114 | 129 | def _ajouter_boitier(self, boitier: Boitier) -> None: |
| 115 | 130 | nom = boitier.nom_lisible or "(non nommé)" |
| @@ -119,9 +134,9 @@ class EcranConfiguration(QWidget): | ||
| 119 | 134 | self._liste_boitiers.addItem(item) |
| 120 | 135 | |
| 121 | 136 | def _enregistrer(self) -> None: |
| 122 | - conn = self._ouvrir_conn() | |
| 137 | + conn = open_global_config_db(self._config_db) | |
| 123 | 138 | try: |
| 124 | - enregistrer_chemins( | |
| 139 | + contexte = enregistrer_chemins( | |
| 125 | 140 | conn, |
| 126 | 141 | temp_dir=Path(self._champ_temp_dir.text()) if self._champ_temp_dir.text() else None, |
| 127 | 142 | local_work_dir=Path(self._champ_local_work_dir.text()) |
| @@ -155,8 +170,28 @@ class EcranConfiguration(QWidget): | ||
| 155 | 170 | finally: |
| 156 | 171 | conn.close() |
| 157 | 172 | |
| 173 | + self._tenter_restauration_contexte(contexte) | |
| 158 | 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 | 195 | def _nommer_boitier_selectionne(self) -> None: |
| 161 | 196 | item = self._liste_boitiers.currentItem() |
| 162 | 197 | if item is None: |
| @@ -167,13 +202,21 @@ class EcranConfiguration(QWidget): | ||
| 167 | 202 | if not ok or not nom.strip(): |
| 168 | 203 | 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 | 214 | try: |
| 172 | - nommer_boitier(conn, boitier_id, nom.strip()) | |
| 215 | + nommer_boitier(environnement.contexte_conn, boitier_id, nom.strip()) | |
| 173 | 216 | except BoitierInconnuError as exc: |
| 174 | 217 | QMessageBox.warning(self, "Boîtier inconnu", str(exc)) |
| 175 | 218 | return |
| 176 | 219 | finally: |
| 177 | - conn.close() | |
| 220 | + self._fermer(environnement) | |
| 178 | 221 | |
| 179 | 222 | 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 technique | 5 | 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 ou | 6 | 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 annotations | 16 | 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_db | 47 | +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.UserRole | 50 | _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ême | 54 | """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_db | 59 | + 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 | return | 203 | 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 | return | 218 | 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 @@ | ||
| 1 | 1 | """Écran de consultation (US2) — appelle exclusivement `regine_core.archive.consultation` |
| 2 | 2 | et `regine_core.config`, aucune logique métier propre (Principe VI). Lecture seule |
| 3 | 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 | 11 | from __future__ import annotations |
| @@ -23,7 +28,7 @@ from PySide6.QtWidgets import ( | ||
| 23 | 28 | ) |
| 24 | 29 | from regine_core.archive.consultation import RestaurationCollisionError, rechercher, restaurer |
| 25 | 30 | 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 | |
| 27 | 32 | from regine_core.config.smb import resoudre_archive_locale |
| 28 | 33 | |
| 29 | 34 | _ROLE_CHEMIN = Qt.ItemDataRole.UserRole |
| @@ -34,11 +39,11 @@ class EcranConsultation(QMainWindow): | ||
| 34 | 39 | (`specs/003`) : recherche (nom de dossier, date, titre) et restauration ciblée |
| 35 | 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 | 43 | super().__init__() |
| 39 | 44 | self.setWindowTitle("Régine — Consultation de l'archive") |
| 40 | 45 | self.resize(720, 480) |
| 41 | - self._contexte_db = contexte_db | |
| 46 | + self._config_db = config_db | |
| 42 | 47 | self._archive_root_local: Path | None = None |
| 43 | 48 | |
| 44 | 49 | conteneur = QWidget() |
| @@ -69,7 +74,7 @@ class EcranConsultation(QMainWindow): | ||
| 69 | 74 | if self._archive_root_local is not None: |
| 70 | 75 | 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 | 78 | try: |
| 74 | 79 | contexte = charger_contexte(conn) |
| 75 | 80 | 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 seule | 2 | 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 annotations | 11 | from __future__ import annotations |
| @@ -23,7 +28,7 @@ from PySide6.QtWidgets import ( | |||
| 23 | ) | 28 | ) |
| 24 | from regine_core.archive.consultation import RestaurationCollisionError, rechercher, restaurer | 29 | from regine_core.archive.consultation import RestaurationCollisionError, rechercher, restaurer |
| 25 | from regine_core.config.context import charger_contexte | 30 | 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 |
| 27 | from regine_core.config.smb import resoudre_archive_locale | 32 | from regine_core.config.smb import resoudre_archive_locale |
| 28 | 33 | ||
| 29 | _ROLE_CHEMIN = Qt.ItemDataRole.UserRole | 34 | _ROLE_CHEMIN = Qt.ItemDataRole.UserRole |
| @@ -34,11 +39,11 @@ class EcranConsultation(QMainWindow): | |||
| 34 | (`specs/003`) : recherche (nom de dossier, date, titre) et restauration ciblée | 39 | (`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_db | 46 | + self._config_db = config_db |
| 42 | self._archive_root_local: Path | None = None | 47 | 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_local | 75 | 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 | ||
| 17 | 17 | n'a plus besoin que le partage SMB configuré soit actuellement monté pour |
| 18 | 18 | importer (seul son chemin sert à calculer où le dossier ira plus tard), ce qui |
| 19 | 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 | 29 | from __future__ import annotations |
| @@ -41,7 +48,7 @@ from regine_core.camera_profile.db import list_boitiers | ||
| 41 | 48 | from regine_core.camera_profile.resolve import assign_manual_source |
| 42 | 49 | from regine_core.config.categories import list_known_categories, register_category_usage |
| 43 | 50 | 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 | |
| 45 | 52 | from regine_core.config.smb import chemin_local_du_partage, disponible |
| 46 | 53 | from regine_core.import_carte.classement import classer_par_format |
| 47 | 54 | from regine_core.import_carte.copie import ( |
| @@ -123,12 +130,12 @@ class EcranImport(QMainWindow): | ||
| 123 | 130 | #: `app.py` propose d'ouvrir le dossier importé dans l'onglet Tri. |
| 124 | 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 | 134 | super().__init__() |
| 128 | 135 | self.setWindowTitle("Régine — Import") |
| 129 | 136 | 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 | |
| 132 | 139 | self._carte: Path | None = None |
| 133 | 140 | self._archive_root: Path | None = None |
| 134 | 141 | self._local_root: Path | None = None |
| @@ -174,9 +181,6 @@ class EcranImport(QMainWindow): | ||
| 174 | 181 | def _log(self, message: str) -> None: |
| 175 | 182 | self._journal.append(message) |
| 176 | 183 | |
| 177 | - def _ouvrir_conn(self): | |
| 178 | - return open_context_db(self._contexte_db) | |
| 179 | - | |
| 180 | 184 | def _choisir_carte(self) -> None: |
| 181 | 185 | chemin = QFileDialog.getExistingDirectory(self, "Choisir une carte mémoire") |
| 182 | 186 | if chemin: |
| @@ -195,10 +199,10 @@ class EcranImport(QMainWindow): | ||
| 195 | 199 | ) |
| 196 | 200 | return |
| 197 | 201 | |
| 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() | |
| 202 | 206 | QMessageBox.warning( |
| 203 | 207 | self, |
| 204 | 208 | "Contexte non configuré", |
| @@ -207,7 +211,8 @@ class EcranImport(QMainWindow): | ||
| 207 | 211 | return |
| 208 | 212 | |
| 209 | 213 | if contexte.archive_smb_path is None: |
| 210 | - conn.close() | |
| 214 | + environnement.config_conn.close() | |
| 215 | + environnement.contexte_conn.close() | |
| 211 | 216 | QMessageBox.warning( |
| 212 | 217 | self, |
| 213 | 218 | "Archive non configurée", |
| @@ -231,6 +236,12 @@ class EcranImport(QMainWindow): | ||
| 231 | 236 | local_root = contexte.local_work_dir |
| 232 | 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 | 245 | try: |
| 235 | 246 | fichiers = copier_carte(self._carte, local_tmp) |
| 236 | 247 | except EchecVerificationError as exc: |
| @@ -258,7 +269,7 @@ class EcranImport(QMainWindow): | ||
| 258 | 269 | groupes = decouper_en_groupes(fichiers) |
| 259 | 270 | groupes = self._proposer_detachement(groupes) |
| 260 | 271 | |
| 261 | - self._conn = conn | |
| 272 | + self._contexte_conn = conn | |
| 262 | 273 | self._archive_root = archive_root |
| 263 | 274 | self._local_root = local_root |
| 264 | 275 | self._local_tmp = local_tmp |
| @@ -366,9 +377,9 @@ class EcranImport(QMainWindow): | ||
| 366 | 377 | des groupes, sinon ferme la connexion à la base de contexte.""" |
| 367 | 378 | if self._groupes: |
| 368 | 379 | 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 | |
| 372 | 383 | self._label_groupes.setText("Tous les groupes ont été traités.") |
| 373 | 384 | |
| 374 | 385 | def _traiter_groupe(self, groupe: GroupeImport) -> bool: |
| @@ -376,11 +387,11 @@ class EcranImport(QMainWindow): | ||
| 376 | 387 | si l'archivage a été lancé en arrière-plan (finalisation différée |
| 377 | 388 | jusqu'au signal `succes`/`echec` du worker), `False` si le groupe a été |
| 378 | 389 | ignoré/annulé (finalisation immédiate par l'appelant).""" |
| 379 | - assert self._conn is not None | |
| 390 | + assert self._contexte_conn is not None | |
| 380 | 391 | assert self._archive_root is not None |
| 381 | 392 | assert self._local_root is not None |
| 382 | 393 | assert self._local_tmp is not None |
| 383 | - conn = self._conn | |
| 394 | + conn = self._contexte_conn | |
| 384 | 395 | archive_root = self._archive_root |
| 385 | 396 | local_root = self._local_root |
| 386 | 397 | local_tmp = self._local_tmp |
| @@ -538,8 +549,8 @@ class EcranImport(QMainWindow): | ||
| 538 | 549 | |
| 539 | 550 | def _sur_publication_succes(self, dossier_local: Path) -> None: |
| 540 | 551 | 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) | |
| 543 | 554 | self._log(f"Copie de travail locale prête : {dossier_local}") |
| 544 | 555 | self._log( |
| 545 | 556 | "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é pour | 17 | 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 qui | 18 | 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 annotations | 29 | 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_source | 48 | from regine_core.camera_profile.resolve import assign_manual_source |
| 42 | from regine_core.config.categories import list_known_categories, register_category_usage | 49 | from regine_core.config.categories import list_known_categories, register_category_usage |
| 43 | from regine_core.config.context import charger_contexte | 50 | 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 |
| 45 | from regine_core.config.smb import chemin_local_du_partage, disponible | 52 | from regine_core.config.smb import chemin_local_du_partage, disponible |
| 46 | from regine_core.import_carte.classement import classer_par_format | 53 | 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_db | 137 | + self._config_db = config_db |
| 131 | - self._conn = None | 138 | + self._contexte_conn = None |
| 132 | self._carte: Path | None = None | 139 | self._carte: Path | None = None |
| 133 | self._archive_root: Path | None = None | 140 | self._archive_root: Path | None = None |
| 134 | self._local_root: Path | None = None | 141 | 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 | return | 200 | 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 | return | 211 | 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_dir | 236 | 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 = conn | 272 | + self._contexte_conn = conn |
| 262 | self._archive_root = archive_root | 273 | self._archive_root = archive_root |
| 263 | self._local_root = local_root | 274 | self._local_root = local_root |
| 264 | self._local_tmp = local_tmp | 275 | 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 = None | 382 | + 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ée | 387 | 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 None | 390 | + assert self._contexte_conn is not None |
| 380 | assert self._archive_root is not None | 391 | assert self._archive_root is not None |
| 381 | assert self._local_root is not None | 392 | assert self._local_root is not None |
| 382 | assert self._local_tmp is not None | 393 | assert self._local_tmp is not None |
| 383 | - conn = self._conn | 394 | + conn = self._contexte_conn |
| 384 | archive_root = self._archive_root | 395 | archive_root = self._archive_root |
| 385 | local_root = self._local_root | 396 | local_root = self._local_root |
| 386 | local_tmp = self._local_tmp | 397 | 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_cours | 551 | 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 | ||
| 37 | 37 | |
| 38 | 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 | 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 | 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 | 43 | ## 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 @@ | ||
| 52 | 52 | 2. Accès de la GUI au contexte de travail : oui, en appelant directement `regine_core.config` |
| 53 | 53 | sans logique propre à la GUI (Principe VI) — nouvelle User Story 4, nouveau FR-013, nouveau |
| 54 | 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. | |
| @@ -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, nouveau | 53 | 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 | ||
| 12 | 12 | ``` |
| 13 | 13 | |
| 14 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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). |
| @@ -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 @@ | ||
| 2 | 2 | |
| 3 | 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 | 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 | 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 | 48 | ## `smb.chemin_local_du_partage(archive_smb_path: str) -> Path` |
| 33 | 49 | |
| 34 | 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 | 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 | 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 | 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. | |
| @@ -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 @@ | ||
| 2 | 2 | |
| 3 | 3 | Entités dérivées de `spec.md` § Key Entities et Functional Requirements. |
| 4 | 4 | |
| 5 | -## Contexte de travail | |
| 5 | +## 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 | 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 | 13 | | `archive_smb_path` | chemin réseau (partage SMB) | DOIT correspondre à un partage réseau, pas à un dossier local (Edge Case) | |
| 14 | 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 | | |
| 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. | |
| 27 | 23 | |
| 28 | -## Base de contexte centralisée | |
| 24 | +**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 | 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 | 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 | +``` | |
| @@ -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 travail | 5 | +## 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ée | 24 | +**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 | ||
| 10 | 10 | |
| 11 | 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 | 15 | ## Technical Context |
| 14 | 16 | |
| 15 | 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 | 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 | 27 | **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 | 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 | 70 | ## Project Structure |
| 54 | 71 | |
| 55 | 72 | ### Documentation (this feature) |
| @@ -119,6 +136,60 @@ packages/ | ||
| 119 | 136 | |
| 120 | 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 | 193 | ## Complexity Tracking |
| 123 | 194 | |
| 124 | 195 | *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 Context | 15 | ## 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îtiers | 27 | **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 Structure | 70 | ## 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 Tracking | 193 | ## 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 | ||
| 74 | 74 | |
| 75 | 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 | 110 | ## 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. | |
| @@ -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 sortie | 110 | ## 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 @@ | ||
| 49 | 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 | 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 | 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. | |
| @@ -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 @@ | ||
| 20 | 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 | 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 | 29 | ## User Scenarios & Testing *(mandatory)* |
| 24 | 30 | |
| 25 | 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 | 117 | ### Edge Cases |
| 94 | 118 | |
| 95 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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. | |
| 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. | |
| 112 | 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 | 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 | 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 | 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 | 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 | 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 | 149 | ### 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 | 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. | |
| 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). | |
| 127 | 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 | 159 | ## Success Criteria *(mandatory)* |
| @@ -138,13 +168,16 @@ Un photographe qui utilise déjà l'interface graphique de Régine pour trier se | ||
| 138 | 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 | 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 | 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 | 174 | ## 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 | 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 | 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é. | |
| 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. | |
| 149 | 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`). | |
| @@ -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 Cases | 117 | ### 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 Entities | 149 | ### 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 | ## Assumptions | 174 | ## 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" | ||
| 25 | 25 | |
| 26 | 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 | 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 | 31 | ## 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 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 | |
| 142 | 174 | |
| 143 | 175 | - [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 | 176 | - [X] T027 [P] `ruff check --fix` + `ruff format` sur `packages/regine-core` et `packages/regine-cli` |
| 145 | 177 | - [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 | 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 | 188 | - **US2 (Phase 4)** : dépend de Foundational ; s'intègre à `regine config show` (US1) mais reste testable indépendamment (`smb.py` seul). |
| 156 | 189 | - **US3 (Phase 5)** : dépend de Foundational et de `specs/002` (déjà livrée) ; indépendante de US1/US2. |
| 157 | 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 | 194 | ## 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. | |
| @@ -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: Setup | 31 | ## 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 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 | ||
| 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éelle | 175 | - [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 explicite | 177 | - [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 Strategy | 194 | ## 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 @@ | ||
| 1 | 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 | 7 | Parcourt l'archive configurée en lecture seule (US2 FR-008). |
| 8 | 8 | |
| @@ -20,7 +20,7 @@ regine browse --search "Montenegro" | ||
| 20 | 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 | 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 | 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 | 33 | **Sorties** : |
| 34 | 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 | 36 | - `--chemin` introuvable dans l'archive : erreur sur stderr, code non-zéro. |
| 37 | 37 | - 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 | ||
| 28 | 28 | |
| 29 | 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 | 33 | ## `import_carte.*` — réutilisées telles quelles par l'onglet d'import (US3) |
| 34 | 34 | |
| @@ -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 @@ | ||
| 38 | 38 | |
| 39 | 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 | 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 | |
| @@ -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 | ||