Aller au contenu
Sandbox

Relations

Comment Noryo relie deux transactions d'un même workspace par une relation refund_of, reversal_of ou transfer_pair, et comment lire son rôle, ses corroborations et ses abstentions.

Voir en Markdown

Ce qu'est une relation

Une relation (Relationship) est un lien économique entre deux transactions du même workspace. Elle est rendue dans le tableau relationships de la création (Enrichment, voir Enrichir une transaction) et de la relecture (StoredEnrichment, voir Relire un enrichissement).

Une relation est privée au workspace : elle ne le quitte jamais et n'alimente jamais le graphe d'entités, car un lien entre deux transactions privées n'est pas une vérité globale. Ce qui vit dans le graphe : Contreparties et entités.

Schéma Relationship

Un lien économique entre deux transactions du même workspace. Il ne quitte jamais ce workspace et n'alimente jamais le graphe d'entités : une relation entre deux transactions privées n'est pas une vérité globale.

  • typestringrequis

    refund_of : l'achat a eu lieu, puis l'argent est revenu — les deux écritures subsistent. reversal_of : l'écriture d'origine est effacée, il ne s'est rien passé. transfer_pair : deux jambes d'un seul mouvement.

    refund_ofreversal_oftransfer_pair
  • rolestringrequis

    Le rôle de cette transaction. peer pour une relation symétrique, où ni source ni cible ne veut rien dire.

    sourcetargetpeer
  • symmetricbooleanrequis
  • counterpartstringrequis

    L'autre transaction, par son identifiant d'enrichissement.

    ^enr_[0-9a-f-]{36}$
  • match_strengthobjectrequis

    Ce n'est pas une probabilité. C'est la part des signaux corroborants disponibles pour ce type qui sont effectivement présents. calibration vaut non_calibree tant qu'aucune vérité terrain n'a certifié une lecture chiffrée de ce nombre.

  • valuenumber · 0.001–1requis

    Le dénominateur dépend du type : 5 pour refund_of, 4 pour reversal_of, 2 pour transfer_pair. Un 0.5 de paire de virement (1/2) et un 0.5 de contre-passation (2/4) ne sont pas la même force. Lisez corroborations à côté. present ne compte que les signaux de l'ensemble propre au type : un marqueur de remboursement sur une paire de virement reste visible dans evidence mais ne corrobore pas.

  • 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
  • corroborationsobjectrequis
  • presentinteger ≥ 0requis
  • possibleinteger ≥ 1requis
  • evidencearray<object>requis

    Les signaux qui fondent la relation, y compris les préconditions. Une relation qui ne peut pas dire pourquoi elle existe n'est pas créée.

  • codestringrequis
  • valuestring
  • value_secondsinteger

    Pour within_window : l'écart mesuré, en secondes. Une durée, pas une confiance.

  • window_secondsinteger· nullable

    La fenêtre du type de relation, en secondes. Une constante de politique, pas une mesure.

  • detector_versionstringrequis
  • statusstringrequis
    activesupersedeeinvalidee
  • created_atstring · date-time

Les trois types

Le contrat définit trois valeurs pour type, aucune autre.

typeDéfinition du contratsymmetricRôles possibles
refund_ofL'achat a eu lieu, puis l'argent est revenu : les deux écritures subsistent.falsesource (le remboursement), target (l'achat)
reversal_ofL'écriture d'origine est effacée : il ne s'est rien passé.falsesource, target (relation orientée ; le contrat n'en montre pas d'exemple)
transfer_pairDeux jambes d'un seul mouvement.truepeer des deux côtés

Une relation n'est pas un type de transaction : resolution.transaction_type dit ce qu'est l'écriture seule, la relation dit à quelle autre elle est liée (voir Types de transaction).

Le rôle de cette transaction

role est le rôle de la transaction que vous lisez, jamais celui de l'autre : un enrichissement qui porte type: "refund_of" et role: "source" est le remboursement, et counterpart désigne l'achat. Le même lien, lu depuis l'achat, porte role: "target".

Pour transfer_pair, ni source ni cible n'a de sens : les deux jambes portent role: "peer" et symmetric: true.

counterpart est l'identifiant d'enrichissement de l'autre transaction (enr_…), relisible directement :

curl https://api.noryo.app/transactions/v1/enrichments/enr_3f2504e0-4f89-41d3-9a0c-0305e82c3301 \
  -H "Authorization: Bearer $NORYO_API_KEY"
Shell
Note

Un enrichissement est relisible 30 jours ; au-delà, relire counterpart rend 404, comme tout identifiant expiré. Voir Limites du sandbox.

Ce qui rend une relation possible

Le détecteur ne lit que ce que vous envoyez : signals.used liste, dans chaque réponse de création (Enrichment), les signaux reçus transmis au moteur (au résolveur d'identité, ou à la base pour les séries, les relations et la couverture d'observation), sans distinguer ceux qui ont servi aux relations. Dans l'exemple de remboursement du contrat, il contient descriptor, direction, occurred_at et amount : envoyez le montant et la date dès que vous les avez.

Deux champs comptent en particulier :

  • account_hint et counterparty_hint : sans les deux, une paire de virement ne peut pas être identifiée, car le seul signal qui la désigne est « mon compte est ta contrepartie, et ta contrepartie est mon compte » ;
  • external_transaction_id : votre propre identifiant de transaction, qui empêche deux envois d'une même transaction de se rapprocher l'un de l'autre.
Attention

Les IBAN de account_hint et de counterparty_hint sont réduits dès l'entrée à leur pays et leur code banque : le numéro de compte n'entre pas dans le système, donc ne peut pas en sortir. Les envoyer rend transfer_pair possible sans exposer un compte.

Corroborations et force d'appariement

evidence liste les signaux qui fondent la relation, préconditions comprises. Une relation qui ne peut pas dire pourquoi elle existe n'est pas créée. Chaque élément porte un code ; within_window porte aussi value_seconds, l'écart mesuré en secondes : une durée, pas une confiance.

match_strength n'est pas une probabilité : c'est la part des signaux corroborants que ce type sait produire qui sont effectivement présents :

  • value vaut present / possible, arrondi à trois décimales ;
  • corroborations.present et corroborations.possible rendent le numérateur et le dénominateur ;
  • calibration vaut non_calibree : aucune calibration statistique n'est disponible.

Le dénominateur dépend du type : cinq signaux possibles pour refund_of, quatre pour reversal_of, deux pour transfer_pair. Un 0.5 sur une paire de virement (un signal sur deux) et un 0.5 sur une annulation (deux sur quatre) ne sont pas la même force. present ne compte que les signaux de l'ensemble propre au type : un marqueur de remboursement sur une paire de virement reste visible dans evidence mais ne corrobore pas.

Cette classe de score s'appelle evidence_ratio. Elle se compare entre deux relations du même type et de la même detector_version, jamais avec series[].match_strength, qui porte le même nom mais un autre dénominateur. Les classes sont décrites dans Sémantique des scores.

window_seconds est la fenêtre du type, en secondes : une constante de politique, pas une mesure, à lire dans la réponse plutôt qu'à coder en dur.

Exemple

Un remboursement rattaché à son achat, tel que le contrat le montre :

{
  "type": "refund_of",
  "role": "source",
  "symmetric": false,
  "counterpart": "enr_3f2504e0-4f89-41d3-9a0c-0305e82c3301",
  "match_strength": {
    "value": 0.8,
    "calibration": "non_calibree",
    "corroborations": { "present": 4, "possible": 5 }
  },
  "evidence": [
    { "code": "same_counterparty_entity" },
    { "code": "refund_marker" },
    { "code": "within_window", "value_seconds": 259200 },
    { "code": "resolved_type_refund" }
  ],
  "window_seconds": 2592000,
  "detector_version": "rel_v1",
  "status": "active",
  "created_at": "2026-09-12T09:00:00Z"
}
JSON

Lisez corroborations : present vaut 4 sur possible 5, donc value vaut 0.8, rien de plus. La liste evidence n'est pas ce compte : elle porte aussi les préconditions.

Pourquoi une relation apparaît à la relecture

Une relation relie deux transactions, et la seconde n'existe pas encore quand la première est enrichie. L'achat du lundi ne peut pas connaître le remboursement du vendredi : sa réponse de création porte relationships: []. Cette réponse est immuable : un rejeu avec la même Idempotency-Key la rend telle quelle, sans le lien.

La relecture, elle, porte le lien : GET /transactions/v1/enrichments/{enrichment_id} rend un StoredEnrichment avec son tableau relationships. Relu le vendredi, l'achat du lundi porte refund_of avec role: "target" ; le remboursement l'a porté dès sa création avec role: "source". Conservez enrichment_id et relisez pour l'état courant.

Quand rien n'est conclu

L'abstention est un résultat, pas un silence : « aucune relation » et « deux candidats également plausibles » sont deux choses différentes. C'est le rôle de relationships_policy (RelationshipsPolicy), rendu à la création (Enrichment) et absent de la relecture (StoredEnrichment).

Schéma RelationshipsPolicy

L'abstention est un résultat, pas un silence. « aucune relation » et « deux candidats également plausibles » sont deux choses différentes ; un client qui ne peut pas les distinguer conclura la première.

  • detector_versionstringrequis
  • calibrationstringrequis
    non_calibree
  • unresolvedarray<object>requis

    Chaque détecteur qui n'a pas conclu dit pourquoi. Pour une relation : candidates_at_least (jamais le compte exact — il ferait un oracle sur les transactions des autres). Pour la série (type = series) : occurrences, et une issue de série. Une relation conclue n'y figure jamais : elle est dans relationships. Une version antérieure y rangeait à tort les issues resolved et already_known.

  • typestringrequis
  • outcomestringrequis
    ambiguoussingle_occurrenceno_recurrence_intervalno_series_keyexcluded_by_relationambiguous_membership
  • candidates_at_leastinteger ≥ 1
  • occurrencesinteger ≥ 0

unresolved liste chaque détecteur qui n'a pas conclu, avec sa raison :

  • pour une relation, type nomme le type de relation, outcome vaut ambiguous et candidates_at_least donne un plancher, jamais le compte exact, qui ferait un oracle sur les transactions des autres ;
  • pour la série, type vaut series, occurrences compte les occurrences vues et outcome porte une issue de série ; l'une d'elles, excluded_by_relation, nomme une relation comme motif (voir Séries récurrentes).

Une relation conclue ne figure jamais dans unresolved : elle est dans relationships. Lisez les deux ensemble :

relationshipsunresolved (relations)Lecture
VideVideAucune relation établie, aucune ambiguïté rencontrée.
VideUne entrée ambiguousPlusieurs candidats également plausibles : Noryo s'abstient.
Une relationVideLien établi ; ses signaux sont dans evidence.

Statut et version

Le contrat déclare trois valeurs pour status : active, supersedee et invalidee ; les exemples du contrat sont active. detector_version nomme le détecteur qui a produit la relation : si vous conservez une valeur de match_strength, conservez detector_version à côté, car la même valeur, sous une autre version, peut ne plus mesurer la même chose. created_at horodate la relation.

Publié le 2026-09-17 11:19 UTC (version 1).

Cette page vous a-t-elle aidé ?