Ajouter le modèle de graphe d'enquête en mémoire #58

Closed
opened 2026-07-21 08:06:25 +02:00 by fy59 · 0 comments
Owner

Ajouter le modèle de graphe d'enquête en mémoire

Contexte

Les composants métier et de persistance suivants existent désormais :

  • EntityRecord et EntityDao ;
  • RelationRecord et RelationDao ;
  • RelationEvidenceDao ;
  • RelationService.

SQLite reste la source de vérité de l'enquête.

Le futur tableau blanc interactif devra représenter :

  • les entités sous forme de nœuds ;
  • les relations orientées sous forme d'arêtes ;
  • les relations entrantes et sortantes d'une entité ;
  • les preuves associées aux relations dans une étape ultérieure.

L'interface GTK ne doit toutefois pas travailler directement avec les DAO ni
reconstruire elle-même les index du graphe.

Une représentation métier en mémoire est donc nécessaire entre la couche de
persistance et la future vue graphique.

Objectif

Créer un modèle InvestigationGraphModel capable de posséder et d'indexer en
mémoire :

  • des EntityRecord ;
  • des RelationRecord ;
  • les relations sortantes de chaque entité ;
  • les relations entrantes de chaque entité.

Le modèle doit être indépendant :

  • de GTK ;
  • de Cairo ;
  • de SQLite ;
  • des coordonnées graphiques ;
  • des paramètres de zoom et d'affichage.

Il ne représente que la structure métier du graphe.

Fichiers

Créer :

include/models/investigation_graph_model.h
src/models/investigation_graph_model.c
tests/test_investigation_graph_model.c

Type opaque

typedef struct InvestigationGraphModel InvestigationGraphModel;

Domaine d'erreur

Créer :

typedef enum
{
    INVESTIGATION_GRAPH_MODEL_ERROR_INVALID_ARGUMENT,
    INVESTIGATION_GRAPH_MODEL_ERROR_MEMORY,
    INVESTIGATION_GRAPH_MODEL_ERROR_DUPLICATE_ENTITY,
    INVESTIGATION_GRAPH_MODEL_ERROR_DUPLICATE_RELATION,
    INVESTIGATION_GRAPH_MODEL_ERROR_SOURCE_NOT_FOUND,
    INVESTIGATION_GRAPH_MODEL_ERROR_TARGET_NOT_FOUND,
    INVESTIGATION_GRAPH_MODEL_ERROR_ENTITY_NOT_FOUND
} InvestigationGraphModelError;

Ajouter :

#define INVESTIGATION_GRAPH_MODEL_ERROR \
    investigation_graph_model_error_quark()

GQuark investigation_graph_model_error_quark(void);

Construction

InvestigationGraphModel *investigation_graph_model_new(
    GError **error
);

Le constructeur doit créer un graphe vide possédant ses structures internes.

Destruction

void investigation_graph_model_free(
    InvestigationGraphModel *graph_model
);

La destruction doit :

  • accepter NULL ;
  • libérer toutes les entités possédées ;
  • libérer toutes les relations possédées ;
  • libérer tous les index internes ;
  • ne provoquer aucun double free.

Propriété des modèles

Entités

gboolean investigation_graph_model_add_entity(
    InvestigationGraphModel *graph_model,
    EntityRecord *entity_record,
    GError **error
);

En cas de succès :

  • le graphe prend possession de entity_record ;
  • l'appelant ne doit plus le libérer.

En cas d'échec :

  • l'appelant conserve la propriété de entity_record.

La fonction doit :

  • refuser un graphe NULL ;
  • refuser un modèle NULL ;
  • vérifier que l'UUID de l'entité est disponible ;
  • refuser deux entités possédant le même UUID ;
  • ne pas remplacer silencieusement une entité existante.

Relations

gboolean investigation_graph_model_add_relation(
    InvestigationGraphModel *graph_model,
    RelationRecord *relation_record,
    GError **error
);

En cas de succès :

  • le graphe prend possession de relation_record ;
  • l'appelant ne doit plus le libérer.

En cas d'échec :

  • l'appelant conserve la propriété de relation_record.

La fonction doit :

  • refuser un graphe NULL ;
  • refuser un modèle NULL ;
  • refuser un UUID de relation déjà présent ;
  • vérifier que l'entité source existe déjà dans le graphe ;
  • vérifier que l'entité cible existe déjà dans le graphe ;
  • conserver l'orientation source vers cible ;
  • enregistrer la relation dans l'index sortant de la source ;
  • enregistrer la relation dans l'index entrant de la cible ;
  • ne modifier aucun index si l'opération échoue.

Les entités doivent donc être ajoutées avant leurs relations.

Recherche directe

Entité

const EntityRecord *investigation_graph_model_find_entity(
    const InvestigationGraphModel *graph_model,
    const char *entity_identifier
);

La fonction doit :

  • retourner un pointeur emprunté ;
  • retourner NULL pour un graphe absent ;
  • retourner NULL pour un UUID invalide ;
  • retourner NULL lorsque l'entité n'existe pas ;
  • ne jamais transférer la propriété du modèle.

Relation

const RelationRecord *investigation_graph_model_find_relation(
    const InvestigationGraphModel *graph_model,
    const char *relation_identifier
);

Les mêmes règles de propriété et de validation s'appliquent.

Comptage

Ajouter :

guint64 investigation_graph_model_get_entity_count(
    const InvestigationGraphModel *graph_model
);

guint64 investigation_graph_model_get_relation_count(
    const InvestigationGraphModel *graph_model
);

Un graphe NULL doit retourner 0.

Listes globales

Entités

GPtrArray *investigation_graph_model_list_entities(
    const InvestigationGraphModel *graph_model,
    GError **error
);

Le tableau doit :

  • appartenir à l'appelant ;
  • contenir des pointeurs const EntityRecord * empruntés ;
  • ne posséder aucune fonction de destruction des éléments ;
  • être trié par UUID croissant ;
  • être vide sans erreur pour un graphe vide.

Relations

GPtrArray *investigation_graph_model_list_relations(
    const InvestigationGraphModel *graph_model,
    GError **error
);

Le tableau doit :

  • appartenir à l'appelant ;
  • contenir des pointeurs const RelationRecord * empruntés ;
  • ne pas détruire les relations ;
  • être trié par UUID croissant ;
  • être vide sans erreur pour un graphe vide.

Navigation du graphe

Relations sortantes

GPtrArray *investigation_graph_model_list_outgoing_relations(
    const InvestigationGraphModel *graph_model,
    const char *entity_identifier,
    GError **error
);

Le tableau doit contenir les relations dont :

source == entity_identifier

Relations entrantes

GPtrArray *investigation_graph_model_list_incoming_relations(
    const InvestigationGraphModel *graph_model,
    const char *entity_identifier,
    GError **error
);

Le tableau doit contenir les relations dont :

target == entity_identifier

Relations incidentes

GPtrArray *investigation_graph_model_list_incident_relations(
    const InvestigationGraphModel *graph_model,
    const char *entity_identifier,
    GError **error
);

Le tableau doit contenir :

  • les relations sortantes ;
  • les relations entrantes ;
  • chaque relation une seule fois.

Les trois méthodes doivent :

  • valider l'UUID ;
  • retourner une erreur ENTITY_NOT_FOUND si l'entité n'existe pas ;
  • retourner un tableau vide sans erreur si l'entité est isolée ;
  • retourner des pointeurs empruntés ;
  • trier les relations par UUID croissant ;
  • laisser la propriété des relations au graphe.

Structures internes attendues

L'implémentation doit utiliser des index permettant d'éviter de parcourir
toutes les relations pour chaque requête.

Une architecture possible est :

entities_by_identifier
relations_by_identifier
outgoing_relations_by_entity
incoming_relations_by_entity

L'implémentation exacte reste libre, mais les recherches directes doivent être
effectuées par index.

Les index peuvent être basés sur GHashTable.

Cohérence métier

Le modèle doit garantir les invariants suivants :

  • aucune entité dupliquée ;
  • aucune relation dupliquée ;
  • aucune relation dont la source est absente ;
  • aucune relation dont la cible est absente ;
  • une relation orientée reste orientée ;
  • les index entrants et sortants restent cohérents ;
  • aucune donnée graphique n'est stockée dans le modèle.

Le modèle ne doit pas modifier les statuts ou le contenu des
EntityRecord et RelationRecord.

Tests

Créer :

tests/test_investigation_graph_model.c

Scénarios minimaux

  1. création d'un graphe vide ;
  2. destruction d'un graphe vide ;
  3. destruction avec NULL ;
  4. comptage d'un graphe vide ;
  5. ajout d'une entité ;
  6. transfert de propriété après succès ;
  7. refus d'un modèle d'entité NULL ;
  8. refus d'un UUID d'entité dupliqué ;
  9. conservation de la propriété par l'appelant après échec ;
  10. ajout de plusieurs entités ;
  11. recherche d'une entité existante ;
  12. recherche d'une entité absente ;
  13. recherche avec un UUID invalide ;
  14. liste globale vide des entités ;
  15. liste globale triée des entités ;
  16. ajout d'une relation valide ;
  17. transfert de propriété de la relation après succès ;
  18. refus d'une relation NULL ;
  19. refus d'un UUID de relation dupliqué ;
  20. refus d'une relation dont la source est absente ;
  21. refus d'une relation dont la cible est absente ;
  22. absence de modification des index après un ajout refusé ;
  23. recherche d'une relation existante ;
  24. recherche d'une relation absente ;
  25. liste globale vide des relations ;
  26. liste globale triée des relations ;
  27. liste sortante d'une entité isolée ;
  28. liste entrante d'une entité isolée ;
  29. liste sortante avec plusieurs relations ;
  30. liste entrante avec plusieurs relations ;
  31. respect de l'orientation source vers cible ;
  32. liste incidente réunissant les deux orientations ;
  33. absence de doublon dans la liste incidente ;
  34. refus d'un UUID d'entité invalide dans les listes ;
  35. erreur ENTITY_NOT_FOUND pour une entité absente ;
  36. comptage après plusieurs ajouts ;
  37. accesseurs appelés avec un graphe NULL ;
  38. tous les anciens tests restent valides.

Makefile

Ajouter :

INVESTIGATION_GRAPH_MODEL_TEST_CFLAGS := \
    $(TEST_CFLAGS) \
    -Wpedantic

TEST_INVESTIGATION_GRAPH_MODEL := \
    tests/test_investigation_graph_model

Ajouter une cible dédiée compilant uniquement :

  • tests/test_investigation_graph_model.c ;
  • src/models/investigation_graph_model.c ;
  • src/models/entity_record.c ;
  • src/models/relation_record.c.

Aucun module GTK ou SQLite ne doit être nécessaire pour ce test.

Ajouter $(TEST_INVESTIGATION_GRAPH_MODEL) :

  • aux dépendances de make test ;
  • aux exécutables lancés par make test ;
  • aux fichiers supprimés par make clean.

La compilation doit conserver :

-std=c17 -Wall -Wextra -Wpedantic -Werror

Hors périmètre

Ce ticket ne couvre pas :

  • le chargement depuis SQLite ;
  • les DAO ;
  • GTK ;
  • Cairo ;
  • le tableau blanc interactif ;
  • le dessin des nœuds ;
  • les coordonnées des nœuds ;
  • les dimensions des nœuds ;
  • le zoom ;
  • le déplacement à la souris ;
  • la sélection graphique ;
  • la création graphique d'une relation ;
  • la modification ou la suppression d'un élément ;
  • les preuves associées aux relations ;
  • les groupes visuels ;
  • les filtres d'affichage ;
  • la persistance de la disposition graphique.

Le chargement du graphe depuis EntityDao et RelationDao fera l'objet du
ticket suivant.

Critères d'acceptation

  • InvestigationGraphModel est opaque.
  • Le graphe possède les entités ajoutées avec succès.
  • Le graphe possède les relations ajoutées avec succès.
  • L'appelant conserve les modèles refusés.
  • Les doublons sont refusés explicitement.
  • Une relation sans source existante est refusée.
  • Une relation sans cible existante est refusée.
  • Les recherches utilisent des index.
  • Les relations sortantes sont correctement indexées.
  • Les relations entrantes sont correctement indexées.
  • Les listes incidentes ne contiennent aucun doublon.
  • Toutes les listes sont triées par UUID.
  • Les tableaux retournés possèdent uniquement leur conteneur.
  • Les éléments retournés restent la propriété du graphe.
  • Aucun changement GTK n'est introduit.
  • Aucune dépendance SQLite n'est introduite dans le modèle.
  • Aucune donnée de disposition graphique n'est stockée.
  • Les tests ciblés passent.
  • make clean && make && make test réussit.
  • git diff --check ne retourne aucune erreur.
# Ajouter le modèle de graphe d'enquête en mémoire ## Contexte Les composants métier et de persistance suivants existent désormais : - `EntityRecord` et `EntityDao` ; - `RelationRecord` et `RelationDao` ; - `RelationEvidenceDao` ; - `RelationService`. SQLite reste la source de vérité de l'enquête. Le futur tableau blanc interactif devra représenter : - les entités sous forme de nœuds ; - les relations orientées sous forme d'arêtes ; - les relations entrantes et sortantes d'une entité ; - les preuves associées aux relations dans une étape ultérieure. L'interface GTK ne doit toutefois pas travailler directement avec les DAO ni reconstruire elle-même les index du graphe. Une représentation métier en mémoire est donc nécessaire entre la couche de persistance et la future vue graphique. ## Objectif Créer un modèle `InvestigationGraphModel` capable de posséder et d'indexer en mémoire : - des `EntityRecord` ; - des `RelationRecord` ; - les relations sortantes de chaque entité ; - les relations entrantes de chaque entité. Le modèle doit être indépendant : - de GTK ; - de Cairo ; - de SQLite ; - des coordonnées graphiques ; - des paramètres de zoom et d'affichage. Il ne représente que la structure métier du graphe. ## Fichiers Créer : ```text include/models/investigation_graph_model.h src/models/investigation_graph_model.c tests/test_investigation_graph_model.c ``` ## Type opaque ```c typedef struct InvestigationGraphModel InvestigationGraphModel; ``` ## Domaine d'erreur Créer : ```c typedef enum { INVESTIGATION_GRAPH_MODEL_ERROR_INVALID_ARGUMENT, INVESTIGATION_GRAPH_MODEL_ERROR_MEMORY, INVESTIGATION_GRAPH_MODEL_ERROR_DUPLICATE_ENTITY, INVESTIGATION_GRAPH_MODEL_ERROR_DUPLICATE_RELATION, INVESTIGATION_GRAPH_MODEL_ERROR_SOURCE_NOT_FOUND, INVESTIGATION_GRAPH_MODEL_ERROR_TARGET_NOT_FOUND, INVESTIGATION_GRAPH_MODEL_ERROR_ENTITY_NOT_FOUND } InvestigationGraphModelError; ``` Ajouter : ```c #define INVESTIGATION_GRAPH_MODEL_ERROR \ investigation_graph_model_error_quark() GQuark investigation_graph_model_error_quark(void); ``` ## Construction ```c InvestigationGraphModel *investigation_graph_model_new( GError **error ); ``` Le constructeur doit créer un graphe vide possédant ses structures internes. ## Destruction ```c void investigation_graph_model_free( InvestigationGraphModel *graph_model ); ``` La destruction doit : - accepter `NULL` ; - libérer toutes les entités possédées ; - libérer toutes les relations possédées ; - libérer tous les index internes ; - ne provoquer aucun double `free`. ## Propriété des modèles ### Entités ```c gboolean investigation_graph_model_add_entity( InvestigationGraphModel *graph_model, EntityRecord *entity_record, GError **error ); ``` En cas de succès : - le graphe prend possession de `entity_record` ; - l'appelant ne doit plus le libérer. En cas d'échec : - l'appelant conserve la propriété de `entity_record`. La fonction doit : - refuser un graphe `NULL` ; - refuser un modèle `NULL` ; - vérifier que l'UUID de l'entité est disponible ; - refuser deux entités possédant le même UUID ; - ne pas remplacer silencieusement une entité existante. ### Relations ```c gboolean investigation_graph_model_add_relation( InvestigationGraphModel *graph_model, RelationRecord *relation_record, GError **error ); ``` En cas de succès : - le graphe prend possession de `relation_record` ; - l'appelant ne doit plus le libérer. En cas d'échec : - l'appelant conserve la propriété de `relation_record`. La fonction doit : - refuser un graphe `NULL` ; - refuser un modèle `NULL` ; - refuser un UUID de relation déjà présent ; - vérifier que l'entité source existe déjà dans le graphe ; - vérifier que l'entité cible existe déjà dans le graphe ; - conserver l'orientation source vers cible ; - enregistrer la relation dans l'index sortant de la source ; - enregistrer la relation dans l'index entrant de la cible ; - ne modifier aucun index si l'opération échoue. Les entités doivent donc être ajoutées avant leurs relations. ## Recherche directe ### Entité ```c const EntityRecord *investigation_graph_model_find_entity( const InvestigationGraphModel *graph_model, const char *entity_identifier ); ``` La fonction doit : - retourner un pointeur emprunté ; - retourner `NULL` pour un graphe absent ; - retourner `NULL` pour un UUID invalide ; - retourner `NULL` lorsque l'entité n'existe pas ; - ne jamais transférer la propriété du modèle. ### Relation ```c const RelationRecord *investigation_graph_model_find_relation( const InvestigationGraphModel *graph_model, const char *relation_identifier ); ``` Les mêmes règles de propriété et de validation s'appliquent. ## Comptage Ajouter : ```c guint64 investigation_graph_model_get_entity_count( const InvestigationGraphModel *graph_model ); guint64 investigation_graph_model_get_relation_count( const InvestigationGraphModel *graph_model ); ``` Un graphe `NULL` doit retourner `0`. ## Listes globales ### Entités ```c GPtrArray *investigation_graph_model_list_entities( const InvestigationGraphModel *graph_model, GError **error ); ``` Le tableau doit : - appartenir à l'appelant ; - contenir des pointeurs `const EntityRecord *` empruntés ; - ne posséder aucune fonction de destruction des éléments ; - être trié par UUID croissant ; - être vide sans erreur pour un graphe vide. ### Relations ```c GPtrArray *investigation_graph_model_list_relations( const InvestigationGraphModel *graph_model, GError **error ); ``` Le tableau doit : - appartenir à l'appelant ; - contenir des pointeurs `const RelationRecord *` empruntés ; - ne pas détruire les relations ; - être trié par UUID croissant ; - être vide sans erreur pour un graphe vide. ## Navigation du graphe ### Relations sortantes ```c GPtrArray *investigation_graph_model_list_outgoing_relations( const InvestigationGraphModel *graph_model, const char *entity_identifier, GError **error ); ``` Le tableau doit contenir les relations dont : ```text source == entity_identifier ``` ### Relations entrantes ```c GPtrArray *investigation_graph_model_list_incoming_relations( const InvestigationGraphModel *graph_model, const char *entity_identifier, GError **error ); ``` Le tableau doit contenir les relations dont : ```text target == entity_identifier ``` ### Relations incidentes ```c GPtrArray *investigation_graph_model_list_incident_relations( const InvestigationGraphModel *graph_model, const char *entity_identifier, GError **error ); ``` Le tableau doit contenir : - les relations sortantes ; - les relations entrantes ; - chaque relation une seule fois. Les trois méthodes doivent : - valider l'UUID ; - retourner une erreur `ENTITY_NOT_FOUND` si l'entité n'existe pas ; - retourner un tableau vide sans erreur si l'entité est isolée ; - retourner des pointeurs empruntés ; - trier les relations par UUID croissant ; - laisser la propriété des relations au graphe. ## Structures internes attendues L'implémentation doit utiliser des index permettant d'éviter de parcourir toutes les relations pour chaque requête. Une architecture possible est : ```text entities_by_identifier relations_by_identifier outgoing_relations_by_entity incoming_relations_by_entity ``` L'implémentation exacte reste libre, mais les recherches directes doivent être effectuées par index. Les index peuvent être basés sur `GHashTable`. ## Cohérence métier Le modèle doit garantir les invariants suivants : - aucune entité dupliquée ; - aucune relation dupliquée ; - aucune relation dont la source est absente ; - aucune relation dont la cible est absente ; - une relation orientée reste orientée ; - les index entrants et sortants restent cohérents ; - aucune donnée graphique n'est stockée dans le modèle. Le modèle ne doit pas modifier les statuts ou le contenu des `EntityRecord` et `RelationRecord`. ## Tests Créer : ```text tests/test_investigation_graph_model.c ``` ### Scénarios minimaux 1. création d'un graphe vide ; 2. destruction d'un graphe vide ; 3. destruction avec `NULL` ; 4. comptage d'un graphe vide ; 5. ajout d'une entité ; 6. transfert de propriété après succès ; 7. refus d'un modèle d'entité `NULL` ; 8. refus d'un UUID d'entité dupliqué ; 9. conservation de la propriété par l'appelant après échec ; 10. ajout de plusieurs entités ; 11. recherche d'une entité existante ; 12. recherche d'une entité absente ; 13. recherche avec un UUID invalide ; 14. liste globale vide des entités ; 15. liste globale triée des entités ; 16. ajout d'une relation valide ; 17. transfert de propriété de la relation après succès ; 18. refus d'une relation `NULL` ; 19. refus d'un UUID de relation dupliqué ; 20. refus d'une relation dont la source est absente ; 21. refus d'une relation dont la cible est absente ; 22. absence de modification des index après un ajout refusé ; 23. recherche d'une relation existante ; 24. recherche d'une relation absente ; 25. liste globale vide des relations ; 26. liste globale triée des relations ; 27. liste sortante d'une entité isolée ; 28. liste entrante d'une entité isolée ; 29. liste sortante avec plusieurs relations ; 30. liste entrante avec plusieurs relations ; 31. respect de l'orientation source vers cible ; 32. liste incidente réunissant les deux orientations ; 33. absence de doublon dans la liste incidente ; 34. refus d'un UUID d'entité invalide dans les listes ; 35. erreur `ENTITY_NOT_FOUND` pour une entité absente ; 36. comptage après plusieurs ajouts ; 37. accesseurs appelés avec un graphe `NULL` ; 38. tous les anciens tests restent valides. ## Makefile Ajouter : ```make INVESTIGATION_GRAPH_MODEL_TEST_CFLAGS := \ $(TEST_CFLAGS) \ -Wpedantic TEST_INVESTIGATION_GRAPH_MODEL := \ tests/test_investigation_graph_model ``` Ajouter une cible dédiée compilant uniquement : - `tests/test_investigation_graph_model.c` ; - `src/models/investigation_graph_model.c` ; - `src/models/entity_record.c` ; - `src/models/relation_record.c`. Aucun module GTK ou SQLite ne doit être nécessaire pour ce test. Ajouter `$(TEST_INVESTIGATION_GRAPH_MODEL)` : - aux dépendances de `make test` ; - aux exécutables lancés par `make test` ; - aux fichiers supprimés par `make clean`. La compilation doit conserver : ```text -std=c17 -Wall -Wextra -Wpedantic -Werror ``` ## Hors périmètre Ce ticket ne couvre pas : - le chargement depuis SQLite ; - les DAO ; - GTK ; - Cairo ; - le tableau blanc interactif ; - le dessin des nœuds ; - les coordonnées des nœuds ; - les dimensions des nœuds ; - le zoom ; - le déplacement à la souris ; - la sélection graphique ; - la création graphique d'une relation ; - la modification ou la suppression d'un élément ; - les preuves associées aux relations ; - les groupes visuels ; - les filtres d'affichage ; - la persistance de la disposition graphique. Le chargement du graphe depuis `EntityDao` et `RelationDao` fera l'objet du ticket suivant. ## Critères d'acceptation - [x] `InvestigationGraphModel` est opaque. - [x] Le graphe possède les entités ajoutées avec succès. - [x] Le graphe possède les relations ajoutées avec succès. - [x] L'appelant conserve les modèles refusés. - [x] Les doublons sont refusés explicitement. - [x] Une relation sans source existante est refusée. - [x] Une relation sans cible existante est refusée. - [x] Les recherches utilisent des index. - [x] Les relations sortantes sont correctement indexées. - [x] Les relations entrantes sont correctement indexées. - [x] Les listes incidentes ne contiennent aucun doublon. - [x] Toutes les listes sont triées par UUID. - [x] Les tableaux retournés possèdent uniquement leur conteneur. - [x] Les éléments retournés restent la propriété du graphe. - [x] Aucun changement GTK n'est introduit. - [x] Aucune dépendance SQLite n'est introduite dans le modèle. - [x] Aucune donnée de disposition graphique n'est stockée. - [x] Les tests ciblés passent. - [x] `make clean && make && make test` réussit. - [x] `git diff --check` ne retourne aucune erreur.
fy59 closed this issue 2026-07-21 08:39:46 +02:00
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: fy59/labfy-investigation#58
No description provided.