feat(evidence): historiser l’usage humain d’identité

This commit is contained in:
grayTerminal-sh 2026-08-02 09:14:05 +02:00
parent 78f0e45450
commit 116b08f2f4
31 changed files with 792 additions and 2126 deletions

View file

@ -2,6 +2,8 @@
### Added
- Assistant de création de personne en sept étapes séparant la révision OCR,
la projection facultative et les relations factuelles avant confirmation.
- Projection contrôlée des champs OCR confirmés vers les attributs structurés
d’une personne, avec provenance append-only, conflits explicites et rollback.
- Saisie et consultation, depuis la fiche preuve, d’un historique immuable
@ -40,6 +42,12 @@
### Fixed
- Schéma de création directe aligné sur la V19, avec champs structurés,
provenance des projections et garde SQLite identique à la migration V19.
- Migration V20 pour l’évaluation humaine append-only de l’usage d’identité,
distincte de l’authenticité et sans automatisme OCR.
- Consultation V20 sur la fiche preuve et test GTK des relations et champs
structurés affichés sur la fiche Person.
- Rafraîchissement immédiat de Workspace après import ou révision OCR et
persistance des données sur l’UUID définitif de la preuve, y compris après
fermeture et réouverture de SQLite.

View file

@ -198,6 +198,8 @@ 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
TEST_DOCUMENT_IDENTITY_MISUSE_EDITOR_GTK := tests/test_document_identity_misuse_editor_gtk
TEST_PERSON_DETAILS_TRACEABILITY_GTK := tests/test_person_details_traceability_gtk
TEST_PERSON_OCR_PROJECTION := tests/test_person_ocr_projection
TEST_PERSON_OCR_PROJECTION_EDITOR_GTK := tests/test_person_ocr_projection_editor_gtk
FAKE_DOCUMENT_TOOL := tests/fake_document_tool
@ -238,6 +240,26 @@ $(TEST_DOCUMENT_AUTHENTICITY_EDITOR_GTK): \
src/database/error.c src/core/relation_type_normalizer.c
$(CC) $(CFLAGS) $^ -o $@ $(LDFLAGS)
$(TEST_DOCUMENT_IDENTITY_MISUSE_EDITOR_GTK): \
tests/test_document_identity_misuse_editor_gtk.c \
src/views/document_identity_misuse_editor.c \
src/core/document_identity_misuse_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)
$(TEST_PERSON_DETAILS_TRACEABILITY_GTK): \
tests/test_person_details_traceability_gtk.c \
src/widgets/entity_details_panel.c src/models/entity_record.c \
src/models/evidence_record.c src/models/identity_traceability.c \
src/models/person_role_assignment.c src/views/person_vocabulary_adapter.c
$(CC) $(CFLAGS) $^ 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 -o $@ $(LDFLAGS)
PERSON_OCR_PROJECTION_SOURCES := src/models/person_ocr_projection.c \
src/core/person_ocr_projection_mapping.c src/core/person_ocr_projection_service.c \
src/dao/person_ocr_projection_dao.c src/dao/identity_ocr_dao.c \
@ -273,6 +295,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/core/document_identity_misuse_service.c \
src/views/person_vocabulary_adapter.c \
src/views/person_factual_relation_editor.c \
src/models/person_role_assignment.c \
@ -1238,6 +1261,8 @@ test: \
$(TEST_DIALOG_GEOMETRY_GTK) \
$(TEST_PERSON_FACTUAL_RELATION_EDITOR_GTK) \
$(TEST_DOCUMENT_AUTHENTICITY_EDITOR_GTK) \
$(TEST_DOCUMENT_IDENTITY_MISUSE_EDITOR_GTK) \
$(TEST_PERSON_DETAILS_TRACEABILITY_GTK) \
$(TEST_PERSON_OCR_PROJECTION) \
$(TEST_PERSON_OCR_PROJECTION_EDITOR_GTK)
@echo "Exécution des tests..."
@ -1337,6 +1362,8 @@ test: \
@$(TEST_DIALOG_GEOMETRY_GTK)
@$(TEST_PERSON_FACTUAL_RELATION_EDITOR_GTK)
@$(TEST_DOCUMENT_AUTHENTICITY_EDITOR_GTK)
@$(TEST_DOCUMENT_IDENTITY_MISUSE_EDITOR_GTK)
@$(TEST_PERSON_DETAILS_TRACEABILITY_GTK)
@$(TEST_PERSON_OCR_PROJECTION)
@$(TEST_PERSON_OCR_PROJECTION_EDITOR_GTK)
@echo "Tous les tests sont valides."
@ -1440,6 +1467,8 @@ clean:
$(TEST_DIALOG_GEOMETRY_GTK) \
$(TEST_PERSON_FACTUAL_RELATION_EDITOR_GTK) \
$(TEST_DOCUMENT_AUTHENTICITY_EDITOR_GTK) \
$(TEST_DOCUMENT_IDENTITY_MISUSE_EDITOR_GTK) \
$(TEST_PERSON_DETAILS_TRACEABILITY_GTK) \
$(TEST_PERSON_OCR_PROJECTION) \
$(TEST_PERSON_OCR_PROJECTION_EDITOR_GTK) \
$(FAKE_DOCUMENT_TOOL)

View file

@ -390,6 +390,18 @@ make clean
Le projet est compilé en C17 avec les avertissements traités comme des erreurs.
L’assistant de création d’une personne suit sept étapes distinctes :
`Personne`, `Rôles`, `Preuves`, `OCR identité`, `Projection OCR`,
`Relations factuelles`, puis `Confirmation`. La révision OCR conserve les
valeurs brute, normalisée, corrigée et confirmée avant toute projection. La
projection et les relations sont facultatives, vides par défaut et résumées
séparément avant l’unique écriture transactionnelle finale.
Le schéma SQLite courant est la V20. Une création neuve installe directement
les extensions V19 et V20 ; une base V19 utilise `database/schema_v20.sql`.
La fiche de preuve sépare l’authenticité de l’évaluation humaine de l’usage
d’identité, dont l’historique est append-only et jamais produit par l’OCR.
---
## Tests
@ -405,7 +417,8 @@ Vérifications recommandées avant chaque commit :
```bash
make clean
make -j8
make -j8 test
make check-source-size
DISPLAY="$DISPLAY" WAYLAND_DISPLAY="$WAYLAND_DISPLAY" make -j8 test
git diff --check
```
@ -427,6 +440,9 @@ G_DEBUG=fatal-criticals timeout 30s ./tests/test_evidence_identity_import_gtk
G_DEBUG=fatal-criticals timeout 30s ./tests/test_workspace_identity_ocr_gtk
G_DEBUG=fatal-criticals timeout 30s ./tests/test_evidence_preview_widget_gtk
G_DEBUG=fatal-criticals timeout 30s ./tests/test_dialog_geometry_gtk
G_DEBUG=fatal-criticals timeout 30s ./tests/test_person_factual_relation_editor_gtk
G_DEBUG=fatal-criticals timeout 30s ./tests/test_document_authenticity_editor_gtk
G_DEBUG=fatal-criticals timeout 30s ./tests/test_person_ocr_projection_editor_gtk
```
Les tests emploient uniquement des documents `SPECIMEN`, des fichiers

View file

@ -1,7 +1,7 @@
/******************************************************************************
* Labfy Investigation
*
* Extensions idempotentes du schéma SQLite courant V9
* Extensions idempotentes du schéma SQLite courant V20
******************************************************************************/
/*
@ -436,3 +436,64 @@ BEFORE UPDATE ON identity_field_observations BEGIN
WHEN NEW.confirmation_state='human_confirmed' AND NEW.confirmed_value IS NULL
THEN RAISE(ABORT,'confirmed value required') END;
END;
CREATE TABLE IF NOT EXISTS person_profile_fields(
person_id TEXT NOT NULL,field_code TEXT NOT NULL,value TEXT NOT NULL,
updated_at TEXT NOT NULL CHECK(length(updated_at)=20),
PRIMARY KEY(person_id,field_code),
CHECK(field_code IN('declared_name','surname','given_names','birth_date',
'birth_place','nationality','sex_as_printed','address_as_printed')),
CHECK(length(trim(value))>0),
FOREIGN KEY(person_id) REFERENCES entites(id) ON DELETE CASCADE);
CREATE TABLE IF NOT EXISTS person_ocr_field_projections(
id TEXT PRIMARY KEY,person_id TEXT NOT NULL,person_field_code TEXT NOT NULL,
previous_value TEXT,new_value TEXT NOT NULL,evidence_id TEXT NOT NULL,
ocr_run_id TEXT NOT NULL,ocr_field_id TEXT NOT NULL,ocr_field_code TEXT NOT NULL,
value_quality TEXT NOT NULL CHECK(value_quality IN('complete','partial')),
review_status TEXT NOT NULL CHECK(review_status IN('accepted','modified')),
strategy TEXT NOT NULL CHECK(strategy IN('fill_empty','replace_existing')),
projected_at TEXT NOT NULL CHECK(length(projected_at)=20),
origin TEXT NOT NULL CHECK(origin='human'),
FOREIGN KEY(person_id) REFERENCES entites(id) ON DELETE RESTRICT,
FOREIGN KEY(evidence_id) REFERENCES preuves(id) ON DELETE RESTRICT,
FOREIGN KEY(ocr_run_id) REFERENCES identity_ocr_runs(id) ON DELETE RESTRICT,
FOREIGN KEY(ocr_field_id) REFERENCES identity_field_observations(id)
ON DELETE RESTRICT);
CREATE INDEX IF NOT EXISTS idx_person_projection_person
ON person_ocr_field_projections(person_id,projected_at,id);
CREATE TRIGGER IF NOT EXISTS projection_v19_consistency
BEFORE INSERT ON person_ocr_field_projections BEGIN
SELECT CASE WHEN NOT EXISTS(SELECT 1 FROM identity_field_observations f
WHERE f.id=NEW.ocr_field_id AND f.ocr_run_id=NEW.ocr_run_id
AND f.evidence_id=NEW.evidence_id AND f.field_code=NEW.ocr_field_code
AND f.confirmation_state='human_confirmed'
AND length(trim(f.confirmed_value))>0
AND f.review_status IN('accepted','modified')
AND f.value_quality IN('complete','partial')
AND f.confirmed_value=NEW.new_value)
THEN RAISE(ABORT,'OCR field is no longer projectable') END;
END;
/* V20 — évaluations humaines distinctes de l’usage abusif d’identité. */
CREATE TABLE IF NOT EXISTS document_identity_misuse_assessments(
id TEXT PRIMARY KEY,evidence_id TEXT NOT NULL,ocr_run_id TEXT,
status TEXT NOT NULL CHECK(status IN('indeterminate','presumed','confirmed')),
justification TEXT,assessed_at TEXT NOT NULL CHECK(length(assessed_at)=20),
previous_assessment_id TEXT,origin TEXT NOT NULL CHECK(origin='human'),
CHECK(status='indeterminate' OR length(trim(justification))>0),
FOREIGN KEY(evidence_id) REFERENCES preuves(id) ON DELETE RESTRICT,
FOREIGN KEY(ocr_run_id) REFERENCES identity_ocr_runs(id) ON DELETE RESTRICT,
FOREIGN KEY(previous_assessment_id) REFERENCES document_identity_misuse_assessments(id) ON DELETE RESTRICT);
CREATE INDEX IF NOT EXISTS idx_identity_misuse_evidence_history
ON document_identity_misuse_assessments(evidence_id,assessed_at,id);
CREATE UNIQUE INDEX IF NOT EXISTS idx_identity_misuse_previous
ON document_identity_misuse_assessments(previous_assessment_id)
WHERE previous_assessment_id IS NOT NULL;
CREATE TRIGGER IF NOT EXISTS identity_misuse_consistency BEFORE INSERT ON document_identity_misuse_assessments BEGIN
SELECT CASE WHEN NEW.ocr_run_id IS NOT NULL AND NOT EXISTS(SELECT 1 FROM identity_ocr_runs r WHERE r.id=NEW.ocr_run_id AND r.evidence_id=NEW.evidence_id) THEN RAISE(ABORT,'OCR run does not belong to evidence') END;
SELECT CASE WHEN NEW.previous_assessment_id IS NOT NULL AND NOT EXISTS(SELECT 1 FROM document_identity_misuse_assessments p WHERE p.id=NEW.previous_assessment_id AND p.evidence_id=NEW.evidence_id) THEN RAISE(ABORT,'previous assessment does not belong to evidence') END;
SELECT CASE WHEN EXISTS(SELECT 1 FROM document_identity_misuse_assessments p WHERE p.evidence_id=NEW.evidence_id) AND NEW.previous_assessment_id IS NULL THEN RAISE(ABORT,'previous assessment is required') END;
SELECT CASE WHEN NEW.previous_assessment_id IS NOT NULL AND EXISTS(SELECT 1 FROM document_identity_misuse_assessments n WHERE n.previous_assessment_id=NEW.previous_assessment_id) THEN RAISE(ABORT,'previous assessment is not current') END;
END;
CREATE TRIGGER IF NOT EXISTS identity_misuse_append_only_update BEFORE UPDATE ON document_identity_misuse_assessments BEGIN SELECT RAISE(ABORT,'identity misuse history is append-only'); END;
CREATE TRIGGER IF NOT EXISTS identity_misuse_append_only_delete BEFORE DELETE ON document_identity_misuse_assessments BEGIN SELECT RAISE(ABORT,'identity misuse history is append-only'); END;

44
database/schema_v20.sql Normal file
View file

@ -0,0 +1,44 @@
/* V20 — évaluations humaines distinctes de l’usage abusif d’identité. */
CREATE TABLE document_identity_misuse_assessments(
id TEXT PRIMARY KEY,
evidence_id TEXT NOT NULL,
ocr_run_id TEXT,
status TEXT NOT NULL CHECK(status IN('indeterminate','presumed','confirmed')),
justification TEXT,
assessed_at TEXT NOT NULL CHECK(length(assessed_at)=20),
previous_assessment_id TEXT,
origin TEXT NOT NULL CHECK(origin='human'),
CHECK(status='indeterminate' OR length(trim(justification))>0),
FOREIGN KEY(evidence_id) REFERENCES preuves(id) ON DELETE RESTRICT,
FOREIGN KEY(ocr_run_id) REFERENCES identity_ocr_runs(id) ON DELETE RESTRICT,
FOREIGN KEY(previous_assessment_id)
REFERENCES document_identity_misuse_assessments(id) ON DELETE RESTRICT);
CREATE INDEX idx_identity_misuse_evidence_history
ON document_identity_misuse_assessments(evidence_id,assessed_at,id);
CREATE UNIQUE INDEX idx_identity_misuse_previous
ON document_identity_misuse_assessments(previous_assessment_id)
WHERE previous_assessment_id IS NOT NULL;
CREATE TRIGGER identity_misuse_consistency BEFORE INSERT
ON document_identity_misuse_assessments BEGIN
SELECT CASE WHEN NEW.ocr_run_id IS NOT NULL AND NOT EXISTS(
SELECT 1 FROM identity_ocr_runs r
WHERE r.id=NEW.ocr_run_id AND r.evidence_id=NEW.evidence_id)
THEN RAISE(ABORT,'OCR run does not belong to evidence') END;
SELECT CASE WHEN NEW.previous_assessment_id IS NOT NULL AND NOT EXISTS(
SELECT 1 FROM document_identity_misuse_assessments p
WHERE p.id=NEW.previous_assessment_id AND p.evidence_id=NEW.evidence_id)
THEN RAISE(ABORT,'previous assessment does not belong to evidence') END;
SELECT CASE WHEN EXISTS(SELECT 1 FROM document_identity_misuse_assessments p
WHERE p.evidence_id=NEW.evidence_id) AND NEW.previous_assessment_id IS NULL
THEN RAISE(ABORT,'previous assessment is required') END;
SELECT CASE WHEN NEW.previous_assessment_id IS NOT NULL AND EXISTS(
SELECT 1 FROM document_identity_misuse_assessments n
WHERE n.previous_assessment_id=NEW.previous_assessment_id)
THEN RAISE(ABORT,'previous assessment is not current') END;
END;
CREATE TRIGGER identity_misuse_append_only_update BEFORE UPDATE
ON document_identity_misuse_assessments BEGIN
SELECT RAISE(ABORT,'identity misuse history is append-only'); END;
CREATE TRIGGER identity_misuse_append_only_delete BEFORE DELETE
ON document_identity_misuse_assessments BEGIN
SELECT RAISE(ABORT,'identity misuse history is append-only'); END;

View file

@ -1,20 +0,0 @@
Makefile | 2 +
include/core/person_creation_task.h | 3 +-
include/dao/identity_traceability_dao.h | 2 +
include/views/create_person_dialog.h | 2 +
include/views/main_window.h | 7 ++++
include/views/person_vocabulary_adapter.h | 3 ++
include/widgets/entity_details_panel.h | 2 +
include/widgets/workspace.h | 4 +-
src/core/application.c | 20 ++++++++--
src/core/person_creation_task.c | 45 +++++++++++++++++++---
src/dao/identity_traceability_dao.c | 26 +++++++++++++
src/views/create_person_dialog.c | 63 +++++++++++++++++++++++++++----
src/views/evidence_metadata_dialog.c | 52 +++++++++++++++++++++++++
src/views/main_window.c | 8 +++-
src/views/person_vocabulary_adapter.c | 39 +++++++++++++++++++
src/widgets/entity_details_panel.c | 56 +++++++++++++++++++++++++++
src/widgets/workspace.c | 7 ++++
tests/test_create_person_dialog_gtk.c | 3 ++
tests/test_create_person_dialog_ocr_gtk.c | 2 +
19 files changed, 327 insertions(+), 19 deletions(-)

View file

@ -30,7 +30,7 @@ pause, retour au début, détachement et libération via des actions injectées.
> **Version :** 3.2
> **Dernière mise à jour :** 2026-07-30
> **Schéma SQLite courant :** V18
> **Schéma SQLite courant :** V20
## Personnes contextuelles — SQLite V14
@ -134,6 +134,21 @@ service relit le champ V18 avant COMMIT et conserve la valeur existante par
défaut en cas de conflit. La fiche personne relit ces champs par
`PersonDetailsProvider` et le DAO de projection.
`CreatePersonDialog` sépare sept pages : personne, rôles, preuves, révision
OCR, projection OCR, relations factuelles et confirmation. La page de
projection contient uniquement `PersonOcrProjectionEditor`; la page suivante
contient uniquement `PersonFactualRelationEditor`. Le résumé distingue la
correction OCR persistée, les projections choisies et les relations préparées.
La V20 ajoute `document_identity_misuse_assessments`, domaine distinct de
l’authenticité documentaire. Ses états `indeterminate`, `presumed` et
`confirmed` sont exclusivement humains, chaînés et append-only. La fiche de
preuve affiche séparément leur origine et leur justification.
La collecte des justifications des rôles sensibles et l’alimentation plus
complète de `person_identification_assessments` restent des renforcements de
cohérence, non des exigences contractuelles explicites du ticket #109.
---
## 1. Objectif
@ -430,7 +445,7 @@ validation du chemin
↓
création de l'arborescence
↓
initialisation transactionnelle de SQLite V18
initialisation transactionnelle de SQLite V20
↓
création de l'identité de l'enquête
↓
@ -879,7 +894,8 @@ Validation :
```sh
make clean
make -j8
make -j8 test
make check-source-size
DISPLAY="$DISPLAY" WAYLAND_DISPLAY="$WAYLAND_DISPLAY" make -j8 test
git diff --check
```

View file

@ -25,6 +25,10 @@ La projection OCR se valide avec `test_person_ocr_projection`,
`test_person_ocr_projection_editor_gtk`, ce dernier sous affichage réel et
`G_DEBUG=fatal-criticals`.
L’assistant complet se valide également avec
`tests/test_create_person_dialog_gtk` : il vérifie l’ordre des sept pages,
leur séparation et les trois rubriques du résumé final.
La validation ciblée de l’aperçu partagé comprend
`test_evidence_preview_widget_gtk`, `test_evidence_preview`,
`test_evidence_video_preview_controller` et
@ -306,6 +310,9 @@ make -j8 \
tests/test_workspace_identity_ocr_gtk \
tests/test_evidence_preview_widget_gtk \
tests/test_dialog_geometry_gtk
tests/test_person_factual_relation_editor_gtk \
tests/test_document_authenticity_editor_gtk \
tests/test_person_ocr_projection_editor_gtk
G_DEBUG=fatal-criticals timeout 30s ./tests/test_create_person_dialog_ocr_gtk
G_DEBUG=fatal-criticals timeout 30s ./tests/test_evidence_identity_import_gtk
@ -313,6 +320,9 @@ G_DEBUG=fatal-criticals timeout 30s ./tests/test_evidence_metadata_dialog_gtk
G_DEBUG=fatal-criticals timeout 30s ./tests/test_workspace_identity_ocr_gtk
G_DEBUG=fatal-criticals timeout 30s ./tests/test_evidence_preview_widget_gtk
G_DEBUG=fatal-criticals timeout 30s ./tests/test_dialog_geometry_gtk
G_DEBUG=fatal-criticals timeout 30s ./tests/test_person_factual_relation_editor_gtk
G_DEBUG=fatal-criticals timeout 30s ./tests/test_document_authenticity_editor_gtk
G_DEBUG=fatal-criticals timeout 30s ./tests/test_person_ocr_projection_editor_gtk
```
Ils doivent cliquer sur les contrôles de production, passer par `MainWindow`
@ -655,10 +665,19 @@ Les tests doivent utiliser :
Avant chaque commit :
```sh
make clean
make -j8
make check-source-size
DISPLAY="$DISPLAY" WAYLAND_DISPLAY="$WAYLAND_DISPLAY" make -j8 test
git diff --check
git status --short
```
Les validations ASan/UBSan ciblées produisent leurs binaires dans `/tmp`.
Lorsque `ASAN_OPTIONS=detect_leaks=0` est nécessaire sous l’environnement GTK
tracé, cela valide AddressSanitizer et UndefinedBehaviorSanitizer, pas
LeakSanitizer.
Inspecter tout nouveau fichier inhabituel à la racine du dépôt.
---
@ -729,5 +748,10 @@ Utiliser exclusivement une enquête, une base SQLite et des documents
6. dialogues à 1200 × 800 et 760 × 560 : parent transitoire, formulaire
défilable, répartition 2/3–1/3 et actions toujours visibles.
La validation manuelle réussie confirme ces parcours, mais ne clôt pas le
ticket #109 et ne couvre pas les limitations encore listées dans la roadmap.
Contrôler aussi les sept étapes de l’assistant, les trois rubriques du résumé,
la projection vide par défaut et l’absence de relation automatique.
La validation manuelle doit également couvrir la section V20 distincte de
l’authenticité. La justification des rôles sensibles et l’historisation
complète de l’identification sont des renforcements de cohérence, pas des
critères d’acceptation explicites du ticket #109.

View file

@ -15,9 +15,9 @@ formulaire défilable, un aperçu redimensionnable et des actions fixes.
La validation manuelle de ces parcours est réussie. Elle ne vaut pas
achèvement du ticket #109.
> **Dernière mise à jour :** 2026-07-30
> **Dernière mise à jour :** 2026-08-02
> **État du projet :** développement actif
> **Schéma SQLite courant :** V18
> **Schéma SQLite courant :** V20
> **Usage opérationnel :** non prêt pour la production
---
@ -131,7 +131,7 @@ La branche `main` contient notamment :
- création, validation et ouverture d'enquêtes ;
- session d'enquête remplaçable proprement ;
- infrastructure SQLite et migrations jusqu'à V18 ;
- infrastructure SQLite et migrations jusqu'à V20 ;
- couche Database, DAO et services métier ;
- import de preuves avec copie contrôlée et SHA-256 ;
- vérification d'intégrité et reclassement des preuves ;
@ -185,13 +185,15 @@ PARTIEL — VALIDATION MANUELLE DES TRANCHES LIVRÉES RÉUSSIE
Limitations restantes :
1. l’OCR groupé de plusieurs preuves dans une même opération n’est pas pris en
charge ;
2. les interfaces GTK complètes des états d’identification et des rôles V18
restent à construire ;
3. la provenance uniforme de tous les autres dérivés reste partielle ;
1. la justification des rôles sensibles et l’alimentation complète de
`person_identification_assessments` restent des renforcements de cohérence,
sans être des exigences explicites du ticket #109 ;
2. l’OCR groupé n’est volontairement pas pris en charge : après un import
multiple, chaque preuve est analysée individuellement ;
3. la provenance uniforme des dérivés hors parcours OCR reste partielle.
Ces limites interdisent de présenter le ticket #109 comme terminé.
La V20 couvre distinctement les états contractuels d’usage d’identité présumé
et confirmé. Une validation manuelle finale reste requise avant clôture.
### Ticket #107 — Pivot e-mail forensique

View file

@ -26,9 +26,12 @@ dernière table conserve personne, cible, valeurs précédente et nouvelle,
preuve, OcrRun, champ OCR, qualité, stratégie, date et origine humaine. Un
trigger refuse une source devenue non projectable.
> **Statut :** architecture courante
> **Version du schéma :** V19
> **Dernière mise à jour :** 2026-07-24
> **Statut :** architecture courante
V20 ajoute l’historique humain append-only
`document_identity_misuse_assessments`, distinct de l’authenticité.
> **Version du schéma :** V20
> **Dernière mise à jour :** 2026-08-02
> **Source de vérité détaillée :** `SCHEMA_AUDIT_CURRENT.md`
---
@ -71,7 +74,7 @@ docs/database/SCHEMA_AUDIT_CURRENT.md
L'état réel du schéma est déterminé par :
1. les constantes de version dans le code ;
2. `database/schema_v1.sql` à `database/schema_v10.sql` ;
2. `database/schema_v1.sql` à `database/schema_v19.sql` ;
3. `database/schema_current.sql` ;
4. les fonctions d'installation et de migration ;
5. `tests/test_database.c` et les tests DAO ;
@ -242,6 +245,7 @@ database/schema_v7.sql
database/schema_v8.sql
database/schema_v9.sql
database/schema_v10.sql
database/schema_v11.sql à database/schema_v19.sql
```
`database/schema_current.sql` contient des extensions ou réparations
@ -264,7 +268,7 @@ PRAGMA foreign_keys = ON
↓
BEGIN
↓
installation V1 à V10
installation du socle historique puis du schéma courant V20
↓
application du schéma courant idempotent
↓
@ -278,7 +282,7 @@ Un échec provoque un rollback.
Une base neuve doit aboutir directement à :
```text
schema_version = 10
schema_version = 19
```
---

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,16 @@
#ifndef LABFY_DOCUMENT_IDENTITY_MISUSE_SERVICE_H
#define LABFY_DOCUMENT_IDENTITY_MISUSE_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;} DocumentIdentityMisuseStatus;
const DocumentIdentityMisuseStatus *document_identity_misuse_service_statuses(gsize *count);
const DocumentIdentityMisuseStatus *document_identity_misuse_service_status(const char *code);
GPtrArray *document_identity_misuse_service_list_runs(Database *database,const char *evidence_identifier,GError **error);
GPtrArray *document_identity_misuse_service_history(Database *database,const char *evidence_identifier,GError **error);
gboolean document_identity_misuse_service_add(Database *database,const char *evidence_identifier,
const char *status,const char *justification,const char *ocr_run_identifier,
DocumentIdentityMisuseAssessment **created,GError **error);
G_END_DECLS
#endif

View file

@ -16,6 +16,13 @@ GPtrArray *identity_traceability_dao_list_authenticity(
IdentityTraceabilityDao *dao,const char *evidence_identifier,GError **error);
DocumentAuthenticityAssessment *identity_traceability_dao_current_authenticity(
IdentityTraceabilityDao *dao,const char *evidence_identifier,GError **error);
gboolean identity_traceability_dao_insert_identity_misuse(
IdentityTraceabilityDao *dao,const DocumentIdentityMisuseAssessment *assessment,
GError **error);
GPtrArray *identity_traceability_dao_list_identity_misuse(
IdentityTraceabilityDao *dao,const char *evidence_identifier,GError **error);
DocumentIdentityMisuseAssessment *identity_traceability_dao_current_identity_misuse(
IdentityTraceabilityDao *dao,const char *evidence_identifier,GError **error);
gboolean identity_traceability_dao_insert_factual_relation(
IdentityTraceabilityDao *dao,const PersonEvidenceFactualRelation *relation,
GError **error);

View file

@ -100,6 +100,7 @@ bool schema_install_v16(Database *database);
bool schema_install_v17(Database *database);
bool schema_install_v18(Database *database);
bool schema_install_v19(Database *database);
bool schema_install_v20(Database *database);
/**
* @brief Garantit la présence des extensions du schéma courant V2.

View file

@ -9,6 +9,12 @@ typedef struct {
char *previous_identifier, *technical_note, *origin;
} DocumentAuthenticityAssessment;
typedef struct {
char *identifier, *evidence_identifier, *ocr_run_identifier;
char *status, *justification, *assessed_at;
char *previous_identifier, *origin;
} DocumentIdentityMisuseAssessment;
typedef struct {
char *identifier, *person_identifier, *evidence_identifier;
char *ocr_run_identifier, *relation_type, *factual_note;
@ -25,6 +31,7 @@ typedef struct {
typedef PersonRoleVocabularyEntry IdentificationStatusVocabularyEntry;
gboolean identity_traceability_authenticity_status_valid(const char *status);
gboolean identity_traceability_identity_misuse_status_valid(const char *status);
gboolean identity_traceability_relation_type_valid(const char *type);
gboolean identity_traceability_identification_status_valid(const char *status);
gboolean identity_traceability_value_quality_valid(const char *quality);
@ -51,6 +58,24 @@ const char *document_authenticity_assessment_get_previous_identifier(const Docum
const char *document_authenticity_assessment_get_technical_note(const DocumentAuthenticityAssessment *assessment);
const char *document_authenticity_assessment_get_origin(const DocumentAuthenticityAssessment *assessment);
DocumentIdentityMisuseAssessment *document_identity_misuse_assessment_new(
const char *identifier, const char *evidence_identifier,
const char *ocr_run_identifier, const char *status,
const char *justification, const char *assessed_at,
const char *previous_identifier);
DocumentIdentityMisuseAssessment *document_identity_misuse_assessment_copy(
const DocumentIdentityMisuseAssessment *assessment);
void document_identity_misuse_assessment_free(
DocumentIdentityMisuseAssessment *assessment);
const char *document_identity_misuse_assessment_get_identifier(const DocumentIdentityMisuseAssessment *assessment);
const char *document_identity_misuse_assessment_get_evidence_identifier(const DocumentIdentityMisuseAssessment *assessment);
const char *document_identity_misuse_assessment_get_ocr_run_identifier(const DocumentIdentityMisuseAssessment *assessment);
const char *document_identity_misuse_assessment_get_status(const DocumentIdentityMisuseAssessment *assessment);
const char *document_identity_misuse_assessment_get_justification(const DocumentIdentityMisuseAssessment *assessment);
const char *document_identity_misuse_assessment_get_assessed_at(const DocumentIdentityMisuseAssessment *assessment);
const char *document_identity_misuse_assessment_get_previous_identifier(const DocumentIdentityMisuseAssessment *assessment);
const char *document_identity_misuse_assessment_get_origin(const DocumentIdentityMisuseAssessment *assessment);
PersonEvidenceFactualRelation *person_evidence_factual_relation_new(
const char *identifier, const char *person_identifier,
const char *evidence_identifier, const char *ocr_run_identifier,

View file

@ -0,0 +1,12 @@
#ifndef LABFY_DOCUMENT_IDENTITY_MISUSE_EDITOR_H
#define LABFY_DOCUMENT_IDENTITY_MISUSE_EDITOR_H
#include "database/database.h"
#include <gtk/gtk.h>
G_BEGIN_DECLS
typedef struct DocumentIdentityMisuseEditor DocumentIdentityMisuseEditor;
DocumentIdentityMisuseEditor *document_identity_misuse_editor_new(Database *database,const char *evidence_identifier);
GtkWidget *document_identity_misuse_editor_get_widget(DocumentIdentityMisuseEditor *editor);
gboolean document_identity_misuse_editor_refresh(DocumentIdentityMisuseEditor *editor,GError **error);
void document_identity_misuse_editor_free(DocumentIdentityMisuseEditor *editor);
G_END_DECLS
#endif

View file

@ -1,19 +0,0 @@
M Makefile
M include/core/person_creation_task.h
M include/dao/identity_traceability_dao.h
M include/views/create_person_dialog.h
M include/views/main_window.h
M include/views/person_vocabulary_adapter.h
M include/widgets/entity_details_panel.h
M include/widgets/workspace.h
M src/core/application.c
M src/core/person_creation_task.c
M src/dao/identity_traceability_dao.c
M src/views/create_person_dialog.c
M src/views/evidence_metadata_dialog.c
M src/views/main_window.c
M src/views/person_vocabulary_adapter.c
M src/widgets/entity_details_panel.c
M src/widgets/workspace.c
M tests/test_create_person_dialog_gtk.c
M tests/test_create_person_dialog_ocr_gtk.c

View file

@ -0,0 +1,40 @@
#include "core/document_identity_misuse_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-identity-misuse-service");}
static const DocumentIdentityMisuseStatus statuses[]={
{"indeterminate","Hypothèse d’usurpation indéterminée","Aucune conclusion humaine sur un usage abusif d’identité.",FALSE},
{"presumed","Document présumé utilisé dans une usurpation d’identité","Hypothèse humaine prudente nécessitant une justification.",TRUE},
{"confirmed","Utilisation dans une usurpation d’identité confirmée","Conclusion humaine documentée, jamais déduite automatiquement.",TRUE}};
const DocumentIdentityMisuseStatus *document_identity_misuse_service_statuses(gsize*c)
{if(c)*c=G_N_ELEMENTS(statuses);return statuses;}
const DocumentIdentityMisuseStatus *document_identity_misuse_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_identity_misuse_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_identity_misuse_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_identity_misuse(dao,e,error);identity_traceability_dao_free(dao);return a;}
gboolean document_identity_misuse_service_add(Database*d,const char*evidence,
const char*status,const char*justification,const char*run,
DocumentIdentityMisuseAssessment**created,GError**error)
{
if(created)*created=NULL;
const DocumentIdentityMisuseStatus*entry=
document_identity_misuse_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 évaluation.");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’usage d’identité.");return FALSE;}
DocumentIdentityMisuseAssessment*previous=identity_traceability_dao_current_identity_misuse(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");
DocumentIdentityMisuseAssessment*a=document_identity_misuse_assessment_new(id,evidence,run,status,j&&*j?j:NULL,at,
previous?document_identity_misuse_assessment_get_identifier(previous):NULL);
gboolean ok=a&&identity_traceability_dao_insert_identity_misuse(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’évaluation d’usage d’identité.");
if(ok&&created)*created=document_identity_misuse_assessment_copy(a);else if(local)g_propagate_error(error,local);
document_identity_misuse_assessment_free(a);document_identity_misuse_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

@ -100,6 +100,83 @@ DocumentAuthenticityAssessment *identity_traceability_dao_current_authenticity(
document_authenticity_assessment_copy(g_ptr_array_index(a,0)):NULL;
g_ptr_array_unref(a);return r;
}
gboolean identity_traceability_dao_insert_identity_misuse(
IdentityTraceabilityDao*d,const DocumentIdentityMisuseAssessment*a,GError**e)
{
DocumentIdentityMisuseAssessment*valid=a?document_identity_misuse_assessment_copy(a):NULL;
if(!d||!valid){fail(e,"Évaluation d’usage d’identité invalide.");return FALSE;}
const char*run=document_identity_misuse_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_identity_misuse_assessment_get_evidence_identifier(valid))&&
database_statement_step(check)==DATABASE_STATEMENT_STEP_ROW;
database_statement_finalize(check);if(!compatible){
document_identity_misuse_assessment_free(valid);
fail(e,"L’exécution OCR ne correspond pas à la preuve évaluée.");return FALSE;}}
const char*previous=document_identity_misuse_assessment_get_previous_identifier(valid);
if(previous){DatabaseStatement*check=database_statement_prepare(d->database,
"SELECT 1 FROM document_identity_misuse_assessments WHERE id=? AND evidence_id=?;");
gboolean compatible=check&&bind(check,1,previous)&&bind(check,2,
document_identity_misuse_assessment_get_evidence_identifier(valid))&&
database_statement_step(check)==DATABASE_STATEMENT_STEP_ROW;
database_statement_finalize(check);if(!compatible){
document_identity_misuse_assessment_free(valid);
fail(e,"L’évaluation précédente ne correspond pas à la preuve.");return FALSE;}}
DatabaseStatement*s=database_statement_prepare(d->database,
"INSERT INTO document_identity_misuse_assessments VALUES(?,?,?,?,?,?,?,?);");
gboolean ok=s&&bind(s,1,document_identity_misuse_assessment_get_identifier(valid))&&
bind(s,2,document_identity_misuse_assessment_get_evidence_identifier(valid))&&
bind(s,3,run)&&bind(s,4,document_identity_misuse_assessment_get_status(valid))&&
bind(s,5,document_identity_misuse_assessment_get_justification(valid))&&
bind(s,6,document_identity_misuse_assessment_get_assessed_at(valid))&&
bind(s,7,previous)&&bind(s,8,"human")&&
database_statement_step(s)==DATABASE_STATEMENT_STEP_DONE;
database_statement_finalize(s);document_identity_misuse_assessment_free(valid);
if(!ok)fail(e,"Impossible de conserver l’évaluation d’usage d’identité.");
return ok;
}
static DocumentIdentityMisuseAssessment *read_misuse(DatabaseStatement*s)
{
char*v[8]={0};gboolean ok=TRUE;for(int i=0;i<8;i++)
ok=ok&&database_statement_column_text(s,i,&v[i]);
DocumentIdentityMisuseAssessment*a=ok?document_identity_misuse_assessment_new(
v[0],v[1],v[2],v[3],v[4],v[5],v[6]):NULL;
for(int i=0;i<8;i++)g_free(v[i]);
return a;
}
GPtrArray *identity_traceability_dao_list_identity_misuse(
IdentityTraceabilityDao*d,const char*evidence,GError**e)
{
if(!d||!evidence){fail(e,"Lecture de l’usage d’identité invalide.");return NULL;}
DatabaseStatement*s=database_statement_prepare(d->database,
"SELECT id,evidence_id,ocr_run_id,status,justification,assessed_at,"
"previous_assessment_id,origin FROM document_identity_misuse_assessments "
"WHERE evidence_id=? ORDER BY assessed_at,id;");
GPtrArray*a=g_ptr_array_new_with_free_func((GDestroyNotify)
document_identity_misuse_assessment_free);if(!s||!bind(s,1,evidence))goto bad_misuse;
for(;;){DatabaseStatementStepResult step=database_statement_step(s);
if(step==DATABASE_STATEMENT_STEP_DONE)break;
if(step!=DATABASE_STATEMENT_STEP_ROW)goto bad_misuse;
DocumentIdentityMisuseAssessment*r=read_misuse(s);if(!r)goto bad_misuse;g_ptr_array_add(a,r);}
database_statement_finalize(s);return a;
bad_misuse:database_statement_finalize(s);g_ptr_array_unref(a);
fail(e,"Impossible de lire l’historique d’usage d’identité.");return NULL;
}
DocumentIdentityMisuseAssessment *identity_traceability_dao_current_identity_misuse(
IdentityTraceabilityDao*d,const char*evidence,GError**e)
{
GPtrArray*a=identity_traceability_dao_list_identity_misuse(d,evidence,e);if(!a)return NULL;
DocumentIdentityMisuseAssessment*current=NULL;
for(guint i=0;i<a->len;i++){DocumentIdentityMisuseAssessment*c=g_ptr_array_index(a,i);
gboolean has_next=FALSE;for(guint j=0;j<a->len;j++){
DocumentIdentityMisuseAssessment*n=g_ptr_array_index(a,j);
if(g_strcmp0(document_identity_misuse_assessment_get_previous_identifier(n),
document_identity_misuse_assessment_get_identifier(c))==0){has_next=TRUE;break;}}
if(!has_next)current=c;}
DocumentIdentityMisuseAssessment*r=document_identity_misuse_assessment_copy(current);
g_ptr_array_unref(a);return r;
}
gboolean identity_traceability_dao_insert_factual_relation(
IdentityTraceabilityDao*d,const PersonEvidenceFactualRelation*r,GError**e)
{

View file

@ -16,12 +16,12 @@
/**
* @brief Version actuelle du schéma SQLite.
*/
#define DATABASE_SCHEMA_VERSION_CURRENT 19
#define DATABASE_SCHEMA_VERSION_CURRENT 20
/**
* @brief Version actuelle sous forme textuelle pour metadata.
*/
#define DATABASE_SCHEMA_VERSION_CURRENT_TEXT "19"
#define DATABASE_SCHEMA_VERSION_CURRENT_TEXT "20"
/**
* @brief Nom de l'application enregistré dans les métadonnées.
@ -947,6 +947,21 @@ rollback:
return false;
}
static bool database_migrate_v19_to_v20(Database *database)
{
bool transaction_started = false;
if (database == NULL || !database_transaction_begin(database)) return false;
transaction_started = true;
if (!schema_install_v20(database) ||
!database_update_schema_version(database, "20") ||
!database_transaction_commit(database)) goto rollback;
return true;
rollback:
if (transaction_started && !database_transaction_rollback(database))
g_warning("Impossible d’annuler la migration SQLite V19 vers V20.");
return false;
}
/**
* @brief Garantit atomiquement la présence des extensions du schéma courant.
*/
@ -1212,6 +1227,10 @@ bool database_migrate_to_latest(
if (!database_migrate_v18_to_v19(database)) return false;
schema_version = 19;
break;
case 19:
if (!database_migrate_v19_to_v20(database)) return false;
schema_version = 20;
break;
default:
database_set_error(
@ -1354,8 +1373,7 @@ bool database_initialize(
if (!schema_ensure_current(
database
) ||
!schema_install_v19(database))
))
{
goto rollback;
}

View file

@ -439,6 +439,12 @@ bool schema_install_v19(Database *database)
"la migration SQLite V19");
}
bool schema_install_v20(Database *database)
{
return schema_execute_file(database, "database/schema_v20.sql",
"la migration SQLite V20");
}
bool schema_ensure_current(
Database *database
)

View file

@ -24,6 +24,11 @@ gboolean identity_traceability_authenticity_status_valid(const char *value)
"suspicious","presumed_forged","confirmed_forged"};
return in_values(value,values,G_N_ELEMENTS(values));
}
gboolean identity_traceability_identity_misuse_status_valid(const char *value)
{
static const char *const values[]={"indeterminate","presumed","confirmed"};
return in_values(value,values,G_N_ELEMENTS(values));
}
gboolean identity_traceability_relation_type_valid(const char *value)
{
static const char *const values[]={"identity_observed_in",
@ -90,6 +95,42 @@ AUTH_GETTER(previous_identifier,previous_identifier)
AUTH_GETTER(technical_note,technical_note)
AUTH_GETTER(origin,origin)
DocumentIdentityMisuseAssessment *document_identity_misuse_assessment_new(
const char *id,const char *evidence,const char *run,const char *status,
const char *justification,const char *at,const char *previous)
{
if(!g_uuid_string_is_valid(id)||!g_uuid_string_is_valid(evidence)||
!uuid_or_null(run)||!uuid_or_null(previous)||
!identity_traceability_identity_misuse_status_valid(status)||
!timestamp(at)||!optional_text(justification)||
(g_strcmp0(status,"indeterminate")!=0&&!text(justification)))return NULL;
DocumentIdentityMisuseAssessment*a=g_new0(DocumentIdentityMisuseAssessment,1);
a->identifier=g_strdup(id);a->evidence_identifier=g_strdup(evidence);
a->ocr_run_identifier=g_strdup(run);a->status=g_strdup(status);
a->justification=g_strdup(justification);a->assessed_at=g_strdup(at);
a->previous_identifier=g_strdup(previous);a->origin=g_strdup("human");return a;
}
DocumentIdentityMisuseAssessment *document_identity_misuse_assessment_copy(
const DocumentIdentityMisuseAssessment*a)
{return a?document_identity_misuse_assessment_new(a->identifier,
a->evidence_identifier,a->ocr_run_identifier,a->status,a->justification,
a->assessed_at,a->previous_identifier):NULL;}
void document_identity_misuse_assessment_free(DocumentIdentityMisuseAssessment*a)
{if(!a)return;g_free(a->identifier);g_free(a->evidence_identifier);
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->origin);g_free(a);}
#define MISUSE_GETTER(name,field) \
const char *document_identity_misuse_assessment_get_##name( \
const DocumentIdentityMisuseAssessment*a){return a?a->field:NULL;}
MISUSE_GETTER(identifier,identifier)
MISUSE_GETTER(evidence_identifier,evidence_identifier)
MISUSE_GETTER(ocr_run_identifier,ocr_run_identifier)
MISUSE_GETTER(status,status)
MISUSE_GETTER(justification,justification)
MISUSE_GETTER(assessed_at,assessed_at)
MISUSE_GETTER(previous_identifier,previous_identifier)
MISUSE_GETTER(origin,origin)
PersonEvidenceFactualRelation *person_evidence_factual_relation_new(
const char *id,const char *person,const char *evidence,const char *run,
const char *type,const char *note,const char *at,gboolean active)

View file

@ -0,0 +1,49 @@
#include "views/document_identity_misuse_editor.h"
#include "core/document_identity_misuse_service.h"
#include "dao/identity_ocr_dao.h"
struct DocumentIdentityMisuseEditor{Database*database;char*evidence;GtkWidget*root;
GtkDropDown*status;GtkStringList*status_labels;GtkDropDown*run;GtkStringList*run_labels;
GPtrArray*run_ids;GtkTextBuffer*justification;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_identity_misuse_editor_refresh(DocumentIdentityMisuseEditor*x,GError**error)
{
if(!x)return FALSE;
clear_box(x->history);GPtrArray*h=document_identity_misuse_service_history(x->database,x->evidence,error);if(!h)return FALSE;
if(h->len==0)gtk_box_append(x->history,gtk_label_new("Aucune évaluation humaine enregistrée."));
for(gint i=(gint)h->len-1;i>=0;i--){DocumentIdentityMisuseAssessment*a=g_ptr_array_index(h,(guint)i);
const DocumentIdentityMisuseStatus*s=document_identity_misuse_service_status(document_identity_misuse_assessment_get_status(a));
const char*j=document_identity_misuse_assessment_get_justification(a),*r=document_identity_misuse_assessment_get_ocr_run_identifier(a);
char*t=g_strdup_printf("%s — %s\nOrigine : humaine%s%s\nJustification : %s",
s?s->label:"Statut inconnu",document_identity_misuse_assessment_get_assessed_at(a),
r?" — OcrRun : ":"",r?r:"",j&&*j?j:"Aucune (statut indéterminé)");
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;DocumentIdentityMisuseEditor*x=data;guint si=gtk_drop_down_get_selected(x->status),ri=gtk_drop_down_get_selected(x->run);gsize count=0;
const DocumentIdentityMisuseStatus*s=document_identity_misuse_service_statuses(&count);if(si>=count)return;
char*j=buffer_text(x->justification);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_identity_misuse_service_add(x->database,x->evidence,s[si].code,j,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);document_identity_misuse_editor_refresh(x,NULL);}g_free(j);
}
DocumentIdentityMisuseEditor *document_identity_misuse_editor_new(Database*d,const char*evidence)
{
if(!d||!g_uuid_string_is_valid(evidence))return NULL;
DocumentIdentityMisuseEditor*x=g_new0(DocumentIdentityMisuseEditor,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-identity-misuse-editor");
GtkWidget*title=gtk_label_new("Usage de l’identité du document — évaluation humaine distincte");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 DocumentIdentityMisuseStatus*s=document_identity_misuse_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),"identity-misuse-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_identity_misuse_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),"identity-misuse-ocr-run");gtk_box_append(GTK_BOX(x->root),GTK_WIDGET(x->run));
gtk_box_append(GTK_BOX(x->root),gtk_label_new("Justification factuelle"));GtkWidget*jv=gtk_text_view_new();x->justification=gtk_text_view_get_buffer(GTK_TEXT_VIEW(jv));gtk_widget_set_name(jv,"identity-misuse-justification");gtk_widget_set_size_request(jv,-1,60);gtk_box_append(GTK_BOX(x->root),jv);
x->error=GTK_LABEL(gtk_label_new(""));gtk_widget_set_name(GTK_WIDGET(x->error),"identity-misuse-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’évaluation humaine");gtk_widget_set_name(button,"identity-misuse-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),"identity-misuse-history");gtk_box_append(GTK_BOX(x->root),GTK_WIDGET(x->history));document_identity_misuse_editor_refresh(x,NULL);return x;
}
GtkWidget *document_identity_misuse_editor_get_widget(DocumentIdentityMisuseEditor*x){return x?x->root:NULL;}
void document_identity_misuse_editor_free(DocumentIdentityMisuseEditor*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

@ -13,6 +13,7 @@
#include "views/evidence_identity_ocr_dialog.h"
#include "views/person_vocabulary_adapter.h"
#include "views/document_authenticity_editor.h"
#include "views/document_identity_misuse_editor.h"
#include "widgets/ocr_provenance_overlay.h"
struct EvidenceMetadataDialogResult
@ -50,6 +51,7 @@ typedef struct
gpointer user_data;
gboolean completed;
DocumentAuthenticityEditor *authenticity_editor;
DocumentIdentityMisuseEditor *identity_misuse_editor;
} EvidenceMetadataDialogContext;
static void evidence_metadata_dialog_complete(
@ -92,6 +94,7 @@ static void evidence_metadata_dialog_context_free(gpointer data)
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);
document_identity_misuse_editor_free(context->identity_misuse_editor);
g_free(context->investigation_root);
g_free(context->evidence_identifier);
g_free(context);
@ -554,6 +557,16 @@ gboolean evidence_metadata_dialog_present_with_ocr(
document_authenticity_editor_get_widget(context->authenticity_editor));
gtk_box_append(GTK_BOX(main_box), auth_scroll);
}
context->identity_misuse_editor = document_identity_misuse_editor_new(
database, evidence_record_get_identifier(record));
if (context->identity_misuse_editor != NULL) {
GtkWidget *misuse_scroll = gtk_scrolled_window_new();
gtk_widget_set_size_request(misuse_scroll, -1, 300);
gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(misuse_scroll),
document_identity_misuse_editor_get_widget(
context->identity_misuse_editor));
gtk_box_append(GTK_BOX(main_box), misuse_scroll);
}
IdentityTraceabilityDao *trace_dao = identity_traceability_dao_new(database);
if (trace_dao != NULL) {
GPtrArray *relations = identity_traceability_dao_list_factual_relations_by_evidence(

View file

@ -525,7 +525,7 @@ static void test_database_initialize_valid_database(void)
"FROM investigation;"
);
assert(strcmp(schema_version, "19") == 0);
assert(strcmp(schema_version, "20") == 0);
test_database_assert_table_exists(database, "person_role_assignments");
test_database_assert_table_exists(database, "bank_account_entities");
test_database_assert_table_exists(database, "relation_types");
@ -543,6 +543,15 @@ static void test_database_initialize_valid_database(void)
"person_role_vocabulary");
test_database_assert_table_exists(database,
"identification_status_vocabulary");
test_database_assert_table_exists(database, "person_profile_fields");
test_database_assert_table_exists(database,
"document_identity_misuse_assessments");
test_database_assert_table_exists(database,
"person_ocr_field_projections");
test_database_assert_column_exists(database,
"person_ocr_field_projections", "previous_value");
test_database_assert_column_exists(database,
"person_ocr_field_projections", "origin");
assert(strcmp(application_name, "Labfy Investigation") == 0);
assert(created_at[0] != '\0');
@ -1001,7 +1010,7 @@ static void test_database_migrate_v1_to_v2(void)
assert(
strcmp(
schema_version,
"19"
"20"
) == 0
);
@ -1431,6 +1440,7 @@ static void test_database_migrate_v12_to_v13_preserves_legacy_link(void)
"'20000000-0000-4000-8000-000000000014','alleged_scammer',"
"'2026-07-28T08:00:00Z');"
"DROP TABLE person_ocr_field_projections;"
"DROP TABLE document_identity_misuse_assessments;"
"DROP TABLE person_profile_fields;"
"DROP TABLE person_identification_assessments;"
"DROP TABLE person_evidence_factual_relations;"
@ -1466,7 +1476,7 @@ static void test_database_migrate_v12_to_v13_preserves_legacy_link(void)
legacy_sources = test_database_read_single_text(sqlite_database,
"SELECT COUNT(*) FROM preuve_entite_sources "
"WHERE source_kind='legacy_manual';");
assert(strcmp(version, "19") == 0);
assert(strcmp(version, "20") == 0);
assert(strcmp(legacy_sources, "1") == 0);
char *legacy_role = test_database_read_single_text(sqlite_database,
"SELECT role_code || ':' || provenance_kind "

View file

@ -0,0 +1,24 @@
#include "views/document_identity_misuse_editor.h"
#include "core/document_identity_misuse_service.h"
#include "database/database.h"
#include <glib/gstdio.h>
#include <sqlite3.h>
#define EVIDENCE "11000000-0000-4000-8000-000000000020"
#define RUN "31000000-0000-4000-8000-000000000020"
#define AT "2026-08-02T10: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*r=named(c,n);if(r)return r;}return NULL;}
static void test_editor(void)
{
GError*error=NULL;char*dir=g_dir_make_tmp("labfy-misuse-gtk-XXXXXX",&error);g_assert_no_error(error);char*path=g_build_filename(dir,"Enquete.sqlite",NULL);g_assert_true(database_initialize(path,"SPECIMEN",dir));
sqlite3*s=NULL;g_assert_cmpint(sqlite3_open(path,&s),==,SQLITE_OK);char*message=NULL;
const char*sql="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');";
g_assert_cmpint(sqlite3_exec(s,sql,NULL,NULL,&message),==,SQLITE_OK);sqlite3_free(message);sqlite3_close(s);
Database*d=database_open(path);g_assert_nonnull(d);DocumentIdentityMisuseEditor*x=document_identity_misuse_editor_new(d,EVIDENCE);g_assert_nonnull(x);GtkWidget*root=document_identity_misuse_editor_get_widget(x);g_object_ref_sink(root);
GtkDropDown*status=GTK_DROP_DOWN(named(root,"identity-misuse-status"));GtkDropDown*run=GTK_DROP_DOWN(named(root,"identity-misuse-ocr-run"));GtkLabel*error_label=GTK_LABEL(named(root,"identity-misuse-error"));GtkWidget*save=named(root,"identity-misuse-save");
g_assert_cmpuint(g_list_model_get_n_items(gtk_drop_down_get_model(status)),==,3);g_assert_cmpuint(g_list_model_get_n_items(gtk_drop_down_get_model(run)),==,2);
gtk_drop_down_set_selected(status,1);g_signal_emit_by_name(save,"clicked");g_assert_true(gtk_widget_get_visible(GTK_WIDGET(error_label)));
GtkTextBuffer*j=gtk_text_view_get_buffer(GTK_TEXT_VIEW(named(root,"identity-misuse-justification")));gtk_text_buffer_set_text(j,"Indices 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(error_label)));
GPtrArray*h=document_identity_misuse_service_history(d,EVIDENCE,&error);g_assert_no_error(error);g_assert_cmpuint(h->len,==,1);DocumentIdentityMisuseAssessment*a=g_ptr_array_index(h,0);g_assert_cmpstr(document_identity_misuse_assessment_get_status(a),==,"presumed");g_assert_cmpstr(document_identity_misuse_assessment_get_origin(a),==,"human");g_ptr_array_unref(h);
g_object_unref(root);document_identity_misuse_editor_free(x);database_close(d);g_remove(path);g_rmdir(dir);g_free(path);g_free(dir);
}
int main(int argc,char**argv){gtk_init();g_test_init(&argc,&argv,NULL);g_test_add_func("/identity-misuse/editor",test_editor);return g_test_run();}

View file

@ -329,6 +329,8 @@ static gboolean drive(gpointer data)
g_assert_true(gtk_window_get_destroy_with_parent(dialog));
g_assert_nonnull(find_named(GTK_WIDGET(dialog),
"document-authenticity-editor"));
g_assert_nonnull(find_named(GTK_WIDGET(dialog),
"document-identity-misuse-editor"));
}
if (dialog == NULL) {
if (context->phase == 1) {

View file

@ -4,6 +4,7 @@
#include "database/database.h"
#include "models/identity_traceability.h"
#include "core/document_authenticity_service.h"
#include "core/document_identity_misuse_service.h"
#include <glib.h>
#include <glib/gstdio.h>
#include <sqlite3.h>
@ -16,6 +17,8 @@
#define ASSESSMENT1 "60000000-0000-4000-8000-000000000018"
#define ASSESSMENT2 "60000000-0000-4000-8000-000000000019"
#define FACT "70000000-0000-4000-8000-000000000018"
#define MISUSE1 "90000000-0000-4000-8000-000000000018"
#define MISUSE2 "90000000-0000-4000-8000-000000000019"
#define AT "2026-07-30T10:00:00Z"
typedef struct{char*dir,*path;Database*database;}Fixture;
@ -114,6 +117,73 @@ static void test_authenticity_service(void)
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_identity_misuse_service(void)
{
gsize count=0;const DocumentIdentityMisuseStatus*statuses=
document_identity_misuse_service_statuses(&count);
static const char*codes[]={"indeterminate","presumed","confirmed"};
static const char*labels[]={"Hypothèse d’usurpation indéterminée",
"Document présumé utilisé dans une usurpation d’identité",
"Utilisation dans une usurpation d’identité confirmée"};
g_assert_cmpuint(count,==,3);for(guint i=0;i<count;i++){
g_assert_cmpstr(statuses[i].code,==,codes[i]);g_assert_cmpstr(statuses[i].label,==,labels[i]);}
g_assert_null(document_identity_misuse_assessment_new(MISUSE1,EVIDENCE,NULL,
"presumed",NULL,AT,NULL));
Fixture f=fixture_new();GError*error=NULL;DocumentIdentityMisuseAssessment*created=NULL;
g_assert_true(document_identity_misuse_service_add(f.database,EVIDENCE,
"indeterminate",NULL,NULL,&created,&error));g_assert_no_error(error);
g_assert_cmpstr(document_identity_misuse_assessment_get_origin(created),==,"human");
document_identity_misuse_assessment_free(created);
g_assert_true(document_identity_misuse_service_add(f.database,EVIDENCE,
"presumed","Indices SPECIMEN",RUN,&created,&error));g_assert_no_error(error);
g_assert_nonnull(document_identity_misuse_assessment_get_previous_identifier(created));
document_identity_misuse_assessment_free(created);
g_assert_false(document_identity_misuse_service_add(f.database,EVIDENCE,
"confirmed","Conclusion SPECIMEN","30000000-0000-4000-8000-000000000099",
NULL,&error));g_assert_nonnull(error);g_clear_error(&error);
GPtrArray*h=document_identity_misuse_service_history(f.database,EVIDENCE,&error);
g_assert_no_error(error);g_assert_cmpuint(h->len,==,2);g_ptr_array_unref(h);
database_close(f.database);f.database=database_open(f.path);g_assert_nonnull(f.database);
h=document_identity_misuse_service_history(f.database,EVIDENCE,&error);
g_assert_cmpuint(h->len,==,2);g_ptr_array_unref(h);
database_close(f.database);f.database=NULL;sqlite3*d=NULL;
g_assert_cmpint(sqlite3_open(f.path,&d),==,SQLITE_OK);
char*automatic=scalar(d,"SELECT (SELECT COUNT(*) FROM person_role_assignments)"
"||(SELECT COUNT(*) FROM person_evidence_factual_relations)"
"||(SELECT COUNT(*) FROM person_identification_assessments)"
"||(SELECT COUNT(*) FROM document_authenticity_assessments);");
g_assert_cmpstr(automatic,==,"0000");g_free(automatic);char*message=NULL;
g_assert_cmpint(sqlite3_exec(d,"UPDATE document_identity_misuse_assessments "
"SET status='confirmed';",NULL,NULL,&message),!=,SQLITE_OK);
sqlite3_free(message);message=NULL;g_assert_cmpint(sqlite3_exec(d,
"DELETE FROM document_identity_misuse_assessments;",NULL,NULL,&message),!=,SQLITE_OK);
sqlite3_free(message);sqlite3_close(d);f.database=database_open(f.path);
g_assert_nonnull(f.database);fixture_free(&f);
}
static void test_migrate_v19_to_v20(void)
{
Fixture f=fixture_new();database_close(f.database);f.database=NULL;sqlite3*d=NULL;
g_assert_cmpint(sqlite3_open(f.path,&d),==,SQLITE_OK);
exec_ok(d,"DROP TABLE document_identity_misuse_assessments;"
"UPDATE metadata SET value='19' WHERE key='schema_version';");sqlite3_close(d);
f.database=database_open(f.path);g_assert_nonnull(f.database);
g_assert_true(database_migrate_to_latest(f.database));database_close(f.database);f.database=NULL;
g_assert_cmpint(sqlite3_open(f.path,&d),==,SQLITE_OK);char*version=scalar(d,
"SELECT value FROM metadata WHERE key='schema_version';");char*table=scalar(d,
"SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='document_identity_misuse_assessments';");
g_assert_cmpstr(version,==,"20");g_assert_cmpstr(table,==,"1");g_free(version);g_free(table);
exec_ok(d,"DROP TABLE document_identity_misuse_assessments;"
"UPDATE metadata SET value='19' WHERE key='schema_version';"
"CREATE TABLE document_identity_misuse_assessments(dummy TEXT);");sqlite3_close(d);
f.database=database_open(f.path);g_test_expect_message(NULL,G_LOG_LEVEL_WARNING,
"*Impossible d’installer la migration SQLite V20*");
g_assert_false(database_migrate_to_latest(f.database));g_test_assert_expected_messages();
database_close(f.database);f.database=NULL;g_assert_cmpint(sqlite3_open(f.path,&d),==,SQLITE_OK);
version=scalar(d,"SELECT value FROM metadata WHERE key='schema_version';");
g_assert_cmpstr(version,==,"19");g_free(version);exec_ok(d,
"DROP TABLE document_identity_misuse_assessments;");sqlite3_close(d);
f.database=database_open(f.path);g_assert_true(database_migrate_to_latest(f.database));fixture_free(&f);
}
static void test_dao_history_and_relations(void)
{
Fixture f=fixture_new();GError*e=NULL;
@ -218,6 +288,7 @@ static void test_migrate_v17_preserves_ocr(void)
Fixture f=fixture_new();database_close(f.database);f.database=NULL;
sqlite3*d=NULL;g_assert_cmpint(sqlite3_open(f.path,&d),==,SQLITE_OK);
exec_ok(d,"PRAGMA foreign_keys=OFF;"
"DROP TABLE document_identity_misuse_assessments;"
"DROP TABLE person_ocr_field_projections;DROP TABLE person_profile_fields;"
"DROP TABLE person_identification_assessments;"
"DROP TABLE person_evidence_factual_relations;"
@ -258,7 +329,7 @@ static void test_migrate_v17_preserves_ocr(void)
char*version=scalar(d,"SELECT value FROM metadata WHERE key='schema_version';");
char*values=scalar(d,"SELECT raw_value||':'||normalized_value||':'||corrected_value"
"||':'||confirmation_state FROM identity_field_observations WHERE id='" FIELD "';");
g_assert_cmpstr(version,==,"19");
g_assert_cmpstr(version,==,"20");
g_assert_cmpstr(values,==,"BRUT SPECIMEN:BRUT SPECIMEN:CORRIGÉ SPECIMEN:unconfirmed");
sqlite3_close(d);g_free(version);g_free(values);
f.database=database_open(f.path);g_assert_true(database_migrate_to_latest(f.database));
@ -268,6 +339,8 @@ 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("/v20/identity-misuse-service",test_identity_misuse_service);
g_test_add_func("/v20/migrate-v19",test_migrate_v19_to_v20);
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();}

View file

@ -0,0 +1,21 @@
#include "widgets/entity_details_panel.h"
#include "models/evidence_record.h"
#include "models/identity_traceability.h"
#include <gtk/gtk.h>
#define PERSON "22000000-0000-4000-8000-000000000020"
#define EVIDENCE "12000000-0000-4000-8000-000000000020"
#define RELATION "72000000-0000-4000-8000-000000000020"
#define AT "2026-08-02T10:00:00Z"
static void collect_text(GtkWidget*w,GString*out){if(GTK_IS_LABEL(w))g_string_append(out,gtk_label_get_text(GTK_LABEL(w)));for(GtkWidget*c=gtk_widget_get_first_child(w);c;c=gtk_widget_get_next_sibling(c)){g_string_append_c(out,'\n');collect_text(c,out);}}
static void test_person_details(void)
{
GError*error=NULL;EntityDetailsPanel*panel=entity_details_panel_new();g_assert_nonnull(panel);GtkWidget*root=entity_details_panel_get_widget(panel);g_object_ref_sink(root);
EntityRecord*person=entity_record_new(PERSON,"person","Personne SPECIMEN","Personne SPECIMEN",NULL,0,AT,AT,ENTITY_STATUS_ACTIVE,&error);g_assert_no_error(error);g_assert_nonnull(person);entity_details_panel_set_entity(panel,person);
EvidenceRecord*evidence=evidence_record_new(EVIDENCE,"preuve-specimen.png","preuve-specimen.png","preuve-specimen.png","2",8,"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",AT,NULL,NULL,NULL,EVIDENCE_INTEGRITY_STATUS_UNKNOWN,&error);g_assert_no_error(error);g_assert_nonnull(evidence);
GPtrArray*evidence_records=g_ptr_array_new();g_ptr_array_add(evidence_records,evidence);PersonEvidenceFactualRelation*relation=person_evidence_factual_relation_new(RELATION,PERSON,EVIDENCE,NULL,"identity_observed_in","Note factuelle SPECIMEN",AT,TRUE);g_assert_nonnull(relation);GPtrArray*relations=g_ptr_array_new();g_ptr_array_add(relations,relation);
GHashTable*fields=g_hash_table_new(g_str_hash,g_str_equal);g_hash_table_insert(fields,"surname","NOM SPECIMEN");g_hash_table_insert(fields,"birth_date","2000-01-01");
entity_details_panel_set_person_profile_fields(panel,fields);entity_details_panel_set_person_factual_relations(panel,relations,evidence_records);
GString*text=g_string_new(NULL);collect_text(root,text);g_assert_nonnull(strstr(text->str,"Valeurs confirmées du profil"));g_assert_nonnull(strstr(text->str,"NOM SPECIMEN"));g_assert_nonnull(strstr(text->str,"Relations factuelles"));g_assert_nonnull(strstr(text->str,"Identité observée dans la preuve"));g_assert_nonnull(strstr(text->str,"Note factuelle SPECIMEN"));
g_string_free(text,TRUE);g_hash_table_unref(fields);g_ptr_array_unref(relations);g_ptr_array_unref(evidence_records);person_evidence_factual_relation_free(relation);evidence_record_free(evidence);entity_record_free(person);entity_details_panel_free(panel);g_object_unref(root);
}
int main(int argc,char**argv){gtk_init();g_test_init(&argc,&argv,NULL);g_test_add_func("/person-details/traceability",test_person_details);return g_test_run();}

View file

@ -1,473 +0,0 @@
## Contexte
Lors de la création d’une personne dans une enquête, l’utilisateur doit
actuellement renseigner les données manuellement et ne peut pas choisir
clairement la nature de la personne ni associer facilement une preuve.
Le parcours devient particulièrement long lorsque les noms de fichiers
ne permettent pas de savoir ce qu’ils contiennent.
Le cas des documents d’identité nécessite également un traitement
forensique strict :
- conservation de l’original ;
- analyse sur une copie de travail ;
- OCR contrôlé ;
- validation humaine ;
- distinction entre texte brut, valeur normalisée et correction manuelle ;
- aucune affirmation automatique concernant l’identité réelle ou
l’authenticité du document.
## Objectif
Créer un assistant fluide permettant, depuis la fenêtre de création d’une
personne :
1. de sélectionner son rôle ou sa nature dans l’enquête ;
2. de choisir une preuve déjà importée ;
3. d’importer immédiatement une nouvelle preuve ;
4. de visualiser la preuve avant de la sélectionner ;
5. de qualifier le type de preuve ;
6. de lancer une analyse adaptée au type choisi ;
7. d’utiliser l’OCR pour préremplir les champs d’un document d’identité ;
8. de corriger manuellement les données proposées ;
9. de conserver la provenance complète de chaque valeur ;
10. de garantir l’intégrité du fichier original.
## Terminologie
Ne pas confondre :
- le rôle de la personne dans l’enquête ;
- son niveau d’identification ;
- l’authenticité du document présenté ;
- la confiance accordée aux différentes observations.
Une personne peut par exemple avoir :
- rôle : Identité présentée ;
- identification : Non vérifiée ;
- document : Authenticité indéterminée ;
- hypothèse : Identité potentiellement usurpée.
L’application ne doit jamais transformer automatiquement cette personne
en auteur identifié.
## Fenêtre de création d’une personne
Ajouter un champ contrôlé :
Rôle dans l’enquête
Valeurs initiales proposées :
- Auteur présumé ;
- Identité présentée ;
- Identité potentiellement usurpée ;
- Victime ;
- Témoin ;
- Titulaire bancaire déclaré ;
- Intermédiaire ;
- Personne citée ;
- Autre.
Ces valeurs doivent provenir du vocabulaire contrôlé du projet.
Le champ existant « Identification » reste distinct et conserve des états
tels que :
- Inconnu ;
- Non vérifié ;
- Partiellement identifié ;
- Identifié ;
- Contesté.
## Association d’une preuve
Depuis la fenêtre de création d’une personne, proposer deux actions :
- Sélectionner une preuve existante ;
- Importer une nouvelle preuve.
L’utilisateur ne doit pas être obligé de fermer la fenêtre, importer la
preuve ailleurs, puis recommencer la création de la personne.
### Preuve existante
La sélection doit afficher au minimum :
- miniature ou aperçu ;
- nom du fichier ;
- type de preuve ;
- taille ;
- date d’import ;
- empreinte SHA-256 abrégée ;
- description éventuelle ;
- état d’intégrité.
Prévoir une recherche et un filtrage par type.
### Nouvelle preuve
L’import déclenché depuis la fenêtre doit utiliser le mécanisme forensique
central du projet :
- copie contrôlée dans l’enquête ;
- calcul du SHA-256 ;
- conservation du nom original ;
- enregistrement de la provenance ;
- aucune modification du fichier source ;
- détection du type MIME réel ;
- détection des collisions ;
- traitement asynchrone ;
- annulation sûre.
À la fin de l’import, la nouvelle preuve doit être automatiquement
sélectionnée dans la fenêtre de création de la personne.
## Aperçu des preuves
La fenêtre de sélection et d’import doit fournir un aperçu suffisamment
grand pour identifier le contenu sans occuper toute la fenêtre.
Prévoir une zone responsive avec :
- conservation du ratio ;
- ajustement à la zone ;
- zoom avant et arrière ;
- retour à l’ajustement ;
- défilement lorsque l’image est agrandie ;
- message explicite lorsque l’aperçu est indisponible.
Prise en charge minimale :
- image : aperçu direct ;
- PDF : première page avec navigation entre les pages ;
- vidéo : miniature, durée et informations principales ;
- EML : résumé des en-têtes et liste des pièces jointes ;
- autre fichier : icône, type MIME, taille et métadonnées disponibles.
L’aperçu est une représentation dérivée.
Il ne doit jamais modifier le fichier original.
Toute conversion, génération de miniature, rotation, amélioration,
redimensionnement ou rendu PDF doit être réalisée dans une zone de
travail temporaire contrôlée.
## Qualification du type de preuve
Permettre de sélectionner ou confirmer un type contrôlé, par exemple :
- Document d’identité ;
- Carte nationale d’identité ;
- Passeport ;
- Permis de conduire ;
- Document bancaire ;
- Capture d’écran ;
- Conversation ;
- Courrier électronique ;
- PDF ;
- Photo ;
- Vidéo ;
- Billet ou justificatif de commande ;
- Autre.
La détection automatique peut proposer un type, mais l’utilisateur doit
toujours pouvoir le corriger avant validation.
Le type choisi détermine les analyses proposées.
## Document d’identité
Lorsqu’une preuve est qualifiée comme document d’identité, proposer :
Analyser le document
Le traitement doit être asynchrone et annulable.
Il doit :
1. vérifier l’intégrité de la preuve ;
2. créer une copie de travail ;
3. conserver l’original strictement intact ;
4. appliquer les conversions uniquement à la copie ;
5. exécuter l’OCR ;
6. conserver le texte OCR brut ;
7. proposer des champs structurés ;
8. attendre une validation humaine avant toute intégration.
## Champs OCR proposés
Selon les informations réellement visibles, proposer notamment :
- nom ;
- prénoms ;
- sexe déclaré ;
- date de naissance ;
- lieu de naissance ;
- nationalité déclarée ;
- taille ;
- numéro du document ;
- date de délivrance ;
- date d’expiration ;
- autorité de délivrance ;
- zone lisible par machine ;
- autres valeurs observées.
Ne jamais inventer une valeur absente ou illisible.
Chaque proposition doit posséder un état :
- détectée ;
- partielle ;
- incertaine ;
- invalide ;
- corrigée ;
- rejetée ;
- confirmée.
## Validation et correction humaine
Avant la création de la personne, afficher côte à côte autant que possible :
- l’aperçu du document ;
- les valeurs extraites ;
- les champs modifiables ;
- le niveau de confiance OCR ;
- la provenance précise.
Pour chaque champ, conserver séparément :
- valeur OCR brute ;
- valeur normalisée ;
- valeur corrigée manuellement ;
- valeur finalement confirmée ;
- méthode d’extraction ;
- langue OCR ;
- version de l’outil ;
- date UTC ;
- preuve source ;
- page ou zone source lorsque disponible.
Une correction manuelle ne doit jamais écraser le résultat OCR brut.
L’utilisateur doit pouvoir :
- modifier une proposition ;
- rejeter une proposition ;
- laisser un champ vide ;
- revenir à la valeur OCR ;
- confirmer uniquement certains champs.
## Authenticité et identité usurpée
Ajouter un statut contrôlé pour le document :
- Authenticité indéterminée ;
- Présumé authentique ;
- Suspect ;
- Présumé falsifié ;
- Falsifié confirmé ;
- Document usurpé présumé ;
- Document usurpé confirmé.
Les statuts affirmatifs doivent nécessiter une validation explicite et
une justification.
L’OCR ne doit jamais conclure :
- que le document est authentique ;
- que la personne figurant sur le document est l’auteur ;
- que l’identité est réellement usurpée ;
- que le titulaire a participé aux faits.
Dans le cas courant, le document doit pouvoir être enregistré comme :
Identité présentée — authenticité indéterminée
avec une hypothèse séparée :
Identité potentiellement usurpée
## Création de la personne
Après validation, créer la personne avec uniquement les champs confirmés.
La personne doit être liée à la preuve par une relation factuelle, par
exemple :
- Identité observée dans ;
- Document présenté au nom de ;
- Données extraites depuis ;
- Identité déclarée dans.
Ne pas créer automatiquement une relation :
- Est l’auteur ;
- Identité réelle de ;
- A usurpé l’identité de.
Les résultats OCR rejetés ne doivent pas devenir des attributs de la
personne.
## Intégrité et provenance
Le fichier original doit rester immuable.
Avant chaque analyse :
- recalculer ou vérifier le SHA-256 ;
- bloquer l’analyse en cas de divergence ;
- ne jamais réécrire l’empreinte enregistrée pour masquer une différence.
Tous les fichiers dérivés doivent être identifiables comme tels :
- miniature ;
- rendu de page PDF ;
- image préparée pour OCR ;
- recadrage ;
- correction d’orientation ;
- texte OCR ;
- JSON de métadonnées.
Chaque dérivé doit conserver :
- preuve parente ;
- outil ;
- version ;
- arguments ;
- date UTC ;
- SHA-256 ;
- statut ;
- avertissements.
## Interface
Le parcours doit rester possible depuis une seule fenêtre ou un assistant
cohérent :
Création de la personne
↓
choix du rôle
↓
sélection ou import de la preuve
↓
aperçu
↓
qualification du type
↓
analyse facultative
↓
révision des propositions
↓
création de la personne et des relations confirmées
Utiliser des boutons compacts avec icônes et infobulles.
Ne pas ajouter de gros boutons occupant inutilement l’interface.
La fermeture ou l’annulation ne doit créer ni personne partielle, ni
attribut partiel, ni relation partielle.
## Transactions
La création finale doit être transactionnelle pour :
- la personne ;
- ses attributs confirmés ;
- le rattachement à la preuve ;
- les observations OCR confirmées ;
- les relations ;
- les références aux fichiers dérivés conservés.
En cas d’échec :
- rollback complet ;
- original inchangé ;
- aucune personne incomplète ;
- aucune relation orpheline ;
- aucun fichier temporaire présenté comme preuve permanente.
## Tests obligatoires
Ajouter des fixtures exclusivement synthétiques.
Couvrir au minimum :
1. choix de chaque rôle contrôlé ;
2. sélection d’une preuve existante ;
3. import d’une nouvelle preuve depuis la fenêtre ;
4. sélection automatique après import ;
5. aperçu image ;
6. aperçu PDF ;
7. aperçu indisponible ;
8. navigation entre pages ;
9. qualification manuelle du type ;
10. document d’identité déclenchant l’OCR ;
11. autre type ne déclenchant pas automatiquement l’OCR ;
12. original inchangé ;
13. SHA-256 inchangé ;
14. travail sur copie ;
15. texte OCR brut conservé ;
16. valeur normalisée séparée ;
17. correction manuelle séparée ;
18. rejet d’un champ ;
19. champ illisible laissé vide ;
20. création avec seulement les valeurs confirmées ;
21. provenance par champ ;
22. annulation ;
23. rollback ;
24. fermeture de fenêtre pendant l’analyse ;
25. changement d’enquête pendant l’analyse ;
26. aucune écriture dans la mauvaise base ;
27. aucune donnée réelle dans les fixtures ;
28. aucune conclusion automatique sur l’authenticité ;
29. aucune fusion automatique avec l’auteur présumé ;
30. réouverture de l’enquête avec données persistantes.
Séparer autant que possible :
- services d’import ;
- génération d’aperçu ;
- OCR ;
- modèle de révision ;
- DAO ;
- tests GTK ciblés ;
- validation visuelle manuelle.
## Critères d’acceptation
Le ticket est terminé seulement si :
- le rôle de la personne peut être sélectionné ;
- une preuve existante peut être choisie ;
- une nouvelle preuve peut être importée sans quitter le parcours ;
- un aperçu exploitable est disponible ;
- le type de preuve peut être confirmé ou corrigé ;
- un document d’identité peut être analysé par OCR ;
- les propositions OCR sont modifiables avant intégration ;
- le texte OCR brut n’est jamais écrasé ;
- l’original reste inchangé ;
- la provenance est persistée ;
- seules les valeurs confirmées alimentent la personne ;
- aucune identité n’est attribuée automatiquement à l’auteur présumé ;
- tous les tests passent.
## Sécurité de développement
Codex et tout agent de développement doivent travailler uniquement avec :
- le dépôt source ;
- des fixtures synthétiques ;
- des bases SQLite temporaires synthétiques.
Ils ne doivent jamais accéder à :
- Enquete.sqlite réelle ;
- documents d’identité réels ;
- captures réelles ;
- e-mails réels ;
- données bancaires réelles ;
- vidéos réelles ;
- autres preuves de l’enquête.

View file

@ -1,10 +0,0 @@
9339 src/core/application.c
142 src/core/person_creation_task.c
2037 src/views/create_person_dialog.c
660 src/views/evidence_metadata_dialog.c
2245 src/views/main_window.c
286 src/views/person_factual_relation_editor.c
193 src/views/person_vocabulary_adapter.c
1550 src/widgets/entity_details_panel.c
4078 src/widgets/workspace.c
20530 total