feat(evidence): ajouter l’évaluation humaine d’authenticité

This commit is contained in:
grayTerminal-sh 2026-08-01 09:52:05 +02:00
parent 11cbc061ae
commit b8debcde2d
18 changed files with 280 additions and 12 deletions

View file

@ -2,6 +2,9 @@
### Added ### Added
- Saisie et consultation, depuis la fiche preuve, dun historique immuable
dappréciations humaines dauthenticité documentaire avec justification et
`OcrRun` facultatif appartenant à la preuve, sans verdict automatique.
- OCR didentité contrôlé et révisable dans la création dune personne, - OCR didentité contrôlé et révisable dans la création dune personne,
limport normal et la fiche de preuve, avec texte brut immuable, limport normal et la fiche de preuve, avec texte brut immuable,
transcription corrigée persistante, `manual_override`, `manual_entry` et transcription corrigée persistante, `manual_override`, `manual_entry` et

View file

@ -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_WORKSPACE_IDENTITY_OCR_GTK := tests/test_workspace_identity_ocr_gtk
TEST_DIALOG_GEOMETRY_GTK := tests/test_dialog_geometry_gtk TEST_DIALOG_GEOMETRY_GTK := tests/test_dialog_geometry_gtk
TEST_PERSON_FACTUAL_RELATION_EDITOR_GTK := tests/test_person_factual_relation_editor_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 FAKE_DOCUMENT_TOOL := tests/fake_document_tool
DOCUMENT_ANALYSIS_TEST_SOURCES := \ DOCUMENT_ANALYSIS_TEST_SOURCES := \
@ -225,6 +226,16 @@ $(TEST_PERSON_FACTUAL_RELATION_EDITOR_GTK): \
src/database/error.c src/core/relation_type_normalizer.c src/database/error.c src/core/relation_type_normalizer.c
$(CC) $(CFLAGS) $^ -o $@ $(LDFLAGS) $(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) all: $(TARGET)
$(TEST_IDENTITY_OCR): tests/test_identity_ocr.c \ $(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 \ $(TEST_IDENTITY_TRACEABILITY): tests/test_identity_traceability.c \
src/models/identity_traceability.c src/dao/identity_traceability_dao.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_vocabulary_adapter.c \
src/views/person_factual_relation_editor.c \ src/views/person_factual_relation_editor.c \
src/models/person_role_assignment.c \ src/models/person_role_assignment.c \
@ -1199,7 +1211,8 @@ test: \
$(TEST_EVIDENCE_IDENTITY_IMPORT_GTK) \ $(TEST_EVIDENCE_IDENTITY_IMPORT_GTK) \
$(TEST_WORKSPACE_IDENTITY_OCR_GTK) \ $(TEST_WORKSPACE_IDENTITY_OCR_GTK) \
$(TEST_DIALOG_GEOMETRY_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..." @echo "Exécution des tests..."
@./$(TEST_NODE) @./$(TEST_NODE)
@./$(TEST_TREE_MODEL) @./$(TEST_TREE_MODEL)
@ -1296,6 +1309,7 @@ test: \
@$(TEST_WORKSPACE_IDENTITY_OCR_GTK) @$(TEST_WORKSPACE_IDENTITY_OCR_GTK)
@$(TEST_DIALOG_GEOMETRY_GTK) @$(TEST_DIALOG_GEOMETRY_GTK)
@$(TEST_PERSON_FACTUAL_RELATION_EDITOR_GTK) @$(TEST_PERSON_FACTUAL_RELATION_EDITOR_GTK)
@$(TEST_DOCUMENT_AUTHENTICITY_EDITOR_GTK)
@echo "Tous les tests sont valides." @echo "Tous les tests sont valides."
%.o: %.c %.o: %.c
@ -1396,7 +1410,8 @@ clean:
$(TEST_WORKSPACE_IDENTITY_OCR_GTK) \ $(TEST_WORKSPACE_IDENTITY_OCR_GTK) \
$(TEST_DIALOG_GEOMETRY_GTK) \ $(TEST_DIALOG_GEOMETRY_GTK) \
$(TEST_PERSON_FACTUAL_RELATION_EDITOR_GTK) \ $(TEST_PERSON_FACTUAL_RELATION_EDITOR_GTK) \
$(FAKE_DOCUMENT_TOOL) $(TEST_DOCUMENT_AUTHENTICITY_EDITOR_GTK) \
$(FAKE_DOCUMENT_TOOL)

View file

@ -17,6 +17,10 @@ Labfy Investigation est un poste de travail libre dinvestigation 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. 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 dune preuve permet aussi de consigner une appréciation humaine
dauthenticité documentaire. Cet historique est immuable, justifié selon le
statut choisi et ne peut jamais être produit automatiquement par lOCR.
> **État du projet : développement actif** > **État du projet : développement actif**
> >
> Le logiciel nest pas encore prêt pour un usage opérationnel en production. Les formats internes, linterface et les mécanismes dintégration peuvent encore évoluer. > Le logiciel nest pas encore prêt pour un usage opérationnel en production. Les formats internes, linterface et les mécanismes dintégration peuvent encore évoluer.

View file

@ -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 sont orthogonales au statut de révision. Les contraintes SQLite et le service
de validation interdisent la projection des valeurs rejetées, en conflit, de validation interdisent la projection des valeurs rejetées, en conflit,
incertaines ou invalides. Cette tranche ne réalise encore aucune projection incertaines ou invalides. Cette tranche ne réalise encore aucune projection
automatique et ne fournit pas linterface GTK complète de saisie. automatique. La fiche preuve délègue la saisie et la consultation de
lauthenticité à `DocumentAuthenticityEditor`, qui appelle
`DocumentAuthenticityService`; le widget naccède jamais directement à
SQLite. Le service ajoute chaque appréciation humaine dans une transaction et
chaîne son identifiant à lappréciation courante.
--- ---

View file

@ -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 multipage via Cairo. Elles ne doivent jamais être remplacées par un document
réel. réel.
Lauthenticité 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 laperçu partagé comprend La validation ciblée de laperçu partagé comprend
`test_evidence_preview_widget_gtk`, `test_evidence_preview`, `test_evidence_preview_widget_gtk`, `test_evidence_preview`,
`test_evidence_video_preview_controller` et `test_evidence_video_preview_controller` et

View file

@ -178,13 +178,15 @@ PARTIEL — VALIDATION MANUELLE DES TRANCHES LIVRÉES RÉUSSIE
- fondations V18 : authenticité humaine historisée, relations factuelles - fondations V18 : authenticité humaine historisée, relations factuelles
typées, vocabulaires didentification et de rôles, cycle OCR jusquà la typées, vocabulaires didentification et de rôles, cycle OCR jusquà la
valeur explicitement confirmée et contraintes anti-automatisme. valeur explicitement confirmée et contraintes anti-automatisme.
- interface GTK de saisie et de consultation de lauthenticité documentaire
depuis la fiche preuve, avec OcrRun facultatif filtré et historique immuable.
Limitations restantes : Limitations restantes :
1. lOCR groupé de plusieurs preuves dans une même opération nest pas pris en 1. lOCR groupé de plusieurs preuves dans une même opération nest pas pris en
charge ; charge ;
2. linterface GTK complète de saisie de lauthenticité, des relations 2. les interfaces GTK complètes des états didentification et des rôles V18
factuelles, des états et des rôles V18 reste à construire ; restent à construire ;
3. la provenance uniforme de tous les dérivés et la projection des seules 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 valeurs OCR confirmées vers les attributs structurés de la personne restent
partielles ; partielles ;

View file

@ -16,8 +16,13 @@ contraintes refusent toute confirmation dun champ rejeté, en conflit,
incertain ou invalide. Aucun OCR ne crée une évaluation dauthenticité, une incertain ou invalide. Aucun OCR ne crée une évaluation dauthenticité, une
relation factuelle, un rôle sensible ou un état `confirmed`. relation factuelle, un rôle sensible ou un état `confirmed`.
Les insertions dauthenticité vérifient hors GTK quun `ocr_run_id` facultatif
appartient à la même preuve. `previous_assessment_id` forme une chaîne
append-only ; lajout et la détermination du maillon courant sont coordonnés
dans une transaction.
> **Statut :** architecture courante > **Statut :** architecture courante
> **Version du schéma :** V10 > **Version du schéma :** V18
> **Dernière mise à jour :** 2026-07-24 > **Dernière mise à jour :** 2026-07-24
> **Source de vérité détaillée :** `SCHEMA_AUDIT_CURRENT.md` > **Source de vérité détaillée :** `SCHEMA_AUDIT_CURRENT.md`

View file

@ -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 <glib.h>
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

View file

@ -41,6 +41,15 @@ DocumentAuthenticityAssessment *document_authenticity_assessment_copy(
const DocumentAuthenticityAssessment *assessment); const DocumentAuthenticityAssessment *assessment);
void document_authenticity_assessment_free( void document_authenticity_assessment_free(
DocumentAuthenticityAssessment *assessment); 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( PersonEvidenceFactualRelation *person_evidence_factual_relation_new(
const char *identifier, const char *person_identifier, const char *identifier, const char *person_identifier,

View file

@ -0,0 +1,12 @@
#ifndef LABFY_DOCUMENT_AUTHENTICITY_EDITOR_H
#define LABFY_DOCUMENT_AUTHENTICITY_EDITOR_H
#include "database/database.h"
#include <gtk/gtk.h>
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

View file

@ -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 lauthenticité.",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;i<G_N_ELEMENTS(statuses);i++)if(g_strcmp0(code,statuses[i].code)==0)return &statuses[i];return NULL;}
GPtrArray *document_authenticity_service_list_runs(Database*d,const char*e,GError**error)
{IdentityOcrDao*dao=identity_ocr_dao_new(d);if(!dao)return NULL;GPtrArray*a=identity_ocr_dao_list_runs_by_evidence(dao,e,error);identity_ocr_dao_free(dao);return a;}
GPtrArray *document_authenticity_service_history(Database*d,const char*e,GError**error)
{IdentityTraceabilityDao*dao=identity_traceability_dao_new(d);if(!dao)return NULL;GPtrArray*a=identity_traceability_dao_list_authenticity(dao,e,error);identity_traceability_dao_free(dao);return a;}
gboolean document_authenticity_service_add(Database*d,const char*evidence,
const char*status,const char*justification,const char*note,const char*run,
DocumentAuthenticityAssessment**created,GError**error)
{
if(created)*created=NULL;
const DocumentAuthenticityStatus*entry=document_authenticity_service_status(status);
char*j=justification?g_strdup(justification):NULL;if(j)g_strstrip(j);
if(!d||!g_uuid_string_is_valid(evidence)||!entry||(entry->requires_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 dauthenticité.");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 denregistrer lappréciation dauthenticité.");
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;
}

View file

@ -15,12 +15,37 @@ gboolean identity_traceability_dao_insert_authenticity(
{ {
DocumentAuthenticityAssessment*valid=a?document_authenticity_assessment_copy(a):NULL; DocumentAuthenticityAssessment*valid=a?document_authenticity_assessment_copy(a):NULL;
if(!d||!valid){fail(e,"Évaluation dauthenticité invalide.");return FALSE;} if(!d||!valid){fail(e,"Évaluation dauthenticité 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,"Lexé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,"Lappréciation précédente ne correspond pas à la preuve évaluée.");return FALSE;}
}
DatabaseStatement*s=database_statement_prepare(d->database, DatabaseStatement*s=database_statement_prepare(d->database,
"INSERT INTO document_authenticity_assessments VALUES(?,?,?,?,?,?,?,?,?);"); "INSERT INTO document_authenticity_assessments VALUES(?,?,?,?,?,?,?,?,?);");
gboolean ok=s&&bind(s,1,a->identifier)&&bind(s,2,a->evidence_identifier)&& gboolean ok=s&&bind(s,1,document_authenticity_assessment_get_identifier(valid))&&
bind(s,3,a->ocr_run_identifier)&&bind(s,4,a->status)&&bind(s,5,a->justification)&& bind(s,2,document_authenticity_assessment_get_evidence_identifier(valid))&&
bind(s,6,a->assessed_at)&&bind(s,7,a->previous_identifier)&& bind(s,3,run)&&bind(s,4,document_authenticity_assessment_get_status(valid))&&
bind(s,8,a->technical_note)&&bind(s,9,"human")&& 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_step(s)==DATABASE_STATEMENT_STEP_DONE;
database_statement_finalize(s);document_authenticity_assessment_free(valid); database_statement_finalize(s);document_authenticity_assessment_free(valid);
if(!ok)fail(e,"Impossible de conserver lévaluation dauthenticité."); if(!ok)fail(e,"Impossible de conserver lévaluation dauthenticité.");
@ -66,8 +91,10 @@ GPtrArray *identity_traceability_dao_list_authenticity(
DocumentAuthenticityAssessment *identity_traceability_dao_current_authenticity( DocumentAuthenticityAssessment *identity_traceability_dao_current_authenticity(
IdentityTraceabilityDao*d,const char*id,GError**e) IdentityTraceabilityDao*d,const char*id,GError**e)
{ {
GPtrArray*a=auth_query(d,"SELECT * FROM document_authenticity_assessments " GPtrArray*a=auth_query(d,"SELECT a.* FROM document_authenticity_assessments a "
"WHERE evidence_id=? ORDER BY assessed_at DESC,id DESC LIMIT 1;",id,e); "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; if(!a)return NULL;
DocumentAuthenticityAssessment*r=a->len? DocumentAuthenticityAssessment*r=a->len?
document_authenticity_assessment_copy(g_ptr_array_index(a,0)):NULL; document_authenticity_assessment_copy(g_ptr_array_index(a,0)):NULL;

View file

@ -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->ocr_run_identifier);g_free(a->status);g_free(a->justification);
g_free(a->assessed_at);g_free(a->previous_identifier); g_free(a->assessed_at);g_free(a->previous_identifier);
g_free(a->technical_note);g_free(a->origin);g_free(a);} 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( PersonEvidenceFactualRelation *person_evidence_factual_relation_new(
const char *id,const char *person,const char *evidence,const char *run, const char *id,const char *person,const char *evidence,const char *run,

View file

@ -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-1<x->run_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 denregistrement.");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;i<count;i++)gtk_string_list_append(x->status_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;i<runs->len;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 lappré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);}

View file

@ -12,6 +12,7 @@
#include "dao/entity_dao.h" #include "dao/entity_dao.h"
#include "views/evidence_identity_ocr_dialog.h" #include "views/evidence_identity_ocr_dialog.h"
#include "views/person_vocabulary_adapter.h" #include "views/person_vocabulary_adapter.h"
#include "views/document_authenticity_editor.h"
#include "widgets/ocr_provenance_overlay.h" #include "widgets/ocr_provenance_overlay.h"
struct EvidenceMetadataDialogResult struct EvidenceMetadataDialogResult
@ -48,6 +49,7 @@ typedef struct
guint64 ocr_generation; guint64 ocr_generation;
gpointer user_data; gpointer user_data;
gboolean completed; gboolean completed;
DocumentAuthenticityEditor *authenticity_editor;
} EvidenceMetadataDialogContext; } EvidenceMetadataDialogContext;
static void evidence_metadata_dialog_complete( 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_documents, g_ptr_array_unref);
g_clear_pointer(&context->ocr_run_identifiers, g_ptr_array_unref); g_clear_pointer(&context->ocr_run_identifiers, g_ptr_array_unref);
g_clear_object(&context->ocr_run_labels); g_clear_object(&context->ocr_run_labels);
document_authenticity_editor_free(context->authenticity_editor);
g_free(context->investigation_root); g_free(context->investigation_root);
g_free(context->evidence_identifier); g_free(context->evidence_identifier);
g_free(context); g_free(context);
@ -542,6 +545,15 @@ gboolean evidence_metadata_dialog_present_with_ocr(
} }
if (database != NULL) { 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); IdentityTraceabilityDao *trace_dao = identity_traceability_dao_new(database);
if (trace_dao != NULL) { if (trace_dao != NULL) {
GPtrArray *relations = identity_traceability_dao_list_factual_relations_by_evidence( GPtrArray *relations = identity_traceability_dao_list_factual_relations_by_evidence(

View file

@ -0,0 +1,24 @@
#include "views/document_authenticity_editor.h"
#include "core/document_authenticity_service.h"
#include "database/database.h"
#include <glib/gstdio.h>
#include <sqlite3.h>
#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();}

View file

@ -327,6 +327,8 @@ static gboolean drive(gpointer data)
context->main_window); context->main_window);
g_assert_true(gtk_window_get_modal(dialog)); g_assert_true(gtk_window_get_modal(dialog));
g_assert_true(gtk_window_get_destroy_with_parent(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 (dialog == NULL) {
if (context->phase == 1) { if (context->phase == 1) {

View file

@ -3,6 +3,7 @@
#include "dao/identity_ocr_dao.h" #include "dao/identity_ocr_dao.h"
#include "database/database.h" #include "database/database.h"
#include "models/identity_traceability.h" #include "models/identity_traceability.h"
#include "core/document_authenticity_service.h"
#include <glib.h> #include <glib.h>
#include <glib/gstdio.h> #include <glib/gstdio.h>
#include <sqlite3.h> #include <sqlite3.h>
@ -66,6 +67,11 @@ static void fixture_free(Fixture*f)
static void test_models(void) 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;i<status_count;i++){g_assert_cmpstr(auth_statuses[i].code,==,auth_codes[i]);g_assert_cmpstr(auth_statuses[i].label,==,auth_labels[i]);g_assert_nonnull(auth_statuses[i].description);}
static const char *codes[]={"identity_observed_in", static const char *codes[]={"identity_observed_in",
"document_presented_in_name_of","declared_holder_in","data_extracted_from"}; "document_presented_in_name_of","declared_holder_in","data_extracted_from"};
static const char *labels[]={"Identité observée dans la preuve", static const char *labels[]={"Identité observée dans la preuve",
@ -96,6 +102,18 @@ static void test_models(void)
document_authenticity_assessment_copy(a);g_assert_cmpstr(c->status,==,a->status); document_authenticity_assessment_copy(a);g_assert_cmpstr(c->status,==,a->status);
document_authenticity_assessment_free(c);document_authenticity_assessment_free(a); 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) static void test_dao_history_and_relations(void)
{ {
Fixture f=fixture_new();GError*e=NULL; 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) int main(int argc,char**argv)
{g_test_init(&argc,&argv,NULL);g_test_add_func("/v18/models",test_models); {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/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/sqlite-negative-guards",test_sqlite_negative_guards);
g_test_add_func("/v18/migrate-v17",test_migrate_v17_preserves_ocr); g_test_add_func("/v18/migrate-v17",test_migrate_v17_preserves_ocr);
return g_test_run();} return g_test_run();}