plan all
a196f68 parent: 23745dd added
specs/001-import-photos/contracts/cli-import.md +31 -0 | new file mode 100644 | ||
| @@ -0,0 +1,31 @@ | ||
| 1 | +# Contrat CLI : `regine import` | |
| 2 | + | |
| 3 | +Première commande CLI concrète du projet (cf. Principe CLI-first). Protocole texte : arguments en entrée, résultat sur stdout, erreurs sur stderr, code de sortie non-zéro en cas d'échec. Interactive par nature (titre, destination, catégorie, résolutions de collision sont des dialogues), mais chaque question DOIT rester pilotable par flags pour un usage scripté (cf. `--yes`/valeurs explicites ci-dessous), cohérent avec `specs/003-config-contexte-travail/contracts/cli-config.md`. | |
| 4 | + | |
| 5 | +## `regine import <chemin_carte>` | |
| 6 | + | |
| 7 | +**Entrée** : | |
| 8 | +``` | |
| 9 | +regine import /Volumes/CARTE_SD [--titre TEXTE] [--destination nouveau|sous-dossier:ID|fusion:ID|parent] [--categorie NOM | --annee] [--yes] | |
| 10 | +``` | |
| 11 | + | |
| 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). | |
| 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) et attribution de l'identifiant pérenne (FR-017). | |
| 18 | +6. Résumé complet par groupe (fichiers, taille, dossier de destination avec répertoire racine) et confirmation explicite avant écriture sur l'archive (FR-018). | |
| 19 | +7. Transfert final vérifié (FR-019). | |
| 20 | + | |
| 21 | +**Sorties** : | |
| 22 | +- Succès : récapitulatif des dossiers archivés sur stdout, code `0`. | |
| 23 | +- Échec de vérification d'un fichier (Edge Case lecture corrompue) : fichier signalé sur stderr, import interrompu pour ce fichier, carte non marquée sûre à effacer, code non-zéro. | |
| 24 | +- 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 | +- **`--destination fusion:ID` ciblant un dossier présent uniquement dans l'archive (pas en local)** : commande refusée avec un message explicite indiquant que ce cas dépend du mécanisme de checkout/réconciliation, **non implémenté dans cette version** (cf. `plan.md` § Complexity Tracking) — code non-zéro, aucune écriture. | |
| 27 | + | |
| 28 | +## Notes de scriptabilité | |
| 29 | + | |
| 30 | +- `--yes` accepte les propositions par défaut (groupe unique non découpé, pas de catégorie) sans les demander interactivement ; ne bipasse jamais la confirmation finale d'écriture sur l'archive (FR-018 reste dû même en mode non interactif — nécessite `--yes` explicitement à ce niveau aussi, jamais implicite). | |
| 31 | +- `--categorie` et `--annee` sont mutuellement exclusifs ; en leur absence en mode interactif, la question est posée normalement. | |
| new file mode 100644 | |||
| @@ -0,0 +1,31 @@ | |||
| 1 | +# Contrat CLI : `regine import` | ||
| 2 | + | ||
| 3 | +Première commande CLI concrète du projet (cf. Principe CLI-first). Protocole texte : arguments en entrée, résultat sur stdout, erreurs sur stderr, code de sortie non-zéro en cas d'échec. Interactive par nature (titre, destination, catégorie, résolutions de collision sont des dialogues), mais chaque question DOIT rester pilotable par flags pour un usage scripté (cf. `--yes`/valeurs explicites ci-dessous), cohérent avec `specs/003-config-contexte-travail/contracts/cli-config.md`. | ||
| 4 | + | ||
| 5 | +## `regine import <chemin_carte>` | ||
| 6 | + | ||
| 7 | +**Entrée** : | ||
| 8 | +``` | ||
| 9 | +regine import /Volumes/CARTE_SD [--titre TEXTE] [--destination nouveau|sous-dossier:ID|fusion:ID|parent] [--categorie NOM | --annee] [--yes] | ||
| 10 | +``` | ||
| 11 | + | ||
| 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). | ||
| 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) et attribution de l'identifiant pérenne (FR-017). | ||
| 18 | +6. Résumé complet par groupe (fichiers, taille, dossier de destination avec répertoire racine) et confirmation explicite avant écriture sur l'archive (FR-018). | ||
| 19 | +7. Transfert final vérifié (FR-019). | ||
| 20 | + | ||
| 21 | +**Sorties** : | ||
| 22 | +- Succès : récapitulatif des dossiers archivés sur stdout, code `0`. | ||
| 23 | +- Échec de vérification d'un fichier (Edge Case lecture corrompue) : fichier signalé sur stderr, import interrompu pour ce fichier, carte non marquée sûre à effacer, code non-zéro. | ||
| 24 | +- 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 | +- **`--destination fusion:ID` ciblant un dossier présent uniquement dans l'archive (pas en local)** : commande refusée avec un message explicite indiquant que ce cas dépend du mécanisme de checkout/réconciliation, **non implémenté dans cette version** (cf. `plan.md` § Complexity Tracking) — code non-zéro, aucune écriture. | ||
| 27 | + | ||
| 28 | +## Notes de scriptabilité | ||
| 29 | + | ||
| 30 | +- `--yes` accepte les propositions par défaut (groupe unique non découpé, pas de catégorie) sans les demander interactivement ; ne bipasse jamais la confirmation finale d'écriture sur l'archive (FR-018 reste dû même en mode non interactif — nécessite `--yes` explicitement à ce niveau aussi, jamais implicite). | ||
| 31 | +- `--categorie` et `--annee` sont mutuellement exclusifs ; en leur absence en mode interactif, la question est posée normalement. | ||
added
specs/001-import-photos/contracts/regine-core-api.md +43 -0 | new file mode 100644 | ||
| @@ -0,0 +1,43 @@ | ||
| 1 | +# Contrat d'API interne : `regine_core.import_carte` | |
| 2 | + | |
| 3 | +Fonctions pures/orchestratrices consommées par `regine-cli` (`regine import`, cf. `contracts/cli-import.md`) — objets structurés, jamais de texte à parser (Principe VI). | |
| 4 | + | |
| 5 | +## `copie.copier_carte(carte: Path, local_tmp: Path) -> list[FichierCandidat]` | |
| 6 | + | |
| 7 | +Copie vérifiée (FR-001), une seule lecture de la carte par fichier. Retourne uniquement les fichiers réellement nouveaux (FR-004, `deja_importe=False` filtré côté appelant ou directement exclu ici). Lève une erreur par fichier en échec de vérification, sans interrompre les autres (Edge Case). | |
| 8 | + | |
| 9 | +## `groupage.decouper_en_groupes(fichiers: list[FichierCandidat]) -> list[GroupeImport]` | |
| 10 | + | |
| 11 | +Construit la répartition jour par jour, exclut les dates aberrantes (FR-002/003), propose un groupe unique par défaut avec les candidats au détachement mis en avant (FR-005/006, cf. research.md § 5). Ne détache jamais automatiquement. | |
| 12 | + | |
| 13 | +## `destination.resoudre_destination(groupe: GroupeImport, choix: ChoixUtilisateur) -> DestinationChoisie` | |
| 14 | + | |
| 15 | +Traduit le choix de l'utilisateur (FR-007) en `DestinationChoisie`. Pour `nouveau_dossier`/`nouveau_parent`, appelle `regine_core.dossier.root.determine_root` (`specs/004-categorisation-dossiers`) avec la catégorie éventuellement choisie. Pour `nouveau_sous_dossier`, réutilise le `RootLocation` du parent sans nouvel appel (FR-006 de specs/004). Si `necessite_checkout_archive` est vrai (fusion vers un dossier archivé, pas local), **lève `ChecoutNonDisponibleError`** plutôt que d'échouer silencieusement — cf. `plan.md` § Complexity Tracking. | |
| 16 | + | |
| 17 | +## `destination.lister_dossiers_candidats(titre_partiel: str, date_proche: date) -> list[Path]` | |
| 18 | + | |
| 19 | +Recherche de dossiers candidats pour `nouveau_sous_dossier`/`fusion` (FR-008), par proximité de titre et de date, en local et dans l'archive (cf. research.md § 4). Retourne une liste triée par pertinence, vide si aucun candidat. | |
| 20 | + | |
| 21 | +## `nommage.construire_nom_dossier(groupe: GroupeImport, root: RootLocation) -> Path` | |
| 22 | + | |
| 23 | +Construit le chemin final (FR-010), vérifie l'absence de collision **au sein du même `RootLocation`** (FR-012, cf. `specs/004-categorisation-dossiers`) ; propose un suffixe en cas de collision plutôt que d'écraser. | |
| 24 | + | |
| 25 | +## `nommage.renommer_fichiers(groupe: GroupeImport, titre: str, dossier: Path) -> list[Renommage]` | |
| 26 | + | |
| 27 | +Renomme chaque fichier maître (`date_titre_nomOrigine.ext`, FR-013) et ses fichiers associés de façon synchronisée (FR-014). | |
| 28 | + | |
| 29 | +## `identifiant.attribuer_identifiants(fichiers: list[Path]) -> dict[Path, str]` | |
| 30 | + | |
| 31 | +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. | |
| 32 | + | |
| 33 | +## `push.preparer_resume(groupe: GroupeImport) -> ResumeConfirmation` | |
| 34 | + | |
| 35 | +Construit l'objet structuré (nombre de fichiers, taille totale, chemin de destination avec répertoire racine) consommé par `regine-cli` pour l'affichage et la confirmation (FR-018). | |
| 36 | + | |
| 37 | +## `push.archiver(groupe: GroupeImport, resume_confirme: bool) -> None` | |
| 38 | + | |
| 39 | +Transfert final vérifié depuis la copie locale déjà renommée (FR-019). Lève une erreur si `resume_confirme` est faux — ne DOIT jamais être appelée sans confirmation explicite préalable côté appelant. | |
| 40 | + | |
| 41 | +## Dépendance bloquée | |
| 42 | + | |
| 43 | +`ChecoutNonDisponibleError` (levée par `resoudre_destination`) documente explicitement le sous-scénario FR-009 non implémenté : fusion vers un dossier présent uniquement dans l'archive. À lever tant qu'aucune spec/plan du mécanisme de checkout/réconciliation n'existe dans ce dépôt (cf. `plan.md` § Complexity Tracking, `research.md` § 6). | |
| new file mode 100644 | |||
| @@ -0,0 +1,43 @@ | |||
| 1 | +# Contrat d'API interne : `regine_core.import_carte` | ||
| 2 | + | ||
| 3 | +Fonctions pures/orchestratrices consommées par `regine-cli` (`regine import`, cf. `contracts/cli-import.md`) — objets structurés, jamais de texte à parser (Principe VI). | ||
| 4 | + | ||
| 5 | +## `copie.copier_carte(carte: Path, local_tmp: Path) -> list[FichierCandidat]` | ||
| 6 | + | ||
| 7 | +Copie vérifiée (FR-001), une seule lecture de la carte par fichier. Retourne uniquement les fichiers réellement nouveaux (FR-004, `deja_importe=False` filtré côté appelant ou directement exclu ici). Lève une erreur par fichier en échec de vérification, sans interrompre les autres (Edge Case). | ||
| 8 | + | ||
| 9 | +## `groupage.decouper_en_groupes(fichiers: list[FichierCandidat]) -> list[GroupeImport]` | ||
| 10 | + | ||
| 11 | +Construit la répartition jour par jour, exclut les dates aberrantes (FR-002/003), propose un groupe unique par défaut avec les candidats au détachement mis en avant (FR-005/006, cf. research.md § 5). Ne détache jamais automatiquement. | ||
| 12 | + | ||
| 13 | +## `destination.resoudre_destination(groupe: GroupeImport, choix: ChoixUtilisateur) -> DestinationChoisie` | ||
| 14 | + | ||
| 15 | +Traduit le choix de l'utilisateur (FR-007) en `DestinationChoisie`. Pour `nouveau_dossier`/`nouveau_parent`, appelle `regine_core.dossier.root.determine_root` (`specs/004-categorisation-dossiers`) avec la catégorie éventuellement choisie. Pour `nouveau_sous_dossier`, réutilise le `RootLocation` du parent sans nouvel appel (FR-006 de specs/004). Si `necessite_checkout_archive` est vrai (fusion vers un dossier archivé, pas local), **lève `ChecoutNonDisponibleError`** plutôt que d'échouer silencieusement — cf. `plan.md` § Complexity Tracking. | ||
| 16 | + | ||
| 17 | +## `destination.lister_dossiers_candidats(titre_partiel: str, date_proche: date) -> list[Path]` | ||
| 18 | + | ||
| 19 | +Recherche de dossiers candidats pour `nouveau_sous_dossier`/`fusion` (FR-008), par proximité de titre et de date, en local et dans l'archive (cf. research.md § 4). Retourne une liste triée par pertinence, vide si aucun candidat. | ||
| 20 | + | ||
| 21 | +## `nommage.construire_nom_dossier(groupe: GroupeImport, root: RootLocation) -> Path` | ||
| 22 | + | ||
| 23 | +Construit le chemin final (FR-010), vérifie l'absence de collision **au sein du même `RootLocation`** (FR-012, cf. `specs/004-categorisation-dossiers`) ; propose un suffixe en cas de collision plutôt que d'écraser. | ||
| 24 | + | ||
| 25 | +## `nommage.renommer_fichiers(groupe: GroupeImport, titre: str, dossier: Path) -> list[Renommage]` | ||
| 26 | + | ||
| 27 | +Renomme chaque fichier maître (`date_titre_nomOrigine.ext`, FR-013) et ses fichiers associés de façon synchronisée (FR-014). | ||
| 28 | + | ||
| 29 | +## `identifiant.attribuer_identifiants(fichiers: list[Path]) -> dict[Path, str]` | ||
| 30 | + | ||
| 31 | +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. | ||
| 32 | + | ||
| 33 | +## `push.preparer_resume(groupe: GroupeImport) -> ResumeConfirmation` | ||
| 34 | + | ||
| 35 | +Construit l'objet structuré (nombre de fichiers, taille totale, chemin de destination avec répertoire racine) consommé par `regine-cli` pour l'affichage et la confirmation (FR-018). | ||
| 36 | + | ||
| 37 | +## `push.archiver(groupe: GroupeImport, resume_confirme: bool) -> None` | ||
| 38 | + | ||
| 39 | +Transfert final vérifié depuis la copie locale déjà renommée (FR-019). Lève une erreur si `resume_confirme` est faux — ne DOIT jamais être appelée sans confirmation explicite préalable côté appelant. | ||
| 40 | + | ||
| 41 | +## Dépendance bloquée | ||
| 42 | + | ||
| 43 | +`ChecoutNonDisponibleError` (levée par `resoudre_destination`) documente explicitement le sous-scénario FR-009 non implémenté : fusion vers un dossier présent uniquement dans l'archive. À lever tant qu'aucune spec/plan du mécanisme de checkout/réconciliation n'existe dans ce dépôt (cf. `plan.md` § Complexity Tracking, `research.md` § 6). | ||
added
specs/001-import-photos/data-model.md +83 -0 | new file mode 100644 | ||
| @@ -0,0 +1,83 @@ | ||
| 1 | +# Data Model: Importation de photos depuis une carte mémoire | |
| 2 | + | |
| 3 | +Entités dérivées de `spec.md` § Key Entities et Functional Requirements. Réutilise sans les redéfinir : `RootLocation` (`specs/004-categorisation-dossiers`), `CameraTags`/`CollisionResolution`/`boitiers` (`specs/002-profil-boitiers-optionnel`). | |
| 4 | + | |
| 5 | +## Fichier candidat à l'import | |
| 6 | + | |
| 7 | +| Champ | Type | Règles | | |
| 8 | +|---|---|---| | |
| 9 | +| `chemin_source` | Path | Emplacement sur la carte mémoire | | |
| 10 | +| `checksum` | str (SHA-256) | Calculé au fil de la copie (FR-001) | | |
| 11 | +| `deja_importe` | bool | `True` si ce checksum figure déjà dans un import précédent (FR-004) — exclu de l'analyse si `True` | | |
| 12 | +| `date_prise_vue` | `datetime \| None` | Tag EXIF `DateTimeOriginal` ; `None` si absent ou aberrant (FR-003) | | |
| 13 | +| `date_aberrante` | bool | `True` si `date_prise_vue` est hors plage plausible (ex. avant 1990) — exclu du calcul de plage (FR-003) | | |
| 14 | +| `type` | énumération : `maitre` \| `associe` | Fichier maître (RAW/JPEG/TIFF issu du boîtier) ou fichier associé (sidecar) — cf. constitution Principe I | | |
| 15 | + | |
| 16 | +## Groupe d'import | |
| 17 | + | |
| 18 | +| Champ | Type | Règles | | |
| 19 | +|---|---|---| | |
| 20 | +| `fichiers` | `list[FichierCandidat]` | Sous-ensemble de fichiers nouveaux partageant une plage de dates contiguë (FR-005) | | |
| 21 | +| `plage_dates` | `(date, date)` | Bornes de la plage couverte par le groupe | | |
| 22 | +| `titre` | `str \| None` | Saisi par l'utilisateur (FR-010) | | |
| 23 | +| `destination` | `DestinationChoisie` | Résultat de l'étape FR-007 | | |
| 24 | + | |
| 25 | +## DestinationChoisie | |
| 26 | + | |
| 27 | +| Champ | Type | Règles | | |
| 28 | +|---|---|---| | |
| 29 | +| `type` | énumération : `nouveau_dossier` \| `nouveau_sous_dossier` \| `fusion` \| `nouveau_parent` | Choix explicite de l'utilisateur (FR-007) | | |
| 30 | +| `dossier_cible` | `Path \| None` | Requis si `type in {nouveau_sous_dossier, fusion}` | | |
| 31 | +| `root_location` | `RootLocation \| None` | Résolu via `regine_core.dossier.root.determine_root` (specs/004) uniquement si `type in {nouveau_dossier, nouveau_parent}` ; hérité du parent sinon (FR-006 de specs/004) | | |
| 32 | +| `necessite_checkout_archive` | bool | `True` si `type == fusion` et `dossier_cible` n'existe qu'archivé, pas en local (FR-009) — **déclenche le sous-scénario bloqué, cf. plan.md § Complexity Tracking** | | |
| 33 | + | |
| 34 | +## Renommage | |
| 35 | + | |
| 36 | +| Champ | Type | Règles | | |
| 37 | +|---|---|---| | |
| 38 | +| `nom_origine` | str | Nom donné par le boîtier (avant import) | | |
| 39 | +| `nom_final` | str | `date_titre_nomOrigine.ext` (FR-013) | | |
| 40 | +| `fichiers_lies` | `list[Path]` | Fichiers associés (JPEG jumeau, sidecars) renommés de façon synchronisée (FR-014) | | |
| 41 | + | |
| 42 | +## Identifiant pérenne | |
| 43 | + | |
| 44 | +| Champ | Type | Règles | | |
| 45 | +|---|---|---| | |
| 46 | +| `valeur` | str (UUID) | Généré à l'import, un par fichier maître (FR-017) | | |
| 47 | +| `champ_xmp` | `"xmpMM:DocumentID"` | Champ standard réutilisé (cf. research.md § 3), écrit via exiftool | | |
| 48 | + | |
| 49 | +## Relations | |
| 50 | + | |
| 51 | +```text | |
| 52 | +Carte mémoire ──▶ FichierCandidat (checksum, date) ──▶ Groupe d'import (découpage jour par jour) | |
| 53 | + │ | |
| 54 | + ▼ | |
| 55 | + DestinationChoisie ──▶ RootLocation (specs/004) | |
| 56 | + │ | |
| 57 | + ▼ | |
| 58 | + Renommage (par fichier maître + fichiers liés) | |
| 59 | + │ | |
| 60 | + ▼ | |
| 61 | + Identifiant pérenne (écrit en XMP) ──▶ Confirmation ──▶ Archive | |
| 62 | +``` | |
| 63 | + | |
| 64 | +Collision de nom d'origine entre deux `FichierCandidat` de même `nom_origine` mais `checksum` différent → déléguée à `regine_core.camera_profile.resolve_collision` (`specs/002-profil-boitiers-optionnel`), pas redéfinie ici. | |
| 65 | + | |
| 66 | +## État / transitions (par groupe d'import) | |
| 67 | + | |
| 68 | +```text | |
| 69 | +[Groupe d'import] | |
| 70 | + Proposé (découpage automatique, titre non saisi) | |
| 71 | + │ utilisateur détache des jours (FR-005/006) | |
| 72 | + ▼ | |
| 73 | + Découpé en sous-groupes (chacun redevient "Proposé") | |
| 74 | + │ utilisateur saisit titre + destination (+ catégorie si nouveau_dossier/nouveau_parent) | |
| 75 | + ▼ | |
| 76 | + Destination résolue (RootLocation déterminé ou hérité) | |
| 77 | + │ renommage local (FR-013/014) + identifiant pérenne (FR-017) | |
| 78 | + ▼ | |
| 79 | + Prêt pour archivage (résumé affiché, FR-018) | |
| 80 | + │ confirmation explicite de l'utilisateur | |
| 81 | + ▼ | |
| 82 | + Archivé (FR-019) | |
| 83 | +``` | |
| new file mode 100644 | |||
| @@ -0,0 +1,83 @@ | |||
| 1 | +# Data Model: Importation de photos depuis une carte mémoire | ||
| 2 | + | ||
| 3 | +Entités dérivées de `spec.md` § Key Entities et Functional Requirements. Réutilise sans les redéfinir : `RootLocation` (`specs/004-categorisation-dossiers`), `CameraTags`/`CollisionResolution`/`boitiers` (`specs/002-profil-boitiers-optionnel`). | ||
| 4 | + | ||
| 5 | +## Fichier candidat à l'import | ||
| 6 | + | ||
| 7 | +| Champ | Type | Règles | | ||
| 8 | +|---|---|---| | ||
| 9 | +| `chemin_source` | Path | Emplacement sur la carte mémoire | | ||
| 10 | +| `checksum` | str (SHA-256) | Calculé au fil de la copie (FR-001) | | ||
| 11 | +| `deja_importe` | bool | `True` si ce checksum figure déjà dans un import précédent (FR-004) — exclu de l'analyse si `True` | | ||
| 12 | +| `date_prise_vue` | `datetime \| None` | Tag EXIF `DateTimeOriginal` ; `None` si absent ou aberrant (FR-003) | | ||
| 13 | +| `date_aberrante` | bool | `True` si `date_prise_vue` est hors plage plausible (ex. avant 1990) — exclu du calcul de plage (FR-003) | | ||
| 14 | +| `type` | énumération : `maitre` \| `associe` | Fichier maître (RAW/JPEG/TIFF issu du boîtier) ou fichier associé (sidecar) — cf. constitution Principe I | | ||
| 15 | + | ||
| 16 | +## Groupe d'import | ||
| 17 | + | ||
| 18 | +| Champ | Type | Règles | | ||
| 19 | +|---|---|---| | ||
| 20 | +| `fichiers` | `list[FichierCandidat]` | Sous-ensemble de fichiers nouveaux partageant une plage de dates contiguë (FR-005) | | ||
| 21 | +| `plage_dates` | `(date, date)` | Bornes de la plage couverte par le groupe | | ||
| 22 | +| `titre` | `str \| None` | Saisi par l'utilisateur (FR-010) | | ||
| 23 | +| `destination` | `DestinationChoisie` | Résultat de l'étape FR-007 | | ||
| 24 | + | ||
| 25 | +## DestinationChoisie | ||
| 26 | + | ||
| 27 | +| Champ | Type | Règles | | ||
| 28 | +|---|---|---| | ||
| 29 | +| `type` | énumération : `nouveau_dossier` \| `nouveau_sous_dossier` \| `fusion` \| `nouveau_parent` | Choix explicite de l'utilisateur (FR-007) | | ||
| 30 | +| `dossier_cible` | `Path \| None` | Requis si `type in {nouveau_sous_dossier, fusion}` | | ||
| 31 | +| `root_location` | `RootLocation \| None` | Résolu via `regine_core.dossier.root.determine_root` (specs/004) uniquement si `type in {nouveau_dossier, nouveau_parent}` ; hérité du parent sinon (FR-006 de specs/004) | | ||
| 32 | +| `necessite_checkout_archive` | bool | `True` si `type == fusion` et `dossier_cible` n'existe qu'archivé, pas en local (FR-009) — **déclenche le sous-scénario bloqué, cf. plan.md § Complexity Tracking** | | ||
| 33 | + | ||
| 34 | +## Renommage | ||
| 35 | + | ||
| 36 | +| Champ | Type | Règles | | ||
| 37 | +|---|---|---| | ||
| 38 | +| `nom_origine` | str | Nom donné par le boîtier (avant import) | | ||
| 39 | +| `nom_final` | str | `date_titre_nomOrigine.ext` (FR-013) | | ||
| 40 | +| `fichiers_lies` | `list[Path]` | Fichiers associés (JPEG jumeau, sidecars) renommés de façon synchronisée (FR-014) | | ||
| 41 | + | ||
| 42 | +## Identifiant pérenne | ||
| 43 | + | ||
| 44 | +| Champ | Type | Règles | | ||
| 45 | +|---|---|---| | ||
| 46 | +| `valeur` | str (UUID) | Généré à l'import, un par fichier maître (FR-017) | | ||
| 47 | +| `champ_xmp` | `"xmpMM:DocumentID"` | Champ standard réutilisé (cf. research.md § 3), écrit via exiftool | | ||
| 48 | + | ||
| 49 | +## Relations | ||
| 50 | + | ||
| 51 | +```text | ||
| 52 | +Carte mémoire ──▶ FichierCandidat (checksum, date) ──▶ Groupe d'import (découpage jour par jour) | ||
| 53 | + │ | ||
| 54 | + ▼ | ||
| 55 | + DestinationChoisie ──▶ RootLocation (specs/004) | ||
| 56 | + │ | ||
| 57 | + ▼ | ||
| 58 | + Renommage (par fichier maître + fichiers liés) | ||
| 59 | + │ | ||
| 60 | + ▼ | ||
| 61 | + Identifiant pérenne (écrit en XMP) ──▶ Confirmation ──▶ Archive | ||
| 62 | +``` | ||
| 63 | + | ||
| 64 | +Collision de nom d'origine entre deux `FichierCandidat` de même `nom_origine` mais `checksum` différent → déléguée à `regine_core.camera_profile.resolve_collision` (`specs/002-profil-boitiers-optionnel`), pas redéfinie ici. | ||
| 65 | + | ||
| 66 | +## État / transitions (par groupe d'import) | ||
| 67 | + | ||
| 68 | +```text | ||
| 69 | +[Groupe d'import] | ||
| 70 | + Proposé (découpage automatique, titre non saisi) | ||
| 71 | + │ utilisateur détache des jours (FR-005/006) | ||
| 72 | + ▼ | ||
| 73 | + Découpé en sous-groupes (chacun redevient "Proposé") | ||
| 74 | + │ utilisateur saisit titre + destination (+ catégorie si nouveau_dossier/nouveau_parent) | ||
| 75 | + ▼ | ||
| 76 | + Destination résolue (RootLocation déterminé ou hérité) | ||
| 77 | + │ renommage local (FR-013/014) + identifiant pérenne (FR-017) | ||
| 78 | + ▼ | ||
| 79 | + Prêt pour archivage (résumé affiché, FR-018) | ||
| 80 | + │ confirmation explicite de l'utilisateur | ||
| 81 | + ▼ | ||
| 82 | + Archivé (FR-019) | ||
| 83 | +``` | ||
added
specs/001-import-photos/plan.md +109 -0 | new file mode 100644 | ||
| @@ -0,0 +1,109 @@ | ||
| 1 | +# Implementation Plan: Importation de photos depuis une carte mémoire | |
| 2 | + | |
| 3 | +**Branch**: `001-import-photos` | **Date**: 2026-09-18 | **Spec**: [spec.md](./spec.md) | |
| 4 | + | |
| 5 | +**Input**: Feature specification from `/specs/001-import-photos/spec.md` | |
| 6 | + | |
| 7 | +## Summary | |
| 8 | + | |
| 9 | +Pipeline complet depuis une carte mémoire jusqu'au premier archivage : copie vérifiée par somme de contrôle (une seule lecture de la carte), analyse des dates de prise de vue et découpage en groupes, choix de destination (dossier simple / sous-dossier / fusion / nouveau dossier parent, plus la catégorisation racine de `specs/004-categorisation-dossiers`), renommage synchronisé des fichiers maîtres et associés, désambiguïsation de boîtiers en cas de collision (`specs/002-profil-boitiers-optionnel`), attribution d'un identifiant pérenne, puis confirmation explicite et transfert vers l'archive. Approche technique : un nouveau module `regine_core.import_carte`, premier consommateur réel de `regine_core.dossier.root` (specs/004), `regine_core.camera_profile` (specs/002) et `regine_core.metadata.exif`, exposé via une première commande CLI concrète `regine import` dans `regine-cli`. | |
| 10 | + | |
| 11 | +## Technical Context | |
| 12 | + | |
| 13 | +**Language/Version**: Python 3.11+ (cohérent avec `regine-core`, cf. `specs/002`, `specs/003`, `specs/004`) | |
| 14 | + | |
| 15 | +**Primary Dependencies**: `exiftool` (déjà une dépendance actée par `specs/002-profil-boitiers-optionnel` pour `regine_core.metadata.exif`, étendu ici pour la lecture de la date de prise de vue et l'écriture de l'identifiant pérenne) ; bibliothèque standard uniquement sinon (`hashlib` pour les sommes de contrôle, `pathlib`/`shutil` pour la copie et le renommage, `difflib` pour la recherche de dossiers candidats par titre proche, réutilisant la même approche que `specs/004-categorisation-dossiers`) | |
| 16 | + | |
| 17 | +**Storage**: ce module n'introduit aucune nouvelle base ; il consomme la base de contexte centralisée via `regine_core.camera_profile` (specs/002) et `regine_core.config.categories`/`regine_core.dossier.root` (specs/004). Le manifeste persistant par dossier (checksums, réconciliation — constitution § Workflow d'archivage) relève d'un futur module `archive`/`dossier` non encore spécifié ; ce plan ne le redéfinit pas (cf. Constraints ci-dessous) | |
| 18 | + | |
| 19 | +**Testing**: pytest ; tests unitaires sur le découpage jour par jour et la détection de dates aberrantes, sur le renommage synchronisé, sur la construction du chemin final (délégué à `specs/004`) ; tests d'intégration sur le pipeline complet copie→analyse→destination→renommage→confirmation, en répertoires temporaires simulant carte/local/archive (aucune écriture réseau réelle) | |
| 20 | + | |
| 21 | +**Target Platform**: poste de travail de bureau, identique aux autres modules de `regine-core` | |
| 22 | + | |
| 23 | +**Project Type**: Monorepo existant (`specs/002`, `specs/003`, `specs/004`) — nouveau module `regine_core/import_carte/` et première commande CLI concrète (`regine import`) dans `regine-cli` | |
| 24 | + | |
| 25 | +**Performance Goals**: aucune lecture de la carte mémoire ne doit dépasser une seule passe par fichier (FR-001/Edge Case déjà tranché) ; pas d'autre cible chiffrée spécifique | |
| 26 | + | |
| 27 | +**Constraints**: le sous-scénario de fusion vers un dossier **déjà archivé** sur le NAS (FR-009, Acceptance Scenario 4 de User Story 3) dépend du mécanisme de checkout/réconciliation par hash (constitution § Workflow d'archivage, notes de conception section 8), qui n'a **aucune spec ni plan dans ce dépôt à ce jour** — ce plan implémente tout le reste du pipeline et documente ce sous-scénario comme bloqué (cf. Complexity Tracking) ; la fusion vers un dossier présent seulement en local reste pleinement implémentable | |
| 28 | + | |
| 29 | +**Scale/Scope**: import typique de quelques dizaines à quelques milliers de fichiers par carte mémoire, usage mono-utilisateur | |
| 30 | + | |
| 31 | +## Constitution Check | |
| 32 | + | |
| 33 | +*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.* | |
| 34 | + | |
| 35 | +| Principe / contrainte | Évaluation | Justification | | |
| 36 | +|---|---|---| | |
| 37 | +| I. Fichier maître intouchable | PASS | Un fichier maître n'est renommé qu'une seule fois, avant son premier archivage (FR-013) ; aucune modification après archivage n'est effectuée par ce module (la vérification d'anomalie ultérieure relève du futur module de réconciliation, hors périmètre). | | |
| 38 | +| II. Confirmation explicite avant toute action à risque | PASS | FR-018 : résumé complet (fichiers, taille, dossier de destination, répertoire racine) présenté et confirmé avant toute écriture sur l'archive ; FR-012 : collision de nom traitée par suffixe ou confirmation, jamais écrasement silencieux. | | |
| 39 | +| III. Identité par contenu, jamais par nom de fichier seul | PASS | FR-004 : seuls les fichiers réellement nouveaux (par somme de contrôle) entrent dans l'analyse ; FR-015 : désambiguïsation de boîtiers déléguée à `specs/002`, elle-même fondée sur les métadonnées, jamais sur le seul nom de fichier. | | |
| 40 | +| IV. Métadonnées ouvertes et embarquées | PASS | FR-017 : l'identifiant pérenne est écrit dans les métadonnées XMP embarquées du fichier (réutilisation du champ standard `xmpMM:DocumentID`, cf. research.md § 3), pas dans une base séparée. | | |
| 41 | +| V. L'utilisateur décide, Régine suggère | PASS | FR-006 (candidat de détachement suggéré, jamais imposé), FR-011 (lieu GPS suggéré, jamais imposé), FR-007 (catégorie/année toujours une décision explicite, délégué à `specs/004`). | | |
| 42 | +| VI. Bibliothèque centrale, façades minces | PASS | Toute la logique vit dans `regine_core.import_carte`, qui consomme `regine_core.dossier`, `regine_core.camera_profile` et `regine_core.metadata` sans dupliquer leur logique ; `regine-cli` n'orchestre que l'appel et l'affichage. | | |
| 43 | +| CLI-first | PASS | `regine import` est la première façade construite pour ce pipeline, avant toute GUI. | | |
| 44 | +| Exécution sans démon | PASS | Le pipeline s'exécute intégralement dans le processus de la commande CLI, sans état en arrière-plan. | | |
| 45 | +| Formats ouverts et documentés | PASS | XMP pour l'identifiant pérenne ; aucune base propriétaire introduite. | | |
| 46 | +| Vérification d'intégrité systématique | PASS | FR-001 : vérification par somme de contrôle sur toute copie carte→local et sur le transfert local→archive (FR-019). | | |
| 47 | + | |
| 48 | +**Violation à justifier** (Complexity Tracking) : le sous-scénario FR-009 (fusion vers dossier déjà archivé) ne peut pas être implémenté sans le mécanisme de checkout/réconciliation, absent du dépôt. Ce n'est pas une violation de principe mais une dépendance externe manquante — documentée ci-dessous plutôt que contournée par une implémentation partielle ou incorrecte. | |
| 49 | + | |
| 50 | +## Complexity Tracking | |
| 51 | + | |
| 52 | +> Rempli exceptionnellement pour documenter une dépendance bloquante, pas une violation de principe. | |
| 53 | + | |
| 54 | +| Élément | Pourquoi nécessaire | Alternative plus simple écartée | | |
| 55 | +|---|---|---| | |
| 56 | +| FR-009 (fusion vers dossier déjà archivé, nécessite un checkout) | Explicitement requis par `specs/001-import-photos` User Story 3 Acceptance Scenario 4 | Implémenter un checkout minimal ad hoc dans ce module plutôt que d'attendre une spec dédiée — écarté : dupliquerait par anticipation un mécanisme déjà nommé et cadré ailleurs (constitution § Workflow d'archivage, notes de conception section 8), avec un risque réel de divergence si le futur module `archive`/`dossier` retient une conception différente. Le sous-scénario reste documenté comme bloqué plutôt que mal implémenté. | | |
| 57 | + | |
| 58 | +**Re-check post Phase 1** (après génération de `data-model.md`, `contracts/`, `quickstart.md`) : le modèle de données (Groupe d'import, Destination, Renommage) et les contrats (CLI `regine import`, API interne `regine_core.import_carte`) ne modifient aucune évaluation ci-dessus ; le sous-scénario FR-009 reste explicitement marqué bloqué dans `contracts/cli-import.md` et `quickstart.md` plutôt que silencieusement omis. | |
| 59 | + | |
| 60 | +## Project Structure | |
| 61 | + | |
| 62 | +### Documentation (this feature) | |
| 63 | + | |
| 64 | +```text | |
| 65 | +specs/001-import-photos/ | |
| 66 | +├── plan.md # This file (/speckit-plan command output) | |
| 67 | +├── research.md # Phase 0 output (/speckit-plan command) | |
| 68 | +├── data-model.md # Phase 1 output (/speckit-plan command) | |
| 69 | +├── quickstart.md # Phase 1 output (/speckit-plan command) | |
| 70 | +├── contracts/ # Phase 1 output (/speckit-plan command) | |
| 71 | +│ ├── cli-import.md | |
| 72 | +│ └── regine-core-api.md | |
| 73 | +└── tasks.md # Phase 2 output (/speckit-tasks command - NOT created by /speckit-plan) | |
| 74 | +``` | |
| 75 | + | |
| 76 | +### Source Code (repository root) — extension du monorepo posé par specs/002/003/004 | |
| 77 | + | |
| 78 | +```text | |
| 79 | +packages/regine-core/ | |
| 80 | +├── src/regine_core/ | |
| 81 | +│ ├── metadata/ | |
| 82 | +│ │ └── exif.py # (existant, specs/002) étendu : read_capture_date(), write_document_id() | |
| 83 | +│ └── import_carte/ # NOUVEAU module | |
| 84 | +│ ├── __init__.py | |
| 85 | +│ ├── copie.py # copie vérifiée carte→local, une seule lecture (FR-001), filtrage nouveaux fichiers (FR-004) | |
| 86 | +│ ├── groupage.py # répartition jour par jour, détection dates aberrantes, découpage en groupes (FR-002/003/005/006) | |
| 87 | +│ ├── destination.py # orchestration destination : simple/sous-dossier/fusion/parent + catégorie (FR-007/008/009), délègue à regine_core.dossier.root | |
| 88 | +│ ├── nommage.py # construction titre/nom dossier, collision (FR-010/012), renommage synchronisé (FR-013/014) | |
| 89 | +│ ├── identifiant.py # génération + écriture identifiant pérenne (FR-017) | |
| 90 | +│ └── push.py # résumé + confirmation + transfert final (FR-018/019) | |
| 91 | +│ | |
| 92 | +└── tests/ | |
| 93 | + ├── unit/ | |
| 94 | + │ ├── test_copie_checksum.py # une seule lecture carte, échec de vérification, carte lente | |
| 95 | + │ ├── test_groupage_dates.py # découpage par défaut, détachement, dates aberrantes, fichiers déjà vus | |
| 96 | + │ ├── test_nommage.py # formats de nom, collision, renommage synchronisé fichiers associés | |
| 97 | + │ └── test_identifiant.py # génération, écriture XMP DocumentID | |
| 98 | + └── integration/ | |
| 99 | + ├── test_pipeline_import_simple.py # US1 : carte → dossier archivé, résumé confirmé | |
| 100 | + ├── test_pipeline_multi_jours.py # US2 : détachement, groupes multiples | |
| 101 | + └── test_pipeline_voyage.py # US3 : dossier parent + sous-dossier + fusion locale + désambiguïsation boîtiers (fusion vers archive : bloquée, cf. Complexity Tracking) | |
| 102 | + | |
| 103 | +packages/regine-cli/ | |
| 104 | +└── src/regine_cli/ | |
| 105 | + └── import_cmd.py # NOUVEAU : commande `regine import <carte>`, premier point d'intégration réel (dossier.root, camera_profile, metadata.exif) | |
| 106 | +``` | |
| 107 | + | |
| 108 | +**Structure Decision**: Extension du monorepo existant — nouveau module `regine_core/import_carte/` (aucun code existant avant ce plan pour ce module), et première commande CLI concrète du projet (`regine import`), qui devient le point d'intégration réel des API déjà contractées par `specs/002-profil-boitiers-optionnel` et `specs/004-categorisation-dossiers` (jusqu'ici documentées mais non consommées). `regine_core/metadata/exif.py` (créé par specs/002) est étendu plutôt que dupliqué. | |
| 109 | + | |
| new file mode 100644 | |||
| @@ -0,0 +1,109 @@ | |||
| 1 | +# Implementation Plan: Importation de photos depuis une carte mémoire | ||
| 2 | + | ||
| 3 | +**Branch**: `001-import-photos` | **Date**: 2026-09-18 | **Spec**: [spec.md](./spec.md) | ||
| 4 | + | ||
| 5 | +**Input**: Feature specification from `/specs/001-import-photos/spec.md` | ||
| 6 | + | ||
| 7 | +## Summary | ||
| 8 | + | ||
| 9 | +Pipeline complet depuis une carte mémoire jusqu'au premier archivage : copie vérifiée par somme de contrôle (une seule lecture de la carte), analyse des dates de prise de vue et découpage en groupes, choix de destination (dossier simple / sous-dossier / fusion / nouveau dossier parent, plus la catégorisation racine de `specs/004-categorisation-dossiers`), renommage synchronisé des fichiers maîtres et associés, désambiguïsation de boîtiers en cas de collision (`specs/002-profil-boitiers-optionnel`), attribution d'un identifiant pérenne, puis confirmation explicite et transfert vers l'archive. Approche technique : un nouveau module `regine_core.import_carte`, premier consommateur réel de `regine_core.dossier.root` (specs/004), `regine_core.camera_profile` (specs/002) et `regine_core.metadata.exif`, exposé via une première commande CLI concrète `regine import` dans `regine-cli`. | ||
| 10 | + | ||
| 11 | +## Technical Context | ||
| 12 | + | ||
| 13 | +**Language/Version**: Python 3.11+ (cohérent avec `regine-core`, cf. `specs/002`, `specs/003`, `specs/004`) | ||
| 14 | + | ||
| 15 | +**Primary Dependencies**: `exiftool` (déjà une dépendance actée par `specs/002-profil-boitiers-optionnel` pour `regine_core.metadata.exif`, étendu ici pour la lecture de la date de prise de vue et l'écriture de l'identifiant pérenne) ; bibliothèque standard uniquement sinon (`hashlib` pour les sommes de contrôle, `pathlib`/`shutil` pour la copie et le renommage, `difflib` pour la recherche de dossiers candidats par titre proche, réutilisant la même approche que `specs/004-categorisation-dossiers`) | ||
| 16 | + | ||
| 17 | +**Storage**: ce module n'introduit aucune nouvelle base ; il consomme la base de contexte centralisée via `regine_core.camera_profile` (specs/002) et `regine_core.config.categories`/`regine_core.dossier.root` (specs/004). Le manifeste persistant par dossier (checksums, réconciliation — constitution § Workflow d'archivage) relève d'un futur module `archive`/`dossier` non encore spécifié ; ce plan ne le redéfinit pas (cf. Constraints ci-dessous) | ||
| 18 | + | ||
| 19 | +**Testing**: pytest ; tests unitaires sur le découpage jour par jour et la détection de dates aberrantes, sur le renommage synchronisé, sur la construction du chemin final (délégué à `specs/004`) ; tests d'intégration sur le pipeline complet copie→analyse→destination→renommage→confirmation, en répertoires temporaires simulant carte/local/archive (aucune écriture réseau réelle) | ||
| 20 | + | ||
| 21 | +**Target Platform**: poste de travail de bureau, identique aux autres modules de `regine-core` | ||
| 22 | + | ||
| 23 | +**Project Type**: Monorepo existant (`specs/002`, `specs/003`, `specs/004`) — nouveau module `regine_core/import_carte/` et première commande CLI concrète (`regine import`) dans `regine-cli` | ||
| 24 | + | ||
| 25 | +**Performance Goals**: aucune lecture de la carte mémoire ne doit dépasser une seule passe par fichier (FR-001/Edge Case déjà tranché) ; pas d'autre cible chiffrée spécifique | ||
| 26 | + | ||
| 27 | +**Constraints**: le sous-scénario de fusion vers un dossier **déjà archivé** sur le NAS (FR-009, Acceptance Scenario 4 de User Story 3) dépend du mécanisme de checkout/réconciliation par hash (constitution § Workflow d'archivage, notes de conception section 8), qui n'a **aucune spec ni plan dans ce dépôt à ce jour** — ce plan implémente tout le reste du pipeline et documente ce sous-scénario comme bloqué (cf. Complexity Tracking) ; la fusion vers un dossier présent seulement en local reste pleinement implémentable | ||
| 28 | + | ||
| 29 | +**Scale/Scope**: import typique de quelques dizaines à quelques milliers de fichiers par carte mémoire, usage mono-utilisateur | ||
| 30 | + | ||
| 31 | +## Constitution Check | ||
| 32 | + | ||
| 33 | +*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.* | ||
| 34 | + | ||
| 35 | +| Principe / contrainte | Évaluation | Justification | | ||
| 36 | +|---|---|---| | ||
| 37 | +| I. Fichier maître intouchable | PASS | Un fichier maître n'est renommé qu'une seule fois, avant son premier archivage (FR-013) ; aucune modification après archivage n'est effectuée par ce module (la vérification d'anomalie ultérieure relève du futur module de réconciliation, hors périmètre). | | ||
| 38 | +| II. Confirmation explicite avant toute action à risque | PASS | FR-018 : résumé complet (fichiers, taille, dossier de destination, répertoire racine) présenté et confirmé avant toute écriture sur l'archive ; FR-012 : collision de nom traitée par suffixe ou confirmation, jamais écrasement silencieux. | | ||
| 39 | +| III. Identité par contenu, jamais par nom de fichier seul | PASS | FR-004 : seuls les fichiers réellement nouveaux (par somme de contrôle) entrent dans l'analyse ; FR-015 : désambiguïsation de boîtiers déléguée à `specs/002`, elle-même fondée sur les métadonnées, jamais sur le seul nom de fichier. | | ||
| 40 | +| IV. Métadonnées ouvertes et embarquées | PASS | FR-017 : l'identifiant pérenne est écrit dans les métadonnées XMP embarquées du fichier (réutilisation du champ standard `xmpMM:DocumentID`, cf. research.md § 3), pas dans une base séparée. | | ||
| 41 | +| V. L'utilisateur décide, Régine suggère | PASS | FR-006 (candidat de détachement suggéré, jamais imposé), FR-011 (lieu GPS suggéré, jamais imposé), FR-007 (catégorie/année toujours une décision explicite, délégué à `specs/004`). | | ||
| 42 | +| VI. Bibliothèque centrale, façades minces | PASS | Toute la logique vit dans `regine_core.import_carte`, qui consomme `regine_core.dossier`, `regine_core.camera_profile` et `regine_core.metadata` sans dupliquer leur logique ; `regine-cli` n'orchestre que l'appel et l'affichage. | | ||
| 43 | +| CLI-first | PASS | `regine import` est la première façade construite pour ce pipeline, avant toute GUI. | | ||
| 44 | +| Exécution sans démon | PASS | Le pipeline s'exécute intégralement dans le processus de la commande CLI, sans état en arrière-plan. | | ||
| 45 | +| Formats ouverts et documentés | PASS | XMP pour l'identifiant pérenne ; aucune base propriétaire introduite. | | ||
| 46 | +| Vérification d'intégrité systématique | PASS | FR-001 : vérification par somme de contrôle sur toute copie carte→local et sur le transfert local→archive (FR-019). | | ||
| 47 | + | ||
| 48 | +**Violation à justifier** (Complexity Tracking) : le sous-scénario FR-009 (fusion vers dossier déjà archivé) ne peut pas être implémenté sans le mécanisme de checkout/réconciliation, absent du dépôt. Ce n'est pas une violation de principe mais une dépendance externe manquante — documentée ci-dessous plutôt que contournée par une implémentation partielle ou incorrecte. | ||
| 49 | + | ||
| 50 | +## Complexity Tracking | ||
| 51 | + | ||
| 52 | +> Rempli exceptionnellement pour documenter une dépendance bloquante, pas une violation de principe. | ||
| 53 | + | ||
| 54 | +| Élément | Pourquoi nécessaire | Alternative plus simple écartée | | ||
| 55 | +|---|---|---| | ||
| 56 | +| FR-009 (fusion vers dossier déjà archivé, nécessite un checkout) | Explicitement requis par `specs/001-import-photos` User Story 3 Acceptance Scenario 4 | Implémenter un checkout minimal ad hoc dans ce module plutôt que d'attendre une spec dédiée — écarté : dupliquerait par anticipation un mécanisme déjà nommé et cadré ailleurs (constitution § Workflow d'archivage, notes de conception section 8), avec un risque réel de divergence si le futur module `archive`/`dossier` retient une conception différente. Le sous-scénario reste documenté comme bloqué plutôt que mal implémenté. | | ||
| 57 | + | ||
| 58 | +**Re-check post Phase 1** (après génération de `data-model.md`, `contracts/`, `quickstart.md`) : le modèle de données (Groupe d'import, Destination, Renommage) et les contrats (CLI `regine import`, API interne `regine_core.import_carte`) ne modifient aucune évaluation ci-dessus ; le sous-scénario FR-009 reste explicitement marqué bloqué dans `contracts/cli-import.md` et `quickstart.md` plutôt que silencieusement omis. | ||
| 59 | + | ||
| 60 | +## Project Structure | ||
| 61 | + | ||
| 62 | +### Documentation (this feature) | ||
| 63 | + | ||
| 64 | +```text | ||
| 65 | +specs/001-import-photos/ | ||
| 66 | +├── plan.md # This file (/speckit-plan command output) | ||
| 67 | +├── research.md # Phase 0 output (/speckit-plan command) | ||
| 68 | +├── data-model.md # Phase 1 output (/speckit-plan command) | ||
| 69 | +├── quickstart.md # Phase 1 output (/speckit-plan command) | ||
| 70 | +├── contracts/ # Phase 1 output (/speckit-plan command) | ||
| 71 | +│ ├── cli-import.md | ||
| 72 | +│ └── regine-core-api.md | ||
| 73 | +└── tasks.md # Phase 2 output (/speckit-tasks command - NOT created by /speckit-plan) | ||
| 74 | +``` | ||
| 75 | + | ||
| 76 | +### Source Code (repository root) — extension du monorepo posé par specs/002/003/004 | ||
| 77 | + | ||
| 78 | +```text | ||
| 79 | +packages/regine-core/ | ||
| 80 | +├── src/regine_core/ | ||
| 81 | +│ ├── metadata/ | ||
| 82 | +│ │ └── exif.py # (existant, specs/002) étendu : read_capture_date(), write_document_id() | ||
| 83 | +│ └── import_carte/ # NOUVEAU module | ||
| 84 | +│ ├── __init__.py | ||
| 85 | +│ ├── copie.py # copie vérifiée carte→local, une seule lecture (FR-001), filtrage nouveaux fichiers (FR-004) | ||
| 86 | +│ ├── groupage.py # répartition jour par jour, détection dates aberrantes, découpage en groupes (FR-002/003/005/006) | ||
| 87 | +│ ├── destination.py # orchestration destination : simple/sous-dossier/fusion/parent + catégorie (FR-007/008/009), délègue à regine_core.dossier.root | ||
| 88 | +│ ├── nommage.py # construction titre/nom dossier, collision (FR-010/012), renommage synchronisé (FR-013/014) | ||
| 89 | +│ ├── identifiant.py # génération + écriture identifiant pérenne (FR-017) | ||
| 90 | +│ └── push.py # résumé + confirmation + transfert final (FR-018/019) | ||
| 91 | +│ | ||
| 92 | +└── tests/ | ||
| 93 | + ├── unit/ | ||
| 94 | + │ ├── test_copie_checksum.py # une seule lecture carte, échec de vérification, carte lente | ||
| 95 | + │ ├── test_groupage_dates.py # découpage par défaut, détachement, dates aberrantes, fichiers déjà vus | ||
| 96 | + │ ├── test_nommage.py # formats de nom, collision, renommage synchronisé fichiers associés | ||
| 97 | + │ └── test_identifiant.py # génération, écriture XMP DocumentID | ||
| 98 | + └── integration/ | ||
| 99 | + ├── test_pipeline_import_simple.py # US1 : carte → dossier archivé, résumé confirmé | ||
| 100 | + ├── test_pipeline_multi_jours.py # US2 : détachement, groupes multiples | ||
| 101 | + └── test_pipeline_voyage.py # US3 : dossier parent + sous-dossier + fusion locale + désambiguïsation boîtiers (fusion vers archive : bloquée, cf. Complexity Tracking) | ||
| 102 | + | ||
| 103 | +packages/regine-cli/ | ||
| 104 | +└── src/regine_cli/ | ||
| 105 | + └── import_cmd.py # NOUVEAU : commande `regine import <carte>`, premier point d'intégration réel (dossier.root, camera_profile, metadata.exif) | ||
| 106 | +``` | ||
| 107 | + | ||
| 108 | +**Structure Decision**: Extension du monorepo existant — nouveau module `regine_core/import_carte/` (aucun code existant avant ce plan pour ce module), et première commande CLI concrète du projet (`regine import`), qui devient le point d'intégration réel des API déjà contractées par `specs/002-profil-boitiers-optionnel` et `specs/004-categorisation-dossiers` (jusqu'ici documentées mais non consommées). `regine_core/metadata/exif.py` (créé par specs/002) est étendu plutôt que dupliqué. | ||
| 109 | + | ||
added
specs/001-import-photos/quickstart.md +44 -0 | new file mode 100644 | ||
| @@ -0,0 +1,44 @@ | ||
| 1 | +# Quickstart : validation de l'import de photos | |
| 2 | + | |
| 3 | +Ce guide valide les 3 user stories de `spec.md` via la commande `regine import` (cf. `contracts/cli-import.md`). À exécuter une fois `regine_core.import_carte` et `regine_cli.import_cmd` implémentés (cf. `tasks.md`). | |
| 4 | + | |
| 5 | +## Prérequis | |
| 6 | + | |
| 7 | +- `regine-core`/`regine-cli` installés, `exiftool` disponible. | |
| 8 | +- Un contexte de travail configuré (`specs/003-config-contexte-travail`) : répertoire temporaire, répertoire de travail local, archive (peut être un dossier local simulant le NAS pour ce test). | |
| 9 | +- Un jeu de fichiers de test avec dates EXIF contrôlées, simulant une "carte mémoire" (simple dossier local en lecture). | |
| 10 | + | |
| 11 | +## Scénario 1 — Import simple (User Story 1, P1) | |
| 12 | + | |
| 13 | +```bash | |
| 14 | +regine import ./fixtures/carte_journee_unique --titre "Sortie parc" --annee --yes | |
| 15 | +``` | |
| 16 | + | |
| 17 | +**Résultat attendu** : un seul groupe proposé (une journée), dossier créé sous `<archive>/<AAAA>/<AAAA-MM-JJ>_Sortie-parc/`, fichiers renommés `<AAAA-MM-JJ>_Sortie-parc_<nomOrigine>.ext`, résumé affiché puis archivage confirmé (`--yes`). Vérifier que la carte source n'a nécessité qu'une seule lecture par fichier (SC-001). | |
| 18 | + | |
| 19 | +## Scénario 2 — Découpage multi-jours (User Story 2, P2) | |
| 20 | + | |
| 21 | +```bash | |
| 22 | +regine import ./fixtures/carte_semaine_avec_pic | |
| 23 | +``` | |
| 24 | + | |
| 25 | +**Résultat attendu** : la répartition jour par jour est affichée, le jour au pic isolé est mis en avant comme candidat au détachement (jamais détaché automatiquement) ; en le détachant, deux groupes distincts sont proposés pour destination/titre séparément. | |
| 26 | + | |
| 27 | +## Scénario 3 — Voyage multi-étapes avec désambiguïsation de boîtiers (User Story 3, P3) | |
| 28 | + | |
| 29 | +```bash | |
| 30 | +regine import ./fixtures/carte_etape1 --destination parent --categorie voyage --titre "Montenegro" | |
| 31 | +regine import ./fixtures/carte_etape2 --destination sous-dossier:<id_parent> --titre "Kotor" | |
| 32 | +``` | |
| 33 | + | |
| 34 | +**Résultat attendu** : structure à deux niveaux sous `<archive>/voyage/<AAAA-MM>_Montenegro/<AAAA-MM-JJ>_Kotor_Montenegro/`, le second import héritant automatiquement de la catégorie `voyage` sans qu'elle soit redemandée (cf. `specs/004-categorisation-dossiers`). | |
| 35 | + | |
| 36 | +```bash | |
| 37 | +regine import ./fixtures/carte_secours_meme_etape --destination fusion:<id_kotor> | |
| 38 | +``` | |
| 39 | + | |
| 40 | +**Résultat attendu** : si `<id_kotor>` n'existe qu'en local, 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 (pas en local), la commande DOIT échouer explicitement** (`ChecoutNonDisponibleError`, cf. `contracts/regine-core-api.md`) — comportement attendu tant que le mécanisme de checkout/réconciliation n'a pas sa propre spec. | |
| 41 | + | |
| 42 | +## Critères de sortie | |
| 43 | + | |
| 44 | +Les scénarios 1 et 2, et la première partie du scénario 3 (fusion locale), doivent passer intégralement. Le cas de fusion vers un dossier déjà archivé reste un échec **attendu et documenté**, pas un bug — à retester une fois le mécanisme de checkout/réconciliation spécifié. | |
| new file mode 100644 | |||
| @@ -0,0 +1,44 @@ | |||
| 1 | +# Quickstart : validation de l'import de photos | ||
| 2 | + | ||
| 3 | +Ce guide valide les 3 user stories de `spec.md` via la commande `regine import` (cf. `contracts/cli-import.md`). À exécuter une fois `regine_core.import_carte` et `regine_cli.import_cmd` implémentés (cf. `tasks.md`). | ||
| 4 | + | ||
| 5 | +## Prérequis | ||
| 6 | + | ||
| 7 | +- `regine-core`/`regine-cli` installés, `exiftool` disponible. | ||
| 8 | +- Un contexte de travail configuré (`specs/003-config-contexte-travail`) : répertoire temporaire, répertoire de travail local, archive (peut être un dossier local simulant le NAS pour ce test). | ||
| 9 | +- Un jeu de fichiers de test avec dates EXIF contrôlées, simulant une "carte mémoire" (simple dossier local en lecture). | ||
| 10 | + | ||
| 11 | +## Scénario 1 — Import simple (User Story 1, P1) | ||
| 12 | + | ||
| 13 | +```bash | ||
| 14 | +regine import ./fixtures/carte_journee_unique --titre "Sortie parc" --annee --yes | ||
| 15 | +``` | ||
| 16 | + | ||
| 17 | +**Résultat attendu** : un seul groupe proposé (une journée), dossier créé sous `<archive>/<AAAA>/<AAAA-MM-JJ>_Sortie-parc/`, fichiers renommés `<AAAA-MM-JJ>_Sortie-parc_<nomOrigine>.ext`, résumé affiché puis archivage confirmé (`--yes`). Vérifier que la carte source n'a nécessité qu'une seule lecture par fichier (SC-001). | ||
| 18 | + | ||
| 19 | +## Scénario 2 — Découpage multi-jours (User Story 2, P2) | ||
| 20 | + | ||
| 21 | +```bash | ||
| 22 | +regine import ./fixtures/carte_semaine_avec_pic | ||
| 23 | +``` | ||
| 24 | + | ||
| 25 | +**Résultat attendu** : la répartition jour par jour est affichée, le jour au pic isolé est mis en avant comme candidat au détachement (jamais détaché automatiquement) ; en le détachant, deux groupes distincts sont proposés pour destination/titre séparément. | ||
| 26 | + | ||
| 27 | +## Scénario 3 — Voyage multi-étapes avec désambiguïsation de boîtiers (User Story 3, P3) | ||
| 28 | + | ||
| 29 | +```bash | ||
| 30 | +regine import ./fixtures/carte_etape1 --destination parent --categorie voyage --titre "Montenegro" | ||
| 31 | +regine import ./fixtures/carte_etape2 --destination sous-dossier:<id_parent> --titre "Kotor" | ||
| 32 | +``` | ||
| 33 | + | ||
| 34 | +**Résultat attendu** : structure à deux niveaux sous `<archive>/voyage/<AAAA-MM>_Montenegro/<AAAA-MM-JJ>_Kotor_Montenegro/`, le second import héritant automatiquement de la catégorie `voyage` sans qu'elle soit redemandée (cf. `specs/004-categorisation-dossiers`). | ||
| 35 | + | ||
| 36 | +```bash | ||
| 37 | +regine import ./fixtures/carte_secours_meme_etape --destination fusion:<id_kotor> | ||
| 38 | +``` | ||
| 39 | + | ||
| 40 | +**Résultat attendu** : si `<id_kotor>` n'existe qu'en local, 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 (pas en local), la commande DOIT échouer explicitement** (`ChecoutNonDisponibleError`, cf. `contracts/regine-core-api.md`) — comportement attendu tant que le mécanisme de checkout/réconciliation n'a pas sa propre spec. | ||
| 41 | + | ||
| 42 | +## Critères de sortie | ||
| 43 | + | ||
| 44 | +Les scénarios 1 et 2, et la première partie du scénario 3 (fusion locale), doivent passer intégralement. Le cas de fusion vers un dossier déjà archivé reste un échec **attendu et documenté**, pas un bug — à retester une fois le mécanisme de checkout/réconciliation spécifié. | ||
added
specs/001-import-photos/research.md +60 -0 | new file mode 100644 | ||
| @@ -0,0 +1,60 @@ | ||
| 1 | +# Research: Importation de photos depuis une carte mémoire | |
| 2 | + | |
| 3 | +## 1. Copie vérifiée sans seconde lecture de la carte | |
| 4 | + | |
| 5 | +**Decision**: Copier chaque fichier par lecture en flux (chunks), en calculant une somme SHA-256 de la source au fil de la lecture (`hashlib.sha256`, mise à jour à chaque chunk lu depuis la carte puis écrit sur disque local) ; une fois la copie terminée, relire uniquement le fichier local (rapide) pour recalculer sa somme et la comparer à celle obtenue pendant le flux source. | |
| 6 | + | |
| 7 | +**Rationale**: Répond exactement à FR-001 et à l'Edge Case "carte lente" déjà tranchés dans la spec : une seule lecture de la carte par fichier, la seconde vérification portant uniquement sur le disque local. Entièrement réalisable avec la bibliothèque standard (`hashlib`, lecture par blocs), cohérent avec la préférence du projet pour des dépendances minimales. | |
| 8 | + | |
| 9 | +**Alternatives considered**: | |
| 10 | +- Copier puis relire la carte une seconde fois pour vérifier — rejeté explicitement par la spec (Edge Case, SC-001). | |
| 11 | +- Bibliothèque tierce de copie vérifiée — rejeté, la stdlib suffit pour un besoin aussi direct. | |
| 12 | + | |
| 13 | +## 2. Lecture de la date de prise de vue | |
| 14 | + | |
| 15 | +**Decision**: Étendre `regine_core.metadata.exif` (créé par `specs/002-profil-boitiers-optionnel`) avec `read_capture_date(chemin) -> datetime | None`, lisant le tag EXIF `DateTimeOriginal` via `exiftool` (même mécanisme que la lecture de `Model`/`BodySerialNumber`). | |
| 16 | + | |
| 17 | +**Rationale**: Réutilise le module de lecture EXIF déjà décidé plutôt que d'introduire un second mécanisme ; cohérent avec le Principe VI (pas de logique dupliquée) et avec le choix déjà justifié de `exiftool` pour sa fiabilité sur les formats RAW propriétaires (`specs/002-profil-boitiers-optionnel/research.md` § 1). | |
| 18 | + | |
| 19 | +**Alternatives considered**: aucune réellement — la décision de `specs/002` s'applique directement ici, pas de nouveau choix technique à faire. | |
| 20 | + | |
| 21 | +## 3. Identifiant pérenne : quel champ de métadonnées écrire | |
| 22 | + | |
| 23 | +**Decision**: Réutiliser le champ XMP standard `xmpMM:DocumentID` (schéma XMP Media Management), en générant un UUID à l'import et en l'écrivant via `exiftool` une seule fois, à la création du fichier maître dans l'archive. | |
| 24 | + | |
| 25 | +**Rationale**: Les notes de conception du projet (`docs/archivage-photo-elements-cles.md` section 3 et section 13) identifient déjà `DocumentID`/`OriginalDocumentID`/`DerivedFrom` (XMP Media Management) comme piste à privilégier avant de construire un champ maison, précisément pour ce besoin d'identifiant indépendant du nom de fichier. Utiliser directement ce standard répond au Principe IV (métadonnées ouvertes) sans attendre la vérification (non encore faite) de son renseignement automatique par DxO à l'export — cette vérification reste utile pour le besoin *différent* de traçabilité d'un export dérivé (section 13), mais n'est pas bloquante ici : Régine écrit elle-même ce champ à l'import, elle n'a pas besoin qu'un outil tiers le fasse à sa place pour cette fonctionnalité. | |
| 26 | + | |
| 27 | +**Alternatives considered**: | |
| 28 | +- Champ XMP maison (ex. `regine:PermanentId`) — rejeté : un standard existant couvre déjà ce besoin, l'utiliser évite d'imposer un vocabulaire propriétaire de plus dans les fichiers de l'utilisateur. | |
| 29 | +- Stocker l'identifiant uniquement dans une base Régine, sans l'écrire dans le fichier — rejeté explicitement par FR-017 et par le Principe IV (métadonnées ouvertes et embarquées). | |
| 30 | + | |
| 31 | +## 4. Recherche de dossiers candidats (FR-008) | |
| 32 | + | |
| 33 | +**Decision**: Lister directement les dossiers présents sous la racine de l'espace de travail local et sous chaque répertoire racine de l'archive (année et catégories déjà connues, via `regine_core.config.categories.list_known_categories`), puis filtrer par proximité de titre (`difflib.get_close_matches`, même approche que `specs/004-categorisation-dossiers`) et de date. Pas d'index persistant construit pour ce besoin. | |
| 34 | + | |
| 35 | +**Rationale**: Le volume de dossiers candidats pertinents pour une recherche à l'import (titre/date proches) reste faible en pratique ; un parcours direct des répertoires suffit et évite de construire par anticipation l'index inter-dossiers en lecture seule déjà identifié comme "travail futur" dans les notes de conception (section 12/13), qui répond à un besoin plus large (recherche globale dans toute l'archive) que celui, ciblé, de cette étape d'import. | |
| 36 | + | |
| 37 | +**Alternatives considered**: | |
| 38 | +- Construire l'index inter-dossiers dès ce plan — rejeté : hors périmètre de cette spec, complexité non justifiée pour ce seul besoin (YAGNI, cohérent avec les décisions précédentes du projet). | |
| 39 | + | |
| 40 | +## 5. Détection d'un jour "candidat au détachement" (FR-006) | |
| 41 | + | |
| 42 | +**Decision**: Heuristique simple — un jour est mis en avant comme candidat si son nombre de photos s'écarte fortement de la médiane des autres jours de la plage (ex. facteur ×3) et qu'il est entouré d'un intervalle sans photo d'au moins un jour de part et d'autre. Reste une suggestion strictement indicative (FR-006), jamais un détachement automatique. | |
| 43 | + | |
| 44 | +**Rationale**: FR-006 n'exige qu'une mise en avant plausible, pas une détection parfaite ; une heuristique simple et explicable (pas de dépendance à une bibliothèque de statistiques) suffit et reste cohérente avec le Principe V (Régine suggère, l'utilisateur décide) — une heuristique imparfaite est acceptable puisque la décision finale reste humaine. | |
| 45 | + | |
| 46 | +**Alternatives considered**: | |
| 47 | +- Bibliothèque de détection d'anomalies (ex. détection de pics par écart-type glissant plus sophistiquée) — rejeté : complexité non justifiée pour une simple suggestion que l'utilisateur peut ignorer. | |
| 48 | + | |
| 49 | +## 6. Dépendance non résolue : fusion vers un dossier déjà archivé (FR-009) | |
| 50 | + | |
| 51 | +**Decision**: Ne pas implémenter ce sous-scénario dans ce plan. Documenté comme bloqué (cf. `plan.md` § Complexity Tracking) en attendant une spec/plan dédiés au mécanisme de checkout/réconciliation (constitution § Workflow d'archivage, notes de conception section 8). | |
| 52 | + | |
| 53 | +**Rationale**: Ce mécanisme est explicitement nommé et cadré ailleurs dans le projet (verrouillage de dossier, manifeste persistant, réconciliation par hash) sans qu'aucune spec ne l'ait encore formalisé. L'implémenter partiellement ici risquerait une divergence de conception avec sa future spec dédiée. | |
| 54 | + | |
| 55 | +**Alternatives considered**: | |
| 56 | +- Implémenter un checkout minimal ad hoc — rejeté, cf. `plan.md` § Complexity Tracking. | |
| 57 | + | |
| 58 | +## Résumé | |
| 59 | + | |
| 60 | +Tous les points du Technical Context sont résolus. Aucune dépendance tierce Python nouvelle (`exiftool` déjà acté). Un sous-scénario (FR-009, fusion vers dossier déjà archivé) reste explicitement documenté comme bloqué plutôt que contourné. | |
| new file mode 100644 | |||
| @@ -0,0 +1,60 @@ | |||
| 1 | +# Research: Importation de photos depuis une carte mémoire | ||
| 2 | + | ||
| 3 | +## 1. Copie vérifiée sans seconde lecture de la carte | ||
| 4 | + | ||
| 5 | +**Decision**: Copier chaque fichier par lecture en flux (chunks), en calculant une somme SHA-256 de la source au fil de la lecture (`hashlib.sha256`, mise à jour à chaque chunk lu depuis la carte puis écrit sur disque local) ; une fois la copie terminée, relire uniquement le fichier local (rapide) pour recalculer sa somme et la comparer à celle obtenue pendant le flux source. | ||
| 6 | + | ||
| 7 | +**Rationale**: Répond exactement à FR-001 et à l'Edge Case "carte lente" déjà tranchés dans la spec : une seule lecture de la carte par fichier, la seconde vérification portant uniquement sur le disque local. Entièrement réalisable avec la bibliothèque standard (`hashlib`, lecture par blocs), cohérent avec la préférence du projet pour des dépendances minimales. | ||
| 8 | + | ||
| 9 | +**Alternatives considered**: | ||
| 10 | +- Copier puis relire la carte une seconde fois pour vérifier — rejeté explicitement par la spec (Edge Case, SC-001). | ||
| 11 | +- Bibliothèque tierce de copie vérifiée — rejeté, la stdlib suffit pour un besoin aussi direct. | ||
| 12 | + | ||
| 13 | +## 2. Lecture de la date de prise de vue | ||
| 14 | + | ||
| 15 | +**Decision**: Étendre `regine_core.metadata.exif` (créé par `specs/002-profil-boitiers-optionnel`) avec `read_capture_date(chemin) -> datetime | None`, lisant le tag EXIF `DateTimeOriginal` via `exiftool` (même mécanisme que la lecture de `Model`/`BodySerialNumber`). | ||
| 16 | + | ||
| 17 | +**Rationale**: Réutilise le module de lecture EXIF déjà décidé plutôt que d'introduire un second mécanisme ; cohérent avec le Principe VI (pas de logique dupliquée) et avec le choix déjà justifié de `exiftool` pour sa fiabilité sur les formats RAW propriétaires (`specs/002-profil-boitiers-optionnel/research.md` § 1). | ||
| 18 | + | ||
| 19 | +**Alternatives considered**: aucune réellement — la décision de `specs/002` s'applique directement ici, pas de nouveau choix technique à faire. | ||
| 20 | + | ||
| 21 | +## 3. Identifiant pérenne : quel champ de métadonnées écrire | ||
| 22 | + | ||
| 23 | +**Decision**: Réutiliser le champ XMP standard `xmpMM:DocumentID` (schéma XMP Media Management), en générant un UUID à l'import et en l'écrivant via `exiftool` une seule fois, à la création du fichier maître dans l'archive. | ||
| 24 | + | ||
| 25 | +**Rationale**: Les notes de conception du projet (`docs/archivage-photo-elements-cles.md` section 3 et section 13) identifient déjà `DocumentID`/`OriginalDocumentID`/`DerivedFrom` (XMP Media Management) comme piste à privilégier avant de construire un champ maison, précisément pour ce besoin d'identifiant indépendant du nom de fichier. Utiliser directement ce standard répond au Principe IV (métadonnées ouvertes) sans attendre la vérification (non encore faite) de son renseignement automatique par DxO à l'export — cette vérification reste utile pour le besoin *différent* de traçabilité d'un export dérivé (section 13), mais n'est pas bloquante ici : Régine écrit elle-même ce champ à l'import, elle n'a pas besoin qu'un outil tiers le fasse à sa place pour cette fonctionnalité. | ||
| 26 | + | ||
| 27 | +**Alternatives considered**: | ||
| 28 | +- Champ XMP maison (ex. `regine:PermanentId`) — rejeté : un standard existant couvre déjà ce besoin, l'utiliser évite d'imposer un vocabulaire propriétaire de plus dans les fichiers de l'utilisateur. | ||
| 29 | +- Stocker l'identifiant uniquement dans une base Régine, sans l'écrire dans le fichier — rejeté explicitement par FR-017 et par le Principe IV (métadonnées ouvertes et embarquées). | ||
| 30 | + | ||
| 31 | +## 4. Recherche de dossiers candidats (FR-008) | ||
| 32 | + | ||
| 33 | +**Decision**: Lister directement les dossiers présents sous la racine de l'espace de travail local et sous chaque répertoire racine de l'archive (année et catégories déjà connues, via `regine_core.config.categories.list_known_categories`), puis filtrer par proximité de titre (`difflib.get_close_matches`, même approche que `specs/004-categorisation-dossiers`) et de date. Pas d'index persistant construit pour ce besoin. | ||
| 34 | + | ||
| 35 | +**Rationale**: Le volume de dossiers candidats pertinents pour une recherche à l'import (titre/date proches) reste faible en pratique ; un parcours direct des répertoires suffit et évite de construire par anticipation l'index inter-dossiers en lecture seule déjà identifié comme "travail futur" dans les notes de conception (section 12/13), qui répond à un besoin plus large (recherche globale dans toute l'archive) que celui, ciblé, de cette étape d'import. | ||
| 36 | + | ||
| 37 | +**Alternatives considered**: | ||
| 38 | +- Construire l'index inter-dossiers dès ce plan — rejeté : hors périmètre de cette spec, complexité non justifiée pour ce seul besoin (YAGNI, cohérent avec les décisions précédentes du projet). | ||
| 39 | + | ||
| 40 | +## 5. Détection d'un jour "candidat au détachement" (FR-006) | ||
| 41 | + | ||
| 42 | +**Decision**: Heuristique simple — un jour est mis en avant comme candidat si son nombre de photos s'écarte fortement de la médiane des autres jours de la plage (ex. facteur ×3) et qu'il est entouré d'un intervalle sans photo d'au moins un jour de part et d'autre. Reste une suggestion strictement indicative (FR-006), jamais un détachement automatique. | ||
| 43 | + | ||
| 44 | +**Rationale**: FR-006 n'exige qu'une mise en avant plausible, pas une détection parfaite ; une heuristique simple et explicable (pas de dépendance à une bibliothèque de statistiques) suffit et reste cohérente avec le Principe V (Régine suggère, l'utilisateur décide) — une heuristique imparfaite est acceptable puisque la décision finale reste humaine. | ||
| 45 | + | ||
| 46 | +**Alternatives considered**: | ||
| 47 | +- Bibliothèque de détection d'anomalies (ex. détection de pics par écart-type glissant plus sophistiquée) — rejeté : complexité non justifiée pour une simple suggestion que l'utilisateur peut ignorer. | ||
| 48 | + | ||
| 49 | +## 6. Dépendance non résolue : fusion vers un dossier déjà archivé (FR-009) | ||
| 50 | + | ||
| 51 | +**Decision**: Ne pas implémenter ce sous-scénario dans ce plan. Documenté comme bloqué (cf. `plan.md` § Complexity Tracking) en attendant une spec/plan dédiés au mécanisme de checkout/réconciliation (constitution § Workflow d'archivage, notes de conception section 8). | ||
| 52 | + | ||
| 53 | +**Rationale**: Ce mécanisme est explicitement nommé et cadré ailleurs dans le projet (verrouillage de dossier, manifeste persistant, réconciliation par hash) sans qu'aucune spec ne l'ait encore formalisé. L'implémenter partiellement ici risquerait une divergence de conception avec sa future spec dédiée. | ||
| 54 | + | ||
| 55 | +**Alternatives considered**: | ||
| 56 | +- Implémenter un checkout minimal ad hoc — rejeté, cf. `plan.md` § Complexity Tracking. | ||
| 57 | + | ||
| 58 | +## Résumé | ||
| 59 | + | ||
| 60 | +Tous les points du Technical Context sont résolus. Aucune dépendance tierce Python nouvelle (`exiftool` déjà acté). Un sous-scénario (FR-009, fusion vers dossier déjà archivé) reste explicitement documenté comme bloqué plutôt que contourné. | ||
added
specs/002-profil-boitiers-optionnel/contracts/regine-core-api.md +39 -0 | new file mode 100644 | ||
| @@ -0,0 +1,39 @@ | ||
| 1 | +# Contrat d'API interne : `regine_core.metadata.exif` / `regine_core.camera_profile` | |
| 2 | + | |
| 3 | +Aucune nouvelle commande CLI de premier niveau : cette fonctionnalité expose une API Python interne, consommée par le futur flux d'import (`specs/001-import-photos`) et par l'écran de configuration des boîtiers (`specs/003-config-contexte-travail` User Story 3) — objets structurés, jamais de texte à parser (Principe VI). | |
| 4 | + | |
| 5 | +## `regine_core.metadata.exif.read_camera_tags(chemin: Path) -> CameraTags` | |
| 6 | + | |
| 7 | +Lit les tags EXIF `Model` et `BodySerialNumber` d'un fichier via `exiftool`. Retourne `CameraTags(chemin, modele, numero_serie)` avec `modele`/`numero_serie` à `None` si absents ou inexploitables (valeur vide, placeholder générique). | |
| 8 | + | |
| 9 | +## `regine_core.camera_profile.resolve_collision(fichiers: list[Path]) -> CollisionResolution` | |
| 10 | + | |
| 11 | +Résout la source de chaque fichier d'un groupe déjà identifié en collision (même nom d'origine, sommes de contrôle différentes — cette identification reste hors périmètre, fournie par l'appelant). | |
| 12 | + | |
| 13 | +**Contrat de comportement** : | |
| 14 | +- Ne DOIT jamais être appelée pour des fichiers qui ne sont pas en collision réelle (FR-003, FR-008) — c'est à l'appelant de ne fournir que des groupes réellement ambigus. | |
| 15 | +- Les fichiers dont le `modele` diffère sont résolus automatiquement, chacun vers son `boitiers.id` correspondant (créé si première rencontre). | |
| 16 | +- Au sein d'un même `modele`, les fichiers dont le `numero_serie` diffère et est exploitable sont résolus automatiquement. | |
| 17 | +- Les fichiers restants (même `modele`, `numero_serie` identique/absent/inexploitable) sont retournés dans `a_etiqueter`, jamais résolus arbitrairement. | |
| 18 | + | |
| 19 | +## `regine_core.camera_profile.assign_manual_source(fichiers: list[Path], boitier_id: int | None = None) -> int` | |
| 20 | + | |
| 21 | +Assigne manuellement un groupe de fichiers (issu de `a_etiqueter`) à un boîtier existant (`boitier_id` fourni) ou nouvellement créé (`boitier_id=None`, `source="manuel"`). Retourne l'`id` du boîtier utilisé. Ne DOIT être appelée qu'après une décision explicite de l'utilisateur (FR-005) — jamais automatiquement. | |
| 22 | + | |
| 23 | +## `regine_core.camera_profile.list_boitiers() -> list[Boitier]` | |
| 24 | + | |
| 25 | +Retourne tous les boîtiers connus (modèle, numéro de série le cas échéant, nom lisible ou `None`, source). Consommée par `specs/003-config-contexte-travail` (écran de configuration des boîtiers) plutôt que d'y être réimplémentée. | |
| 26 | + | |
| 27 | +## `regine_core.camera_profile.rename_boitier(boitier_id: int, nom: str) -> None` | |
| 28 | + | |
| 29 | +Attribue ou modifie le `nom_lisible` d'un boîtier existant. Réutilisée à l'identique par `specs/003-config-contexte-travail` FR-008/FR-009 (renommage à la demande) — cette spécification (002) ne redéfinit pas ce point d'entrée deux fois, `config` appelle celui-ci. | |
| 30 | + | |
| 31 | +## Point d'intégration côté façade (futur, hors périmètre de ce plan) | |
| 32 | + | |
| 33 | +Le futur code du module import (`specs/001-import-photos`) DEVRA, une fois une collision de nom détectée par somme de contrôle : | |
| 34 | +1. Appeler `read_camera_tags` sur chaque fichier du groupe en collision. | |
| 35 | +2. Appeler `resolve_collision` avec ces tags. | |
| 36 | +3. Pour tout groupe présent dans `a_etiqueter`, interrompre le flux d'import de ce groupe et demander à l'utilisateur un étiquetage manuel, puis appeler `assign_manual_source`. | |
| 37 | +4. Poursuivre l'import normalement pour tous les fichiers résolus automatiquement. | |
| 38 | + | |
| 39 | +Ce point d'intégration n'est pas implémenté par ce plan ; il est documenté ici pour que l'implémentation future de l'import consomme cette API sans la redéfinir. | |
| new file mode 100644 | |||
| @@ -0,0 +1,39 @@ | |||
| 1 | +# Contrat d'API interne : `regine_core.metadata.exif` / `regine_core.camera_profile` | ||
| 2 | + | ||
| 3 | +Aucune nouvelle commande CLI de premier niveau : cette fonctionnalité expose une API Python interne, consommée par le futur flux d'import (`specs/001-import-photos`) et par l'écran de configuration des boîtiers (`specs/003-config-contexte-travail` User Story 3) — objets structurés, jamais de texte à parser (Principe VI). | ||
| 4 | + | ||
| 5 | +## `regine_core.metadata.exif.read_camera_tags(chemin: Path) -> CameraTags` | ||
| 6 | + | ||
| 7 | +Lit les tags EXIF `Model` et `BodySerialNumber` d'un fichier via `exiftool`. Retourne `CameraTags(chemin, modele, numero_serie)` avec `modele`/`numero_serie` à `None` si absents ou inexploitables (valeur vide, placeholder générique). | ||
| 8 | + | ||
| 9 | +## `regine_core.camera_profile.resolve_collision(fichiers: list[Path]) -> CollisionResolution` | ||
| 10 | + | ||
| 11 | +Résout la source de chaque fichier d'un groupe déjà identifié en collision (même nom d'origine, sommes de contrôle différentes — cette identification reste hors périmètre, fournie par l'appelant). | ||
| 12 | + | ||
| 13 | +**Contrat de comportement** : | ||
| 14 | +- Ne DOIT jamais être appelée pour des fichiers qui ne sont pas en collision réelle (FR-003, FR-008) — c'est à l'appelant de ne fournir que des groupes réellement ambigus. | ||
| 15 | +- Les fichiers dont le `modele` diffère sont résolus automatiquement, chacun vers son `boitiers.id` correspondant (créé si première rencontre). | ||
| 16 | +- Au sein d'un même `modele`, les fichiers dont le `numero_serie` diffère et est exploitable sont résolus automatiquement. | ||
| 17 | +- Les fichiers restants (même `modele`, `numero_serie` identique/absent/inexploitable) sont retournés dans `a_etiqueter`, jamais résolus arbitrairement. | ||
| 18 | + | ||
| 19 | +## `regine_core.camera_profile.assign_manual_source(fichiers: list[Path], boitier_id: int | None = None) -> int` | ||
| 20 | + | ||
| 21 | +Assigne manuellement un groupe de fichiers (issu de `a_etiqueter`) à un boîtier existant (`boitier_id` fourni) ou nouvellement créé (`boitier_id=None`, `source="manuel"`). Retourne l'`id` du boîtier utilisé. Ne DOIT être appelée qu'après une décision explicite de l'utilisateur (FR-005) — jamais automatiquement. | ||
| 22 | + | ||
| 23 | +## `regine_core.camera_profile.list_boitiers() -> list[Boitier]` | ||
| 24 | + | ||
| 25 | +Retourne tous les boîtiers connus (modèle, numéro de série le cas échéant, nom lisible ou `None`, source). Consommée par `specs/003-config-contexte-travail` (écran de configuration des boîtiers) plutôt que d'y être réimplémentée. | ||
| 26 | + | ||
| 27 | +## `regine_core.camera_profile.rename_boitier(boitier_id: int, nom: str) -> None` | ||
| 28 | + | ||
| 29 | +Attribue ou modifie le `nom_lisible` d'un boîtier existant. Réutilisée à l'identique par `specs/003-config-contexte-travail` FR-008/FR-009 (renommage à la demande) — cette spécification (002) ne redéfinit pas ce point d'entrée deux fois, `config` appelle celui-ci. | ||
| 30 | + | ||
| 31 | +## Point d'intégration côté façade (futur, hors périmètre de ce plan) | ||
| 32 | + | ||
| 33 | +Le futur code du module import (`specs/001-import-photos`) DEVRA, une fois une collision de nom détectée par somme de contrôle : | ||
| 34 | +1. Appeler `read_camera_tags` sur chaque fichier du groupe en collision. | ||
| 35 | +2. Appeler `resolve_collision` avec ces tags. | ||
| 36 | +3. Pour tout groupe présent dans `a_etiqueter`, interrompre le flux d'import de ce groupe et demander à l'utilisateur un étiquetage manuel, puis appeler `assign_manual_source`. | ||
| 37 | +4. Poursuivre l'import normalement pour tous les fichiers résolus automatiquement. | ||
| 38 | + | ||
| 39 | +Ce point d'intégration n'est pas implémenté par ce plan ; il est documenté ici pour que l'implémentation future de l'import consomme cette API sans la redéfinir. | ||
added
specs/002-profil-boitiers-optionnel/data-model.md +65 -0 | new file mode 100644 | ||
| @@ -0,0 +1,65 @@ | ||
| 1 | +# Data Model: Profil de boîtiers optionnel, déclaré à la demande | |
| 2 | + | |
| 3 | +Entités dérivées de `spec.md` § Key Entities et Functional Requirements. | |
| 4 | + | |
| 5 | +## Table `boitiers` (base de contexte centralisée, `specs/003-config-contexte-travail`) | |
| 6 | + | |
| 7 | +Possédée par `regine_core.camera_profile` (cf. `research.md` § 2 — corrige le placement provisoire de `specs/003-config-contexte-travail/plan.md`). | |
| 8 | + | |
| 9 | +| Champ | Type | Règles | | |
| 10 | +|---|---|---| | |
| 11 | +| `id` | identifiant interne | Clé primaire | | |
| 12 | +| `modele` | texte | Tag EXIF `Model`, obligatoire (un boîtier sans modèle lisible n'est identifiable que par étiquetage manuel, cf. Edge Case) | | |
| 13 | +| `numero_serie` | texte, nullable | Tag EXIF `BodySerialNumber`, quand présent et exploitable (non vide, non valeur générique) | | |
| 14 | +| `nom_lisible` | texte, nullable | Attribué par l'utilisateur (via `specs/003-config-contexte-travail` ou au moment d'un étiquetage manuel) ; `NULL` tant que non nommé | | |
| 15 | +| `premiere_rencontre` | horodatage | Date de la première résolution ayant créé cette entrée | | |
| 16 | +| `source` | énumération : `modele` \| `numero_serie` \| `manuel` | Méthode ayant permis de créer/distinguer cette entrée | | |
| 17 | + | |
| 18 | +**Règle d'unicité** : unique sur (`modele`, `numero_serie`) quand `numero_serie` est renseigné ; sinon unique sur `modele` seul, jusqu'à ce qu'une collision réelle force une entrée `source=manuel` distincte pour le même modèle (FR-005). | |
| 19 | + | |
| 20 | +## Objets d'échange (non persistés) | |
| 21 | + | |
| 22 | +**`CameraTags`** (retour de `regine_core.metadata.exif.read_camera_tags`) : | |
| 23 | + | |
| 24 | +| Champ | Type | | |
| 25 | +|---|---| | |
| 26 | +| `chemin` | Path | | |
| 27 | +| `modele` | `str \| None` | | |
| 28 | +| `numero_serie` | `str \| None` | | |
| 29 | + | |
| 30 | +**`CollisionResolution`** (retour de `regine_core.camera_profile.resolve_collision`) : | |
| 31 | + | |
| 32 | +| Champ | Type | Description | | |
| 33 | +|---|---|---| | |
| 34 | +| `resolues` | `dict[Path, int]` | Fichier → `id` de boîtier déterminé automatiquement (modèle ou numéro de série) | | |
| 35 | +| `a_etiqueter` | `list[list[Path]]` | Groupes de fichiers encore indistincts, nécessitant un étiquetage manuel (FR-005) | | |
| 36 | + | |
| 37 | +## Relation avec le reste du système | |
| 38 | + | |
| 39 | +```text | |
| 40 | +Fichier en collision (checksum différent, même nom d'origine — détecté par le module import, hors périmètre) | |
| 41 | + │ | |
| 42 | + ▼ | |
| 43 | +CameraTags (lu via exiftool) | |
| 44 | + │ | |
| 45 | + ▼ | |
| 46 | +resolve_collision() ── regroupe par modele ── distingue par numero_serie ── sinon → a_etiqueter | |
| 47 | + │ | |
| 48 | + ▼ | |
| 49 | +boitiers (id réutilisé pour tout import ultérieur du même modèle/numéro de série, FR-006) | |
| 50 | +``` | |
| 51 | + | |
| 52 | +## État / transitions | |
| 53 | + | |
| 54 | +```text | |
| 55 | +[Boîtier] | |
| 56 | + Détecté (modele connu, numero_serie éventuel, nom_lisible=NULL, source=modele|numero_serie) | |
| 57 | + │ collision non résolue par modele/numero_serie → étiquetage manuel (FR-005) | |
| 58 | + ▼ | |
| 59 | + Détecté (source=manuel) | |
| 60 | + │ utilisateur attribue un nom (specs/003, ou au moment de l'étiquetage manuel) | |
| 61 | + ▼ | |
| 62 | + Nommé (nom_lisible défini) | |
| 63 | +``` | |
| 64 | + | |
| 65 | +Un boîtier retiré du profil par l'utilisateur (Edge Case) redevient sujet à une nouvelle résolution s'il réapparaît dans une collision future — pas de suppression physique requise par cette spécification, une simple absence de correspondance suffit à redéclencher le flux normal. | |
| new file mode 100644 | |||
| @@ -0,0 +1,65 @@ | |||
| 1 | +# Data Model: Profil de boîtiers optionnel, déclaré à la demande | ||
| 2 | + | ||
| 3 | +Entités dérivées de `spec.md` § Key Entities et Functional Requirements. | ||
| 4 | + | ||
| 5 | +## Table `boitiers` (base de contexte centralisée, `specs/003-config-contexte-travail`) | ||
| 6 | + | ||
| 7 | +Possédée par `regine_core.camera_profile` (cf. `research.md` § 2 — corrige le placement provisoire de `specs/003-config-contexte-travail/plan.md`). | ||
| 8 | + | ||
| 9 | +| Champ | Type | Règles | | ||
| 10 | +|---|---|---| | ||
| 11 | +| `id` | identifiant interne | Clé primaire | | ||
| 12 | +| `modele` | texte | Tag EXIF `Model`, obligatoire (un boîtier sans modèle lisible n'est identifiable que par étiquetage manuel, cf. Edge Case) | | ||
| 13 | +| `numero_serie` | texte, nullable | Tag EXIF `BodySerialNumber`, quand présent et exploitable (non vide, non valeur générique) | | ||
| 14 | +| `nom_lisible` | texte, nullable | Attribué par l'utilisateur (via `specs/003-config-contexte-travail` ou au moment d'un étiquetage manuel) ; `NULL` tant que non nommé | | ||
| 15 | +| `premiere_rencontre` | horodatage | Date de la première résolution ayant créé cette entrée | | ||
| 16 | +| `source` | énumération : `modele` \| `numero_serie` \| `manuel` | Méthode ayant permis de créer/distinguer cette entrée | | ||
| 17 | + | ||
| 18 | +**Règle d'unicité** : unique sur (`modele`, `numero_serie`) quand `numero_serie` est renseigné ; sinon unique sur `modele` seul, jusqu'à ce qu'une collision réelle force une entrée `source=manuel` distincte pour le même modèle (FR-005). | ||
| 19 | + | ||
| 20 | +## Objets d'échange (non persistés) | ||
| 21 | + | ||
| 22 | +**`CameraTags`** (retour de `regine_core.metadata.exif.read_camera_tags`) : | ||
| 23 | + | ||
| 24 | +| Champ | Type | | ||
| 25 | +|---|---| | ||
| 26 | +| `chemin` | Path | | ||
| 27 | +| `modele` | `str \| None` | | ||
| 28 | +| `numero_serie` | `str \| None` | | ||
| 29 | + | ||
| 30 | +**`CollisionResolution`** (retour de `regine_core.camera_profile.resolve_collision`) : | ||
| 31 | + | ||
| 32 | +| Champ | Type | Description | | ||
| 33 | +|---|---|---| | ||
| 34 | +| `resolues` | `dict[Path, int]` | Fichier → `id` de boîtier déterminé automatiquement (modèle ou numéro de série) | | ||
| 35 | +| `a_etiqueter` | `list[list[Path]]` | Groupes de fichiers encore indistincts, nécessitant un étiquetage manuel (FR-005) | | ||
| 36 | + | ||
| 37 | +## Relation avec le reste du système | ||
| 38 | + | ||
| 39 | +```text | ||
| 40 | +Fichier en collision (checksum différent, même nom d'origine — détecté par le module import, hors périmètre) | ||
| 41 | + │ | ||
| 42 | + ▼ | ||
| 43 | +CameraTags (lu via exiftool) | ||
| 44 | + │ | ||
| 45 | + ▼ | ||
| 46 | +resolve_collision() ── regroupe par modele ── distingue par numero_serie ── sinon → a_etiqueter | ||
| 47 | + │ | ||
| 48 | + ▼ | ||
| 49 | +boitiers (id réutilisé pour tout import ultérieur du même modèle/numéro de série, FR-006) | ||
| 50 | +``` | ||
| 51 | + | ||
| 52 | +## État / transitions | ||
| 53 | + | ||
| 54 | +```text | ||
| 55 | +[Boîtier] | ||
| 56 | + Détecté (modele connu, numero_serie éventuel, nom_lisible=NULL, source=modele|numero_serie) | ||
| 57 | + │ collision non résolue par modele/numero_serie → étiquetage manuel (FR-005) | ||
| 58 | + ▼ | ||
| 59 | + Détecté (source=manuel) | ||
| 60 | + │ utilisateur attribue un nom (specs/003, ou au moment de l'étiquetage manuel) | ||
| 61 | + ▼ | ||
| 62 | + Nommé (nom_lisible défini) | ||
| 63 | +``` | ||
| 64 | + | ||
| 65 | +Un boîtier retiré du profil par l'utilisateur (Edge Case) redevient sujet à une nouvelle résolution s'il réapparaît dans une collision future — pas de suppression physique requise par cette spécification, une simple absence de correspondance suffit à redéclencher le flux normal. | ||
added
specs/002-profil-boitiers-optionnel/plan.md +91 -0 | new file mode 100644 | ||
| @@ -0,0 +1,91 @@ | ||
| 1 | +# Implementation Plan: Profil de boîtiers optionnel, déclaré à la demande | |
| 2 | + | |
| 3 | +**Branch**: `002-profil-boitiers-optionnel` | **Date**: 2026-09-18 | **Spec**: [spec.md](./spec.md) | |
| 4 | + | |
| 5 | +**Input**: Feature specification from `/specs/002-profil-boitiers-optionnel/spec.md` | |
| 6 | + | |
| 7 | +## Summary | |
| 8 | + | |
| 9 | +Désambiguïse par défaut deux fichiers en collision de nom d'origine à partir du seul tag EXIF `Model`, sans exiger de profil de boîtiers préalable ; ne sollicite le profil (numéro de série, puis étiquetage manuel en dernier recours) que lorsque le modèle seul ne suffit pas à distinguer les sources d'une collision réelle. Approche technique : un nouveau module `regine_core.camera_profile` (bibliothèque centrale, cf. `docs/interface-cli-gui-architecture.md`) exposant un algorithme de résolution pur (modèle → numéro de série → étiquetage manuel), consommé par le futur flux d'import (`specs/001-import-photos`) et par l'écran de configuration des boîtiers (`specs/003-config-contexte-travail` User Story 3). Les tags EXIF sont lus via `exiftool` (dépendance externe déjà implicite dans les notes de conception du projet pour ce type de lecture) ; la table `boitiers` réutilise la base de contexte centralisée déjà actée par `specs/003-config-contexte-travail`. | |
| 10 | + | |
| 11 | +## Technical Context | |
| 12 | + | |
| 13 | +**Language/Version**: Python 3.11+ (cohérent avec `regine-core`, cf. `specs/003-config-contexte-travail/plan.md` et `specs/004-categorisation-dossiers/plan.md`) | |
| 14 | + | |
| 15 | +**Primary Dependencies**: `exiftool` (binaire externe, Phil Harvey ExifTool — déjà une dépendance implicite du projet pour la lecture EXIF/XMP avancée, cf. `docs/archivage-photo-elements-cles.md` sections 11 et 13), invoqué en processus persistant (`-stay_open`) via un wrapper léger `regine_core/metadata/exif.py` ; aucune autre dépendance tierce Python | |
| 16 | + | |
| 17 | +**Storage**: réutilise la base de contexte centralisée SQLite déjà définie par `specs/003-config-contexte-travail` (même fichier, `regine_core/config/db.py` créé par `specs/004-categorisation-dossiers`) — table `boitiers`, possédée par ce module (`camera_profile`) plutôt que par `config` (cf. research.md § 2, correction de placement par rapport au plan initial de specs/003) | |
| 18 | + | |
| 19 | +**Testing**: pytest ; tests unitaires sur l'algorithme de résolution pur (modèle → numéro de série → manuel), tests d'intégration sur la persistance et la réutilisation d'une résolution déjà établie | |
| 20 | + | |
| 21 | +**Target Platform**: identique aux autres modules de `regine-core` (poste de bureau) ; nécessite `exiftool` installé sur le poste — dépendance externe à documenter à l'installation | |
| 22 | + | |
| 23 | +**Project Type**: Monorepo existant (`specs/003`, `specs/004`) — nouveau module `regine_core/camera_profile/` dans le paquet `regine-core` ; aucune nouvelle façade | |
| 24 | + | |
| 25 | +**Performance Goals**: résolution quasi instantanée pour un import typique (dizaines à centaines de fichiers) ; le coût dominant est la lecture EXIF elle-même, minimisé par un processus `exiftool` persistant plutôt qu'un lancement par fichier | |
| 26 | + | |
| 27 | +**Constraints**: ne jamais interrompre un import ni poser de question en l'absence de collision réelle (FR-008) ; jamais plus d'une question par groupe de fichiers en collision non résolu automatiquement (FR-005) | |
| 28 | + | |
| 29 | +**Scale/Scope**: profil mono-utilisateur ; nombre de boîtiers attendu faible (unités à dizaines) sur la durée de vie d'une archive personnelle | |
| 30 | + | |
| 31 | +## Constitution Check | |
| 32 | + | |
| 33 | +*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.* | |
| 34 | + | |
| 35 | +| Principe / contrainte | Évaluation | Justification | | |
| 36 | +|---|---|---| | |
| 37 | +| I. Fichier maître intouchable | N/A | Ce module ne modifie aucun fichier maître, il ne fait que lire des métadonnées EXIF déjà embarquées. | | |
| 38 | +| II. Confirmation explicite avant toute action à risque | PASS | Aucune écriture sur l'archive n'est effectuée par ce module ; la demande d'étiquetage manuel (FR-005) n'est posée qu'en cas d'ambiguïté réelle, jamais par anticipation (FR-003, FR-008), cohérent avec l'esprit du principe (ne pas sursolliciter l'utilisateur). | | |
| 39 | +| III. Identité par contenu, jamais par nom de fichier seul | PASS | La collision elle-même reste détectée par somme de contrôle de contenu (hors périmètre de ce module, fourni par l'appelant) ; ce module ajoute une désambiguïsation de la *source* par métadonnées EXIF, complémentaire et non contradictoire. | | |
| 40 | +| IV. Métadonnées ouvertes et embarquées | PASS | Le modèle et le numéro de série sont des métadonnées EXIF déjà embarquées, lues en lecture seule. Le nom lisible attribué par l'utilisateur reste une convenance locale (base de contexte), pas une métadonnée descriptive/de droits au sens du Principe IV — même justification que `specs/003-config-contexte-travail`. | | |
| 41 | +| V. L'utilisateur décide, Régine suggère | PASS | FR-005 : l'étiquetage manuel est toujours demandé, jamais deviné ; la résolution automatique ne s'appuie que sur des faits (tags EXIF identiques ou différents), jamais une heuristique probabiliste. | | |
| 42 | +| VI. Bibliothèque centrale, façades minces | PASS | Nouveau module `camera_profile` dans `regine-core` ; consommé par les futures façades CLI de l'import (`specs/001`) et de la configuration (`specs/003`) sans logique dupliquée entre elles. | | |
| 43 | +| CLI-first | PASS | Aucune commande dédiée introduite par ce plan ; l'algorithme est consommé par les futures façades CLI d'autres specs. | | |
| 44 | +| Exécution sans démon | PASS | Le processus `exiftool -stay_open` est démarré et arrêté par le processus d'import lui-même, pas un service permanent. | | |
| 45 | +| Formats ouverts et documentés | PASS | Réutilise la base SQLite déjà actée ; EXIF est un standard ouvert documenté. | | |
| 46 | + | |
| 47 | +Aucune violation identifiée ; la section Complexity Tracking reste vide. | |
| 48 | + | |
| 49 | +**Re-check post Phase 1** (après génération de `data-model.md`, `contracts/regine-core-api.md`, `quickstart.md`) : le modèle de données (une seule table `boitiers`, aucune donnée sensible) et le contrat d'API (fonctions pures + accès base de contexte, jamais d'écriture archive) confirment chaque évaluation PASS ci-dessus. Aucune violation nouvelle. | |
| 50 | + | |
| 51 | +## Project Structure | |
| 52 | + | |
| 53 | +### Documentation (this feature) | |
| 54 | + | |
| 55 | +```text | |
| 56 | +specs/002-profil-boitiers-optionnel/ | |
| 57 | +├── plan.md # This file (/speckit-plan command output) | |
| 58 | +├── research.md # Phase 0 output (/speckit-plan command) | |
| 59 | +├── data-model.md # Phase 1 output (/speckit-plan command) | |
| 60 | +├── quickstart.md # Phase 1 output (/speckit-plan command) | |
| 61 | +├── contracts/ # Phase 1 output (/speckit-plan command) | |
| 62 | +│ └── regine-core-api.md | |
| 63 | +└── tasks.md # Phase 2 output (/speckit-tasks command - NOT created by /speckit-plan) | |
| 64 | +``` | |
| 65 | + | |
| 66 | +### Source Code (repository root) — extension du monorepo posé par specs/003/004 | |
| 67 | + | |
| 68 | +```text | |
| 69 | +packages/regine-core/ | |
| 70 | +├── src/regine_core/ | |
| 71 | +│ ├── metadata/ | |
| 72 | +│ │ ├── __init__.py | |
| 73 | +│ │ └── exif.py # NOUVEAU : read_camera_tags(path) -> CameraTags, process exiftool persistant | |
| 74 | +│ └── camera_profile/ # NOUVEAU module (cf. docs/interface-cli-gui-architecture.md) | |
| 75 | +│ ├── __init__.py | |
| 76 | +│ ├── resolve.py # resolve_collision(files) -> CollisionResolution (algorithme pur) | |
| 77 | +│ └── db.py # table `boitiers` dans la base de contexte centralisée (regine_core.config.db) | |
| 78 | +│ | |
| 79 | +└── tests/ | |
| 80 | + ├── unit/ | |
| 81 | + │ ├── test_exif_reader.py # lecture Model/BodySerialNumber, tags absents | |
| 82 | + │ └── test_resolve_collision.py # modèle différent, même modèle + série, même modèle sans série (manuel) | |
| 83 | + └── integration/ | |
| 84 | + └── test_boitier_persistence.py # résolution réutilisée sans nouvelle question à l'import suivant | |
| 85 | +``` | |
| 86 | + | |
| 87 | +**Structure Decision**: Extension du monorepo existant — nouveau module `regine_core/camera_profile/` (absent jusqu'ici), plus un petit module `regine_core/metadata/` pour l'accès EXIF (également absent, réutilisable par de futurs modules `integrity`/`import`). **Correction de placement par rapport à `specs/003-config-contexte-travail/plan.md`** : ce dernier avait provisoirement prévu `regine_core/config/cameras.py` pour la logique boîtiers ; ce plan établit que la table `boitiers` et sa logique de résolution appartiennent à `camera_profile` (module de premier niveau déjà nommé dans l'architecture du projet), pas à `config`. `specs/003-config-contexte-travail/plan.md` doit être corrigé en conséquence pour que son écran de configuration des boîtiers délègue à `regine_core.camera_profile` — correction appliquée séparément après ce plan (cf. rapport de fin de commande). | |
| 88 | + | |
| 89 | +## Complexity Tracking | |
| 90 | + | |
| 91 | +*Aucune violation de gate à justifier — section laissée vide intentionnellement.* | |
| new file mode 100644 | |||
| @@ -0,0 +1,91 @@ | |||
| 1 | +# Implementation Plan: Profil de boîtiers optionnel, déclaré à la demande | ||
| 2 | + | ||
| 3 | +**Branch**: `002-profil-boitiers-optionnel` | **Date**: 2026-09-18 | **Spec**: [spec.md](./spec.md) | ||
| 4 | + | ||
| 5 | +**Input**: Feature specification from `/specs/002-profil-boitiers-optionnel/spec.md` | ||
| 6 | + | ||
| 7 | +## Summary | ||
| 8 | + | ||
| 9 | +Désambiguïse par défaut deux fichiers en collision de nom d'origine à partir du seul tag EXIF `Model`, sans exiger de profil de boîtiers préalable ; ne sollicite le profil (numéro de série, puis étiquetage manuel en dernier recours) que lorsque le modèle seul ne suffit pas à distinguer les sources d'une collision réelle. Approche technique : un nouveau module `regine_core.camera_profile` (bibliothèque centrale, cf. `docs/interface-cli-gui-architecture.md`) exposant un algorithme de résolution pur (modèle → numéro de série → étiquetage manuel), consommé par le futur flux d'import (`specs/001-import-photos`) et par l'écran de configuration des boîtiers (`specs/003-config-contexte-travail` User Story 3). Les tags EXIF sont lus via `exiftool` (dépendance externe déjà implicite dans les notes de conception du projet pour ce type de lecture) ; la table `boitiers` réutilise la base de contexte centralisée déjà actée par `specs/003-config-contexte-travail`. | ||
| 10 | + | ||
| 11 | +## Technical Context | ||
| 12 | + | ||
| 13 | +**Language/Version**: Python 3.11+ (cohérent avec `regine-core`, cf. `specs/003-config-contexte-travail/plan.md` et `specs/004-categorisation-dossiers/plan.md`) | ||
| 14 | + | ||
| 15 | +**Primary Dependencies**: `exiftool` (binaire externe, Phil Harvey ExifTool — déjà une dépendance implicite du projet pour la lecture EXIF/XMP avancée, cf. `docs/archivage-photo-elements-cles.md` sections 11 et 13), invoqué en processus persistant (`-stay_open`) via un wrapper léger `regine_core/metadata/exif.py` ; aucune autre dépendance tierce Python | ||
| 16 | + | ||
| 17 | +**Storage**: réutilise la base de contexte centralisée SQLite déjà définie par `specs/003-config-contexte-travail` (même fichier, `regine_core/config/db.py` créé par `specs/004-categorisation-dossiers`) — table `boitiers`, possédée par ce module (`camera_profile`) plutôt que par `config` (cf. research.md § 2, correction de placement par rapport au plan initial de specs/003) | ||
| 18 | + | ||
| 19 | +**Testing**: pytest ; tests unitaires sur l'algorithme de résolution pur (modèle → numéro de série → manuel), tests d'intégration sur la persistance et la réutilisation d'une résolution déjà établie | ||
| 20 | + | ||
| 21 | +**Target Platform**: identique aux autres modules de `regine-core` (poste de bureau) ; nécessite `exiftool` installé sur le poste — dépendance externe à documenter à l'installation | ||
| 22 | + | ||
| 23 | +**Project Type**: Monorepo existant (`specs/003`, `specs/004`) — nouveau module `regine_core/camera_profile/` dans le paquet `regine-core` ; aucune nouvelle façade | ||
| 24 | + | ||
| 25 | +**Performance Goals**: résolution quasi instantanée pour un import typique (dizaines à centaines de fichiers) ; le coût dominant est la lecture EXIF elle-même, minimisé par un processus `exiftool` persistant plutôt qu'un lancement par fichier | ||
| 26 | + | ||
| 27 | +**Constraints**: ne jamais interrompre un import ni poser de question en l'absence de collision réelle (FR-008) ; jamais plus d'une question par groupe de fichiers en collision non résolu automatiquement (FR-005) | ||
| 28 | + | ||
| 29 | +**Scale/Scope**: profil mono-utilisateur ; nombre de boîtiers attendu faible (unités à dizaines) sur la durée de vie d'une archive personnelle | ||
| 30 | + | ||
| 31 | +## Constitution Check | ||
| 32 | + | ||
| 33 | +*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.* | ||
| 34 | + | ||
| 35 | +| Principe / contrainte | Évaluation | Justification | | ||
| 36 | +|---|---|---| | ||
| 37 | +| I. Fichier maître intouchable | N/A | Ce module ne modifie aucun fichier maître, il ne fait que lire des métadonnées EXIF déjà embarquées. | | ||
| 38 | +| II. Confirmation explicite avant toute action à risque | PASS | Aucune écriture sur l'archive n'est effectuée par ce module ; la demande d'étiquetage manuel (FR-005) n'est posée qu'en cas d'ambiguïté réelle, jamais par anticipation (FR-003, FR-008), cohérent avec l'esprit du principe (ne pas sursolliciter l'utilisateur). | | ||
| 39 | +| III. Identité par contenu, jamais par nom de fichier seul | PASS | La collision elle-même reste détectée par somme de contrôle de contenu (hors périmètre de ce module, fourni par l'appelant) ; ce module ajoute une désambiguïsation de la *source* par métadonnées EXIF, complémentaire et non contradictoire. | | ||
| 40 | +| IV. Métadonnées ouvertes et embarquées | PASS | Le modèle et le numéro de série sont des métadonnées EXIF déjà embarquées, lues en lecture seule. Le nom lisible attribué par l'utilisateur reste une convenance locale (base de contexte), pas une métadonnée descriptive/de droits au sens du Principe IV — même justification que `specs/003-config-contexte-travail`. | | ||
| 41 | +| V. L'utilisateur décide, Régine suggère | PASS | FR-005 : l'étiquetage manuel est toujours demandé, jamais deviné ; la résolution automatique ne s'appuie que sur des faits (tags EXIF identiques ou différents), jamais une heuristique probabiliste. | | ||
| 42 | +| VI. Bibliothèque centrale, façades minces | PASS | Nouveau module `camera_profile` dans `regine-core` ; consommé par les futures façades CLI de l'import (`specs/001`) et de la configuration (`specs/003`) sans logique dupliquée entre elles. | | ||
| 43 | +| CLI-first | PASS | Aucune commande dédiée introduite par ce plan ; l'algorithme est consommé par les futures façades CLI d'autres specs. | | ||
| 44 | +| Exécution sans démon | PASS | Le processus `exiftool -stay_open` est démarré et arrêté par le processus d'import lui-même, pas un service permanent. | | ||
| 45 | +| Formats ouverts et documentés | PASS | Réutilise la base SQLite déjà actée ; EXIF est un standard ouvert documenté. | | ||
| 46 | + | ||
| 47 | +Aucune violation identifiée ; la section Complexity Tracking reste vide. | ||
| 48 | + | ||
| 49 | +**Re-check post Phase 1** (après génération de `data-model.md`, `contracts/regine-core-api.md`, `quickstart.md`) : le modèle de données (une seule table `boitiers`, aucune donnée sensible) et le contrat d'API (fonctions pures + accès base de contexte, jamais d'écriture archive) confirment chaque évaluation PASS ci-dessus. Aucune violation nouvelle. | ||
| 50 | + | ||
| 51 | +## Project Structure | ||
| 52 | + | ||
| 53 | +### Documentation (this feature) | ||
| 54 | + | ||
| 55 | +```text | ||
| 56 | +specs/002-profil-boitiers-optionnel/ | ||
| 57 | +├── plan.md # This file (/speckit-plan command output) | ||
| 58 | +├── research.md # Phase 0 output (/speckit-plan command) | ||
| 59 | +├── data-model.md # Phase 1 output (/speckit-plan command) | ||
| 60 | +├── quickstart.md # Phase 1 output (/speckit-plan command) | ||
| 61 | +├── contracts/ # Phase 1 output (/speckit-plan command) | ||
| 62 | +│ └── regine-core-api.md | ||
| 63 | +└── tasks.md # Phase 2 output (/speckit-tasks command - NOT created by /speckit-plan) | ||
| 64 | +``` | ||
| 65 | + | ||
| 66 | +### Source Code (repository root) — extension du monorepo posé par specs/003/004 | ||
| 67 | + | ||
| 68 | +```text | ||
| 69 | +packages/regine-core/ | ||
| 70 | +├── src/regine_core/ | ||
| 71 | +│ ├── metadata/ | ||
| 72 | +│ │ ├── __init__.py | ||
| 73 | +│ │ └── exif.py # NOUVEAU : read_camera_tags(path) -> CameraTags, process exiftool persistant | ||
| 74 | +│ └── camera_profile/ # NOUVEAU module (cf. docs/interface-cli-gui-architecture.md) | ||
| 75 | +│ ├── __init__.py | ||
| 76 | +│ ├── resolve.py # resolve_collision(files) -> CollisionResolution (algorithme pur) | ||
| 77 | +│ └── db.py # table `boitiers` dans la base de contexte centralisée (regine_core.config.db) | ||
| 78 | +│ | ||
| 79 | +└── tests/ | ||
| 80 | + ├── unit/ | ||
| 81 | + │ ├── test_exif_reader.py # lecture Model/BodySerialNumber, tags absents | ||
| 82 | + │ └── test_resolve_collision.py # modèle différent, même modèle + série, même modèle sans série (manuel) | ||
| 83 | + └── integration/ | ||
| 84 | + └── test_boitier_persistence.py # résolution réutilisée sans nouvelle question à l'import suivant | ||
| 85 | +``` | ||
| 86 | + | ||
| 87 | +**Structure Decision**: Extension du monorepo existant — nouveau module `regine_core/camera_profile/` (absent jusqu'ici), plus un petit module `regine_core/metadata/` pour l'accès EXIF (également absent, réutilisable par de futurs modules `integrity`/`import`). **Correction de placement par rapport à `specs/003-config-contexte-travail/plan.md`** : ce dernier avait provisoirement prévu `regine_core/config/cameras.py` pour la logique boîtiers ; ce plan établit que la table `boitiers` et sa logique de résolution appartiennent à `camera_profile` (module de premier niveau déjà nommé dans l'architecture du projet), pas à `config`. `specs/003-config-contexte-travail/plan.md` doit être corrigé en conséquence pour que son écran de configuration des boîtiers délègue à `regine_core.camera_profile` — correction appliquée séparément après ce plan (cf. rapport de fin de commande). | ||
| 88 | + | ||
| 89 | +## Complexity Tracking | ||
| 90 | + | ||
| 91 | +*Aucune violation de gate à justifier — section laissée vide intentionnellement.* | ||
added
specs/002-profil-boitiers-optionnel/quickstart.md +74 -0 | new file mode 100644 | ||
| @@ -0,0 +1,74 @@ | ||
| 1 | +# Quickstart : validation du profil de boîtiers optionnel | |
| 2 | + | |
| 3 | +Ce guide valide les 3 user stories de `spec.md` via l'API interne décrite dans `contracts/regine-core-api.md`. Comme cette fonctionnalité n'a pas de surface CLI propre (elle s'intègre au futur flux de l'import, `specs/001-import-photos`), la validation se fait directement contre `regine_core`. À exécuter une fois `regine_core.metadata.exif` et `regine_core.camera_profile` implémentés (cf. `tasks.md`). | |
| 4 | + | |
| 5 | +## Prérequis | |
| 6 | + | |
| 7 | +- `regine-core` installé, `exiftool` disponible sur le poste (`exiftool -ver`). | |
| 8 | +- Un petit jeu de fichiers de test avec métadonnées EXIF contrôlées (modèle, numéro de série) — via `exiftool` sur des copies jetables plutôt que sur des fichiers réels. | |
| 9 | + | |
| 10 | +## Scénario 1 — Aucune collision, aucune question (User Story 1, P1) | |
| 11 | + | |
| 12 | +```python | |
| 13 | +from regine_core.metadata.exif import read_camera_tags | |
| 14 | +from regine_core.camera_profile import resolve_collision | |
| 15 | + | |
| 16 | +# Un seul fichier, ou deux fichiers de modèles différents (pas de collision de nom en amont) : | |
| 17 | +# resolve_collision ne doit même pas être appelée par l'appelant dans ce cas. | |
| 18 | +``` | |
| 19 | + | |
| 20 | +**Résultat attendu** : aucune question posée ; ce scénario valide surtout le contrat côté appelant (FR-003/FR-008 : ne jamais appeler `resolve_collision` hors collision réelle). | |
| 21 | + | |
| 22 | +## Scénario 2 — Collision résolue par le modèle (User Story 2, P2) | |
| 23 | + | |
| 24 | +```python | |
| 25 | +tags = [ | |
| 26 | + read_camera_tags(fichier_a), # modele="Fujifilm X100V" | |
| 27 | + read_camera_tags(fichier_b), # modele="Ricoh GR III" | |
| 28 | +] | |
| 29 | +resolution = resolve_collision([fichier_a, fichier_b]) | |
| 30 | +assert len(resolution.a_etiqueter) == 0 | |
| 31 | +assert resolution.resolues[fichier_a] != resolution.resolues[fichier_b] | |
| 32 | +``` | |
| 33 | + | |
| 34 | +**Résultat attendu** : résolution automatique, `a_etiqueter` vide. | |
| 35 | + | |
| 36 | +## Scénario 3 — Collision résolue par le numéro de série (User Story 3, P3) | |
| 37 | + | |
| 38 | +```python | |
| 39 | +# Deux fichiers du même modèle, numéro de série différent et exploitable : | |
| 40 | +resolution = resolve_collision([fichier_a, fichier_b]) | |
| 41 | +assert len(resolution.a_etiqueter) == 0 | |
| 42 | +assert resolution.resolues[fichier_a] != resolution.resolues[fichier_b] | |
| 43 | +``` | |
| 44 | + | |
| 45 | +**Résultat attendu** : résolution automatique via le numéro de série, sans intervention utilisateur. | |
| 46 | + | |
| 47 | +## Scénario 4 — Étiquetage manuel en dernier recours | |
| 48 | + | |
| 49 | +```python | |
| 50 | +# Deux fichiers du même modèle, numéro de série absent ou identique : | |
| 51 | +resolution = resolve_collision([fichier_a, fichier_b]) | |
| 52 | +assert [fichier_a, fichier_b] in resolution.a_etiqueter | |
| 53 | + | |
| 54 | +from regine_core.camera_profile import assign_manual_source | |
| 55 | +id_a = assign_manual_source([fichier_a]) | |
| 56 | +id_b = assign_manual_source([fichier_b]) | |
| 57 | +assert id_a != id_b | |
| 58 | +``` | |
| 59 | + | |
| 60 | +**Résultat attendu** : le groupe apparaît dans `a_etiqueter`, jamais résolu arbitrairement ; `assign_manual_source` crée deux entrées distinctes. | |
| 61 | + | |
| 62 | +## Scénario 5 — Réutilisation d'une résolution déjà établie | |
| 63 | + | |
| 64 | +```python | |
| 65 | +# Réimporter un fichier du même boîtier déjà résolu (id_a) : | |
| 66 | +resolution2 = resolve_collision([fichier_c]) # même modele/numero_serie que fichier_a | |
| 67 | +assert resolution2.resolues[fichier_c] == id_a | |
| 68 | +``` | |
| 69 | + | |
| 70 | +**Résultat attendu** : aucune nouvelle question, le même `id` de boîtier est réutilisé (FR-006). | |
| 71 | + | |
| 72 | +## Critères de sortie | |
| 73 | + | |
| 74 | +Les scénarios 2 à 5 doivent passer sans accès réseau (lecture EXIF locale, base de contexte locale). Le scénario 1 valide une règle de contrat côté appelant, pas un comportement testable isolément dans `regine_core`. | |
| new file mode 100644 | |||
| @@ -0,0 +1,74 @@ | |||
| 1 | +# Quickstart : validation du profil de boîtiers optionnel | ||
| 2 | + | ||
| 3 | +Ce guide valide les 3 user stories de `spec.md` via l'API interne décrite dans `contracts/regine-core-api.md`. Comme cette fonctionnalité n'a pas de surface CLI propre (elle s'intègre au futur flux de l'import, `specs/001-import-photos`), la validation se fait directement contre `regine_core`. À exécuter une fois `regine_core.metadata.exif` et `regine_core.camera_profile` implémentés (cf. `tasks.md`). | ||
| 4 | + | ||
| 5 | +## Prérequis | ||
| 6 | + | ||
| 7 | +- `regine-core` installé, `exiftool` disponible sur le poste (`exiftool -ver`). | ||
| 8 | +- Un petit jeu de fichiers de test avec métadonnées EXIF contrôlées (modèle, numéro de série) — via `exiftool` sur des copies jetables plutôt que sur des fichiers réels. | ||
| 9 | + | ||
| 10 | +## Scénario 1 — Aucune collision, aucune question (User Story 1, P1) | ||
| 11 | + | ||
| 12 | +```python | ||
| 13 | +from regine_core.metadata.exif import read_camera_tags | ||
| 14 | +from regine_core.camera_profile import resolve_collision | ||
| 15 | + | ||
| 16 | +# Un seul fichier, ou deux fichiers de modèles différents (pas de collision de nom en amont) : | ||
| 17 | +# resolve_collision ne doit même pas être appelée par l'appelant dans ce cas. | ||
| 18 | +``` | ||
| 19 | + | ||
| 20 | +**Résultat attendu** : aucune question posée ; ce scénario valide surtout le contrat côté appelant (FR-003/FR-008 : ne jamais appeler `resolve_collision` hors collision réelle). | ||
| 21 | + | ||
| 22 | +## Scénario 2 — Collision résolue par le modèle (User Story 2, P2) | ||
| 23 | + | ||
| 24 | +```python | ||
| 25 | +tags = [ | ||
| 26 | + read_camera_tags(fichier_a), # modele="Fujifilm X100V" | ||
| 27 | + read_camera_tags(fichier_b), # modele="Ricoh GR III" | ||
| 28 | +] | ||
| 29 | +resolution = resolve_collision([fichier_a, fichier_b]) | ||
| 30 | +assert len(resolution.a_etiqueter) == 0 | ||
| 31 | +assert resolution.resolues[fichier_a] != resolution.resolues[fichier_b] | ||
| 32 | +``` | ||
| 33 | + | ||
| 34 | +**Résultat attendu** : résolution automatique, `a_etiqueter` vide. | ||
| 35 | + | ||
| 36 | +## Scénario 3 — Collision résolue par le numéro de série (User Story 3, P3) | ||
| 37 | + | ||
| 38 | +```python | ||
| 39 | +# Deux fichiers du même modèle, numéro de série différent et exploitable : | ||
| 40 | +resolution = resolve_collision([fichier_a, fichier_b]) | ||
| 41 | +assert len(resolution.a_etiqueter) == 0 | ||
| 42 | +assert resolution.resolues[fichier_a] != resolution.resolues[fichier_b] | ||
| 43 | +``` | ||
| 44 | + | ||
| 45 | +**Résultat attendu** : résolution automatique via le numéro de série, sans intervention utilisateur. | ||
| 46 | + | ||
| 47 | +## Scénario 4 — Étiquetage manuel en dernier recours | ||
| 48 | + | ||
| 49 | +```python | ||
| 50 | +# Deux fichiers du même modèle, numéro de série absent ou identique : | ||
| 51 | +resolution = resolve_collision([fichier_a, fichier_b]) | ||
| 52 | +assert [fichier_a, fichier_b] in resolution.a_etiqueter | ||
| 53 | + | ||
| 54 | +from regine_core.camera_profile import assign_manual_source | ||
| 55 | +id_a = assign_manual_source([fichier_a]) | ||
| 56 | +id_b = assign_manual_source([fichier_b]) | ||
| 57 | +assert id_a != id_b | ||
| 58 | +``` | ||
| 59 | + | ||
| 60 | +**Résultat attendu** : le groupe apparaît dans `a_etiqueter`, jamais résolu arbitrairement ; `assign_manual_source` crée deux entrées distinctes. | ||
| 61 | + | ||
| 62 | +## Scénario 5 — Réutilisation d'une résolution déjà établie | ||
| 63 | + | ||
| 64 | +```python | ||
| 65 | +# Réimporter un fichier du même boîtier déjà résolu (id_a) : | ||
| 66 | +resolution2 = resolve_collision([fichier_c]) # même modele/numero_serie que fichier_a | ||
| 67 | +assert resolution2.resolues[fichier_c] == id_a | ||
| 68 | +``` | ||
| 69 | + | ||
| 70 | +**Résultat attendu** : aucune nouvelle question, le même `id` de boîtier est réutilisé (FR-006). | ||
| 71 | + | ||
| 72 | +## Critères de sortie | ||
| 73 | + | ||
| 74 | +Les scénarios 2 à 5 doivent passer sans accès réseau (lecture EXIF locale, base de contexte locale). Le scénario 1 valide une règle de contrat côté appelant, pas un comportement testable isolément dans `regine_core`. | ||
added
specs/002-profil-boitiers-optionnel/research.md +36 -0 | new file mode 100644 | ||
| @@ -0,0 +1,36 @@ | ||
| 1 | +# Research: Profil de boîtiers optionnel, déclaré à la demande | |
| 2 | + | |
| 3 | +## 1. Lecture des tags EXIF `Model` et `BodySerialNumber` | |
| 4 | + | |
| 5 | +**Decision**: Lire ces tags via le binaire externe `exiftool`, en processus persistant (`exiftool -stay_open`) plutôt qu'un lancement par fichier, à travers un wrapper léger `regine_core/metadata/exif.py`. | |
| 6 | + | |
| 7 | +**Rationale**: Les notes de conception du projet (`docs/archivage-photo-elements-cles.md` sections 11 et 13) utilisent déjà `exiftool` pour toute lecture EXIF avancée sur ce projet (`RawDataUniqueID`, `ImageDataHash`, `DocumentID`/`OriginalDocumentID`) en raison de son support fiable des RAW propriétaires, que peu de bibliothèques Python pures gèrent correctement. Réutiliser le même outil pour `Model`/`BodySerialNumber` évite d'introduire un second mécanisme de lecture EXIF avec un comportement potentiellement différent selon le format. Le mode `-stay_open` évite le coût de démarrage d'un nouveau processus par fichier sur un import de plusieurs centaines de photos. | |
| 8 | + | |
| 9 | +**Alternatives considered**: | |
| 10 | +- Bibliothèque Python pure (`piexif`, `exifread`, `Pillow.ExifTags`) — rejeté : support inconsistant des formats RAW propriétaires (RAF, CR2, NEF, ARW...), alors que `exiftool` est déjà le choix retenu ailleurs dans le projet pour cette même raison. | |
| 11 | +- Appel `exiftool` ponctuel par fichier (sans `-stay_open`) — rejeté : coût de démarrage du processus non négligeable multiplié par le nombre de fichiers d'un import typique. | |
| 12 | + | |
| 13 | +## 2. Emplacement de la table `boitiers` et de la logique de résolution | |
| 14 | + | |
| 15 | +**Decision**: Nouveau module `regine_core/camera_profile/`, propriétaire de la table `boitiers` (dans la base de contexte centralisée déjà actée par `specs/003-config-contexte-travail`) et de l'algorithme de résolution. **Corrige un placement provisoire** : `specs/003-config-contexte-travail/plan.md` avait prévu `regine_core/config/cameras.py` pour cette logique ; ce plan établit `camera_profile` comme propriétaire, `config` ne conservant que son écran de nommage proactif (User Story 3 de specs/003), qui appelle `camera_profile` plutôt que de posséder sa propre logique. | |
| 16 | + | |
| 17 | +**Rationale**: `docs/interface-cli-gui-architecture.md` liste déjà `camera_profile` comme module de premier niveau distinct de la configuration des chemins de travail. Laisser deux specs (002 et 003) définir chacune leur propre table `boitiers` contredirait le Principe VI de la constitution (bibliothèque centrale, pas de logique métier dupliquée) et risquerait une divergence de schéma entre les deux. | |
| 18 | + | |
| 19 | +**Alternatives considered**: | |
| 20 | +- Conserver la table sous `regine_core/config/` comme provisoirement planifié en specs/003 — rejeté pour la raison ci-dessus ; `specs/003-config-contexte-travail/plan.md` est corrigé en conséquence dans la foulée de ce plan. | |
| 21 | + | |
| 22 | +## 3. Conception de l'algorithme de résolution | |
| 23 | + | |
| 24 | +**Decision**: Fonction pure `resolve_collision(files: list[FileWithTags]) -> CollisionResolution`, qui prend en entrée un groupe de fichiers déjà identifié en collision (même nom d'origine, sommes de contrôle différentes — détection hors périmètre de ce module, fournie par l'appelant), et qui : | |
| 25 | +1. Regroupe les fichiers par tag `Model`. | |
| 26 | +2. Au sein d'un groupe de même modèle, tente de les distinguer par `BodySerialNumber` (quand présent et exploitable). | |
| 27 | +3. Retourne, pour les fichiers encore indistincts après ces deux étapes, un groupe nécessitant un étiquetage manuel (FR-005), plutôt que de choisir arbitrairement. | |
| 28 | + | |
| 29 | +**Rationale**: Séparer la détection de collision (par somme de contrôle, module import/integrity) de la désambiguïsation de source (par métadonnées, ce module) garde chaque responsabilité testable indépendamment — cohérent avec la découpe en fonctions pures déjà retenue pour `specs/004-categorisation-dossiers`. | |
| 30 | + | |
| 31 | +**Alternatives considered**: | |
| 32 | +- Fusionner détection de collision et résolution de source dans une seule fonction — rejeté : mélangerait deux responsabilités relevant de modules différents (checksum vs métadonnées EXIF). | |
| 33 | + | |
| 34 | +## Résumé | |
| 35 | + | |
| 36 | +Tous les points du Technical Context sont résolus. Aucune dépendance tierce Python nouvelle ; `exiftool` est une dépendance externe déjà implicite du projet, pas une addition. Une correction de placement de module est propagée vers `specs/003-config-contexte-travail/plan.md`. | |
| new file mode 100644 | |||
| @@ -0,0 +1,36 @@ | |||
| 1 | +# Research: Profil de boîtiers optionnel, déclaré à la demande | ||
| 2 | + | ||
| 3 | +## 1. Lecture des tags EXIF `Model` et `BodySerialNumber` | ||
| 4 | + | ||
| 5 | +**Decision**: Lire ces tags via le binaire externe `exiftool`, en processus persistant (`exiftool -stay_open`) plutôt qu'un lancement par fichier, à travers un wrapper léger `regine_core/metadata/exif.py`. | ||
| 6 | + | ||
| 7 | +**Rationale**: Les notes de conception du projet (`docs/archivage-photo-elements-cles.md` sections 11 et 13) utilisent déjà `exiftool` pour toute lecture EXIF avancée sur ce projet (`RawDataUniqueID`, `ImageDataHash`, `DocumentID`/`OriginalDocumentID`) en raison de son support fiable des RAW propriétaires, que peu de bibliothèques Python pures gèrent correctement. Réutiliser le même outil pour `Model`/`BodySerialNumber` évite d'introduire un second mécanisme de lecture EXIF avec un comportement potentiellement différent selon le format. Le mode `-stay_open` évite le coût de démarrage d'un nouveau processus par fichier sur un import de plusieurs centaines de photos. | ||
| 8 | + | ||
| 9 | +**Alternatives considered**: | ||
| 10 | +- Bibliothèque Python pure (`piexif`, `exifread`, `Pillow.ExifTags`) — rejeté : support inconsistant des formats RAW propriétaires (RAF, CR2, NEF, ARW...), alors que `exiftool` est déjà le choix retenu ailleurs dans le projet pour cette même raison. | ||
| 11 | +- Appel `exiftool` ponctuel par fichier (sans `-stay_open`) — rejeté : coût de démarrage du processus non négligeable multiplié par le nombre de fichiers d'un import typique. | ||
| 12 | + | ||
| 13 | +## 2. Emplacement de la table `boitiers` et de la logique de résolution | ||
| 14 | + | ||
| 15 | +**Decision**: Nouveau module `regine_core/camera_profile/`, propriétaire de la table `boitiers` (dans la base de contexte centralisée déjà actée par `specs/003-config-contexte-travail`) et de l'algorithme de résolution. **Corrige un placement provisoire** : `specs/003-config-contexte-travail/plan.md` avait prévu `regine_core/config/cameras.py` pour cette logique ; ce plan établit `camera_profile` comme propriétaire, `config` ne conservant que son écran de nommage proactif (User Story 3 de specs/003), qui appelle `camera_profile` plutôt que de posséder sa propre logique. | ||
| 16 | + | ||
| 17 | +**Rationale**: `docs/interface-cli-gui-architecture.md` liste déjà `camera_profile` comme module de premier niveau distinct de la configuration des chemins de travail. Laisser deux specs (002 et 003) définir chacune leur propre table `boitiers` contredirait le Principe VI de la constitution (bibliothèque centrale, pas de logique métier dupliquée) et risquerait une divergence de schéma entre les deux. | ||
| 18 | + | ||
| 19 | +**Alternatives considered**: | ||
| 20 | +- Conserver la table sous `regine_core/config/` comme provisoirement planifié en specs/003 — rejeté pour la raison ci-dessus ; `specs/003-config-contexte-travail/plan.md` est corrigé en conséquence dans la foulée de ce plan. | ||
| 21 | + | ||
| 22 | +## 3. Conception de l'algorithme de résolution | ||
| 23 | + | ||
| 24 | +**Decision**: Fonction pure `resolve_collision(files: list[FileWithTags]) -> CollisionResolution`, qui prend en entrée un groupe de fichiers déjà identifié en collision (même nom d'origine, sommes de contrôle différentes — détection hors périmètre de ce module, fournie par l'appelant), et qui : | ||
| 25 | +1. Regroupe les fichiers par tag `Model`. | ||
| 26 | +2. Au sein d'un groupe de même modèle, tente de les distinguer par `BodySerialNumber` (quand présent et exploitable). | ||
| 27 | +3. Retourne, pour les fichiers encore indistincts après ces deux étapes, un groupe nécessitant un étiquetage manuel (FR-005), plutôt que de choisir arbitrairement. | ||
| 28 | + | ||
| 29 | +**Rationale**: Séparer la détection de collision (par somme de contrôle, module import/integrity) de la désambiguïsation de source (par métadonnées, ce module) garde chaque responsabilité testable indépendamment — cohérent avec la découpe en fonctions pures déjà retenue pour `specs/004-categorisation-dossiers`. | ||
| 30 | + | ||
| 31 | +**Alternatives considered**: | ||
| 32 | +- Fusionner détection de collision et résolution de source dans une seule fonction — rejeté : mélangerait deux responsabilités relevant de modules différents (checksum vs métadonnées EXIF). | ||
| 33 | + | ||
| 34 | +## Résumé | ||
| 35 | + | ||
| 36 | +Tous les points du Technical Context sont résolus. Aucune dépendance tierce Python nouvelle ; `exiftool` est une dépendance externe déjà implicite du projet, pas une addition. Une correction de placement de module est propagée vers `specs/003-config-contexte-travail/plan.md`. | ||
modified
specs/003-config-contexte-travail/plan.md +6 -5 | @@ -14,7 +14,7 @@ Le module de configuration établit le contexte de travail dont dépendent tous | ||
| 14 | 14 | |
| 15 | 15 | **Primary Dependencies**: bibliothèque standard uniquement pour ce module (`sqlite3`, `pathlib`, `shutil`, `subprocess` pour déclencher le montage natif) ; pas de dépendance tierce nouvelle identifiée (cf. research.md) |
| 16 | 16 | |
| 17 | -**Storage**: SQLite — deux bases distinctes : la base de contexte centralisée (niveau contexte de travail, ce module) et les bases de données de travail par dossier (une par dossier local, initialisées par ce module mais dont le schéma détaillé relève du module `archive`/`dossier`, cf. constitution § Workflow d'archivage) | |
| 17 | +**Storage**: SQLite — deux bases distinctes : la base de contexte centralisée (fichier niveau contexte de travail, ouverture/bootstrap assurés par ce module via `regine_core/config/db.py`, mais dont la table `boitiers` est possédée par le module `camera_profile`, cf. `specs/002-profil-boitiers-optionnel/plan.md` § Correction de placement) et les bases de données de travail par dossier (une par dossier local, initialisées par ce module mais dont le schéma détaillé relève du module `archive`/`dossier`, cf. constitution § Workflow d'archivage) | |
| 18 | 18 | |
| 19 | 19 | **Testing**: pytest ; tests de contrat sur la surface CLI (entrées/sorties texte, codes de sortie), tests d'intégration sur le cycle configuration → initialisation de la base de travail, tests unitaires sur la validation des chemins et la logique de désambiguïsation de boîtiers |
| 20 | 20 | |
| @@ -77,14 +77,15 @@ packages/ | ||
| 77 | 77 | │ │ ├── context.py # Contexte de travail : lecture/écriture des 3 chemins, validation |
| 78 | 78 | │ │ ├── db.py # Ouverture/initialisation SQLite (base de contexte centralisée + base de travail par dossier) |
| 79 | 79 | │ │ ├── smb.py # Détection de disponibilité + déclenchement du montage natif SMB (research.md) |
| 80 | -│ │ └── cameras.py # Enregistrement/désambiguïsation/nommage des boîtiers | |
| 80 | +│ │ └── cameras_screen.py # Écran de nommage (User Story 3) : délègue à regine_core.camera_profile (specs/002), ne possède aucune logique de désambiguïsation propre | |
| 81 | 81 | │ └── tests/ |
| 82 | 82 | │ ├── contract/ # Contrats internes de regine_core (objets structurés retournés, cf. Principe VI) |
| 83 | 83 | │ ├── integration/ |
| 84 | 84 | │ │ └── test_config_context.py # Cycle configuration → initialisation base de travail → changement de chemin bloqué |
| 85 | 85 | │ └── unit/ |
| 86 | -│ ├── test_context_paths.py # Validation/création des répertoires, cas de collision | |
| 87 | -│ └── test_cameras_db.py # Désambiguïsation modèle/numéro de série, nommage | |
| 86 | +│ └── test_context_paths.py # Validation/création des répertoires, cas de collision | |
| 87 | +│ # La désambiguïsation modèle/numéro de série est testée dans specs/002-profil-boitiers-optionnel | |
| 88 | +│ # (packages/regine-core/tests/unit/test_resolve_collision.py), pas ici. | |
| 88 | 89 | │ |
| 89 | 90 | ├── regine-cli/ # Façade CLI — dépend de regine-core, aucune logique métier propre |
| 90 | 91 | │ ├── pyproject.toml |
| @@ -103,7 +104,7 @@ packages/ | ||
| 103 | 104 | └── (scaffold vide : pyproject.toml déclarant la dépendance à regine-core, aucun code métier) |
| 104 | 105 | ``` |
| 105 | 106 | |
| 106 | -**Structure Decision**: Monorepo à quatre parties (Option 1 adaptée du template, multi-packages plutôt que `src/` unique), conformément au Principe VI de la constitution (bibliothèque centrale, façades minces) et à la demande explicite d'organiser dès maintenant la place de la GUI et de l'agent IA. `regine-core` est le premier package de la bibliothèque centrale de Régine (aucun code existant avant ce plan) et pose la convention `packages/<nom>/src/<nom_paquet>/` que les modules métier futurs (`import`, `archive`, `dossier`, `metadata`, `integrity`, `camera_profile` de specs/002) réutiliseront à l'intérieur de `regine-core`. Ce plan implémente `regine-core/config` et `regine-cli` ; `regine-gui` et `regine-agent` sont créés comme paquets vides (dépendance déclarée vers `regine-core`, sans code métier) pour réserver la structure, sans être développés ici — leur implémentation relèvera de specs dédiées. L'outillage de workspace (gestion des dépendances inter-paquets, lockfile commun) est tranché en research.md § 5. | |
| 107 | +**Structure Decision**: Monorepo à quatre parties (Option 1 adaptée du template, multi-packages plutôt que `src/` unique), conformément au Principe VI de la constitution (bibliothèque centrale, façades minces) et à la demande explicite d'organiser dès maintenant la place de la GUI et de l'agent IA. `regine-core` est le premier package de la bibliothèque centrale de Régine (aucun code existant avant ce plan) et pose la convention `packages/<nom>/src/<nom_paquet>/` que les modules métier futurs (`import`, `archive`, `dossier`, `metadata`, `integrity`, `camera_profile` de specs/002) réutiliseront à l'intérieur de `regine-core`. Ce plan implémente `regine-core/config` (chemins, montage SMB, écran de nommage des boîtiers) et `regine-cli` ; `regine-gui` et `regine-agent` sont créés comme paquets vides (dépendance déclarée vers `regine-core`, sans code métier) pour réserver la structure, sans être développés ici. **Correction du 2026-09-18** : la logique de désambiguïsation et la table `boitiers` (initialement esquissées ici sous `config/cameras.py`) sont possédées par le module `regine_core.camera_profile`, spécifié séparément par `specs/002-profil-boitiers-optionnel/plan.md` — `config` ne fait qu'exposer un écran qui délègue à cette API, cohérent avec le Principe VI (une seule bibliothèque centrale, pas de logique dupliquée entre deux specs). L'outillage de workspace (gestion des dépendances inter-paquets, lockfile commun) est tranché en research.md § 5. | |
| 107 | 108 | |
| 108 | 109 | ## Complexity Tracking |
| 109 | 110 | |
| @@ -14,7 +14,7 @@ Le module de configuration établit le contexte de travail dont dépendent tous | |||
| 14 | 14 | ||
| 15 | **Primary Dependencies**: bibliothèque standard uniquement pour ce module (`sqlite3`, `pathlib`, `shutil`, `subprocess` pour déclencher le montage natif) ; pas de dépendance tierce nouvelle identifiée (cf. research.md) | 15 | **Primary Dependencies**: bibliothèque standard uniquement pour ce module (`sqlite3`, `pathlib`, `shutil`, `subprocess` pour déclencher le montage natif) ; pas de dépendance tierce nouvelle identifiée (cf. research.md) |
| 16 | 16 | ||
| 17 | -**Storage**: SQLite — deux bases distinctes : la base de contexte centralisée (niveau contexte de travail, ce module) et les bases de données de travail par dossier (une par dossier local, initialisées par ce module mais dont le schéma détaillé relève du module `archive`/`dossier`, cf. constitution § Workflow d'archivage) | 17 | +**Storage**: SQLite — deux bases distinctes : la base de contexte centralisée (fichier niveau contexte de travail, ouverture/bootstrap assurés par ce module via `regine_core/config/db.py`, mais dont la table `boitiers` est possédée par le module `camera_profile`, cf. `specs/002-profil-boitiers-optionnel/plan.md` § Correction de placement) et les bases de données de travail par dossier (une par dossier local, initialisées par ce module mais dont le schéma détaillé relève du module `archive`/`dossier`, cf. constitution § Workflow d'archivage) |
| 18 | 18 | ||
| 19 | **Testing**: pytest ; tests de contrat sur la surface CLI (entrées/sorties texte, codes de sortie), tests d'intégration sur le cycle configuration → initialisation de la base de travail, tests unitaires sur la validation des chemins et la logique de désambiguïsation de boîtiers | 19 | **Testing**: pytest ; tests de contrat sur la surface CLI (entrées/sorties texte, codes de sortie), tests d'intégration sur le cycle configuration → initialisation de la base de travail, tests unitaires sur la validation des chemins et la logique de désambiguïsation de boîtiers |
| 20 | 20 | ||
| @@ -77,14 +77,15 @@ packages/ | |||
| 77 | │ │ ├── context.py # Contexte de travail : lecture/écriture des 3 chemins, validation | 77 | │ │ ├── context.py # Contexte de travail : lecture/écriture des 3 chemins, validation |
| 78 | │ │ ├── db.py # Ouverture/initialisation SQLite (base de contexte centralisée + base de travail par dossier) | 78 | │ │ ├── db.py # Ouverture/initialisation SQLite (base de contexte centralisée + base de travail par dossier) |
| 79 | │ │ ├── smb.py # Détection de disponibilité + déclenchement du montage natif SMB (research.md) | 79 | │ │ ├── smb.py # Détection de disponibilité + déclenchement du montage natif SMB (research.md) |
| 80 | -│ │ └── cameras.py # Enregistrement/désambiguïsation/nommage des boîtiers | 80 | +│ │ └── cameras_screen.py # Écran de nommage (User Story 3) : délègue à regine_core.camera_profile (specs/002), ne possède aucune logique de désambiguïsation propre |
| 81 | │ └── tests/ | 81 | │ └── tests/ |
| 82 | │ ├── contract/ # Contrats internes de regine_core (objets structurés retournés, cf. Principe VI) | 82 | │ ├── contract/ # Contrats internes de regine_core (objets structurés retournés, cf. Principe VI) |
| 83 | │ ├── integration/ | 83 | │ ├── integration/ |
| 84 | │ │ └── test_config_context.py # Cycle configuration → initialisation base de travail → changement de chemin bloqué | 84 | │ │ └── test_config_context.py # Cycle configuration → initialisation base de travail → changement de chemin bloqué |
| 85 | │ └── unit/ | 85 | │ └── unit/ |
| 86 | -│ ├── test_context_paths.py # Validation/création des répertoires, cas de collision | 86 | +│ └── test_context_paths.py # Validation/création des répertoires, cas de collision |
| 87 | -│ └── test_cameras_db.py # Désambiguïsation modèle/numéro de série, nommage | 87 | +│ # La désambiguïsation modèle/numéro de série est testée dans specs/002-profil-boitiers-optionnel |
| 88 | +│ # (packages/regine-core/tests/unit/test_resolve_collision.py), pas ici. | ||
| 88 | │ | 89 | │ |
| 89 | ├── regine-cli/ # Façade CLI — dépend de regine-core, aucune logique métier propre | 90 | ├── regine-cli/ # Façade CLI — dépend de regine-core, aucune logique métier propre |
| 90 | │ ├── pyproject.toml | 91 | │ ├── pyproject.toml |
| @@ -103,7 +104,7 @@ packages/ | |||
| 103 | └── (scaffold vide : pyproject.toml déclarant la dépendance à regine-core, aucun code métier) | 104 | └── (scaffold vide : pyproject.toml déclarant la dépendance à regine-core, aucun code métier) |
| 104 | ``` | 105 | ``` |
| 105 | 106 | ||
| 106 | -**Structure Decision**: Monorepo à quatre parties (Option 1 adaptée du template, multi-packages plutôt que `src/` unique), conformément au Principe VI de la constitution (bibliothèque centrale, façades minces) et à la demande explicite d'organiser dès maintenant la place de la GUI et de l'agent IA. `regine-core` est le premier package de la bibliothèque centrale de Régine (aucun code existant avant ce plan) et pose la convention `packages/<nom>/src/<nom_paquet>/` que les modules métier futurs (`import`, `archive`, `dossier`, `metadata`, `integrity`, `camera_profile` de specs/002) réutiliseront à l'intérieur de `regine-core`. Ce plan implémente `regine-core/config` et `regine-cli` ; `regine-gui` et `regine-agent` sont créés comme paquets vides (dépendance déclarée vers `regine-core`, sans code métier) pour réserver la structure, sans être développés ici — leur implémentation relèvera de specs dédiées. L'outillage de workspace (gestion des dépendances inter-paquets, lockfile commun) est tranché en research.md § 5. | 107 | +**Structure Decision**: Monorepo à quatre parties (Option 1 adaptée du template, multi-packages plutôt que `src/` unique), conformément au Principe VI de la constitution (bibliothèque centrale, façades minces) et à la demande explicite d'organiser dès maintenant la place de la GUI et de l'agent IA. `regine-core` est le premier package de la bibliothèque centrale de Régine (aucun code existant avant ce plan) et pose la convention `packages/<nom>/src/<nom_paquet>/` que les modules métier futurs (`import`, `archive`, `dossier`, `metadata`, `integrity`, `camera_profile` de specs/002) réutiliseront à l'intérieur de `regine-core`. Ce plan implémente `regine-core/config` (chemins, montage SMB, écran de nommage des boîtiers) et `regine-cli` ; `regine-gui` et `regine-agent` sont créés comme paquets vides (dépendance déclarée vers `regine-core`, sans code métier) pour réserver la structure, sans être développés ici. **Correction du 2026-09-18** : la logique de désambiguïsation et la table `boitiers` (initialement esquissées ici sous `config/cameras.py`) sont possédées par le module `regine_core.camera_profile`, spécifié séparément par `specs/002-profil-boitiers-optionnel/plan.md` — `config` ne fait qu'exposer un écran qui délègue à cette API, cohérent avec le Principe VI (une seule bibliothèque centrale, pas de logique dupliquée entre deux specs). L'outillage de workspace (gestion des dépendances inter-paquets, lockfile commun) est tranché en research.md § 5. |
| 107 | 108 | ||
| 108 | ## Complexity Tracking | 109 | ## Complexity Tracking |
| 109 | 110 | ||