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

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

duplicates

Fabien Champigny committed 2026-09-22T14:26:20+02:00 Browse files
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 (
3131 detacher_jours,
3232 jours_candidats_au_detachement,
3333 )
34+from regine_core.import_carte.historique import enregistrer_import, rechercher_doublons
3435 from regine_core.import_carte.identifiant import attribuer_identifiants
3536 from regine_core.import_carte.nommage import (
3637 construire_nom_dossier,
@@ -60,6 +61,22 @@ def _proposer_detachement(groupes: list[GroupeImport], interactif: bool) -> list
6061 return groupes
6162
6263
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+
6380 def _resoudre_etiquetage_manuel(
6481 a_etiqueter: list[list[FichierCandidat]], *, conn: sqlite3.Connection, interactif: bool
6582 ) -> None:
@@ -273,6 +290,7 @@ def _traiter_groupe(
273290 return
274291
275292 publier_localement(chemins, local_tmp, dossier_local)
293+ enregistrer_import(conn, {f.checksum for f in groupe.fichiers}, dossier_local)
276294 print(f"Copie de travail locale prête : {dossier_local}")
277295 print(
278296 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:
290308 )
291309 interactif = not args.yes
292310
293- fichiers = copier_carte(carte, local_tmp)
294- if not fichiers:
295- print("Aucun fichier nouveau à importer.")
296- return 0
297-
298311 conn = open_context_db(contexte_db)
299312 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+
300323 a_etiqueter = resoudre_collisions_boitiers(fichiers, conn=conn)
301324 _resoudre_etiquetage_manuel(a_etiqueter, conn=conn, interactif=interactif)
302325
@@ -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_identifiants35 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 groupes61 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: bool81 a_etiqueter: list[list[FichierCandidat]], *, conn: sqlite3.Connection, interactif: bool
65 ) -> None:82 ) -> None:
@@ -273,6 +290,7 @@ def _traiter_groupe(
273 return290 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.yes309 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 (
6060 detacher_jours,
6161 jours_candidats_au_detachement,
6262 )
63+from regine_core.import_carte.historique import enregistrer_import, rechercher_doublons
6364 from regine_core.import_carte.identifiant import attribuer_identifiants
6465 from regine_core.import_carte.nommage import (
6566 construire_nom_dossier,
@@ -137,6 +138,7 @@ class EcranImport(QMainWindow):
137138 self._worker: _PublicationLocaleWorker | None = None
138139 self._titre_en_cours: str | None = None
139140 self._dossier_archive_en_cours: Path | None = None
141+ self._checksums_en_cours: set[str] = set()
140142
141143 conteneur = QWidget()
142144 self.setCentralWidget(conteneur)
@@ -241,6 +243,12 @@ class EcranImport(QMainWindow):
241243 self._log("Aucun fichier nouveau à importer.")
242244 return
243245
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+
244252 a_etiqueter = resoudre_collisions_boitiers(fichiers, conn=conn)
245253 if not self._resoudre_etiquetage_manuel(a_etiqueter, conn=conn):
246254 conn.close()
@@ -266,6 +274,19 @@ class EcranImport(QMainWindow):
266274 f"Analyse terminée : {len(fichiers)} fichier(s) nouveau(x), {len(groupes)} groupe(s)."
267275 )
268276
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+
269290 def _proposer_detachement(self, groupes: list[GroupeImport]) -> list[GroupeImport]:
270291 """Propose le détachement d'un jour candidat (FR-006) — jamais automatique."""
271292 if len(groupes) != 1:
@@ -499,6 +520,7 @@ class EcranImport(QMainWindow):
499520 # illégal à la connexion SQLite et aux widgets (trouvé par test manuel).
500521 self._titre_en_cours = titre
501522 self._dossier_archive_en_cours = dossier_archive
523+ self._checksums_en_cours = {f.checksum for f in fichiers}
502524
503525 thread = QThread(self)
504526 worker = _PublicationLocaleWorker(fichiers, local_tmp, dossier_local)
@@ -516,6 +538,8 @@ class EcranImport(QMainWindow):
516538
517539 def _sur_publication_succes(self, dossier_local: Path) -> None:
518540 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)
519543 self._log(f"Copie de travail locale prête : {dossier_local}")
520544 self._log(
521545 "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_identifiants64 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 = None138 self._worker: _PublicationLocaleWorker | None = None
138 self._titre_en_cours: str | None = None139 self._titre_en_cours: str | None = None
139 self._dossier_archive_en_cours: Path | None = None140 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 return244 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 = titre521 self._titre_en_cours = titre
501 self._dossier_archive_en_cours = dossier_archive522 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_cours540 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 @@
5555 (répertoire racine affiché dans le résumé de confirmation), Key Entities (Dossier / Dossier
5656 parent), Acceptance Scenarios de User Story 3 (héritage du répertoire racine par les
5757 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.
5859 - **Révision du 2026-09-22 (clarifications)** : deux écarts trouvés par test manuel de la GUI
5960 (`specs/006-gui-tri-consultation` User Story 3) — (1) l'import poussait automatiquement vers
6061 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 / Dossier55 (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 les56 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 GUI59 - **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 vers60 (`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 locale61 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
1111
1212 **Déroulé (mode interactif, sans flags optionnels)** :
1313 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.
2021
2122 **Sorties** :
2223 - 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
2425 - Espace disque insuffisant (Edge Case) : message clair avant toute copie, code non-zéro.
2526 - Collision de nom de dossier (FR-012) : proposition de suffixe ou demande de confirmation sur stdout ; sans `--yes`, attend une réponse interactive.
2627 - **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.
2729 - **`--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`).
2830
2931 ## Notes de scriptabilité
@@ -48,3 +50,8 @@ regine import /Volumes/CARTE_SD [--titre TEXTE] [--destination nouveau|sous-doss
4850 - **É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.
4951 - `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.
5052 - 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
3838
3939 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).
4040
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+
4145 ## `identifiant.attribuer_identifiants(fichiers_maitres: list[Path]) -> dict[Path, str]`
4246
4347 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
4848 | `nom_final` | str | `date_titre_nomOrigine.ext` (FR-013) |
4949 | `fichiers_lies` | `list[Path]` | Fichiers associés (JPEG jumeau, sidecars) renommés de façon synchronisée (FR-014) |
5050
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+
5161 ## Identifiant pérenne
5262
5363 | Champ | Type | Règles |
@@ -58,7 +68,10 @@ Créé à la demande lors du classement (FR-020), pas une entité stockée — u
5868 ## Relations
5969
6070 ```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)
6275 │
6376 ▼
6477 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érenne61 ## 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 ## Relations68 ## Relations
59 69
60 ```text70 ```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
4545
4646 **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.
4747
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+
4857 ## Critères de sortie
4958
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 sortie57 ## 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 @@
1414
1515 - 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.
1616 - 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.
1718
1819 ## User Scenarios & Testing *(mandatory)*
1920
@@ -33,6 +34,7 @@ Un photographe rentre d'une sortie photo avec une carte mémoire. Il demande à
3334 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.
3435 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.
3536 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.
3638
3739 ---
3840
@@ -76,7 +78,7 @@ Un photographe est en voyage sur plusieurs villes. Pour la deuxième étape, il
7678 - 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é.
7779 - 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.
7880 - 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é).
8082 - 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.
8183 - 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.
8284 - 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
8991 - **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.
9092 - **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é.
9193 - **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.
9395 - **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.
9496 - **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.
9597 - **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
120122 - **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.
121123 - **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.
122124 - **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`).
123126
124127 ## Success Criteria *(mandatory)*
125128
@@ -135,6 +138,7 @@ Un photographe est en voyage sur plusieurs villes. Pour la deuxième étape, il
135138 - **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`).
136139 - **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.
137140 - **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.
138142
139143 ## Assumptions
140144
@@ -145,3 +149,4 @@ Un photographe est en voyage sur plusieurs villes. Pour la deuxième étape, il
145149 - 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.
146150 - 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.
147151 - 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 ## Assumptions143 ## 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à
148148
149149 ---
150150
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+
151165 ## Dependencies & Execution Order
152166
153167 ### 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 Order165 ## Dependencies & Execution Order
152 166
153 ### Phase Dependencies167 ### Phase Dependencies
modified specs/006-gui-tri-consultation/tasks.md +1 -0
@@ -128,6 +128,7 @@ description: "Task list for feature implementation"
128128 - [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`)
129129 - [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)
130130 - [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
131132
132133 ---
133134
@@ -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