# 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](https://developer.noryo.app/fr-FR/transaction-intelligence/recurring-series), [Cycle de vie d'une série](https://developer.noryo.app/fr-FR/transaction-intelligence/series-lifecycle)) ; 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.

| Champ | Type | Requis | Description |
| --- | --- | --- | --- |
| `status` | string | oui | `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. Valeurs : `current`, `superseded`. |
| `superseded_by` | object \| null | — | 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. |
| `superseded_by.reason` | string | — | `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. Valeurs : `split`, `merge`. |
| `superseded_by.at` | string · date-time | — | — |
| `superseded_by.evidence` | array<string> | — | Les codes de preuve qui ont autorisé la correction. Jamais un score interne, jamais un identifiant de ligne. |
| `superseded_by.successor_series_ids` | array<string> | — | Un seul après une fusion, plusieurs après une scission. Jamais vide quand `status` vaut `superseded`. |
| `supersedes` | array<string> | oui | Le chemin de retour, et il compte autant que l'autre. Les identités que celle-ci remplace. Sans lui, l'ancien `series_id` que 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_by` vaut `null` sur une identité courante. Sinon il porte `reason` (`split` ou `merge`), `at`, `evidence` (les codes de preuve qui ont autorisé la correction, jamais un score interne) et `successor_series_ids`, jamais vide quand `status` vaut `superseded`.
- `supersedes` est 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`.

> **Note** 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.

| Champ | Type | Requis | Description |
| --- | --- | --- | --- |
| `original_series_id` | string \| null | — | La série à laquelle cette occurrence a été rattachée la première fois. Égale à `current_series_id` tant qu'aucune correction n'a eu lieu. Motif : `^ser_[0-9a-f-]{36}$`. |
| `current_series_id` | string \| null | oui | Motif : `^ser_[0-9a-f-]{36}$`. |
| `corrections` | array<object> | oui | Les déplacements successifs, du plus ancien au plus récent. Vide pour une occurrence qui n'a jamais bougé. |
| `corrections[].reason` | string | — | Valeurs : `split`, `merge`. |
| `corrections[].from_series_id` | string \| null | — | Motif : `^ser_[0-9a-f-]{36}$`. |
| `corrections[].to_series_id` | string \| null | — | Motif : `^ser_[0-9a-f-]{36}$`. |
| `corrections[].at` | string · 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 :

```json
{
  "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"
      }
    ]
  }
}
```

L'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](https://developer.noryo.app/fr-FR/transaction-intelligence/read-an-enrichment).

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

```ts
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 });
```

Deux 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 = unknown` d'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](https://developer.noryo.app/fr-FR/transaction-intelligence/recurring-series) : `series_id`, `type`, `status`, cadence.
- [Cycle de vie d'une série](https://developer.noryo.app/fr-FR/transaction-intelligence/series-lifecycle) : `lifecycle.state` et couverture d'observation.
- [Référence](https://developer.noryo.app/fr-FR/transaction-intelligence/reference) : les schémas `Series`, `SeriesIdentity` et `SeriesMembership` générés depuis le contrat.

---

Page canonique : https://developer.noryo.app/fr-FR/transaction-intelligence/split-merge-supersession
