feat(person): intégrer la traçabilité V18 atomique

This commit is contained in:
grayTerminal-sh 2026-07-30 11:10:32 +02:00
parent d7131aa1b6
commit 4b2fb93076
38 changed files with 1698 additions and 148 deletions

1
.gitignore vendored
View file

@ -5,6 +5,7 @@ labfy-investigation
# Executables de tests
/tests/test_*
/tests/fake_document_tool
# Conserver les sources des tests
!/tests/test_*.c

View file

@ -8,6 +8,36 @@ endif
.DEFAULT_GOAL := all
SOURCE_SIZE_LIMIT := 2000
# Exceptions historiques : plafonds constatés avant la tranche V18.
SOURCE_SIZE_EXCEPTIONS := \
src/core/application.c:9325 \
src/widgets/investigation_graph_view.c:4901 \
src/widgets/workspace.c:4071 \
src/views/main_window.c:2239 \
src/views/create_relation_dialog.c:2043
.PHONY: check-source-size
check-source-size:
@limit=$(SOURCE_SIZE_LIMIT); failed=0; \
files="$$(git ls-files --cached --others --exclude-standard -- \
'src/*.c' 'src/**/*.c' | sort -u)"; \
for file in $$files; do \
lines=$$(wc -l < "$$file"); allowed=$$limit; historical=0; \
for exception in $(SOURCE_SIZE_EXCEPTIONS); do \
case "$$exception" in "$$file":*) \
allowed=$${exception##*:}; historical=1;; esac; \
done; \
if [ $$historical -eq 1 ]; then \
echo "EXCEPTION HISTORIQUE $$file : $$lines/$$allowed lignes (limite normale $$limit)"; \
fi; \
if [ $$lines -gt $$allowed ]; then \
echo "ERREUR taille source $$file : $$lines lignes, maximum $$allowed"; \
failed=1; \
fi; \
done; \
exit $$failed
CFLAGS = -std=c17 \
-Wall \
-Wextra \
@ -160,6 +190,7 @@ TEST_PDF_ANALYSIS := tests/test_pdf_analysis
TEST_DOCUMENT_TOOL_RUNNER := tests/test_document_tool_runner
TEST_IDENTITY_OCR := tests/test_identity_ocr
TEST_IDENTITY_OCR_PREPROCESSOR := tests/test_identity_ocr_preprocessor
TEST_IDENTITY_TRACEABILITY := tests/test_identity_traceability
TEST_OCR_PROVENANCE_OVERLAY_GTK := tests/test_ocr_provenance_overlay_gtk
TEST_EVIDENCE_METADATA_DIALOG_GTK := tests/test_evidence_metadata_dialog_gtk
TEST_EVIDENCE_IDENTITY_IMPORT_GTK := tests/test_evidence_identity_import_gtk
@ -200,6 +231,17 @@ $(TEST_IDENTITY_OCR_PREPROCESSOR): tests/test_identity_ocr_preprocessor.c \
src/core/tool_registry.c
$(CC) $(CFLAGS) $^ -o $@ $(LDFLAGS)
$(TEST_IDENTITY_TRACEABILITY): tests/test_identity_traceability.c \
src/models/identity_traceability.c src/dao/identity_traceability_dao.c \
src/views/person_vocabulary_adapter.c \
src/models/person_role_assignment.c \
src/models/identity_ocr.c src/dao/identity_ocr_dao.c \
src/core/relation_type_normalizer.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_OCR_PROVENANCE_OVERLAY_GTK): \
tests/test_ocr_provenance_overlay_gtk.c \
src/widgets/ocr_provenance_overlay.c src/core/ocr_region_geometry.c \
@ -890,6 +932,8 @@ $(TEST_PERSON_DIALOG_LIFECYCLE): tests/test_person_dialog_lifecycle.c \
$(TEST_CREATE_PERSON_DIALOG_GTK): tests/test_create_person_dialog_gtk.c \
src/views/create_person_dialog.c src/views/dialog_geometry.c \
src/views/person_vocabulary_adapter.c \
src/views/identity_ocr_option_adapter.c \
src/core/person_dialog_lifecycle.c \
src/core/person_confirmation_summary.c \
src/core/evidence_staging.c src/core/evidence_staging_task.c \
@ -908,12 +952,18 @@ $(TEST_CREATE_PERSON_DIALOG_GTK): tests/test_create_person_dialog_gtk.c \
src/core/background_task.c src/core/task_manager.c \
src/models/evidence_selection_model.c src/models/evidence_record.c \
src/models/person_role_assignment.c \
src/models/person_evidence_selection.c
src/models/person_evidence_selection.c \
src/models/identity_traceability.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_CREATE_PERSON_DIALOG_OCR_GTK): \
tests/test_create_person_dialog_ocr_gtk.c \
src/views/create_person_dialog.c src/views/dialog_geometry.c \
src/views/person_vocabulary_adapter.c \
src/views/identity_ocr_option_adapter.c \
src/core/person_dialog_lifecycle.c \
src/core/person_confirmation_summary.c \
src/core/evidence_staging.c src/core/evidence_staging_task.c \
@ -937,6 +987,7 @@ $(TEST_CREATE_PERSON_DIALOG_OCR_GTK): \
src/models/evidence_selection_model.c src/models/evidence_record.c \
src/models/person_role_assignment.c \
src/models/person_evidence_selection.c \
src/models/identity_traceability.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 $(FAKE_DOCUMENT_TOOL)
@ -981,6 +1032,7 @@ $(TEST_PERSON_CREATION_COORDINATOR): \
src/core/person_creation_coordinator.c src/core/evidence_staging.c \
src/core/file_hash.c \
src/models/identity_ocr.c src/dao/identity_ocr_dao.c \
src/models/identity_traceability.c src/dao/identity_traceability_dao.c \
src/models/person_evidence_selection.c src/models/evidence_record.c \
src/models/person_role_assignment.c src/models/entity_record.c \
src/models/evidence_observation.c \
@ -1127,6 +1179,7 @@ test: \
$(TEST_DOCUMENT_TOOL_RUNNER) \
$(TEST_IDENTITY_OCR) \
$(TEST_IDENTITY_OCR_PREPROCESSOR) \
$(TEST_IDENTITY_TRACEABILITY) \
$(TEST_OCR_PROVENANCE_OVERLAY_GTK) \
$(TEST_EVIDENCE_METADATA_DIALOG_GTK) \
$(TEST_EVIDENCE_IDENTITY_IMPORT_GTK) \
@ -1221,6 +1274,7 @@ test: \
@$(TEST_DOCUMENT_TOOL_RUNNER)
@$(TEST_IDENTITY_OCR)
@$(TEST_IDENTITY_OCR_PREPROCESSOR)
@$(TEST_IDENTITY_TRACEABILITY)
@$(TEST_OCR_PROVENANCE_OVERLAY_GTK)
@$(TEST_EVIDENCE_METADATA_DIALOG_GTK)
@$(TEST_EVIDENCE_IDENTITY_IMPORT_GTK)
@ -1319,6 +1373,7 @@ clean:
$(TEST_DOCUMENT_TOOL_RUNNER) \
$(TEST_IDENTITY_OCR) \
$(TEST_IDENTITY_OCR_PREPROCESSOR) \
$(TEST_IDENTITY_TRACEABILITY) \
$(TEST_OCR_PROVENANCE_OVERLAY_GTK) \
$(TEST_EVIDENCE_METADATA_DIALOG_GTK) \
$(TEST_EVIDENCE_IDENTITY_IMPORT_GTK) \

View file

@ -306,6 +306,11 @@ CREATE TABLE IF NOT EXISTS identity_field_observations (
source_width INTEGER, source_height INTEGER, source_image_width INTEGER,
source_image_height INTEGER, display_order INTEGER NOT NULL,
reviewed_at TEXT NOT NULL, review_note TEXT,
confirmed_value TEXT,
confirmation_state TEXT NOT NULL DEFAULT 'unconfirmed'
CHECK(confirmation_state IN ('unconfirmed','human_confirmed')),
value_quality TEXT NOT NULL DEFAULT 'complete'
CHECK(value_quality IN ('complete','partial','uncertain','invalid')),
CHECK (
(origin = 'manual_entry' AND raw_value IS NULL
AND corrected_value IS NOT NULL AND confidence IS NULL)
@ -318,3 +323,116 @@ CREATE TABLE IF NOT EXISTS identity_field_observations (
FOREIGN KEY(ocr_run_id) REFERENCES identity_ocr_runs(id) ON DELETE CASCADE);
CREATE INDEX IF NOT EXISTS idx_identity_fields_observation
ON identity_field_observations(observation_id,display_order);
CREATE TABLE IF NOT EXISTS identification_status_vocabulary(
code TEXT PRIMARY KEY,label TEXT NOT NULL,description TEXT NOT NULL,
display_order INTEGER NOT NULL UNIQUE,
active INTEGER NOT NULL DEFAULT 1 CHECK(active IN(0,1)),
requires_justification INTEGER NOT NULL DEFAULT 0 CHECK(requires_justification IN(0,1)),
sensitive INTEGER NOT NULL DEFAULT 0 CHECK(sensitive IN(0,1)));
INSERT OR IGNORE INTO identification_status_vocabulary VALUES
('unknown','Inconnu','Aucune identification disponible.',10,1,0,0),
('unverified','Non vérifié','Identification déclarée, non vérifiée.',20,1,0,0),
('presumed','Présumé','Identification présumée à justifier.',30,1,1,1),
('partially_identified','Partiellement identifié','Identification incomplète.',40,1,0,0),
('confirmed','Confirmé','Identification confirmée à justifier.',50,1,1,1),
('disputed','Contesté','Identification contestée à justifier.',60,1,1,1);
CREATE TABLE IF NOT EXISTS person_role_vocabulary(
code TEXT PRIMARY KEY,label TEXT NOT NULL,description TEXT NOT NULL,
display_order INTEGER NOT NULL UNIQUE,active INTEGER NOT NULL CHECK(active IN(0,1)),
requires_justification INTEGER NOT NULL CHECK(requires_justification IN(0,1)),
sensitive INTEGER NOT NULL CHECK(sensitive IN(0,1)));
INSERT OR IGNORE INTO person_role_vocabulary VALUES
('alleged_author','Auteur présumé','Rôle allégué.',10,1,1,1),
('presented_identity','Identité présentée','Identité déclarée.',20,1,0,0),
('potentially_impersonated_identity','Identité potentiellement usurpée','Hypothèse sensible.',30,1,1,1),
('victim','Victime','Personne déclarée victime.',40,1,0,0),
('witness','Témoin','Personne déclarée témoin.',50,1,0,0),
('declared_bank_holder','Titulaire bancaire déclaré','Titulaire déclaré.',60,1,0,0),
('intermediary','Intermédiaire','Intermédiaire observé.',70,1,0,0),
('mentioned_person','Personne citée','Personne citée.',80,1,0,0),
('other','Autre','Rôle factuel non couvert.',90,1,1,0),
('uncategorized','Non catégorisée','Code historique.',100,1,0,0),
('alleged_scammer','Escroc présumé (historique)','Code historique.',110,0,1,1),
('suspect','Suspect (historique)','Code historique.',120,0,1,1),
('related_person','Personne liée (historique)','Code historique.',130,0,0,0),
('impersonated_identity','Identité usurpée (historique)','Code historique.',140,0,1,1);
CREATE TABLE IF NOT EXISTS document_authenticity_assessments(
id TEXT PRIMARY KEY,evidence_id TEXT NOT NULL,ocr_run_id TEXT,status TEXT NOT NULL
CHECK(status IN('indeterminate','presumed_authentic','suspicious',
'presumed_forged','confirmed_forged')),justification TEXT,
assessed_at TEXT NOT NULL,previous_assessment_id TEXT,technical_note TEXT,
origin TEXT NOT NULL CHECK(origin='human'),
CHECK(status='indeterminate' OR
(justification IS NOT NULL AND 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 SET NULL,
FOREIGN KEY(previous_assessment_id) REFERENCES document_authenticity_assessments(id));
CREATE TABLE IF NOT EXISTS person_evidence_factual_relations(
id TEXT PRIMARY KEY,person_id TEXT NOT NULL,evidence_id TEXT NOT NULL,
ocr_run_id TEXT,relation_type TEXT NOT NULL CHECK(relation_type IN(
'identity_observed_in','document_presented_in_name_of','declared_holder_in',
'data_extracted_from')),factual_note TEXT,observed_at TEXT NOT NULL,
origin TEXT NOT NULL CHECK(origin='human'),active INTEGER NOT NULL DEFAULT 1
CHECK(active IN(0,1)),FOREIGN KEY(person_id) REFERENCES entites(id),
FOREIGN KEY(evidence_id) REFERENCES preuves(id),
FOREIGN KEY(ocr_run_id) REFERENCES identity_ocr_runs(id) ON DELETE SET NULL);
CREATE TABLE IF NOT EXISTS person_identification_assessments(
id TEXT PRIMARY KEY,person_id TEXT NOT NULL,status_code TEXT NOT NULL,
justification TEXT,assessed_at TEXT NOT NULL,origin TEXT NOT NULL CHECK(origin='human'),
previous_assessment_id TEXT,CHECK(status_code<>'disputed' OR
(justification IS NOT NULL AND length(trim(justification))>0)),
FOREIGN KEY(person_id) REFERENCES entites(id),
FOREIGN KEY(status_code) REFERENCES identification_status_vocabulary(code),
FOREIGN KEY(previous_assessment_id) REFERENCES person_identification_assessments(id));
CREATE INDEX IF NOT EXISTS idx_authenticity_evidence_history
ON document_authenticity_assessments(evidence_id,assessed_at,id);
CREATE INDEX IF NOT EXISTS idx_person_evidence_factual_person
ON person_evidence_factual_relations(person_id,observed_at,id);
CREATE INDEX IF NOT EXISTS idx_person_evidence_factual_evidence
ON person_evidence_factual_relations(evidence_id,observed_at,id);
CREATE TRIGGER IF NOT EXISTS authenticity_v18_consistency
BEFORE INSERT ON document_authenticity_assessments BEGIN
SELECT CASE WHEN NEW.ocr_run_id IS NOT NULL AND NOT EXISTS(
SELECT 1 FROM identity_ocr_runs WHERE id=NEW.ocr_run_id
AND evidence_id=NEW.evidence_id)
THEN RAISE(ABORT,'OCR run belongs to another evidence')
WHEN NEW.previous_assessment_id IS NOT NULL AND NOT EXISTS(
SELECT 1 FROM document_authenticity_assessments
WHERE id=NEW.previous_assessment_id AND evidence_id=NEW.evidence_id)
THEN RAISE(ABORT,'previous assessment belongs to another evidence') END;
END;
CREATE TRIGGER IF NOT EXISTS factual_relation_v18_consistency
BEFORE INSERT ON person_evidence_factual_relations BEGIN
SELECT CASE WHEN NOT EXISTS(SELECT 1 FROM entites e JOIN types_entite t
ON t.id=e.type_id WHERE e.id=NEW.person_id AND t.code='person')
THEN RAISE(ABORT,'factual relation requires a person')
WHEN NEW.ocr_run_id IS NOT NULL AND NOT EXISTS(SELECT 1 FROM identity_ocr_runs
WHERE id=NEW.ocr_run_id AND evidence_id=NEW.evidence_id)
THEN RAISE(ABORT,'OCR run belongs to another evidence') END;
END;
CREATE TRIGGER IF NOT EXISTS identity_fields_v18_insert_guard
BEFORE INSERT ON identity_field_observations BEGIN
SELECT CASE
WHEN NEW.review_status IN('rejected','conflict') AND NEW.confirmed_value IS NOT NULL
THEN RAISE(ABORT,'non-projectable review status')
WHEN NEW.value_quality IN('uncertain','invalid') AND NEW.confirmed_value IS NOT NULL
THEN RAISE(ABORT,'non-projectable quality')
WHEN NEW.confirmed_value IS NOT NULL AND NEW.confirmation_state<>'human_confirmed'
THEN RAISE(ABORT,'human confirmation required')
WHEN NEW.confirmation_state='human_confirmed' AND NEW.confirmed_value IS NULL
THEN RAISE(ABORT,'confirmed value required') END;
END;
CREATE TRIGGER IF NOT EXISTS identity_fields_v18_update_guard
BEFORE UPDATE ON identity_field_observations BEGIN
SELECT CASE
WHEN NEW.review_status IN('rejected','conflict') AND NEW.confirmed_value IS NOT NULL
THEN RAISE(ABORT,'non-projectable review status')
WHEN NEW.value_quality IN('uncertain','invalid') AND NEW.confirmed_value IS NOT NULL
THEN RAISE(ABORT,'non-projectable quality')
WHEN NEW.confirmed_value IS NOT NULL AND NEW.confirmation_state<>'human_confirmed'
THEN RAISE(ABORT,'human confirmation required')
WHEN NEW.confirmation_state='human_confirmed' AND NEW.confirmed_value IS NULL
THEN RAISE(ABORT,'confirmed value required') END;
END;

217
database/schema_v18.sql Normal file
View file

@ -0,0 +1,217 @@
/* Migration V18 — traçabilité humaine des documents d'identité. */
CREATE TABLE identification_status_vocabulary (
code TEXT PRIMARY KEY,
label TEXT NOT NULL,
description TEXT NOT NULL,
display_order INTEGER NOT NULL UNIQUE,
active INTEGER NOT NULL DEFAULT 1 CHECK(active IN (0,1)),
requires_justification INTEGER NOT NULL DEFAULT 0
CHECK(requires_justification IN (0,1)),
sensitive INTEGER NOT NULL DEFAULT 0 CHECK(sensitive IN (0,1))
);
INSERT INTO identification_status_vocabulary VALUES
('unknown','Inconnu','Aucune identification disponible.',10,1,0,0),
('unverified','Non vérifié','Identification déclarée, non vérifiée.',20,1,0,0),
('presumed','Présumé','Identification présumée à justifier.',30,1,1,1),
('partially_identified','Partiellement identifié','Identification incomplète.',40,1,0,0),
('confirmed','Confirmé','Identification confirmée à justifier.',50,1,1,1),
('disputed','Contesté','Identification contestée à justifier.',60,1,1,1);
CREATE TABLE person_role_vocabulary (
code TEXT PRIMARY KEY,
label TEXT NOT NULL,
description TEXT NOT NULL,
display_order INTEGER NOT NULL UNIQUE,
active INTEGER NOT NULL DEFAULT 1 CHECK(active IN (0,1)),
requires_justification INTEGER NOT NULL DEFAULT 0
CHECK(requires_justification IN (0,1)),
sensitive INTEGER NOT NULL DEFAULT 0 CHECK(sensitive IN (0,1))
);
INSERT INTO person_role_vocabulary VALUES
('alleged_author','Auteur présumé','Rôle allégué, sans attribution automatique.',10,1,1,1),
('presented_identity','Identité présentée','Identité déclarée ou présentée dans une preuve.',20,1,0,0),
('potentially_impersonated_identity','Identité potentiellement usurpée','Hypothèse sensible nécessitant une justification.',30,1,1,1),
('victim','Victime','Personne déclarée victime.',40,1,0,0),
('witness','Témoin','Personne déclarée témoin.',50,1,0,0),
('declared_bank_holder','Titulaire bancaire déclaré','Titulaire déclaré par une source.',60,1,0,0),
('intermediary','Intermédiaire','Personne observée comme intermédiaire.',70,1,0,0),
('mentioned_person','Personne citée','Personne seulement citée.',80,1,0,0),
('other','Autre','Rôle factuel non couvert.',90,1,1,0),
('uncategorized','Non catégorisée','Code historique.',100,1,0,0),
('alleged_scammer','Escroc présumé (historique)','Code historique sensible.',110,0,1,1),
('suspect','Suspect (historique)','Code historique sensible.',120,0,1,1),
('related_person','Personne liée (historique)','Code historique.',130,0,0,0),
('impersonated_identity','Identité usurpée (historique)','Code historique sensible.',140,0,1,1);
CREATE TABLE document_authenticity_assessments (
id TEXT PRIMARY KEY,
evidence_id TEXT NOT NULL,
ocr_run_id TEXT,
status TEXT NOT NULL CHECK(status IN (
'indeterminate','presumed_authentic','suspicious',
'presumed_forged','confirmed_forged')),
justification TEXT,
assessed_at TEXT NOT NULL CHECK(length(assessed_at)=20),
previous_assessment_id TEXT,
technical_note TEXT,
origin TEXT NOT NULL CHECK(origin='human'),
CHECK(status='indeterminate' OR
(justification IS NOT NULL AND 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 SET NULL,
FOREIGN KEY(previous_assessment_id)
REFERENCES document_authenticity_assessments(id) ON DELETE RESTRICT
);
CREATE INDEX idx_authenticity_evidence_history
ON document_authenticity_assessments(evidence_id,assessed_at,id);
CREATE UNIQUE INDEX idx_authenticity_previous
ON document_authenticity_assessments(previous_assessment_id)
WHERE previous_assessment_id IS NOT NULL;
CREATE TABLE person_evidence_factual_relations (
id TEXT PRIMARY KEY,
person_id TEXT NOT NULL,
evidence_id TEXT NOT NULL,
ocr_run_id TEXT,
relation_type TEXT NOT NULL CHECK(relation_type IN (
'identity_observed_in','document_presented_in_name_of',
'declared_holder_in','data_extracted_from')),
factual_note TEXT,
observed_at TEXT NOT NULL CHECK(length(observed_at)=20),
origin TEXT NOT NULL CHECK(origin='human'),
active INTEGER NOT NULL DEFAULT 1 CHECK(active IN (0,1)),
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 SET NULL
);
CREATE INDEX idx_person_evidence_factual_person
ON person_evidence_factual_relations(person_id,observed_at,id);
CREATE INDEX idx_person_evidence_factual_evidence
ON person_evidence_factual_relations(evidence_id,observed_at,id);
CREATE TRIGGER authenticity_v18_consistency
BEFORE INSERT ON document_authenticity_assessments BEGIN
SELECT CASE
WHEN NEW.ocr_run_id IS NOT NULL AND NOT EXISTS(
SELECT 1 FROM identity_ocr_runs
WHERE id=NEW.ocr_run_id AND evidence_id=NEW.evidence_id)
THEN RAISE(ABORT,'OCR run belongs to another evidence')
WHEN NEW.previous_assessment_id IS NOT NULL AND NOT EXISTS(
SELECT 1 FROM document_authenticity_assessments
WHERE id=NEW.previous_assessment_id AND evidence_id=NEW.evidence_id)
THEN RAISE(ABORT,'previous assessment belongs to another evidence') END;
END;
CREATE TRIGGER factual_relation_v18_consistency
BEFORE INSERT ON person_evidence_factual_relations BEGIN
SELECT CASE
WHEN NOT EXISTS(SELECT 1 FROM entites e JOIN types_entite t ON t.id=e.type_id
WHERE e.id=NEW.person_id AND t.code='person')
THEN RAISE(ABORT,'factual relation requires a person')
WHEN NEW.ocr_run_id IS NOT NULL AND NOT EXISTS(
SELECT 1 FROM identity_ocr_runs
WHERE id=NEW.ocr_run_id AND evidence_id=NEW.evidence_id)
THEN RAISE(ABORT,'OCR run belongs to another evidence') END;
END;
CREATE TABLE person_identification_assessments (
id TEXT PRIMARY KEY,
person_id TEXT NOT NULL,
status_code TEXT NOT NULL,
justification TEXT,
assessed_at TEXT NOT NULL CHECK(length(assessed_at)=20),
origin TEXT NOT NULL CHECK(origin='human'),
previous_assessment_id TEXT,
CHECK(status_code<>'disputed' OR
(justification IS NOT NULL AND length(trim(justification))>0)),
FOREIGN KEY(person_id) REFERENCES entites(id) ON DELETE CASCADE,
FOREIGN KEY(status_code) REFERENCES identification_status_vocabulary(code)
ON DELETE RESTRICT,
FOREIGN KEY(previous_assessment_id)
REFERENCES person_identification_assessments(id) ON DELETE RESTRICT
);
CREATE TABLE identity_field_observations_v18 (
id TEXT PRIMARY KEY,
observation_id TEXT NOT NULL,
field_code TEXT NOT NULL,
raw_value TEXT,
corrected_value TEXT,
normalized_value TEXT,
confidence REAL CHECK(confidence IS NULL OR confidence BETWEEN 0 AND 100),
review_status TEXT NOT NULL CHECK(review_status IN
('proposed','accepted','modified','rejected','conflict')),
origin TEXT NOT NULL CHECK(origin IN
('ocr','mrz','manual_override','manual_entry')),
evidence_id TEXT NOT NULL,
ocr_run_id TEXT NOT NULL,
page_number INTEGER NOT NULL CHECK(page_number>0),
source_x INTEGER,source_y INTEGER,source_width INTEGER,source_height INTEGER,
source_image_width INTEGER,source_image_height INTEGER,
display_order INTEGER NOT NULL CHECK(display_order>=0),
reviewed_at TEXT NOT NULL CHECK(length(reviewed_at)=20),
review_note TEXT,
confirmed_value TEXT,
confirmation_state TEXT NOT NULL DEFAULT 'unconfirmed'
CHECK(confirmation_state IN ('unconfirmed','human_confirmed')),
value_quality TEXT NOT NULL DEFAULT 'complete'
CHECK(value_quality IN ('complete','partial','uncertain','invalid')),
CHECK((origin='manual_entry' AND raw_value IS NULL
AND corrected_value IS NOT NULL AND confidence IS NULL)
OR (origin<>'manual_entry' AND raw_value IS NOT NULL)),
FOREIGN KEY(observation_id) REFERENCES identity_document_observations(id)
ON DELETE CASCADE,
FOREIGN KEY(evidence_id) REFERENCES preuves(id) ON DELETE CASCADE,
FOREIGN KEY(ocr_run_id) REFERENCES identity_ocr_runs(id) ON DELETE CASCADE
);
INSERT INTO identity_field_observations_v18(
id,observation_id,field_code,raw_value,corrected_value,normalized_value,
confidence,review_status,origin,evidence_id,ocr_run_id,page_number,
source_x,source_y,source_width,source_height,source_image_width,
source_image_height,display_order,reviewed_at,review_note)
SELECT id,observation_id,field_code,raw_value,corrected_value,normalized_value,
confidence,review_status,origin,evidence_id,ocr_run_id,page_number,
source_x,source_y,source_width,source_height,source_image_width,
source_image_height,display_order,reviewed_at,review_note
FROM identity_field_observations;
DROP TABLE identity_field_observations;
ALTER TABLE identity_field_observations_v18
RENAME TO identity_field_observations;
CREATE INDEX idx_identity_fields_observation
ON identity_field_observations(observation_id,display_order);
CREATE TRIGGER identity_fields_v18_insert_guard
BEFORE INSERT ON identity_field_observations
BEGIN
SELECT CASE
WHEN NEW.review_status IN ('rejected','conflict')
AND NEW.confirmed_value IS NOT NULL
THEN RAISE(ABORT,'rejected or conflict field cannot be confirmed')
WHEN NEW.value_quality IN ('uncertain','invalid')
AND NEW.confirmed_value IS NOT NULL
THEN RAISE(ABORT,'uncertain or invalid field cannot be confirmed')
WHEN NEW.confirmed_value IS NOT NULL
AND NEW.confirmation_state<>'human_confirmed'
THEN RAISE(ABORT,'confirmed value requires human confirmation')
WHEN NEW.confirmation_state='human_confirmed'
AND NEW.confirmed_value IS NULL
THEN RAISE(ABORT,'human confirmation requires a value')
END;
END;
CREATE TRIGGER identity_fields_v18_update_guard
BEFORE UPDATE ON identity_field_observations
BEGIN
SELECT CASE
WHEN NEW.review_status IN ('rejected','conflict')
AND NEW.confirmed_value IS NOT NULL
THEN RAISE(ABORT,'rejected or conflict field cannot be confirmed')
WHEN NEW.value_quality IN ('uncertain','invalid')
AND NEW.confirmed_value IS NOT NULL
THEN RAISE(ABORT,'uncertain or invalid field cannot be confirmed')
WHEN NEW.confirmed_value IS NOT NULL
AND NEW.confirmation_state<>'human_confirmed'
THEN RAISE(ABORT,'confirmed value requires human confirmation')
WHEN NEW.confirmation_state='human_confirmed'
AND NEW.confirmed_value IS NULL
THEN RAISE(ABORT,'human confirmation requires a value')
END;
END;

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 :** V17
> **Schéma SQLite courant :** V18
## Personnes contextuelles — SQLite V14
@ -99,6 +99,31 @@ première allocation réelle puis reste entièrement modifiable. Les actions
restent fixes en bas. Cette politique exclut les alertes simples, les popups
`GtkDropDown` et les sélecteurs de fichiers natifs.
## Traçabilité didentité — SQLite V18
V18 sépare quatre dimensions qui ne doivent jamais être confondues :
lidentification dune personne, son rôle contextuel, lauthenticité humaine
dun document et la qualité dune valeur OCR. Les statuts didentification et
les rôles proviennent de vocabulaires SQLite ordonnés. Les codes historiques
de rôles sont conservés, mais les rôles sensibles ne sont jamais assignés par
un moteur automatique.
`document_authenticity_assessments` conserve un historique append-only
dévaluations humaines. Toute conclusion autre que `indeterminate` exige une
justification. `person_evidence_factual_relations` porte uniquement quatre
relations factuelles contrôlées et humaines ; elle ne remplace ni
`preuve_entites`, rattachement générique, ni `relations`, relation entre
entités. Aucun type « auteur », « identité réelle », « a falsifié » ou
« a usurpé » nest accepté.
Les observations OCR conservent désormais séparément `raw_value`,
`normalized_value`, `corrected_value` et `confirmed_value`. La confirmation
humaine et la qualification `complete`, `partial`, `uncertain` ou `invalid`
sont orthogonales au statut de révision. Les contraintes SQLite et le service
de validation interdisent la projection des valeurs rejetées, en conflit,
incertaines ou invalides. Cette tranche ne réalise encore aucune projection
automatique et ne fournit pas linterface GTK complète de saisie.
---
## 1. Objectif
@ -395,7 +420,7 @@ validation du chemin
création de l'arborescence
initialisation transactionnelle de SQLite V17
initialisation transactionnelle de SQLite V18
création de l'identité de l'enquête

View file

@ -17,7 +17,7 @@ achèvement du ticket #109.
> **Dernière mise à jour :** 2026-07-30
> **État du projet :** développement actif
> **Schéma SQLite courant :** V17
> **Schéma SQLite courant :** V18
> **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'à V17 ;
- infrastructure SQLite et migrations jusqu'à V18 ;
- 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 ;
@ -175,21 +175,19 @@ PARTIEL — VALIDATION MANUELLE DES TRANCHES LIVRÉES RÉUSSIE
- consultation, révision sans Tesseract et nouvelle analyse depuis la fiche ;
- provenance graphique et aperçu partagé avec PDF multipage ;
- politique commune de géométrie des dialogues GTK métiers.
- fondations V18 : authenticité humaine historisée, relations factuelles
typées, vocabulaires didentification et de rôles, cycle OCR jusquà la
valeur explicitement confirmée et contraintes anti-automatisme.
Limitations restantes :
1. lOCR groupé de plusieurs preuves dans une même opération nest pas pris en
charge ;
2. le statut contrôlé dauthenticité du document et la justification
obligatoire des statuts affirmatifs ne sont pas implémentés ;
3. la relation factuelle typée entre personne et preuve et certains états
didentification demandés restent incomplets ;
4. une partie du vocabulaire des rôles demeure codée en dur ;
5. la provenance uniforme de tous les dérivés et la projection des seules
2. linterface GTK complète de saisie de lauthenticité, des relations
factuelles, des états et des rôles V18 reste à construire ;
3. la provenance uniforme de tous les dérivés et la projection des seules
valeurs OCR confirmées vers les attributs structurés de la personne restent
partielles ;
6. les tests négatifs garantissant labsence de relation automatique
attribuant lidentité ou le rôle dauteur doivent encore être renforcés.
Ces limites interdisent de présenter le ticket #109 comme terminé.

View file

@ -1,5 +1,21 @@
# Architecture de la base de données
## Extension V18 — traçabilité didentité
La migration transactionnelle `database/schema_v18.sql` ajoute les
vocabulaires `identification_status_vocabulary` et
`person_role_vocabulary`, lhistorique
`document_authenticity_assessments`, les relations humaines contrôlées
`person_evidence_factual_relations` et lhistorique
`person_identification_assessments`.
`identity_field_observations` distingue les valeurs brute, normalisée,
corrigée et confirmée. `confirmation_state` prouve laction humaine et
`value_quality` sépare la complétude de la décision de révision. Les
contraintes refusent toute confirmation dun champ rejeté, en conflit,
incertain ou invalide. Aucun OCR ne crée une évaluation dauthenticité, une
relation factuelle, un rôle sensible ou un état `confirmed`.
> **Statut :** architecture courante
> **Version du schéma :** V10
> **Dernière mise à jour :** 2026-07-24

View file

@ -4,6 +4,7 @@
#include "core/person_entity_service.h"
#include "models/person_evidence_selection.h"
#include "models/identity_ocr.h"
#include "models/identity_traceability.h"
#include <gio/gio.h>
G_BEGIN_DECLS
@ -31,12 +32,19 @@ typedef enum {
PERSON_CREATION_FAILURE_INSERT_DOCUMENT_OBSERVATION,
PERSON_CREATION_FAILURE_INSERT_FIELD,
PERSON_CREATION_FAILURE_CREATE_SOURCE,
PERSON_CREATION_FAILURE_INSERT_FACTUAL_RELATION,
PERSON_CREATION_FAILURE_SESSION_BEFORE_COMMIT,
PERSON_CREATION_FAILURE_ARTIFACT_TEXT_CHANGED,
PERSON_CREATION_FAILURE_ARTIFACT_TSV_CHANGED,
PERSON_CREATION_FAILURE_COMMIT,
PERSON_CREATION_FAILURE_COMPENSATION
} PersonCreationFailurePoint;
typedef struct {
const char *evidence_selection_identifier;
const char *ocr_run_identifier;
const char *relation_type;
const char *factual_note;
} PersonCreationFactualRelationInput;
typedef gboolean (*PersonCreationSessionCheck)(gpointer user_data);
typedef struct {
PersonCreationFailurePoint failure_point;
@ -44,6 +52,8 @@ typedef struct {
gboolean inject_compensation_failure;
PersonCreationSessionCheck session_check;
gpointer session_check_data;
/** PersonCreationFactualRelationInput* empruntés, choix humains explicites. */
const GPtrArray *factual_relations;
} PersonCreationCoordinatorOptions;
typedef struct {
const char *collected_at;

View file

@ -21,6 +21,7 @@ typedef struct {
typedef struct {
char *id,*observation_id,*field_code,*raw_value,*corrected_value;
char *normalized_value,*review_status,*origin,*evidence_id,*ocr_run_id;
char *confirmed_value,*confirmation_state,*value_quality;
char *reviewed_at,*review_note; double confidence; gboolean has_confidence;
gint64 page_number,source_x,source_y,source_width,source_height;
gint64 source_image_width,source_image_height,display_order;
@ -47,6 +48,8 @@ IdentityFieldObservationRecord *identity_ocr_dao_find_field(
IdentityOcrDao *dao,const char *identifier,GError **error);
GPtrArray *identity_ocr_dao_list_fields_by_document(IdentityOcrDao *dao,
const char *document_identifier,GError **error);
GPtrArray *identity_ocr_dao_list_confirmed_fields(IdentityOcrDao *dao,
const char *document_identifier,GError **error);
IdentityOcrRun *identity_ocr_dao_load_run(
IdentityOcrDao *dao,const char *investigation_root,
const char *run_identifier,char **person_identifier,GError **error);

View file

@ -0,0 +1,29 @@
#ifndef LABFY_IDENTITY_TRACEABILITY_DAO_H
#define LABFY_IDENTITY_TRACEABILITY_DAO_H
#include "database/database.h"
#include "models/identity_traceability.h"
#include <glib.h>
G_BEGIN_DECLS
typedef struct IdentityTraceabilityDao IdentityTraceabilityDao;
IdentityTraceabilityDao *identity_traceability_dao_new(Database *database);
void identity_traceability_dao_free(IdentityTraceabilityDao *dao);
gboolean identity_traceability_dao_insert_authenticity(
IdentityTraceabilityDao *dao,const DocumentAuthenticityAssessment *assessment,
GError **error);
DocumentAuthenticityAssessment *identity_traceability_dao_find_authenticity(
IdentityTraceabilityDao *dao,const char *identifier,GError **error);
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_factual_relation(
IdentityTraceabilityDao *dao,const PersonEvidenceFactualRelation *relation,
GError **error);
GPtrArray *identity_traceability_dao_list_factual_relations(
IdentityTraceabilityDao *dao,const char *evidence_identifier,GError **error);
GPtrArray *identity_traceability_dao_list_roles(
IdentityTraceabilityDao *dao,gboolean include_inactive,GError **error);
GPtrArray *identity_traceability_dao_list_identification_statuses(
IdentityTraceabilityDao *dao,gboolean include_inactive,GError **error);
G_END_DECLS
#endif

View file

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

View file

@ -37,6 +37,22 @@ const char *identity_field_observation_get_raw_value(
const IdentityFieldObservation *field);
const char *identity_field_observation_get_corrected_value(
const IdentityFieldObservation *field);
const char *identity_field_observation_get_normalized_value(
const IdentityFieldObservation *field);
const char *identity_field_observation_get_confirmed_value(
const IdentityFieldObservation *field);
const char *identity_field_observation_get_value_quality(
const IdentityFieldObservation *field);
gboolean identity_field_observation_set_normalized_value(
IdentityFieldObservation *field, const char *value);
gboolean identity_field_observation_set_value_quality(
IdentityFieldObservation *field, const char *quality);
gboolean identity_field_observation_confirm(
IdentityFieldObservation *field, const char *value);
void identity_field_observation_clear_confirmation(
IdentityFieldObservation *field);
gboolean identity_field_observation_is_human_confirmed(
const IdentityFieldObservation *field);
IdentityReviewStatus identity_field_observation_get_status(
const IdentityFieldObservation *field);
double identity_field_observation_get_confidence(

View file

@ -0,0 +1,58 @@
#ifndef LABFY_IDENTITY_TRACEABILITY_H
#define LABFY_IDENTITY_TRACEABILITY_H
#include <glib.h>
G_BEGIN_DECLS
typedef struct {
char *identifier, *evidence_identifier, *ocr_run_identifier;
char *status, *justification, *assessed_at;
char *previous_identifier, *technical_note, *origin;
} DocumentAuthenticityAssessment;
typedef struct {
char *identifier, *person_identifier, *evidence_identifier;
char *ocr_run_identifier, *relation_type, *factual_note;
char *observed_at, *origin;
gboolean active;
} PersonEvidenceFactualRelation;
typedef struct {
char *code, *label, *description;
gint display_order;
gboolean active, requires_justification, sensitive;
} PersonRoleVocabularyEntry;
typedef PersonRoleVocabularyEntry IdentificationStatusVocabularyEntry;
gboolean identity_traceability_authenticity_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);
gboolean identity_traceability_field_is_projectable(
const char *review_status, const char *quality,
const char *confirmation_state, const char *confirmed_value);
DocumentAuthenticityAssessment *document_authenticity_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, const char *technical_note);
DocumentAuthenticityAssessment *document_authenticity_assessment_copy(
const DocumentAuthenticityAssessment *assessment);
void document_authenticity_assessment_free(
DocumentAuthenticityAssessment *assessment);
PersonEvidenceFactualRelation *person_evidence_factual_relation_new(
const char *identifier, const char *person_identifier,
const char *evidence_identifier, const char *ocr_run_identifier,
const char *relation_type, const char *factual_note,
const char *observed_at, gboolean active);
PersonEvidenceFactualRelation *person_evidence_factual_relation_copy(
const PersonEvidenceFactualRelation *relation);
void person_evidence_factual_relation_free(
PersonEvidenceFactualRelation *relation);
void person_role_vocabulary_entry_free(PersonRoleVocabularyEntry *entry);
void identification_status_vocabulary_entry_free(
IdentificationStatusVocabularyEntry *entry);
G_END_DECLS
#endif

View file

@ -27,7 +27,8 @@ typedef gboolean (*CreatePersonDialogSessionCheck)(gpointer user_data);
* @return TRUE si le dialogue a é présenté.
*/
gboolean create_person_dialog_present(GtkWindow *parent,
const GPtrArray *evidence_records, const char *investigation_root_path,
Database *database, const GPtrArray *evidence_records,
const char *investigation_root_path,
TaskManager *task_manager, const ToolInfo *tesseract_tool,
CreatePersonDialogSessionCheck session_check,
CreatePersonDialogCallback callback, gpointer user_data,

View file

@ -0,0 +1,9 @@
#ifndef LABFY_IDENTITY_OCR_OPTION_ADAPTER_H
#define LABFY_IDENTITY_OCR_OPTION_ADAPTER_H
#include <glib.h>
G_BEGIN_DECLS
const char *identity_ocr_option_adapter_language_label(const char *code);
guint identity_ocr_option_adapter_default_language_index(
const GPtrArray *codes);
G_END_DECLS
#endif

View file

@ -0,0 +1,33 @@
#ifndef LABFY_PERSON_VOCABULARY_ADAPTER_H
#define LABFY_PERSON_VOCABULARY_ADAPTER_H
#include "database/database.h"
#include "models/identity_traceability.h"
#include <gtk/gtk.h>
G_BEGIN_DECLS
typedef struct PersonVocabularyAdapter PersonVocabularyAdapter;
PersonVocabularyAdapter *person_vocabulary_adapter_new(
Database *database, GError **error);
void person_vocabulary_adapter_free(PersonVocabularyAdapter *adapter);
const GPtrArray *person_vocabulary_adapter_get_roles(
const PersonVocabularyAdapter *adapter);
const GPtrArray *person_vocabulary_adapter_get_statuses(
const PersonVocabularyAdapter *adapter);
GtkStringList *person_vocabulary_adapter_create_role_labels(
const PersonVocabularyAdapter *adapter);
GtkStringList *person_vocabulary_adapter_create_status_labels(
const PersonVocabularyAdapter *adapter);
const char *person_vocabulary_adapter_status_code(
const PersonVocabularyAdapter *adapter, guint index);
const char *person_vocabulary_adapter_status_label(
const PersonVocabularyAdapter *adapter, guint index);
GPtrArray *person_vocabulary_adapter_selected_role_labels(
const GPtrArray *buttons);
GPtrArray *person_vocabulary_adapter_create_role_buttons(
const PersonVocabularyAdapter *adapter, GtkBox *container);
GPtrArray *person_vocabulary_adapter_build_role_assignments(
const PersonVocabularyAdapter *adapter, const GPtrArray *buttons,
const char *evidence_identifier);
gboolean person_vocabulary_adapter_justification_valid(
const PersonRoleVocabularyEntry *entry, const char *justification);
G_END_DECLS
#endif

View file

@ -6116,6 +6116,7 @@ static void application_on_add_person_requested(gpointer user_data)
const ToolInfo *tesseract =
tool_registry_find(registry, "ocr.tesseract");
create_person_dialog_present(main_window_get_window(application->main_window),
investigation_session_get_database(application->session),
records, investigation_project_get_root_path(project),
application->task_manager, tesseract,
application_person_dialog_session_matches,

View file

@ -5,6 +5,7 @@
#include "dao/evidence_entity_dao.h"
#include "dao/person_role_assignment_dao.h"
#include "dao/identity_ocr_dao.h"
#include "dao/identity_traceability_dao.h"
#include "database/transaction.h"
#include <errno.h>
#include <glib/gstdio.h>
@ -261,6 +262,53 @@ static gboolean persist_ocr_runs(IdentityOcrDao *dao, const char *root,
return TRUE;
}
static gboolean persist_factual_relations(IdentityTraceabilityDao *dao,
const char *person_identifier,
const PersonEvidenceSelection *selection,
const GPtrArray *evidence_identifiers,
const GPtrArray *inputs, const char *timestamp,
FailureControl *failure, GError **error)
{
for (guint i = 0; inputs != NULL && i < inputs->len; i++) {
const PersonCreationFactualRelationInput *input =
g_ptr_array_index((GPtrArray *) inputs, i);
const char *evidence_identifier = NULL;
for (guint j = 0; input != NULL &&
j < person_evidence_selection_get_count(selection); j++) {
const PersonEvidenceSelectionItem *item =
person_evidence_selection_get(selection, j);
if (g_strcmp0(
person_evidence_selection_item_get_identifier(item),
input->evidence_selection_identifier) == 0) {
evidence_identifier = g_ptr_array_index(
(GPtrArray *) evidence_identifiers, j);
break;
}
}
char *identifier = g_uuid_string_random();
PersonEvidenceFactualRelation *relation =
input != NULL && evidence_identifier != NULL
? person_evidence_factual_relation_new(identifier,
person_identifier, evidence_identifier,
input->ocr_run_identifier, input->relation_type,
input->factual_note, timestamp, TRUE) : NULL;
g_free(identifier);
if (relation == NULL ||
fail_at(failure,
PERSON_CREATION_FAILURE_INSERT_FACTUAL_RELATION, error) ||
!identity_traceability_dao_insert_factual_relation(
dao, relation, error)) {
person_evidence_factual_relation_free(relation);
if (error != NULL && *error == NULL)
g_set_error_literal(error, coordinator_error(), 10,
"La relation factuelle explicite est invalide.");
return FALSE;
}
person_evidence_factual_relation_free(relation);
}
return TRUE;
}
static PersonCreationCoordinatorResult *person_creation_coordinator_execute_internal(
Database *database, const char *root, const PersonEntityInput *person,
const char *existing_person_identifier,
@ -276,6 +324,7 @@ static PersonCreationCoordinatorResult *person_creation_coordinator_execute_inte
EvidenceEntityDao *link_dao = NULL;
PersonRoleAssignmentDao *role_dao = NULL;
IdentityOcrDao *ocr_dao = NULL;
IdentityTraceabilityDao *traceability_dao = NULL;
EntityRecord *person_record = NULL;
GPtrArray *created_paths = g_ptr_array_new_with_free_func(g_free);
GPtrArray *created_directories = g_ptr_array_new_with_free_func(g_free);
@ -319,12 +368,14 @@ static PersonCreationCoordinatorResult *person_creation_coordinator_execute_inte
link_dao = evidence_entity_dao_new(database, error);
role_dao = person_role_assignment_dao_new(database, error);
ocr_dao = identity_ocr_dao_new(database);
traceability_dao = identity_traceability_dao_new(database);
person_record = entity_dao == NULL ? NULL : create_person
? build_person(person, result->person_identifier, timestamp, error)
: entity_dao_find_by_identifier(
entity_dao, result->person_identifier, error);
if (timestamp == NULL || entity_dao == NULL || evidence_dao == NULL ||
link_dao == NULL || role_dao == NULL || ocr_dao == NULL ||
traceability_dao == NULL ||
person_record == NULL ||
g_strcmp0(entity_record_get_type_identifier(person_record),
"person") != 0 ||
@ -419,6 +470,12 @@ static PersonCreationCoordinatorResult *person_creation_coordinator_execute_inte
created_paths, created_directories, &failure,
cancellable, error))
goto cleanup;
if (!persist_factual_relations(traceability_dao,
result->person_identifier, selection,
result->evidence_identifiers,
options != NULL ? options->factual_relations : NULL,
timestamp, &failure, error))
goto cleanup;
for (guint i = 0; ocr_runs != NULL && i < ocr_runs->len; i++) {
IdentityOcrRun *run = g_ptr_array_index((GPtrArray *) ocr_runs, i);
char *directory = g_build_filename(root, "02_Preuves_Traitees",
@ -513,6 +570,7 @@ cleanup:
evidence_entity_dao_free(link_dao);
person_role_assignment_dao_free(role_dao);
identity_ocr_dao_free(ocr_dao);
identity_traceability_dao_free(traceability_dao);
g_free(timestamp);
if (!success) {
person_creation_coordinator_result_free(result);

View file

@ -14,8 +14,12 @@
static gboolean person_entity_service_status_valid(const char *status)
{
return g_strcmp0(status, "unknown") == 0 ||
g_strcmp0(status, "unverified") == 0 ||
g_strcmp0(status, "presumed") == 0 ||
g_strcmp0(status, "partially_identified") == 0 ||
g_strcmp0(status, "suspected") == 0 ||
g_strcmp0(status, "confirmed") == 0;
g_strcmp0(status, "confirmed") == 0 ||
g_strcmp0(status, "disputed") == 0;
}
gboolean person_entity_service_update_display_name(Database *database,

View file

@ -66,24 +66,32 @@ gboolean identity_ocr_dao_insert(IdentityOcrDao*d,const char*person,
IdentityReviewStatus status=identity_field_observation_get_status(f);
const IdentitySourceBox*b=identity_field_observation_get_box(f);char*id=g_uuid_string_random();
s=database_statement_prepare(d->database,"INSERT INTO identity_field_observations("
"id,observation_id,field_code,raw_value,corrected_value,confidence,review_status,"
"id,observation_id,field_code,raw_value,corrected_value,normalized_value,"
"confidence,review_status,"
"origin,evidence_id,ocr_run_id,page_number,source_x,source_y,source_width,"
"source_height,source_image_width,source_image_height,display_order,reviewed_at)"
" VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?);");
"source_height,source_image_width,source_image_height,display_order,reviewed_at,"
"confirmed_value,confirmation_state,value_quality)"
" VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?);");
ok=s&&bind_text(s,1,id)&&bind_text(s,2,obs)&&bind_text(s,3,identity_field_observation_get_code(f))&&
bind_text(s,4,identity_field_observation_get_raw_value(f))&&
bind_text(s,5,identity_field_observation_get_corrected_value(f))&&
bind_text(s,6,identity_field_observation_get_normalized_value(f))&&
(identity_field_observation_get_confidence(f)>=0?
database_statement_bind_double(s,6,identity_field_observation_get_confidence(f)):
database_statement_bind_null(s,6))&&bind_text(s,7,review_status_text(status))&&
bind_text(s,8,identity_field_observation_get_origin(f))&&bind_text(s,9,evidence)&&
bind_text(s,10,identity_ocr_run_get_identifier(r))&&database_statement_bind_int64(s,11,identity_ocr_run_get_page(r));
for(int column=12;ok&&column<=17;column++){gint value=0;if(b&&b->available){
database_statement_bind_double(s,7,identity_field_observation_get_confidence(f)):
database_statement_bind_null(s,7))&&bind_text(s,8,review_status_text(status))&&
bind_text(s,9,identity_field_observation_get_origin(f))&&bind_text(s,10,evidence)&&
bind_text(s,11,identity_ocr_run_get_identifier(r))&&database_statement_bind_int64(s,12,identity_ocr_run_get_page(r));
for(int column=13;ok&&column<=18;column++){gint value=0;if(b&&b->available){
const gint values[]={b->x,b->y,b->width,b->height,b->image_width,b->image_height};
value=values[column-12];ok=database_statement_bind_int64(s,column,value);
value=values[column-13];ok=database_statement_bind_int64(s,column,value);
}else ok=database_statement_bind_null(s,column);}
ok=ok&&database_statement_bind_int64(s,18,identity_field_observation_get_order(f))&&
bind_text(s,19,timestamp)&&database_statement_step(s)==DATABASE_STATEMENT_STEP_DONE;
ok=ok&&database_statement_bind_int64(s,19,identity_field_observation_get_order(f))&&
bind_text(s,20,timestamp)&&
bind_text(s,21,identity_field_observation_get_confirmed_value(f))&&
bind_text(s,22,identity_field_observation_is_human_confirmed(f)
?"human_confirmed":"unconfirmed")&&
bind_text(s,23,identity_field_observation_get_value_quality(f))&&
database_statement_step(s)==DATABASE_STATEMENT_STEP_DONE;
database_statement_finalize(s);g_free(id);if(!ok){g_free(obs);goto fail;}}
g_free(obs);return TRUE;
fail:g_set_error_literal(error,g_quark_from_static_string("identity-ocr-dao"),1,
@ -111,6 +119,8 @@ void identity_document_observation_record_free(IdentityDocumentObservationRecord
void identity_field_observation_record_free(IdentityFieldObservationRecord*r)
{if(!r)return;FREE_FIELD(r,id);FREE_FIELD(r,observation_id);FREE_FIELD(r,field_code);
FREE_FIELD(r,raw_value);FREE_FIELD(r,corrected_value);FREE_FIELD(r,normalized_value);
FREE_FIELD(r,confirmed_value);FREE_FIELD(r,confirmation_state);
FREE_FIELD(r,value_quality);
FREE_FIELD(r,review_status);FREE_FIELD(r,origin);FREE_FIELD(r,evidence_id);
FREE_FIELD(r,ocr_run_id);FREE_FIELD(r,reviewed_at);FREE_FIELD(r,review_note);g_free(r);}
@ -188,7 +198,10 @@ static IdentityFieldObservationRecord *read_field(DatabaseStatement*s)
ok=database_statement_column_int64(s,12+(int)i,values[i]);
ok=ok&&database_statement_column_int64(s,18,&r->display_order)&&
database_statement_column_text(s,19,&r->reviewed_at)&&
database_statement_column_text(s,20,&r->review_note);
database_statement_column_text(s,20,&r->review_note)&&
database_statement_column_text(s,21,&r->confirmed_value)&&
database_statement_column_text(s,22,&r->confirmation_state)&&
database_statement_column_text(s,23,&r->value_quality);
if(!ok){identity_field_observation_record_free(r);return NULL;}return r;
}
static const char run_columns[]="id,evidence_id,expected_sha256,page_number,"
@ -203,7 +216,8 @@ static const char document_columns[]="id,person_id,evidence_id,ocr_run_id,"
static const char field_columns[]="id,observation_id,field_code,raw_value,"
"corrected_value,normalized_value,confidence,review_status,origin,evidence_id,"
"ocr_run_id,page_number,source_x,source_y,source_width,source_height,"
"source_image_width,source_image_height,display_order,reviewed_at,review_note";
"source_image_width,source_image_height,display_order,reviewed_at,review_note,"
"confirmed_value,confirmation_state,value_quality";
typedef gpointer(*ReadRecord)(DatabaseStatement*);
static gpointer read_run_record(DatabaseStatement*s){return read_run(s);}
@ -272,6 +286,29 @@ GPtrArray *identity_ocr_dao_list_fields_by_document(IdentityOcrDao*d,const char*
{return list_records(d,"identity_field_observations",field_columns,
"observation_id",i,read_field_record,
(GDestroyNotify)identity_field_observation_record_free,e);}
GPtrArray *identity_ocr_dao_list_confirmed_fields(IdentityOcrDao*d,
const char*i,GError**e)
{
if(!d||!i){g_set_error_literal(e,g_quark_from_static_string("identity-ocr-dao"),2,
"Lecture OCR invalide.");return NULL;}
char*sql=g_strdup_printf("SELECT %s FROM identity_field_observations "
"WHERE observation_id=? AND confirmation_state='human_confirmed' "
"AND confirmed_value IS NOT NULL AND review_status IN ('accepted','modified') "
"AND value_quality IN ('complete','partial') ORDER BY display_order,rowid;",
field_columns);
DatabaseStatement*s=database_statement_prepare(d->database,sql);g_free(sql);
GPtrArray*a=g_ptr_array_new_with_free_func(
(GDestroyNotify)identity_field_observation_record_free);
if(!s||!database_statement_bind_text(s,1,i))goto failed;
for(;;){DatabaseStatementStepResult step=database_statement_step(s);
if(step==DATABASE_STATEMENT_STEP_DONE)break;
if(step!=DATABASE_STATEMENT_STEP_ROW)goto failed;
IdentityFieldObservationRecord*r=read_field(s);if(!r)goto failed;g_ptr_array_add(a,r);}
database_statement_finalize(s);return a;
failed:database_statement_finalize(s);g_ptr_array_unref(a);
g_set_error_literal(e,g_quark_from_static_string("identity-ocr-dao"),4,
"Impossible de lire les champs OCR confirmés.");return NULL;
}
IdentityOcrRun *identity_ocr_dao_load_run(
IdentityOcrDao*d,const char*root,const char*identifier,
@ -329,6 +366,11 @@ IdentityOcrRun *identity_ocr_dao_load_run(
else if(g_strcmp0(f->review_status,"conflict")==0)
identity_field_observation_mark_conflict(field);
identity_field_observation_set_origin(field,f->origin);
if(f->normalized_value!=NULL)
identity_field_observation_set_normalized_value(field,f->normalized_value);
identity_field_observation_set_value_quality(field,f->value_quality);
if(g_strcmp0(f->confirmation_state,"human_confirmed")==0)
identity_field_observation_confirm(field,f->confirmed_value);
identity_ocr_run_add_field(run,field);
}
g_clear_pointer(&fields,g_ptr_array_unref);
@ -354,34 +396,40 @@ static gboolean update_review_fields(IdentityOcrDao*d,const char*observation,
char*id=g_uuid_string_random();
DatabaseStatement*s=database_statement_prepare(d->database,
"INSERT INTO identity_field_observations(id,observation_id,field_code,"
"raw_value,corrected_value,confidence,review_status,origin,evidence_id,"
"raw_value,corrected_value,normalized_value,confidence,review_status,origin,evidence_id,"
"ocr_run_id,page_number,source_x,source_y,source_width,source_height,"
"source_image_width,source_image_height,display_order,reviewed_at)"
" VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?);");
"source_image_width,source_image_height,display_order,reviewed_at,"
"confirmed_value,confirmation_state,value_quality)"
" VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?);");
gboolean ok=s&&bind_text(s,1,id)&&bind_text(s,2,observation)&&
bind_text(s,3,identity_field_observation_get_code(f))&&
bind_text(s,4,identity_field_observation_get_raw_value(f))&&
bind_text(s,5,identity_field_observation_get_corrected_value(f))&&
bind_text(s,6,identity_field_observation_get_normalized_value(f))&&
(identity_field_observation_get_confidence(f)>=0
?database_statement_bind_double(s,6,
?database_statement_bind_double(s,7,
identity_field_observation_get_confidence(f))
:database_statement_bind_null(s,6))&&
bind_text(s,7,review_status_text(
:database_statement_bind_null(s,7))&&
bind_text(s,8,review_status_text(
identity_field_observation_get_status(f)))&&
bind_text(s,8,identity_field_observation_get_origin(f))&&
bind_text(s,9,identity_ocr_run_get_evidence_id(run))&&
bind_text(s,10,identity_ocr_run_get_identifier(run))&&
database_statement_bind_int64(s,11,identity_ocr_run_get_page(run));
bind_text(s,9,identity_field_observation_get_origin(f))&&
bind_text(s,10,identity_ocr_run_get_evidence_id(run))&&
bind_text(s,11,identity_ocr_run_get_identifier(run))&&
database_statement_bind_int64(s,12,identity_ocr_run_get_page(run));
const gint values[]={b!=NULL?b->x:0,b!=NULL?b->y:0,
b!=NULL?b->width:0,b!=NULL?b->height:0,
b!=NULL?b->image_width:0,b!=NULL?b->image_height:0};
for(gint column=12;ok&&column<=17;column++)
for(gint column=13;ok&&column<=18;column++)
ok=b!=NULL&&b->available
?database_statement_bind_int64(s,column,values[column-12])
?database_statement_bind_int64(s,column,values[column-13])
:database_statement_bind_null(s,column);
ok=ok&&database_statement_bind_int64(s,18,
ok=ok&&database_statement_bind_int64(s,19,
identity_field_observation_get_order(f))&&
bind_text(s,19,timestamp)&&
bind_text(s,20,timestamp)&&
bind_text(s,21,identity_field_observation_get_confirmed_value(f))&&
bind_text(s,22,identity_field_observation_is_human_confirmed(f)
?"human_confirmed":"unconfirmed")&&
bind_text(s,23,identity_field_observation_get_value_quality(f))&&
database_statement_step(s)==DATABASE_STATEMENT_STEP_DONE;
database_statement_finalize(s);g_free(id);if(!ok)return FALSE;
}

View file

@ -0,0 +1,184 @@
#include "dao/identity_traceability_dao.h"
#include "database/statement.h"
struct IdentityTraceabilityDao{Database*database;};
static GQuark domain(void){return g_quark_from_static_string("identity-traceability-dao");}
static void fail(GError**e,const char*m){if(e&&*e==NULL)g_set_error_literal(e,domain(),1,m);}
static gboolean bind(DatabaseStatement*s,int i,const char*v)
{return v?database_statement_bind_text(s,i,v):database_statement_bind_null(s,i);}
IdentityTraceabilityDao *identity_traceability_dao_new(Database*d)
{if(!d)return NULL;IdentityTraceabilityDao*dao=g_new0(IdentityTraceabilityDao,1);
dao->database=d;return dao;}
void identity_traceability_dao_free(IdentityTraceabilityDao*d){g_free(d);}
gboolean identity_traceability_dao_insert_authenticity(
IdentityTraceabilityDao*d,const DocumentAuthenticityAssessment*a,GError**e)
{
DocumentAuthenticityAssessment*valid=a?document_authenticity_assessment_copy(a):NULL;
if(!d||!valid){fail(e,"Évaluation dauthenticité invalide.");return FALSE;}
DatabaseStatement*s=database_statement_prepare(d->database,
"INSERT INTO document_authenticity_assessments VALUES(?,?,?,?,?,?,?,?,?);");
gboolean ok=s&&bind(s,1,a->identifier)&&bind(s,2,a->evidence_identifier)&&
bind(s,3,a->ocr_run_identifier)&&bind(s,4,a->status)&&bind(s,5,a->justification)&&
bind(s,6,a->assessed_at)&&bind(s,7,a->previous_identifier)&&
bind(s,8,a->technical_note)&&bind(s,9,"human")&&
database_statement_step(s)==DATABASE_STATEMENT_STEP_DONE;
database_statement_finalize(s);document_authenticity_assessment_free(valid);
if(!ok)fail(e,"Impossible de conserver lévaluation dauthenticité.");
return ok;
}
static DocumentAuthenticityAssessment *read_auth(DatabaseStatement*s)
{
char *v[9]={0};gboolean ok=TRUE;
for(int i=0;i<9;i++)ok=ok&&database_statement_column_text(s,i,&v[i]);
DocumentAuthenticityAssessment*a=ok?document_authenticity_assessment_new(
v[0],v[1],v[2],v[3],v[4],v[5],v[6],v[7]):NULL;
for(int i=0;i<9;i++)g_free(v[i]);
return a;
}
static GPtrArray *auth_query(IdentityTraceabilityDao*d,const char*sql,
const char*value,GError**e)
{
if(!d||!value){fail(e,"Lecture dauthenticité invalide.");return NULL;}
DatabaseStatement*s=database_statement_prepare(d->database,sql);
GPtrArray*a=g_ptr_array_new_with_free_func(
(GDestroyNotify)document_authenticity_assessment_free);
if(!s||!bind(s,1,value))goto bad;
for(;;){DatabaseStatementStepResult step=database_statement_step(s);
if(step==DATABASE_STATEMENT_STEP_DONE)break;
if(step!=DATABASE_STATEMENT_STEP_ROW)goto bad;
DocumentAuthenticityAssessment*r=read_auth(s);if(!r)goto bad;g_ptr_array_add(a,r);}
database_statement_finalize(s);return a;
bad:database_statement_finalize(s);g_ptr_array_unref(a);
fail(e,"Impossible de lire lhistorique dauthenticité.");return NULL;
}
DocumentAuthenticityAssessment *identity_traceability_dao_find_authenticity(
IdentityTraceabilityDao*d,const char*id,GError**e)
{
GPtrArray*a=auth_query(d,"SELECT * FROM document_authenticity_assessments "
"WHERE id=? ORDER BY assessed_at,id;",id,e);if(!a)return NULL;
DocumentAuthenticityAssessment*r=a->len?document_authenticity_assessment_copy(
g_ptr_array_index(a,0)):NULL;g_ptr_array_unref(a);return r;
}
GPtrArray *identity_traceability_dao_list_authenticity(
IdentityTraceabilityDao*d,const char*id,GError**e)
{return auth_query(d,"SELECT * FROM document_authenticity_assessments "
"WHERE evidence_id=? ORDER BY assessed_at,id;",id,e);}
DocumentAuthenticityAssessment *identity_traceability_dao_current_authenticity(
IdentityTraceabilityDao*d,const char*id,GError**e)
{
GPtrArray*a=auth_query(d,"SELECT * FROM document_authenticity_assessments "
"WHERE evidence_id=? ORDER BY assessed_at DESC,id DESC LIMIT 1;",id,e);
if(!a)return NULL;
DocumentAuthenticityAssessment*r=a->len?
document_authenticity_assessment_copy(g_ptr_array_index(a,0)):NULL;
g_ptr_array_unref(a);return r;
}
gboolean identity_traceability_dao_insert_factual_relation(
IdentityTraceabilityDao*d,const PersonEvidenceFactualRelation*r,GError**e)
{
PersonEvidenceFactualRelation*valid=r?person_evidence_factual_relation_copy(r):NULL;
if(!d||!valid){fail(e,"Relation factuelle invalide.");return FALSE;}
DatabaseStatement*s=database_statement_prepare(d->database,
"INSERT INTO person_evidence_factual_relations VALUES(?,?,?,?,?,?,?,?,?);");
gboolean ok=s&&bind(s,1,r->identifier)&&bind(s,2,r->person_identifier)&&
bind(s,3,r->evidence_identifier)&&bind(s,4,r->ocr_run_identifier)&&
bind(s,5,r->relation_type)&&bind(s,6,r->factual_note)&&bind(s,7,r->observed_at)&&
bind(s,8,"human")&&database_statement_bind_int64(s,9,r->active?1:0)&&
database_statement_step(s)==DATABASE_STATEMENT_STEP_DONE;
database_statement_finalize(s);person_evidence_factual_relation_free(valid);
if(!ok)fail(e,"Impossible de conserver la relation factuelle.");
return ok;
}
GPtrArray *identity_traceability_dao_list_factual_relations(
IdentityTraceabilityDao*d,const char*id,GError**e)
{
if(!d||!id){fail(e,"Lecture des relations factuelles invalide.");return NULL;}
DatabaseStatement*s=database_statement_prepare(d->database,
"SELECT id,person_id,evidence_id,ocr_run_id,relation_type,factual_note,"
"observed_at,origin,active FROM person_evidence_factual_relations "
"WHERE evidence_id=? ORDER BY observed_at,id;");
GPtrArray*a=g_ptr_array_new_with_free_func(
(GDestroyNotify)person_evidence_factual_relation_free);
if(!s||!bind(s,1,id))goto bad;
for(;;){DatabaseStatementStepResult step=database_statement_step(s);
if(step==DATABASE_STATEMENT_STEP_DONE)break;
if(step!=DATABASE_STATEMENT_STEP_ROW)goto bad;
char*v[8]={0};gint64 active=0;gboolean ok=TRUE;
for(int i=0;i<8;i++)ok=ok&&database_statement_column_text(s,i,&v[i]);
ok=ok&&database_statement_column_int64(s,8,&active);
PersonEvidenceFactualRelation*r=ok?person_evidence_factual_relation_new(
v[0],v[1],v[2],v[3],v[4],v[5],v[6],active!=0):NULL;
for(int i=0;i<8;i++)g_free(v[i]);
if(!r)goto bad;
g_ptr_array_add(a,r);}
database_statement_finalize(s);return a;
bad:database_statement_finalize(s);g_ptr_array_unref(a);
fail(e,"Impossible de lire les relations factuelles.");return NULL;
}
GPtrArray *identity_traceability_dao_list_roles(
IdentityTraceabilityDao*d,gboolean inactive,GError**e)
{
if(!d){fail(e,"Lecture du vocabulaire invalide.");return NULL;}
DatabaseStatement*s=database_statement_prepare(d->database,
inactive?"SELECT code,label,description,display_order,active,"
"requires_justification,sensitive FROM person_role_vocabulary "
"ORDER BY display_order,code;":"SELECT code,label,description,display_order,"
"active,requires_justification,sensitive FROM person_role_vocabulary "
"WHERE active=1 ORDER BY display_order,code;");
GPtrArray*a=g_ptr_array_new_with_free_func(
(GDestroyNotify)person_role_vocabulary_entry_free);
if(!s)goto bad;
for(;;){DatabaseStatementStepResult step=database_statement_step(s);
if(step==DATABASE_STATEMENT_STEP_DONE)break;
if(step!=DATABASE_STATEMENT_STEP_ROW)goto bad;
PersonRoleVocabularyEntry*r=g_new0(PersonRoleVocabularyEntry,1);
gint64 order=0,active=0,required=0,sensitive=0;
gboolean ok=database_statement_column_text(s,0,&r->code)&&
database_statement_column_text(s,1,&r->label)&&
database_statement_column_text(s,2,&r->description)&&
database_statement_column_int64(s,3,&order)&&
database_statement_column_int64(s,4,&active)&&
database_statement_column_int64(s,5,&required)&&
database_statement_column_int64(s,6,&sensitive);
if(!ok){person_role_vocabulary_entry_free(r);goto bad;}
r->display_order=(gint)order;r->active=active!=0;
r->requires_justification=required!=0;r->sensitive=sensitive!=0;
g_ptr_array_add(a,r);}database_statement_finalize(s);return a;
bad:database_statement_finalize(s);g_ptr_array_unref(a);
fail(e,"Impossible de lire le vocabulaire des rôles.");return NULL;
}
GPtrArray *identity_traceability_dao_list_identification_statuses(
IdentityTraceabilityDao*d,gboolean inactive,GError**e)
{
if(!d){fail(e,"Lecture du vocabulaire invalide.");return NULL;}
DatabaseStatement*s=database_statement_prepare(d->database,
inactive?"SELECT code,label,description,display_order,active,"
"requires_justification,sensitive FROM identification_status_vocabulary "
"ORDER BY display_order,code;":"SELECT code,label,description,display_order,"
"active,requires_justification,sensitive FROM identification_status_vocabulary "
"WHERE active=1 ORDER BY display_order,code;");
GPtrArray*a=g_ptr_array_new_with_free_func(
(GDestroyNotify)identification_status_vocabulary_entry_free);
if(!s)goto bad;
for(;;){DatabaseStatementStepResult step=database_statement_step(s);
if(step==DATABASE_STATEMENT_STEP_DONE)break;
if(step!=DATABASE_STATEMENT_STEP_ROW)goto bad;
IdentificationStatusVocabularyEntry*r=g_new0(
IdentificationStatusVocabularyEntry,1);
gint64 order=0,active=0,required=0,sensitive=0;
gboolean ok=database_statement_column_text(s,0,&r->code)&&
database_statement_column_text(s,1,&r->label)&&
database_statement_column_text(s,2,&r->description)&&
database_statement_column_int64(s,3,&order)&&
database_statement_column_int64(s,4,&active)&&
database_statement_column_int64(s,5,&required)&&
database_statement_column_int64(s,6,&sensitive);
if(!ok){identification_status_vocabulary_entry_free(r);goto bad;}
r->display_order=(gint)order;r->active=active!=0;
r->requires_justification=required!=0;r->sensitive=sensitive!=0;
g_ptr_array_add(a,r);}
database_statement_finalize(s);return a;
bad:database_statement_finalize(s);g_ptr_array_unref(a);
fail(e,"Impossible de lire le vocabulaire des états didentification.");
return NULL;
}

View file

@ -30,6 +30,8 @@ gboolean person_role_assignment_dao_insert(PersonRoleAssignmentDao *dao,
"SELECT ?,?,?,?,?,?,?,?,? WHERE EXISTS(SELECT 1 FROM entites e "
"JOIN types_entite t ON t.id=e.type_id WHERE e.id=? AND t.code='person') "
"AND (? IS NULL OR EXISTS(SELECT 1 FROM preuves WHERE id=?)) "
"AND EXISTS(SELECT 1 FROM person_role_vocabulary v "
"WHERE v.code=? AND v.active=1) "
"RETURNING id;";
DatabaseStatement *statement = NULL;
GDateTime *now = NULL;
@ -65,6 +67,7 @@ gboolean person_role_assignment_dao_insert(PersonRoleAssignmentDao *dao,
!(input->evidence_identifier != NULL
? database_statement_bind_text(statement, 12, input->evidence_identifier)
: database_statement_bind_null(statement, 12)) ||
!database_statement_bind_text(statement, 13, input->role_code) ||
database_statement_step(statement) != DATABASE_STATEMENT_STEP_ROW)
goto failed;
success = TRUE;

View file

@ -16,12 +16,12 @@
/**
* @brief Version actuelle du schéma SQLite.
*/
#define DATABASE_SCHEMA_VERSION_CURRENT 17
#define DATABASE_SCHEMA_VERSION_CURRENT 18
/**
* @brief Version actuelle sous forme textuelle pour metadata.
*/
#define DATABASE_SCHEMA_VERSION_CURRENT_TEXT "17"
#define DATABASE_SCHEMA_VERSION_CURRENT_TEXT "18"
/**
* @brief Nom de l'application enregistré dans les métadonnées.
@ -917,6 +917,21 @@ rollback:
return false;
}
static bool database_migrate_v17_to_v18(Database *database)
{
bool transaction_started = false;
if (database == NULL || !database_transaction_begin(database)) return false;
transaction_started = true;
if (!schema_install_v18(database) ||
!database_update_schema_version(database, "18") ||
!database_transaction_commit(database)) goto rollback;
return true;
rollback:
if (transaction_started && !database_transaction_rollback(database))
g_warning("Impossible dannuler la migration SQLite V17 vers V18.");
return false;
}
/**
* @brief Garantit atomiquement la présence des extensions du schéma courant.
*/
@ -1174,6 +1189,10 @@ bool database_migrate_to_latest(
if (!database_migrate_v16_to_v17(database)) return false;
schema_version = 17;
break;
case 17:
if (!database_migrate_v17_to_v18(database)) return false;
schema_version = 18;
break;
default:
database_set_error(

View file

@ -407,6 +407,32 @@ bool schema_install_v17(Database *database)
"la migration SQLite V17");
}
bool schema_install_v18(Database *database)
{
sqlite3_stmt *statement = NULL;
sqlite3 *handle = database_get_handle(database);
if (handle != NULL &&
sqlite3_prepare_v2(handle,
"SELECT confirmed_value,confirmation_state,value_quality "
"FROM identity_field_observations LIMIT 0",
-1, &statement, NULL) == SQLITE_OK) {
sqlite3_finalize(statement);
statement = NULL;
if (sqlite3_prepare_v2(handle,
"SELECT 1 FROM document_authenticity_assessments,"
"person_evidence_factual_relations,"
"identification_status_vocabulary,"
"person_role_vocabulary LIMIT 0",
-1, &statement, NULL) == SQLITE_OK) {
sqlite3_finalize(statement);
return true;
}
}
sqlite3_finalize(statement);
return schema_execute_file(database, "database/schema_v18.sql",
"la migration SQLite V18");
}
bool schema_ensure_current(
Database *database
)

View file

@ -1,6 +1,7 @@
#include "models/identity_ocr.h"
struct IdentityFieldObservation {
char *code, *raw_value, *corrected_value, *normalized_value, *note;
char *confirmed_value, *value_quality;
char *origin;
double confidence;
IdentityReviewStatus status;
@ -48,7 +49,7 @@ IdentityFieldObservation *identity_field_observation_new(
IdentityFieldObservation *f=g_new0(IdentityFieldObservation,1);
f->code=g_strdup(code); f->raw_value=g_strdup(raw);
f->confidence=confidence; f->status=IDENTITY_REVIEW_PROPOSED;
f->origin=g_strdup("ocr"); f->order=order;
f->origin=g_strdup("ocr"); f->value_quality=g_strdup("complete"); f->order=order;
if (box != NULL) f->box=*box;
return f;
}
@ -64,6 +65,7 @@ IdentityFieldObservation *identity_field_observation_new_manual(
field->confidence = -1.0;
field->status = IDENTITY_REVIEW_PROPOSED;
field->origin = g_strdup("manual_entry");
field->value_quality = g_strdup("complete");
field->order = order;
return field;
}
@ -78,6 +80,8 @@ IdentityFieldObservation *identity_field_observation_copy(
f->code,f->corrected_value,f->order);
c->corrected_value=g_strdup(f->corrected_value);
c->normalized_value=g_strdup(f->normalized_value);
c->confirmed_value=g_strdup(f->confirmed_value);
g_free(c->value_quality);c->value_quality=g_strdup(f->value_quality);
c->note=g_strdup(f->note); c->status=f->status;
g_free(c->origin); c->origin=g_strdup(f->origin); return c;
}
@ -86,7 +90,7 @@ void identity_field_observation_free(IdentityFieldObservation *f)
if(f==NULL)return;
g_free(f->code);g_free(f->raw_value);
g_free(f->corrected_value);g_free(f->normalized_value);g_free(f->note);
g_free(f->origin);g_free(f);
g_free(f->origin);g_free(f->confirmed_value);g_free(f->value_quality);g_free(f);
}
gboolean identity_field_observation_accept(IdentityFieldObservation *f)
{ if(f==NULL)return FALSE;f->status=IDENTITY_REVIEW_ACCEPTED;return TRUE; }
@ -112,7 +116,8 @@ gboolean identity_field_observation_restore_raw(IdentityFieldObservation *f)
return TRUE;
}
void identity_field_observation_reject(IdentityFieldObservation *f)
{if(f!=NULL)f->status=IDENTITY_REVIEW_REJECTED;}
{if(f!=NULL){f->status=IDENTITY_REVIEW_REJECTED;
g_clear_pointer(&f->confirmed_value,g_free);}}
gboolean identity_field_observation_set_origin(IdentityFieldObservation*f,
const char*origin)
{if(f==NULL||(!g_str_equal(origin,"ocr")&&!g_str_equal(origin,"mrz")&&
@ -120,17 +125,44 @@ gboolean identity_field_observation_set_origin(IdentityFieldObservation*f,
return FALSE;
g_free(f->origin);f->origin=g_strdup(origin);return TRUE;}
void identity_field_observation_mark_conflict(IdentityFieldObservation*f)
{if(f!=NULL)f->status=IDENTITY_REVIEW_CONFLICT;}
{if(f!=NULL){f->status=IDENTITY_REVIEW_CONFLICT;
g_clear_pointer(&f->confirmed_value,g_free);}}
#define FG(name,type,field,zero) type identity_field_observation_get_##name(\
const IdentityFieldObservation*f){return f!=NULL?f->field:zero;}
FG(code,const char*,code,NULL) FG(raw_value,const char*,raw_value,NULL)
FG(corrected_value,const char*,corrected_value,NULL)
FG(normalized_value,const char*,normalized_value,NULL)
FG(confirmed_value,const char*,confirmed_value,NULL)
FG(value_quality,const char*,value_quality,NULL)
FG(origin,const char*,origin,NULL)
FG(status,IdentityReviewStatus,status,IDENTITY_REVIEW_PROPOSED)
FG(confidence,double,confidence,-1.0)
FG(order,guint,order,0)
const IdentitySourceBox *identity_field_observation_get_box(
const IdentityFieldObservation*f){return f!=NULL?&f->box:NULL;}
gboolean identity_field_observation_set_normalized_value(
IdentityFieldObservation*f,const char*v)
{if(!f||!v||!v[0]||!g_utf8_validate(v,-1,NULL))return FALSE;
g_free(f->normalized_value);f->normalized_value=g_strdup(v);return TRUE;}
gboolean identity_field_observation_set_value_quality(
IdentityFieldObservation*f,const char*q)
{if(!f||(!g_str_equal(q,"complete")&&!g_str_equal(q,"partial")&&
!g_str_equal(q,"uncertain")&&!g_str_equal(q,"invalid")))return FALSE;
g_free(f->value_quality);f->value_quality=g_strdup(q);
if(g_str_equal(q,"uncertain")||g_str_equal(q,"invalid"))
g_clear_pointer(&f->confirmed_value,g_free);
return TRUE;}
gboolean identity_field_observation_confirm(IdentityFieldObservation*f,
const char*v)
{if(!f||!v||!v[0]||f->status==IDENTITY_REVIEW_REJECTED||
f->status==IDENTITY_REVIEW_CONFLICT||
g_strcmp0(f->value_quality,"uncertain")==0||
g_strcmp0(f->value_quality,"invalid")==0)return FALSE;
g_free(f->confirmed_value);f->confirmed_value=g_strdup(v);return TRUE;}
void identity_field_observation_clear_confirmation(IdentityFieldObservation*f)
{if(f)g_clear_pointer(&f->confirmed_value,g_free);}
gboolean identity_field_observation_is_human_confirmed(
const IdentityFieldObservation*f){return f&&f->confirmed_value!=NULL;}
IdentityOcrRun *identity_ocr_run_new(const char *evidence_id,
const char *sha,const char *type,const char *side,guint page,
const char *languages,const char *profile)

View file

@ -0,0 +1,109 @@
#include "models/identity_traceability.h"
#include <string.h>
static gboolean in_values(const char *value, const char *const *values,
gsize count)
{
if (value == NULL) return FALSE;
for (gsize i = 0; i < count; i++)
if (g_str_equal(value, values[i])) return TRUE;
return FALSE;
}
static gboolean text(const char *value)
{ return value != NULL && value[0] != '\0' && g_utf8_validate(value,-1,NULL); }
static gboolean optional_text(const char *value)
{ return value == NULL || (text(value) && strlen(value) <= 65536); }
static gboolean uuid_or_null(const char *value)
{ return value == NULL || g_uuid_string_is_valid(value); }
static gboolean timestamp(const char *value)
{ return value != NULL && strlen(value) == 20; }
gboolean identity_traceability_authenticity_status_valid(const char *value)
{
static const char *const values[]={"indeterminate","presumed_authentic",
"suspicious","presumed_forged","confirmed_forged"};
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",
"document_presented_in_name_of","declared_holder_in",
"data_extracted_from"};
return in_values(value,values,G_N_ELEMENTS(values));
}
gboolean identity_traceability_identification_status_valid(const char *value)
{
static const char *const values[]={"unknown","unverified","presumed",
"partially_identified","confirmed","disputed"};
return in_values(value,values,G_N_ELEMENTS(values));
}
gboolean identity_traceability_value_quality_valid(const char *value)
{
static const char *const values[]={"complete","partial","uncertain","invalid"};
return in_values(value,values,G_N_ELEMENTS(values));
}
gboolean identity_traceability_field_is_projectable(const char *review,
const char *quality,const char *confirmation,const char *confirmed)
{
return confirmed != NULL && confirmed[0] != '\0' &&
g_strcmp0(confirmation,"human_confirmed")==0 &&
(g_strcmp0(review,"accepted")==0||g_strcmp0(review,"modified")==0) &&
(g_strcmp0(quality,"complete")==0||g_strcmp0(quality,"partial")==0);
}
DocumentAuthenticityAssessment *document_authenticity_assessment_new(
const char *id,const char *evidence,const char *run,const char *status,
const char *justification,const char *at,const char *previous,const char *note)
{
if(!g_uuid_string_is_valid(id)||!g_uuid_string_is_valid(evidence)||
!uuid_or_null(run)||!uuid_or_null(previous)||
!identity_traceability_authenticity_status_valid(status)||
!timestamp(at)||!optional_text(justification)||!optional_text(note)||
(g_strcmp0(status,"indeterminate")!=0&&!text(justification)))return NULL;
DocumentAuthenticityAssessment*a=g_new0(DocumentAuthenticityAssessment,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->technical_note=g_strdup(note);
a->origin=g_strdup("human");return a;
}
DocumentAuthenticityAssessment *document_authenticity_assessment_copy(
const DocumentAuthenticityAssessment*a)
{return a?document_authenticity_assessment_new(a->identifier,
a->evidence_identifier,a->ocr_run_identifier,a->status,a->justification,
a->assessed_at,a->previous_identifier,a->technical_note):NULL;}
void document_authenticity_assessment_free(DocumentAuthenticityAssessment*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->technical_note);g_free(a->origin);g_free(a);}
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)
{
if(!g_uuid_string_is_valid(id)||!g_uuid_string_is_valid(person)||
!g_uuid_string_is_valid(evidence)||!uuid_or_null(run)||
!identity_traceability_relation_type_valid(type)||!optional_text(note)||
!timestamp(at))return NULL;
PersonEvidenceFactualRelation*r=g_new0(PersonEvidenceFactualRelation,1);
r->identifier=g_strdup(id);r->person_identifier=g_strdup(person);
r->evidence_identifier=g_strdup(evidence);r->ocr_run_identifier=g_strdup(run);
r->relation_type=g_strdup(type);r->factual_note=g_strdup(note);
r->observed_at=g_strdup(at);r->origin=g_strdup("human");r->active=active;return r;
}
PersonEvidenceFactualRelation *person_evidence_factual_relation_copy(
const PersonEvidenceFactualRelation*r)
{return r?person_evidence_factual_relation_new(r->identifier,
r->person_identifier,r->evidence_identifier,r->ocr_run_identifier,
r->relation_type,r->factual_note,r->observed_at,r->active):NULL;}
void person_evidence_factual_relation_free(PersonEvidenceFactualRelation*r)
{if(!r)return;g_free(r->identifier);g_free(r->person_identifier);
g_free(r->evidence_identifier);g_free(r->ocr_run_identifier);
g_free(r->relation_type);g_free(r->factual_note);g_free(r->observed_at);
g_free(r->origin);g_free(r);}
void person_role_vocabulary_entry_free(PersonRoleVocabularyEntry*e)
{if(!e)return;g_free(e->code);g_free(e->label);g_free(e->description);g_free(e);}
void identification_status_vocabulary_entry_free(
IdentificationStatusVocabularyEntry*e)
{person_role_vocabulary_entry_free(e);}

View file

@ -4,6 +4,8 @@
******************************************************************************/
#include "views/create_person_dialog.h"
#include "views/dialog_geometry.h"
#include "views/person_vocabulary_adapter.h"
#include "views/identity_ocr_option_adapter.h"
#include "core/evidence_staging.h"
#include "core/evidence_staging_task.h"
#include "core/person_confirmation_summary.h"
@ -78,7 +80,8 @@ typedef struct
GtkStringList *evidence_labels;
GtkStringList *retained_labels;
GtkStringList *type_filter_labels;
GtkCheckButton *roles[9];
GPtrArray *role_buttons;
PersonVocabularyAdapter *vocabularies;
guint step;
GPtrArray *evidence_identifiers;
GPtrArray *visible_records;
@ -125,7 +128,6 @@ typedef struct {
GWeakRef window;
guint64 generation;
} CreatePersonStagingContext;
static const char *const status_codes[] = {"unknown", "suspected", "confirmed"};
static void create_person_dialog_clear_preview(CreatePersonDialogState *state);
static void create_person_dialog_select_record(CreatePersonDialogState *state,
const EvidenceRecord *record, const char *business_type);
@ -162,33 +164,6 @@ static void identity_languages_worker(GTask *task, gpointer source,
else g_task_return_error(task, error);
}
static const char *identity_language_label(const char *code)
{
if (g_str_equal(code, "fra")) return "Français (fra)";
if (g_str_equal(code, "eng")) return "Anglais (eng)";
if (g_str_equal(code, "fra+eng")) return "Français + anglais (fra+eng)";
return code;
}
static guint identity_default_language_index(const GPtrArray *codes)
{
guint english_index = GTK_INVALID_LIST_POSITION;
for (guint index = 0; codes != NULL && index < codes->len; index++) {
const char *code = g_ptr_array_index((GPtrArray *) codes, index);
if (g_strcmp0(code, "fra") == 0)
return index;
if (g_strcmp0(code, "eng") == 0)
english_index = index;
}
return english_index != GTK_INVALID_LIST_POSITION
? english_index
: (codes != NULL && codes->len > 0
? 0
: GTK_INVALID_LIST_POSITION);
}
static const char *create_person_dialog_effective_ocr_mime(
const PersonEvidenceSelectionItem *item)
{
@ -235,11 +210,13 @@ static void identity_languages_completed(GObject *source,
for (guint i = 0; state->ocr_language_codes != NULL &&
i < state->ocr_language_codes->len; i++) {
const char *code = g_ptr_array_index(state->ocr_language_codes, i);
gtk_string_list_append(labels, identity_language_label(code));
gtk_string_list_append(labels,
identity_ocr_option_adapter_language_label(code));
}
gtk_drop_down_set_model(state->ocr_languages, G_LIST_MODEL(labels));
gtk_drop_down_set_selected(state->ocr_languages,
identity_default_language_index(state->ocr_language_codes));
identity_ocr_option_adapter_default_language_index(
state->ocr_language_codes));
gtk_widget_set_sensitive(GTK_WIDGET(state->ocr_start),
state->ocr_language_codes != NULL &&
state->ocr_language_codes->len > 0);
@ -749,6 +726,8 @@ static void create_person_dialog_state_free(gpointer data)
g_clear_pointer(&state->ocr_runs, g_ptr_array_unref);
g_clear_pointer(&state->ocr_language_codes, g_ptr_array_unref);
g_clear_pointer(&state->ocr_overlay, ocr_provenance_overlay_free);
g_clear_pointer(&state->role_buttons, g_ptr_array_unref);
g_clear_pointer(&state->vocabularies, person_vocabulary_adapter_free);
person_dialog_lifecycle_cancel(state->lifecycle);
g_clear_pointer(&state->evidence_identifiers, g_ptr_array_unref);
g_clear_pointer(&state->visible_records, g_ptr_array_unref);
@ -1301,6 +1280,8 @@ static void create_person_dialog_on_create(GtkButton *button, gpointer data)
CreatePersonDialogResult *result = NULL;
char *notes = NULL;
guint status = gtk_drop_down_get_selected(state->status);
const GPtrArray *statuses =
person_vocabulary_adapter_get_statuses(state->vocabularies);
(void) button;
if (state->session_check != NULL &&
!state->session_check(state->user_data)) {
@ -1309,7 +1290,7 @@ static void create_person_dialog_on_create(GtkButton *button, gpointer data)
gtk_widget_set_visible(GTK_WIDGET(state->error), TRUE);
return;
}
if (status >= G_N_ELEMENTS(status_codes) ||
if (statuses == NULL || status >= statuses->len ||
gtk_editable_get_text(GTK_EDITABLE(state->designation))[0] == '\0')
{
gtk_label_set_text(state->error, "La désignation de la personne est obligatoire.");
@ -1323,7 +1304,8 @@ static void create_person_dialog_on_create(GtkButton *button, gpointer data)
gtk_editable_get_text(GTK_EDITABLE(state->name)));
result->pseudonym = create_person_dialog_copy(
gtk_editable_get_text(GTK_EDITABLE(state->pseudonym)));
result->status = g_strdup(status_codes[status]);
result->status = g_strdup(person_vocabulary_adapter_status_code(
state->vocabularies, status));
result->notes = create_person_dialog_copy(notes);
if (person_evidence_selection_get_count(
state->person_evidence_selection) > 0) {
@ -1340,23 +1322,10 @@ static void create_person_dialog_on_create(GtkButton *button, gpointer data)
result->input.notes = result->notes;
result->input.confidence = gtk_spin_button_get_value_as_int(state->confidence);
result->input.evidence_identifier = result->evidence_identifier;
result->role_assignments = g_ptr_array_new_with_free_func(
(GDestroyNotify) person_role_assignment_input_free);
static const char *const role_codes[] = {
"alleged_author", "presented_identity",
"potentially_impersonated_identity", "victim", "witness",
"declared_bank_holder", "intermediary", "mentioned_person", "other"};
for (guint i = 0; i < G_N_ELEMENTS(role_codes); i++)
if (gtk_check_button_get_active(state->roles[i])) {
PersonRoleAssignmentInput assignment = {
.role_code = (char *) role_codes[i],
.evidence_identifier = result->evidence_identifier,
.provenance_kind = "manual",
.has_confidence = FALSE
};
g_ptr_array_add(result->role_assignments,
person_role_assignment_input_copy(&assignment));
}
result->role_assignments =
person_vocabulary_adapter_build_role_assignments(
state->vocabularies, state->role_buttons,
result->evidence_identifier);
result->input.role_assignments = result->role_assignments;
result->evidence_selection = state->person_evidence_selection;
state->person_evidence_selection = NULL;
@ -1395,23 +1364,20 @@ static void create_person_dialog_update_navigation(CreatePersonDialogState *stat
gtk_label_set_markup(state->progress, progress->str);
g_string_free(progress, TRUE);
if (state->step == 4) {
static const char *const status_labels[] = {
"Inconnu", "Présumé", "Confirmé"};
GPtrArray *role_labels = g_ptr_array_new();
GPtrArray *role_labels =
person_vocabulary_adapter_selected_role_labels(
state->role_buttons);
char *notes = create_person_dialog_get_notes(state);
char *text;
for (guint i = 0; i < G_N_ELEMENTS(state->roles); i++)
if (gtk_check_button_get_active(state->roles[i]))
g_ptr_array_add(role_labels, (gpointer)
gtk_check_button_get_label(state->roles[i]));
guint selected_status = gtk_drop_down_get_selected(state->status);
const char *status_label =
person_vocabulary_adapter_status_label(
state->vocabularies, selected_status);
text = person_confirmation_summary_build_multiple(
gtk_editable_get_text(GTK_EDITABLE(state->designation)),
gtk_editable_get_text(GTK_EDITABLE(state->name)),
gtk_editable_get_text(GTK_EDITABLE(state->pseudonym)),
gtk_drop_down_get_selected(state->status) <
G_N_ELEMENTS(status_labels)
? status_labels[gtk_drop_down_get_selected(state->status)]
: "Non renseigné",
status_label,
gtk_spin_button_get_value_as_int(state->confidence), notes,
role_labels, state->person_evidence_selection);
GString *confirmation = g_string_new(text);
@ -1512,20 +1478,29 @@ static void create_person_dialog_add_row(GtkGrid *grid, int row,
gtk_grid_attach(grid, widget, 1, row, 1, 1);
}
gboolean create_person_dialog_present(GtkWindow *parent,
const GPtrArray *records, const char *investigation_root_path,
Database *database, const GPtrArray *records,
const char *investigation_root_path,
TaskManager *task_manager, const ToolInfo *tesseract_tool,
CreatePersonDialogSessionCheck session_check,
CreatePersonDialogCallback callback, gpointer data,
GDestroyNotify data_destroy)
{
static const char *const statuses[] = {"Inconnu", "Présumé", "Confirmé", NULL};
CreatePersonDialogState *state = NULL;
GtkWidget *box = NULL, *grid = NULL, *actions = NULL, *cancel = NULL;
GtkWidget *roles_box = NULL, *evidence_box = NULL, *ocr_box = NULL;
GtkWidget *summary = NULL;
if (parent == NULL || investigation_root_path == NULL ||
if (parent == NULL || database == NULL ||
investigation_root_path == NULL ||
task_manager == NULL) return FALSE;
state = g_new0(CreatePersonDialogState, 1);
GError *vocabulary_error = NULL;
state->vocabularies = person_vocabulary_adapter_new(
database, &vocabulary_error);
if (state->vocabularies == NULL) {
g_clear_error(&vocabulary_error);
g_free(state);
return FALSE;
}
state->callback = callback; state->session_check = session_check;
state->user_data = data; state->user_data_destroy = data_destroy;
state->evidence_identifiers = g_ptr_array_new_with_free_func(g_free);
@ -1568,7 +1543,10 @@ gboolean create_person_dialog_present(GtkWindow *parent,
gtk_entry_set_placeholder_text(state->designation, "Personne présumée liée aux comptes");
state->name = GTK_ENTRY(gtk_entry_new());
state->pseudonym = GTK_ENTRY(gtk_entry_new());
state->status = GTK_DROP_DOWN(gtk_drop_down_new_from_strings(statuses));
GtkStringList *status_labels =
person_vocabulary_adapter_create_status_labels(state->vocabularies);
state->status = GTK_DROP_DOWN(gtk_drop_down_new(
G_LIST_MODEL(status_labels), NULL));
gtk_drop_down_set_selected(state->status, 1);
state->confidence = GTK_SPIN_BUTTON(gtk_spin_button_new_with_range(0, 100, 5));
gtk_spin_button_set_value(state->confidence, 30);
@ -1620,15 +1598,9 @@ gboolean create_person_dialog_present(GtkWindow *parent,
GTK_STACK_TRANSITION_TYPE_SLIDE_LEFT_RIGHT);
gtk_stack_add_titled(state->stack, grid, "person", "1 — Personne");
roles_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 6);
static const char *const role_codes[] = {
"alleged_author", "presented_identity",
"potentially_impersonated_identity", "victim", "witness",
"declared_bank_holder", "intermediary", "mentioned_person", "other"};
for (guint i = 0; i < G_N_ELEMENTS(role_codes); i++) {
state->roles[i] = GTK_CHECK_BUTTON(gtk_check_button_new_with_label(
person_role_assignment_role_label(role_codes[i])));
gtk_box_append(GTK_BOX(roles_box), GTK_WIDGET(state->roles[i]));
}
state->role_buttons =
person_vocabulary_adapter_create_role_buttons(
state->vocabularies, GTK_BOX(roles_box));
gtk_stack_add_titled(state->stack, roles_box, "roles", "2 — Rôles");
evidence_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 8);
gtk_box_append(GTK_BOX(evidence_box), gtk_label_new(

View file

@ -107,7 +107,8 @@ static void paned_geometry_apply(
{
int width = gtk_widget_get_width(GTK_WIDGET(paned));
int position;
if (width <= 0 || geometry == NULL) return;
if (geometry == NULL ||
width < geometry->minimum_start + geometry->minimum_end) return;
position = (int) (width * geometry->ratio);
position = MAX(geometry->minimum_start, position);
position = MIN(position, MAX(0, width - geometry->minimum_end));
@ -120,7 +121,8 @@ static gboolean paned_geometry_idle(gpointer data)
LabfyPanedGeometry *geometry = g_object_get_data(
G_OBJECT(paned), "labfy-paned-initial-geometry");
if (geometry == NULL) return G_SOURCE_REMOVE;
if (gtk_widget_get_width(GTK_WIDGET(paned)) <= 0)
if (gtk_widget_get_width(GTK_WIDGET(paned)) <
geometry->minimum_start + geometry->minimum_end)
return G_SOURCE_CONTINUE;
paned_geometry_apply(paned, geometry);
return G_SOURCE_REMOVE;

View file

@ -0,0 +1,23 @@
#include "views/identity_ocr_option_adapter.h"
const char *identity_ocr_option_adapter_language_label(const char *code)
{
if (g_str_equal(code, "fra")) return "Français (fra)";
if (g_str_equal(code, "eng")) return "Anglais (eng)";
if (g_str_equal(code, "fra+eng"))
return "Français + anglais (fra+eng)";
return code;
}
guint identity_ocr_option_adapter_default_language_index(
const GPtrArray *codes)
{
guint english_index = G_MAXUINT;
for (guint index = 0; codes != NULL && index < codes->len; index++) {
const char *code = g_ptr_array_index((GPtrArray *) codes, index);
if (g_strcmp0(code, "fra") == 0) return index;
if (g_strcmp0(code, "eng") == 0) english_index = index;
}
return english_index != G_MAXUINT ? english_index :
(codes != NULL && codes->len > 0 ? 0 : G_MAXUINT);
}

View file

@ -0,0 +1,154 @@
#include "views/person_vocabulary_adapter.h"
#include "dao/identity_traceability_dao.h"
#include "models/person_role_assignment.h"
struct PersonVocabularyAdapter {
GPtrArray *roles;
GPtrArray *statuses;
};
PersonVocabularyAdapter *person_vocabulary_adapter_new(
Database *database, GError **error)
{
IdentityTraceabilityDao *dao = identity_traceability_dao_new(database);
PersonVocabularyAdapter *adapter = NULL;
if (dao == NULL) return NULL;
adapter = g_new0(PersonVocabularyAdapter, 1);
adapter->roles = identity_traceability_dao_list_roles(
dao, FALSE, error);
if (adapter->roles != NULL)
adapter->statuses =
identity_traceability_dao_list_identification_statuses(
dao, FALSE, error);
identity_traceability_dao_free(dao);
if (adapter->roles == NULL || adapter->statuses == NULL) {
person_vocabulary_adapter_free(adapter);
return NULL;
}
return adapter;
}
void person_vocabulary_adapter_free(PersonVocabularyAdapter *adapter)
{
if (adapter == NULL) return;
g_clear_pointer(&adapter->roles, g_ptr_array_unref);
g_clear_pointer(&adapter->statuses, g_ptr_array_unref);
g_free(adapter);
}
const GPtrArray *person_vocabulary_adapter_get_roles(
const PersonVocabularyAdapter *adapter)
{ return adapter != NULL ? adapter->roles : NULL; }
const GPtrArray *person_vocabulary_adapter_get_statuses(
const PersonVocabularyAdapter *adapter)
{ return adapter != NULL ? adapter->statuses : NULL; }
static GtkStringList *create_labels(const GPtrArray *entries)
{
GtkStringList *labels = gtk_string_list_new(NULL);
for (guint i = 0; entries != NULL && i < entries->len; i++) {
PersonRoleVocabularyEntry *entry = g_ptr_array_index(
(GPtrArray *) entries, i);
gtk_string_list_append(labels, entry->label);
}
return labels;
}
GtkStringList *person_vocabulary_adapter_create_role_labels(
const PersonVocabularyAdapter *adapter)
{ return create_labels(person_vocabulary_adapter_get_roles(adapter)); }
GtkStringList *person_vocabulary_adapter_create_status_labels(
const PersonVocabularyAdapter *adapter)
{ return create_labels(person_vocabulary_adapter_get_statuses(adapter)); }
const char *person_vocabulary_adapter_status_code(
const PersonVocabularyAdapter *adapter, guint index)
{
const GPtrArray *statuses =
person_vocabulary_adapter_get_statuses(adapter);
return statuses != NULL && index < statuses->len
? ((IdentificationStatusVocabularyEntry *) g_ptr_array_index(
(GPtrArray *) statuses, index))->code : NULL;
}
const char *person_vocabulary_adapter_status_label(
const PersonVocabularyAdapter *adapter, guint index)
{
const GPtrArray *statuses =
person_vocabulary_adapter_get_statuses(adapter);
return statuses != NULL && index < statuses->len
? ((IdentificationStatusVocabularyEntry *) g_ptr_array_index(
(GPtrArray *) statuses, index))->label : "Non renseigné";
}
GPtrArray *person_vocabulary_adapter_selected_role_labels(
const GPtrArray *buttons)
{
GPtrArray *labels = g_ptr_array_new();
for (guint i = 0; buttons != NULL && i < buttons->len; i++) {
GtkCheckButton *button = g_ptr_array_index(
(GPtrArray *) buttons, i);
if (gtk_check_button_get_active(button))
g_ptr_array_add(labels, (gpointer)
gtk_check_button_get_label(button));
}
return labels;
}
GPtrArray *person_vocabulary_adapter_create_role_buttons(
const PersonVocabularyAdapter *adapter, GtkBox *container)
{
const GPtrArray *roles = person_vocabulary_adapter_get_roles(adapter);
GPtrArray *buttons = g_ptr_array_new();
for (guint i = 0; roles != NULL && i < roles->len; i++) {
PersonRoleVocabularyEntry *entry =
g_ptr_array_index((GPtrArray *) roles, i);
GtkCheckButton *button = GTK_CHECK_BUTTON(
gtk_check_button_new_with_label(entry->label));
gtk_widget_set_tooltip_text(GTK_WIDGET(button), entry->description);
g_ptr_array_add(buttons, button);
gtk_box_append(container, GTK_WIDGET(button));
}
return buttons;
}
GPtrArray *person_vocabulary_adapter_build_role_assignments(
const PersonVocabularyAdapter *adapter, const GPtrArray *buttons,
const char *evidence_identifier)
{
const GPtrArray *roles = person_vocabulary_adapter_get_roles(adapter);
GPtrArray *assignments = g_ptr_array_new_with_free_func(
(GDestroyNotify) person_role_assignment_input_free);
for (guint i = 0; roles != NULL && buttons != NULL &&
i < roles->len && i < buttons->len; i++) {
if (!gtk_check_button_get_active(g_ptr_array_index(
(GPtrArray *) buttons, i))) continue;
PersonRoleVocabularyEntry *entry =
g_ptr_array_index((GPtrArray *) roles, i);
PersonRoleAssignmentInput input = {
.role_code = entry->code,
.evidence_identifier = (char *) evidence_identifier,
.provenance_kind = "manual"
};
g_ptr_array_add(assignments,
person_role_assignment_input_copy(&input));
}
return assignments;
}
gboolean person_vocabulary_adapter_justification_valid(
const PersonRoleVocabularyEntry *entry, const char *justification)
{
char *copy;
gboolean valid;
if (entry == NULL) return FALSE;
if (!entry->requires_justification) return TRUE;
copy = g_strdup(justification);
if (copy != NULL) g_strstrip(copy);
valid = copy != NULL && copy[0] != '\0' &&
g_utf8_validate(copy, -1, NULL);
g_free(copy);
return valid;
}

Binary file not shown.

View file

@ -1,6 +1,8 @@
#include "views/create_person_dialog.h"
#include <gtk/gtk.h>
#include <glib/gstdio.h>
#include <string.h>
#include <unistd.h>
typedef struct
{
@ -241,6 +243,16 @@ static void activate(GtkApplication *application, gpointer data)
{
TestContext *context = data;
GError *error = NULL;
char *database_path = NULL;
int database_fd = g_file_open_tmp(
"labfy-create-person-XXXXXX.sqlite", &database_path, &error);
g_assert_cmpint(database_fd, >=, 0);
close(database_fd);
g_unlink(database_path);
g_assert_true(database_initialize(database_path,
"Enquête SPECIMEN dialogue", "/tmp"));
Database *database = database_open(database_path);
g_assert_nonnull(database);
GPtrArray *records = g_ptr_array_new_with_free_func(
(GDestroyNotify) evidence_record_free);
EvidenceRecord *record = evidence_record_new(
@ -270,8 +282,11 @@ static void activate(GtkApplication *application, gpointer data)
gtk_application_window_new(application));
gtk_window_present(context->main_window);
g_assert_true(create_person_dialog_present(context->main_window,
records, "/tmp", context->task_manager, NULL, NULL, completed,
database, records, "/tmp", context->task_manager, NULL, NULL, completed,
context, NULL));
database_close(database);
g_unlink(database_path);
g_free(database_path);
g_ptr_array_unref(records);
g_idle_add(click_cancel, context);
}

View file

@ -270,8 +270,10 @@ static gboolean drive(gpointer data)
g_assert_true(gtk_widget_get_mapped(actions));
if(context->layout_index==0){
int position=gtk_paned_get_position(GTK_PANED(paned));
g_assert_cmpint(position,>=,600);
g_assert_cmpint(position,<=,680);
int allocated=gtk_widget_get_width(paned);
g_assert_cmpint(position,>=,480);
g_assert_cmpint(allocated-position,>=,240);
g_assert_cmpint(position,<,allocated-allocated/10);
}
context->layout_index++;
context->layout_resize_pending=FALSE;
@ -582,10 +584,12 @@ static gboolean drive(gpointer data)
verify_persisted_review(context);
g_assert_true(gtk_widget_get_visible(GTK_WIDGET(context->main_window)));
GPtrArray *empty=g_ptr_array_new();
g_assert_true(create_person_dialog_present(context->main_window,empty,
Database *database = database_open(context->database_path);
g_assert_true(create_person_dialog_present(context->main_window,database,empty,
context->root,context->task_manager,
tool_registry_find(context->registry,"tesseract"),NULL,completed,
context,NULL));
database_close(database);
g_ptr_array_unref(empty);
GList *windows=gtk_application_get_windows(context->application);
context->dialog=windows->data==context->main_window
@ -667,7 +671,7 @@ static void activate(GtkApplication *application,gpointer data)
g_assert_true(evidence_dao_insert(evidence_dao,
g_ptr_array_index(records,index),&error));
g_assert_no_error(error);
evidence_dao_free(evidence_dao);database_close(database);
evidence_dao_free(evidence_dao);
context->registry=tool_registry_new();
char *tool=g_canonicalize_filename("tests/fake_document_tool",NULL);
g_assert_true(tool_registry_register(context->registry,"tesseract",
@ -677,10 +681,11 @@ static void activate(GtkApplication *application,gpointer data)
"5.0.0 SPECIMEN",&error));g_assert_no_error(error);
context->main_window=GTK_WINDOW(gtk_application_window_new(application));
gtk_window_present(context->main_window);
g_assert_true(create_person_dialog_present(context->main_window,records,
g_assert_true(create_person_dialog_present(context->main_window,database,records,
context->root,context->task_manager,
tool_registry_find(context->registry,"tesseract"),NULL,completed,
context,NULL));
database_close(database);
GList *windows=gtk_application_get_windows(application);
context->dialog=windows->data==context->main_window
?GTK_WINDOW(windows->next->data):GTK_WINDOW(windows->data);

View file

@ -525,7 +525,7 @@ static void test_database_initialize_valid_database(void)
"FROM investigation;"
);
assert(strcmp(schema_version, "17") == 0);
assert(strcmp(schema_version, "18") == 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");
@ -535,6 +535,14 @@ static void test_database_initialize_valid_database(void)
test_database_assert_table_exists(database, "osint_execution_relations");
test_database_assert_table_exists(database, "comptes_sociaux");
test_database_assert_table_exists(database, "person_roles");
test_database_assert_table_exists(database,
"document_authenticity_assessments");
test_database_assert_table_exists(database,
"person_evidence_factual_relations");
test_database_assert_table_exists(database,
"person_role_vocabulary");
test_database_assert_table_exists(database,
"identification_status_vocabulary");
assert(strcmp(application_name, "Labfy Investigation") == 0);
assert(created_at[0] != '\0');
@ -993,7 +1001,7 @@ static void test_database_migrate_v1_to_v2(void)
assert(
strcmp(
schema_version,
"17"
"18"
) == 0
);
@ -1422,6 +1430,14 @@ static void test_database_migrate_v12_to_v13_preserves_legacy_link(void)
"INSERT INTO person_roles(entity_id,role,updated_at) VALUES("
"'20000000-0000-4000-8000-000000000014','alleged_scammer',"
"'2026-07-28T08:00:00Z');"
"DROP TABLE person_identification_assessments;"
"DROP TABLE person_evidence_factual_relations;"
"DROP TABLE document_authenticity_assessments;"
"DROP TABLE person_role_vocabulary;"
"DROP TABLE identification_status_vocabulary;"
"DROP TABLE identity_field_observations;"
"DROP TABLE identity_document_observations;"
"DROP TABLE identity_ocr_runs;"
"DROP TABLE preuve_entite_sources;"
"DROP TABLE person_role_assignments;"
"UPDATE metadata SET value='12' WHERE key='schema_version';"
@ -1448,7 +1464,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, "17") == 0);
assert(strcmp(version, "18") == 0);
assert(strcmp(legacy_sources, "1") == 0);
char *legacy_role = test_database_read_single_text(sqlite_database,
"SELECT role_code || ':' || provenance_kind "

View file

@ -54,14 +54,20 @@ static void test_paned_applied_once(void)
gtk_window_present(window);
for (guint frame = 0; frame < 6; frame++)
wait_for_frame(GTK_WIDGET(window));
int expected = (gtk_widget_get_width(GTK_WIDGET(paned)) * 2) / 3;
g_assert_cmpint(ABS(gtk_paned_get_position(paned) - expected), <=, 2);
gtk_paned_set_position(paned, 450);
int allocated = gtk_widget_get_width(GTK_WIDGET(paned));
int expected = (allocated * 2) / 3;
int tolerance = MAX(4, allocated / 50);
int position = gtk_paned_get_position(paned);
g_assert_cmpint(ABS(position - expected), <=, tolerance);
g_assert_cmpint(position, >=, 320);
g_assert_cmpint(allocated - position, >=, 180);
int user_position = (allocated * 11) / 20;
gtk_paned_set_position(paned, user_position);
gtk_window_set_default_size(window, 1000, 700);
wait_for_frame(GTK_WIDGET(window));
g_assert_cmpint(gtk_paned_get_position(paned), ==, 450);
g_assert_cmpint(gtk_paned_get_position(paned), ==, user_position);
labfy_paned_apply_initial_ratio(paned, 0.5, 100, 100);
g_assert_cmpint(gtk_paned_get_position(paned), ==, 450);
g_assert_cmpint(gtk_paned_get_position(paned), ==, user_position);
gtk_window_destroy(window);
}

View file

@ -36,6 +36,7 @@ typedef struct {
gboolean import_done;
gboolean cancellation_done;
gboolean layout_resize_pending;
int user_paned_position;
gboolean passed;
gboolean revision_done;
char *final_evidence_identifier;
@ -501,19 +502,35 @@ static gboolean drive(gpointer data)
if (!context->layout_resize_pending) {
int initial_position =
gtk_paned_get_position(GTK_PANED(paned));
int expected_position =
(gtk_widget_get_width(paned) * 2) / 3;
int allocated_width = gtk_widget_get_width(paned);
int start_min = 0, end_min = 0;
gtk_widget_measure(gtk_paned_get_start_child(GTK_PANED(paned)),
GTK_ORIENTATION_HORIZONTAL, -1, &start_min, NULL, NULL, NULL);
gtk_widget_measure(gtk_paned_get_end_child(GTK_PANED(paned)),
GTK_ORIENTATION_HORIZONTAL, -1, &end_min, NULL, NULL, NULL);
int minimum_start = MAX(480, start_min);
int minimum_end = MAX(320, end_min);
int expected_position = CLAMP((allocated_width * 2) / 3,
minimum_start, MAX(minimum_start,
allocated_width - minimum_end));
int tolerance = MAX(4, allocated_width / 50);
g_assert_cmpint(
ABS(initial_position - expected_position), <=, 3);
gtk_paned_set_position(GTK_PANED(paned), 650);
ABS(initial_position - expected_position), <=, tolerance);
g_assert_cmpint(initial_position, >=, minimum_start);
g_assert_cmpint(allocated_width - initial_position, >=,
minimum_end);
int user_position = (allocated_width * 11) / 20;
gtk_paned_set_position(GTK_PANED(paned), user_position);
g_assert_cmpint(
gtk_paned_get_position(GTK_PANED(paned)), ==, 650);
gtk_paned_get_position(GTK_PANED(paned)), ==, user_position);
context->user_paned_position = user_position;
gtk_window_set_default_size(dialog, 1000, 700);
context->layout_resize_pending = TRUE;
return G_SOURCE_CONTINUE;
}
g_assert_cmpint(
gtk_paned_get_position(GTK_PANED(paned)), ==, 650);
gtk_paned_get_position(GTK_PANED(paned)), ==,
context->user_paned_position);
GtkDropDown *person = GTK_DROP_DOWN(find_named(root,
"identity-person"));
g_assert_nonnull(person);

View file

@ -0,0 +1,233 @@
#include "dao/identity_traceability_dao.h"
#include "views/person_vocabulary_adapter.h"
#include "dao/identity_ocr_dao.h"
#include "database/database.h"
#include "models/identity_traceability.h"
#include <glib.h>
#include <glib/gstdio.h>
#include <sqlite3.h>
#define EVIDENCE "10000000-0000-4000-8000-000000000018"
#define PERSON "20000000-0000-4000-8000-000000000018"
#define RUN "30000000-0000-4000-8000-000000000018"
#define DOCUMENT "40000000-0000-4000-8000-000000000018"
#define FIELD "50000000-0000-4000-8000-000000000018"
#define ASSESSMENT1 "60000000-0000-4000-8000-000000000018"
#define ASSESSMENT2 "60000000-0000-4000-8000-000000000019"
#define FACT "70000000-0000-4000-8000-000000000018"
#define AT "2026-07-30T10:00:00Z"
typedef struct{char*dir,*path;Database*database;}Fixture;
static void exec_ok(sqlite3*d,const char*sql)
{char*message=NULL;g_assert_cmpint(sqlite3_exec(d,sql,NULL,NULL,&message),==,SQLITE_OK);
sqlite3_free(message);}
static char *scalar(sqlite3*d,const char*sql)
{sqlite3_stmt*s=NULL;g_assert_cmpint(sqlite3_prepare_v2(d,sql,-1,&s,NULL),==,SQLITE_OK);
g_assert_cmpint(sqlite3_step(s),==,SQLITE_ROW);char*r=g_strdup(
(const char*)sqlite3_column_text(s,0));sqlite3_finalize(s);return r;}
static Fixture fixture_new(void)
{
Fixture f={0};GError*e=NULL;f.dir=g_dir_make_tmp("labfy-v18-XXXXXX",&e);
g_assert_no_error(e);f.path=g_build_filename(f.dir,"Enquete.sqlite",NULL);
g_assert_true(database_initialize(f.path,"SPECIMEN V18",f.dir));
sqlite3*d=NULL;g_assert_cmpint(sqlite3_open(f.path,&d),==,SQLITE_OK);
exec_ok(d,"PRAGMA foreign_keys=ON;"
"INSERT INTO preuves(id,name,relative_path,type_id,size_bytes,sha256,"
"imported_at,updated_at,status,locked,original_name) VALUES('" EVIDENCE
"','specimen.png','specimen.png',2,8,"
"'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa','" AT
"','" AT "','active',0,'specimen.png');"
"INSERT INTO entites(id,type_id,valeur,label,confiance,created_at,updated_at,status)"
" VALUES('" PERSON "',(SELECT id FROM types_entite WHERE code='person'),"
"'Personne SPECIMEN','Personne SPECIMEN',0,'" AT "','" AT "','active');"
"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');"
"INSERT INTO identity_document_observations(id,person_id,evidence_id,ocr_run_id,"
"document_type,document_side,page_number,review_state,observed_at) VALUES('"
DOCUMENT "','" PERSON "','" EVIDENCE "','" RUN "','identity_card','front',1,"
"'accepted','" AT "');"
"INSERT INTO identity_field_observations(id,observation_id,field_code,raw_value,"
"normalized_value,review_status,origin,evidence_id,ocr_run_id,page_number,"
"display_order,reviewed_at) VALUES('" FIELD "','" DOCUMENT "','surname',"
"'BRUT SPECIMEN','BRUT SPECIMEN','accepted','ocr','" EVIDENCE "','" RUN
"',1,0,'" AT "');");
sqlite3_close(d);f.database=database_open(f.path);g_assert_nonnull(f.database);
g_assert_true(database_migrate_to_latest(f.database));return f;
}
static void fixture_free(Fixture*f)
{database_close(f->database);g_remove(f->path);g_rmdir(f->dir);
g_free(f->path);g_free(f->dir);}
static void test_models(void)
{
PersonRoleVocabularyEntry justified={.requires_justification=TRUE};
g_assert_false(person_vocabulary_adapter_justification_valid(
&justified," "));
g_assert_true(person_vocabulary_adapter_justification_valid(
&justified,"Justification SPECIMEN"));
g_assert_true(identity_traceability_identification_status_valid("disputed"));
g_assert_false(identity_traceability_identification_status_valid("identified"));
g_assert_false(identity_traceability_relation_type_valid("is_author"));
g_assert_false(identity_traceability_field_is_projectable(
"accepted","uncertain","human_confirmed","SPECIMEN"));
g_assert_true(identity_traceability_field_is_projectable(
"modified","partial","human_confirmed","SPECIMEN"));
g_assert_null(document_authenticity_assessment_new(ASSESSMENT1,EVIDENCE,NULL,
"confirmed_forged",NULL,AT,NULL,NULL));
DocumentAuthenticityAssessment*a=document_authenticity_assessment_new(
ASSESSMENT1,EVIDENCE,NULL,"confirmed_forged","Justification SPECIMEN",AT,NULL,NULL);
g_assert_nonnull(a);DocumentAuthenticityAssessment*c=
document_authenticity_assessment_copy(a);g_assert_cmpstr(c->status,==,a->status);
document_authenticity_assessment_free(c);document_authenticity_assessment_free(a);
}
static void test_dao_history_and_relations(void)
{
Fixture f=fixture_new();GError*e=NULL;
IdentityTraceabilityDao*d=identity_traceability_dao_new(f.database);
DocumentAuthenticityAssessment*a1=document_authenticity_assessment_new(
ASSESSMENT1,EVIDENCE,RUN,"indeterminate",NULL,AT,NULL,"Note SPECIMEN");
DocumentAuthenticityAssessment*a2=document_authenticity_assessment_new(
ASSESSMENT2,EVIDENCE,RUN,"suspicious","Anomalie SPECIMEN",
"2026-07-30T11:00:00Z",ASSESSMENT1,NULL);
g_assert_true(identity_traceability_dao_insert_authenticity(d,a1,&e));
g_assert_no_error(e);g_assert_true(identity_traceability_dao_insert_authenticity(d,a2,&e));
GPtrArray*h=identity_traceability_dao_list_authenticity(d,EVIDENCE,&e);
g_assert_cmpuint(h->len,==,2);g_ptr_array_unref(h);
DocumentAuthenticityAssessment*current=
identity_traceability_dao_current_authenticity(d,EVIDENCE,&e);
g_assert_cmpstr(current->identifier,==,ASSESSMENT2);
PersonEvidenceFactualRelation*r=person_evidence_factual_relation_new(
FACT,PERSON,EVIDENCE,RUN,"identity_observed_in","Observation SPECIMEN",AT,TRUE);
g_assert_true(identity_traceability_dao_insert_factual_relation(d,r,&e));
GPtrArray*relations=identity_traceability_dao_list_factual_relations(d,EVIDENCE,&e);
g_assert_cmpuint(relations->len,==,1);g_ptr_array_unref(relations);
GPtrArray*roles=identity_traceability_dao_list_roles(d,TRUE,&e);
g_assert_cmpuint(roles->len,>=,14);g_ptr_array_unref(roles);
roles=identity_traceability_dao_list_roles(d,FALSE,&e);
g_assert_cmpuint(roles->len,==,10);
for(guint i=0;i<roles->len;i++){
PersonRoleVocabularyEntry*role=g_ptr_array_index(roles,i);
g_assert_true(role->active);g_assert_nonnull(role->code);
g_assert_nonnull(role->label);g_assert_nonnull(role->description);
if(i>0)g_assert_cmpint(((PersonRoleVocabularyEntry*)
g_ptr_array_index(roles,i-1))->display_order,<,role->display_order);
}
g_ptr_array_unref(roles);
GPtrArray*statuses=identity_traceability_dao_list_identification_statuses(
d,FALSE,&e);g_assert_no_error(e);g_assert_cmpuint(statuses->len,==,6);
for(guint i=0;i<statuses->len;i++){
IdentificationStatusVocabularyEntry*status=g_ptr_array_index(statuses,i);
g_assert_true(status->active);g_assert_nonnull(status->description);
if(i>0)g_assert_cmpint(((IdentificationStatusVocabularyEntry*)
g_ptr_array_index(statuses,i-1))->display_order,<,status->display_order);
}
g_ptr_array_unref(statuses);
person_evidence_factual_relation_free(r);
document_authenticity_assessment_free(current);
document_authenticity_assessment_free(a2);document_authenticity_assessment_free(a1);
identity_traceability_dao_free(d);fixture_free(&f);
}
static void test_sqlite_negative_guards(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=ON;");
char*before=scalar(d,"SELECT raw_value FROM identity_field_observations WHERE id='" FIELD "';");
char*message=NULL;
g_assert_cmpint(sqlite3_exec(d,"INSERT INTO document_authenticity_assessments"
"(id,evidence_id,status,assessed_at,origin) VALUES("
"'80000000-0000-4000-8000-000000000018','" EVIDENCE "',"
"'confirmed_forged','" AT "','human');",NULL,NULL,&message),!=,SQLITE_OK);
sqlite3_free(message);message=NULL;
g_assert_cmpint(sqlite3_exec(d,"INSERT INTO person_evidence_factual_relations"
"(id,person_id,evidence_id,relation_type,observed_at,origin) VALUES("
"'80000000-0000-4000-8000-000000000019','" PERSON "','" EVIDENCE "',"
"'is_author','" AT "','human');",NULL,NULL,&message),!=,SQLITE_OK);
sqlite3_free(message);message=NULL;
g_assert_cmpint(sqlite3_exec(d,"UPDATE identity_field_observations SET "
"confirmed_value='SPECIMEN',confirmation_state='human_confirmed',"
"value_quality='uncertain' WHERE id='" FIELD "';",NULL,NULL,&message),!=,SQLITE_OK);
sqlite3_free(message);
char*raw=scalar(d,"SELECT raw_value FROM identity_field_observations WHERE id='" FIELD "';");
char*confirmed=scalar(d,"SELECT COALESCE(confirmed_value,'NULL') FROM "
"identity_field_observations WHERE id='" FIELD "';");
char*auth=scalar(d,"SELECT COUNT(*) FROM document_authenticity_assessments;");
char*relations=scalar(d,"SELECT COUNT(*) FROM person_evidence_factual_relations;");
g_assert_cmpstr(raw,==,before);g_assert_cmpstr(confirmed,==,"NULL");
g_assert_cmpstr(auth,==,"0");g_assert_cmpstr(relations,==,"0");
exec_ok(d,"UPDATE identity_field_observations SET "
"confirmed_value='CONFIRMÉ SPECIMEN',confirmation_state='human_confirmed',"
"value_quality='partial' WHERE id='" FIELD "';");
g_free(before);g_free(raw);g_free(confirmed);g_free(auth);g_free(relations);
sqlite3_close(d);f.database=database_open(f.path);g_assert_nonnull(f.database);
g_assert_true(database_migrate_to_latest(f.database));
IdentityOcrDao*ocr=identity_ocr_dao_new(f.database);GError*error=NULL;
GPtrArray*confirmed_fields=identity_ocr_dao_list_confirmed_fields(
ocr,DOCUMENT,&error);g_assert_no_error(error);
g_assert_cmpuint(confirmed_fields->len,==,1);
IdentityFieldObservationRecord*record=g_ptr_array_index(confirmed_fields,0);
g_assert_cmpstr(record->confirmed_value,==,"CONFIRMÉ SPECIMEN");
g_ptr_array_unref(confirmed_fields);identity_ocr_dao_free(ocr);fixture_free(&f);
}
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 person_identification_assessments;"
"DROP TABLE person_evidence_factual_relations;"
"DROP TABLE document_authenticity_assessments;"
"DROP TABLE person_role_vocabulary;DROP TABLE identification_status_vocabulary;"
"ALTER TABLE identity_field_observations RENAME TO identity_fields_v18_old;"
"CREATE TABLE identity_field_observations(id TEXT PRIMARY KEY,"
"observation_id TEXT NOT NULL,field_code TEXT NOT NULL,raw_value TEXT,"
"corrected_value TEXT,normalized_value TEXT,confidence REAL,"
"review_status TEXT NOT NULL,origin TEXT NOT NULL,evidence_id TEXT NOT NULL,"
"ocr_run_id TEXT NOT NULL,page_number INTEGER NOT NULL,source_x INTEGER,"
"source_y INTEGER,source_width INTEGER,source_height INTEGER,"
"source_image_width INTEGER,source_image_height INTEGER,display_order INTEGER,"
"reviewed_at TEXT NOT NULL,review_note TEXT);"
"INSERT INTO identity_field_observations SELECT id,observation_id,field_code,"
"raw_value,'CORRIGÉ SPECIMEN',normalized_value,confidence,review_status,origin,"
"evidence_id,ocr_run_id,page_number,source_x,source_y,source_width,source_height,"
"source_image_width,source_image_height,display_order,reviewed_at,review_note "
"FROM identity_fields_v18_old;DROP TABLE identity_fields_v18_old;"
"UPDATE metadata SET value='17' WHERE key='schema_version';"
"CREATE TABLE identification_status_vocabulary(dummy TEXT);");
sqlite3_close(d);f.database=database_open(f.path);g_assert_nonnull(f.database);
g_test_expect_message(NULL,G_LOG_LEVEL_WARNING,
"*Impossible dinstaller la migration SQLite V18*");
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);
char*rolled_back=scalar(d,
"SELECT value FROM metadata WHERE key='schema_version';");
char*partial=scalar(d,"SELECT COUNT(*) FROM sqlite_master WHERE type='table' "
"AND name='document_authenticity_assessments';");
g_assert_cmpstr(rolled_back,==,"17");g_assert_cmpstr(partial,==,"0");
g_free(rolled_back);g_free(partial);
exec_ok(d,"DROP TABLE identification_status_vocabulary;");
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*values=scalar(d,"SELECT raw_value||':'||normalized_value||':'||corrected_value"
"||':'||confirmation_state FROM identity_field_observations WHERE id='" FIELD "';");
g_assert_cmpstr(version,==,"18");
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));
fixture_free(&f);
}
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/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

@ -56,7 +56,8 @@ static void assert_empty_after_reopen(const char *database_path)
static const char *const tables[] = {
"entites", "preuves", "preuve_entites", "preuve_entite_sources",
"person_role_assignments", "identity_ocr_runs",
"identity_document_observations", "identity_field_observations"
"identity_document_observations", "identity_field_observations",
"person_evidence_factual_relations"
};
Database *database = database_open(database_path);
g_assert_nonnull(database);
@ -100,6 +101,7 @@ static void test_failure_matrix(void)
{PERSON_CREATION_FAILURE_CREATE_SOURCE,0},
{PERSON_CREATION_FAILURE_CREATE_SOURCE,1},
{PERSON_CREATION_FAILURE_CREATE_SOURCE,2},
{PERSON_CREATION_FAILURE_INSERT_FACTUAL_RELATION,0},
{PERSON_CREATION_FAILURE_SESSION_BEFORE_COMMIT,0},
{PERSON_CREATION_FAILURE_ARTIFACT_TEXT_CHANGED,0},
{PERSON_CREATION_FAILURE_ARTIFACT_TSV_CHANGED,0},
@ -167,11 +169,23 @@ static void test_failure_matrix(void)
.confidence = 10,
.role_assignments = roles
};
PersonCreationFactualRelationInput factual_relation = {
.evidence_selection_identifier =
person_evidence_selection_item_get_identifier(
person_evidence_selection_get(selection, 0)),
.ocr_run_identifier = identity_ocr_run_get_identifier(
g_ptr_array_index(runs, 0)),
.relation_type = "identity_observed_in",
.factual_note = "Choix humain SPECIMEN"
};
GPtrArray *factual_relations = g_ptr_array_new();
g_ptr_array_add(factual_relations, &factual_relation);
PersonCreationCoordinatorOptions options = {
.failure_point = cases[scenario].point,
.failure_occurrence = cases[scenario].occurrence,
.inject_compensation_failure =
scenario == G_N_ELEMENTS(cases) - 1
scenario == G_N_ELEMENTS(cases) - 1,
.factual_relations = factual_relations
};
char *ocr_parent=g_build_filename(root,"02_Preuves_Traitees",
"OCR",NULL);
@ -223,6 +237,7 @@ static void test_failure_matrix(void)
g_free(contents);
}
g_ptr_array_unref(roles);
g_ptr_array_unref(factual_relations);
g_ptr_array_unref(runs);
person_evidence_selection_free(selection);
g_ptr_array_unref(prepared);
@ -391,9 +406,25 @@ static void test_success_and_rollback(void)
g_ptr_array_add(coordinator_runs, coordinator_run);
PersonEvidenceSelection *task_selection =
person_evidence_selection_copy(selection);
PersonCreationFactualRelationInput explicit_relation = {
.evidence_selection_identifier =
person_evidence_selection_item_get_identifier(
person_evidence_selection_get(task_selection, 0)),
.ocr_run_identifier =
identity_ocr_run_get_identifier(coordinator_run),
.relation_type = "data_extracted_from",
.factual_note = "Choix humain explicite SPECIMEN"
};
GPtrArray *explicit_relations = g_ptr_array_new();
g_ptr_array_add(explicit_relations, &explicit_relation);
PersonCreationCoordinatorOptions explicit_options = {
.factual_relations = explicit_relations
};
database = database_open(database_path);
result = person_creation_coordinator_execute(database, root, &person,
task_selection, coordinator_runs, NULL, &error);
result = person_creation_coordinator_execute_with_options(
database, root, &person, task_selection, coordinator_runs,
&explicit_options, NULL, &error);
g_ptr_array_unref(explicit_relations);
person_evidence_selection_free(task_selection);
g_assert_no_error(error);
g_assert_nonnull(result);
@ -405,6 +436,8 @@ static void test_success_and_rollback(void)
g_assert_cmpuint(count(database, "identity_ocr_runs"), ==, 1);
g_assert_cmpuint(count(database, "identity_document_observations"), ==, 1);
g_assert_cmpuint(count(database, "identity_field_observations"), ==, 2);
g_assert_cmpuint(count(database,
"person_evidence_factual_relations"), ==, 1);
char *ocr_root = g_build_filename(root, "02_Preuves_Traitees", "OCR",
identity_ocr_run_get_identifier(coordinator_run), NULL);
char *ocr_text = g_build_filename(ocr_root, "ocr.txt", NULL);