Second Brain documentation

ADR 0017 – Valeurs par défaut dynamiques des aspects (`x-default`)

Accepté (2026-10-15, mode autonome)

← Toutes les décisions

ADR 0017 – Valeurs par défaut dynamiques des aspects (x-default)

Statut : Accepté (2026-10-15, mode autonome)

Décisions prises en mode autonome (Aurélien, 2026-10-15 : options les plus simples, sûres et réversibles). Un champ de premier niveau du schéma d’un aspect peut porter l’annotation x-default. C’est une chaîne qui contient au moins une variable de l’ADR 0016 ({{today}}, {{now}}, décalage ± N d|w|m), avec la même grammaire fermée, stricte et sans SpEL :

dateReception: { type: string, format: date, title: Date de réception, x-default: "{{today}}" }

x-default et default sont exclusifs sur un même champ. Au chargement, la valeur résolue à une date de référence doit être conforme au schéma de l’aspect. Sinon, le fichier est invalide, ignoré et signalé.

Le serveur applique x-default là où il applique déjà default, et seulement là : aux champs absents d’un aspect ajouté au nœud. C’est le cas à la création, au changement de type et à l’ajout d’un aspect par PUT …/aspects/{a}. Une valeur fournie n’est jamais remplacée, un aspect déjà porté n’est jamais complété, et rien n’est réécrit à la lecture. Côté serveur, la résolution se fait dans le fuseau de la JVM. La valeur passe ensuite par la validation habituelle (« valider à l’écriture ») et figure dans les événements existants (node.created, aspect.added).

Le front ne résout rien lui-même. GET /api/definitions/defaults?zone= renvoie les x-default de tous les aspects, résolus dans le fuseau du navigateur. Le formulaire les injecte comme default dans une copie du schéma. FormlyJsonschema les préremplit alors dans les sections nouvelles, sans toucher aux aspects déjà enregistrés. Le champ format: date-time reçoit un widget qui convertit entre l’heure locale et la valeur avec décalage. Sans ce correctif, ni {{now}} ni une saisie manuelle ne passent aujourd’hui.

Il n’y a pas de migration et pas de nouvel événement. Le méta-schéma des aspects gagne x-default. Exemple : courrier.dateReception: x-default "{{today}}".

Contexte

Le cahier (« Modèle de contenu », MVP) prévoit des valeurs par défaut : « Défauts et templates de valeurs par aspect ». L’ADR 0016 a livré les modèles de nœud et reporté les défauts dynamiques à une ligne de backlog distincte. Il a aussi écarté default: "{{today}}" : default doit être une valeur valide de l’instance, FormlyJsonschema l’afficherait brute et applyDefaults l’écrirait telle quelle. Il annonçait une « annotation de schéma (x-default: "{{today}}", résolue par TemplateVariables dans applyDefaults et dans le formulaire) ».

État du code :

  • NodeService.applyDefaults (privé) copie le default de chaque propriété de premier niveau dans les champs absents (!data.containsKey). Trois chemins l’appellent :

    • prepareAspect à la création, pour les aspects obligatoires du type et les aspects fournis ;
    • prepareAspect au changement de type, pour les aspects obligatoires ajoutés (ADR 0015) ;
    • putAspect, seulement si l’aspect n’est pas encore porté.

    La validation JSON Schema vient ensuite. Les valeurs complétées figurent dans node.created.aspects et dans aspect.added.after.

  • TemplateVariables (ADR 0016) fournit une grammaire fermée, resolve(text, now) et unknown(text). DefinitionLoader dispose d’une date de référence CHECK_DATE et d’un contrôle de conformité : il compile une copie du schéma de l’aspect privée de son required racine, puis valide les valeurs résolues (checkTemplate, étape 6).

  • Horloge et fuseau : un bean Clock (Clock.systemUTC(), TimeConfig) est en place. Le fuseau vient de la requête (GET …/templates/{name}/draft?zone=, 400 zone s’il est inconnu) ou, à défaut, de ZoneId.systemDefault(). Le front envoie browserTimeZone() et réessaie sans zone après un 400 sur ce paramètre (loadDraft).

  • Méta-schéma des aspects : $defs/field ne décrit que x-target. JsonSchemas déclare x-target comme annotation du dialecte. DefinitionLoader.findNestedRefs refuse un node-ref ou un x-target hors d’une propriété de premier niveau.

  • Front :

    • AspectFormBuilder.fields(aspect) passe le schéma brut à FormlyJsonschema. Celui-ci traduit default en defaultValue, que Formly applique à tout champ dont la valeur est undefined ;
    • ces champs servent aux sections nouvelles (création, selectType, addAspect) comme aux sections chargées d’un nœud existant (loadNode), et aux formulaires du dialogue « Classer comme… » ;
    • un préremplissage ne marque pas le formulaire comme modifié : stateKey ignore les modèles, et defaultValue ne rend pas le contrôle dirty.
  • Champ date-heure : mapField associe format: date-time à <input type="datetime-local">. Ce champ ne peut afficher une valeur avec décalage (2026-10-15T09:30:00+02:00 est remplacé par une chaîne vide). Il produit une valeur sans décalage (2026-10-15T09:30), que le serveur refuse puisque les assertions de format sont actives (JsonSchemas.CompiledSchema.validate). Aucun champ date-time n’existe dans config/, si bien que le défaut est latent. Mais {{now}} produit justement une valeur avec décalage. C’est la remarque soulevée à la revue de l’ADR 0016.

Trois choix sont difficiles à défaire :

  1. Le nom et la place de l’annotation, écrite à la main dans config/aspects/ puis, en V2, par l’API des définitions.
  2. La sémantique d’application (champ absent d’un aspect ajouté), dont dépendent les scripts, les événements et les règles du jalon 3.
  3. L’endroit où les variables sont résolues pour le formulaire : le contrat d’API en dépend.

Décision

Format : x-default sur un champ de premier niveau

# config/aspects/courrier.yaml (extrait)
schema:
  type: object
  properties:
    dateReception:
      type: string
      format: date
      title: Date de réception
      x-default: "{{today}}"        # résolu à l'écriture ; guillemets obligatoires
    canal:
      type: string
      enum: [poste, email, recommande, main-propre]
      default: poste                # défaut statique, inchangé

Règles :

  • Premier niveau seulement, comme default (que applyDefaults ne lit qu’à ce niveau) et comme node-ref (ADR 0005). Un x-default dans un objet imbriqué, dans items, dans $defs ou dans allOf rend le fichier invalide.
  • Une chaîne de 1 à 200 caractères, avec la grammaire de l’ADR 0016 (TemplateVariables), sans rien y ajouter. Le contrôle est strict : tout {{…}} non reconnu rend le fichier invalide. La chaîne doit contenir au moins une variable. Sinon, on écrit default.
  • Interpolation admise : x-default: "Semaine du {{today}}". Le résultat est toujours une chaîne.
  • Exclusif de default sur un même champ. Il n’y a donc pas de règle de précédence à apprendre.
  • Interdit sur un champ format: node-ref, puisqu’une variable ne produit jamais un UUID.
  • Conforme une fois résolu : la valeur résolue doit respecter le schéma de l’aspect (voir Validation). {{now}} sur un format: date, {{today}} sur un format: date-time, ou un x-default sur un champ number, boolean, enum ou pattern incompatible, sont refusés au chargement.
  • Permis sur un champ requis, et c’est même son meilleur usage : le champ est rempli sans saisie, à la création comme au changement de type.
  • Sans incidence sur version : un défaut ne change ni la forme ni le sens des données déjà stockées.
  • Guillemets YAML : sans eux, x-default: {{today}} est lu comme un objet, et le méta-schéma refuse le fichier (« must be string »), comme pour les modèles.

Méta-schéma backend/src/main/resources/definitions/meta/aspect.schema.json, dans $defs/field.properties :

"x-default": {
  "type": "string",
  "minLength": 1,
  "maxLength": 200,
  "description": "Valeur par défaut dynamique (ADR 0017) : variables {{today}} et {{now}} de l'ADR 0016, résolue à l'écriture ; exclusive de default"
}

JsonSchemas gagne public static final String X_DEFAULT = "x-default", déclaré comme AnnotationKeyword du dialecte au même titre que x-target. La validation des données reste identique à celle de la draft 2020-12.

Validation au chargement

DefinitionLoader.checkAspect appelle un nouveau contrôle :

/**
 * Contrôles des x-default (ADR 0017), champ par champ et dans cet ordre ; un champ fautif à une étape ne passe pas
 * aux suivantes. Messages préfixés « Champ <champ> : ».
 */
private List<String> checkDynamicDefaults(JsonNode schema);
  1. Un x-default hors d’une propriété de premier niveau donne « Champ : x-default n’est accepté qu’en propriété de premier niveau ». Le parcours est celui de findNestedRefs, généralisé par un prédicat : findNested(String where, JsonNode node, Predicate<JsonNode> match, String message, List<String> problems).
  2. default et x-default sur le même champ : « Champ : default et x-default sont exclusifs ».
  3. format: node-ref : « Champ : x-default n’est pas admis sur un champ node-ref ».
  4. Une variable inconnue (TemplateVariables.unknown) : « Champ : x-default, variable inconnue {{todya}} (admises : today, now, avec un décalage ± N d, w ou m) ».
  5. Aucune variable : « Champ : x-default sans variable, écrire default ».
  6. Conformité : {<f>: resolve(x-default, CHECK_DATE)} est validé contre une copie du schéma de l’aspect privée de son required racine. La copie est compilée une seule fois par aspect, dans un try/catch, comme dans checkTemplate. En cas d’échec : « Champ : x-default non conforme une fois résolu (2026-01-15) : ». Une date fixe suffit, car la forme d’une valeur résolue ne dépend pas de la date.

L’aspect fautif est ignoré et signalé dans GET /api/definitions/status, comme pour toute autre faute (ADR 0002). Au rechargement, keepPreviousOnError garde sa version précédente, sans code nouveau : les expressions sont relues depuis le schéma conservé.

Moteur

Classe pure, dans le paquet definitions :

/** Valeurs par défaut dynamiques des aspects (ADR 0017) : annotation x-default des champs de premier niveau. */
public final class AspectDefaults {

    /** x-default de premier niveau, non résolus, dans l'ordre du schéma ; vide si aucun. */
    public static Map<String, String> expressions(JsonNode schema);

    /** Les mêmes, résolus par TemplateVariables.resolve à {@code now}. */
    public static Map<String, String> resolve(JsonNode schema, ZonedDateTime now);
}

Definitions ne change pas. Les expressions sont lues dans le schéma à chaque écriture, comme applyDefaults le fait déjà pour default. Le coût est négligeable.

NodeService (paquet node) :

NodeService(NodeRepository nodes, NodeTagRepository tagRepository, NodeEventRepository events, WikiLinkService wiki,
            BodyTagIndex bodyTagIndex, DefinitionRegistry definitions, JsonMapper json, Clock clock);

/** Instant des défauts dynamiques : horloge de l'application, fuseau de la JVM (variable TZ du conteneur). */
private ZonedDateTime now() { return ZonedDateTime.now(clock.withZone(ZoneId.systemDefault())); }

/** default statique, puis x-default résolu à now, pour chaque champ de premier niveau absent de data. */
private void applyDefaults(AspectDefinition aspect, Map<String, Object> data, ZonedDateTime now);

private AspectDefinition prepareAspect(String name, Map<String, Object> data, ZonedDateTime now,
                                       Map<String, List<String>> errors);
  • create, changeType et putAspect calculent now() une fois par requête. Tous les aspects d’une même création voient donc le même instant.

  • Où la valeur s’applique. Les points d’appel ne changent pas :

    Écriture x-default appliqué
    POST /api/nodes oui, aux champs absents des aspects obligatoires du type et des aspects fournis (un aspect fourni par un modèle compris)
    PUT /api/nodes/{id}/type oui, aux champs absents des aspects obligatoires ajoutés
    PUT /api/nodes/{id}/aspects/{a}, aspect absent oui, aux champs absents
    PUT /api/nodes/{id}/aspects/{a}, aspect porté non, comme default aujourd’hui
    POST /api/nodes/files, capture, liens wiki sans objet (ni type, ni aspect)
    lecture, réindexation, rechargement de config/ jamais : les nœuds existants ne sont pas complétés
  • Une valeur fournie n’est jamais remplacée, même null ou "" : seule l’absence de la clé déclenche le défaut. La validation décide ensuite (« valider à l’écriture »).

  • Modèles (ADR 0016) : une valeur du modèle est une valeur fournie, elle l’emporte. TemplateDrafts et checkTemplate ne changent pas. Un modèle n’a pas à répéter un x-default.

  • Fuseau côté serveur : celui de la JVM. Il n’y a pas de paramètre zone sur les écritures. Le formulaire envoie déjà des valeurs résolues dans le fuseau du navigateur, et le serveur ne complète que les champs absents : scripts, champ vidé par l’utilisateur, défauts indisponibles dans le front. L’étape de déploiement fixe TZ=Europe/Brussels (ADR 0016).

  • Événements : rien de nouveau. Les valeurs complétées figurent dans node.created.aspects, dans aspect.added.after (avec reason: type.mandatory au changement de type), et dans le texte indexé si le champ est du texte libre (ADR 0013).

API

Méthode Chemin Réponse
GET /api/definitions/defaults?zone=<IANA> 200 ResolvedDefaults ; 400 zone si le fuseau est inconnu ou trop long
// DefinitionsController
@Schema(description = "Valeurs x-default résolues (ADR 0017), par aspect puis par champ ; aspects sans x-default omis")
public record ResolvedDefaults(Map<String, Map<String, String>> aspects) { }

@GetMapping("/defaults")
@Operation(summary = "Valeurs par défaut dynamiques (x-default) des aspects, résolues dans le fuseau demandé ; rien "
        + "n'est écrit. Le serveur applique les mêmes valeurs aux champs absents d'un aspect ajouté")
public ResolvedDefaults aspectDefaults(
        @Parameter(description = "Identifiant de fuseau accepté par java.time.ZoneId.of (64 caractères au plus) ; "
                + "absent = fuseau du serveur", schema = @Schema(maxLength = 64))
        @RequestParam(required = false) String zone);
// aspects triés par nom (TreeMap) ; AspectDefaults.resolve(aspect.schema(), ZonedDateTime.now(clock.withZone(zoneId)))

/** Contrôle partagé avec templateDraft : mêmes messages 400 « zone ». */
private static ZoneId zoneOf(String zone);

Le chemin est /defaults et non /aspects/defaults, pour ne pas masquer un futur /aspects/{name}.

Front

Client régénéré (skill api-contract) : DefinitionsService.aspectDefaults(zone?) et le modèle ResolvedDefaults.

  • core/time-zone.ts :

    /** Appelle `call` avec le fuseau du navigateur ; s'il est refusé (400 sur `zone`), recommence sans : le serveur prend le sien. */
    export function withBrowserZone<T>(call: (zone?: string) => Observable<T>): Observable<T>;

    loadDraft (ADR 0016) est réécrit avec cette fonction, sans changement de comportement.

  • DefinitionsStore :

    /** x-default résolus dans le fuseau du navigateur. Non mis en cache (la date change) ; toute erreur donne {} : le serveur complétera à l'écriture. */
    aspectDefaults(): Observable<Readonly<Record<string, Record<string, string>>>>;
  • core/aspect-form-builder.ts :

    /** Copie du schéma où chaque champ de `values` présent dans `properties` reçoit `default: valeur` ; l'original est intact. */
    export function withDefaults<S extends object>(schema: S, values: Readonly<Record<string, unknown>> | undefined): S;
    
    fields(aspect: AspectDefinition, defaults?: Readonly<Record<string, unknown>>): FormlyFieldConfig[];

    Formly ne remplit que les champs dont la valeur est undefined. Une valeur de modèle déjà présente l’emporte donc.

  • Éditeur (node-editor-page.ts) :

    • le forkJoin de chargement gagne defaults: this.definitions.aspectDefaults(), conservé dans un champ privé dynamicDefaults ;
    • section(name, model, mandatory, fresh = false) ne passe dynamicDefaults[name] à fields que si fresh est vrai ;
    • fresh vaut vrai dans applyCreationDefaults, pour les sections ajoutées par selectType et dans addAspect ; il vaut faux dans loadNode. Un aspect déjà enregistré n’est jamais prérempli par un x-default. Sinon, la date du jour serait écrite dans un ancien courrier au prochain enregistrement ;
    • le préremplissage ne rend pas la page « modifiée » (voir Contexte).
  • Dialogue « Classer comme… » (shared/node-type/node-type-dialog.ts) :

    • defaults = toSignal(this.definitions.aspectDefaults()) ;
    • loaded attend aussi ce signal ;
    • les formulaires des aspects manquants (tous nouveaux) reçoivent defaults()[name].

    La page inbox en profite sans modification.

Champ date-heure

Ce correctif est indépendant du reste ; il est nécessaire pour qu’un {{now}} (modèle ou x-default) et une saisie manuelle passent la validation.

  • core/date-time-input.ts, fonctions pures :

    /** Valeur RFC 3339 avec décalage → valeur d'un <input type="datetime-local"> dans le fuseau du navigateur ('YYYY-MM-DDTHH:mm', secondes si non nulles) ; '' si illisible. */
    export function toLocalInput(value: unknown): string;
    /** Saisie 'YYYY-MM-DDTHH:mm[:ss]' → RFC 3339 avec le décalage du navigateur à cette date-là (secondes complétées) ; undefined si vide ou illisible. */
    export function fromLocalInput(local: string): string | undefined;
  • core/date-time-field.ts : type Formly datetime (dateTimeFieldConfig, enveloppe form-field), enregistré dans app.config.ts à côté de node-ref. Il affiche toLocalInput(formControl.value) et n’écrit fromLocalInput(saisie) que sur saisie. Une valeur enregistrée et non touchée reste donc identique, même illisible (« tolérer à la lecture »).

  • mapField associe format: date-time au type datetime et non plus à input/datetime-local.

Exemple dans config/

config/aspects/courrier.yaml : dateReception reçoit x-default: "{{today}}". Un courrier est presque toujours saisi le jour de sa réception, et le type facture-fournisseur impose l’aspect courrier. Le modèle facture-papier garde sa ligne dateReception: "{{today}}" : elle est redondante mais inoffensive, puisque la valeur fournie l’emporte, et NodeTemplateIT reste stable. Aucun autre aspect n’en reçoit : la date d’un document n’est pas celle du jour, et l’échéance d’une tâche n’a pas de valeur naturelle.

Documentation

  • Skill definitions-format, section Aspect, paragraphe « Valeurs par défaut » :

    • default (statique) ou x-default (dynamique), exclusifs ;
    • premier niveau seulement ;
    • variables et guillemets ;
    • application aux champs absents d’un aspect ajouté, jamais aux aspects portés ;
    • fuseau.

    La section Expressions mentionne x-default à côté des modèles.

  • Backlog : la case est cochée, avec un résumé et les éléments reportés.

  • CLAUDE.md ne change pas.

Tests

Backend, unitaires :

  • AspectDefaultsTest :
    • expressions dans l’ordre du schéma ; vide sans x-default ou sans properties ;
    • resolve : {{today}}, {{today + 30d}}, une interpolation, et {{now + 1w}} qui franchit le dernier dimanche d’octobre (heure locale conservée, décalage +01:00) ;
    • le schéma n’est pas modifié.
  • DefinitionLoaderTest :
    • accepté : un aspect avec x-default sur format: date, date-time et string libre, y compris sur un champ requis ;
    • refusé : variable inconnue, absence de variable, default et x-default ensemble, x-default imbriqué (objet, items), {{now}} sur format: date, {{today}} sur date-time, x-default sur number, sur un enum et sur un node-ref, x-default: {{today}} sans guillemets, chaîne de plus de 200 caractères ;
    • un aspect refusé pour son x-default ignore ses types dépendants et n’empêche pas les autres aspects de se charger.
  • JsonSchemasTest : un schéma qui porte x-default valide les données exactement comme la draft 2020-12.
  • ExampleDefinitionsTest : AspectDefaults.expressions(courrier) vaut {dateReception: "{{today}}"}.

Backend, intégration (AspectDefaultsIT, Testcontainers, Clock fixée par @TestBean au 2026-10-14T23:30:00Z) :

  • GET /api/definitions/defaults :
    • avec zone=Europe/Brussels, courrier.dateReception vaut 2026-10-15 ; avec zone=UTC, 2026-10-14 ;
    • sans zone, la valeur est LocalDate.ofInstant(…, ZoneId.systemDefault()). Le test calcule cette valeur attendue : une assertion du type startsWith("2026-10-") serait vacante ;
    • zone=Mars/Olympus donne 400 zone ; seuls les aspects porteurs d’un x-default figurent dans la réponse.
  • POST /api/nodes de type facture-fournisseur :
    • sans données courrier : dateReception vaut la date de la JVM, et node.created.aspects.courrier la contient ;
    • avec dateReception: "2026-01-02" : la valeur est conservée.
  • PUT …/aspects/courrier :
    • sur un nœud sans courrier, la valeur est complétée et aspect.added.after la contient ;
    • sur un nœud qui porte courrier sans dateReception, rien n’est complété.
  • PUT …/type vers facture-fournisseur (avec facture.montant et facture.echeance) : la valeur est complétée, et aspect.added porte reason: type.mandatory.
  • OpenApiContractIT : présence de /api/definitions/defaults.

Front (Vitest) :

  • aspect-form-builder.spec.ts :

    • withDefaults : copie, champs inconnus ignorés, original intact ;
    • fields(aspect, defaults) produit defaultValue ;
    • format: date-time donne le type datetime.
  • time-zone.spec.ts : withBrowserZone réessaie sans zone après un 400 sur zone, et pas après un autre 400.

  • node-editor-page-defaults.spec.ts :

    • ?type=facture-fournisseur préremplit dateReception, et la page n’est pas modifiée ;
    • la valeur d’un modèle l’emporte ;
    • en édition, un courrier enregistré sans dateReception reste vide ;
    • addAspect('courrier') en édition préremplit le champ ;
    • si aspectDefaults échoue, le formulaire s’ouvre avec le champ vide.
  • node-type-dialog.spec.ts : le formulaire de l’aspect manquant est prérempli.

  • date-time-input.spec.ts :

    • aller-retour local ↔ décalage, y compris de part et d’autre d’un changement d’heure ;
    • Z et +05:30 lus ;
    • valeur illisible donnant '', saisie vide donnant undefined.

    Un fuseau de test est fixé (TZ=Europe/Brussels dans la configuration Vitest, ou un bouchon de Date).

Découpage

  1. Annotation validée (feat(definitions): annotation x-default des aspects, validée au chargement) :

    • méta-schéma, JsonSchemas.X_DEFAULT, AspectDefaults, checkDynamicDefaults et généralisation de findNestedRefs ;
    • AspectDefaultsTest, DefinitionLoaderTest et JsonSchemasTest ;
    • skill definitions-format (format et contrôles).

    Testable seule : un aspect cassé exprès apparaît dans GET /api/definitions/status, un aspect correct se charge. Rien n’est encore appliqué.

  2. Application à l’écriture et endpoint (feat(nodes): valeurs par défaut dynamiques à la création, au changement de type et à l'ajout d'un aspect) :

    • Clock dans NodeService, applyDefaults(…, now) ;
    • GET /api/definitions/defaults et zoneOf ;
    • l’exemple courrier.dateReception et ExampleDefinitionsTest ;
    • AspectDefaultsIT et OpenApiContractIT ;
    • le skill (application et fuseau).

    Testable seule par Swagger UI : on lit les défauts, puis on crée une facture fournisseur sans date de réception. Lancer toute la suite verify : les tests qui créent une facture-fournisseur voient désormais dateReception.

  3. Préremplissage du formulaire (feat(nodes): préremplissage des valeurs par défaut dynamiques) :

    • client régénéré, withBrowserZone, DefinitionsStore.aspectDefaults, withDefaults, fields(aspect, defaults) ;
    • sections fresh de l’éditeur et dialogue « Classer comme… » ;
    • tests et backlog.
  4. Champ date-heure (fix(nodes): champ date-heure saisi en heure locale et enregistré avec décalage) :

    • date-time-input.ts, type Formly datetime, mapField ;
    • tests.

    Cette tranche est indépendante des trois autres et peut être livrée avant elles.

Alternatives écartées

  • default: "{{today}}" (déjà écarté par l’ADR 0016) : default doit être une valeur valide de l’instance. FormlyJsonschema et tout outil JSON Schema l’afficheraient brute.
  • Un bloc defaults: au niveau de l’aspect, hors du schéma (defaults: { dateReception: "{{today}}" }) : le schéma resterait pur, mais l’information d’un champ serait répartie entre deux endroits du fichier, et le contrôle « champ connu » serait à réécrire. x-default se lit à côté du champ, comme x-target (ADR 0005), et le nom est celui qu’annonçait le backlog.
  • Résoudre les variables dans le front, avec une grammaire portée en TypeScript et une table de cas commune (sur le modèle de body-tag-cases.json) :
    • le calcul des dates est le point délicat. plusMonths borne la fin de mois, now ± N conserve l’heure locale à travers un changement d’heure (bogue corrigé après la revue de l’ADR 0016), et le décalage doit être formaté ;
    • en JavaScript, sans Temporal disponible partout, ce calcul est sujet aux erreurs, et il faudrait tenir deux implémentations synchronisées ;
    • l’endpoint ne coûte qu’un petit GET par ouverture de formulaire, et il sert aussi aux scripts. Le choix de l’ADR 0016 (« le serveur seul maître des variables ») s’applique ici aussi.
  • Un brouillon par aspect (GET /api/definitions/aspects/{name}/defaults) : il faudrait un appel asynchrone à chaque addAspect ou selectType, avec ses états intermédiaires. Un seul appel pour tous les aspects, au chargement, est plus simple, et la réponse est minuscule.
  • Des défauts résolus dans GET /api/definitions/aspects?zone= : DefinitionsStore met les aspects en cache pour toute la session, et la date serait périmée après minuit dans un onglet resté ouvert. Les définitions brutes doivent par ailleurs rester telles qu’écrites, en vue de l’édition par l’API en V2.
  • Des défauts ajoutés au brouillon de modèle (NodeDraft) : cela ne couvre que les modèles, et laisse de côté le type choisi dans le formulaire, l’ajout d’un aspect et le dialogue « Classer comme… ».
  • Un paramètre zone sur POST /api/nodes, PUT …/type et PUT …/aspects/{a} : trois contrats modifiés pour des cas marginaux (champ vidé, script), alors que le formulaire envoie déjà des valeurs résolues dans le bon fuseau. Ce paramètre est reporté avec app.zone.
  • x-default à toute profondeur : applyDefaults ne traite que le premier niveau. Un défaut dans un tableau demanderait une sémantique (par élément ? à la création du tableau ?), et aucun cas d’usage ne l’exige. La règle est celle des node-ref.
  • default et x-default cumulables avec une précédence : une règle de plus à apprendre, sans cas d’usage.
  • Tolérer un x-default sans variable : il y aurait deux façons d’écrire un défaut statique. Relâcher la règle plus tard ne casse rien ; la durcir, si.
  • Ignorer seulement le x-default fautif (avertissement) au lieu de rejeter l’aspect : ce serait plus doux, mais introduirait une gravité « avertissement » que le moteur n’a pas. Le statut d’un fichier resterait aussi ambigu. Toute faute rend le fichier invalide (ADR 0002), et keepPreviousOnError protège un rechargement.
  • Compléter aussi les aspects déjà portés (à la lecture, à la mise à jour, ou par un rattrapage) : la date du jour serait écrite dans d’anciens nœuds qui n’avaient pas cette information, ce qui falsifie les données. Les données anciennes restent lisibles telles quelles (« tolérer à la lecture »).
  • Interdire {{now}} dans x-default au lieu de corriger le champ date-heure : la grammaire varierait selon le contexte, et la saisie manuelle d’une date-heure resterait impossible.
  • Appliquer x-default à une valeur présente mais vide ("") : le front transforme déjà "" en champ absent (emptyToUndefined, cleanAspectModel). Côté serveur, une valeur présente est une valeur fournie, et la validation tranche.

Conséquences

  • Une facture fournisseur ou un courrier reçoit la date du jour à la création, quel que soit le chemin : formulaire, palette, modèle, « Classer comme… » depuis l’inbox, ajout de l’aspect, script. La date est visible dans le formulaire avant l’enregistrement.
  • Un champ vidé à la création est rempli de nouveau par le serveur, comme aujourd’hui pour un default statique : le serveur ne distingue pas « vidé » d’« absent ». Pour qu’un champ puisse rester vide, on ne lui donne pas de x-default. Après la création, le vider fonctionne, puisqu’un aspect porté n’est jamais complété.
  • Si GET /api/definitions/defaults échoue, le champ s’affiche vide et le serveur le remplit à l’enregistrement, dans le fuseau de la JVM. La création n’est jamais bloquée.
  • Hors formulaire (scripts), les dates dépendent du fuseau du conteneur. La variable TZ du déploiement, déjà nécessaire pour l’ADR 0016, devient plus importante.
  • Une faute dans un x-default rend tout l’aspect invalide au premier démarrage, et donc aussi les types qui l’imposent. Au rechargement, la version précédente est gardée. Le message désigne le champ et l’ADR guide la correction.
  • Les default statiques ne changent pas : FormlyJsonschema continue de les appliquer aux sections d’un aspect déjà enregistré, alors que le serveur ne le fait pas. Cette divergence existait avant cette décision. Elle n’est pas étendue aux x-default (voir Reporté).
  • Le champ date-heure affiche l’heure locale du navigateur et enregistre une valeur avec décalage. Une valeur stockée sans décalage (donnée ancienne ou importée) s’affiche vide mais n’est pas modifiée tant qu’on ne la saisit pas.
  • GET /api/definitions/aspects expose x-default brut, comme x-target. Les clients l’ignorent ou s’en servent pour l’affichage.
  • Au jalon 3, l’annotation pourra accepter des variables du nœud ({{facture.echeance - 7d}}) sans invalider l’existant, puisque la grammaire actuelle est un sous-ensemble de ce langage. Les défauts qui dépendent d’autres champs relèvent plutôt des règles aspect-added.
  • L’API des définitions (V2) devra écrire x-default et le valider à blanc.

Reporté

  • x-default imbriqué (objets, éléments de tableau) et valeurs non textuelles (nombre ou booléen calculés).
  • Variables du contexte ({{title}}, {{facture.echeance - 7d}}, {{parent.title}}) et SpEL : jalon 3, avec les règles.
  • Paramètre zone sur les écritures et propriété serveur app.zone, comme fuseau par défaut et pour les règles (jalon 3, ADR 0016).
  • Alignement des default statiques sur la règle « aspect ajouté seulement » dans le formulaire d’édition. Cela changerait un comportement existant, et ce choix revient à Aurélien.
  • Affichage des défauts (statiques et dynamiques) dans les cartes d’aspect de la page des définitions.
  • Défaut par type (« pour une facture fournisseur, canal: poste ») : les modèles de nœud (ADR 0016) couvrent ce besoin aujourd’hui.

Questions ouvertes tranchées (mode autonome)

  • Nom et place : x-default, sur une propriété de premier niveau du schéma d’un aspect. Toute autre place rend le fichier invalide.
  • Cohabitation avec default : exclusifs sur un même champ. Un x-default sans variable est refusé, au profit de default.
  • Grammaire : celle de l’ADR 0016, inchangée (today, now, ± N d|w|m), stricte, interpolation admise, résultat toujours textuel.
  • Validation au chargement : variables reconnues, au moins une variable, pas de node-ref, valeur résolue à CHECK_DATE conforme au schéma de l’aspect, champs requis exceptés.
  • Application : champs absents d’un aspect ajouté (création, changement de type, putAspect d’un aspect non porté). Jamais sur un aspect porté, jamais à la lecture, jamais à la place d’une valeur fournie. Une valeur de modèle l’emporte.
  • Fuseau : navigateur pour le formulaire (GET /api/definitions/defaults?zone=) ; JVM pour ce que le serveur complète, sans paramètre zone sur les écritures.
  • Front : pas de grammaire en TypeScript. Les défauts sont résolus par le serveur et injectés comme default dans une copie du schéma, pour les sections nouvelles seulement.
  • datetime-local et décalage : correctif du champ date-heure (affichage local, enregistrement avec décalage), dans une tranche indépendante.
  • Exemple : courrier.dateReception: "{{today}}", couvert par ExampleDefinitionsTest et AspectDefaultsIT.
  • Migration : aucune. Événements : aucun nouveau.