← Toutes les capacités

05 / EVIDENCE

Réponses porteuses de preuves

Une réponse ne voyage jamais seule: elle emporte le sous-graphe qui la soutient, les contre-preuves, sa provenance, sa confiance et ce qui reste inconnu.

RUST CORE
01

Pourquoi cette capacité

Séparer réponse, certitude et couverture

L’enveloppe de réponse référence des nœuds, relations, claims, preuves, sources et trajectoires de récupération. La confiance de l’analyse reste indépendante de la complétude de la recherche.

02

Scénario

FIELD NOTE / SCENARIO

Répondre sans sur-vendre une attribution

Un agent estime qu’une infrastructure appartient probablement à Atlas. Deux sources soutiennent l’hypothèse, une source commerciale la conteste et le journal primaire manque encore.

  1. 01

    Construire les sous-graphes de soutien et de contestation

  2. 02

    Calculer confiance et complétude séparément

  3. 03

    Énumérer les inconnues qui empêchent une conclusion forte

Résultat attendu

Le lecteur peut vérifier la conclusion, voir ses limites et décider s’il faut chercher davantage.

03

Jeu de données synthétique

Quatre preuves pour, trois contre, trois trous qui empêchent de conclure

Une question d’attribution dont la réponse honnête est inconfortable. Le chargeur décisif a fuité publiquement il y a sept mois et sert depuis à trois opérateurs sans lien. Le meilleur appui restant est un flux commercial qui ne publie pas sa base. Et le journal d’authentification primaire, qui trancherait, n’a pas été obtenu. Les inconnues sont des enregistrements du graphe : un trou qui n’existe que dans la tête d’un analyste n’est pas auditable.

12Mémoires
11Relations
4Preuves à l’appui
3Contre-preuves
3Inconnues
evidence--feed-assertionAppui le plus faible
Télécharger le jeu de donnéesdataset.json · JSON · 16 kB
04

Pourquoi Corrobore

Une réponse réduite à « confiance 0,62 » a détruit tout ce qui permettait de la contester.

Ce qu’il faut construire soi-même

  • Un score unique confond deux choses : la force des preuves dont on dispose, et la part des preuves atteignables qu’on a réellement regardée. Le cas dangereux, forte confiance sur récupération tronquée, devient invisible.
  • Un RAG classique ne renvoie que ce qui ressemble à la question. Ce qui la contredit ne remonte pas, donc l’absence de contre-preuve n’est jamais distinguable d’une recherche qui n’en a pas cherché.
  • Les lacunes vivent dans un document Word ou dans la tête de l’analyste. L’agent suivant les redécouvre, ou pire, ne les redécouvre pas.

Ce que le contrat garantit

  • completeness est renvoyé par le moteur, pas estimé par le client : complete, truncated et les sorties bornées disent ce que la traversée n’a pas atteint.
  • La confiance analytique et la complétude de récupération restent deux nombres distincts, ce qui rend le cas dangereux détectable par une politique de publication en une dizaine de lignes.
  • Le recall_id ramène à la traversée : chemins empruntés, identifiants de sources de preuve, attribution injectée par le serveur. Un lecteur vérifie sans interroger l’auteur.
05

Notebook exécutable

Douze cellules qui refusent de publier une réponse trop sûre d’elle

Le notebook assemble l’enveloppe depuis une récupération réelle, vérifie qu’elle porte bien contre-preuves et lacunes, puis rejoue la même question sous un budget insuffisant : la confiance reste à 0,62 alors que l’agent est passé de douze enregistrements à un seul, et le moteur nomme cost_budget_exhausted. Une politique de publication lisant les deux nombres refuse la réponse affamée que la confiance seule aurait laissée passer.

proof-carrying/notebook.ipynbEXTRAIT / 13 CELLULES
MARKDOWN [0]

Reproduisez le scénario sur une copie de travail et conservez les identifiants de preuves, de session et de snapshot dans le résultat.

CODE [1] / PYTHON
import json, os, pathlib, time
import requests
import urllib3

# The quick-start server uses a self-signed certificate; disable verification
# for the local playbook only, never against a real deployment.
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

BASE_URL = os.environ.get('CORROBORE_URL', 'https://127.0.0.1:8080')
TOKEN = os.environ.get('CORROBORE_HTTP_AUTH_TOKEN', 'change-me')

http = requests.Session()
http.verify = False
http.headers.update({'Authorization': f'Bearer {TOKEN}', 'Content-Type': 'application/json'})


class MemoryError_(RuntimeError):
    """Carries the stable v1 error taxonomy instead of a bare HTTP status."""

    def __init__(self, code, message, status):
        super().__init__(f'{code}: {message}')
        self.code, self.message, self.status = code, message, status


def memory_op(operation, payload, idempotency_key=None, expect_error=False):
    """POST /v1/memory/operations and unwrap the typed result."""
    body = {'contract_version': 'v1', 'operation': operation, 'input': payload}
    if idempotency_key is not None:
        body['idempotency_key'] = idempotency_key
    for attempt in range(12):
        response = http.post(f'{BASE_URL}/v1/memory/operations', data=json.dumps(body))
        # Protected routes share a global token bucket (50 rps sustained, 200 burst by
        # default) that a bulk load will hit. Two details matter here: the 429 body is
        # plain text, not the JSON error envelope, and `Retry-After` can be `0` — so
        # honour it as a floor, never as the whole wait.
        if response.status_code == 429:
            hinted = float(response.headers.get('Retry-After', 0) or 0)
            time.sleep(max(hinted, 0.2 * (attempt + 1)))
            continue
        break
    if response.status_code != 200:
        try:
            error = response.json().get('error', {})
        except ValueError:  # 429 and other transport rejections are not JSON
            error = {}
        failure = MemoryError_(error.get('code', 'UNKNOWN'), error.get('message', response.text), response.status_code)
        if expect_error:
            return failure
        raise failure
    if expect_error:
        raise AssertionError(f'{operation} unexpectedly succeeded')
    return response.json()['result']['result']


ready = http.get(f'{BASE_URL}/health/ready').json()
version = http.get(f'{BASE_URL}/version').json()
print('ready  :', json.dumps(ready)[:160])
print('version:', json.dumps(version)[:160])
CODE [2] / PYTHON
LIMITS = {
    'max_items': 200, 'max_depth': 3, 'max_payload_bytes': 1024 * 1024,
    'max_cost': 5000, 'timeout_ms': 10000, 'supernode_threshold': 64,
}


def build_envelope(recall, question):
    records = {i['record']['id']: i['record'] for i in recall['items']}
    buckets = {'supports': [], 'challenges': [], 'limits': []}
    for edge in recall['relationships']:
        if edge['kind'] in buckets and edge['source_id'] in records:
            buckets[edge['kind']].append(records[edge['source_id']])

    statement = next((r for r in records.values() if r['kind'] == 'analysis.statement'), None)
    provenance = sorted({p['source_id'] for r in records.values() for p in r['provenance']})

    return {
        'question': question,
        'statement': statement['content']['value']['text'] if statement else None,
        'confidence': statement['confidence'] if statement else None,
        'supporting_evidence': [
            {'id': r['identity_key'], 'reliability': r['content']['value']['properties'].get('source_reliability')}
            for r in buckets['supports']
        ],
        'counter_evidence': [
            {'id': r['identity_key'], 'reliability': r['content']['value']['properties'].get('source_reliability')}
            for r in buckets['challenges']
        ],
        'unresolved_unknowns': [
            {'id': r['identity_key'], 'impact': r['content']['value']['properties'].get('impact')}
            for r in buckets['limits']
        ],
        'source_provenance': provenance,
        'retrieval_completeness': {
            'complete': recall['completeness']['complete'],
            'truncated': recall['completeness']['truncated'],
            'outcomes': recall['completeness']['outcomes'],
            'usage': recall['usage'],
        },
        'recall_id': recall['recall_id'],
    }


full = memory_op('recall', {
    'objective': 'kestrel',
    'seed_ids': [ids['question--kestrel-attribution']],
    'limits': LIMITS,
})
envelope = build_envelope(full, 'Is the intrusion attributable to Kestrel?')
print(json.dumps(envelope, indent=2))
CODE [3] / PYTHON
# An answer that cannot name what contradicts it is not an answer, it is a claim.
assert envelope['counter_evidence'], 'the envelope must carry counter-evidence'
assert envelope['unresolved_unknowns'], 'the envelope must carry its gaps'

print('supporting :', len(envelope['supporting_evidence']))
print('challenging:', len(envelope['counter_evidence']))
print('unknowns   :', len(envelope['unresolved_unknowns']))
print()

# The strongest-sounding support is the weakest source in the set.
for item in sorted(envelope['supporting_evidence'], key=lambda e: e['reliability']):
    print(f"  supports   {item['id']:<32} {item['reliability']}")
for item in envelope['counter_evidence']:
    print(f"  challenges {item['id']:<32} {item['reliability']}")
CODE [4] / PYTHON
# Seed the statement explicitly so it is guaranteed to be in the page: the
# point is to lose the *argument*, not the conclusion.
starved = memory_op('recall', {
    'objective': 'kestrel',
    'seed_ids': [ids['statement--attribution-plausible']],
    'limits': dict(LIMITS, max_items=3, max_cost=4),
})
partial = build_envelope(starved, 'Is the intrusion attributable to Kestrel?')

print(f"{'':<22}{'full':>10}{'starved':>12}")
print('-' * 44)
print(f"{'confidence':<22}{str(envelope['confidence']):>10}{str(partial['confidence']):>12}")
print(f"{'supporting':<22}{len(envelope['supporting_evidence']):>10}{len(partial['supporting_evidence']):>12}")
print(f"{'counter-evidence':<22}{len(envelope['counter_evidence']):>10}{len(partial['counter_evidence']):>12}")
print(f"{'unknowns':<22}{len(envelope['unresolved_unknowns']):>10}{len(partial['unresolved_unknowns']):>12}")
print(f"{'complete':<22}{str(envelope['retrieval_completeness']['complete']):>10}"
      f"{str(partial['retrieval_completeness']['complete']):>12}")
print(f"{'truncated':<22}{str(envelope['retrieval_completeness']['truncated']):>10}"
      f"{str(partial['retrieval_completeness']['truncated']):>12}")
print(f"{'items examined':<22}{envelope['retrieval_completeness']['usage']['items']:>10}"
      f"{partial['retrieval_completeness']['usage']['items']:>12}")

# Same conclusion, same confidence, a fraction of the argument behind it.
assert partial['confidence'] == envelope['confidence']
assert partial['retrieval_completeness']['truncated'] is True
assert len(partial['counter_evidence']) < len(envelope['counter_evidence'])
Télécharger le notebooknotebook.ipynb · IPYNB · 13 cellules
06

Playbook à télécharger

Trois minutes pour voir une réponse honnête refuser de partir

Le playbook démarre un Corrobore éphémère, exécute le notebook et propose de retirer la contre-preuve décisive du jeu de données pour mesurer à quel point la réponse devient plus forte, et moins honnête.

  1. 01

    Démarrer un Corrobore jetable en stockage éphémère

  2. 02

    Installer requests

  3. 03

    Exécuter le notebook et comparer les deux enveloppes

  4. 04

    Retirer une contre-preuve, relancer, relire la conclusion

Télécharger le playbookplaybook.md · MARKDOWN · requests
07

Ce qui est disponible

Enveloppe ProofCarryingAnswer, provenance de trajectoire, complétude et évaluation calibrée disponibles dans graph-core. Les consommateurs choisissent encore leur transport et leur rendu.