Cycle de vie d'une série
Ce que la série traverse maintenant, avec les cinq états de lifecycle.state, la couverture d'observation qui autorise à conclure d'une absence, et le renvoi identity_status après une correction d'identité.
Trois axes, jamais mélangés
Une series rendue par l'API porte trois blocs qui répondent à trois questions différentes, et qu'il ne faut pas confondre.
| Axe | Champ | Question | Valeurs |
|---|---|---|---|
| Établissement | status | À quel point Noryo croit-il que c'est une série ? | emerging, established, uncertain, broken |
| Cycle de vie | lifecycle.state | Que lui arrive-t-il maintenant ? | active, late, inactive, ended, unknown |
| Identité | identity.status | Son nom est-il toujours le bon ? | current, superseded |
Une série très bien établie peut avoir cessé de prélever ; une série encore emerging peut être parfaitement vivante. Le premier axe est décrit dans Séries récurrentes, le troisième dans Scission, fusion, supersession. Cette page traite du deuxième.
Les cinq états
Dans le bloc lifecycle, seuls state, policy_version et calibration sont requis. state prend l'une des valeurs suivantes, et son sens est exact.
state | Sens |
|---|---|
active | L'échéance attendue est arrivée, ou n'est pas encore échue. |
late | Une échéance manque, et le compte était observé. |
inactive | Plusieurs échéances consécutives manquent, sous observation complète. La série ne prélève plus, sans que Noryo sache pourquoi. |
ended | Réservé à une preuve terminale explicite. Aucune accumulation d'absences ne produit cet état. |
unknown | Noryo ne peut pas conclure, et reason dit pourquoi. |
late et inactive exigent l'un comme l'autre que le compte ait été observé : ils ne se déduisent jamais du seul silence. ended ne se déduit pas non plus d'un silence, même long : un prélèvement qui s'arrête peut être une résiliation, une suspension, une carte expirée ou un échec de prélèvement, et l'API ne devine pas laquelle. Quand ended est rendu, evidence porte la preuve terminale qui l'a produit.
unknown est un résultat, pas une panne : reason en donne le motif en une phrase.
Une absence ne prouve rien sans couverture
Le cycle de vie repose sur une distinction simple : « je n'ai rien vu » n'est pas « il n'y a rien eu ». Si votre intégration cesse d'envoyer les opérations d'un compte, chaque abonnement de ce compte semblera s'arrêter. Pour que l'absence d'une échéance ait un sens, Noryo doit savoir que le compte était observé, et observé complètement.
C'est le rôle de la couverture d'observation. Vous déclarez, par POST /transactions/v1/coverage-windows, que toutes les opérations d'un compte sur un intervalle vous ont été fournies (completeness = complete). partial et unknown n'y autorisent pas : dix opérations en octobre peuvent être dix sur cent. La déclaration est enregistrée telle quelle, avec provenance = caller_asserted : Noryo ne vérifie pas l'exhaustivité d'une banque.
Le bloc lifecycle rend ce qu'il a pu vérifier dans observation_coverage :
observation_coverage | Sens |
|---|---|
not_required | Aucune échéance close : rien n'avait à être observé. |
complete | Chaque source de la série était couverte sur la fenêtre attendue. Un manque est un manque. |
incomplete | Au moins une source ne l'était pas. L'absence ne prouve rien, et state est unknown. |
unknown | Les sources sont inconnues, ou trop nombreuses pour être vérifiées. |
Tant que la couverture n'est pas complete, aucun état de manque (late, inactive) n'est conclu. Le compteur consecutive_misses n'avance que sous couverture complète ; il est absent quand rien n'est attendu, ce qui n'est pas la même ignorance que « nous ne regardions pas ».
La couverture se rattache à la série par le compte : account_hint (iban ou external_account_id) doit être le même sur la transaction enrichie et sur la fenêtre déclarée. Une transaction envoyée sans account_hint n'a aucune source à couvrir, et son lifecycle.state reste unknown.
Une fenêtre, pas une date
Quand une échéance est attendue, expected_next_window en donne l'intervalle : from, to, et tolerance_days, la demi-largeur en jours. Un salaire tombe vers les derniers jours ouvrés, un prélèvement SEPA vers le 5 : un instant exact serait une précision que Noryo n'a pas. La largeur vient du type d'ancrage, rendu dans anchor ; widened_to_family_floor indique si la tolérance a été relevée au plancher de la famille de série.
Deux champs sont recopiés ici pour que le cycle de vie se lise seul : cadence_strength est le même nombre que cadence_detail.strength (classe calendar_fill), et calendar_anchor le même objet que calendar_pattern (classe anchor_stability). Aucun des deux n'est un score supplémentaire ni une probabilité : voir Sémantique des scores. calendar_anchor vaut { "type": "unknown" } quand aucun ancrage n'a été inféré, et il est absent quand aucune échéance n'est attendue. last_observed_on donne le dernier jour où une occurrence a été vue.
Quand l'identité a été corrigée
Après une correction d'identité (split ou merge dans superseded_by.reason), l'ancien series_id reste lisible, mais son cycle de vie ne lui appartient plus : le verdict revient aux successeurs. Le bloc rend alors state = unknown avec identity_status = superseded. Ce champ n'est présent que dans ce cas et ne prend jamais la valeur current. Ce n'est pas une ignorance économique, c'est un renvoi : l'état d'identité complet, avec superseded_by et supersedes, se lit dans identity, et le déplacement de l'occurrence dans membership. La page Scission, fusion, supersession détaille ces deux blocs.
Lire le bloc
Une série mensuelle vivante, tirée du contrat :
{
"state": "active",
"reason": "la prochaine echeance n'est pas encore echue",
"last_observed_on": "2026-09-05",
"expected_next_window": {
"from": "2026-10-03",
"to": "2026-10-07",
"tolerance_days": 5,
"anchor": "fixed_day_of_month",
"widened_to_family_floor": true
},
"consecutive_misses": 0,
"observation_coverage": "complete",
"cadence_strength": 1,
"calendar_anchor": {
"type": "fixed_day_of_month",
"nominal_day": 5,
"weekday": null,
"day_range": [4, 6],
"weekend_shifts": 0,
"strength": 0.67,
"calibration": "non_calibree"
},
"policy_version": "cyc_v2",
"calibration": "non_calibree"
}JSONLa même série après une scission, lue par son ancien series_id :
{
"state": "unknown",
"reason": "identite corrigee : le cycle de vie appartient desormais aux successeurs",
"identity_status": "superseded",
"last_observed_on": "2026-09-05",
"policy_version": "cyc_v2",
"calibration": "non_calibree"
}JSONLe schéma complet :
Schéma SeriesLifecycle
Un axe différent de `status`. `status` dit à quel point nous croyons que c'est une série ; `lifecycle` dit ce qui lui arrive maintenant. Une série très bien établie peut avoir cessé de prélever, et une série fraîche peut être parfaitement vivante. Et aucun état ne se conclut d'une absence sans couverture : si la source n'était pas observée de façon complète sur la fenêtre attendue, l'état est `unknown`, pas `late`. « Je n'ai rien vu » ne devient jamais « il n'y a rien eu » — c'est la différence entre un client qui a cessé d'envoyer et un abonnement qui a cessé de prélever.
- statestringrequis
active: l'échéance attendue est arrivée, ou n'est pas encore échue.late: une échéance manque ET le compte était observé.inactive: plusieurs échéances consécutives manquent sous observation complète — la série ne prélève plus, sans que nous sachions pourquoi.ended: réservé à une preuve terminale explicite. Aucune accumulation d'absences ne produit cet état : un prélèvement qui s'arrête peut être une résiliation, une suspension, une carte expirée ou un échec de prélèvement, et nous ne devinons pas laquelle.unknown: nous ne pouvons pas conclure, etreasondit pourquoi.activelateinactiveendedunknown - reasonstring
- last_observed_onstring · date· nullable
- expected_next_windowobject· nullable
Une fenêtre, pas une date. Un salaire tombe vers les derniers jours ouvrés, un prélèvement SEPA vers le 5. Rendre un instant exact serait une précision que nous n'avons pas, et vous la croiriez.
- fromstring · date
- tostring · date
- tolerance_daysinteger
La demi-largeur de la fenêtre, en jours. Plus grand = moins précis. Vient du type d'ancrage, relevé au plancher de la famille de série.
- anchorstring
Le type d'ancrage qui a donné la fenêtre.
- widened_to_family_floorboolean
truesi la tolérance a été relevée au plancher de la famille (5 jours pour un abonnement, 6 pour un salaire, 7 sinon). - consecutive_missesinteger· nullable
Le compteur n'avance que sous couverture complète. Absent quand rien n'est attendu — ce qui n'est pas la même ignorance que « nous ne regardions pas ».
- observation_coveragestring
not_required: aucune échéance close, rien n'avait à être observé.complete: chaque source de la série était couverte sur la fenêtre attendue — un manque est un manque.incomplete: au moins une source ne l'était pas — l'absence ne prouve rien,stateestunknown.unknown: les sources sont inconnues ou trop nombreuses pour être vérifiées.not_requiredcompleteincompleteunknown - cadence_strengthnumber· nullable
Le même nombre que
cadence_detail.strength, rendu ici pour que le cycle de vie soit lisible seul. Ce n'est pas un second score. - calendar_anchorany
Le même objet que
calendar_pattern(classeanchor_stability), rendu ici pour la même raison ;{ type: unknown }quand aucun ancrage n'a été inféré. Absent quand aucune échéance n'est attendue. duplicate_of :series[].calendar_pattern— ce n'est pas un second score. - evidencearray<object>
Présent pour
ended: la preuve terminale qui l'a produit. - codestring
- identity_statusstring· nullable
Présent uniquement quand l'identité de la série a été corrigée, pour dire pourquoi
statevautunknown: ce n'est pas une ignorance économique, c'est que le verdict appartient désormais aux successeurs. L'état d'identité complet se lit dansidentity, qui est un axe à part entière ; ce champ n'est qu'un renvoi, et il ne prend jamais la valeurcurrent.supersedednull - policy_versionstringrequis
- calibrationstringrequis
Toujours
non_calibreedans cette version, et c'est une contrainte, pas une convention : la valeur est un vocabulaire en base, les colonnes qui la portent y font référence, et une garde refuse tout littéral hors vocabulaire chez chaque producteur. Le jour où une calibration réelle existe, elle entrera par une migration déclarée, avec le corpus qui la défend.non_calibree
Réagir à chaque état
Un traitement aiguille sur state sans confondre unknown et ended :
const response = await fetch("https://api.noryo.app/transactions/v1/enrichments", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.NORYO_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
descriptor: "PRLV SEPA NETFLIX INTERNATIONAL",
direction: "outgoing",
occurred_at: "2026-09-05T03:10:00+02:00",
amount: { value: "13.49", currency: "EUR" },
account_hint: { iban: "FR7630006000011234567890189" },
}),
});
const enrichment = await response.json();
for (const series of enrichment.series ?? []) {
const lifecycle = series.lifecycle ?? {};
switch (lifecycle.state) {
case "active":
break; // rien à faire
case "late":
case "inactive":
break; // le compte était observé : l'absence est un fait
case "ended":
break; // une preuve terminale existe : lisez lifecycle.evidence
default:
if (lifecycle.identity_status === "superseded") {
// suivez series.identity.superseded_by
} else if (lifecycle.observation_coverage === "incomplete") {
// déclarez une couverture sur le compte
} else {
// lisez lifecycle.reason
}
}
}TypeScriptTrois règles suffisent. Ne traitez jamais unknown comme une fin : reason, observation_coverage et identity_status disent lequel des trois cas s'applique. Ne dérivez pas votre propre ended en comptant les absences : l'API ne le fait pas, pour les raisons données plus haut. Enfin, comparez cadence_strength seulement entre séries de même cadence, et conservez policy_version à côté de tout état que vous stockez : calibration vaut toujours non_calibree.
Publié le 2026-09-17 11:19 UTC (version 1).