ADR 0017 – Valeurs par défaut dynamiques des aspects (`x-default`)
Accepté (2026-10-15, mode autonome)
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 ledefaultde 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 ;prepareAspectau 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.aspectset dansaspect.added.after. -
TemplateVariables(ADR 0016) fournit une grammaire fermée,resolve(text, now)etunknown(text).DefinitionLoaderdispose d’une date de référenceCHECK_DATEet d’un contrôle de conformité : il compile une copie du schéma de l’aspect privée de sonrequiredracine, 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=, 400zones’il est inconnu) ou, à défaut, deZoneId.systemDefault(). Le front envoiebrowserTimeZone()et réessaie sanszoneaprès un 400 sur ce paramètre (loadDraft). -
Méta-schéma des aspects :
$defs/fieldne décrit quex-target.JsonSchemasdéclarex-targetcomme annotation du dialecte.DefinitionLoader.findNestedRefsrefuse unnode-refou unx-targethors d’une propriété de premier niveau. -
Front :
AspectFormBuilder.fields(aspect)passe le schéma brut à FormlyJsonschema. Celui-ci traduitdefaultendefaultValue, que Formly applique à tout champ dont la valeur estundefined;- 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é :
stateKeyignore les modèles, etdefaultValuene rend pas le contrôledirty.
-
Champ date-heure :
mapFieldassocieformat: date-timeà<input type="datetime-local">. Ce champ ne peut afficher une valeur avec décalage (2026-10-15T09:30:00+02:00est 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 champdate-timen’existe dansconfig/, 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 :
- Le nom et la place de l’annotation, écrite à la main dans
config/aspects/puis, en V2, par l’API des définitions. - 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.
- 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(queapplyDefaultsne lit qu’à ce niveau) et commenode-ref(ADR 0005). Unx-defaultdans un objet imbriqué, dansitems, dans$defsou dansallOfrend 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 écritdefault. - Interpolation admise :
x-default: "Semaine du {{today}}". Le résultat est toujours une chaîne. - Exclusif de
defaultsur 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 unformat: date,{{today}}sur unformat: date-time, ou unx-defaultsur un champnumber,boolean,enumoupatternincompatible, 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);
- Un
x-defaulthors 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). defaultetx-defaultsur le même champ : « Champ: default et x-default sont exclusifs ». format: node-ref: « Champ: x-default n’est pas admis sur un champ node-ref ». - Une variable inconnue (
TemplateVariables.unknown) : « Champ: x-default, variable inconnue {{todya}} (admises : today, now, avec un décalage ± N d, w ou m) ». - Aucune variable : « Champ
: x-default sans variable, écrire default ». - Conformité :
{<f>: resolve(x-default, CHECK_DATE)}est validé contre une copie du schéma de l’aspect privée de sonrequiredracine. La copie est compilée une seule fois par aspect, dans untry/catch, comme danscheckTemplate. 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,changeTypeetputAspectcalculentnow()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-defaultappliquéPOST /api/nodesoui, aux champs absents des aspects obligatoires du type et des aspects fournis (un aspect fourni par un modèle compris) PUT /api/nodes/{id}/typeoui, aux champs absents des aspects obligatoires ajoutés PUT /api/nodes/{id}/aspects/{a}, aspect absentoui, aux champs absents PUT /api/nodes/{id}/aspects/{a}, aspect porténon, comme defaultaujourd’huiPOST /api/nodes/files, capture, liens wikisans 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
nullou"": 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.
TemplateDraftsetcheckTemplatene changent pas. Un modèle n’a pas à répéter unx-default. -
Fuseau côté serveur : celui de la JVM. Il n’y a pas de paramètre
zonesur 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 fixeTZ=Europe/Brussels(ADR 0016). -
Événements : rien de nouveau. Les valeurs complétées figurent dans
node.created.aspects, dansaspect.added.after(avecreason: type.mandatoryau 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
forkJoinde chargement gagnedefaults: this.definitions.aspectDefaults(), conservé dans un champ privédynamicDefaults; section(name, model, mandatory, fresh = false)ne passedynamicDefaults[name]àfieldsque sifreshest vrai ;freshvaut vrai dansapplyCreationDefaults, pour les sections ajoutées parselectTypeet dansaddAspect; il vaut faux dansloadNode. Un aspect déjà enregistré n’est jamais prérempli par unx-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).
- le
-
Dialogue « Classer comme… » (
shared/node-type/node-type-dialog.ts) :defaults = toSignal(this.definitions.aspectDefaults());loadedattend 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 Formlydatetime(dateTimeFieldConfig, enveloppeform-field), enregistré dansapp.config.tsà côté denode-ref. Il affichetoLocalInput(formControl.value)et n’écritfromLocalInput(saisie)que sur saisie. Une valeur enregistrée et non touchée reste donc identique, même illisible (« tolérer à la lecture »). -
mapFieldassocieformat: date-timeau typedatetimeet 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) oux-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.mdne change pas.
Tests
Backend, unitaires :
AspectDefaultsTest:expressionsdans l’ordre du schéma ; vide sansx-defaultou sansproperties;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-defaultsurformat: date,date-timeetstringlibre, y compris sur un champ requis ; - refusé : variable inconnue, absence de variable,
defaultetx-defaultensemble,x-defaultimbriqué (objet,items),{{now}}surformat: date,{{today}}surdate-time,x-defaultsurnumber, sur unenumet sur unnode-ref,x-default: {{today}}sans guillemets, chaîne de plus de 200 caractères ; - un aspect refusé pour son
x-defaultignore ses types dépendants et n’empêche pas les autres aspects de se charger.
- accepté : un aspect avec
JsonSchemasTest: un schéma qui portex-defaultvalide 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.dateReceptionvaut2026-10-15; aveczone=UTC,2026-10-14; - sans
zone, la valeur estLocalDate.ofInstant(…, ZoneId.systemDefault()). Le test calcule cette valeur attendue : une assertion du typestartsWith("2026-10-")serait vacante ; zone=Mars/Olympusdonne 400zone; seuls les aspects porteurs d’unx-defaultfigurent dans la réponse.
- avec
POST /api/nodesde typefacture-fournisseur:- sans données
courrier:dateReceptionvaut la date de la JVM, etnode.created.aspects.courrierla contient ; - avec
dateReception: "2026-01-02": la valeur est conservée.
- sans données
PUT …/aspects/courrier:- sur un nœud sans
courrier, la valeur est complétée etaspect.added.afterla contient ; - sur un nœud qui porte
courriersansdateReception, rien n’est complété.
- sur un nœud sans
PUT …/typeversfacture-fournisseur(avecfacture.montantetfacture.echeance) : la valeur est complétée, etaspect.addedportereason: 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)produitdefaultValue;format: date-timedonne le typedatetime.
-
time-zone.spec.ts:withBrowserZoneréessaie sanszoneaprès un 400 surzone, et pas après un autre 400. -
node-editor-page-defaults.spec.ts:?type=facture-fournisseurpréremplitdateReception, et la page n’est pas modifiée ;- la valeur d’un modèle l’emporte ;
- en édition, un
courrierenregistré sansdateReceptionreste 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 ;
Zet+05:30lus ;- valeur illisible donnant
'', saisie vide donnantundefined.
Un fuseau de test est fixé (
TZ=Europe/Brusselsdans la configuration Vitest, ou un bouchon deDate).
Découpage
-
Annotation validée (
feat(definitions): annotation x-default des aspects, validée au chargement) :- méta-schéma,
JsonSchemas.X_DEFAULT,AspectDefaults,checkDynamicDefaultset généralisation defindNestedRefs; AspectDefaultsTest,DefinitionLoaderTestetJsonSchemasTest;- 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é. - méta-schéma,
-
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) :ClockdansNodeService,applyDefaults(…, now);GET /api/definitions/defaultsetzoneOf;- l’exemple
courrier.dateReceptionetExampleDefinitionsTest; AspectDefaultsITetOpenApiContractIT;- 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 unefacture-fournisseurvoient désormaisdateReception. -
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
freshde l’éditeur et dialogue « Classer comme… » ; - tests et backlog.
- client régénéré,
-
Champ date-heure (
fix(nodes): champ date-heure saisi en heure locale et enregistré avec décalage) :date-time-input.ts, type Formlydatetime,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) :defaultdoit ê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-defaultse lit à côté du champ, commex-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.
plusMonthsborne la fin de mois,now ± Nconserve 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
Temporaldisponible partout, ce calcul est sujet aux erreurs, et il faudrait tenir deux implémentations synchronisées ; - l’endpoint ne coûte qu’un petit
GETpar 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.
- le calcul des dates est le point délicat.
- Un brouillon par aspect (
GET /api/definitions/aspects/{name}/defaults) : il faudrait un appel asynchrone à chaqueaddAspectouselectType, 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=:DefinitionsStoremet 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
zonesurPOST /api/nodes,PUT …/typeetPUT …/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é avecapp.zone. x-defaultà toute profondeur :applyDefaultsne 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 desnode-ref.defaultetx-defaultcumulables avec une précédence : une règle de plus à apprendre, sans cas d’usage.- Tolérer un
x-defaultsans 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-defaultfautif (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), etkeepPreviousOnErrorprotè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}}dansx-defaultau 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
defaultstatique : le serveur ne distingue pas « vidé » d’« absent ». Pour qu’un champ puisse rester vide, on ne lui donne pas dex-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
TZdu déploiement, déjà nécessaire pour l’ADR 0016, devient plus importante. - Une faute dans un
x-defaultrend 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
defaultstatiques 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 auxx-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/aspectsexposex-defaultbrut, commex-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èglesaspect-added. - L’API des définitions (V2) devra écrire
x-defaultet le valider à blanc.
Reporté
x-defaultimbriqué (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
zonesur les écritures et propriété serveurapp.zone, comme fuseau par défaut et pour les règles (jalon 3, ADR 0016). - Alignement des
defaultstatiques 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. Unx-defaultsans variable est refusé, au profit dedefault. - 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_DATEconforme au schéma de l’aspect, champs requis exceptés. - Application : champs absents d’un aspect ajouté (création, changement de type,
putAspectd’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ètrezonesur les écritures. - Front : pas de grammaire en TypeScript. Les défauts sont résolus par le serveur et injectés comme
defaultdans une copie du schéma, pour les sections nouvelles seulement. datetime-localet décalage : correctif du champ date-heure (affichage local, enregistrement avec décalage), dans une tranche indépendante.- Exemple :
courrier.dateReception: "{{today}}", couvert parExampleDefinitionsTestetAspectDefaultsIT. - Migration : aucune. Événements : aucun nouveau.