Scission, fusion, supersession
Comment Noryo corrige l'identité d'une série sans jamais effacer un series_id, et comment relier vos anciens identifiants aux nouveaux à la relecture.
Une série (series) regroupe des occurrences économiquement liées dans le temps, dans votre workspace. Son series_id est stable : il se dérive de ce qui ne change pas (workspace, famille, contrepartie, sens, devise), jamais de la dernière occurrence ni du libellé courant. Une occurrence de plus ne crée pas une autre série.
Il arrive pourtant que Noryo comprenne, avec davantage d'occurrences, qu'un nom recouvrait deux flux, ou que deux noms désignaient un seul flux. Cette page décrit ce qui se passe alors, et ce que vous devez lire.
Un troisième axe, distinct des deux autres
Une série porte trois axes que le contrat refuse de mélanger :
| Axe | Champ | Question à laquelle il répond |
|---|---|---|
| Solidité | status (emerging, established, uncertain, broken) | À quel point est-ce une série ? |
| Cycle de vie | lifecycle.state (active, late, inactive, ended, unknown) | Que lui arrive-t-il maintenant ? |
| Identité | identity.status (current, superseded) | Son nom est-il toujours le bon ? |
Une série peut prélever tous les mois, être établie, et avoir changé d'identité parce que Noryo a compris qu'elle était en réalité deux flux, ou deux moitiés d'un seul. Les deux premiers axes ont leur page (Séries récurrentes, Cycle de vie d'une série) ; celle-ci ne traite que du troisième.
Scission (split)
Une contrepartie n'est pas une série : une même contrepartie peut porter plusieurs flux, par exemple deux abonnements chez le même service. Au début, ces flux peuvent être rangés sous un seul nom. Quand les occurrences font apparaître deux flux qui coexistent dans le temps, Noryo scinde : l'ancienne identité est marquée superseded, et chaque flux reçoit la sienne.
Après une scission, il n'y a pas de redirection unique. Aucun des successeurs n'est « la suite » de l'ancien nom : la moitié des occurrences de l'ancien n'est pas dans l'un d'eux. C'est pourquoi superseded_by.successor_series_ids est une liste, et pourquoi élire un successeur canonique serait faux.
Fusion (merge)
Le cas inverse : deux séries que Noryo tenait séparées étaient en réalité un seul flux. Les deux identités sont alors remplacées par une identité neuve : chacune des deux anciennes est marquée superseded, et sa liste successor_series_ids contient un seul identifiant, le nouveau.
Supersession : ce que devient l'ancien nom
Dans les deux cas, un series_id n'est jamais réécrit ni réutilisé en silence. L'ancien nom subsiste, marqué superseded, et il porte ses successeurs. Il reste valide comme référence historique et ne disparaîtra pas, mais les occurrences futures porteront le nom de ses successeurs.
Schéma SeriesIdentity
Un troisième axe, et il ne se mélange ni à `status` NI À `lifecycle`. `status` dit à quel point nous croyons que c'est une série ; `lifecycle` dit ce qui lui arrive économiquement ; `identity` dit si son nom est toujours le bon. Une série peut prélever tous les mois et avoir changé d'identité parce que nous avons compris qu'elle était en réalité deux flux, ou deux moitiés d'un seul. ET UN `series_id` n'est jamais réécrit ni réutilisé en silence. Quand nous corrigeons, l'ancien nom subsiste, marqué `superseded`, et il porte ses successeurs.
- statusstringrequis
current: ce nom est le nom courant de ce flux.superseded: ce nom a été remplacé par une correction d'identité. Il reste valide comme référence historique — il ne disparaîtra pas — mais les occurrences futures porteront le nom de ses successeurs.currentsuperseded - superseded_byobject· nullable
Après une scission il n'y a pas de redirection unique, et c'est pourquoi ce champ rend une liste. Si nous découvrons qu'une série mélangeait deux flux, aucun des deux successeurs n'est « la suite » de l'ancien : la moitié des occurrences de l'ancien n'est pas dans l'un d'eux. Élire un canonique serait confortable et faux.
- reasonstring
split: cette identité en recouvrait plusieurs, et chacune a désormais la sienne.merge: cette identité et une autre étaient le même flux ; toutes deux sont remplacées par une identité neuve.splitmerge - atstring · date-time
- evidencearray<string>
Les codes de preuve qui ont autorisé la correction. Jamais un score interne, jamais un identifiant de ligne.
- successor_series_idsarray<string>
Un seul après une fusion, plusieurs après une scission. Jamais vide quand
statusvautsuperseded. - supersedesarray<string>requis
Le chemin de retour, et il compte autant que l'autre. Les identités que celle-ci remplace. Sans lui, l'ancien
series_idque vous aviez stocké ne correspondrait plus à rien de ce que vous recevez, et vous n'auriez aucun moyen de faire le lien. Vide pour une série qui n'est née d'aucune correction.
superseded_byvautnullsur une identité courante. Sinon il portereason(splitoumerge),at,evidence(les codes de preuve qui ont autorisé la correction, jamais un score interne) etsuccessor_series_ids, jamais vide quandstatusvautsuperseded.supersedesest le chemin de retour : les identités que celle-ci remplace. Vide pour une série qui n'est née d'aucune correction.
Sur une série remplacée, lifecycle.state vaut unknown et lifecycle.identity_status vaut superseded : ce n'est pas une ignorance économique, le verdict appartient désormais aux successeurs. Ce champ n'est qu'un renvoi vers identity et ne prend jamais la valeur current.
L'exemple serie_scindee du contrat montre trois codes de preuve pour une scission : two_supported_partitions, time_overlap, basis_amount. Aucun seuil ne les accompagne, et vous n'avez pas à les reproduire.
D'où vient l'occurrence, où est-elle maintenant
Quand une identité est corrigée, l'occurrence change de série. Sans le bloc membership, vous n'auriez aucun moyen de relier le series_id que vous aviez stocké à celui que vous recevez. Vous n'avez jamais à reconstruire le graphe vous-même.
Schéma SeriesMembership
D'où vient cette occurrence, et où est-elle maintenant. Quand une identité est corrigée, l'occurrence change de série : sans ce bloc, vous n'auriez aucun moyen de relier le `series_id` que vous aviez stocké à celui que vous recevez. Vous n'avez jamais à reconstruire le graphe vous-même.
- original_series_idstring· nullable
La série à laquelle cette occurrence a été rattachée la première fois. Égale à
current_series_idtant qu'aucune correction n'a eu lieu.^ser_[0-9a-f-]{36}$ - current_series_idstring· nullablerequis
^ser_[0-9a-f-]{36}$ - correctionsarray<object>requis
Les déplacements successifs, du plus ancien au plus récent. Vide pour une occurrence qui n'a jamais bougé.
- reasonstring
splitmerge - from_series_idstring· nullable
^ser_[0-9a-f-]{36}$ - to_series_idstring· nullable
^ser_[0-9a-f-]{36}$ - atstring · date-time
original_series_id est la série à laquelle l'occurrence a été rattachée la première fois ; il reste égal à current_series_id tant qu'aucune correction n'a eu lieu. corrections liste les déplacements du plus ancien au plus récent, chacun avec reason, from_series_id, to_series_id et at.
Extrait de series[], occurrence dont la série a été scindée :
{
"series_id": "ser_6ba7b811-9dad-11d1-80b4-00c04fd430c8",
"identity": {
"status": "superseded",
"superseded_by": {
"reason": "split",
"at": "2026-09-10T22:14:00Z",
"evidence": ["two_supported_partitions", "time_overlap", "basis_amount"],
"successor_series_ids": [
"ser_6ba7b812-9dad-11d1-80b4-00c04fd430c8",
"ser_6ba7b810-9dad-11d1-80b4-00c04fd430c8"
]
},
"supersedes": []
},
"membership": {
"original_series_id": "ser_6ba7b811-9dad-11d1-80b4-00c04fd430c8",
"current_series_id": "ser_6ba7b810-9dad-11d1-80b4-00c04fd430c8",
"corrections": [
{
"reason": "split",
"from_series_id": "ser_6ba7b811-9dad-11d1-80b4-00c04fd430c8",
"to_series_id": "ser_6ba7b810-9dad-11d1-80b4-00c04fd430c8",
"at": "2026-09-10T22:14:00Z"
}
]
}
}JSONL'ancien nom annonce deux successeurs ; le bloc membership dit dans lequel des deux cette occurrence précise est passée.
Relier vos anciens series_id aux nouveaux
Les séries rendues par GET /transactions/v1/enrichments/{enrichment_id} reflètent ce que Noryo sait au moment de la lecture, pas de l'écriture : c'est déjà ainsi que status peut passer à broken par le seul passage du temps. Une correction se découvre à la relecture, comme sur la réponse d'un nouvel envoi : l'exemple serie_scindee du contrat est une réponse 201. La fenêtre de relecture est décrite dans Relire un enrichissement.
| Vous lisez | Vous en concluez |
|---|---|
identity.status = current, supersedes vide | Rien n'a changé. |
identity.status = superseded | Le nom stocké est historique. Conservez-le comme alias et enregistrez successor_series_ids avec la reason. |
membership.current_series_id différent de original_series_id | Cette occurrence est rattachée à current_series_id. Après une scission, ce champ seul dit dans quel successeur elle est allée. |
Un series_id inconnu de vous, supersedes non vide | Une identité neuve qui remplace des identifiants que vous connaissez peut-être. Reliez-les. |
const enrichmentId = process.argv[2]; // enr_…
const res = await fetch(
`https://api.noryo.app/transactions/v1/enrichments/${enrichmentId}`,
{ headers: { Authorization: `Bearer ${process.env.NORYO_API_KEY}` } },
);
if (!res.ok) throw new Error(`relecture refusée : ${res.status}`);
const stored = await res.json();
const lineage = new Map<string, string[]>(); // ancien series_id -> successeurs
const moved: Array<{ from: string; to: string }> = [];
for (const s of stored.series ?? []) {
// 1. Le nom stocké est-il remplacé ? (un ancien -> un ou plusieurs nouveaux)
if (s.identity?.status === "superseded") {
lineage.set(s.series_id, s.identity.superseded_by.successor_series_ids);
}
// 2. Le chemin de retour : un nom neuf remplace-t-il des anciens ?
for (const replaced of s.identity?.supersedes ?? []) {
lineage.set(replaced, [...(lineage.get(replaced) ?? []), s.series_id]);
}
// 3. Où cette occurrence est-elle maintenant ? (par occurrence, jamais en bloc)
const m = s.membership;
if (m?.original_series_id && m.current_series_id !== m.original_series_id) {
moved.push({ from: m.original_series_id, to: m.current_series_id });
}
}
console.log({ lineage: Object.fromEntries(lineage), moved });TypeScriptDeux erreurs à éviter :
- Remplacer en bloc l'ancien identifiant par le premier successeur de la liste. Après une scission, chaque occurrence a son propre
current_series_id. - Lire
lifecycle.state = unknownd'une série remplacée comme un flux dont on ignore le sort. Le cycle de vie se lit sur les successeurs.
Dans tous les cas, original_series_id ne bouge pas : seul current_series_id avance, et corrections en garde le chemin.
Voir aussi
- Séries récurrentes :
series_id,type,status, cadence. - Cycle de vie d'une série :
lifecycle.stateet couverture d'observation. - Référence : les schémas
Series,SeriesIdentityetSeriesMembershipgénérés depuis le contrat.
Publié le 2026-09-17 11:19 UTC (version 1).