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.
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.
peerpour 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.
calibrationvautnon_calibreetant 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 pourreversal_of, 2 pourtransfer_pair. Un0.5de paire de virement (1/2) et un0.5de contre-passation (2/4) ne sont pas la même force. Lisezcorroborationsà côté.presentne compte que les signaux de l'ensemble propre au type : un marqueur de remboursement sur une paire de virement reste visible dansevidencemais ne corrobore pas. - 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 - 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.
type | Définition du contrat | symmetric | Rôles possibles |
|---|---|---|---|
refund_of | L'achat a eu lieu, puis l'argent est revenu : les deux écritures subsistent. | false | source (le remboursement), target (l'achat) |
reversal_of | L'écriture d'origine est effacée : il ne s'est rien passé. | false | source, target (relation orientée ; le contrat n'en montre pas d'exemple) |
transfer_pair | Deux jambes d'un seul mouvement. | true | peer 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"ShellUn 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_hintetcounterparty_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.
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 :
valuevautpresent / possible, arrondi à trois décimales ;corroborations.presentetcorroborations.possiblerendent le numérateur et le dénominateur ;calibrationvautnon_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"
}JSONLisez 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 dansrelationships. Une version antérieure y rangeait à tort les issuesresolvedetalready_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,
typenomme le type de relation,outcomevautambiguousetcandidates_at_leastdonne un plancher, jamais le compte exact, qui ferait un oracle sur les transactions des autres ; - pour la série,
typevautseries,occurrencescompte les occurrences vues etoutcomeporte 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 :
relationships | unresolved (relations) | Lecture |
|---|---|---|
| Vide | Vide | Aucune relation établie, aucune ambiguïté rencontrée. |
| Vide | Une entrée ambiguous | Plusieurs candidats également plausibles : Noryo s'abstient. |
| Une relation | Vide | Lien é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).