diff --git a/CHANGELOG.md b/CHANGELOG.md index 54ebfa0..f4f27a0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ ### Added +- Saisie et consultation, depuis la fiche preuve, d’un historique immuable + d’appréciations humaines d’authenticité documentaire avec justification et + `OcrRun` facultatif appartenant à la preuve, sans verdict automatique. - OCR d’identité contrôlé et révisable dans la création d’une personne, l’import normal et la fiche de preuve, avec texte brut immuable, transcription corrigée persistante, `manual_override`, `manual_entry` et diff --git a/Makefile b/Makefile index 2c57cac..7dda9d2 100644 --- a/Makefile +++ b/Makefile @@ -197,6 +197,7 @@ TEST_EVIDENCE_IDENTITY_IMPORT_GTK := tests/test_evidence_identity_import_gtk TEST_WORKSPACE_IDENTITY_OCR_GTK := tests/test_workspace_identity_ocr_gtk TEST_DIALOG_GEOMETRY_GTK := tests/test_dialog_geometry_gtk TEST_PERSON_FACTUAL_RELATION_EDITOR_GTK := tests/test_person_factual_relation_editor_gtk +TEST_DOCUMENT_AUTHENTICITY_EDITOR_GTK := tests/test_document_authenticity_editor_gtk FAKE_DOCUMENT_TOOL := tests/fake_document_tool DOCUMENT_ANALYSIS_TEST_SOURCES := \ @@ -225,6 +226,16 @@ $(TEST_PERSON_FACTUAL_RELATION_EDITOR_GTK): \ src/database/error.c src/core/relation_type_normalizer.c $(CC) $(CFLAGS) $^ -o $@ $(LDFLAGS) +$(TEST_DOCUMENT_AUTHENTICITY_EDITOR_GTK): \ + tests/test_document_authenticity_editor_gtk.c \ + src/views/document_authenticity_editor.c \ + src/core/document_authenticity_service.c src/models/identity_traceability.c \ + src/models/identity_ocr.c src/dao/identity_ocr_dao.c \ + src/dao/identity_traceability_dao.c src/database/database.c \ + src/database/schema.c src/database/statement.c src/database/transaction.c \ + src/database/error.c src/core/relation_type_normalizer.c + $(CC) $(CFLAGS) $^ -o $@ $(LDFLAGS) + all: $(TARGET) $(TEST_IDENTITY_OCR): tests/test_identity_ocr.c \ @@ -244,6 +255,7 @@ $(TEST_IDENTITY_OCR_PREPROCESSOR): tests/test_identity_ocr_preprocessor.c \ $(TEST_IDENTITY_TRACEABILITY): tests/test_identity_traceability.c \ src/models/identity_traceability.c src/dao/identity_traceability_dao.c \ + src/core/document_authenticity_service.c \ src/views/person_vocabulary_adapter.c \ src/views/person_factual_relation_editor.c \ src/models/person_role_assignment.c \ @@ -1199,7 +1211,8 @@ test: \ $(TEST_EVIDENCE_IDENTITY_IMPORT_GTK) \ $(TEST_WORKSPACE_IDENTITY_OCR_GTK) \ $(TEST_DIALOG_GEOMETRY_GTK) \ - $(TEST_PERSON_FACTUAL_RELATION_EDITOR_GTK) + $(TEST_PERSON_FACTUAL_RELATION_EDITOR_GTK) \ + $(TEST_DOCUMENT_AUTHENTICITY_EDITOR_GTK) @echo "Exécution des tests..." @./$(TEST_NODE) @./$(TEST_TREE_MODEL) @@ -1296,6 +1309,7 @@ test: \ @$(TEST_WORKSPACE_IDENTITY_OCR_GTK) @$(TEST_DIALOG_GEOMETRY_GTK) @$(TEST_PERSON_FACTUAL_RELATION_EDITOR_GTK) + @$(TEST_DOCUMENT_AUTHENTICITY_EDITOR_GTK) @echo "Tous les tests sont valides." %.o: %.c @@ -1396,7 +1410,8 @@ clean: $(TEST_WORKSPACE_IDENTITY_OCR_GTK) \ $(TEST_DIALOG_GEOMETRY_GTK) \ $(TEST_PERSON_FACTUAL_RELATION_EDITOR_GTK) \ - $(FAKE_DOCUMENT_TOOL) + $(TEST_DOCUMENT_AUTHENTICITY_EDITOR_GTK) \ + $(FAKE_DOCUMENT_TOOL) diff --git a/README.md b/README.md index 3b95cc5..b2494ab 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,10 @@ Labfy Investigation est un poste de travail libre d’investigation numérique e Le projet vise à fournir un environnement local, modulaire et traçable pour organiser une enquête, préserver les preuves originales, analyser des données, corréler des entités et produire des rapports exploitables. +La fiche d’une preuve permet aussi de consigner une appréciation humaine +d’authenticité documentaire. Cet historique est immuable, justifié selon le +statut choisi et ne peut jamais être produit automatiquement par l’OCR. + > **État du projet : développement actif** > > Le logiciel n’est pas encore prêt pour un usage opérationnel en production. Les formats internes, l’interface et les mécanismes d’intégration peuvent encore évoluer. diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index edeb79d..36aa69b 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -122,7 +122,11 @@ humaine et la qualification `complete`, `partial`, `uncertain` ou `invalid` sont orthogonales au statut de révision. Les contraintes SQLite et le service de validation interdisent la projection des valeurs rejetées, en conflit, incertaines ou invalides. Cette tranche ne réalise encore aucune projection -automatique et ne fournit pas l’interface GTK complète de saisie. +automatique. La fiche preuve délègue la saisie et la consultation de +l’authenticité à `DocumentAuthenticityEditor`, qui appelle +`DocumentAuthenticityService`; le widget n’accède jamais directement à +SQLite. Le service ajoute chaque appréciation humaine dans une transaction et +chaîne son identifiant à l’appréciation courante. --- diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index cd2465a..b33757d 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -14,6 +14,12 @@ répertoire temporaire : JPEG avec APP1 EXIF, HEIC/HEIF via libheif et PDF multipage via Cairo. Elles ne doivent jamais être remplacées par un document réel. +L’authenticité documentaire se valide avec +`tests/test_identity_traceability` et +`tests/test_document_authenticity_editor_gtk`. Ce dernier doit être exécuté +sur un affichage GTK réel avec `G_DEBUG=fatal-criticals`; ses données et sa +base SQLite sont exclusivement temporaires et marquées `SPECIMEN`. + La validation ciblée de l’aperçu partagé comprend `test_evidence_preview_widget_gtk`, `test_evidence_preview`, `test_evidence_video_preview_controller` et diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 6b67099..1183f70 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -178,13 +178,15 @@ PARTIEL — VALIDATION MANUELLE DES TRANCHES LIVRÉES RÉUSSIE - fondations V18 : authenticité humaine historisée, relations factuelles typées, vocabulaires d’identification et de rôles, cycle OCR jusqu’à la valeur explicitement confirmée et contraintes anti-automatisme. +- interface GTK de saisie et de consultation de l’authenticité documentaire + depuis la fiche preuve, avec OcrRun facultatif filtré et historique immuable. Limitations restantes : 1. l’OCR groupé de plusieurs preuves dans une même opération n’est pas pris en charge ; -2. l’interface GTK complète de saisie de l’authenticité, des relations - factuelles, des états et des rôles V18 reste à construire ; +2. les interfaces GTK complètes des états d’identification et des rôles V18 + restent à construire ; 3. la provenance uniforme de tous les dérivés et la projection des seules valeurs OCR confirmées vers les attributs structurés de la personne restent partielles ; diff --git a/docs/database/DATABASE_ARCHITECTURE.md b/docs/database/DATABASE_ARCHITECTURE.md index 810f69e..fd92216 100644 --- a/docs/database/DATABASE_ARCHITECTURE.md +++ b/docs/database/DATABASE_ARCHITECTURE.md @@ -16,8 +16,13 @@ contraintes refusent toute confirmation d’un champ rejeté, en conflit, incertain ou invalide. Aucun OCR ne crée une évaluation d’authenticité, une relation factuelle, un rôle sensible ou un état `confirmed`. +Les insertions d’authenticité vérifient hors GTK qu’un `ocr_run_id` facultatif +appartient à la même preuve. `previous_assessment_id` forme une chaîne +append-only ; l’ajout et la détermination du maillon courant sont coordonnés +dans une transaction. + > **Statut :** architecture courante -> **Version du schéma :** V10 +> **Version du schéma :** V18 > **Dernière mise à jour :** 2026-07-24 > **Source de vérité détaillée :** `SCHEMA_AUDIT_CURRENT.md` diff --git a/include/core/document_authenticity_service.h b/include/core/document_authenticity_service.h new file mode 100644 index 0000000..8b7a3e6 --- /dev/null +++ b/include/core/document_authenticity_service.h @@ -0,0 +1,16 @@ +#ifndef LABFY_DOCUMENT_AUTHENTICITY_SERVICE_H +#define LABFY_DOCUMENT_AUTHENTICITY_SERVICE_H +#include "database/database.h" +#include "models/identity_traceability.h" +#include +G_BEGIN_DECLS +typedef struct { const char *code,*label,*description; gboolean requires_justification; } DocumentAuthenticityStatus; +const DocumentAuthenticityStatus *document_authenticity_service_statuses(gsize *count); +const DocumentAuthenticityStatus *document_authenticity_service_status(const char *code); +GPtrArray *document_authenticity_service_list_runs(Database *database,const char *evidence_identifier,GError **error); +GPtrArray *document_authenticity_service_history(Database *database,const char *evidence_identifier,GError **error); +gboolean document_authenticity_service_add(Database *database,const char *evidence_identifier, + const char *status,const char *justification,const char *technical_note, + const char *ocr_run_identifier,DocumentAuthenticityAssessment **created,GError **error); +G_END_DECLS +#endif diff --git a/include/models/identity_traceability.h b/include/models/identity_traceability.h index de7422c..e2cac78 100644 --- a/include/models/identity_traceability.h +++ b/include/models/identity_traceability.h @@ -41,6 +41,15 @@ DocumentAuthenticityAssessment *document_authenticity_assessment_copy( const DocumentAuthenticityAssessment *assessment); void document_authenticity_assessment_free( DocumentAuthenticityAssessment *assessment); +const char *document_authenticity_assessment_get_identifier(const DocumentAuthenticityAssessment *assessment); +const char *document_authenticity_assessment_get_evidence_identifier(const DocumentAuthenticityAssessment *assessment); +const char *document_authenticity_assessment_get_ocr_run_identifier(const DocumentAuthenticityAssessment *assessment); +const char *document_authenticity_assessment_get_status(const DocumentAuthenticityAssessment *assessment); +const char *document_authenticity_assessment_get_justification(const DocumentAuthenticityAssessment *assessment); +const char *document_authenticity_assessment_get_assessed_at(const DocumentAuthenticityAssessment *assessment); +const char *document_authenticity_assessment_get_previous_identifier(const DocumentAuthenticityAssessment *assessment); +const char *document_authenticity_assessment_get_technical_note(const DocumentAuthenticityAssessment *assessment); +const char *document_authenticity_assessment_get_origin(const DocumentAuthenticityAssessment *assessment); PersonEvidenceFactualRelation *person_evidence_factual_relation_new( const char *identifier, const char *person_identifier, diff --git a/include/views/document_authenticity_editor.h b/include/views/document_authenticity_editor.h new file mode 100644 index 0000000..b43e3df --- /dev/null +++ b/include/views/document_authenticity_editor.h @@ -0,0 +1,12 @@ +#ifndef LABFY_DOCUMENT_AUTHENTICITY_EDITOR_H +#define LABFY_DOCUMENT_AUTHENTICITY_EDITOR_H +#include "database/database.h" +#include +G_BEGIN_DECLS +typedef struct DocumentAuthenticityEditor DocumentAuthenticityEditor; +DocumentAuthenticityEditor *document_authenticity_editor_new(Database *database,const char *evidence_identifier); +GtkWidget *document_authenticity_editor_get_widget(DocumentAuthenticityEditor *editor); +gboolean document_authenticity_editor_refresh(DocumentAuthenticityEditor *editor,GError **error); +void document_authenticity_editor_free(DocumentAuthenticityEditor *editor); +G_END_DECLS +#endif diff --git a/src/core/document_authenticity_service.c b/src/core/document_authenticity_service.c new file mode 100644 index 0000000..d3a7290 --- /dev/null +++ b/src/core/document_authenticity_service.c @@ -0,0 +1,44 @@ +#include "core/document_authenticity_service.h" +#include "dao/identity_ocr_dao.h" +#include "dao/identity_traceability_dao.h" +#include "database/transaction.h" + +static GQuark domain(void){return g_quark_from_static_string("document-authenticity-service");} +static const DocumentAuthenticityStatus statuses[]={ + {"indeterminate","Authenticité indéterminée","Appréciation humaine ne concluant pas sur l’authenticité.",FALSE}, + {"presumed_authentic","Document présumé authentique","Appréciation humaine provisoire, sans certification automatique.",TRUE}, + {"suspicious","Document présentant des éléments suspects","Appréciation humaine signalant des éléments à examiner.",TRUE}, + {"presumed_forged","Document présumé falsifié","Appréciation humaine provisoire nécessitant une justification.",TRUE}, + {"confirmed_forged","Falsification confirmée","Conclusion humaine documentée, jamais déduite automatiquement.",TRUE}}; +const DocumentAuthenticityStatus *document_authenticity_service_statuses(gsize*c) +{if(c)*c=G_N_ELEMENTS(statuses);return statuses;} +const DocumentAuthenticityStatus *document_authenticity_service_status(const char*code) +{for(guint i=0;irequires_justification&&(!j||!*j))){ + g_free(j);g_set_error_literal(error,domain(),1,"Une justification est requise pour cette appréciation.");return FALSE;} + IdentityTraceabilityDao*dao=identity_traceability_dao_new(d);GError*local=NULL; + if(!database_transaction_begin(d)){identity_traceability_dao_free(dao);g_free(j); + g_set_error_literal(error,domain(),2,"Impossible de démarrer la transaction d’authenticité.");return FALSE;} + DocumentAuthenticityAssessment*previous=identity_traceability_dao_current_authenticity(dao,evidence,&local); + if(local){database_transaction_rollback(d);g_propagate_error(error,local);identity_traceability_dao_free(dao);g_free(j);return FALSE;} + char*id=g_uuid_string_random();GDateTime*now=g_date_time_new_now_utc();char*at=g_date_time_format(now,"%Y-%m-%dT%H:%M:%SZ"); + DocumentAuthenticityAssessment*a=document_authenticity_assessment_new(id,evidence,run,status, + j&&*j?j:NULL,at,previous?document_authenticity_assessment_get_identifier(previous):NULL,note); + gboolean ok=a&&identity_traceability_dao_insert_authenticity(dao,a,&local)&&database_transaction_commit(d); + if(!ok&&database_transaction_is_active(d))database_transaction_rollback(d); + if(!ok&&!local)g_set_error_literal(&local,domain(),2,"Impossible d’enregistrer l’appréciation d’authenticité."); + if(ok&&created)*created=document_authenticity_assessment_copy(a);else if(local)g_propagate_error(error,local); + document_authenticity_assessment_free(a);document_authenticity_assessment_free(previous);identity_traceability_dao_free(dao); + g_date_time_unref(now);g_free(at);g_free(id);g_free(j);return ok; +} diff --git a/src/dao/identity_traceability_dao.c b/src/dao/identity_traceability_dao.c index 2b94782..b6d5125 100644 --- a/src/dao/identity_traceability_dao.c +++ b/src/dao/identity_traceability_dao.c @@ -15,12 +15,37 @@ gboolean identity_traceability_dao_insert_authenticity( { DocumentAuthenticityAssessment*valid=a?document_authenticity_assessment_copy(a):NULL; if(!d||!valid){fail(e,"Évaluation d’authenticité invalide.");return FALSE;} + const char*run=document_authenticity_assessment_get_ocr_run_identifier(valid); + if(run){ + DatabaseStatement*check=database_statement_prepare(d->database, + "SELECT 1 FROM identity_ocr_runs WHERE id=? AND evidence_id=?;"); + gboolean compatible=check&&bind(check,1,run)&&bind(check,2, + document_authenticity_assessment_get_evidence_identifier(valid))&& + database_statement_step(check)==DATABASE_STATEMENT_STEP_ROW; + database_statement_finalize(check); + if(!compatible){document_authenticity_assessment_free(valid); + fail(e,"L’exécution OCR ne correspond pas à la preuve évaluée.");return FALSE;} + } + const char*previous=document_authenticity_assessment_get_previous_identifier(valid); + if(previous){ + DatabaseStatement*check=database_statement_prepare(d->database, + "SELECT 1 FROM document_authenticity_assessments WHERE id=? AND evidence_id=?;"); + gboolean compatible=check&&bind(check,1,previous)&&bind(check,2, + document_authenticity_assessment_get_evidence_identifier(valid))&& + database_statement_step(check)==DATABASE_STATEMENT_STEP_ROW; + database_statement_finalize(check); + if(!compatible){document_authenticity_assessment_free(valid); + fail(e,"L’appréciation précédente ne correspond pas à la preuve évaluée.");return FALSE;} + } DatabaseStatement*s=database_statement_prepare(d->database, "INSERT INTO document_authenticity_assessments VALUES(?,?,?,?,?,?,?,?,?);"); - gboolean ok=s&&bind(s,1,a->identifier)&&bind(s,2,a->evidence_identifier)&& - bind(s,3,a->ocr_run_identifier)&&bind(s,4,a->status)&&bind(s,5,a->justification)&& - bind(s,6,a->assessed_at)&&bind(s,7,a->previous_identifier)&& - bind(s,8,a->technical_note)&&bind(s,9,"human")&& + gboolean ok=s&&bind(s,1,document_authenticity_assessment_get_identifier(valid))&& + bind(s,2,document_authenticity_assessment_get_evidence_identifier(valid))&& + bind(s,3,run)&&bind(s,4,document_authenticity_assessment_get_status(valid))&& + bind(s,5,document_authenticity_assessment_get_justification(valid))&& + bind(s,6,document_authenticity_assessment_get_assessed_at(valid))&& + bind(s,7,document_authenticity_assessment_get_previous_identifier(valid))&& + bind(s,8,document_authenticity_assessment_get_technical_note(valid))&&bind(s,9,"human")&& database_statement_step(s)==DATABASE_STATEMENT_STEP_DONE; database_statement_finalize(s);document_authenticity_assessment_free(valid); if(!ok)fail(e,"Impossible de conserver l’évaluation d’authenticité."); @@ -66,8 +91,10 @@ GPtrArray *identity_traceability_dao_list_authenticity( DocumentAuthenticityAssessment *identity_traceability_dao_current_authenticity( IdentityTraceabilityDao*d,const char*id,GError**e) { - GPtrArray*a=auth_query(d,"SELECT * FROM document_authenticity_assessments " - "WHERE evidence_id=? ORDER BY assessed_at DESC,id DESC LIMIT 1;",id,e); + GPtrArray*a=auth_query(d,"SELECT a.* FROM document_authenticity_assessments a " + "WHERE a.evidence_id=? AND NOT EXISTS(SELECT 1 FROM " + "document_authenticity_assessments n WHERE n.previous_assessment_id=a.id) " + "ORDER BY a.assessed_at DESC,a.id DESC LIMIT 1;",id,e); if(!a)return NULL; DocumentAuthenticityAssessment*r=a->len? document_authenticity_assessment_copy(g_ptr_array_index(a,0)):NULL; diff --git a/src/models/identity_traceability.c b/src/models/identity_traceability.c index 6d2a5f1..66ffc5a 100644 --- a/src/models/identity_traceability.c +++ b/src/models/identity_traceability.c @@ -77,6 +77,18 @@ void document_authenticity_assessment_free(DocumentAuthenticityAssessment*a) g_free(a->ocr_run_identifier);g_free(a->status);g_free(a->justification); g_free(a->assessed_at);g_free(a->previous_identifier); g_free(a->technical_note);g_free(a->origin);g_free(a);} +#define AUTH_GETTER(name,field) \ +const char *document_authenticity_assessment_get_##name( \ + const DocumentAuthenticityAssessment*a){return a?a->field:NULL;} +AUTH_GETTER(identifier,identifier) +AUTH_GETTER(evidence_identifier,evidence_identifier) +AUTH_GETTER(ocr_run_identifier,ocr_run_identifier) +AUTH_GETTER(status,status) +AUTH_GETTER(justification,justification) +AUTH_GETTER(assessed_at,assessed_at) +AUTH_GETTER(previous_identifier,previous_identifier) +AUTH_GETTER(technical_note,technical_note) +AUTH_GETTER(origin,origin) PersonEvidenceFactualRelation *person_evidence_factual_relation_new( const char *id,const char *person,const char *evidence,const char *run, diff --git a/src/views/document_authenticity_editor.c b/src/views/document_authenticity_editor.c new file mode 100644 index 0000000..9d62474 --- /dev/null +++ b/src/views/document_authenticity_editor.c @@ -0,0 +1,52 @@ +#include "views/document_authenticity_editor.h" +#include "core/document_authenticity_service.h" +#include "dao/identity_ocr_dao.h" + +struct DocumentAuthenticityEditor{Database*database;char*evidence;GtkWidget*root; + GtkDropDown*status;GtkStringList*status_labels;GtkDropDown*run;GtkStringList*run_labels; + GPtrArray*run_ids;GtkTextBuffer*justification;GtkTextBuffer*note;GtkLabel*error;GtkBox*history;}; +static void clear_box(GtkBox*b){GtkWidget*c=gtk_widget_get_first_child(GTK_WIDGET(b));while(c){GtkWidget*n=gtk_widget_get_next_sibling(c);gtk_box_remove(b,c);c=n;}} +static char *buffer_text(GtkTextBuffer*b){GtkTextIter s,e;gtk_text_buffer_get_bounds(b,&s,&e);char*t=gtk_text_buffer_get_text(b,&s,&e,FALSE);g_strstrip(t);return t;} +gboolean document_authenticity_editor_refresh(DocumentAuthenticityEditor*x,GError**error) +{ + if(!x)return FALSE; + clear_box(x->history);GPtrArray*h=document_authenticity_service_history(x->database,x->evidence,error);if(!h)return FALSE; + if(h->len==0)gtk_box_append(x->history,gtk_label_new("Aucune appréciation humaine enregistrée.")); + for(gint i=(gint)h->len-1;i>=0;i--){DocumentAuthenticityAssessment*a=g_ptr_array_index(h,(guint)i); + const DocumentAuthenticityStatus*s=document_authenticity_service_status(document_authenticity_assessment_get_status(a)); + const char*j=document_authenticity_assessment_get_justification(a),*n=document_authenticity_assessment_get_technical_note(a),*r=document_authenticity_assessment_get_ocr_run_identifier(a); + char*t=g_strdup_printf("%s — %s — origine humaine%s%s%s%s%s%s",s?s->label:"Statut inconnu", + document_authenticity_assessment_get_assessed_at(a),r?" — OcrRun : ":"",r?r:"",j?"\nJustification : ":"",j?j:"",n?"\nNote technique : ":"",n?n:""); + GtkWidget*l=gtk_label_new(t);gtk_label_set_xalign(GTK_LABEL(l),0);gtk_label_set_wrap(GTK_LABEL(l),TRUE);gtk_box_append(x->history,l);g_free(t);} + g_ptr_array_unref(h);return TRUE; +} +static void save(GtkButton*b,gpointer data) +{ + (void)b;DocumentAuthenticityEditor*x=data;guint si=gtk_drop_down_get_selected(x->status),ri=gtk_drop_down_get_selected(x->run);gsize count=0; + const DocumentAuthenticityStatus*s=document_authenticity_service_statuses(&count);if(si>=count)return; + char*j=buffer_text(x->justification),*n=buffer_text(x->note);const char*run=ri>0&&ri-1run_ids->len?g_ptr_array_index(x->run_ids,ri-1):NULL;GError*error=NULL; + if(!document_authenticity_service_add(x->database,x->evidence,s[si].code,j, + n&&*n?n:NULL,run,NULL,&error)){ + gtk_label_set_text(x->error,error?error->message:"Erreur d’enregistrement.");gtk_widget_set_visible(GTK_WIDGET(x->error),TRUE);g_clear_error(&error); + }else{gtk_widget_set_visible(GTK_WIDGET(x->error),FALSE);gtk_text_buffer_set_text(x->justification,"",-1);gtk_text_buffer_set_text(x->note,"",-1);document_authenticity_editor_refresh(x,NULL);} + g_free(j);g_free(n); +} +DocumentAuthenticityEditor *document_authenticity_editor_new(Database*d,const char*evidence) +{ + if(!d||!g_uuid_string_is_valid(evidence))return NULL; + DocumentAuthenticityEditor*x=g_new0(DocumentAuthenticityEditor,1);x->database=d;x->evidence=g_strdup(evidence); + x->root=gtk_box_new(GTK_ORIENTATION_VERTICAL,6);gtk_widget_set_name(x->root,"document-authenticity-editor"); + GtkWidget*title=gtk_label_new("Authenticité documentaire — appréciation humaine");gtk_label_set_xalign(GTK_LABEL(title),0);gtk_box_append(GTK_BOX(x->root),title); + x->status_labels=gtk_string_list_new(NULL);gsize count=0;const DocumentAuthenticityStatus*s=document_authenticity_service_statuses(&count);for(guint i=0;istatus_labels,s[i].label); + x->status=GTK_DROP_DOWN(gtk_drop_down_new(G_LIST_MODEL(g_object_ref(x->status_labels)),NULL));gtk_widget_set_name(GTK_WIDGET(x->status),"authenticity-status");gtk_box_append(GTK_BOX(x->root),GTK_WIDGET(x->status)); + x->run_labels=gtk_string_list_new(NULL);gtk_string_list_append(x->run_labels,"Aucune exécution OCR");x->run_ids=g_ptr_array_new_with_free_func(g_free);GPtrArray*runs=document_authenticity_service_list_runs(d,evidence,NULL); + if(runs){for(guint i=0;ilen;i++){IdentityOcrRunRecord*r=g_ptr_array_index(runs,i);char*l=g_strdup_printf("%s — %s",r->executed_at,r->engine);gtk_string_list_append(x->run_labels,l);g_ptr_array_add(x->run_ids,g_strdup(r->id));g_free(l);}g_ptr_array_unref(runs);} + x->run=GTK_DROP_DOWN(gtk_drop_down_new(G_LIST_MODEL(g_object_ref(x->run_labels)),NULL));gtk_widget_set_name(GTK_WIDGET(x->run),"authenticity-ocr-run");gtk_box_append(GTK_BOX(x->root),GTK_WIDGET(x->run)); + GtkWidget*jv=gtk_text_view_new();x->justification=gtk_text_view_get_buffer(GTK_TEXT_VIEW(jv));gtk_widget_set_name(jv,"authenticity-justification");gtk_widget_set_size_request(jv,-1,60);gtk_box_append(GTK_BOX(x->root),gtk_label_new("Justification factuelle"));gtk_box_append(GTK_BOX(x->root),jv); + GtkWidget*nv=gtk_text_view_new();x->note=gtk_text_view_get_buffer(GTK_TEXT_VIEW(nv));gtk_widget_set_name(nv,"authenticity-technical-note");gtk_widget_set_size_request(nv,-1,45);gtk_box_append(GTK_BOX(x->root),gtk_label_new("Note technique facultative"));gtk_box_append(GTK_BOX(x->root),nv); + x->error=GTK_LABEL(gtk_label_new(""));gtk_widget_set_name(GTK_WIDGET(x->error),"authenticity-error");gtk_widget_add_css_class(GTK_WIDGET(x->error),"error");gtk_widget_set_visible(GTK_WIDGET(x->error),FALSE);gtk_box_append(GTK_BOX(x->root),GTK_WIDGET(x->error)); + GtkWidget*button=gtk_button_new_with_label("Enregistrer l’appréciation humaine");gtk_widget_set_name(button,"authenticity-save");g_signal_connect(button,"clicked",G_CALLBACK(save),x);gtk_box_append(GTK_BOX(x->root),button); + x->history=GTK_BOX(gtk_box_new(GTK_ORIENTATION_VERTICAL,4));gtk_widget_set_name(GTK_WIDGET(x->history),"authenticity-history");gtk_box_append(GTK_BOX(x->root),GTK_WIDGET(x->history));document_authenticity_editor_refresh(x,NULL);return x; +} +GtkWidget *document_authenticity_editor_get_widget(DocumentAuthenticityEditor*x){return x?x->root:NULL;} +void document_authenticity_editor_free(DocumentAuthenticityEditor*x){if(!x)return;g_clear_object(&x->status_labels);g_clear_object(&x->run_labels);g_ptr_array_unref(x->run_ids);g_free(x->evidence);g_free(x);} diff --git a/src/views/evidence_metadata_dialog.c b/src/views/evidence_metadata_dialog.c index 97d02c1..12e3064 100644 --- a/src/views/evidence_metadata_dialog.c +++ b/src/views/evidence_metadata_dialog.c @@ -12,6 +12,7 @@ #include "dao/entity_dao.h" #include "views/evidence_identity_ocr_dialog.h" #include "views/person_vocabulary_adapter.h" +#include "views/document_authenticity_editor.h" #include "widgets/ocr_provenance_overlay.h" struct EvidenceMetadataDialogResult @@ -48,6 +49,7 @@ typedef struct guint64 ocr_generation; gpointer user_data; gboolean completed; + DocumentAuthenticityEditor *authenticity_editor; } EvidenceMetadataDialogContext; static void evidence_metadata_dialog_complete( @@ -89,6 +91,7 @@ static void evidence_metadata_dialog_context_free(gpointer data) g_clear_pointer(&context->ocr_documents, g_ptr_array_unref); g_clear_pointer(&context->ocr_run_identifiers, g_ptr_array_unref); g_clear_object(&context->ocr_run_labels); + document_authenticity_editor_free(context->authenticity_editor); g_free(context->investigation_root); g_free(context->evidence_identifier); g_free(context); @@ -542,6 +545,15 @@ gboolean evidence_metadata_dialog_present_with_ocr( } if (database != NULL) { + context->authenticity_editor = document_authenticity_editor_new( + database, evidence_record_get_identifier(record)); + if (context->authenticity_editor != NULL) { + GtkWidget *auth_scroll = gtk_scrolled_window_new(); + gtk_widget_set_size_request(auth_scroll, -1, 300); + gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(auth_scroll), + document_authenticity_editor_get_widget(context->authenticity_editor)); + gtk_box_append(GTK_BOX(main_box), auth_scroll); + } IdentityTraceabilityDao *trace_dao = identity_traceability_dao_new(database); if (trace_dao != NULL) { GPtrArray *relations = identity_traceability_dao_list_factual_relations_by_evidence( diff --git a/tests/test_document_authenticity_editor_gtk.c b/tests/test_document_authenticity_editor_gtk.c new file mode 100644 index 0000000..d3c6b58 --- /dev/null +++ b/tests/test_document_authenticity_editor_gtk.c @@ -0,0 +1,24 @@ +#include "views/document_authenticity_editor.h" +#include "core/document_authenticity_service.h" +#include "database/database.h" +#include +#include +#define EVIDENCE "10000000-0000-4000-8000-000000000018" +#define RUN "30000000-0000-4000-8000-000000000018" +#define AT "2026-07-30T10:00:00Z" +static GtkWidget *named(GtkWidget*w,const char*n){if(g_strcmp0(gtk_widget_get_name(w),n)==0)return w;for(GtkWidget*c=gtk_widget_get_first_child(w);c;c=gtk_widget_get_next_sibling(c)){GtkWidget*f=named(c,n);if(f)return f;}return NULL;} +static void exec_ok(sqlite3*d,const char*s){char*m=NULL;g_assert_cmpint(sqlite3_exec(d,s,NULL,NULL,&m),==,SQLITE_OK);sqlite3_free(m);} +static void remove_fixture(char*dir,char*path){g_remove(path);g_rmdir(dir);g_free(path);g_free(dir);} +static void test_editor(void) +{ + GError*error=NULL;char*dir=g_dir_make_tmp("labfy-auth-gtk-XXXXXX",&error);g_assert_no_error(error);char*path=g_build_filename(dir,"SPECIMEN.sqlite",NULL);g_assert_true(database_initialize(path,"SPECIMEN",dir)); + sqlite3*sql=NULL;g_assert_cmpint(sqlite3_open(path,&sql),==,SQLITE_OK);exec_ok(sql,"PRAGMA foreign_keys=ON;INSERT INTO preuves(id,name,relative_path,type_id,size_bytes,sha256,imported_at,updated_at,status,locked,original_name) VALUES('" EVIDENCE "','specimen.png','specimen.png',2,8,'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa','" AT "','" AT "','active',0,'specimen.png');INSERT INTO identity_ocr_runs(id,evidence_id,expected_sha256,page_number,document_type,document_side,engine,requested_languages,available_languages,parameters,preprocessing_profile,executed_at,status,text_relative_path,text_sha256,tsv_relative_path,tsv_sha256) VALUES('" RUN "','" EVIDENCE "','aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',1,'identity_card','front','tesseract','fra','fra','SPECIMEN','none','" AT "','success','raw.txt','bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb','raw.tsv','cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc');");sqlite3_close(sql); + Database*d=database_open(path);g_assert_nonnull(d);g_assert_true(database_migrate_to_latest(d));DocumentAuthenticityEditor*x=document_authenticity_editor_new(d,EVIDENCE);g_assert_nonnull(x);GtkWidget*root=document_authenticity_editor_get_widget(x);g_object_ref_sink(root); + GtkDropDown*status=GTK_DROP_DOWN(named(root,"authenticity-status"));GtkDropDown*run=GTK_DROP_DOWN(named(root,"authenticity-ocr-run"));GtkWidget*save=named(root,"authenticity-save");GtkLabel*message=GTK_LABEL(named(root,"authenticity-error"));g_assert_cmpuint(g_list_model_get_n_items(gtk_drop_down_get_model(status)),==,5);g_assert_cmpuint(g_list_model_get_n_items(gtk_drop_down_get_model(run)),==,2); + GPtrArray*h=document_authenticity_service_history(d,EVIDENCE,&error);g_assert_no_error(error);g_assert_cmpuint(h->len,==,0);g_ptr_array_unref(h); + gtk_drop_down_set_selected(status,2);g_signal_emit_by_name(save,"clicked");g_assert_true(gtk_widget_get_visible(GTK_WIDGET(message)));g_assert_nonnull(g_strstr_len(gtk_label_get_text(message),-1,"justification")); + GtkTextBuffer*j=gtk_text_view_get_buffer(GTK_TEXT_VIEW(named(root,"authenticity-justification")));gtk_text_buffer_set_text(j,"Élément factuel SPECIMEN",-1);gtk_drop_down_set_selected(run,1);g_signal_emit_by_name(save,"clicked");g_assert_false(gtk_widget_get_visible(GTK_WIDGET(message))); + h=document_authenticity_service_history(d,EVIDENCE,&error);g_assert_cmpuint(h->len,==,1);DocumentAuthenticityAssessment*a=g_ptr_array_index(h,0);g_assert_cmpstr(document_authenticity_assessment_get_status(a),==,"suspicious");g_assert_cmpstr(document_authenticity_assessment_get_ocr_run_identifier(a),==,RUN);g_assert_cmpstr(document_authenticity_assessment_get_origin(a),==,"human");g_ptr_array_unref(h); + g_object_unref(root);document_authenticity_editor_free(x);database_close(d);d=database_open(path);h=document_authenticity_service_history(d,EVIDENCE,&error);g_assert_no_error(error);g_assert_cmpuint(h->len,==,1);g_ptr_array_unref(h);database_close(d);remove_fixture(dir,path); +} +int main(int argc,char**argv){gtk_init();g_test_init(&argc,&argv,NULL);g_test_add_func("/authenticity/editor",test_editor);return g_test_run();} diff --git a/tests/test_evidence_metadata_dialog_gtk.c b/tests/test_evidence_metadata_dialog_gtk.c index 8f79b06..3cb18f5 100644 --- a/tests/test_evidence_metadata_dialog_gtk.c +++ b/tests/test_evidence_metadata_dialog_gtk.c @@ -327,6 +327,8 @@ static gboolean drive(gpointer data) context->main_window); g_assert_true(gtk_window_get_modal(dialog)); g_assert_true(gtk_window_get_destroy_with_parent(dialog)); + g_assert_nonnull(find_named(GTK_WIDGET(dialog), + "document-authenticity-editor")); } if (dialog == NULL) { if (context->phase == 1) { diff --git a/tests/test_identity_traceability.c b/tests/test_identity_traceability.c index 208bda4..bb561b6 100644 --- a/tests/test_identity_traceability.c +++ b/tests/test_identity_traceability.c @@ -3,6 +3,7 @@ #include "dao/identity_ocr_dao.h" #include "database/database.h" #include "models/identity_traceability.h" +#include "core/document_authenticity_service.h" #include #include #include @@ -66,6 +67,11 @@ static void fixture_free(Fixture*f) static void test_models(void) { + gsize status_count=0;const DocumentAuthenticityStatus*auth_statuses= + document_authenticity_service_statuses(&status_count); + static const char*auth_codes[]={"indeterminate","presumed_authentic","suspicious","presumed_forged","confirmed_forged"}; + static const char*auth_labels[]={"Authenticité indéterminée","Document présumé authentique","Document présentant des éléments suspects","Document présumé falsifié","Falsification confirmée"}; + g_assert_cmpuint(status_count,==,5);for(guint i=0;istatus,==,a->status); document_authenticity_assessment_free(c);document_authenticity_assessment_free(a); } +static void test_authenticity_service(void) +{ + Fixture f=fixture_new();GError*error=NULL;DocumentAuthenticityAssessment*created=NULL; + g_assert_false(document_authenticity_service_add(f.database,EVIDENCE,"suspicious"," ",NULL,NULL,&created,&error));g_assert_error(error,g_quark_from_static_string("document-authenticity-service"),1);g_clear_error(&error); + g_assert_true(document_authenticity_service_add(f.database,EVIDENCE,"indeterminate",NULL,"Note SPECIMEN",NULL,&created,&error));g_assert_no_error(error);g_assert_null(document_authenticity_assessment_get_previous_identifier(created));document_authenticity_assessment_free(created); + g_assert_true(document_authenticity_service_add(f.database,EVIDENCE,"suspicious","Élément SPECIMEN",NULL,RUN,&created,&error));g_assert_no_error(error);g_assert_nonnull(document_authenticity_assessment_get_previous_identifier(created));document_authenticity_assessment_free(created); + GPtrArray*h=document_authenticity_service_history(f.database,EVIDENCE,&error);g_assert_no_error(error);g_assert_cmpuint(h->len,==,2);g_ptr_array_unref(h); + g_assert_false(document_authenticity_service_add(f.database,EVIDENCE, + "indeterminate",NULL,NULL,"30000000-0000-4000-8000-000000000099", + NULL,&error));g_assert_nonnull(error);g_clear_error(&error); + h=document_authenticity_service_history(f.database,EVIDENCE,&error);g_assert_cmpuint(h->len,==,2);g_ptr_array_unref(h);fixture_free(&f); +} static void test_dao_history_and_relations(void) { Fixture f=fixture_new();GError*e=NULL; @@ -248,6 +266,7 @@ static void test_migrate_v17_preserves_ocr(void) int main(int argc,char**argv) {g_test_init(&argc,&argv,NULL);g_test_add_func("/v18/models",test_models); g_test_add_func("/v18/dao-history-relations",test_dao_history_and_relations); + g_test_add_func("/v18/authenticity-service",test_authenticity_service); g_test_add_func("/v18/sqlite-negative-guards",test_sqlite_negative_guards); g_test_add_func("/v18/migrate-v17",test_migrate_v17_preserves_ocr); return g_test_run();}