Aller au contenu
Sandbox

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é.

Voir en Markdown

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.

AxeChampQuestionValeurs
ÉtablissementstatusÀ quel point Noryo croit-il que c'est une série ?emerging, established, uncertain, broken
Cycle de vielifecycle.stateQue lui arrive-t-il maintenant ?active, late, inactive, ended, unknown
Identitéidentity.statusSon 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.

stateSens
activeL'échéance attendue est arrivée, ou n'est pas encore échue.
lateUne échéance manque, et le compte était observé.
inactivePlusieurs échéances consécutives manquent, sous observation complète. La série ne prélève plus, sans que Noryo sache pourquoi.
endedRéservé à une preuve terminale explicite. Aucune accumulation d'absences ne produit cet état.
unknownNoryo 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.

Note

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_coverageSens
not_requiredAucune échéance close : rien n'avait à être observé.
completeChaque source de la série était couverte sur la fenêtre attendue. Un manque est un manque.
incompleteAu moins une source ne l'était pas. L'absence ne prouve rien, et state est unknown.
unknownLes 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 ».

Attention

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"
}
JSON

La 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"
}
JSON

Le 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, et reason dit 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

    true si 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, state est unknown. 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 (classe anchor_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 state vaut unknown : ce n'est pas une ignorance économique, c'est que le verdict appartient désormais aux successeurs. L'état d'identité complet se lit dans identity, qui est un axe à part entière ; ce champ n'est qu'un renvoi, et il ne prend jamais la valeur current.

    supersedednull
  • policy_versionstringrequis
  • calibrationstringrequis

    Toujours non_calibree dans 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
      }
  }
}
TypeScript

Trois 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).

Cette page vous a-t-elle aidé ?