← Toutes les capacités

01 / TEMPORAL

Temps valide et temps de transaction

Corrobore ne confond pas le moment où un fait est vrai avec le moment où le système l’apprend. Les snapshots rendent chaque décision rejouable.

RUST CORE
01

Pourquoi cette capacité

Conserver l’histoire sans figer le présent

Chaque état porte son intervalle de validité, son temps de transaction et, si disponibles, les temps d’observation et de publication. Une correction ferme un intervalle et ajoute un nouvel état au lieu de réécrire le passé.

02

Scénario

FIELD NOTE / SCENARIO

Reconstituer une alerte tardive

Un fournisseur est compromis lundi, mais la preuve n’arrive que mercredi. L’analyste doit savoir ce qui était réellement compromis lundi et ce que l’équipe pouvait raisonnablement savoir mardi.

  1. 01

    Enregistrer les temps du monde et du système séparément

  2. 02

    Interroger l’état as-of de chaque dimension

  3. 03

    Exporter le snapshot exact utilisé pour la décision

Résultat attendu

L’audit explique la décision initiale sans appliquer rétroactivement une information arrivée plus tard.

03

Jeu de données synthétique

Une chronologie où la preuve décisive arrive dix jours trop tard

Quatorze mémoires et treize relations décrivent la compromission d’un compte de service fournisseur : la poussée anormale, l’absence de badge, le déni du fournisseur, et l’image disque qui tranche, acquise dix jours après les faits. La validité est stockée en décalages de jours, pas en dates absolues : le notebook les résout au moment où vous l’exécutez, donc « déjà clos » et « pas encore en vigueur » gardent leur sens quelle que soit la date.

14Mémoires
13Relations
4Intervalles clos
9Intervalles ouverts
1Intervalles futurs
obs--forensic-imagePreuve tardive
Télécharger le jeu de donnéesdataset.json · JSON · 19 kB
04

Pourquoi Corrobore

Ce qui était vrai lundi et ce que l’équipe pouvait savoir mardi sont deux questions différentes.

Ce qu’il faut construire soi-même

  • Une base classique écrase la valeur précédente. La décision d’hier devient inexplicable : il ne reste que la conclusion d’aujourd’hui, et rien ne dit sur quoi elle reposait.
  • Ajouter des colonnes created_at et updated_at ne suffit pas : ce sont des champs applicatifs, donc n’importe quel client, migration ou agent peut les rédiger a posteriori.
  • Recoller l’historique depuis un journal d’audit demande d’écrire soi-même le rejeu, et rien ne garantit que le journal et la table racontent la même histoire.

Ce que le contrat garantit

  • Le temps valide est une donnée, le temps de transaction ne l’est pas. Envoyer recorded_at dans une écriture est refusé par INVALID_REQUEST : personne ne peut antidater ce que le système a appris.
  • Fermer un intervalle n’est pas supprimer. L’évaluation périmée reste dans le graphe avec ses preuves, elle cesse simplement d’être retournée par recall.
  • Une correction est une version : la provenance s’accumule au lieu d’être remplacée, et expected_version transforme une course entre deux agents en VERSION_CONFLICT explicite.
05

Notebook exécutable

Dix-huit cellules qui rejouent une décision passée sans tricher

Le notebook matérialise la chronologie, montre quels enregistrements le moteur retourne aujourd’hui et lesquels sont hors intervalle, tente d’antidater une écriture et se fait refuser, applique la correction issue de l’image disque, provoque un VERSION_CONFLICT entre deux agents, reconstruit l’échelle des versions avec trace, trace le diagramme bitemporel, puis vérifie qu’un enregistrement mis en pierre tombale disparaît de recall tout en restant explicable.

temporal/notebook.ipynbEXTRAIT / 19 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': 100,
    'max_depth': 3,
    'max_payload_bytes': 512 * 1024,
    'max_cost': 2000,
    'timeout_ms': 5000,
    'supernode_threshold': 64,
}

visible = memory_op('recall', {
    'objective': 'northwind',
    'seed_ids': [ids['case--nw-supply']],
    'limits': LIMITS,
})
returned = {item['record']['identity_key'] for item in visible['items']}

print(f"{'record':<34} {'valid window (days)':<22} retrieved")
print('-' * 70)
for key, (start, end) in sorted(valid_windows.items()):
    window = f"{start:+d} .. {'open' if end is None else format(end, '+d')}"
    print(f'{key:<34} {window:<22} {"yes" if key in returned else "no"}')

assert 'assessment--access-compromised' in returned
assert 'assessment--access-legitimate' not in returned  # interval closed
assert 'contract--renewal' not in returned              # not yet in force
assert 'obs--maintenance-notice' not in returned        # expired
CODE [3] / PYTHON
backdated = dict(dataset['memories'][0])
backdated['identity_key'] = 'assessment--backdated-attempt'
backdated['valid_from'] = at(-30)
backdated['valid_until'] = None
backdated['recorded_at'] = at(-30)  # the field a client wishes existed

refused = memory_op('remember', backdated, idempotency_key='temporal:backdate', expect_error=True)
print(refused.status, refused.code)
print(refused.message[:200])
assert refused.code == 'INVALID_REQUEST'
CODE [4] / PYTHON
target = {'kind': 'memory', 'id': ids['assessment--access-compromised']}
before = memory_op('trace', {'target': target})
print('versions before:', [v['version'] for v in before['versions']])

corrected = memory_op('update', {
    'target': target,
    'expected_version': before['versions'][-1]['version'],
    'patch': {
        'confidence': 0.94,
        'add_provenance': [{
            'source_id': 'forensics',
            'locator': 'image#build-03',
            'observed_at': at(-4),
        }],
        'add_tags': ['forensically-confirmed'],
    },
}, idempotency_key='temporal:correction')

record = corrected['record']
print('version    :', record['version'])
print('confidence :', record['confidence'])
print('recorded_at:', record['recorded_at'])
print('provenance :', [p['source_id'] for p in record['provenance']])
print('valid_from :', record['valid_from'], '(unchanged: the fact was always true then)')

# The correction did not erase what justified the earlier, weaker belief.
assert len(record['provenance']) > 1
assert record['version'] > 1
CODE [5] / PYTHON
ladder = memory_op('trace', {'target': target})

for version in ladder['versions']:
    print(f"v{version['version']}  {version['recorded_at']}  {version['lifecycle']}")

print()
print('actor    :', ladder['actor_id'])
print('session  :', ladder['session_id'])
print('policies :', ladder['policy_decisions'])


def state_known_at(versions, moment):
    """The version the system would have served at `moment`."""
    seen = [v for v in versions if datetime.fromisoformat(v['recorded_at']) <= moment]
    return seen[-1] if seen else None


first = datetime.fromisoformat(ladder['versions'][0]['recorded_at'])
print()
print('as the system stood at ingest    :', state_known_at(ladder['versions'], first))
print('as the system stands now         :', state_known_at(ladder['versions'], datetime.now(timezone.utc)))
assert state_known_at(ladder['versions'], first)['version'] == 1
Télécharger le notebooknotebook.ipynb · IPYNB · 19 cellules
06

Playbook à télécharger

Trois minutes, un serveur jetable, une chronologie qui vieillit bien

Le playbook démarre un Corrobore éphémère, exécute le notebook et liste les décalages à modifier pour voir les intervalles basculer d’un côté ou de l’autre du présent.

  1. 01

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

  2. 02

    Installer requests et matplotlib

  3. 03

    Exécuter le notebook : les dates sont résolues au lancement

  4. 04

    Déplacer un intervalle et observer ce qui devient visible

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

Ce qui est disponible

Primitive stable de graph-core pour les états bitemporels et snapshots logiques. Les vues de timeline sont aussi exposées au runtime; la syntaxe Cypher complète de voyage temporel reste distincte.