duplicates
a1d9142 parent: 02d8847 modified
packages/regine-cli/src/regine_cli/import_cmd.py +28 -5 | @@ -31,6 +31,7 @@ from regine_core.import_carte.groupage import ( | ||
| 31 | 31 | detacher_jours, |
| 32 | 32 | jours_candidats_au_detachement, |
| 33 | 33 | ) |
| 34 | +from regine_core.import_carte.historique import enregistrer_import, rechercher_doublons | |
| 34 | 35 | from regine_core.import_carte.identifiant import attribuer_identifiants |
| 35 | 36 | from regine_core.import_carte.nommage import ( |
| 36 | 37 | construire_nom_dossier, |
| @@ -60,6 +61,22 @@ def _proposer_detachement(groupes: list[GroupeImport], interactif: bool) -> list | ||
| 60 | 61 | return groupes |
| 61 | 62 | |
| 62 | 63 | |
| 64 | +def _confirmer_malgre_doublons(doublons: dict[str, str], *, interactif: bool) -> bool: | |
| 65 | + """Avertit l'utilisateur que certains fichiers de la carte ont déjà été | |
| 66 | + importés précédemment, ailleurs (FR-004 révisé) — ne bloque jamais | |
| 67 | + silencieusement, laisse l'utilisateur décider (Principe V). En mode non | |
| 68 | + interactif (`--yes`), n'interrompt pas l'import (le doublon n'est pas | |
| 69 | + destructif, seulement redondant) mais reste visible sur stdout.""" | |
| 70 | + dossiers = sorted(set(doublons.values())) | |
| 71 | + print(f"{len(doublons)} fichier(s) de cette carte ont déjà été importés précédemment :") | |
| 72 | + for dossier in dossiers: | |
| 73 | + print(f" - dans {dossier}") | |
| 74 | + if not interactif: | |
| 75 | + return True | |
| 76 | + reponse = input("Continuer l'import malgré tout ? [o/n] ") | |
| 77 | + return reponse.strip().lower() == "o" | |
| 78 | + | |
| 79 | + | |
| 63 | 80 | def _resoudre_etiquetage_manuel( |
| 64 | 81 | a_etiqueter: list[list[FichierCandidat]], *, conn: sqlite3.Connection, interactif: bool |
| 65 | 82 | ) -> None: |
| @@ -273,6 +290,7 @@ def _traiter_groupe( | ||
| 273 | 290 | return |
| 274 | 291 | |
| 275 | 292 | publier_localement(chemins, local_tmp, dossier_local) |
| 293 | + enregistrer_import(conn, {f.checksum for f in groupe.fichiers}, dossier_local) | |
| 276 | 294 | print(f"Copie de travail locale prête : {dossier_local}") |
| 277 | 295 | print( |
| 278 | 296 | f"Rien n'a été écrit sur l'archive : une fois votre sélection terminée, " |
| @@ -290,13 +308,18 @@ def _cmd_import(args: argparse.Namespace) -> int: | ||
| 290 | 308 | ) |
| 291 | 309 | interactif = not args.yes |
| 292 | 310 | |
| 293 | - fichiers = copier_carte(carte, local_tmp) | |
| 294 | - if not fichiers: | |
| 295 | - print("Aucun fichier nouveau à importer.") | |
| 296 | - return 0 | |
| 297 | - | |
| 298 | 311 | conn = open_context_db(contexte_db) |
| 299 | 312 | try: |
| 313 | + fichiers = copier_carte(carte, local_tmp) | |
| 314 | + if not fichiers: | |
| 315 | + print("Aucun fichier nouveau à importer.") | |
| 316 | + return 0 | |
| 317 | + | |
| 318 | + doublons = rechercher_doublons(conn, {f.checksum for f in fichiers}) | |
| 319 | + if doublons and not _confirmer_malgre_doublons(doublons, interactif=interactif): | |
| 320 | + print("Import annulé (doublons détectés).") | |
| 321 | + return 0 | |
| 322 | + | |
| 300 | 323 | a_etiqueter = resoudre_collisions_boitiers(fichiers, conn=conn) |
| 301 | 324 | _resoudre_etiquetage_manuel(a_etiqueter, conn=conn, interactif=interactif) |
| 302 | 325 | |
| @@ -31,6 +31,7 @@ from regine_core.import_carte.groupage import ( | |||
| 31 | detacher_jours, | 31 | detacher_jours, |
| 32 | jours_candidats_au_detachement, | 32 | jours_candidats_au_detachement, |
| 33 | ) | 33 | ) |
| 34 | +from regine_core.import_carte.historique import enregistrer_import, rechercher_doublons | ||
| 34 | from regine_core.import_carte.identifiant import attribuer_identifiants | 35 | from regine_core.import_carte.identifiant import attribuer_identifiants |
| 35 | from regine_core.import_carte.nommage import ( | 36 | from regine_core.import_carte.nommage import ( |
| 36 | construire_nom_dossier, | 37 | construire_nom_dossier, |
| @@ -60,6 +61,22 @@ def _proposer_detachement(groupes: list[GroupeImport], interactif: bool) -> list | |||
| 60 | return groupes | 61 | return groupes |
| 61 | 62 | ||
| 62 | 63 | ||
| 64 | +def _confirmer_malgre_doublons(doublons: dict[str, str], *, interactif: bool) -> bool: | ||
| 65 | + """Avertit l'utilisateur que certains fichiers de la carte ont déjà été | ||
| 66 | + importés précédemment, ailleurs (FR-004 révisé) — ne bloque jamais | ||
| 67 | + silencieusement, laisse l'utilisateur décider (Principe V). En mode non | ||
| 68 | + interactif (`--yes`), n'interrompt pas l'import (le doublon n'est pas | ||
| 69 | + destructif, seulement redondant) mais reste visible sur stdout.""" | ||
| 70 | + dossiers = sorted(set(doublons.values())) | ||
| 71 | + print(f"{len(doublons)} fichier(s) de cette carte ont déjà été importés précédemment :") | ||
| 72 | + for dossier in dossiers: | ||
| 73 | + print(f" - dans {dossier}") | ||
| 74 | + if not interactif: | ||
| 75 | + return True | ||
| 76 | + reponse = input("Continuer l'import malgré tout ? [o/n] ") | ||
| 77 | + return reponse.strip().lower() == "o" | ||
| 78 | + | ||
| 79 | + | ||
| 63 | def _resoudre_etiquetage_manuel( | 80 | def _resoudre_etiquetage_manuel( |
| 64 | a_etiqueter: list[list[FichierCandidat]], *, conn: sqlite3.Connection, interactif: bool | 81 | a_etiqueter: list[list[FichierCandidat]], *, conn: sqlite3.Connection, interactif: bool |
| 65 | ) -> None: | 82 | ) -> None: |
| @@ -273,6 +290,7 @@ def _traiter_groupe( | |||
| 273 | return | 290 | return |
| 274 | 291 | ||
| 275 | publier_localement(chemins, local_tmp, dossier_local) | 292 | publier_localement(chemins, local_tmp, dossier_local) |
| 293 | + enregistrer_import(conn, {f.checksum for f in groupe.fichiers}, dossier_local) | ||
| 276 | print(f"Copie de travail locale prête : {dossier_local}") | 294 | print(f"Copie de travail locale prête : {dossier_local}") |
| 277 | print( | 295 | print( |
| 278 | f"Rien n'a été écrit sur l'archive : une fois votre sélection terminée, " | 296 | f"Rien n'a été écrit sur l'archive : une fois votre sélection terminée, " |
| @@ -290,13 +308,18 @@ def _cmd_import(args: argparse.Namespace) -> int: | |||
| 290 | ) | 308 | ) |
| 291 | interactif = not args.yes | 309 | interactif = not args.yes |
| 292 | 310 | ||
| 293 | - fichiers = copier_carte(carte, local_tmp) | ||
| 294 | - if not fichiers: | ||
| 295 | - print("Aucun fichier nouveau à importer.") | ||
| 296 | - return 0 | ||
| 297 | - | ||
| 298 | conn = open_context_db(contexte_db) | 311 | conn = open_context_db(contexte_db) |
| 299 | try: | 312 | try: |
| 313 | + fichiers = copier_carte(carte, local_tmp) | ||
| 314 | + if not fichiers: | ||
| 315 | + print("Aucun fichier nouveau à importer.") | ||
| 316 | + return 0 | ||
| 317 | + | ||
| 318 | + doublons = rechercher_doublons(conn, {f.checksum for f in fichiers}) | ||
| 319 | + if doublons and not _confirmer_malgre_doublons(doublons, interactif=interactif): | ||
| 320 | + print("Import annulé (doublons détectés).") | ||
| 321 | + return 0 | ||
| 322 | + | ||
| 300 | a_etiqueter = resoudre_collisions_boitiers(fichiers, conn=conn) | 323 | a_etiqueter = resoudre_collisions_boitiers(fichiers, conn=conn) |
| 301 | _resoudre_etiquetage_manuel(a_etiqueter, conn=conn, interactif=interactif) | 324 | _resoudre_etiquetage_manuel(a_etiqueter, conn=conn, interactif=interactif) |
| 302 | 325 | ||
added
packages/regine-core/src/regine_core/import_carte/historique.py +72 -0 | new file mode 100644 | ||
| @@ -0,0 +1,72 @@ | ||
| 1 | +"""Historique persistant des sommes de contrôle déjà importées (FR-004 révisé). | |
| 2 | + | |
| 3 | +Possédé par ce module plutôt que par `regine_core.config` (même principe que | |
| 4 | +`regine_core.camera_profile.db` pour la table `boitiers`, cf. specs/002 research.md | |
| 5 | +§ 2) : table `checksums_importes` dans la base de contexte centralisée | |
| 6 | +(`regine_core.config.db`), alimentée à chaque import réussi — quel que soit le | |
| 7 | +dossier de destination — pour permettre de détecter qu'un fichier a déjà été | |
| 8 | +importé ailleurs, y compris dans une session distincte et vers un dossier | |
| 9 | +différent (écart trouvé par test manuel de l'utilisateur : importer deux fois la | |
| 10 | +même carte vers deux dossiers différents ne déclenchait aucun avertissement, cf. | |
| 11 | +Clarifications de `spec.md`). | |
| 12 | +""" | |
| 13 | + | |
| 14 | +from __future__ import annotations | |
| 15 | + | |
| 16 | +import sqlite3 | |
| 17 | +from datetime import UTC, datetime | |
| 18 | +from pathlib import Path | |
| 19 | + | |
| 20 | +_CREATE_TABLE = """ | |
| 21 | +CREATE TABLE IF NOT EXISTS checksums_importes ( | |
| 22 | + checksum TEXT PRIMARY KEY, | |
| 23 | + dossier_destination TEXT NOT NULL, | |
| 24 | + date_import TEXT NOT NULL | |
| 25 | +) | |
| 26 | +""" | |
| 27 | + | |
| 28 | + | |
| 29 | +def ensure_schema(conn: sqlite3.Connection) -> None: | |
| 30 | + """Crée la table `checksums_importes` si elle n'existe pas déjà (idempotent).""" | |
| 31 | + conn.execute(_CREATE_TABLE) | |
| 32 | + conn.commit() | |
| 33 | + | |
| 34 | + | |
| 35 | +def enregistrer_import(conn: sqlite3.Connection, checksums: set[str], dossier: Path) -> None: | |
| 36 | + """Enregistre chaque somme de contrôle d'un import réussi, avec son dossier de | |
| 37 | + destination (espace de travail local, cf. `push.publier_localement`). | |
| 38 | + | |
| 39 | + Ne DOIT être appelée qu'après une publication locale réussie — jamais avant, | |
| 40 | + pour ne pas enregistrer des fichiers dont l'import a finalement été annulé. | |
| 41 | + Idempotent (`INSERT OR REPLACE`) : un même fichier réimporté vers un nouveau | |
| 42 | + dossier met simplement à jour son dossier de référence le plus récent. | |
| 43 | + """ | |
| 44 | + ensure_schema(conn) | |
| 45 | + maintenant = datetime.now(UTC).isoformat() | |
| 46 | + for checksum in checksums: | |
| 47 | + conn.execute( | |
| 48 | + "INSERT OR REPLACE INTO checksums_importes " | |
| 49 | + "(checksum, dossier_destination, date_import) VALUES (?, ?, ?)", | |
| 50 | + (checksum, str(dossier), maintenant), | |
| 51 | + ) | |
| 52 | + conn.commit() | |
| 53 | + | |
| 54 | + | |
| 55 | +def rechercher_doublons(conn: sqlite3.Connection, checksums: set[str]) -> dict[str, str]: | |
| 56 | + """Retourne, pour chaque somme de contrôle de `checksums` déjà connue, le | |
| 57 | + dossier où elle a été importée précédemment (FR-004 révisé). | |
| 58 | + | |
| 59 | + Ne modifie jamais le comportement d'import lui-même (Principe V, l'utilisateur | |
| 60 | + décide) : signale seulement, à l'appelant (CLI/GUI) de présenter l'information | |
| 61 | + et de recueillir la décision de continuer ou non. | |
| 62 | + """ | |
| 63 | + ensure_schema(conn) | |
| 64 | + if not checksums: | |
| 65 | + return {} | |
| 66 | + marqueurs = ",".join("?" * len(checksums)) | |
| 67 | + lignes = conn.execute( | |
| 68 | + f"SELECT checksum, dossier_destination FROM checksums_importes " # noqa: S608 | |
| 69 | + f"WHERE checksum IN ({marqueurs})", | |
| 70 | + list(checksums), | |
| 71 | + ).fetchall() | |
| 72 | + return dict(lignes) | |
| new file mode 100644 | |||
| @@ -0,0 +1,72 @@ | |||
| 1 | +"""Historique persistant des sommes de contrôle déjà importées (FR-004 révisé). | ||
| 2 | + | ||
| 3 | +Possédé par ce module plutôt que par `regine_core.config` (même principe que | ||
| 4 | +`regine_core.camera_profile.db` pour la table `boitiers`, cf. specs/002 research.md | ||
| 5 | +§ 2) : table `checksums_importes` dans la base de contexte centralisée | ||
| 6 | +(`regine_core.config.db`), alimentée à chaque import réussi — quel que soit le | ||
| 7 | +dossier de destination — pour permettre de détecter qu'un fichier a déjà été | ||
| 8 | +importé ailleurs, y compris dans une session distincte et vers un dossier | ||
| 9 | +différent (écart trouvé par test manuel de l'utilisateur : importer deux fois la | ||
| 10 | +même carte vers deux dossiers différents ne déclenchait aucun avertissement, cf. | ||
| 11 | +Clarifications de `spec.md`). | ||
| 12 | +""" | ||
| 13 | + | ||
| 14 | +from __future__ import annotations | ||
| 15 | + | ||
| 16 | +import sqlite3 | ||
| 17 | +from datetime import UTC, datetime | ||
| 18 | +from pathlib import Path | ||
| 19 | + | ||
| 20 | +_CREATE_TABLE = """ | ||
| 21 | +CREATE TABLE IF NOT EXISTS checksums_importes ( | ||
| 22 | + checksum TEXT PRIMARY KEY, | ||
| 23 | + dossier_destination TEXT NOT NULL, | ||
| 24 | + date_import TEXT NOT NULL | ||
| 25 | +) | ||
| 26 | +""" | ||
| 27 | + | ||
| 28 | + | ||
| 29 | +def ensure_schema(conn: sqlite3.Connection) -> None: | ||
| 30 | + """Crée la table `checksums_importes` si elle n'existe pas déjà (idempotent).""" | ||
| 31 | + conn.execute(_CREATE_TABLE) | ||
| 32 | + conn.commit() | ||
| 33 | + | ||
| 34 | + | ||
| 35 | +def enregistrer_import(conn: sqlite3.Connection, checksums: set[str], dossier: Path) -> None: | ||
| 36 | + """Enregistre chaque somme de contrôle d'un import réussi, avec son dossier de | ||
| 37 | + destination (espace de travail local, cf. `push.publier_localement`). | ||
| 38 | + | ||
| 39 | + Ne DOIT être appelée qu'après une publication locale réussie — jamais avant, | ||
| 40 | + pour ne pas enregistrer des fichiers dont l'import a finalement été annulé. | ||
| 41 | + Idempotent (`INSERT OR REPLACE`) : un même fichier réimporté vers un nouveau | ||
| 42 | + dossier met simplement à jour son dossier de référence le plus récent. | ||
| 43 | + """ | ||
| 44 | + ensure_schema(conn) | ||
| 45 | + maintenant = datetime.now(UTC).isoformat() | ||
| 46 | + for checksum in checksums: | ||
| 47 | + conn.execute( | ||
| 48 | + "INSERT OR REPLACE INTO checksums_importes " | ||
| 49 | + "(checksum, dossier_destination, date_import) VALUES (?, ?, ?)", | ||
| 50 | + (checksum, str(dossier), maintenant), | ||
| 51 | + ) | ||
| 52 | + conn.commit() | ||
| 53 | + | ||
| 54 | + | ||
| 55 | +def rechercher_doublons(conn: sqlite3.Connection, checksums: set[str]) -> dict[str, str]: | ||
| 56 | + """Retourne, pour chaque somme de contrôle de `checksums` déjà connue, le | ||
| 57 | + dossier où elle a été importée précédemment (FR-004 révisé). | ||
| 58 | + | ||
| 59 | + Ne modifie jamais le comportement d'import lui-même (Principe V, l'utilisateur | ||
| 60 | + décide) : signale seulement, à l'appelant (CLI/GUI) de présenter l'information | ||
| 61 | + et de recueillir la décision de continuer ou non. | ||
| 62 | + """ | ||
| 63 | + ensure_schema(conn) | ||
| 64 | + if not checksums: | ||
| 65 | + return {} | ||
| 66 | + marqueurs = ",".join("?" * len(checksums)) | ||
| 67 | + lignes = conn.execute( | ||
| 68 | + f"SELECT checksum, dossier_destination FROM checksums_importes " # noqa: S608 | ||
| 69 | + f"WHERE checksum IN ({marqueurs})", | ||
| 70 | + list(checksums), | ||
| 71 | + ).fetchall() | ||
| 72 | + return dict(lignes) | ||
added
packages/regine-core/tests/unit/test_historique.py +50 -0 | new file mode 100644 | ||
| @@ -0,0 +1,50 @@ | ||
| 1 | +"""Tests de `regine_core.import_carte.historique` (FR-004 révisé).""" | |
| 2 | + | |
| 3 | +from __future__ import annotations | |
| 4 | + | |
| 5 | +from pathlib import Path | |
| 6 | + | |
| 7 | +from regine_core.config.db import open_context_db | |
| 8 | +from regine_core.import_carte.historique import enregistrer_import, rechercher_doublons | |
| 9 | + | |
| 10 | + | |
| 11 | +def test_rechercher_doublons_vide_si_rien_enregistre(tmp_path: Path) -> None: | |
| 12 | + conn = open_context_db(tmp_path / "contexte.sqlite3") | |
| 13 | + assert rechercher_doublons(conn, {"abc123"}) == {} | |
| 14 | + conn.close() | |
| 15 | + | |
| 16 | + | |
| 17 | +def test_enregistrer_puis_retrouver_un_doublon(tmp_path: Path) -> None: | |
| 18 | + conn = open_context_db(tmp_path / "contexte.sqlite3") | |
| 19 | + dossier = tmp_path / "local" / "2026" / "2026-09-01_Sortie" | |
| 20 | + | |
| 21 | + enregistrer_import(conn, {"abc123", "def456"}, dossier) | |
| 22 | + doublons = rechercher_doublons(conn, {"abc123", "zzz999"}) | |
| 23 | + | |
| 24 | + assert doublons == {"abc123": str(dossier)} | |
| 25 | + conn.close() | |
| 26 | + | |
| 27 | + | |
| 28 | +def test_enregistrer_import_est_idempotent_et_met_a_jour_le_dossier(tmp_path: Path) -> None: | |
| 29 | + """Une même somme de contrôle réimportée vers un nouveau dossier met à jour | |
| 30 | + la référence plutôt que d'échouer (INSERT OR REPLACE).""" | |
| 31 | + conn = open_context_db(tmp_path / "contexte.sqlite3") | |
| 32 | + premier_dossier = tmp_path / "local" / "premier" | |
| 33 | + second_dossier = tmp_path / "local" / "second" | |
| 34 | + | |
| 35 | + enregistrer_import(conn, {"abc123"}, premier_dossier) | |
| 36 | + enregistrer_import(conn, {"abc123"}, second_dossier) | |
| 37 | + | |
| 38 | + assert rechercher_doublons(conn, {"abc123"}) == {"abc123": str(second_dossier)} | |
| 39 | + conn.close() | |
| 40 | + | |
| 41 | + | |
| 42 | +def test_rechercher_doublons_ignore_les_checksums_inconnus(tmp_path: Path) -> None: | |
| 43 | + conn = open_context_db(tmp_path / "contexte.sqlite3") | |
| 44 | + dossier = tmp_path / "local" / "dossier" | |
| 45 | + enregistrer_import(conn, {"abc123"}, dossier) | |
| 46 | + | |
| 47 | + doublons = rechercher_doublons(conn, {"inconnu"}) | |
| 48 | + | |
| 49 | + assert doublons == {} | |
| 50 | + conn.close() | |
| new file mode 100644 | |||
| @@ -0,0 +1,50 @@ | |||
| 1 | +"""Tests de `regine_core.import_carte.historique` (FR-004 révisé).""" | ||
| 2 | + | ||
| 3 | +from __future__ import annotations | ||
| 4 | + | ||
| 5 | +from pathlib import Path | ||
| 6 | + | ||
| 7 | +from regine_core.config.db import open_context_db | ||
| 8 | +from regine_core.import_carte.historique import enregistrer_import, rechercher_doublons | ||
| 9 | + | ||
| 10 | + | ||
| 11 | +def test_rechercher_doublons_vide_si_rien_enregistre(tmp_path: Path) -> None: | ||
| 12 | + conn = open_context_db(tmp_path / "contexte.sqlite3") | ||
| 13 | + assert rechercher_doublons(conn, {"abc123"}) == {} | ||
| 14 | + conn.close() | ||
| 15 | + | ||
| 16 | + | ||
| 17 | +def test_enregistrer_puis_retrouver_un_doublon(tmp_path: Path) -> None: | ||
| 18 | + conn = open_context_db(tmp_path / "contexte.sqlite3") | ||
| 19 | + dossier = tmp_path / "local" / "2026" / "2026-09-01_Sortie" | ||
| 20 | + | ||
| 21 | + enregistrer_import(conn, {"abc123", "def456"}, dossier) | ||
| 22 | + doublons = rechercher_doublons(conn, {"abc123", "zzz999"}) | ||
| 23 | + | ||
| 24 | + assert doublons == {"abc123": str(dossier)} | ||
| 25 | + conn.close() | ||
| 26 | + | ||
| 27 | + | ||
| 28 | +def test_enregistrer_import_est_idempotent_et_met_a_jour_le_dossier(tmp_path: Path) -> None: | ||
| 29 | + """Une même somme de contrôle réimportée vers un nouveau dossier met à jour | ||
| 30 | + la référence plutôt que d'échouer (INSERT OR REPLACE).""" | ||
| 31 | + conn = open_context_db(tmp_path / "contexte.sqlite3") | ||
| 32 | + premier_dossier = tmp_path / "local" / "premier" | ||
| 33 | + second_dossier = tmp_path / "local" / "second" | ||
| 34 | + | ||
| 35 | + enregistrer_import(conn, {"abc123"}, premier_dossier) | ||
| 36 | + enregistrer_import(conn, {"abc123"}, second_dossier) | ||
| 37 | + | ||
| 38 | + assert rechercher_doublons(conn, {"abc123"}) == {"abc123": str(second_dossier)} | ||
| 39 | + conn.close() | ||
| 40 | + | ||
| 41 | + | ||
| 42 | +def test_rechercher_doublons_ignore_les_checksums_inconnus(tmp_path: Path) -> None: | ||
| 43 | + conn = open_context_db(tmp_path / "contexte.sqlite3") | ||
| 44 | + dossier = tmp_path / "local" / "dossier" | ||
| 45 | + enregistrer_import(conn, {"abc123"}, dossier) | ||
| 46 | + | ||
| 47 | + doublons = rechercher_doublons(conn, {"inconnu"}) | ||
| 48 | + | ||
| 49 | + assert doublons == {} | ||
| 50 | + conn.close() | ||
modified
packages/regine-gui/src/regine_gui/import_view.py +24 -0 | @@ -60,6 +60,7 @@ from regine_core.import_carte.groupage import ( | ||
| 60 | 60 | detacher_jours, |
| 61 | 61 | jours_candidats_au_detachement, |
| 62 | 62 | ) |
| 63 | +from regine_core.import_carte.historique import enregistrer_import, rechercher_doublons | |
| 63 | 64 | from regine_core.import_carte.identifiant import attribuer_identifiants |
| 64 | 65 | from regine_core.import_carte.nommage import ( |
| 65 | 66 | construire_nom_dossier, |
| @@ -137,6 +138,7 @@ class EcranImport(QMainWindow): | ||
| 137 | 138 | self._worker: _PublicationLocaleWorker | None = None |
| 138 | 139 | self._titre_en_cours: str | None = None |
| 139 | 140 | self._dossier_archive_en_cours: Path | None = None |
| 141 | + self._checksums_en_cours: set[str] = set() | |
| 140 | 142 | |
| 141 | 143 | conteneur = QWidget() |
| 142 | 144 | self.setCentralWidget(conteneur) |
| @@ -241,6 +243,12 @@ class EcranImport(QMainWindow): | ||
| 241 | 243 | self._log("Aucun fichier nouveau à importer.") |
| 242 | 244 | return |
| 243 | 245 | |
| 246 | + doublons = rechercher_doublons(conn, {f.checksum for f in fichiers}) | |
| 247 | + if doublons and not self._confirmer_malgre_doublons(doublons): | |
| 248 | + conn.close() | |
| 249 | + self._log("Import annulé (doublons détectés).") | |
| 250 | + return | |
| 251 | + | |
| 244 | 252 | a_etiqueter = resoudre_collisions_boitiers(fichiers, conn=conn) |
| 245 | 253 | if not self._resoudre_etiquetage_manuel(a_etiqueter, conn=conn): |
| 246 | 254 | conn.close() |
| @@ -266,6 +274,19 @@ class EcranImport(QMainWindow): | ||
| 266 | 274 | f"Analyse terminée : {len(fichiers)} fichier(s) nouveau(x), {len(groupes)} groupe(s)." |
| 267 | 275 | ) |
| 268 | 276 | |
| 277 | + def _confirmer_malgre_doublons(self, doublons: dict[str, str]) -> bool: | |
| 278 | + """Avertit l'utilisateur que certains fichiers de la carte ont déjà été | |
| 279 | + importés précédemment, ailleurs (FR-004 révisé) — ne bloque jamais | |
| 280 | + silencieusement, laisse l'utilisateur décider (Principe V).""" | |
| 281 | + dossiers = "\n".join(f" - {d}" for d in sorted(set(doublons.values()))) | |
| 282 | + reponse = QMessageBox.question( | |
| 283 | + self, | |
| 284 | + "Doublons détectés", | |
| 285 | + f"{len(doublons)} fichier(s) de cette carte ont déjà été importés " | |
| 286 | + f"précédemment :\n{dossiers}\n\nContinuer l'import malgré tout ?", | |
| 287 | + ) | |
| 288 | + return reponse == QMessageBox.StandardButton.Yes | |
| 289 | + | |
| 269 | 290 | def _proposer_detachement(self, groupes: list[GroupeImport]) -> list[GroupeImport]: |
| 270 | 291 | """Propose le détachement d'un jour candidat (FR-006) — jamais automatique.""" |
| 271 | 292 | if len(groupes) != 1: |
| @@ -499,6 +520,7 @@ class EcranImport(QMainWindow): | ||
| 499 | 520 | # illégal à la connexion SQLite et aux widgets (trouvé par test manuel). |
| 500 | 521 | self._titre_en_cours = titre |
| 501 | 522 | self._dossier_archive_en_cours = dossier_archive |
| 523 | + self._checksums_en_cours = {f.checksum for f in fichiers} | |
| 502 | 524 | |
| 503 | 525 | thread = QThread(self) |
| 504 | 526 | worker = _PublicationLocaleWorker(fichiers, local_tmp, dossier_local) |
| @@ -516,6 +538,8 @@ class EcranImport(QMainWindow): | ||
| 516 | 538 | |
| 517 | 539 | def _sur_publication_succes(self, dossier_local: Path) -> None: |
| 518 | 540 | 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) | |
| 519 | 543 | self._log(f"Copie de travail locale prête : {dossier_local}") |
| 520 | 544 | self._log( |
| 521 | 545 | "Rien n'a été écrit sur l'archive. Une fois votre sélection terminée, " |
| @@ -60,6 +60,7 @@ from regine_core.import_carte.groupage import ( | |||
| 60 | detacher_jours, | 60 | detacher_jours, |
| 61 | jours_candidats_au_detachement, | 61 | jours_candidats_au_detachement, |
| 62 | ) | 62 | ) |
| 63 | +from regine_core.import_carte.historique import enregistrer_import, rechercher_doublons | ||
| 63 | from regine_core.import_carte.identifiant import attribuer_identifiants | 64 | from regine_core.import_carte.identifiant import attribuer_identifiants |
| 64 | from regine_core.import_carte.nommage import ( | 65 | from regine_core.import_carte.nommage import ( |
| 65 | construire_nom_dossier, | 66 | construire_nom_dossier, |
| @@ -137,6 +138,7 @@ class EcranImport(QMainWindow): | |||
| 137 | self._worker: _PublicationLocaleWorker | None = None | 138 | self._worker: _PublicationLocaleWorker | None = None |
| 138 | self._titre_en_cours: str | None = None | 139 | self._titre_en_cours: str | None = None |
| 139 | self._dossier_archive_en_cours: Path | None = None | 140 | self._dossier_archive_en_cours: Path | None = None |
| 141 | + self._checksums_en_cours: set[str] = set() | ||
| 140 | 142 | ||
| 141 | conteneur = QWidget() | 143 | conteneur = QWidget() |
| 142 | self.setCentralWidget(conteneur) | 144 | self.setCentralWidget(conteneur) |
| @@ -241,6 +243,12 @@ class EcranImport(QMainWindow): | |||
| 241 | self._log("Aucun fichier nouveau à importer.") | 243 | self._log("Aucun fichier nouveau à importer.") |
| 242 | return | 244 | return |
| 243 | 245 | ||
| 246 | + doublons = rechercher_doublons(conn, {f.checksum for f in fichiers}) | ||
| 247 | + if doublons and not self._confirmer_malgre_doublons(doublons): | ||
| 248 | + conn.close() | ||
| 249 | + self._log("Import annulé (doublons détectés).") | ||
| 250 | + return | ||
| 251 | + | ||
| 244 | a_etiqueter = resoudre_collisions_boitiers(fichiers, conn=conn) | 252 | a_etiqueter = resoudre_collisions_boitiers(fichiers, conn=conn) |
| 245 | if not self._resoudre_etiquetage_manuel(a_etiqueter, conn=conn): | 253 | if not self._resoudre_etiquetage_manuel(a_etiqueter, conn=conn): |
| 246 | conn.close() | 254 | conn.close() |
| @@ -266,6 +274,19 @@ class EcranImport(QMainWindow): | |||
| 266 | f"Analyse terminée : {len(fichiers)} fichier(s) nouveau(x), {len(groupes)} groupe(s)." | 274 | f"Analyse terminée : {len(fichiers)} fichier(s) nouveau(x), {len(groupes)} groupe(s)." |
| 267 | ) | 275 | ) |
| 268 | 276 | ||
| 277 | + def _confirmer_malgre_doublons(self, doublons: dict[str, str]) -> bool: | ||
| 278 | + """Avertit l'utilisateur que certains fichiers de la carte ont déjà été | ||
| 279 | + importés précédemment, ailleurs (FR-004 révisé) — ne bloque jamais | ||
| 280 | + silencieusement, laisse l'utilisateur décider (Principe V).""" | ||
| 281 | + dossiers = "\n".join(f" - {d}" for d in sorted(set(doublons.values()))) | ||
| 282 | + reponse = QMessageBox.question( | ||
| 283 | + self, | ||
| 284 | + "Doublons détectés", | ||
| 285 | + f"{len(doublons)} fichier(s) de cette carte ont déjà été importés " | ||
| 286 | + f"précédemment :\n{dossiers}\n\nContinuer l'import malgré tout ?", | ||
| 287 | + ) | ||
| 288 | + return reponse == QMessageBox.StandardButton.Yes | ||
| 289 | + | ||
| 269 | def _proposer_detachement(self, groupes: list[GroupeImport]) -> list[GroupeImport]: | 290 | def _proposer_detachement(self, groupes: list[GroupeImport]) -> list[GroupeImport]: |
| 270 | """Propose le détachement d'un jour candidat (FR-006) — jamais automatique.""" | 291 | """Propose le détachement d'un jour candidat (FR-006) — jamais automatique.""" |
| 271 | if len(groupes) != 1: | 292 | if len(groupes) != 1: |
| @@ -499,6 +520,7 @@ class EcranImport(QMainWindow): | |||
| 499 | # illégal à la connexion SQLite et aux widgets (trouvé par test manuel). | 520 | # illégal à la connexion SQLite et aux widgets (trouvé par test manuel). |
| 500 | self._titre_en_cours = titre | 521 | self._titre_en_cours = titre |
| 501 | self._dossier_archive_en_cours = dossier_archive | 522 | self._dossier_archive_en_cours = dossier_archive |
| 523 | + self._checksums_en_cours = {f.checksum for f in fichiers} | ||
| 502 | 524 | ||
| 503 | thread = QThread(self) | 525 | thread = QThread(self) |
| 504 | worker = _PublicationLocaleWorker(fichiers, local_tmp, dossier_local) | 526 | worker = _PublicationLocaleWorker(fichiers, local_tmp, dossier_local) |
| @@ -516,6 +538,8 @@ class EcranImport(QMainWindow): | |||
| 516 | 538 | ||
| 517 | def _sur_publication_succes(self, dossier_local: Path) -> None: | 539 | def _sur_publication_succes(self, dossier_local: Path) -> None: |
| 518 | dossier_archive = self._dossier_archive_en_cours | 540 | 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) | ||
| 519 | self._log(f"Copie de travail locale prête : {dossier_local}") | 543 | self._log(f"Copie de travail locale prête : {dossier_local}") |
| 520 | self._log( | 544 | self._log( |
| 521 | "Rien n'a été écrit sur l'archive. Une fois votre sélection terminée, " | 545 | "Rien n'a été écrit sur l'archive. Une fois votre sélection terminée, " |
modified
specs/001-import-photos/checklists/requirements.md +1 -0 | @@ -55,6 +55,7 @@ | ||
| 55 | 55 | (répertoire racine affiché dans le résumé de confirmation), Key Entities (Dossier / Dossier |
| 56 | 56 | parent), Acceptance Scenarios de User Story 3 (héritage du répertoire racine par les |
| 57 | 57 | sous-dossiers) et Assumptions (dépendance déclarée vers specs/004) mis à jour ; nouveau SC-008. |
| 58 | +- **Révision du 2026-09-22 (bis)** : troisième écart trouvé, cette fois par test manuel de l'utilisateur (import de la même carte deux fois vers deux dossiers différents, sans aucun avertissement). Cause racine : FR-004 existait déjà en spec et en code (`copie.copier_carte`, paramètre `checksums_deja_importes`) mais n'était jamais alimenté par aucun appelant réel — la déduplication ne fonctionnait dans aucun cas réel. Corrigé par un historique persistant des imports (nouveau module `historique.py`, table `checksums_importes` dans la base de contexte centralisée) consulté avant chaque import et alimenté après chaque publication locale réussie. FR-004 réécrite, nouvelle entité « Historique des imports », nouveau SC-011, Edge Case et Acceptance Scenario (US1 #7) mis à jour. Tous les critères de la checklist restent satisfaits. | |
| 58 | 59 | - **Révision du 2026-09-22 (clarifications)** : deux écarts trouvés par test manuel de la GUI |
| 59 | 60 | (`specs/006-gui-tri-consultation` User Story 3) — (1) l'import poussait automatiquement vers |
| 60 | 61 | l'archive NAS sans laisser au photographe l'occasion de relire/retoucher sa copie locale |
| @@ -55,6 +55,7 @@ | |||
| 55 | (répertoire racine affiché dans le résumé de confirmation), Key Entities (Dossier / Dossier | 55 | (répertoire racine affiché dans le résumé de confirmation), Key Entities (Dossier / Dossier |
| 56 | parent), Acceptance Scenarios de User Story 3 (héritage du répertoire racine par les | 56 | parent), Acceptance Scenarios de User Story 3 (héritage du répertoire racine par les |
| 57 | sous-dossiers) et Assumptions (dépendance déclarée vers specs/004) mis à jour ; nouveau SC-008. | 57 | sous-dossiers) et Assumptions (dépendance déclarée vers specs/004) mis à jour ; nouveau SC-008. |
| 58 | +- **Révision du 2026-09-22 (bis)** : troisième écart trouvé, cette fois par test manuel de l'utilisateur (import de la même carte deux fois vers deux dossiers différents, sans aucun avertissement). Cause racine : FR-004 existait déjà en spec et en code (`copie.copier_carte`, paramètre `checksums_deja_importes`) mais n'était jamais alimenté par aucun appelant réel — la déduplication ne fonctionnait dans aucun cas réel. Corrigé par un historique persistant des imports (nouveau module `historique.py`, table `checksums_importes` dans la base de contexte centralisée) consulté avant chaque import et alimenté après chaque publication locale réussie. FR-004 réécrite, nouvelle entité « Historique des imports », nouveau SC-011, Edge Case et Acceptance Scenario (US1 #7) mis à jour. Tous les critères de la checklist restent satisfaits. | ||
| 58 | - **Révision du 2026-09-22 (clarifications)** : deux écarts trouvés par test manuel de la GUI | 59 | - **Révision du 2026-09-22 (clarifications)** : deux écarts trouvés par test manuel de la GUI |
| 59 | (`specs/006-gui-tri-consultation` User Story 3) — (1) l'import poussait automatiquement vers | 60 | (`specs/006-gui-tri-consultation` User Story 3) — (1) l'import poussait automatiquement vers |
| 60 | l'archive NAS sans laisser au photographe l'occasion de relire/retoucher sa copie locale | 61 | l'archive NAS sans laisser au photographe l'occasion de relire/retoucher sa copie locale |
modified
specs/001-import-photos/contracts/cli-import.md +13 -6 | @@ -11,12 +11,13 @@ regine import /Volumes/CARTE_SD [--titre TEXTE] [--destination nouveau|sous-doss | ||
| 11 | 11 | |
| 12 | 12 | **Déroulé (mode interactif, sans flags optionnels)** : |
| 13 | 13 | 1. Copie vérifiée (FR-001) — barre de progression sur stdout, erreurs de lecture sur stderr (Edge Case fichier corrompu). |
| 14 | -2. Analyse des dates et proposition de groupe(s) (FR-002/003/005/006) — affiche la répartition jour par jour, invite à confirmer ou détacher des jours. | |
| 15 | -3. Pour chaque groupe : demande la destination (FR-007), la catégorie/année si `nouveau_dossier`/`nouveau_parent` (avec suggestions `regine_core.config.categories.list_known_categories`/`suggest_categories`), puis le titre (FR-010). | |
| 16 | -4. Si une collision de nom d'origine est détectée (FR-015) : résolution automatique silencieuse, ou question d'étiquetage manuel uniquement si `regine_core.camera_profile.resolve_collision` renvoie un groupe non résolu. | |
| 17 | -5. Renommage local (FR-013/014), classement par dossier de format `raw/`/`jpeg/`/`tiff/`... (FR-020, `import_carte.classement.classer_par_format`) et attribution de l'identifiant pérenne (FR-017). | |
| 18 | -6. Résumé complet par groupe (fichiers, taille, dossier de destination locale avec répertoire racine) et confirmation explicite avant toute écriture locale (FR-018) — **aucune écriture sur l'archive NAS à ce stade** (cf. Clarifications de `spec.md`, révision 2026-09-22). | |
| 19 | -7. Transfert final vérifié vers l'espace de travail local définitif (FR-019, `push.publier_localement`) — prêt pour un tri/une retouche immédiate. Rien n'est archivé : l'étape d'archivage est distincte et volontaire, cf. Sorties ci-dessous. | |
| 14 | +2. **Détection de doublons (FR-004 révisé, 2026-09-22)** : les sommes de contrôle des fichiers copiés sont comparées à l'historique persistant des imports déjà réalisés (`historique.rechercher_doublons`). En cas de correspondance, la commande affiche les fichiers concernés et le dossier où ils ont déjà été importés, puis demande confirmation avant de poursuivre (sauf `--yes`, qui continue sans bloquer mais affiche quand même l'avertissement). | |
| 15 | +3. Analyse des dates et proposition de groupe(s) (FR-002/003/005/006) — affiche la répartition jour par jour, invite à confirmer ou détacher des jours. | |
| 16 | +4. Pour chaque groupe : demande la destination (FR-007), la catégorie/année si `nouveau_dossier`/`nouveau_parent` (avec suggestions `regine_core.config.categories.list_known_categories`/`suggest_categories`), puis le titre (FR-010). | |
| 17 | +5. Si une collision de nom d'origine est détectée (FR-015) : résolution automatique silencieuse, ou question d'étiquetage manuel uniquement si `regine_core.camera_profile.resolve_collision` renvoie un groupe non résolu. | |
| 18 | +6. Renommage local (FR-013/014), classement par dossier de format `raw/`/`jpeg/`/`tiff/`... (FR-020, `import_carte.classement.classer_par_format`) et attribution de l'identifiant pérenne (FR-017). | |
| 19 | +7. Résumé complet par groupe (fichiers, taille, dossier de destination locale avec répertoire racine) et confirmation explicite avant toute écriture locale (FR-018) — **aucune écriture sur l'archive NAS à ce stade** (cf. Clarifications de `spec.md`, révision 2026-09-22). | |
| 20 | +8. Transfert final vérifié vers l'espace de travail local définitif (FR-019, `push.publier_localement`) — prêt pour un tri/une retouche immédiate. Rien n'est archivé : l'étape d'archivage est distincte et volontaire, cf. Sorties ci-dessous. La somme de contrôle de chaque fichier publié est alors enregistrée dans l'historique des imports (`historique.enregistrer_import`), pour la détection de doublon d'un futur import. | |
| 20 | 21 | |
| 21 | 22 | **Sorties** : |
| 22 | 23 | - Succès : récapitulatif des dossiers de travail locaux (classés par format) sur stdout, code `0`, accompagné de la commande à lancer plus tard pour archiver (`regine reconcile <dossier_archive> --local-dest <dossier_local>`) une fois la sélection terminée — ce dossier n'existe pas encore sur l'archive et n'est ni verrouillé ni référencé par aucun manifeste tant que cette commande n'a pas été lancée. |
| @@ -24,6 +25,7 @@ regine import /Volumes/CARTE_SD [--titre TEXTE] [--destination nouveau|sous-doss | ||
| 24 | 25 | - Espace disque insuffisant (Edge Case) : message clair avant toute copie, code non-zéro. |
| 25 | 26 | - Collision de nom de dossier (FR-012) : proposition de suffixe ou demande de confirmation sur stdout ; sans `--yes`, attend une réponse interactive. |
| 26 | 27 | - **Partage NAS non monté ou inaccessible** : sans conséquence sur l'import (Edge Case, `spec.md`) — `--archive-root` n'a besoin d'être qu'un chemin valide, pas un chemin actuellement accessible, puisqu'aucune écriture n'y a lieu. |
| 28 | +- **Doublon détecté (FR-004 révisé)** : avertissement sur stdout listant les fichiers concernés et leur dossier d'import précédent ; en mode interactif, une réponse négative annule l'import (code `0`, aucune écriture) avant même l'analyse des groupes. | |
| 27 | 29 | - **`--destination fusion:ID` ciblant un dossier présent uniquement dans l'archive (pas en local)** : la commande effectue d'abord un checkout de ce dossier (`regine_core.archive.checkout`, `specs/005-checkout-reconciliation`) avant d'y intégrer les nouveaux fichiers — ce cas particulier lit bien l'archive dès l'import (le dossier ciblé y existe déjà), contrairement au cas général ci-dessus. Si ce dossier est déjà verrouillé par un autre checkout en cours, la commande échoue explicitement (cf. `specs/005-checkout-reconciliation/contracts/cli-checkout-reconcile.md`). |
| 28 | 30 | |
| 29 | 31 | ## Notes de scriptabilité |
| @@ -48,3 +50,8 @@ regine import /Volumes/CARTE_SD [--titre TEXTE] [--destination nouveau|sous-doss | ||
| 48 | 50 | - **Écart trouvé par test manuel de la GUI (utilisateur)** : l'archivage immédiat en fin d'import ne laissait au photographe aucune occasion de relire/retoucher sa copie de travail avant l'écriture sur le NAS, et les fichiers importés n'étaient jamais classés dans un dossier de format (`raw/`/`jpeg/`/`tiff/`), contrairement à ce que `docs/archivage-photo-elements-cles.md` section 10 et l'écran de tri (`specs/006-gui-tri-consultation`) supposent déjà en place. Cf. Clarifications de `spec.md` pour le détail des deux décisions. |
| 49 | 51 | - `regine import` se termine désormais par `push.publier_localement` (renommée depuis `archiver`, plus jamais d'écriture NAS) précédée de `classement.classer_par_format` (nouveau, FR-020) — `push.publier_copie_locale` (manifeste + verrou immédiats) est supprimée : ce rôle est repris intégralement par `regine reconcile`, appelé plus tard et séparément par le photographe. Un dossier jamais encore archivé produit un manifeste vide au premier `regine reconcile` : chaque fichier local y est alors classé "nouveau", archivable sur confirmation explicite — comportement déjà existant de `regine_core.archive.reconciliation`, réutilisé sans modification. |
| 50 | 52 | - Cas d'une `fusion:CHEMIN` vers un dossier déjà archivé (checkout automatique préalable) : ce cas continue d'accéder au NAS dès l'import (lecture du dossier ciblé, déjà présent là-bas) — seule différence par rapport au cas général, documentée dans Sorties ci-dessus. La publication locale des nouveaux fichiers dans ce cas n'écrase jamais un fichier local préexistant issu d'un import précédent non encore réconcilié. |
| 53 | + | |
| 54 | +## Notes d'implémentation (révision 2026-09-22 bis — détection de doublons, FR-004) | |
| 55 | + | |
| 56 | +- **Écart trouvé par test manuel de l'utilisateur** : importer deux fois la même carte vers deux dossiers différents n'affichait aucun avertissement. Cause racine : le paramètre `checksums_deja_importes` de `copie.copier_carte` (déjà en place et testé isolément) n'était jamais renseigné par aucun appelant réel (`regine_cli.import_cmd` ni `regine_gui.import_view` ne le passaient) — la déduplication ne fonctionnait donc dans aucun cas réel, y compris le cas initialement visé par FR-004 (carte non effacée entre deux imports). | |
| 57 | +- Corrigé par un nouveau module `historique.py` (persistance dans la base de contexte centralisée, `specs/003`, table `checksums_importes`) : `regine import` interroge `rechercher_doublons` juste après `copier_carte`, avant toute autre étape, et appelle `enregistrer_import` juste après chaque `publier_localement` réussi. `copier_carte`/`checksums_deja_importes` restent inchangés (mécanisme générique toujours disponible), la nouvelle détection étant une couche additive dans l'orchestration CLI/GUI plutôt qu'une modification de `copier_carte` elle-même. | |
| @@ -11,12 +11,13 @@ regine import /Volumes/CARTE_SD [--titre TEXTE] [--destination nouveau|sous-doss | |||
| 11 | 11 | ||
| 12 | **Déroulé (mode interactif, sans flags optionnels)** : | 12 | **Déroulé (mode interactif, sans flags optionnels)** : |
| 13 | 1. Copie vérifiée (FR-001) — barre de progression sur stdout, erreurs de lecture sur stderr (Edge Case fichier corrompu). | 13 | 1. Copie vérifiée (FR-001) — barre de progression sur stdout, erreurs de lecture sur stderr (Edge Case fichier corrompu). |
| 14 | -2. Analyse des dates et proposition de groupe(s) (FR-002/003/005/006) — affiche la répartition jour par jour, invite à confirmer ou détacher des jours. | 14 | +2. **Détection de doublons (FR-004 révisé, 2026-09-22)** : les sommes de contrôle des fichiers copiés sont comparées à l'historique persistant des imports déjà réalisés (`historique.rechercher_doublons`). En cas de correspondance, la commande affiche les fichiers concernés et le dossier où ils ont déjà été importés, puis demande confirmation avant de poursuivre (sauf `--yes`, qui continue sans bloquer mais affiche quand même l'avertissement). |
| 15 | -3. Pour chaque groupe : demande la destination (FR-007), la catégorie/année si `nouveau_dossier`/`nouveau_parent` (avec suggestions `regine_core.config.categories.list_known_categories`/`suggest_categories`), puis le titre (FR-010). | 15 | +3. Analyse des dates et proposition de groupe(s) (FR-002/003/005/006) — affiche la répartition jour par jour, invite à confirmer ou détacher des jours. |
| 16 | -4. Si une collision de nom d'origine est détectée (FR-015) : résolution automatique silencieuse, ou question d'étiquetage manuel uniquement si `regine_core.camera_profile.resolve_collision` renvoie un groupe non résolu. | 16 | +4. Pour chaque groupe : demande la destination (FR-007), la catégorie/année si `nouveau_dossier`/`nouveau_parent` (avec suggestions `regine_core.config.categories.list_known_categories`/`suggest_categories`), puis le titre (FR-010). |
| 17 | -5. Renommage local (FR-013/014), classement par dossier de format `raw/`/`jpeg/`/`tiff/`... (FR-020, `import_carte.classement.classer_par_format`) et attribution de l'identifiant pérenne (FR-017). | 17 | +5. Si une collision de nom d'origine est détectée (FR-015) : résolution automatique silencieuse, ou question d'étiquetage manuel uniquement si `regine_core.camera_profile.resolve_collision` renvoie un groupe non résolu. |
| 18 | -6. Résumé complet par groupe (fichiers, taille, dossier de destination locale avec répertoire racine) et confirmation explicite avant toute écriture locale (FR-018) — **aucune écriture sur l'archive NAS à ce stade** (cf. Clarifications de `spec.md`, révision 2026-09-22). | 18 | +6. Renommage local (FR-013/014), classement par dossier de format `raw/`/`jpeg/`/`tiff/`... (FR-020, `import_carte.classement.classer_par_format`) et attribution de l'identifiant pérenne (FR-017). |
| 19 | -7. Transfert final vérifié vers l'espace de travail local définitif (FR-019, `push.publier_localement`) — prêt pour un tri/une retouche immédiate. Rien n'est archivé : l'étape d'archivage est distincte et volontaire, cf. Sorties ci-dessous. | 19 | +7. Résumé complet par groupe (fichiers, taille, dossier de destination locale avec répertoire racine) et confirmation explicite avant toute écriture locale (FR-018) — **aucune écriture sur l'archive NAS à ce stade** (cf. Clarifications de `spec.md`, révision 2026-09-22). |
| 20 | +8. Transfert final vérifié vers l'espace de travail local définitif (FR-019, `push.publier_localement`) — prêt pour un tri/une retouche immédiate. Rien n'est archivé : l'étape d'archivage est distincte et volontaire, cf. Sorties ci-dessous. La somme de contrôle de chaque fichier publié est alors enregistrée dans l'historique des imports (`historique.enregistrer_import`), pour la détection de doublon d'un futur import. | ||
| 20 | 21 | ||
| 21 | **Sorties** : | 22 | **Sorties** : |
| 22 | - Succès : récapitulatif des dossiers de travail locaux (classés par format) sur stdout, code `0`, accompagné de la commande à lancer plus tard pour archiver (`regine reconcile <dossier_archive> --local-dest <dossier_local>`) une fois la sélection terminée — ce dossier n'existe pas encore sur l'archive et n'est ni verrouillé ni référencé par aucun manifeste tant que cette commande n'a pas été lancée. | 23 | - Succès : récapitulatif des dossiers de travail locaux (classés par format) sur stdout, code `0`, accompagné de la commande à lancer plus tard pour archiver (`regine reconcile <dossier_archive> --local-dest <dossier_local>`) une fois la sélection terminée — ce dossier n'existe pas encore sur l'archive et n'est ni verrouillé ni référencé par aucun manifeste tant que cette commande n'a pas été lancée. |
| @@ -24,6 +25,7 @@ regine import /Volumes/CARTE_SD [--titre TEXTE] [--destination nouveau|sous-doss | |||
| 24 | - Espace disque insuffisant (Edge Case) : message clair avant toute copie, code non-zéro. | 25 | - Espace disque insuffisant (Edge Case) : message clair avant toute copie, code non-zéro. |
| 25 | - Collision de nom de dossier (FR-012) : proposition de suffixe ou demande de confirmation sur stdout ; sans `--yes`, attend une réponse interactive. | 26 | - Collision de nom de dossier (FR-012) : proposition de suffixe ou demande de confirmation sur stdout ; sans `--yes`, attend une réponse interactive. |
| 26 | - **Partage NAS non monté ou inaccessible** : sans conséquence sur l'import (Edge Case, `spec.md`) — `--archive-root` n'a besoin d'être qu'un chemin valide, pas un chemin actuellement accessible, puisqu'aucune écriture n'y a lieu. | 27 | - **Partage NAS non monté ou inaccessible** : sans conséquence sur l'import (Edge Case, `spec.md`) — `--archive-root` n'a besoin d'être qu'un chemin valide, pas un chemin actuellement accessible, puisqu'aucune écriture n'y a lieu. |
| 28 | +- **Doublon détecté (FR-004 révisé)** : avertissement sur stdout listant les fichiers concernés et leur dossier d'import précédent ; en mode interactif, une réponse négative annule l'import (code `0`, aucune écriture) avant même l'analyse des groupes. | ||
| 27 | - **`--destination fusion:ID` ciblant un dossier présent uniquement dans l'archive (pas en local)** : la commande effectue d'abord un checkout de ce dossier (`regine_core.archive.checkout`, `specs/005-checkout-reconciliation`) avant d'y intégrer les nouveaux fichiers — ce cas particulier lit bien l'archive dès l'import (le dossier ciblé y existe déjà), contrairement au cas général ci-dessus. Si ce dossier est déjà verrouillé par un autre checkout en cours, la commande échoue explicitement (cf. `specs/005-checkout-reconciliation/contracts/cli-checkout-reconcile.md`). | 29 | - **`--destination fusion:ID` ciblant un dossier présent uniquement dans l'archive (pas en local)** : la commande effectue d'abord un checkout de ce dossier (`regine_core.archive.checkout`, `specs/005-checkout-reconciliation`) avant d'y intégrer les nouveaux fichiers — ce cas particulier lit bien l'archive dès l'import (le dossier ciblé y existe déjà), contrairement au cas général ci-dessus. Si ce dossier est déjà verrouillé par un autre checkout en cours, la commande échoue explicitement (cf. `specs/005-checkout-reconciliation/contracts/cli-checkout-reconcile.md`). |
| 28 | 30 | ||
| 29 | ## Notes de scriptabilité | 31 | ## Notes de scriptabilité |
| @@ -48,3 +50,8 @@ regine import /Volumes/CARTE_SD [--titre TEXTE] [--destination nouveau|sous-doss | |||
| 48 | - **Écart trouvé par test manuel de la GUI (utilisateur)** : l'archivage immédiat en fin d'import ne laissait au photographe aucune occasion de relire/retoucher sa copie de travail avant l'écriture sur le NAS, et les fichiers importés n'étaient jamais classés dans un dossier de format (`raw/`/`jpeg/`/`tiff/`), contrairement à ce que `docs/archivage-photo-elements-cles.md` section 10 et l'écran de tri (`specs/006-gui-tri-consultation`) supposent déjà en place. Cf. Clarifications de `spec.md` pour le détail des deux décisions. | 50 | - **Écart trouvé par test manuel de la GUI (utilisateur)** : l'archivage immédiat en fin d'import ne laissait au photographe aucune occasion de relire/retoucher sa copie de travail avant l'écriture sur le NAS, et les fichiers importés n'étaient jamais classés dans un dossier de format (`raw/`/`jpeg/`/`tiff/`), contrairement à ce que `docs/archivage-photo-elements-cles.md` section 10 et l'écran de tri (`specs/006-gui-tri-consultation`) supposent déjà en place. Cf. Clarifications de `spec.md` pour le détail des deux décisions. |
| 49 | - `regine import` se termine désormais par `push.publier_localement` (renommée depuis `archiver`, plus jamais d'écriture NAS) précédée de `classement.classer_par_format` (nouveau, FR-020) — `push.publier_copie_locale` (manifeste + verrou immédiats) est supprimée : ce rôle est repris intégralement par `regine reconcile`, appelé plus tard et séparément par le photographe. Un dossier jamais encore archivé produit un manifeste vide au premier `regine reconcile` : chaque fichier local y est alors classé "nouveau", archivable sur confirmation explicite — comportement déjà existant de `regine_core.archive.reconciliation`, réutilisé sans modification. | 51 | - `regine import` se termine désormais par `push.publier_localement` (renommée depuis `archiver`, plus jamais d'écriture NAS) précédée de `classement.classer_par_format` (nouveau, FR-020) — `push.publier_copie_locale` (manifeste + verrou immédiats) est supprimée : ce rôle est repris intégralement par `regine reconcile`, appelé plus tard et séparément par le photographe. Un dossier jamais encore archivé produit un manifeste vide au premier `regine reconcile` : chaque fichier local y est alors classé "nouveau", archivable sur confirmation explicite — comportement déjà existant de `regine_core.archive.reconciliation`, réutilisé sans modification. |
| 50 | - Cas d'une `fusion:CHEMIN` vers un dossier déjà archivé (checkout automatique préalable) : ce cas continue d'accéder au NAS dès l'import (lecture du dossier ciblé, déjà présent là-bas) — seule différence par rapport au cas général, documentée dans Sorties ci-dessus. La publication locale des nouveaux fichiers dans ce cas n'écrase jamais un fichier local préexistant issu d'un import précédent non encore réconcilié. | 52 | - Cas d'une `fusion:CHEMIN` vers un dossier déjà archivé (checkout automatique préalable) : ce cas continue d'accéder au NAS dès l'import (lecture du dossier ciblé, déjà présent là-bas) — seule différence par rapport au cas général, documentée dans Sorties ci-dessus. La publication locale des nouveaux fichiers dans ce cas n'écrase jamais un fichier local préexistant issu d'un import précédent non encore réconcilié. |
| 53 | + | ||
| 54 | +## Notes d'implémentation (révision 2026-09-22 bis — détection de doublons, FR-004) | ||
| 55 | + | ||
| 56 | +- **Écart trouvé par test manuel de l'utilisateur** : importer deux fois la même carte vers deux dossiers différents n'affichait aucun avertissement. Cause racine : le paramètre `checksums_deja_importes` de `copie.copier_carte` (déjà en place et testé isolément) n'était jamais renseigné par aucun appelant réel (`regine_cli.import_cmd` ni `regine_gui.import_view` ne le passaient) — la déduplication ne fonctionnait donc dans aucun cas réel, y compris le cas initialement visé par FR-004 (carte non effacée entre deux imports). | ||
| 57 | +- Corrigé par un nouveau module `historique.py` (persistance dans la base de contexte centralisée, `specs/003`, table `checksums_importes`) : `regine import` interroge `rechercher_doublons` juste après `copier_carte`, avant toute autre étape, et appelle `enregistrer_import` juste après chaque `publier_localement` réussi. `copier_carte`/`checksums_deja_importes` restent inchangés (mécanisme générique toujours disponible), la nouvelle détection étant une couche additive dans l'orchestration CLI/GUI plutôt qu'une modification de `copier_carte` elle-même. | ||
modified
specs/001-import-photos/contracts/regine-core-api.md +4 -0 | @@ -38,6 +38,10 @@ Construisent respectivement le nom d'un dossier simple (`AAAA-MM-JJ_Titre` ou pl | ||
| 38 | 38 | |
| 39 | 39 | Renomme chaque fichier maître (`date_titre_nomOrigine.ext`, FR-013) et ses fichiers associés de façon synchronisée, par regroupement sur le nom de base avant le premier point (FR-014). |
| 40 | 40 | |
| 41 | +## `historique.rechercher_doublons(conn: sqlite3.Connection, checksums: set[str]) -> dict[str, str]` / `historique.enregistrer_import(conn, checksums: set[str], dossier: Path) -> None` | |
| 42 | + | |
| 43 | +**Nouveau module, révision 2026-09-22 (FR-004 révisé)** : `rechercher_doublons` retourne, pour chaque somme de contrôle déjà connue parmi `checksums`, le dossier de l'espace de travail local où elle a été importée précédemment (table `checksums_importes` de la base de contexte centralisée, `regine_core.config.db`, `specs/003` — même schéma d'implantation que `camera_profile.db` pour la table `boitiers`). Ne modifie jamais le comportement d'import : à l'appelant (CLI/GUI) d'informer l'utilisateur et de recueillir sa décision. `enregistrer_import` persiste les sommes de contrôle d'un import réussi avec leur dossier de destination — à appeler uniquement après une publication locale réussie (`push.publier_localement`), jamais avant, pour ne pas enregistrer un import finalement annulé. | |
| 44 | + | |
| 41 | 45 | ## `identifiant.attribuer_identifiants(fichiers_maitres: list[Path]) -> dict[Path, str]` |
| 42 | 46 | |
| 43 | 47 | Génère un UUID par fichier maître et l'écrit dans `xmpMM:DocumentID` via `exiftool` (FR-017, cf. research.md § 3). Idempotent : ne réécrit pas un identifiant déjà présent. |
| @@ -38,6 +38,10 @@ Construisent respectivement le nom d'un dossier simple (`AAAA-MM-JJ_Titre` ou pl | |||
| 38 | 38 | ||
| 39 | Renomme chaque fichier maître (`date_titre_nomOrigine.ext`, FR-013) et ses fichiers associés de façon synchronisée, par regroupement sur le nom de base avant le premier point (FR-014). | 39 | Renomme chaque fichier maître (`date_titre_nomOrigine.ext`, FR-013) et ses fichiers associés de façon synchronisée, par regroupement sur le nom de base avant le premier point (FR-014). |
| 40 | 40 | ||
| 41 | +## `historique.rechercher_doublons(conn: sqlite3.Connection, checksums: set[str]) -> dict[str, str]` / `historique.enregistrer_import(conn, checksums: set[str], dossier: Path) -> None` | ||
| 42 | + | ||
| 43 | +**Nouveau module, révision 2026-09-22 (FR-004 révisé)** : `rechercher_doublons` retourne, pour chaque somme de contrôle déjà connue parmi `checksums`, le dossier de l'espace de travail local où elle a été importée précédemment (table `checksums_importes` de la base de contexte centralisée, `regine_core.config.db`, `specs/003` — même schéma d'implantation que `camera_profile.db` pour la table `boitiers`). Ne modifie jamais le comportement d'import : à l'appelant (CLI/GUI) d'informer l'utilisateur et de recueillir sa décision. `enregistrer_import` persiste les sommes de contrôle d'un import réussi avec leur dossier de destination — à appeler uniquement après une publication locale réussie (`push.publier_localement`), jamais avant, pour ne pas enregistrer un import finalement annulé. | ||
| 44 | + | ||
| 41 | ## `identifiant.attribuer_identifiants(fichiers_maitres: list[Path]) -> dict[Path, str]` | 45 | ## `identifiant.attribuer_identifiants(fichiers_maitres: list[Path]) -> dict[Path, str]` |
| 42 | 46 | ||
| 43 | Génère un UUID par fichier maître et l'écrit dans `xmpMM:DocumentID` via `exiftool` (FR-017, cf. research.md § 3). Idempotent : ne réécrit pas un identifiant déjà présent. | 47 | Génère un UUID par fichier maître et l'écrit dans `xmpMM:DocumentID` via `exiftool` (FR-017, cf. research.md § 3). Idempotent : ne réécrit pas un identifiant déjà présent. |
modified
specs/001-import-photos/data-model.md +14 -1 | @@ -48,6 +48,16 @@ Créé à la demande lors du classement (FR-020), pas une entité stockée — u | ||
| 48 | 48 | | `nom_final` | str | `date_titre_nomOrigine.ext` (FR-013) | |
| 49 | 49 | | `fichiers_lies` | `list[Path]` | Fichiers associés (JPEG jumeau, sidecars) renommés de façon synchronisée (FR-014) | |
| 50 | 50 | |
| 51 | +## Historique des imports (FR-004 révisé, 2026-09-22) | |
| 52 | + | |
| 53 | +Persisté dans la base de contexte centralisée (`regine_core.config.db`, `specs/003`), table `checksums_importes` — un enregistrement par somme de contrôle déjà importée, alimenté à chaque import réussi. | |
| 54 | + | |
| 55 | +| Champ | Type | Règles | | |
| 56 | +|---|---|---| | |
| 57 | +| `checksum` | str (SHA-256), clé primaire | Un enregistrement par fichier distinct déjà importé, tous imports confondus | | |
| 58 | +| `dossier_destination` | str (chemin) | Dossier de l'espace de travail local où ce fichier a été publié (`push.publier_localement`) | | |
| 59 | +| `date_import` | str (ISO 8601) | Horodatage de l'enregistrement — mis à jour si le même checksum est réimporté vers un nouveau dossier | | |
| 60 | + | |
| 51 | 61 | ## Identifiant pérenne |
| 52 | 62 | |
| 53 | 63 | | Champ | Type | Règles | |
| @@ -58,7 +68,10 @@ Créé à la demande lors du classement (FR-020), pas une entité stockée — u | ||
| 58 | 68 | ## Relations |
| 59 | 69 | |
| 60 | 70 | ```text |
| 61 | -Carte mémoire ──▶ FichierCandidat (checksum, date) ──▶ Groupe d'import (découpage jour par jour) | |
| 71 | +Carte mémoire ──▶ FichierCandidat (checksum, date) ──▶ Historique des imports (détection doublon, FR-004) | |
| 72 | + │ | |
| 73 | + ▼ | |
| 74 | + Groupe d'import (découpage jour par jour) | |
| 62 | 75 | │ |
| 63 | 76 | ▼ |
| 64 | 77 | DestinationChoisie ──▶ RootLocation (specs/004) |
| @@ -48,6 +48,16 @@ Créé à la demande lors du classement (FR-020), pas une entité stockée — u | |||
| 48 | | `nom_final` | str | `date_titre_nomOrigine.ext` (FR-013) | | 48 | | `nom_final` | str | `date_titre_nomOrigine.ext` (FR-013) | |
| 49 | | `fichiers_lies` | `list[Path]` | Fichiers associés (JPEG jumeau, sidecars) renommés de façon synchronisée (FR-014) | | 49 | | `fichiers_lies` | `list[Path]` | Fichiers associés (JPEG jumeau, sidecars) renommés de façon synchronisée (FR-014) | |
| 50 | 50 | ||
| 51 | +## Historique des imports (FR-004 révisé, 2026-09-22) | ||
| 52 | + | ||
| 53 | +Persisté dans la base de contexte centralisée (`regine_core.config.db`, `specs/003`), table `checksums_importes` — un enregistrement par somme de contrôle déjà importée, alimenté à chaque import réussi. | ||
| 54 | + | ||
| 55 | +| Champ | Type | Règles | | ||
| 56 | +|---|---|---| | ||
| 57 | +| `checksum` | str (SHA-256), clé primaire | Un enregistrement par fichier distinct déjà importé, tous imports confondus | | ||
| 58 | +| `dossier_destination` | str (chemin) | Dossier de l'espace de travail local où ce fichier a été publié (`push.publier_localement`) | | ||
| 59 | +| `date_import` | str (ISO 8601) | Horodatage de l'enregistrement — mis à jour si le même checksum est réimporté vers un nouveau dossier | | ||
| 60 | + | ||
| 51 | ## Identifiant pérenne | 61 | ## Identifiant pérenne |
| 52 | 62 | ||
| 53 | | Champ | Type | Règles | | 63 | | Champ | Type | Règles | |
| @@ -58,7 +68,10 @@ Créé à la demande lors du classement (FR-020), pas une entité stockée — u | |||
| 58 | ## Relations | 68 | ## Relations |
| 59 | 69 | ||
| 60 | ```text | 70 | ```text |
| 61 | -Carte mémoire ──▶ FichierCandidat (checksum, date) ──▶ Groupe d'import (découpage jour par jour) | 71 | +Carte mémoire ──▶ FichierCandidat (checksum, date) ──▶ Historique des imports (détection doublon, FR-004) |
| 72 | + │ | ||
| 73 | + ▼ | ||
| 74 | + Groupe d'import (découpage jour par jour) | ||
| 62 | │ | 75 | │ |
| 63 | ▼ | 76 | ▼ |
| 64 | DestinationChoisie ──▶ RootLocation (specs/004) | 77 | DestinationChoisie ──▶ RootLocation (specs/004) |
modified
specs/001-import-photos/quickstart.md +10 -1 | @@ -45,6 +45,15 @@ regine import ./fixtures/carte_secours_meme_etape --destination fusion:<id_kotor | ||
| 45 | 45 | |
| 46 | 46 | **Résultat attendu** : que `<id_kotor>` existe en local ou seulement dans l'archive, la fusion aboutit avec désambiguïsation automatique des boîtiers par modèle EXIF (ou question d'étiquetage manuel si nécessaire, cf. `specs/002-profil-boitiers-optionnel`). Si `<id_kotor>` n'existe que dans l'archive, un checkout automatique (`specs/005-checkout-reconciliation`) précède l'intégration des nouveaux fichiers — visible sur stdout comme une étape supplémentaire, pas un échec. Si ce dossier est déjà verrouillé par un autre checkout en cours, la commande échoue explicitement. |
| 47 | 47 | |
| 48 | +## Scénario 4 — Détection de doublon entre deux imports séparés (FR-004 révisé, 2026-09-22) | |
| 49 | + | |
| 50 | +```bash | |
| 51 | +regine import ./fixtures/carte_journee_unique --titre "Premier essai" --annee --yes | |
| 52 | +regine import ./fixtures/carte_journee_unique --titre "Deuxieme essai" --annee | |
| 53 | +``` | |
| 54 | + | |
| 55 | +**Résultat attendu** : le second import (même carte, titre et dossier de destination différents) affiche un avertissement listant les fichiers dont la somme de contrôle correspond à ceux déjà importés sous « Premier essai » et le chemin de ce dossier, puis attend une confirmation avant de poursuivre — jamais une exclusion ou une inclusion silencieuse. | |
| 56 | + | |
| 48 | 57 | ## Critères de sortie |
| 49 | 58 | |
| 50 | -Les 3 scénarios, y compris la fusion vers un dossier déjà archivé, doivent passer intégralement — plus aucun sous-scénario documenté comme bloqué. | |
| 59 | +Les 4 scénarios, y compris la fusion vers un dossier déjà archivé et la détection de doublon, doivent passer intégralement — plus aucun sous-scénario documenté comme bloqué. | |
| @@ -45,6 +45,15 @@ regine import ./fixtures/carte_secours_meme_etape --destination fusion:<id_kotor | |||
| 45 | 45 | ||
| 46 | **Résultat attendu** : que `<id_kotor>` existe en local ou seulement dans l'archive, la fusion aboutit avec désambiguïsation automatique des boîtiers par modèle EXIF (ou question d'étiquetage manuel si nécessaire, cf. `specs/002-profil-boitiers-optionnel`). Si `<id_kotor>` n'existe que dans l'archive, un checkout automatique (`specs/005-checkout-reconciliation`) précède l'intégration des nouveaux fichiers — visible sur stdout comme une étape supplémentaire, pas un échec. Si ce dossier est déjà verrouillé par un autre checkout en cours, la commande échoue explicitement. | 46 | **Résultat attendu** : que `<id_kotor>` existe en local ou seulement dans l'archive, la fusion aboutit avec désambiguïsation automatique des boîtiers par modèle EXIF (ou question d'étiquetage manuel si nécessaire, cf. `specs/002-profil-boitiers-optionnel`). Si `<id_kotor>` n'existe que dans l'archive, un checkout automatique (`specs/005-checkout-reconciliation`) précède l'intégration des nouveaux fichiers — visible sur stdout comme une étape supplémentaire, pas un échec. Si ce dossier est déjà verrouillé par un autre checkout en cours, la commande échoue explicitement. |
| 47 | 47 | ||
| 48 | +## Scénario 4 — Détection de doublon entre deux imports séparés (FR-004 révisé, 2026-09-22) | ||
| 49 | + | ||
| 50 | +```bash | ||
| 51 | +regine import ./fixtures/carte_journee_unique --titre "Premier essai" --annee --yes | ||
| 52 | +regine import ./fixtures/carte_journee_unique --titre "Deuxieme essai" --annee | ||
| 53 | +``` | ||
| 54 | + | ||
| 55 | +**Résultat attendu** : le second import (même carte, titre et dossier de destination différents) affiche un avertissement listant les fichiers dont la somme de contrôle correspond à ceux déjà importés sous « Premier essai » et le chemin de ce dossier, puis attend une confirmation avant de poursuivre — jamais une exclusion ou une inclusion silencieuse. | ||
| 56 | + | ||
| 48 | ## Critères de sortie | 57 | ## Critères de sortie |
| 49 | 58 | ||
| 50 | -Les 3 scénarios, y compris la fusion vers un dossier déjà archivé, doivent passer intégralement — plus aucun sous-scénario documenté comme bloqué. | 59 | +Les 4 scénarios, y compris la fusion vers un dossier déjà archivé et la détection de doublon, doivent passer intégralement — plus aucun sous-scénario documenté comme bloqué. |
modified
specs/001-import-photos/spec.md +7 -2 | @@ -14,6 +14,7 @@ | ||
| 14 | 14 | |
| 15 | 15 | - Q: L'import doit-il continuer à pousser automatiquement le dossier fini vers l'archive NAS, ou s'arrêter à une copie de travail locale ? → A: Il s'arrête à une copie de travail locale, classée par dossier de format (`raw/`/`jpeg/`/`tiff/`...). Le photographe doit pouvoir relire et retoucher cette copie avant tout envoi au NAS ; l'archivage devient une action distincte et explicite, déclenchée plus tard (réutilise `regine reconcile`, `specs/005-checkout-reconciliation`, déjà conçu pour traiter un dossier jamais encore archivé comme un ensemble de fichiers "nouveaux" à archiver sur confirmation). Conséquence directe : importer une carte ne nécessite plus que le partage NAS soit monté au moment de l'import (seul son chemin configuré sert à situer où le dossier ira plus tard) — utile aussi pour tester l'import sans accès réel au NAS. |
| 16 | 16 | - Q: Les fichiers importés doivent-ils être classés dans un dossier de format (`raw/`, `jpeg/`, `tiff/`...) dès l'import ? → A: Oui — écart trouvé à l'implémentation : cette spec ne le prévoyait pas explicitement alors que `docs/archivage-photo-elements-cles.md` section 10 et `specs/006-gui-tri-consultation` (écran de tri) supposent tous deux cette structure déjà en place dès qu'un dossier existe. Sans ce classement, un dossier fraîchement importé se retrouvait entièrement à plat, ce qui faisait apparaître chaque fichier comme "déjà promu à la racine" dans l'écran de tri — plus rien à trier. Corrigé par une nouvelle étape de classement (FR-020), après le renommage et avant la publication locale. |
| 17 | +- Q: Le système doit-il détecter qu'une photo a déjà été importée précédemment, même vers un dossier différent d'un import antérieur ? → A: Oui — écart trouvé par test manuel de l'utilisateur : importer deux fois la même carte vers deux dossiers différents ne déclenchait aucun avertissement, alors que FR-004 (comparaison par somme de contrôle) existait déjà en code mais n'était jamais réellement alimenté par aucun appelant (CLI ou GUI) — la déduplication ne fonctionnait donc dans aucun cas réel, y compris le cas initialement visé (carte non effacée entre deux imports). Corrigé par un historique persistant des sommes de contrôle déjà importées (FR-004 révisé), alimenté à chaque import réussi et consulté avant le suivant ; en cas de correspondance, Régine informe l'utilisateur (fichiers concernés, dossier où ils ont déjà été importés) et laisse le choix de continuer ou d'annuler (Principe V) — jamais un blocage ou une exclusion silencieuse. | |
| 17 | 18 | |
| 18 | 19 | ## User Scenarios & Testing *(mandatory)* |
| 19 | 20 | |
| @@ -33,6 +34,7 @@ Un photographe rentre d'une sortie photo avec une carte mémoire. Il demande à | ||
| 33 | 34 | 4. **Given** un nom de dossier validé, **When** Régine renomme les fichiers avant publication locale, **Then** chaque fichier maître est renommé en `date_titre_nomOrigine.ext` en conservant le nom d'origine du boîtier en suffixe, et tout fichier associé (JPEG jumeau, sidecar) est renommé de façon synchronisée. |
| 34 | 35 | 5. **Given** des fichiers renommés localement, **When** Régine les classe par format, **Then** chaque fichier maître rejoint son dossier de format (`raw/` pour les RAW propriétaires, `jpeg/`, `tiff/` pour les scans, un dossier dédié pour un format maître plus marginal — cf. `docs/archivage-photo-elements-cles.md` section 10), et chaque fichier associé rejoint le dossier de son fichier maître. |
| 35 | 36 | 6. **Given** des fichiers renommés et classés par format, **When** Régine les publie dans l'espace de travail local, **Then** elle présente d'abord un résumé (nombre de fichiers, taille, nom du dossier de destination) et attend une confirmation explicite avant d'écrire quoi que ce soit localement — aucune écriture n'a lieu sur l'archive NAS à ce stade. |
| 37 | +7. **Given** une carte dont certains fichiers correspondent, par somme de contrôle, à des fichiers déjà importés lors d'une session précédente vers un dossier différent, **When** l'utilisateur relance un import avec cette carte, **Then** Régine l'informe de ces correspondances (fichiers concernés, dossier où ils ont déjà été importés) avant de poursuivre, et attend sa décision explicite de continuer ou d'annuler. | |
| 36 | 38 | |
| 37 | 39 | --- |
| 38 | 40 | |
| @@ -76,7 +78,7 @@ Un photographe est en voyage sur plusieurs villes. Pour la deuxième étape, il | ||
| 76 | 78 | - Que se passe-t-il si un fichier ne peut pas être vérifié par somme de contrôle pendant la copie depuis la carte (lecture corrompue) ? La copie de ce fichier DOIT être signalée en échec et la carte ne DOIT PAS être considérée comme sûre à effacer tant que le fichier n'est pas correctement recopié et vérifié. |
| 77 | 79 | - Que se passe-t-il si la carte mémoire a une vitesse d'accès en lecture particulièrement faible ? La vérification d'intégrité ne DOIT jamais nécessiter une seconde lecture de la carte pour un même fichier : la comparaison se fait entre la somme de contrôle calculée pendant l'unique lecture de la carte et celle recalculée en relisant la copie locale sur disque, pour ne pas doubler le temps d'import sur une carte lente. |
| 78 | 80 | - Comment le système gère-t-il une carte contenant des dates de prise de vue aberrantes (horloge de boîtier réinitialisée, ex. dates en 1980 ou 2002) ? Ces fichiers DOIVENT être exclus du calcul de la plage de dates et signalés à l'utilisateur comme anomalie, plutôt que de fausser silencieusement le découpage en groupes. |
| 79 | -- Comment le système gère-t-il une carte contenant à la fois des fichiers déjà importés lors d'une session précédente (non effacés de la carte) et des fichiers réellement nouveaux ? Seuls les fichiers réellement nouveaux (identifiés par somme de contrôle, absents des imports précédents) DOIVENT entrer dans l'analyse et le découpage en groupes. | |
| 81 | +- Comment le système gère-t-il une carte dont certains fichiers ont déjà été importés précédemment (non effacés de la carte, ou déjà présents dans un dossier différent d'un import antérieur) ? Le système DOIT signaler ces fichiers à l'utilisateur (dossier où ils ont déjà été importés) et attendre sa décision explicite (continuer ou annuler) avant de poursuivre l'analyse et le découpage en groupes — jamais les exclure ou les inclure silencieusement (cf. FR-004 révisé). | |
| 80 | 82 | - Que se passe-t-il si le nom de dossier calculé entre en collision avec un dossier existant (local ou archivé) ? Le système DOIT proposer un suffixe ou demander une confirmation explicite, jamais écraser silencieusement le dossier existant. |
| 81 | 83 | - Que se passe-t-il si l'espace disque local est insuffisant pour accueillir la copie de travail avant même de commencer la copie ? Le système DOIT vérifier l'espace disponible avant de démarrer la copie et interrompre l'import avec un message clair plutôt que d'échouer en cours de copie. |
| 82 | 84 | - Comment le système traite-t-il un fichier présent sur la carte qui n'est ni une photo ni un fichier associé reconnu (ex. vidéo, fichier système du boîtier) ? Ce fichier est hors périmètre de l'import et n'est ni copié ni pris en compte dans l'analyse (cf. Assumptions). |
| @@ -89,7 +91,7 @@ Un photographe est en voyage sur plusieurs villes. Pour la deuxième étape, il | ||
| 89 | 91 | - **FR-001**: Le système DOIT copier l'intégralité des fichiers nouveaux d'une carte mémoire vers un espace de travail local en une seule lecture de la carte par fichier, en calculant la somme de contrôle de la source au fil de cette copie. Il DOIT ensuite vérifier l'intégrité de la copie en relisant le fichier déjà écrit sur le disque local — jamais en relisant une seconde fois la carte mémoire — et comparer les deux sommes de contrôle avant de considérer la carte comme sûre à effacer. |
| 90 | 92 | - **FR-002**: Le système DOIT lire la date de prise de vue (métadonnée EXIF d'origine, jamais la date de fichier) de chaque fichier nouvellement importé, et construire une répartition jour par jour (nombre de photos, plage horaire) du lot importé. |
| 91 | 93 | - **FR-003**: Le système DOIT exclure du calcul de plage les fichiers dont la date de prise de vue est aberrante et signaler cette anomalie à l'utilisateur, plutôt que d'inclure silencieusement ces dates dans la plage détectée. |
| 92 | -- **FR-004**: Le système DOIT ne prendre en compte, pour l'analyse et le découpage en groupes, que les fichiers réellement nouveaux pour l'import en cours (identifiés par somme de contrôle), à l'exclusion de tout fichier déjà importé lors d'une session précédente et resté sur la carte. | |
| 94 | +- **FR-004**: Le système DOIT détecter, par somme de contrôle, qu'un fichier de la carte a déjà été importé lors d'une session précédente — que ce fichier soit resté sur la carte (import répété sans effacement) ou qu'il ait déjà été placé dans un dossier différent d'un import antérieur — en comparant à un historique persistant des imports déjà réalisés (révisé le 2026-09-22 : couvre désormais tous les imports passés, pas seulement la session en cours). Quand une correspondance est trouvée, le système DOIT en informer l'utilisateur (fichiers concernés, dossier où ils ont déjà été importés) avant de poursuivre, et lui laisser le choix explicite de continuer l'import malgré tout ou de l'annuler (Principe V) — jamais une exclusion silencieuse ni un blocage automatique. | |
| 93 | 95 | - **FR-005**: Le système DOIT proposer par défaut un seul groupe couvrant toute la plage de dates contiguë détectée, et permettre à l'utilisateur de détacher un ou plusieurs jours de cette plage pour former des groupes distincts. |
| 94 | 96 | - **FR-006**: Le système PEUT mettre en avant un ou plusieurs jours comme candidats plausibles au détachement (ex. pic de prises de vue isolé), mais ne DOIT jamais détacher un groupe automatiquement sans validation de l'utilisateur. |
| 95 | 97 | - **FR-007**: Pour chaque groupe résultant du découpage, le système DOIT demander explicitement à l'utilisateur sa destination, parmi : nouveau dossier simple, nouveau sous-dossier d'un dossier parent existant, fusion dans un dossier existant, ou nouveau dossier parent avec sa première étape — sans jamais retenir un état "voyage en cours" d'un import à l'autre. Pour un nouveau dossier simple ou un nouveau dossier parent (jamais pour un nouveau sous-dossier ni une fusion, qui héritent du placement déjà établi), le système DOIT également demander si ce dossier relève d'une catégorie thématique ou du placement par défaut par année (cf. `specs/004-categorisation-dossiers`). |
| @@ -120,6 +122,7 @@ Un photographe est en voyage sur plusieurs villes. Pour la deuxième étape, il | ||
| 120 | 122 | - **Fichier maître** : photo issue directement du boîtier (RAW ou JPEG selon les cas), renommée à l'import et jamais modifiée une fois archivée. |
| 121 | 123 | - **Fichier associé** : fichier partageant le nom de base d'un fichier maître (JPEG jumeau, sidecar de réglages), renommé de façon synchronisée avec lui. |
| 122 | 124 | - **Identifiant pérenne** : identifiant attribué à chaque photo importée, indépendant de son nom de fichier, inscrit dans ses métadonnées. |
| 125 | +- **Historique des imports** : registre persistant associant chaque somme de contrôle déjà importée au dossier de l'espace de travail local où elle a atterri, alimenté à chaque import réussi et consulté avant tout nouvel import pour détecter un doublon (FR-004 révisé) — distinct de la copie de travail temporaire (éphémère) et de la base de données de travail par dossier (`specs/003-config-contexte-travail`). | |
| 123 | 126 | |
| 124 | 127 | ## Success Criteria *(mandatory)* |
| 125 | 128 | |
| @@ -135,6 +138,7 @@ Un photographe est en voyage sur plusieurs villes. Pour la deuxième étape, il | ||
| 135 | 138 | - **SC-008**: Le répertoire racine de destination (année ou catégorie thématique) est visible dans 100% des résumés présentés avant confirmation de la copie locale (cf. `specs/004-categorisation-dossiers`). |
| 136 | 139 | - **SC-009**: 100% des fichiers maîtres publiés localement se trouvent dans leur dossier de format attendu (`raw/`, `jpeg/`, `tiff/`...), et chaque fichier associé dans le dossier de format de son fichier maître. |
| 137 | 140 | - **SC-010**: Un import peut être mené jusqu'au bout (copie, groupes, renommage, classement, publication locale) sans qu'un partage d'archive configuré ait besoin d'être monté ou accessible durant l'opération. |
| 141 | +- **SC-011**: 100% des fichiers dont la somme de contrôle correspond à un import déjà réalisé (quel que soit son dossier de destination) sont signalés à l'utilisateur avant la poursuite de l'import, avec le dossier où ils ont déjà été importés. | |
| 138 | 142 | |
| 139 | 143 | ## Assumptions |
| 140 | 144 | |
| @@ -145,3 +149,4 @@ Un photographe est en voyage sur plusieurs villes. Pour la deuxième étape, il | ||
| 145 | 149 | - La suggestion automatique de lieu par coordonnées GPS dépend de la présence de ces coordonnées dans les métadonnées du fichier ; en leur absence, l'utilisateur saisit le lieu librement, sans que cela bloque l'import. |
| 146 | 150 | - Une seule carte mémoire est traitée à la fois par import ; l'import simultané de plusieurs cartes n'est pas couvert par ce module. |
| 147 | 151 | - Le placement racine d'un dossier ou d'un dossier parent (répertoire d'année par défaut, ou répertoire de catégorie thématique) est défini par `specs/004-categorisation-dossiers`, que ce module ne redéfinit pas : il s'appuie dessus pour l'étape de destination (FR-007), la construction du chemin final (FR-010), la vérification de collision (FR-012) et le résumé de confirmation (FR-018). |
| 152 | +- **Ajouté le 2026-09-22** : l'historique des imports (FR-004 révisé) vit dans la base de contexte centralisée déjà en place (`regine_core.config.db`, `specs/003-config-contexte-travail`), au même titre que la liste des boîtiers rencontrés — pas une base séparée. Il n'enregistre qu'une somme de contrôle, un dossier de destination et une date : aucune donnée personnelle, aucun contenu de fichier. La détection reste locale à l'installation de Régine (un seul jeu de chemins par installation, cf. Assumptions de `specs/003`) ; deux installations distinctes de Régine ne partagent pas leurs historiques. | |
| @@ -14,6 +14,7 @@ | |||
| 14 | 14 | ||
| 15 | - Q: L'import doit-il continuer à pousser automatiquement le dossier fini vers l'archive NAS, ou s'arrêter à une copie de travail locale ? → A: Il s'arrête à une copie de travail locale, classée par dossier de format (`raw/`/`jpeg/`/`tiff/`...). Le photographe doit pouvoir relire et retoucher cette copie avant tout envoi au NAS ; l'archivage devient une action distincte et explicite, déclenchée plus tard (réutilise `regine reconcile`, `specs/005-checkout-reconciliation`, déjà conçu pour traiter un dossier jamais encore archivé comme un ensemble de fichiers "nouveaux" à archiver sur confirmation). Conséquence directe : importer une carte ne nécessite plus que le partage NAS soit monté au moment de l'import (seul son chemin configuré sert à situer où le dossier ira plus tard) — utile aussi pour tester l'import sans accès réel au NAS. | 15 | - Q: L'import doit-il continuer à pousser automatiquement le dossier fini vers l'archive NAS, ou s'arrêter à une copie de travail locale ? → A: Il s'arrête à une copie de travail locale, classée par dossier de format (`raw/`/`jpeg/`/`tiff/`...). Le photographe doit pouvoir relire et retoucher cette copie avant tout envoi au NAS ; l'archivage devient une action distincte et explicite, déclenchée plus tard (réutilise `regine reconcile`, `specs/005-checkout-reconciliation`, déjà conçu pour traiter un dossier jamais encore archivé comme un ensemble de fichiers "nouveaux" à archiver sur confirmation). Conséquence directe : importer une carte ne nécessite plus que le partage NAS soit monté au moment de l'import (seul son chemin configuré sert à situer où le dossier ira plus tard) — utile aussi pour tester l'import sans accès réel au NAS. |
| 16 | - Q: Les fichiers importés doivent-ils être classés dans un dossier de format (`raw/`, `jpeg/`, `tiff/`...) dès l'import ? → A: Oui — écart trouvé à l'implémentation : cette spec ne le prévoyait pas explicitement alors que `docs/archivage-photo-elements-cles.md` section 10 et `specs/006-gui-tri-consultation` (écran de tri) supposent tous deux cette structure déjà en place dès qu'un dossier existe. Sans ce classement, un dossier fraîchement importé se retrouvait entièrement à plat, ce qui faisait apparaître chaque fichier comme "déjà promu à la racine" dans l'écran de tri — plus rien à trier. Corrigé par une nouvelle étape de classement (FR-020), après le renommage et avant la publication locale. | 16 | - Q: Les fichiers importés doivent-ils être classés dans un dossier de format (`raw/`, `jpeg/`, `tiff/`...) dès l'import ? → A: Oui — écart trouvé à l'implémentation : cette spec ne le prévoyait pas explicitement alors que `docs/archivage-photo-elements-cles.md` section 10 et `specs/006-gui-tri-consultation` (écran de tri) supposent tous deux cette structure déjà en place dès qu'un dossier existe. Sans ce classement, un dossier fraîchement importé se retrouvait entièrement à plat, ce qui faisait apparaître chaque fichier comme "déjà promu à la racine" dans l'écran de tri — plus rien à trier. Corrigé par une nouvelle étape de classement (FR-020), après le renommage et avant la publication locale. |
| 17 | +- Q: Le système doit-il détecter qu'une photo a déjà été importée précédemment, même vers un dossier différent d'un import antérieur ? → A: Oui — écart trouvé par test manuel de l'utilisateur : importer deux fois la même carte vers deux dossiers différents ne déclenchait aucun avertissement, alors que FR-004 (comparaison par somme de contrôle) existait déjà en code mais n'était jamais réellement alimenté par aucun appelant (CLI ou GUI) — la déduplication ne fonctionnait donc dans aucun cas réel, y compris le cas initialement visé (carte non effacée entre deux imports). Corrigé par un historique persistant des sommes de contrôle déjà importées (FR-004 révisé), alimenté à chaque import réussi et consulté avant le suivant ; en cas de correspondance, Régine informe l'utilisateur (fichiers concernés, dossier où ils ont déjà été importés) et laisse le choix de continuer ou d'annuler (Principe V) — jamais un blocage ou une exclusion silencieuse. | ||
| 17 | 18 | ||
| 18 | ## User Scenarios & Testing *(mandatory)* | 19 | ## User Scenarios & Testing *(mandatory)* |
| 19 | 20 | ||
| @@ -33,6 +34,7 @@ Un photographe rentre d'une sortie photo avec une carte mémoire. Il demande à | |||
| 33 | 4. **Given** un nom de dossier validé, **When** Régine renomme les fichiers avant publication locale, **Then** chaque fichier maître est renommé en `date_titre_nomOrigine.ext` en conservant le nom d'origine du boîtier en suffixe, et tout fichier associé (JPEG jumeau, sidecar) est renommé de façon synchronisée. | 34 | 4. **Given** un nom de dossier validé, **When** Régine renomme les fichiers avant publication locale, **Then** chaque fichier maître est renommé en `date_titre_nomOrigine.ext` en conservant le nom d'origine du boîtier en suffixe, et tout fichier associé (JPEG jumeau, sidecar) est renommé de façon synchronisée. |
| 34 | 5. **Given** des fichiers renommés localement, **When** Régine les classe par format, **Then** chaque fichier maître rejoint son dossier de format (`raw/` pour les RAW propriétaires, `jpeg/`, `tiff/` pour les scans, un dossier dédié pour un format maître plus marginal — cf. `docs/archivage-photo-elements-cles.md` section 10), et chaque fichier associé rejoint le dossier de son fichier maître. | 35 | 5. **Given** des fichiers renommés localement, **When** Régine les classe par format, **Then** chaque fichier maître rejoint son dossier de format (`raw/` pour les RAW propriétaires, `jpeg/`, `tiff/` pour les scans, un dossier dédié pour un format maître plus marginal — cf. `docs/archivage-photo-elements-cles.md` section 10), et chaque fichier associé rejoint le dossier de son fichier maître. |
| 35 | 6. **Given** des fichiers renommés et classés par format, **When** Régine les publie dans l'espace de travail local, **Then** elle présente d'abord un résumé (nombre de fichiers, taille, nom du dossier de destination) et attend une confirmation explicite avant d'écrire quoi que ce soit localement — aucune écriture n'a lieu sur l'archive NAS à ce stade. | 36 | 6. **Given** des fichiers renommés et classés par format, **When** Régine les publie dans l'espace de travail local, **Then** elle présente d'abord un résumé (nombre de fichiers, taille, nom du dossier de destination) et attend une confirmation explicite avant d'écrire quoi que ce soit localement — aucune écriture n'a lieu sur l'archive NAS à ce stade. |
| 37 | +7. **Given** une carte dont certains fichiers correspondent, par somme de contrôle, à des fichiers déjà importés lors d'une session précédente vers un dossier différent, **When** l'utilisateur relance un import avec cette carte, **Then** Régine l'informe de ces correspondances (fichiers concernés, dossier où ils ont déjà été importés) avant de poursuivre, et attend sa décision explicite de continuer ou d'annuler. | ||
| 36 | 38 | ||
| 37 | --- | 39 | --- |
| 38 | 40 | ||
| @@ -76,7 +78,7 @@ Un photographe est en voyage sur plusieurs villes. Pour la deuxième étape, il | |||
| 76 | - Que se passe-t-il si un fichier ne peut pas être vérifié par somme de contrôle pendant la copie depuis la carte (lecture corrompue) ? La copie de ce fichier DOIT être signalée en échec et la carte ne DOIT PAS être considérée comme sûre à effacer tant que le fichier n'est pas correctement recopié et vérifié. | 78 | - Que se passe-t-il si un fichier ne peut pas être vérifié par somme de contrôle pendant la copie depuis la carte (lecture corrompue) ? La copie de ce fichier DOIT être signalée en échec et la carte ne DOIT PAS être considérée comme sûre à effacer tant que le fichier n'est pas correctement recopié et vérifié. |
| 77 | - Que se passe-t-il si la carte mémoire a une vitesse d'accès en lecture particulièrement faible ? La vérification d'intégrité ne DOIT jamais nécessiter une seconde lecture de la carte pour un même fichier : la comparaison se fait entre la somme de contrôle calculée pendant l'unique lecture de la carte et celle recalculée en relisant la copie locale sur disque, pour ne pas doubler le temps d'import sur une carte lente. | 79 | - Que se passe-t-il si la carte mémoire a une vitesse d'accès en lecture particulièrement faible ? La vérification d'intégrité ne DOIT jamais nécessiter une seconde lecture de la carte pour un même fichier : la comparaison se fait entre la somme de contrôle calculée pendant l'unique lecture de la carte et celle recalculée en relisant la copie locale sur disque, pour ne pas doubler le temps d'import sur une carte lente. |
| 78 | - Comment le système gère-t-il une carte contenant des dates de prise de vue aberrantes (horloge de boîtier réinitialisée, ex. dates en 1980 ou 2002) ? Ces fichiers DOIVENT être exclus du calcul de la plage de dates et signalés à l'utilisateur comme anomalie, plutôt que de fausser silencieusement le découpage en groupes. | 80 | - Comment le système gère-t-il une carte contenant des dates de prise de vue aberrantes (horloge de boîtier réinitialisée, ex. dates en 1980 ou 2002) ? Ces fichiers DOIVENT être exclus du calcul de la plage de dates et signalés à l'utilisateur comme anomalie, plutôt que de fausser silencieusement le découpage en groupes. |
| 79 | -- Comment le système gère-t-il une carte contenant à la fois des fichiers déjà importés lors d'une session précédente (non effacés de la carte) et des fichiers réellement nouveaux ? Seuls les fichiers réellement nouveaux (identifiés par somme de contrôle, absents des imports précédents) DOIVENT entrer dans l'analyse et le découpage en groupes. | 81 | +- Comment le système gère-t-il une carte dont certains fichiers ont déjà été importés précédemment (non effacés de la carte, ou déjà présents dans un dossier différent d'un import antérieur) ? Le système DOIT signaler ces fichiers à l'utilisateur (dossier où ils ont déjà été importés) et attendre sa décision explicite (continuer ou annuler) avant de poursuivre l'analyse et le découpage en groupes — jamais les exclure ou les inclure silencieusement (cf. FR-004 révisé). |
| 80 | - Que se passe-t-il si le nom de dossier calculé entre en collision avec un dossier existant (local ou archivé) ? Le système DOIT proposer un suffixe ou demander une confirmation explicite, jamais écraser silencieusement le dossier existant. | 82 | - Que se passe-t-il si le nom de dossier calculé entre en collision avec un dossier existant (local ou archivé) ? Le système DOIT proposer un suffixe ou demander une confirmation explicite, jamais écraser silencieusement le dossier existant. |
| 81 | - Que se passe-t-il si l'espace disque local est insuffisant pour accueillir la copie de travail avant même de commencer la copie ? Le système DOIT vérifier l'espace disponible avant de démarrer la copie et interrompre l'import avec un message clair plutôt que d'échouer en cours de copie. | 83 | - Que se passe-t-il si l'espace disque local est insuffisant pour accueillir la copie de travail avant même de commencer la copie ? Le système DOIT vérifier l'espace disponible avant de démarrer la copie et interrompre l'import avec un message clair plutôt que d'échouer en cours de copie. |
| 82 | - Comment le système traite-t-il un fichier présent sur la carte qui n'est ni une photo ni un fichier associé reconnu (ex. vidéo, fichier système du boîtier) ? Ce fichier est hors périmètre de l'import et n'est ni copié ni pris en compte dans l'analyse (cf. Assumptions). | 84 | - Comment le système traite-t-il un fichier présent sur la carte qui n'est ni une photo ni un fichier associé reconnu (ex. vidéo, fichier système du boîtier) ? Ce fichier est hors périmètre de l'import et n'est ni copié ni pris en compte dans l'analyse (cf. Assumptions). |
| @@ -89,7 +91,7 @@ Un photographe est en voyage sur plusieurs villes. Pour la deuxième étape, il | |||
| 89 | - **FR-001**: Le système DOIT copier l'intégralité des fichiers nouveaux d'une carte mémoire vers un espace de travail local en une seule lecture de la carte par fichier, en calculant la somme de contrôle de la source au fil de cette copie. Il DOIT ensuite vérifier l'intégrité de la copie en relisant le fichier déjà écrit sur le disque local — jamais en relisant une seconde fois la carte mémoire — et comparer les deux sommes de contrôle avant de considérer la carte comme sûre à effacer. | 91 | - **FR-001**: Le système DOIT copier l'intégralité des fichiers nouveaux d'une carte mémoire vers un espace de travail local en une seule lecture de la carte par fichier, en calculant la somme de contrôle de la source au fil de cette copie. Il DOIT ensuite vérifier l'intégrité de la copie en relisant le fichier déjà écrit sur le disque local — jamais en relisant une seconde fois la carte mémoire — et comparer les deux sommes de contrôle avant de considérer la carte comme sûre à effacer. |
| 90 | - **FR-002**: Le système DOIT lire la date de prise de vue (métadonnée EXIF d'origine, jamais la date de fichier) de chaque fichier nouvellement importé, et construire une répartition jour par jour (nombre de photos, plage horaire) du lot importé. | 92 | - **FR-002**: Le système DOIT lire la date de prise de vue (métadonnée EXIF d'origine, jamais la date de fichier) de chaque fichier nouvellement importé, et construire une répartition jour par jour (nombre de photos, plage horaire) du lot importé. |
| 91 | - **FR-003**: Le système DOIT exclure du calcul de plage les fichiers dont la date de prise de vue est aberrante et signaler cette anomalie à l'utilisateur, plutôt que d'inclure silencieusement ces dates dans la plage détectée. | 93 | - **FR-003**: Le système DOIT exclure du calcul de plage les fichiers dont la date de prise de vue est aberrante et signaler cette anomalie à l'utilisateur, plutôt que d'inclure silencieusement ces dates dans la plage détectée. |
| 92 | -- **FR-004**: Le système DOIT ne prendre en compte, pour l'analyse et le découpage en groupes, que les fichiers réellement nouveaux pour l'import en cours (identifiés par somme de contrôle), à l'exclusion de tout fichier déjà importé lors d'une session précédente et resté sur la carte. | 94 | +- **FR-004**: Le système DOIT détecter, par somme de contrôle, qu'un fichier de la carte a déjà été importé lors d'une session précédente — que ce fichier soit resté sur la carte (import répété sans effacement) ou qu'il ait déjà été placé dans un dossier différent d'un import antérieur — en comparant à un historique persistant des imports déjà réalisés (révisé le 2026-09-22 : couvre désormais tous les imports passés, pas seulement la session en cours). Quand une correspondance est trouvée, le système DOIT en informer l'utilisateur (fichiers concernés, dossier où ils ont déjà été importés) avant de poursuivre, et lui laisser le choix explicite de continuer l'import malgré tout ou de l'annuler (Principe V) — jamais une exclusion silencieuse ni un blocage automatique. |
| 93 | - **FR-005**: Le système DOIT proposer par défaut un seul groupe couvrant toute la plage de dates contiguë détectée, et permettre à l'utilisateur de détacher un ou plusieurs jours de cette plage pour former des groupes distincts. | 95 | - **FR-005**: Le système DOIT proposer par défaut un seul groupe couvrant toute la plage de dates contiguë détectée, et permettre à l'utilisateur de détacher un ou plusieurs jours de cette plage pour former des groupes distincts. |
| 94 | - **FR-006**: Le système PEUT mettre en avant un ou plusieurs jours comme candidats plausibles au détachement (ex. pic de prises de vue isolé), mais ne DOIT jamais détacher un groupe automatiquement sans validation de l'utilisateur. | 96 | - **FR-006**: Le système PEUT mettre en avant un ou plusieurs jours comme candidats plausibles au détachement (ex. pic de prises de vue isolé), mais ne DOIT jamais détacher un groupe automatiquement sans validation de l'utilisateur. |
| 95 | - **FR-007**: Pour chaque groupe résultant du découpage, le système DOIT demander explicitement à l'utilisateur sa destination, parmi : nouveau dossier simple, nouveau sous-dossier d'un dossier parent existant, fusion dans un dossier existant, ou nouveau dossier parent avec sa première étape — sans jamais retenir un état "voyage en cours" d'un import à l'autre. Pour un nouveau dossier simple ou un nouveau dossier parent (jamais pour un nouveau sous-dossier ni une fusion, qui héritent du placement déjà établi), le système DOIT également demander si ce dossier relève d'une catégorie thématique ou du placement par défaut par année (cf. `specs/004-categorisation-dossiers`). | 97 | - **FR-007**: Pour chaque groupe résultant du découpage, le système DOIT demander explicitement à l'utilisateur sa destination, parmi : nouveau dossier simple, nouveau sous-dossier d'un dossier parent existant, fusion dans un dossier existant, ou nouveau dossier parent avec sa première étape — sans jamais retenir un état "voyage en cours" d'un import à l'autre. Pour un nouveau dossier simple ou un nouveau dossier parent (jamais pour un nouveau sous-dossier ni une fusion, qui héritent du placement déjà établi), le système DOIT également demander si ce dossier relève d'une catégorie thématique ou du placement par défaut par année (cf. `specs/004-categorisation-dossiers`). |
| @@ -120,6 +122,7 @@ Un photographe est en voyage sur plusieurs villes. Pour la deuxième étape, il | |||
| 120 | - **Fichier maître** : photo issue directement du boîtier (RAW ou JPEG selon les cas), renommée à l'import et jamais modifiée une fois archivée. | 122 | - **Fichier maître** : photo issue directement du boîtier (RAW ou JPEG selon les cas), renommée à l'import et jamais modifiée une fois archivée. |
| 121 | - **Fichier associé** : fichier partageant le nom de base d'un fichier maître (JPEG jumeau, sidecar de réglages), renommé de façon synchronisée avec lui. | 123 | - **Fichier associé** : fichier partageant le nom de base d'un fichier maître (JPEG jumeau, sidecar de réglages), renommé de façon synchronisée avec lui. |
| 122 | - **Identifiant pérenne** : identifiant attribué à chaque photo importée, indépendant de son nom de fichier, inscrit dans ses métadonnées. | 124 | - **Identifiant pérenne** : identifiant attribué à chaque photo importée, indépendant de son nom de fichier, inscrit dans ses métadonnées. |
| 125 | +- **Historique des imports** : registre persistant associant chaque somme de contrôle déjà importée au dossier de l'espace de travail local où elle a atterri, alimenté à chaque import réussi et consulté avant tout nouvel import pour détecter un doublon (FR-004 révisé) — distinct de la copie de travail temporaire (éphémère) et de la base de données de travail par dossier (`specs/003-config-contexte-travail`). | ||
| 123 | 126 | ||
| 124 | ## Success Criteria *(mandatory)* | 127 | ## Success Criteria *(mandatory)* |
| 125 | 128 | ||
| @@ -135,6 +138,7 @@ Un photographe est en voyage sur plusieurs villes. Pour la deuxième étape, il | |||
| 135 | - **SC-008**: Le répertoire racine de destination (année ou catégorie thématique) est visible dans 100% des résumés présentés avant confirmation de la copie locale (cf. `specs/004-categorisation-dossiers`). | 138 | - **SC-008**: Le répertoire racine de destination (année ou catégorie thématique) est visible dans 100% des résumés présentés avant confirmation de la copie locale (cf. `specs/004-categorisation-dossiers`). |
| 136 | - **SC-009**: 100% des fichiers maîtres publiés localement se trouvent dans leur dossier de format attendu (`raw/`, `jpeg/`, `tiff/`...), et chaque fichier associé dans le dossier de format de son fichier maître. | 139 | - **SC-009**: 100% des fichiers maîtres publiés localement se trouvent dans leur dossier de format attendu (`raw/`, `jpeg/`, `tiff/`...), et chaque fichier associé dans le dossier de format de son fichier maître. |
| 137 | - **SC-010**: Un import peut être mené jusqu'au bout (copie, groupes, renommage, classement, publication locale) sans qu'un partage d'archive configuré ait besoin d'être monté ou accessible durant l'opération. | 140 | - **SC-010**: Un import peut être mené jusqu'au bout (copie, groupes, renommage, classement, publication locale) sans qu'un partage d'archive configuré ait besoin d'être monté ou accessible durant l'opération. |
| 141 | +- **SC-011**: 100% des fichiers dont la somme de contrôle correspond à un import déjà réalisé (quel que soit son dossier de destination) sont signalés à l'utilisateur avant la poursuite de l'import, avec le dossier où ils ont déjà été importés. | ||
| 138 | 142 | ||
| 139 | ## Assumptions | 143 | ## Assumptions |
| 140 | 144 | ||
| @@ -145,3 +149,4 @@ Un photographe est en voyage sur plusieurs villes. Pour la deuxième étape, il | |||
| 145 | - La suggestion automatique de lieu par coordonnées GPS dépend de la présence de ces coordonnées dans les métadonnées du fichier ; en leur absence, l'utilisateur saisit le lieu librement, sans que cela bloque l'import. | 149 | - La suggestion automatique de lieu par coordonnées GPS dépend de la présence de ces coordonnées dans les métadonnées du fichier ; en leur absence, l'utilisateur saisit le lieu librement, sans que cela bloque l'import. |
| 146 | - Une seule carte mémoire est traitée à la fois par import ; l'import simultané de plusieurs cartes n'est pas couvert par ce module. | 150 | - Une seule carte mémoire est traitée à la fois par import ; l'import simultané de plusieurs cartes n'est pas couvert par ce module. |
| 147 | - Le placement racine d'un dossier ou d'un dossier parent (répertoire d'année par défaut, ou répertoire de catégorie thématique) est défini par `specs/004-categorisation-dossiers`, que ce module ne redéfinit pas : il s'appuie dessus pour l'étape de destination (FR-007), la construction du chemin final (FR-010), la vérification de collision (FR-012) et le résumé de confirmation (FR-018). | 151 | - Le placement racine d'un dossier ou d'un dossier parent (répertoire d'année par défaut, ou répertoire de catégorie thématique) est défini par `specs/004-categorisation-dossiers`, que ce module ne redéfinit pas : il s'appuie dessus pour l'étape de destination (FR-007), la construction du chemin final (FR-010), la vérification de collision (FR-012) et le résumé de confirmation (FR-018). |
| 152 | +- **Ajouté le 2026-09-22** : l'historique des imports (FR-004 révisé) vit dans la base de contexte centralisée déjà en place (`regine_core.config.db`, `specs/003-config-contexte-travail`), au même titre que la liste des boîtiers rencontrés — pas une base séparée. Il n'enregistre qu'une somme de contrôle, un dossier de destination et une date : aucune donnée personnelle, aucun contenu de fichier. La détection reste locale à l'installation de Régine (un seul jeu de chemins par installation, cf. Assumptions de `specs/003`) ; deux installations distinctes de Régine ne partagent pas leurs historiques. | ||
modified
specs/001-import-photos/tasks.md +14 -0 | @@ -148,6 +148,20 @@ Ce module est le premier point d'intégration réel de trois autres specs déjà | ||
| 148 | 148 | |
| 149 | 149 | --- |
| 150 | 150 | |
| 151 | +## Phase 8: Révision 2026-09-22 (bis) — détection de doublons entre imports (FR-004 révisé) | |
| 152 | + | |
| 153 | +**Contexte** : test manuel de l'utilisateur — importer deux fois la même carte vers deux dossiers différents n'affichait aucun avertissement. Cause racine trouvée : le paramètre `checksums_deja_importes` de `copie.copier_carte` (déjà en place et testé isolément depuis T009) n'était jamais renseigné par aucun appelant réel — ni `regine_cli.import_cmd`, ni `regine_gui.import_view`. La déduplication ne fonctionnait donc dans aucun cas réel, y compris le cas initialement visé par FR-004 (carte non effacée entre deux imports). | |
| 154 | + | |
| 155 | +- [X] T047 [P] Créer `regine_core.import_carte.historique` (`enregistrer_import`, `rechercher_doublons`) — nouvelle table `checksums_importes` dans la base de contexte centralisée (`regine_core.config.db`, `specs/003`), même schéma d'implantation que `camera_profile.db` pour `boitiers`. Testé dans `packages/regine-core/tests/unit/test_historique.py` (4/4 tests : recherche vide, retrouve un doublon, idempotence/mise à jour du dossier, ignore les checksums inconnus) | |
| 156 | +- [X] T048 [US1] Étendre `packages/regine-cli/src/regine_cli/import_cmd.py` : `conn` ouvert avant `copier_carte` (au lieu d'après) pour permettre `rechercher_doublons(conn, ...)` juste après la copie ; `_confirmer_malgre_doublons` affiche les fichiers/dossiers concernés et demande confirmation (continue sans bloquer en mode `--yes`, mais reste visible sur stdout) ; `enregistrer_import` appelé juste après chaque `publier_localement` réussi | |
| 157 | +- [X] T049 [US3] Étendre `packages/regine-gui/src/regine_gui/import_view.py` : `_confirmer_malgre_doublons` (QMessageBox) après `copier_carte` dans `_analyser` ; `_checksums_en_cours` mémorisé comme `_titre_en_cours`/`_dossier_archive_en_cours` avant de lancer le worker, `enregistrer_import` appelé dans `_sur_publication_succes` (thread principal, jamais dans le worker — même précaution que pour la connexion SQLite déjà rencontrée en Phase 7) | |
| 158 | +- [X] T050 [P] Validé manuellement en mode headless : script import et deux fois la même carte (fake RAW+JPEG) vers deux dossiers différents (« DossierA » puis « DossierB ») — le second import affiche bien l'avertissement de doublon avec le chemin du premier dossier, avant la confirmation de copie locale. `ruff check --fix`/`ruff format` sur les trois paquets — 180/180 tests `regine-core`/`regine-cli` au vert | |
| 159 | +- [X] T051 Mettre à jour `spec.md` (Clarifications, FR-004 réécrite, Edge Case, nouvelle entité Historique des imports, SC-011, Assumptions), `data-model.md`, `contracts/cli-import.md`, `contracts/regine-core-api.md` pour refléter cette révision | |
| 160 | + | |
| 161 | +**Checkpoint** : un import détecte désormais tout fichier déjà importé précédemment (même carte non effacée, ou dossier de destination différent d'un import antérieur) et en informe l'utilisateur avant de poursuivre — plus jamais une exclusion ou une inclusion silencieuse. | |
| 162 | + | |
| 163 | +--- | |
| 164 | + | |
| 151 | 165 | ## Dependencies & Execution Order |
| 152 | 166 | |
| 153 | 167 | ### Phase Dependencies |
| @@ -148,6 +148,20 @@ Ce module est le premier point d'intégration réel de trois autres specs déjà | |||
| 148 | 148 | ||
| 149 | --- | 149 | --- |
| 150 | 150 | ||
| 151 | +## Phase 8: Révision 2026-09-22 (bis) — détection de doublons entre imports (FR-004 révisé) | ||
| 152 | + | ||
| 153 | +**Contexte** : test manuel de l'utilisateur — importer deux fois la même carte vers deux dossiers différents n'affichait aucun avertissement. Cause racine trouvée : le paramètre `checksums_deja_importes` de `copie.copier_carte` (déjà en place et testé isolément depuis T009) n'était jamais renseigné par aucun appelant réel — ni `regine_cli.import_cmd`, ni `regine_gui.import_view`. La déduplication ne fonctionnait donc dans aucun cas réel, y compris le cas initialement visé par FR-004 (carte non effacée entre deux imports). | ||
| 154 | + | ||
| 155 | +- [X] T047 [P] Créer `regine_core.import_carte.historique` (`enregistrer_import`, `rechercher_doublons`) — nouvelle table `checksums_importes` dans la base de contexte centralisée (`regine_core.config.db`, `specs/003`), même schéma d'implantation que `camera_profile.db` pour `boitiers`. Testé dans `packages/regine-core/tests/unit/test_historique.py` (4/4 tests : recherche vide, retrouve un doublon, idempotence/mise à jour du dossier, ignore les checksums inconnus) | ||
| 156 | +- [X] T048 [US1] Étendre `packages/regine-cli/src/regine_cli/import_cmd.py` : `conn` ouvert avant `copier_carte` (au lieu d'après) pour permettre `rechercher_doublons(conn, ...)` juste après la copie ; `_confirmer_malgre_doublons` affiche les fichiers/dossiers concernés et demande confirmation (continue sans bloquer en mode `--yes`, mais reste visible sur stdout) ; `enregistrer_import` appelé juste après chaque `publier_localement` réussi | ||
| 157 | +- [X] T049 [US3] Étendre `packages/regine-gui/src/regine_gui/import_view.py` : `_confirmer_malgre_doublons` (QMessageBox) après `copier_carte` dans `_analyser` ; `_checksums_en_cours` mémorisé comme `_titre_en_cours`/`_dossier_archive_en_cours` avant de lancer le worker, `enregistrer_import` appelé dans `_sur_publication_succes` (thread principal, jamais dans le worker — même précaution que pour la connexion SQLite déjà rencontrée en Phase 7) | ||
| 158 | +- [X] T050 [P] Validé manuellement en mode headless : script import et deux fois la même carte (fake RAW+JPEG) vers deux dossiers différents (« DossierA » puis « DossierB ») — le second import affiche bien l'avertissement de doublon avec le chemin du premier dossier, avant la confirmation de copie locale. `ruff check --fix`/`ruff format` sur les trois paquets — 180/180 tests `regine-core`/`regine-cli` au vert | ||
| 159 | +- [X] T051 Mettre à jour `spec.md` (Clarifications, FR-004 réécrite, Edge Case, nouvelle entité Historique des imports, SC-011, Assumptions), `data-model.md`, `contracts/cli-import.md`, `contracts/regine-core-api.md` pour refléter cette révision | ||
| 160 | + | ||
| 161 | +**Checkpoint** : un import détecte désormais tout fichier déjà importé précédemment (même carte non effacée, ou dossier de destination différent d'un import antérieur) et en informe l'utilisateur avant de poursuivre — plus jamais une exclusion ou une inclusion silencieuse. | ||
| 162 | + | ||
| 163 | +--- | ||
| 164 | + | ||
| 151 | ## Dependencies & Execution Order | 165 | ## Dependencies & Execution Order |
| 152 | 166 | ||
| 153 | ### Phase Dependencies | 167 | ### Phase Dependencies |
modified
specs/006-gui-tri-consultation/tasks.md +1 -0 | @@ -128,6 +128,7 @@ description: "Task list for feature implementation" | ||
| 128 | 128 | - [X] T029 Mettre à jour `contracts/regine-core-api.md`/`cli-dossier-selection.md`/`cli-archive-consultation.md` si l'implémentation révèle un écart avec les signatures documentées (patron déjà établi dans les specs précédentes) — tous mis à jour au fil de l'implémentation (signature `promouvoir`, renommage `browse`/`restore`, `resoudre_archive_locale`) |
| 129 | 129 | - [X] T039 [P] Exécuté le Scénario 8 de `quickstart.md` (cf. T038 ; Scénario 7 déjà couvert par `specs/001-import-photos`, sans duplication) et `ruff check --fix`/`ruff format` sur `packages/regine-gui`, `packages/regine-cli`, `packages/regine-core` (nouveau fichier `import_view.py`, nouvelle fonction publique `root_location_depuis_chemin`) — 172/172 tests `regine-core`/`regine-cli` toujours au vert, aucun nouveau test requis côté core (aucune nouvelle règle métier ajoutée par US3, seulement une fonction déjà testée indirectement via `import_cmd` rendue publique) |
| 130 | 130 | - [X] T040 [US3] Révision 2026-09-22 (cf. Clarifications de `specs/001-import-photos/spec.md`) : `import_view.py` mis à jour pour suivre le nouveau pipeline `specs/001` — `classer_par_format`/`publier_localement` remplacent `archiver`/`publier_copie_locale` dans `_PublicationLocaleWorker` (ex-`_ArchivageWorker`) ; `_analyser` n'exige plus que le partage SMB soit monté (`config.smb.chemin_local_du_partage`, calcul pur, remplace `resoudre_archive_locale`) ; dialogues/journal reformulés pour ne plus jamais mentionner un « archivage » à l'import (« Confirmer la copie de travail locale », message final indiquant la commande `regine reconcile` à lancer plus tard). Revalidé manuellement en mode headless sans archive configurée/montée : structure `raw`/`jpeg` obtenue, aucune écriture sous `/Volumes/...` — 176/176 tests `regine-core`/`regine-cli` au vert (172 + 4 issus de la révision `specs/001`, cf. son `tasks.md` Phase 7) |
| 131 | +- [X] T041 [US3] Révision 2026-09-22 (bis) — détection de doublons (FR-004 révisé, cf. `specs/001-import-photos/tasks.md` Phase 8) : couverte sans modification de spec grâce à FR-014 (« toute règle déjà vraie côté CLI reste vraie à l'identique depuis la GUI ») — `import_view._analyser` appelle `historique.rechercher_doublons` juste après `copier_carte` (nouvelle méthode `_confirmer_malgre_doublons`, `QMessageBox.question`), et `_sur_publication_succes` appelle `historique.enregistrer_import` sur le thread principal (jamais dans `_PublicationLocaleWorker`, même précaution que pour la connexion SQLite déjà rencontrée en T036). Revalidé manuellement en mode headless : import de la même carte vers deux dossiers différents déclenche bien l'avertissement au second import, avec le chemin du premier dossier — 180/180 tests `regine-core`/`regine-cli` au vert | |
| 131 | 132 | |
| 132 | 133 | --- |
| 133 | 134 | |
| @@ -128,6 +128,7 @@ description: "Task list for feature implementation" | |||
| 128 | - [X] T029 Mettre à jour `contracts/regine-core-api.md`/`cli-dossier-selection.md`/`cli-archive-consultation.md` si l'implémentation révèle un écart avec les signatures documentées (patron déjà établi dans les specs précédentes) — tous mis à jour au fil de l'implémentation (signature `promouvoir`, renommage `browse`/`restore`, `resoudre_archive_locale`) | 128 | - [X] T029 Mettre à jour `contracts/regine-core-api.md`/`cli-dossier-selection.md`/`cli-archive-consultation.md` si l'implémentation révèle un écart avec les signatures documentées (patron déjà établi dans les specs précédentes) — tous mis à jour au fil de l'implémentation (signature `promouvoir`, renommage `browse`/`restore`, `resoudre_archive_locale`) |
| 129 | - [X] T039 [P] Exécuté le Scénario 8 de `quickstart.md` (cf. T038 ; Scénario 7 déjà couvert par `specs/001-import-photos`, sans duplication) et `ruff check --fix`/`ruff format` sur `packages/regine-gui`, `packages/regine-cli`, `packages/regine-core` (nouveau fichier `import_view.py`, nouvelle fonction publique `root_location_depuis_chemin`) — 172/172 tests `regine-core`/`regine-cli` toujours au vert, aucun nouveau test requis côté core (aucune nouvelle règle métier ajoutée par US3, seulement une fonction déjà testée indirectement via `import_cmd` rendue publique) | 129 | - [X] T039 [P] Exécuté le Scénario 8 de `quickstart.md` (cf. T038 ; Scénario 7 déjà couvert par `specs/001-import-photos`, sans duplication) et `ruff check --fix`/`ruff format` sur `packages/regine-gui`, `packages/regine-cli`, `packages/regine-core` (nouveau fichier `import_view.py`, nouvelle fonction publique `root_location_depuis_chemin`) — 172/172 tests `regine-core`/`regine-cli` toujours au vert, aucun nouveau test requis côté core (aucune nouvelle règle métier ajoutée par US3, seulement une fonction déjà testée indirectement via `import_cmd` rendue publique) |
| 130 | - [X] T040 [US3] Révision 2026-09-22 (cf. Clarifications de `specs/001-import-photos/spec.md`) : `import_view.py` mis à jour pour suivre le nouveau pipeline `specs/001` — `classer_par_format`/`publier_localement` remplacent `archiver`/`publier_copie_locale` dans `_PublicationLocaleWorker` (ex-`_ArchivageWorker`) ; `_analyser` n'exige plus que le partage SMB soit monté (`config.smb.chemin_local_du_partage`, calcul pur, remplace `resoudre_archive_locale`) ; dialogues/journal reformulés pour ne plus jamais mentionner un « archivage » à l'import (« Confirmer la copie de travail locale », message final indiquant la commande `regine reconcile` à lancer plus tard). Revalidé manuellement en mode headless sans archive configurée/montée : structure `raw`/`jpeg` obtenue, aucune écriture sous `/Volumes/...` — 176/176 tests `regine-core`/`regine-cli` au vert (172 + 4 issus de la révision `specs/001`, cf. son `tasks.md` Phase 7) | 130 | - [X] T040 [US3] Révision 2026-09-22 (cf. Clarifications de `specs/001-import-photos/spec.md`) : `import_view.py` mis à jour pour suivre le nouveau pipeline `specs/001` — `classer_par_format`/`publier_localement` remplacent `archiver`/`publier_copie_locale` dans `_PublicationLocaleWorker` (ex-`_ArchivageWorker`) ; `_analyser` n'exige plus que le partage SMB soit monté (`config.smb.chemin_local_du_partage`, calcul pur, remplace `resoudre_archive_locale`) ; dialogues/journal reformulés pour ne plus jamais mentionner un « archivage » à l'import (« Confirmer la copie de travail locale », message final indiquant la commande `regine reconcile` à lancer plus tard). Revalidé manuellement en mode headless sans archive configurée/montée : structure `raw`/`jpeg` obtenue, aucune écriture sous `/Volumes/...` — 176/176 tests `regine-core`/`regine-cli` au vert (172 + 4 issus de la révision `specs/001`, cf. son `tasks.md` Phase 7) |
| 131 | +- [X] T041 [US3] Révision 2026-09-22 (bis) — détection de doublons (FR-004 révisé, cf. `specs/001-import-photos/tasks.md` Phase 8) : couverte sans modification de spec grâce à FR-014 (« toute règle déjà vraie côté CLI reste vraie à l'identique depuis la GUI ») — `import_view._analyser` appelle `historique.rechercher_doublons` juste après `copier_carte` (nouvelle méthode `_confirmer_malgre_doublons`, `QMessageBox.question`), et `_sur_publication_succes` appelle `historique.enregistrer_import` sur le thread principal (jamais dans `_PublicationLocaleWorker`, même précaution que pour la connexion SQLite déjà rencontrée en T036). Revalidé manuellement en mode headless : import de la même carte vers deux dossiers différents déclenche bien l'avertissement au second import, avec le chemin du premier dossier — 180/180 tests `regine-core`/`regine-cli` au vert | ||
| 131 | 132 | ||
| 132 | --- | 133 | --- |
| 133 | 134 | ||