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 ### 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 - Projection contrôlée des champs OCR confirmés vers les attributs structurés
dune personne, avec provenance append-only, conflits explicites et rollback. dune personne, avec provenance append-only, conflits explicites et rollback.
- Saisie et consultation, depuis la fiche preuve, dun historique immuable - Saisie et consultation, depuis la fiche preuve, dun historique immuable
@ -40,6 +42,12 @@
### Fixed ### 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 lusage didentité,
distincte de lauthenticité 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 - Rafraîchissement immédiat de Workspace après import ou révision OCR et
persistance des données sur lUUID définitif de la preuve, y compris après persistance des données sur lUUID définitif de la preuve, y compris après
fermeture et réouverture de SQLite. 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_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 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 := tests/test_person_ocr_projection
TEST_PERSON_OCR_PROJECTION_EDITOR_GTK := tests/test_person_ocr_projection_editor_gtk TEST_PERSON_OCR_PROJECTION_EDITOR_GTK := tests/test_person_ocr_projection_editor_gtk
FAKE_DOCUMENT_TOOL := tests/fake_document_tool 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 src/database/error.c src/core/relation_type_normalizer.c
$(CC) $(CFLAGS) $^ -o $@ $(LDFLAGS) $(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 \ 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/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 \ 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 \ $(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/core/document_authenticity_service.c \
src/core/document_identity_misuse_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 \
@ -1238,6 +1261,8 @@ test: \
$(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) \ $(TEST_DOCUMENT_AUTHENTICITY_EDITOR_GTK) \
$(TEST_DOCUMENT_IDENTITY_MISUSE_EDITOR_GTK) \
$(TEST_PERSON_DETAILS_TRACEABILITY_GTK) \
$(TEST_PERSON_OCR_PROJECTION) \ $(TEST_PERSON_OCR_PROJECTION) \
$(TEST_PERSON_OCR_PROJECTION_EDITOR_GTK) $(TEST_PERSON_OCR_PROJECTION_EDITOR_GTK)
@echo "Exécution des tests..." @echo "Exécution des tests..."
@ -1337,6 +1362,8 @@ test: \
@$(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) @$(TEST_DOCUMENT_AUTHENTICITY_EDITOR_GTK)
@$(TEST_DOCUMENT_IDENTITY_MISUSE_EDITOR_GTK)
@$(TEST_PERSON_DETAILS_TRACEABILITY_GTK)
@$(TEST_PERSON_OCR_PROJECTION) @$(TEST_PERSON_OCR_PROJECTION)
@$(TEST_PERSON_OCR_PROJECTION_EDITOR_GTK) @$(TEST_PERSON_OCR_PROJECTION_EDITOR_GTK)
@echo "Tous les tests sont valides." @echo "Tous les tests sont valides."
@ -1440,6 +1467,8 @@ clean:
$(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) \ $(TEST_DOCUMENT_AUTHENTICITY_EDITOR_GTK) \
$(TEST_DOCUMENT_IDENTITY_MISUSE_EDITOR_GTK) \
$(TEST_PERSON_DETAILS_TRACEABILITY_GTK) \
$(TEST_PERSON_OCR_PROJECTION) \ $(TEST_PERSON_OCR_PROJECTION) \
$(TEST_PERSON_OCR_PROJECTION_EDITOR_GTK) \ $(TEST_PERSON_OCR_PROJECTION_EDITOR_GTK) \
$(FAKE_DOCUMENT_TOOL) $(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. Le projet est compilé en C17 avec les avertissements traités comme des erreurs.
Lassistant de création dune 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 lunique é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 lauthenticité de lévaluation humaine de lusage
didentité, dont lhistorique est append-only et jamais produit par lOCR.
--- ---
## Tests ## Tests
@ -405,7 +417,8 @@ Vérifications recommandées avant chaque commit :
```bash ```bash
make clean make clean
make -j8 make -j8
make -j8 test make check-source-size
DISPLAY="$DISPLAY" WAYLAND_DISPLAY="$WAYLAND_DISPLAY" make -j8 test
git diff --check 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_workspace_identity_ocr_gtk
G_DEBUG=fatal-criticals timeout 30s ./tests/test_evidence_preview_widget_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_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 Les tests emploient uniquement des documents `SPECIMEN`, des fichiers

View file

@ -1,7 +1,7 @@
/****************************************************************************** /******************************************************************************
* Labfy Investigation * 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 WHEN NEW.confirmation_state='human_confirmed' AND NEW.confirmed_value IS NULL
THEN RAISE(ABORT,'confirmed value required') END; THEN RAISE(ABORT,'confirmed value required') END;
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 lusage abusif didentité. */
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 lusage abusif didentité. */
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 > **Version :** 3.2
> **Dernière mise à jour :** 2026-07-30 > **Dernière mise à jour :** 2026-07-30
> **Schéma SQLite courant :** V18 > **Schéma SQLite courant :** V20
## Personnes contextuelles — SQLite V14 ## 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 défaut en cas de conflit. La fiche personne relit ces champs par
`PersonDetailsProvider` et le DAO de projection. `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
lauthenticité 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 lalimentation plus
complète de `person_identification_assessments` restent des renforcements de
cohérence, non des exigences contractuelles explicites du ticket #109.
--- ---
## 1. Objectif ## 1. Objectif
@ -430,7 +445,7 @@ validation du chemin
création de l'arborescence création de l'arborescence
initialisation transactionnelle de SQLite V18 initialisation transactionnelle de SQLite V20
création de l'identité de l'enquête création de l'identité de l'enquête
@ -879,7 +894,8 @@ Validation :
```sh ```sh
make clean make clean
make -j8 make -j8
make -j8 test make check-source-size
DISPLAY="$DISPLAY" WAYLAND_DISPLAY="$WAYLAND_DISPLAY" make -j8 test
git diff --check 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 `test_person_ocr_projection_editor_gtk`, ce dernier sous affichage réel et
`G_DEBUG=fatal-criticals`. `G_DEBUG=fatal-criticals`.
Lassistant complet se valide également avec
`tests/test_create_person_dialog_gtk` : il vérifie lordre des sept pages,
leur séparation et les trois rubriques du résumé final.
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
@ -306,6 +310,9 @@ make -j8 \
tests/test_workspace_identity_ocr_gtk \ tests/test_workspace_identity_ocr_gtk \
tests/test_evidence_preview_widget_gtk \ tests/test_evidence_preview_widget_gtk \
tests/test_dialog_geometry_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_create_person_dialog_ocr_gtk
G_DEBUG=fatal-criticals timeout 30s ./tests/test_evidence_identity_import_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_workspace_identity_ocr_gtk
G_DEBUG=fatal-criticals timeout 30s ./tests/test_evidence_preview_widget_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_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` Ils doivent cliquer sur les contrôles de production, passer par `MainWindow`
@ -655,10 +665,19 @@ Les tests doivent utiliser :
Avant chaque commit : Avant chaque commit :
```sh ```sh
make clean
make -j8
make check-source-size
DISPLAY="$DISPLAY" WAYLAND_DISPLAY="$WAYLAND_DISPLAY" make -j8 test
git diff --check git diff --check
git status --short git status --short
``` ```
Les validations ASan/UBSan ciblées produisent leurs binaires dans `/tmp`.
Lorsque `ASAN_OPTIONS=detect_leaks=0` est nécessaire sous lenvironnement GTK
tracé, cela valide AddressSanitizer et UndefinedBehaviorSanitizer, pas
LeakSanitizer.
Inspecter tout nouveau fichier inhabituel à la racine du dépôt. 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 6. dialogues à 1200 × 800 et 760 × 560 : parent transitoire, formulaire
défilable, répartition 2/31/3 et actions toujours visibles. défilable, répartition 2/31/3 et actions toujours visibles.
La validation manuelle réussie confirme ces parcours, mais ne clôt pas le Contrôler aussi les sept étapes de lassistant, les trois rubriques du résumé,
ticket #109 et ne couvre pas les limitations encore listées dans la roadmap. la projection vide par défaut et labsence de relation automatique.
La validation manuelle doit également couvrir la section V20 distincte de
lauthenticité. La justification des rôles sensibles et lhistorisation
complète de lidentification sont des renforcements de cohérence, pas des
critères dacceptation 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 La validation manuelle de ces parcours est réussie. Elle ne vaut pas
achèvement du ticket #109. 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 > **É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 > **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 ; - création, validation et ouverture d'enquêtes ;
- session d'enquête remplaçable proprement ; - 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 ; - couche Database, DAO et services métier ;
- import de preuves avec copie contrôlée et SHA-256 ; - import de preuves avec copie contrôlée et SHA-256 ;
- vérification d'intégrité et reclassement des preuves ; - vérification d'intégrité et reclassement des preuves ;
@ -185,13 +185,15 @@ PARTIEL — VALIDATION MANUELLE DES TRANCHES LIVRÉES RÉUSSIE
Limitations restantes : Limitations restantes :
1. lOCR groupé de plusieurs preuves dans une même opération nest pas pris en 1. la justification des rôles sensibles et lalimentation complète de
charge ; `person_identification_assessments` restent des renforcements de cohérence,
2. les interfaces GTK complètes des états didentification et des rôles V18 sans être des exigences explicites du ticket #109 ;
restent à construire ; 2. lOCR groupé nest volontairement pas pris en charge : après un import
3. la provenance uniforme de tous les autres dérivés reste partielle ; 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 dusage didentité présumé
et confirmé. Une validation manuelle finale reste requise avant clôture.
### Ticket #107 — Pivot e-mail forensique ### 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 preuve, OcrRun, champ OCR, qualité, stratégie, date et origine humaine. Un
trigger refuse une source devenue non projectable. trigger refuse une source devenue non projectable.
> **Statut :** architecture courante > **Statut :** architecture courante
> **Version du schéma :** V19 V20 ajoute lhistorique humain append-only
> **Dernière mise à jour :** 2026-07-24 `document_identity_misuse_assessments`, distinct de lauthenticité.
> **Version du schéma :** V20
> **Dernière mise à jour :** 2026-08-02
> **Source de vérité détaillée :** `SCHEMA_AUDIT_CURRENT.md` > **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 : L'état réel du schéma est déterminé par :
1. les constantes de version dans le code ; 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` ; 3. `database/schema_current.sql` ;
4. les fonctions d'installation et de migration ; 4. les fonctions d'installation et de migration ;
5. `tests/test_database.c` et les tests DAO ; 5. `tests/test_database.c` et les tests DAO ;
@ -242,6 +245,7 @@ database/schema_v7.sql
database/schema_v8.sql database/schema_v8.sql
database/schema_v9.sql database/schema_v9.sql
database/schema_v10.sql database/schema_v10.sql
database/schema_v11.sql à database/schema_v19.sql
``` ```
`database/schema_current.sql` contient des extensions ou réparations `database/schema_current.sql` contient des extensions ou réparations
@ -264,7 +268,7 @@ PRAGMA foreign_keys = ON
BEGIN BEGIN
installation V1 à V10 installation du socle historique puis du schéma courant V20
application du schéma courant idempotent application du schéma courant idempotent
@ -278,7 +282,7 @@ Un échec provoque un rollback.
Une base neuve doit aboutir directement à : Une base neuve doit aboutir directement à :
```text ```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); IdentityTraceabilityDao *dao,const char *evidence_identifier,GError **error);
DocumentAuthenticityAssessment *identity_traceability_dao_current_authenticity( DocumentAuthenticityAssessment *identity_traceability_dao_current_authenticity(
IdentityTraceabilityDao *dao,const char *evidence_identifier,GError **error); 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( gboolean identity_traceability_dao_insert_factual_relation(
IdentityTraceabilityDao *dao,const PersonEvidenceFactualRelation *relation, IdentityTraceabilityDao *dao,const PersonEvidenceFactualRelation *relation,
GError **error); GError **error);

View file

@ -100,6 +100,7 @@ bool schema_install_v16(Database *database);
bool schema_install_v17(Database *database); bool schema_install_v17(Database *database);
bool schema_install_v18(Database *database); bool schema_install_v18(Database *database);
bool schema_install_v19(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. * @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; char *previous_identifier, *technical_note, *origin;
} DocumentAuthenticityAssessment; } DocumentAuthenticityAssessment;
typedef struct {
char *identifier, *evidence_identifier, *ocr_run_identifier;
char *status, *justification, *assessed_at;
char *previous_identifier, *origin;
} DocumentIdentityMisuseAssessment;
typedef struct { typedef struct {
char *identifier, *person_identifier, *evidence_identifier; char *identifier, *person_identifier, *evidence_identifier;
char *ocr_run_identifier, *relation_type, *factual_note; char *ocr_run_identifier, *relation_type, *factual_note;
@ -25,6 +31,7 @@ typedef struct {
typedef PersonRoleVocabularyEntry IdentificationStatusVocabularyEntry; typedef PersonRoleVocabularyEntry IdentificationStatusVocabularyEntry;
gboolean identity_traceability_authenticity_status_valid(const char *status); 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_relation_type_valid(const char *type);
gboolean identity_traceability_identification_status_valid(const char *status); gboolean identity_traceability_identification_status_valid(const char *status);
gboolean identity_traceability_value_quality_valid(const char *quality); 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_technical_note(const DocumentAuthenticityAssessment *assessment);
const char *document_authenticity_assessment_get_origin(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( PersonEvidenceFactualRelation *person_evidence_factual_relation_new(
const char *identifier, const char *person_identifier, const char *identifier, const char *person_identifier,
const char *evidence_identifier, const char *ocr_run_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 dusurpation indéterminée","Aucune conclusion humaine sur un usage abusif didentité.",FALSE},
{"presumed","Document présumé utilisé dans une usurpation didentité","Hypothèse humaine prudente nécessitant une justification.",TRUE},
{"confirmed","Utilisation dans une usurpation didentité 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 dusage didentité.");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 denregistrer lévaluation dusage didentité.");
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; document_authenticity_assessment_copy(g_ptr_array_index(a,0)):NULL;
g_ptr_array_unref(a);return r; 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 dusage didentité 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,"Lexé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 dusage didentité.");
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 lusage didentité 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 lhistorique dusage didentité.");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( gboolean identity_traceability_dao_insert_factual_relation(
IdentityTraceabilityDao*d,const PersonEvidenceFactualRelation*r,GError**e) IdentityTraceabilityDao*d,const PersonEvidenceFactualRelation*r,GError**e)
{ {

View file

@ -16,12 +16,12 @@
/** /**
* @brief Version actuelle du schéma SQLite. * @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. * @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. * @brief Nom de l'application enregistré dans les métadonnées.
@ -947,6 +947,21 @@ rollback:
return false; 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 dannuler la migration SQLite V19 vers V20.");
return false;
}
/** /**
* @brief Garantit atomiquement la présence des extensions du schéma courant. * @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; if (!database_migrate_v18_to_v19(database)) return false;
schema_version = 19; schema_version = 19;
break; break;
case 19:
if (!database_migrate_v19_to_v20(database)) return false;
schema_version = 20;
break;
default: default:
database_set_error( database_set_error(
@ -1354,8 +1373,7 @@ bool database_initialize(
if (!schema_ensure_current( if (!schema_ensure_current(
database database
) || ))
!schema_install_v19(database))
{ {
goto rollback; goto rollback;
} }

View file

@ -439,6 +439,12 @@ bool schema_install_v19(Database *database)
"la migration SQLite V19"); "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( bool schema_ensure_current(
Database *database Database *database
) )

View file

@ -24,6 +24,11 @@ gboolean identity_traceability_authenticity_status_valid(const char *value)
"suspicious","presumed_forged","confirmed_forged"}; "suspicious","presumed_forged","confirmed_forged"};
return in_values(value,values,G_N_ELEMENTS(values)); 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) gboolean identity_traceability_relation_type_valid(const char *value)
{ {
static const char *const values[]={"identity_observed_in", 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(technical_note,technical_note)
AUTH_GETTER(origin,origin) 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( 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,
const char *type,const char *note,const char *at,gboolean active) 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 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);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 lidentité 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/evidence_identity_ocr_dialog.h"
#include "views/person_vocabulary_adapter.h" #include "views/person_vocabulary_adapter.h"
#include "views/document_authenticity_editor.h" #include "views/document_authenticity_editor.h"
#include "views/document_identity_misuse_editor.h"
#include "widgets/ocr_provenance_overlay.h" #include "widgets/ocr_provenance_overlay.h"
struct EvidenceMetadataDialogResult struct EvidenceMetadataDialogResult
@ -50,6 +51,7 @@ typedef struct
gpointer user_data; gpointer user_data;
gboolean completed; gboolean completed;
DocumentAuthenticityEditor *authenticity_editor; DocumentAuthenticityEditor *authenticity_editor;
DocumentIdentityMisuseEditor *identity_misuse_editor;
} EvidenceMetadataDialogContext; } EvidenceMetadataDialogContext;
static void evidence_metadata_dialog_complete( 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_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); document_authenticity_editor_free(context->authenticity_editor);
document_identity_misuse_editor_free(context->identity_misuse_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);
@ -554,6 +557,16 @@ gboolean evidence_metadata_dialog_present_with_ocr(
document_authenticity_editor_get_widget(context->authenticity_editor)); document_authenticity_editor_get_widget(context->authenticity_editor));
gtk_box_append(GTK_BOX(main_box), auth_scroll); 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); 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

@ -525,7 +525,7 @@ static void test_database_initialize_valid_database(void)
"FROM investigation;" "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, "person_role_assignments");
test_database_assert_table_exists(database, "bank_account_entities"); test_database_assert_table_exists(database, "bank_account_entities");
test_database_assert_table_exists(database, "relation_types"); test_database_assert_table_exists(database, "relation_types");
@ -543,6 +543,15 @@ static void test_database_initialize_valid_database(void)
"person_role_vocabulary"); "person_role_vocabulary");
test_database_assert_table_exists(database, test_database_assert_table_exists(database,
"identification_status_vocabulary"); "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(strcmp(application_name, "Labfy Investigation") == 0);
assert(created_at[0] != '\0'); assert(created_at[0] != '\0');
@ -1001,7 +1010,7 @@ static void test_database_migrate_v1_to_v2(void)
assert( assert(
strcmp( strcmp(
schema_version, schema_version,
"19" "20"
) == 0 ) == 0
); );
@ -1431,6 +1440,7 @@ static void test_database_migrate_v12_to_v13_preserves_legacy_link(void)
"'20000000-0000-4000-8000-000000000014','alleged_scammer'," "'20000000-0000-4000-8000-000000000014','alleged_scammer',"
"'2026-07-28T08:00:00Z');" "'2026-07-28T08:00:00Z');"
"DROP TABLE person_ocr_field_projections;" "DROP TABLE person_ocr_field_projections;"
"DROP TABLE document_identity_misuse_assessments;"
"DROP TABLE person_profile_fields;" "DROP TABLE person_profile_fields;"
"DROP TABLE person_identification_assessments;" "DROP TABLE person_identification_assessments;"
"DROP TABLE person_evidence_factual_relations;" "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, legacy_sources = test_database_read_single_text(sqlite_database,
"SELECT COUNT(*) FROM preuve_entite_sources " "SELECT COUNT(*) FROM preuve_entite_sources "
"WHERE source_kind='legacy_manual';"); "WHERE source_kind='legacy_manual';");
assert(strcmp(version, "19") == 0); assert(strcmp(version, "20") == 0);
assert(strcmp(legacy_sources, "1") == 0); assert(strcmp(legacy_sources, "1") == 0);
char *legacy_role = test_database_read_single_text(sqlite_database, char *legacy_role = test_database_read_single_text(sqlite_database,
"SELECT role_code || ':' || provenance_kind " "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_true(gtk_window_get_destroy_with_parent(dialog));
g_assert_nonnull(find_named(GTK_WIDGET(dialog), g_assert_nonnull(find_named(GTK_WIDGET(dialog),
"document-authenticity-editor")); "document-authenticity-editor"));
g_assert_nonnull(find_named(GTK_WIDGET(dialog),
"document-identity-misuse-editor"));
} }
if (dialog == NULL) { if (dialog == NULL) {
if (context->phase == 1) { if (context->phase == 1) {

View file

@ -4,6 +4,7 @@
#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 "core/document_authenticity_service.h"
#include "core/document_identity_misuse_service.h"
#include <glib.h> #include <glib.h>
#include <glib/gstdio.h> #include <glib/gstdio.h>
#include <sqlite3.h> #include <sqlite3.h>
@ -16,6 +17,8 @@
#define ASSESSMENT1 "60000000-0000-4000-8000-000000000018" #define ASSESSMENT1 "60000000-0000-4000-8000-000000000018"
#define ASSESSMENT2 "60000000-0000-4000-8000-000000000019" #define ASSESSMENT2 "60000000-0000-4000-8000-000000000019"
#define FACT "70000000-0000-4000-8000-000000000018" #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" #define AT "2026-07-30T10:00:00Z"
typedef struct{char*dir,*path;Database*database;}Fixture; 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); 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); 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 dusurpation indéterminée",
"Document présumé utilisé dans une usurpation didentité",
"Utilisation dans une usurpation didentité 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 dinstaller 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) static void test_dao_history_and_relations(void)
{ {
Fixture f=fixture_new();GError*e=NULL; 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; Fixture f=fixture_new();database_close(f.database);f.database=NULL;
sqlite3*d=NULL;g_assert_cmpint(sqlite3_open(f.path,&d),==,SQLITE_OK); sqlite3*d=NULL;g_assert_cmpint(sqlite3_open(f.path,&d),==,SQLITE_OK);
exec_ok(d,"PRAGMA foreign_keys=OFF;" 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_ocr_field_projections;DROP TABLE person_profile_fields;"
"DROP TABLE person_identification_assessments;" "DROP TABLE person_identification_assessments;"
"DROP TABLE person_evidence_factual_relations;" "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*version=scalar(d,"SELECT value FROM metadata WHERE key='schema_version';");
char*values=scalar(d,"SELECT raw_value||':'||normalized_value||':'||corrected_value" char*values=scalar(d,"SELECT raw_value||':'||normalized_value||':'||corrected_value"
"||':'||confirmation_state FROM identity_field_observations WHERE id='" FIELD "';"); "||':'||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"); g_assert_cmpstr(values,==,"BRUT SPECIMEN:BRUT SPECIMEN:CORRIGÉ SPECIMEN:unconfirmed");
sqlite3_close(d);g_free(version);g_free(values); sqlite3_close(d);g_free(version);g_free(values);
f.database=database_open(f.path);g_assert_true(database_migrate_to_latest(f.database)); 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_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/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/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();}

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 dune personne dans une enquête, lutilisateur 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 quils contiennent.
Le cas des documents didentité nécessite également un traitement
forensique strict :
- conservation de loriginal ;
- 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 lidentité réelle ou
lauthenticité du document.
## Objectif
Créer un assistant fluide permettant, depuis la fenêtre de création dune
personne :
1. de sélectionner son rôle ou sa nature dans lenquête ;
2. de choisir une preuve déjà importée ;
3. dimporter 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. dutiliser lOCR pour préremplir les champs dun document didentité ;
8. de corriger manuellement les données proposées ;
9. de conserver la provenance complète de chaque valeur ;
10. de garantir lintégrité du fichier original.
## Terminologie
Ne pas confondre :
- le rôle de la personne dans lenquête ;
- son niveau didentification ;
- lauthenticité 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.
Lapplication ne doit jamais transformer automatiquement cette personne
en auteur identifié.
## Fenêtre de création dune personne
Ajouter un champ contrôlé :
Rôle dans lenquê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 dune preuve
Depuis la fenêtre de création dune personne, proposer deux actions :
- Sélectionner une preuve existante ;
- Importer une nouvelle preuve.
Lutilisateur 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 dimport ;
- empreinte SHA-256 abrégée ;
- description éventuelle ;
- état dintégrité.
Prévoir une recherche et un filtrage par type.
### Nouvelle preuve
Limport déclenché depuis la fenêtre doit utiliser le mécanisme forensique
central du projet :
- copie contrôlée dans lenquê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 limport, 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 dimport 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 à lajustement ;
- défilement lorsque limage est agrandie ;
- message explicite lorsque laperç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.
Laperç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 didentité ;
- Carte nationale didentité ;
- 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 lutilisateur doit
toujours pouvoir le corriger avant validation.
Le type choisi détermine les analyses proposées.
## Document didentité
Lorsquune preuve est qualifiée comme document didentité, proposer :
Analyser le document
Le traitement doit être asynchrone et annulable.
Il doit :
1. vérifier lintégrité de la preuve ;
2. créer une copie de travail ;
3. conserver loriginal strictement intact ;
4. appliquer les conversions uniquement à la copie ;
5. exécuter lOCR ;
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 dexpiration ;
- 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 :
- laperç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 dextraction ;
- langue OCR ;
- version de loutil ;
- date UTC ;
- preuve source ;
- page ou zone source lorsque disponible.
Une correction manuelle ne doit jamais écraser le résultat OCR brut.
Lutilisateur 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.
LOCR ne doit jamais conclure :
- que le document est authentique ;
- que la personne figurant sur le document est lauteur ;
- que lidentité 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 lauteur ;
- Identité réelle de ;
- A usurpé lidentité 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 lanalyse en cas de divergence ;
- ne jamais réécrire lempreinte 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 dorientation ;
- 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 linterface.
La fermeture ou lannulation 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 dune preuve existante ;
3. import dune 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 didentité déclenchant lOCR ;
11. autre type ne déclenchant pas automatiquement lOCR ;
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 dun 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 lanalyse ;
25. changement denquête pendant lanalyse ;
26. aucune écriture dans la mauvaise base ;
27. aucune donnée réelle dans les fixtures ;
28. aucune conclusion automatique sur lauthenticité ;
29. aucune fusion automatique avec lauteur présumé ;
30. réouverture de lenquête avec données persistantes.
Séparer autant que possible :
- services dimport ;
- génération daperçu ;
- OCR ;
- modèle de révision ;
- DAO ;
- tests GTK ciblés ;
- validation visuelle manuelle.
## Critères dacceptation
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 didentité peut être analysé par OCR ;
- les propositions OCR sont modifiables avant intégration ;
- le texte OCR brut nest jamais écrasé ;
- loriginal reste inchangé ;
- la provenance est persistée ;
- seules les valeurs confirmées alimentent la personne ;
- aucune identité nest attribuée automatiquement à lauteur 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 didentité réels ;
- captures réelles ;
- e-mails réels ;
- données bancaires réelles ;
- vidéos réelles ;
- autres preuves de lenquê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