Aller au contenu

Interface en ligne de commande (CLI)

À côté de l'IHM JavaFX, VigieChiro Companion expose un point d'entrée sans interface graphique : fr.univ_amu.iut.cli.Cli. Il répond au besoin de scriptabilité (parcours A10 : enchaîner des imports/exports sans clics, pour les utilisateurs avancés). La CLI n'a pas de logique propre : elle orchestre les services métier existants (ServiceImport, ServiceLot, ServiceValidation, DAO multi-features).

Principe : réutiliser, pas réimplémenter

La CLI et l'IHM sont deux façades sur le même cœur métier. Tout ce que fait la ligne de commande, l'application graphique le fait aussi, via les mêmes services. C'est l'intérêt d'avoir isolé le métier des vues (cf. Architecture) : on peut lui greffer une seconde surface sans le dupliquer.

Comment elle s'assemble (injecteur enfant)

La CLI a besoin de tout le graphe applicatif (socle + features) plus quelques aides de lecture qui lui sont propres. Plutôt que de modifier la composition racine, elle crée un injecteur enfant :

RacineInjecteur.creer().createChildInjector(new CliModule());

L'enfant hérite de tous les bindings du socle et des features (dont les services et DAO), et y ajoute les aides CLI sans rien retirer ni remplacer. C'est le patron injecteur enfant de Guice, détaillé dans Injection (Guice).

flowchart LR
    main["Cli.main()"] --> child
    subgraph child["Injecteur enfant"]
        CliModule["CliModule<br/>(RegistrePassages)"]
    end
    child -.hérite de.-> root["RacineInjecteur.creer()<br/>socle + features"]
    child --> svc["ServiceImport · ServiceLot<br/>ServiceValidation · DAO"]

CliModule n'apporte qu'une chose : RegistrePassages, une lecture transverse qui croise les DAO de passage et sites pour reconstituer le contexte « carré / point » de chaque passage. La dépendance va cli → <feature>.model.dao (jamais vers une view/viewmodel) : c'est l'unique entorse autorisée par la règle ArchUnit assouplie, et cli reste un puits (aucune feature ne dépend de lui), donc le graphe reste acyclique.

Les sous-commandes

Ce tableau est tenu par un test

DocumentationAJourTest confronte ce tableau aux sous-commandes réellement câblées dans l'annotation @Command de CommandeRacine : ajouter une commande sans lui écrire sa ligne fait rougir la CI (#1458). Le tableau a compté jusqu'à 22 commandes sur 29 avant qu'on s'en aperçoive.

Commande Options Parcours Service
creer-site --carre <n> [--nom ..] [--protocole ..] [--commentaire ..] [--sans-verification] A10 ServiceSites.creerSite
recuperer-carre --carre <n> [--nom ..] [--protocole ..] [--commentaire ..] A10 RapatriementCarre.rapatrier
situer-carre --position "<lat, lon>" A10 PropositionCarre.pour. Aucun réseau : le carroyage national est embarqué (137 479 mailles, métropole), et c'est ce qui rend la commande utile à un script - traiter un lot de nuits dont on a les positions GPS sans ouvrir l'application. Trois issues distinguées par le code de sortie, pour qu'un script tranche sans lire la prose : un carré unique s'écrit sur la sortie standard (exit 0) ; sur une frontière les candidats s'y écrivent tous et le motif part sur l'erreur (exit 1), parce que deux centres à distance égale ne se départagent pas ; hors métropole rien ne s'écrit (exit 1) ; un texte illisible rend un code d'usage (exit 2), qui n'est pas la même chose qu'une position sans carré
modifier-site --site <id> --carre <n> [--nom ..] [--protocole ..] [--commentaire ..] A10 ServiceSites.modifierSite
supprimer-site --site <id> [--confirmer] A10 ServiceSites.supprimerSite
ajouter-point --site <id> --code <c> [--lat ..] [--lon ..] [--description ..] A10 ServiceSites.ajouterPoint. Avec une position, contrôle le carré du site hors ligne (#4671) : une divergence part sur stderr, l'identifiant du point reste seul sur stdout et le code de sortie ne change pas - POINT=$(vigiechiro ajouter-point ...) continue de marcher
modifier-point --point <id> --site <id> --code <c> [--lat ..] [--lon ..] [--description ..] A10 ServiceSites.modifierPoint. Même contrôle qu'ajouter-point : le défaut se pose aussi bien en déplaçant qu'en créant
lister-sites [--json] A10 ServiceSites (lecture)
lister-sites-vigiechiro [--portee mes\|plateforme] [--pages <n> \| --tout] [--point <code>] [--carre <n>] [--recenser] [--json] [--token <jeton>] #3003 ClientVigieChiro.sitesPlateforme / ClientVigieChiro.mesSites / ClientVigieChiro.chercherCarre. Interroge le catalogue de la plateforme (20 767 sites au 2026-08-14) ou vos sites. --recenser compte les sites par code de point : c'est ainsi qu'on établit qu'un code comme Z1 est porté par des centaines de carrés (#2993). Chaque sortie porte son dénominateur (« 300 lus sur 20517 annoncés, 3 pages sur 206 ») et signale les sites sans point ponctuel (transects routiers, dont les localités sont des lignes) : un échantillon ne doit jamais passer pour un recensement. --carre fait exception depuis #3769 : il est porté par le serveur (q=<numéro>, filtre mesuré le 2026-08-14 - 20 767 → 1), donc en une requête au lieu de deux cents pages. Le gain n'est pas d'abord la vitesse : sans --tout, la commande lisait une page sur 208 et rendait un tableau vide sur un carré qui existe. Sa sortie annonce alors une recherche (« Recherche du carré 130711 sur toute la collection : 1 site(s) trouvé(s) ») et non une lecture partielle, et --pages / --tout sont refusés avec lui (exit 2) plutôt qu'acceptés sans effet. Les autres filtres (--point) restent appliqués chez nous, ce backend ignorant where= en silence
lister-participations-vigiechiro [--json] [--token <jeton>] #3005 ClientVigieChiro.mesParticipations. Vos nuits déposées, avec leur identifiant : c'est lui que réclament importer-vigiechiro --participation et reconstruire-passage --participation, et qu'aucune commande ne donnait. reconstruire-passage sans argument ne liste que les participations orphelines ; une nuit déjà rattachée n'apparaissait nulle part
api lire --chemin <chemin> [--page <n>], ressources [--sonder] #3006 Interrogation brute de l'API, en lecture seule. Groupe volontairement discret : ses sous-commandes ne sont pas détaillées ici (elles parlent le langage de l'API, pas celui du produit) - le détail vit dans api-vigiechiro.md. lire refuse avant d'émettre les deux pièges qui ne préviennent pas (max_results au-delà de 100, where= que le serveur ignore) ; ressources affiche la carte des lectures et sait la confronter au serveur. Aucune écriture : ADR 3006
creer-campagne --nom <n> [--annee N] [--commentaire ..] #2355 ServiceCampagne.creerCampagne : crée une campagne (regroupement facultatif de passages). Année par défaut = année courante. Feature campagne (désactivable)
lister-campagnes [--json] #2355 ServiceCampagne (lecture) : les campagnes de suivi, de la plus récente à la plus ancienne
modifier-campagne --campagne <id> --nom <n> --annee N [--commentaire ..] #2355 ServiceCampagne.modifierCampagne : corrige nom, année et commentaire. Sans elle, corriger une campagne obligeait à la supprimer, donc à détacher tous ses passages
rattacher-campagne --passage <id> [--campagne <id>] #2355 ServiceCampagne.rattacherPassage : rattache un passage à une campagne, ou l'en détache si --campagne est omis. Supprimer la campagne détache aussi (ON DELETE SET NULL)
supprimer-campagne --campagne <id> #2355 ServiceCampagne.supprimerCampagne : supprime le regroupement, jamais les nuits, les passages rattachés sont détachés (ON DELETE SET NULL)
lister-passages [--carre <n>] [--lieu <texte>…] [--annee <a>] [--statut <s>] [--verdict <v>] [--analyse <e>] [--campagne <nom>] [--json] P5, #3269 RegistrePassages (lecture). Les sept filtres sont ceux de l'écran « Carte & passages » et passent par le même prédicat (FiltresMultisite, un record dont accepte ignore les critères non renseignés) ; --lieu lit FiltresLieu, comme lister-observations. Aucune règle n'est réécrite. --analyse porte un état déduit (EtatAnalyse.deduire) et non lu : « Déposé » sans relevé vaut JAMAIS_RELEVE, « Importé » vaut SANS_OBJET. Une base sans aucun passage le dit avant tout filtrage : sinon --lieu refuserait le lieu (ADR 3082) là où la vérité est qu'il n'y a rien. Un filtre qui ne retient rien le dit aussi
solde-saison [--annee N] [--campagne <nom>] [--lieu <texte>] [--reste-a-faire] [--format texte\|csv\|json] #2356 ServiceSoldeSaison (lecture) : ce qu'il reste à faire, point par point, pour une saison (règles R3/R4 restituées). --lieu et --reste-a-faire sont les deux filtres de l'écran « Ma saison » (#3103), portés ici à la clôture de #3092 et lus sur la même écriture (FiltresSaison). Ils ne touchent que la liste des points : l'en-tête continue d'annoncer le solde de la saison entière, comme à l'écran. Un filtre qui ne retient rien le dit. Même décompte que l'écran « Ma saison » (service partagé, parité IHM/CLI). texte par défaut, csv/json scriptables. Les trois sorties montrent le nom du carré (#3289), par lequel --lieu sait déjà chercher : le texte le qualifie (640380 · Vallon), le CSV a sa colonne « Nom du carré », le JSON sa clé nom_site. L'écran, lui, lui donne une colonne : qualifié dans « Carré », le nom s'y faisait tronquer. La commune suit le même chemin (#3313) : colonne à l'écran, Commune au CSV, commune en JSON, entre parenthèses en texte, et --lieu la retient
statut-passage --passage <id> [--json] M-Passage, #1878 ServicePassage.detailPassage + ResultatsIdentificationDao + ServiceConditionsPassage.heuresProuvees (lecture). La ligne « Nuit » dit l'origine de ses heures - [attestées par les enregistrements] ou [déclarées, modifiables] -, et le JSON porte heuresProuvees : un script sait ainsi avant d'essayer si metadonnees-passage --heure-debut sera refusé, au lieu de l'apprendre en échouant
verifier-maj (aucune option) #2109 VerificateurMiseAJour (lecture seule, réseau) : indique si une version plus récente est publiée. Pendant CLI de l'annonce au démarrage de l'IHM. Trois codes de sortie, parce qu'un script ne pilote pas « à jour » et « je n'ai pas pu savoir » de la même façon : 0 à jour, 10 mise à jour disponible, 1 vérification impossible (hors ligne, ou version locale inconnue car lancée hors d'un artefact publié). Se tait dès qu'un doute existe plutôt que d'annoncer à tort ; désactivable avec la feature maj
diagnostiquer --passage <id> [--json] [--csv <serie\|anomalies>] P6, #1672 ServiceDiagnostic.diagnostiquer (lecture seule) : bilan matériel d'une nuit (climat R20, T° début nuit, cohérence horaire #548, GPS du point, anomalies/évènements R19). Parité CLI de M-Diagnostic. --csv exporte série ou anomalies via ExportDiagnostic (P6-CA6). La cohérence horaire dit le protocole (#4984) : la fenêtre exigée va de 30 minutes avant le coucher à 30 minutes après le lever, c'est un plancher, et la dépasser n'est pas un défaut. Le texte cite les deux plages - protocole 21:28 à 06:23, enregistré 22:00 à 05:00 - et le JSON porte debutExige, finExigee, debutEnregistre, finEnregistree plus couvertureDuProtocole. Cette dernière vaut COUVERTE, INCOMPLETE ou INDISPONIBLE : elle nomme l'état du domaine, pas la gravité de son annonce, laquelle est une décision de surface (ADR 0038). Un script qui voudrait alerter teste INCOMPLETE, et jamais l'absence de COUVERTE, INDISPONIBLE ne signifiant pas « mauvais » mais « on n'a pas pu savoir »
importer --source <chemin> --point <id> [--annee N] [--passage N] [--conserver-originaux \| --sans-originaux] [--ecraser] P2 ServiceImport. Le mode de conservation suit le réglage import.conserver-originaux (par défaut : pas de copie) ; les deux options le forcent pour un import ponctuel et s'excluent. Auparavant la commande conservait en dur, quel que soit le réglage (#2064). La sortie rapporte aussi le doublon de nuit et les anomalies du journal : l'écran les montre depuis #2044, la commande les taisait (#2004). Collision de numéro (#2278) : la perte est chiffrée (séquences, validations Tadarida) et, sans --ecraser, rien n'est importé, sortie 2. Avec, ServiceImport.ecraserEtImporter sauvegarde d'abord, puis remplace la nuit existante. Parité #2350 : la sortie dit aussi les volumes lus / écrits (#2358, les mêmes chiffres que la bande de l'écran, sans les barres) et la participation Vigie-Chiro créée (#1488), une écriture distante ne se découvre pas ailleurs. Parité #3195 : --source accepte aussi une archive .zip, décompressée sous le dossier de travail (jamais sous /tmp, souvent un tmpfs en RAM) puis effacée, refus compris ; les bornes de ressources de #2732 valent donc aussi pour la ligne de commande, et un refus y sort en 2
importer-transformes --dossier <dir> --point <id> [--annee N] [--passage N] [--referencer] #2433 (EPIC #2258) ServiceImportReference. Crée un passage à partir d'un dossier de séquences déjà transformées, sans rejouer la transformation : l'original est un placeholder (pas de brut), l'identité (empreinte + taille) est calculée à l'inscription pour que la réactivation la revérifie au réveil. Par défaut copie les WAV dans l'espace de travail ; --referencer les laisse en place, la base pointe l'emplacement externe et aucun octet audio n'est recopié (ADR 2433). Série et date déduites des noms de fichiers. Refus métier (dossier introuvable, aucun WAV, point inconnu, quadruplet déjà pris) en sortie 2, rien n'est créé, l'état est intact
importer-tadarida --passage <id> --csv <fichier> [--remplacer] P6 ServiceValidation.importer / reimporter
qualifier --passage <id> --verdict <ok\|utilisable\|inexploitable> [--commentaire ..] R13 ServicePassage.poserVerdict (alias douteux/a-jeter rétro-compatibles)
qualifier-fichier --passage <id> --sequence <id> --verdict <bon\|mauvais\|inexploitable> #1512 Verdict par fichier d'une séquence de la sélection d'écoute ; recalcule le verdict final proposé. Parité CLI de M-Qualification
lister-selection --passage <id> [--json] #1512 Sélection d'écoute d'un passage : verdict par fichier de chaque séquence + verdict final proposé (dérivé), et les séquences illisibles que la jointure a dû écarter (sequencesIllisibles en JSON, #4739). Parité CLI de M-Qualification, lecture seule
pre-check --passage <id> [--json] #1512 Pré-check consultatif d'une nuit (3 feux : couverture horaire, nombre de fichiers, renommage) + résumé des anomalies. Parité CLI de M-Qualification, lecture seule, jamais bloquant (R13)
constituer-selection --passage <id> [--methode <reparti\|aleatoire\|manuel>] [--taille <n>] R12 (Re)constitue la sélection d'écoute (échantillon à écouter). Remplace la sélection existante et efface ses verdicts. Parité CLI de « Personnaliser… » / « Régénérer »
emporter-nuit --passage <id> --vers <fichier> [--oui] #4729 Écrit le paquet d'une nuit pour la faire relire ailleurs. Annonce le volume et s'arrête sans --oui : on n'emporte pas des gigaoctets sur un script lancé de travers. Parité CLI de « Emporter cette nuit… »
ouvrir-paquet-recu --fichier <paquet> #4729 Le relecteur ouvre un paquet confié : la sélection de l'expéditeur devient la sienne, figée. L'identité s'appose à l'ouverture (#4626), et son absence est un refus
renvoyer-avis --passage <id> --vers <fichier> #4729 Le relecteur renvoie son jugement : un manifeste signé de lui, sans séquence, l'expéditeur les ayant déjà
reprendre-avis --fichier <paquet> [--remplacer] #4729 Range l'avis revenu à côté du nôtre, jamais dessus (ADR 4517). Un second avis exige --remplacer : en ligne de commande, aucune modale ne rattrape un écrasement
exporter-lot --passage <id> P4 ServiceLot
deposer --passage <id> P8 ServiceLot.preparerLot + marquerDepose (marquage manuel)
recuperer-vigiechiro [--token <jeton>] #1181, #1866 rejoue les RapprochementVigieChiro (taxons, sites/points) après un GET /moi de contrôle ; 0 ssi connecté. Le geste ne fait que recevoir, d'où le verbe (ADR 0022) ; alias synchroniser-vigiechiro, conservé pour les scripts
deposer-vigiechiro --passage <id> [--token <jeton>] [--archives\|--wav] #1043 DepotVigieChiro.deposer (moteur reprenable #982, téléversement parallèle #984). Défaut = ChoixSourceDepot.pour, le même choix que M-Lot : ZIP si présentes, sinon invite à les générer (étape 2), sinon repli WAV. --archives/--wav forcent l'un ou l'autre. Parité #2350 : depuis le réessai gradué (#2354) une coupure momentanée n'échoue plus, elle attend ; la ligne ~ <unité>, nouvelle tentative dans N s le dit, là où le silence faisait passer la temporisation pour un blocage
lancer-traitement-vigiechiro --passage <id> [--token <jeton>] [--forcer] #984, #1261, #1265 DepotVigieChiro.lancerTraitement (POST /participations/{id}/compute), équivalent du bouton « Lancer la participation ». 0 dès lors que le traitement est en route (accepté ou déjà en cours : la commande est idempotente), 1 si le serveur a refusé la relance, 2 pour un refus métier en amont (dépôt indisponible, aucune participation liée au passage, rendu par le handler central, #2294). Une nuit déjà analysée n'est pas relancée : le serveur détruirait ses observations pour les recalculer, sans pouvoir les régénérer (audio absent d'un dépôt en archives, #1244), --forcer lève cette garde, typiquement après un échec
etat-traitement-vigiechiro --passage <id> [--token <jeton>] #1265 SuiviTraitement.relever (lecture seule : GET /participations/{id} → bloc traitement, et mise à jour du cache local #1262). Codes faits pour un script : 0 terminé, 3 planifié/en cours/nouvel essai, 1 en échec côté serveur, 4 jamais lancé, 2 indisponible (on n'a pas pu demander : nuit non déposée, jeton absent, plateforme injoignable, refus métier rendu par le handler central, #2294)
traiter-passages --action <preparer-depot\|televerser\|importer-resultats\|declencher-calcul> --passage <id> [--passage <id>…] [--json] #2357 MoteurTraitementGroupe sur les quatre ActionGroupee, équivalent du menu « Traiter la sélection » de « Carte & passages ». Ce qu'une boucle shell ne sait pas faire : savoir à l'avance lesquels sont éligibles (les règles lisent l'état du dépôt, le rattachement et la présence de résultats, rien de tout cela n'étant dans lister-passages). Les écartés sont annoncés avec leur motif sans être tentés, un échec n'arrête pas les suivants, chaque passage a sa ligne. Ni --forcer ni --remplacer : les actions groupées ne les exposent pas, un recalcul détruisant les observations d'un dépôt en archives et un remplacement touchant à ce que l'observateur a validé, vingt d'un coup ne serait pas un service. 0 si aucun échec (les écartés en font partie : rejouer un lot déjà traité est le cas idempotent, comme lancer-traitement-vigiechiro sur « déjà en cours »), 1 si au moins un passage a échoué, 2 pour un refus en amont (fonctionnalité désactivée, identifiant inconnu)
reinitialiser-depot --passage <id> #984 ServiceLot.reinitialiserDepot (efface le plan depot_unite, retour « Prêt à déposer » ; local, archives ZIP et lien de participation conservés) : équivalent du bouton « Réinitialiser le dépôt »
supprimer-passage --passage <id> [--confirmer] #2278 ServicePassage.supprimer, équivalent du bouton « Supprimer » de la fiche passage. Destructif : chiffre d'abord la perte (séquences, validations Tadarida menacées) puis, sans --confirmer, ne touche à rien et sort en 2. Un passage déposé ou introuvable est refusé par le métier (code 2 : état intact). La cascade s'arrête à la base : les fichiers de la nuit restent sur le disque
verifier-depot-vigiechiro --passage <id> [--token <jeton>] #1132 VerificationDepot.verifier (lecture seule : journal de traitement + titres des donnees vs plan depot_unite ; 0 ssi tout est retrouvé)
importer-vigiechiro --passage <id> [--remplacer] [--participation <objectid>] [--token <jeton>] #1181, #1838 ImportVigieChiro.importerRapide (résultats Tadarida depuis l'API ; --participation = rattachement préalable). Au premier import, prend le CSV d'observations d'un coup (#1565) quand la plateforme l'expose, avec repli sur la pagination donnees. Avec --remplacer, reste sur les donnees : un ré-import va chercher ce qui a changé côté serveur (avis du validateur, fils), que le CSV effacerait. Le CSV ne porte ni ancrage ni fils de discussion : la publication les acquiert ensemble quand elle en a besoin (ADR 0019)
publier-corrections-vigiechiro --passage <id> [--token <jeton>] #723, #1838 PublicationCorrections.publier (un PATCH par observation publiable : taxon + certitude + ancrage ; idempotente ; code 1 si au moins un envoi est refusé par le serveur (échec partiel de publication), code 2 si la publication est indisponible dans ce contexte, fonctionnalité désactivée (refus métier, état intact)). Acquiert d'abord l'ancrage qui manque (#1838) quand la nuit est rattachée à une participation : une nuit importée par CSV (#1565) n'en porte pas, ses corrections seraient sinon toutes écartées. Le rapatriement peut durer ; son avancement va sur la sortie d'erreur, la sortie standard restant réservée au bilan. Une nuit déjà ancrée n'en paie pas le coût
reactiver --passage <id> --source <dir> [--referencer] [--json] #1302, #1406, #1571, #2255 ServiceReactivationPassage.reactiver : rebranche les fichiers retrouvés, fichier par fichier, via la cascade de preuves. Le dossier est reconnu, pas déclaré : s'il ne contient que les bruts, les séquences sont régénérées (transformation déterministe) puis vérifiées comme n'importe quel candidat, la voie empruntée est dite (champ voie en JSON). Non destructive et idempotente. 0 si l'audio redevient complet, 1 s'il reste partiel (les écarts sont énumérés). Sur un passage reconstruit (observations sans ancrage), acquiert en plus l'ancrage plateforme par ré-import des donnees (#1571) pour rendre les corrections publiables, le seul cas où reactiver touche le réseau. Avec --referencer (#2255), rien n'est copié : la base pointe les fichiers là où ils vivent (NAS, disque externe, dossier de travail). La nuit devient muette si ce support n'est plus joignable et redevient écoutable quand il revient, l'identité étant revérifiée (#2254). Les tranches régénérées depuis les bruts restent copiées : elles sont produites par l'application, pas par l'utilisateur
reconstruire-passage [--participation <objectid>] [--json] #1305, #1565 ServiceReconstructionPassages : sans argument, liste les participations VigieChiro sans équivalent local (nuits déposées depuis un autre poste, ou avant l'application) ; avec --participation, en reconstruit une en passage archivé (séquences recréées + observations rapatriées). La reconstruction bascule sur le CSV d'observations téléchargé d'un coup (#1565, quasi instantanée), avec repli sur la pagination donnees si le CSV n'est pas exposé. Les lacunes sont imprimées avec le rapport
metadonnees-passage (--passage <id> \| --tout) [--recuperer] [--envoyer] [--enregistreur <serie>] [--heure-debut <HH:mm> --heure-fin <HH:mm>] [--confirmer] [--json] #1861 Parité CLI de la modale « Modifier le passage » : --recuperer rapatrie météo/micro/n° de série depuis la participation, --envoyer réécrit les métadonnées locales dessus (les heures y sont réalignées sur les enregistrements, #1878), --enregistreur et les heures écrivent en local sans réseau. --tout est le rattrapage de saison : sans lui, les correctifs #1814/#1828/#1844 ne réparent que la nuit sur laquelle on repasse. Comme il écrit sur la plateforme, il exige --confirmer ; sans, il énumère ce qu'il ferait. Best-effort par nuit, compte rendu nuit par nuit, code 1 s'il reste des nuits ignorées
retro-empreintes (aucune option) #1299 BackfillEmpreintes : pose les empreintes manquantes sur toutes les nuits importées avant V23. Rejouable sans risque (ne touche que ce qui manque)
rattraper-communes (aucune option) #2791 ServiceCommunes : comble la commune des points en attente (GPS présent, commune absente) via l'API Géo. Rejouable sans risque ; hors ligne, les points restent en attente
exporter-vu --passage <id> --sortie <fichier> P7 ServiceValidation
exporter-observations --passage <id> --sortie <fichier> #149 ProjectionsAudioDao.lignesAudioDuPassage + ExportObservationsCsv
exporter-sons (--passage <id> \| --espece <code>) --sortie <zip> [--lieu <lieu>]… [--proba-min <0..1>] #2795 ProjectionsAudioDao.lignesAudioDuPassage / lignesAudioDeLEspece + ExportObservationsEtSons (#2792). Parité CLI du geste « Exporter les observations et les sons » de la vue audio (#2793) : archive observations.csv + sons/<session>/<fichier>, sons introuvables comptés sans bloquer. --passage couvre le même sous-ensemble qu'exporter-observations (CSV identique) et refuse un passage inconnu (code 2) ; --espece couvre l'espèce sur tous les passages de l'utilisateur, tous statuts, et une espèce sans observation produit une archive au CSV d'en-têtes seuls
exporter-activite (--passage <id> \| --tout) --sortie <fichier> [--tranche 15\|30\|60] [--format csv] #2352 ServiceActivite.contactsDuPassage + AgregationActivite.parEspece + ExportActiviteCsv. Facette données de la courbe d'activité (pendant CLI de l'export image de l'IHM), rattachée à la nuit biologique. Une ligne par (carré, point, nuit, espèce, tranche) : chaque ligne porte son lieu, sans quoi un export couvrant plusieurs nuits ne se recouperait pas (#2613). --tout couvre la vue transverse de l'écran. Non gouvernée par la fonctionnalité activite-nuit : l'agrégation est une capacité stable de analyse, celle-ci ne gouvernant que l'accès à la vue
audit-coherence [--passage <id>] [--gravite <niveau>] [--categorie <nature>] [--contient <texte>] [--json] [--online] [--token <jeton>] #1133, #1254, #1347 ServiceAuditCoherence : confronte disque, base et serveur. --gravite et --categorie sont les deux puces de l'écran « Audit de cohérence » (#3100), portées ici à la clôture de #3092 ; chacune retient une valeur exacte. Elles filtrent l'affichage seul : le code de sortie continue de juger le rapport entier, sans quoi --gravite INFO sur un workspace abîmé rendrait 0 et un script d'intégration conclurait que tout va bien. Sans --passage, audite tout le workspace ; avec, une seule nuit (utile après l'avoir réparée). --online ajoute les constats qui demandent le réseau (dépôts, points). 0 ssi aucun constat d'erreur
sauvegarder [--complet] [--dossier <dir>] #148, #1346 ServiceSauvegarde : instantané cohérent de la base (VACUUM INTO). --complet emporte aussi l'audio (dossiers de session), c'est la seule sauvegarde qui protège vraiment, la plateforme ne rendant pas l'audio d'un dépôt en archives. Le bilan dit ce qui n'a pas pu être copié (carte SD non montée) et sort en 2 : une sauvegarde qu'on croit complète et qui ne l'est pas vaut moins que pas de sauvegarde
restaurer <chemin> [--complet] --confirmer #148, #1346 ServiceSauvegarde.restaurer / restaurerComplet : remet la base (et, avec --complet, les dossiers de son, remis là où ils étaient, avec les root_path corrigés et un compte rendu de ce qui a changé de place, #2727). Écrase l'état local : --confirmer est obligatoire. La base courante est mise de côté (vigiechiro.db.avant-restauration). Trois codes : 0 restauration entière, 10 restaurée mais il manque quelque chose, 2 refus (#3500)
lister-sauvegardes [--dossier <d>] [--json] #3197 InventaireSauvegardes.lire. Ce que sauvegardes/ contient : nom, date, taille, et le total. L'application y écrit un filet complet avant chaque migration de schéma et n'en supprime jamais aucun - délibérément (ADR 0048 : le filet appartient à l'utilisateur), mais jusqu'ici sans que rien ne dise combien il y en avait ni ce qu'ils pesaient. La commande observe : elle ne purge rien et ne conseille rien. Les sauvegardes complètes étant des dossiers, leur taille est celle de leur contenu - un inventaire qui ne verrait que les fichiers mentirait là où le chiffre compte
supprimer-sauvegarde --nom <nom> [--dossier <d>] --confirmer #3197 Le ménage que la doc conseille, explicitement demandé. Même parade que supprimer-passage : sans --confirmer, la commande chiffre la perte et ne touche à rien, sortie 2. Un nom inconnu est une erreur d'usage, pas un succès silencieux : croire avoir supprimé ce qu'on n'a pas touché est précisément ce qu'on veut éviter
reset-guide [--json] [--executer --confirmer [--accepter-perte] [--sauvegarde <dir>]] #1151, #1419 Sans --executer : lecture seule, ce que deviendrait l'audio de chaque nuit si l'on repartait d'une base neuve (disque / serveur / perdu), code 2 dès qu'une nuit est en « perdu », pour qu'un script puisse refuser d'enchaîner. Avec --executer : ServiceReset mène la procédure (sauvegarde complète → base neuve → repeuplement depuis VigieChiro → audit final). Il refuse de démarrer si la perte n'est pas acceptée, ou si VigieChiro ne répond pas, une base neuve qu'on ne peut pas repeupler est une destruction sèche. Dans les deux cas, la base reste intacte
synthetiser-passage --passage <id> [--carre ..] [--milieu ..] [--validees-seulement] [--format csv\|json] [--sortie <f>] #2351 Facette CLI de M-Synthese. Contacts par espèce et classe d'activité au regard du référentiel ACTICHIRO, avec les quantiles retenus. Même ServiceSynthese que l'écran : mêmes règles de repli, même résultat, une synthèse qui différerait d'une surface à l'autre serait pire qu'absente. Le contexte, l'avertissement et la citation sont écrits en tête du CSV (lignes #) et dans un objet contexte en JSON : la source est libre d'usage avec citation obligatoire, et un avertissement resté à l'écran ne prévient personne qui ouvre le fichier
lister-observations --passage <id> [--statut ..] [--taxon ..] [--douteux] [--reference] [--a-enjeu] [--certitude ..] [--json] [--lieu <lieu>]… [--proba-min <0..1>] #1311 La surface de découverte de la revue. Liste les observations d'un passage avec leur identifiant, l'avis de Tadarida, le vôtre, celui du validateur, le statut et les drapeaux. Sans elle, les gestes de revue (et discussion) sont aveugles : rien ne donnait les identifiants. Ses filtres sont exactement ceux des gestes (SelectionObservations partagé) : ce qu'elle montre est ce qu'ils toucheraient. --a-enjeu (#2353) ne garde que les espèces prioritaires du Plan National d'Actions Chiroptères, et le drapeau enjeu les signale dans la sortie (champ aEnjeu en JSON)
lister-especes [--statut <s>] [--taxon-parent <t>] [--lieu <texte>…] [--nature protocole\|opportuniste] [--a-enjeu] [--format csv\|json] [--sortie <f>] #3269 Facette CLI de l'inventaire par espèce d'« Espèces & observations ». Même AgregationAnalyse.parEspece que l'écran et mêmes colonnes que son export : le tableau ne peut pas dire une chose ici et une autre là. Les cinq critères sont ceux de la barre à puces, mais posés en fragments tapés (partiels, insensibles à la casse et aux accents) là où l'écran fait cocher dans une liste : c'est l'écart assumé de FiltresAnalyse avec le catalogue de la vue. Le point n'est pas filtrable (un code seul désigne autant de lieux qu'il y a de carrés). Une base sans aucune observation se constate avant tout filtrage : sinon --lieu refuserait le lieu (ADR 3082) là où la vérité est qu'il n'y a rien
lister-carres [mêmes filtres que lister-especes] [--format csv\|json] [--sortie <f>] #3269 Facette CLI de l'inventaire par carré du même écran : la richesse en espèces et les détections, par carré prospecté. Répond à « où » quand sa jumelle répond à « quoi » - deux commandes plutôt qu'une à --regrouper, parce que leurs colonnes n'ont rien en commun et qu'un script ne doit pas avoir à deviner celles qu'il recevra. La richesse suit la sélection, comme à l'écran : --a-enjeu donne la richesse en espèces prioritaires, --statut VALIDEE celle qu'on peut défendre
valider-observations (--observation <ids> \| --passage <id> [filtres] [--confirmer]) R15, #1311 Accepte la proposition de Tadarida, en lot atomique (mode Activité : traite exactement les lignes visées ; le mode Inventaire, qui propage à d'autres lignes, reste à l'écran - propager dans un script toucherait des lignes que l'utilisateur ne voit pas)
corriger-observations --taxon <code> (--observation <ids> \| --passage <id> [filtres] [--confirmer]) R16, #1311 Retient un autre taxon, en lot atomique. Le taxon doit exister au référentiel : un code inconnu arrête tout avant la moindre écriture
marquer-douteux [--retirer] (--observation <ids> \| --passage <id> [filtres] [--confirmer]) #160, #1311 Lève (ou baisse) le drapeau « douteuse ». Ce drapeau ne dit rien du taxon : il dit « je ne sais pas », une troisième réponse qui n'est ni valider ni corriger. Réversible
marquer-reference [--retirer] (--observation <ids> \| --passage <id> [filtres] [--confirmer]) P10, #1311 Verse (ou retire) les observations dans la bibliothèque de sons de référence - la source References de l'écran, et la matière de son export
poser-certitude (--certitude <SUR\|PROBABLE\|POSSIBLE> \| --effacer) (--observation <ids> \| --passage <id> [filtres] [--confirmer]) #1139, #1311 Déclare la certitude observateur. Il faut choisir explicitement : elle ne se déduit ni de la probabilité Tadarida ni d'une validation, et reste vide par défaut. C'est un jugement, que la plateforme exigera avec le taxon (#723) et qu'un naturaliste lira comme la parole de l'observateur
discussion --observation <id> [--message <texte> --confirmer] #1417, #1418 Le fil d'échange avec le validateur du MNHN. Sans --message, le lit (le fil vient de la base, rafraîchi à chaque import). Avec, y répond, écriture définitive : le serveur ajoute par $push et n'offre aucune route de suppression. --confirmer est donc obligatoire, et le message n'est écrit localement qu'après que le serveur l'a accepté
emplacements [--definir-travail <dir>] [--definir-base <dir>] [--reinitialiser] [--json] #1038 Parité CLI de l'onglet « Emplacements » (ADR 1038) : ServiceEmplacements. Sans option, affiche où vivent le dossier de travail, la base et les journaux (et leurs défauts). Les journaux y figurent parce que la CLI n'imprime plus la pile d'un incident : le détail n'existe qu'une fois, dans ce dossier, et l'IHM est seule à savoir l'ouvrir (#3624). --definir-* sonde chaque dossier (un fichier ou un dossier non inscriptible est refusé, code 2 : rien n'est écrit) puis écrit le choix ; --reinitialiser l'efface. Ne déplace rien : change le pointeur lu au prochain démarrage, pas les données - une base pointée vers un dossier vide démarre neuve. --reinitialiser et --definir-* sont exclusifs (code 2)
--help / -h, --version / -V, ou aucun argument

La couleur est choisie, pas déduite (#3738)

Cli pose explicitement le mode ANSI à la construction du CommandLine. Sans cela, picocli applique son heuristique AUTO, qui ne décide pas la même chose partout : la première exécution de la suite sous Windows a rendu une aide colorisée là où Linux la rend nue - Usage: ESC[1mvigiechiro….

La règle : de la couleur pour un humain devant un terminal (System.console() != null), jamais dans un tuyau, un fichier ou un journal de CI ; et l'utilisateur a le dernier mot dans les deux sens :

Variable Effet Convention
NO_COLOR éteint, toujours spécifiée
FORCE_COLOR allume, même sans console usage répandu, non spécifié

Les deux comptent dès qu'elles sont présentes et non vides, quelle que soit leur valeur - une seule lecture à retenir pour deux variables voisines. NO_COLOR l'emporte quand les deux sont posées : un refus explicite prime sur une demande explicite. Se tromper dans ce sens affiche du texte nu ; se tromper dans l'autre crache des séquences d'échappement chez quelqu'un qui a demandé qu'on ne le fasse pas.

FORCE_COLOR a été ajoutée parce que la règle précédente ne donnait le dernier mot que dans un sens (#3796). Trois situations ordinaires en souffraient, et elles ont un point commun : la sortie est redirigée alors qu'un humain la lit - … | less -R, un journal de CI qui interprète l'ANSI (les Actions GitHub le font), un enrobage script/unbuffer. Dans les trois, l'utilisateur veut la couleur, la console sait l'afficher, et le produit refusait.

La décision vit dans CouleurCli, sur des entrées fournies plutôt que lues du système : la console et l'environnement d'une JVM en cours ne se manipulent pas de façon portable, et c'est précisément cette non-portabilité qui avait créé le défaut. cli.bats garde par ailleurs les quatre cas sur la sortie réelle du binaire, dont deux qui n'existent que là : la couleur s'allume devant un vrai terminal, et FORCE_COLOR l'allume même redirigée.

Il faut un vrai pseudo-terminal (script -qec) pour les éprouver. Sans lui, deux cas sur quatre sont indiscernables : sous une sortie redirigée, l'aide est nue de toute façon, donc « NO_COLOR éteint » y serait vert sans rien prouver.

Socle : registre de commandes picocli (#614)

Le CLI repose sur picocli 4.7.7 : chaque commande est une classe annotée @Command de cli.commande (ListerPassages, Importer, ExporterLot, ExporterVu) déclarant son nom, ses @Option (types convertis automatiquement) et son aide. La commande racine CommandeRacine liste les sous-commandes ; l'aide, l'usage et la liste des commandes sont générés par picocli (plus de texte d'aide maintenu à la main). Les commandes restent des façades : aucune logique propre, elles appellent les services.

  • Instanciation par Guice : FabriqueGuice (une IFactory picocli) fait construire chaque commande par l'injecteur, pour que ses services @Inject soient fournis ; picocli renseigne ensuite les champs @Option. Le module étant un open module, aucun opens ... to info.picocli n'est nécessaire.
  • Migration : Cli.executer migre la base (idempotent) avant d'exécuter une sous-commande (pas pour l'aide seule), via une IExecutionStrategy.
  • Sortie --json : convention uniforme pour les commandes de lecture (scriptabilité), sérialisée par FormatJson (écrivain JSON minimal, sans dépendance supplémentaire).
  • Erreurs : les erreurs de parsing (commande inconnue, argument requis manquant) sont reformulées en français et sortent en code 2 ; une ErreurUsage levée dans la logique (ex. point introuvable) sort aussi en 2 ; un refus métier (RegleMetierException ou l'IllegalArgumentException des validateurs) sort en 2 (état intact) ; toute autre exception (échec inattendu, état incertain) en 1 (message seul, jamais la trace).

« Jamais la trace » n'était vrai qu'à moitié jusqu'à #3570, et de deux façons. D'abord Cli.main amorce la journalisation et migre la base avant d'entrer dans la commande, donc hors du gestionnaire de picocli : une exception née là sortait par la JVM, Exception in thread "main" et pile complète, en code 1 - y compris un RefusAvantEcriture que #3498 avait pourtant appris à traduire en 2. Ensuite, la JVM installe d'office un ConsoleHandler que personne ne retirait : tout incident reversait donc sa pile sur la sortie d'erreur, en plus de la phrase.

Les deux sont corrigés : le classement exception vers code vit dans VerdictCli, appelable des deux côtés, et la CLI retire la console au démarrage (ConfigurationJournalisation.configurerSansConsole). L'IHM garde la sienne : le trajet des journaux est un choix de surface, et personne ne script la sortie d'une fenêtre. La trace n'est pas perdue, elle est dans <workspace>/logs/.

Workspace surchargeable

Comme l'IHM, la CLI travaille dans un workspace (qui contient la base vigiechiro.db). L'option globale --workspace <dir> est consommée par main() avant de bâtir l'injecteur (elle positionne la propriété système vigiechiro.workspace, lue par CommunModule). Sans elle, le workspace par défaut est <Documents>/VigieChiro-Companion.

Relever une borne : --reglage <cle>=<valeur> (#4075)

Sept bornes protègent la lecture d'une entrée externe : taille d'un fichier de la carte, corps d'une réponse, contenu d'une archive, hôtes admis pour un lien signé. Elles se surchargeaient par propriété JVM, et trois refus le conseillaient : « relancez avec -Dvigiechiro.… ».

Ce conseil était inapplicable pour qui installe le produit. Le lanceur jpackage passe ses arguments à main, jamais à la machine virtuelle : -D n'y existe pas. La seule issue chez l'utilisateur était de renoncer au fichier, ce que le doc-comment de PlafondLecture voulait précisément éviter.

L'option est répétable, consommée par main() avant l'injecteur - même trajet que --workspace, et pour la même raison : les bornes sont lues à la construction des services.

vigiechiro --reglage import.zip.max-entrees=5000 importer --point 12 --source /media/moi/CARTE

Les clés vivent dans le registre CleDeReglage, et rien d'autre ne les liste : une clé inconnue nomme celles qui existent, ce qui met la liste là où on en a besoin. Le registre est aussi ce qui rend l'option sûre - elle écrit une propriété système, donc sans lui elle en écrirait n'importe laquelle, y compris celles de la plateforme.

Ce que cette option ne fait pas

Elle ne rend pas ces bornes réglables : il n'y a toujours pas d'entrée dans l'écran Réglages, et cette décision tient - un naturaliste n'a pas à choisir une taille de corps de réponse. Ce qui change est qu'une limite atteinte nomme une porte de sortie qui existe là où l'utilisateur se trouve. Le message est le même à l'écran et au terminal : la ligne de commande est le seul endroit où ces bornes se relèvent, et inventer une consigne propre à l'écran promettrait un geste absent.

Le dossier de travail est réservé pendant l'écriture (#3498)

L'application graphique réserve le dossier de travail pour toute sa durée (VerrouWorkspace,

2731) ; la CLI ne le demandait jamais et écrivait donc par-dessus, alors que la doc du verrou nomme

elle-même le cas : « deux instances graphiques, une IHM et une CLI, ou une restauration pendant un import ». Depuis #3498, Cli.migrerPuisExecuter prend le verrou par défaut, pour toute la durée de la commande. Un dossier déjà occupé donne un refus : code 2, état intact.

Une commande s'en dispense en portant l'interface marqueur fr.univ_amu.iut.cli.LectureSeule. La déclaration porte sur les lectrices, pas sur les écrivaines, et le sens compte : oublier de déclarer une écrivaine laisserait son écriture échapper au verrou, en silence ; oublier une lectrice fait refuser une consultation pendant que l'application est ouverte, ce qui se voit et se signale le jour même. « Lecture seule » veut dire : ne touche ni la base ni les dossiers de session. Interroger le réseau, ou écrire hors du dossier de travail (lister-carres --sortie, synthetiser-passage --sortie), reste de la lecture seule - tout comme écrire la configuration d'amorçage, qui vit ailleurs et s'écrit d'un seul coup (#3507). C'est pourquoi emplacements est lectrice : elle sert à repointer le dossier de travail, et la verrouiller refuserait de déménager à qui déménage parce que la place actuelle est occupée.

Le journal fait exception. Cli.main amorce la journalisation avant tout, donc toute commande crée et écrit <dossier de travail>/logs - les lectrices comprises. C'est voulu : un incident doit laisser une trace même sur une commande qui ne fait que lire, et deux processus qui écrivent chacun ses lignes dans un journal ne se corrompent pas, là où deux processus qui écrivent la même base le feraient (#3575).

C'est aussi pourquoi `emplacements` **nomme** ce dossier : la trace retirée de la console doit
rester trouvable sans le deviner. `cli.bats` le vérifie des deux bouts - la pile est dans le
fichier, et la commande désigne le dossier réellement écrit.

La migration du schéma prend le verrou de son côté : une commande de lecture sur une base à mettre à jour peut donc être refusée, et c'est voulu - mettre à jour le schéma est une écriture.

ClassementLectureEcritureTest exige que chaque commande soit classée : elle porte le marqueur, ou elle figure dans la liste des écrivaines, tenue dans ce test. Le garde ne juge pas le classement - aucune analyse statique n'y arrive - il rend l'oubli impossible.

Codes de sortie

Code Signification
0 succès
1 échec d'exécution : accès aux données, E/S, incident inattendu (état incertain)
2 mauvaise invocation (commande inconnue, argument requis manquant ou mal formé) ou refus (règle métier, ou garde destructive : état intact, rien n'a été fait)
10 succès partiel, sur les seules commandes qui le documentent (verifier-maj, restaurer) : l'opération a abouti, mais quelque chose reste à regarder

2 dit aussi « j'ai refusé, je n'ai rien fait ». Les commandes destructives exigent un drapeau explicite (--confirmer, --ecraser) : sans lui, elles chiffrent la perte et sortent en 2 sans rien toucher. C'est volontairement distinct de 1 : après un 1, l'état est incertain ; après un 2, il est intact, et un script peut s'arrêter sans avoir à vérifier quoi que ce soit. Le message de refus part sur stderr, pour ne pas se mêler au compte rendu.

Suivent cette règle : supprimer-passage, importer --ecraser, restaurer, reset-guide --executer, discussion, sauvegarder (incomplète), emplacements (dossier refusé par la sonde, options exclusives : rien n'est écrit). restaurer rendait 1 sur stdout jusqu'à #2294, la convention n'était écrite nulle part, et c'est ainsi qu'elle a dérivé.

Les refus de la couche persistance en font partie depuis #3146 : un fichier qui n'est pas une base, une sauvegarde écrite par une version plus récente, un manifeste abîmé, un dossier de travail occupé. Tous sont émis avant la moindre écriture, et sortaient pourtant en 1, avec une pile. Ils portent maintenant RefusAvantEcriture, que le gestionnaire d'erreurs classe avec les autres refus. Une DataAccessException ordinaire, elle, reste un incident : sa pile est l'information utile.

Un troisième code quand « réussi » ne suffit pas (#3500). restaurer --complet imprimait qu'une nuit manquait, puis sortait en 0 : le fait était dit dans un texte qu'un script ne lit pas. Elle rend maintenant 10 quand la restauration laisse un manque - la sauvegarde ne dit pas d'où venaient les dossiers, ou une nuit connue de la base n'y était pas.

Pas 1 ni 2 : la restauration a réussi. Confondre « ça n'a pas marché » avec « ça a marché, regarde quand même » est exactement ce que verifier-maj a payé avant elle.

Et pas sur les dossiers simplement replacés ailleurs, qui restent 0 : c'est le cas normal d'une restauration sur une autre machine, l'usage principal de --complet, et le compte rendu nomme déjà l'ancienne et la nouvelle adresse. Un 10 permanent s'apprend à s'ignorer, et emporterait avec lui celui qui compte. L'IHM, elle, garde son avertissement sur les trois cas : elle parle à quelqu'un qui lit le compte rendu (ADR 3500).

deposer-vigiechiro marque d'un ~ ce que la réconciliation n'a pas pu vérifier (#4631) : aucune unité n'a échoué, c'est l'étape d'avant qui n'a pas tourné, et des archives déjà en ligne vont repartir. Un script distingue donc ! (une unité a échoué) de ~ (on n'a pas pu savoir).

deposer-vigiechiro étend la convention : 0 seulement si le dépôt est complet ; 1 si des fichiers restent à reprendre (relancer la même commande ne re-téléverse que les manquants). Le jeton vient de --token, sinon de la variable d'environnement VIGIECHIRO_TOKEN, sinon de la connexion enregistrée dans l'application (préférer la variable d'environnement : --token laisse le jeton dans l'historique du shell).

Ces codes rendent le cycle de dépôt complet scriptable (#984). Le dépôt ne déclenche pas le traitement serveur : il faut l'appeler explicitement.

export VIGIECHIRO_TOKEN=vigiechiro deposer-vigiechiro --passage 9 \
  && vigiechiro lancer-traitement-vigiechiro --passage 9 \
  && vigiechiro verifier-depot-vigiechiro --passage 9   # après le calcul serveur

Le calcul serveur dure des dizaines de minutes (Tadarida tourne sur une ferme de calcul distante). L'application ne surveille jamais la plateforme d'elle-même : le site officiel ne le fait pas davantage : mais un script, lui, peut l'interroger à son rythme. C'est le rôle des codes de retour d'etat-traitement-vigiechiro, le 3 signifiant « patiente » :

# attendre la fin du calcul, puis importer les observations
until vigiechiro etat-traitement-vigiechiro --passage 9; [ $? -ne 3 ]; do sleep 300; done
vigiechiro importer-vigiechiro --passage 9

En cas de dépôt à refaire de zéro (unités marquées déposées à tort), reinitialiser-depot --passage 9 efface le plan local et ramène le passage à « Prêt à déposer » : les archives et la participation sont conservées, le dépôt suivant re-téléverse tout.

executer(...) ne fait pas System.exit (il renvoie le code) : c'est ce qui le rend testable. Seul main() traduit le code en System.exit. La base est migrée au démarrage (idempotent) avant toute commande, donc une première invocation crée le schéma si besoin.

Désigner les observations d'un geste de revue (#1311)

C'était la vraie difficulté de la parité : l'écran raisonne par sélection dans une table, la ligne de commande n'a pas de table. Les gestes de revue offrent donc deux manières de désigner, exclusives et obligatoires (on ne pose pas un geste sans dire sur quoi), portées par la classe partagée CiblesRevue :

# 1. Chirurgical : sur des lignes qu'on a LUES.
./vigiechiro lister-observations --passage 3 --statut NON_TOUCHEE
./vigiechiro valider-observations --observation 12,13,14

# 2. Scripté : sur un sous-ensemble DÉCRIT, avec les MÊMES filtres que lister-observations.
./vigiechiro corriger-observations --taxon Pippip --passage 3 --taxon-tadarida Pipkuh

La garantie : c'est le même SelectionObservations qui choisit pour lister et pour agir. Ce que lister-observations --passage 3 --statut NON_TOUCHEE montre est exactement ce que valider-observations --passage 3 --statut NON_TOUCHEE touche. On regarde, puis on agit - et c'est mécanique, pas promis.

--proba-min : l'échelle, et la détection sans probabilité

Garde les détections dont la probabilité Tadarida atteint le seuil, et conserve celles qui n'en ont pas : une absence de probabilité n'est pas une mauvaise probabilité, et l'écarter perdrait précisément une ligne à revoir. C'est la règle de la puce « Proba », reprise telle quelle.

L'échelle est 0 à 1, pas le pourcentage de l'écran. lister-observations imprime déjà probTadarida brut (0.74) : entrée et sortie d'un même appel parlent ainsi la même langue. --proba-min 90, réflexe du pourcentage, est refusé (code 2) avec l'unité rappelée, plutôt que borné en silence, ce qui rendrait zéro ligne sans dire pourquoi.

Un résultat vide n'est pas un refus ici, contrairement à --lieu : un seuil est un nombre, il ne peut pas désigner ce qui n'existe pas, et « aucune détection au-dessus de 0,99 » est une réponse.

Mais une réponse muette : c'est le seul filtre qui puisse légitimement tout écarter sans rien dire de ce qu'on a raté. Quand le seuil vide le lot, la commande nomme donc la meilleure probabilité présente (« la plus sûre du lot est à 0,74 : abaissez le seuil pour l'atteindre »), ce qui apprend du même coup que le lot n'était pas vide et de combien descendre.

--lieu : ce qu'il couvre, et ce qu'il ne couvre pas

Répétable, il retient les observations dont la commune ou le carré correspond, en correspondance partielle et insensible à la casse comme aux accents : --lieu aix trouve « Aix-en-Provence ». À l'écran on coche dans une liste fermée, en ligne de commande on tape à l'aveugle, sans rien pour rappeler l'orthographe.

Le nom convivial du carré n'est pas une valeur de plus : c'est l'autre étiquette du même lieu (ADR 3157). --lieu 640380 et --lieu vallon retiennent donc le même carré, et le refus le nomme d'un seul tenant, « 640380 · Vallon », comme l'écran l'affiche - pour que la valeur suggérée se recopie telle quelle. Personne n'a pour autant à taper le point médian.

Le point n'en fait pas partie, contrairement à la puce « Lieu » de l'écran. Le schéma pose UNIQUE(site_id, code) : un code seul (« A1 », « Z1 ») désigne autant de lieux qu'il y a de carrés. L'écran s'en tire en l'affichant qualifié (« 640380 · A1 »), ce qui suppose une liste sous les yeux ; --lieu A1 rouvrirait le défaut sans que rien ne le montre. Le point restera atteignable par un croisement --carre / --point.

Un lieu sans correspondance est un refus (code 2) qui nomme les lieux présents, jamais une archive vide en code 0 : un script enchaînerait sans voir la faute de frappe, et l'expert recevrait une archive creuse.

Viser un passage sans filtre exige --confirmer

--passage 3 seul, c'est toutes les observations du passage - des centaines. Un filtre oublié dans un script transforme « corrige ces trois lignes » en « corrige la nuit entière », et rien dans la commande ne distinguerait l'un de l'autre. Ce cas exige donc --confirmer.

Et une sélection qui ne retient rien lève, au lieu de répondre « 0 observation traitée » : un geste qui ne touche rien est presque toujours une erreur de filtre.

Les filtres booléens (--douteux, --reference) sont ternaires : posés, ils ne gardent que les lignes concernées ; absents, ils laissent passer les deux - ils ne veulent pas dire « seulement les non-douteuses ».

Lancer la CLI

Depuis un produit installé

Le lanceur des emballages est un point d'entrée unique : le mot ihm ouvre la fenêtre, tout le reste part à la ligne de commande (#4071). Personne n'a à taper ihm pour ouvrir l'interface : chaque emballage l'écrit pour son double-clic - jpackage --arguments ihm le dépose dans le .cfg du lanceur, le script Flatpak porte le même défaut.

La commande s'appelle vigiechiro, comme l'annonce déjà l'aide de picocli. C'est une seconde enveloppe posée par --add-launcher sur la même classe principale, et non un second point d'entrée : VigieChiroCompanion lister-passages répond exactement pareil. Elle existe pour deux raisons, dont une seule est technique - sous Windows, elle porte la console que le lanceur graphique ne peut pas avoir (ADR 4071) ; ailleurs, elle donne simplement à la commande le nom qu'on tape.

# paquet Debian installé : la commande est dans le PATH
vigiechiro lister-passages

# archive portable Linux (et app-image)
bin/vigiechiro lister-passages

# .msi et archive portable Windows
bin\vigiechiro.exe lister-passages

# Flatpak
flatpak run fr.echonuit.VigieChiroCompanion lister-passages

# AppImage
./VigieChiroCompanion-2.185.0-linux-x86_64.AppImage lister-passages

Sous Flatpak, --workspace exige que l'appelant accorde le chemin

Le bac à sable n'ouvre que l'espace de travail par défaut (~/Documents/VigieChiro-Companion). Viser ailleurs demande donc les deux, l'accès et l'option :

flatpak run --filesystem=/chemin/ws fr.echonuit.VigieChiroCompanion \
  lister-passages --workspace /chemin/ws

Sans --filesystem, la commande échoue sur un dossier qu'elle ne voit pas, et non sur un refus explicite.

Depuis les sources

On l'exécute via exec-maven-plugin (même mécanique que le banc de performance), avec le JDK 25 standard (comme la CI) :

export JAVA_HOME=~/.sdkman/candidates/java/25.0.2-open
./mvnw -q -DskipTests compile
./mvnw -q org.codehaus.mojo:exec-maven-plugin:exec \
  -Dexec.executable="$JAVA_HOME/bin/java" -Dexec.classpathScope=runtime \
  -Dexec.args="-cp %classpath fr.univ_amu.iut.cli.Cli --workspace /tmp/vigiechiro-cli lister-passages"

Cli.main(String[]) reste le point d'entrée direct, et c'est aussi celui vers lequel Launcher aiguille dans les emballages : le harnais bats peut donc viser l'un ou l'autre. En CI il vise le lanceur, par VIGIECHIRO_LANCEUR (#4071) ; sans cette variable, il retombe sur le fat-jar, ce qui reste la façon la plus rapide de le lancer en local :

./mvnw -q -DskipTests package && bats src/test/bats                       # sur le fat-jar
VIGIECHIRO_LANCEUR=target/dist/VigieChiroCompanion/bin/vigiechiro \
  bats src/test/bats                                                       # sur le lanceur livré

Une invocation sans aucun argument rend l'usage de la ligne de commande, elle n'ouvre pas la fenêtre - y compris pour java -jar vigiechiro-*-shaded.jar, qui demande désormais ihm. Déduire une demande d'interface de l'absence d'arguments serait la figure que refuse l'ADR 3828 : une condition ambiante tenant lieu de déclaration.

Tests

La CLI est couverte par CliTest (dispatch, codes de sortie, aide), CliImportTest et CliExportVuTest. Ils positionnent vigiechiro.workspace sur un @TempDir et capturent les flux sortie/erreur : aucun JavaFX, donc des tests rapides et déterministes.