runward

RW™ · V0.22.0

Docs · Comprendre · Preuve et intégrité

Preuve et intégrité.

runward encadre la livraison qu'un agent produit ; il n'écrit pas le code. Cette page explique comment la garde prouve que les décisions derrière ce code sont rattachées à quelque chose de réel : des pointeurs de preuve typés qui doivent résoudre vers un contenu non vide, un manifeste de conformité aux règles qui rend compte de chaque règle à fort impact, un sceau SHA-256 qui rougit quand la preuve scellée dérive, et les preuves consultatives comportementale et de tâche refaite qui se situent au-dessus de la garde. Chaque vérification ici est déterministe et s'exécute sans aucun LLM et sans aucun réseau : elle lit des octets et les compare, elle n'exécute jamais votre code et ne demande jamais de verdict à un modèle.

La couche de preuve est une courte chaîne : un pointeur typé résout vers un contenu réel, puis l'ensemble est scellé.

La preuve : un pointeur typé qui résout, puis se scelleUne preuve n’est pas une phrase : c’est un pointeur typé (file:/test:/adr:) qui doit résoudre vers un contenu réel et non vide. Une fois la porte stricte au vert, la preuve est scellée par empreinte SHA-256 ; toute dérive ultérieure rougit la porte jusqu’à re-vérification. Pointeur typéfile: · test: · adr:Contenu réelrésout, non videScelléSHA-256 · dérive = rouge résoutscelle
La preuve : un pointeur typé qui résout, puis se scelle

Le manifeste de conformité aux règles

Chaque livrable gardé porte un tableau markdown ## Rule conformance. Les paires (phase → livrable) gardées sont fixes :

  • architecture.md (architect)
  • execution-topology.md (topology)
  • floor.md (floor)
  • governance/threat-model.md (govern)
  • handover.md (handover)

Chaque règle de métier CRITICAL ou HIGH rattachée à une phase doit apparaître comme une ligne, dans l'un des trois états de comptabilisation :

  • applied : nécessite un pointeur de preuve. Une ligne applied dont la cellule Evidence est vide est une violation.
  • deviated : nécessite un ADR qui existe réellement dans runward/adr/ ; sinon elle échoue.
  • n/a : nécessite une vraie raison en une ligne : plus courte que 8 caractères ou un [bracketed placeholder] est rejeté comme trivial.

Comment l'ensemble attendu est dérivé

L'ensemble attendu est dérivé des fichiers de règles eux-mêmes : une règle est attendue à une phase quand son frontmatter indique impact: CRITICAL ou HIGH et que ses phases: [...] incluent cette phase. La garde lit le runward/rules/ propre à la mission quand il est présent, sinon les règles empaquetées.

conformance() signale alors :

  • une règle pas comptabilisée du tout
  • un statut non défini ou invalide
  • un slug de règle inconnu (avec un indice de migration s'il a été renommé/supprimé)
  • une règle listée plus d'une fois

Le plancher de non-vacuité

Il y a aussi un plancher de non-vacuité : si le nombre de règles CRITICAL/HIGH rattachées à une phase tombe sous un minimum épinglé, la garde lève une violation (mapping) : vous ne pouvez pas discrètement retirer le rattachement pour faire passer la garde.

Échafauder la forme du manifeste (jamais la décision)

runward manifest donne un aperçu en lecture seule ; runward manifest --sync échafaude seulement la forme :

  • il ajoute les lignes attendues manquantes avec un statut vide
  • il réécrit les slugs renommés selon le registre de migration
  • il crée une section manquante

Il ne fixe jamais un statut, n'écrit jamais de preuve, et ne supprime jamais une ligne (il ne fait que signaler les doublons, les slugs inconnus et les slugs supprimés pour que l'opérateur les corrige à la main). Une ligne échafaudée a un statut vide, et la garde refuse les lignes à statut vide tant que l'opérateur n'a pas décidé. Codes de sortie de manifest : 0 = terminé, 2 = aucune mission.

Pointeurs de preuve typés

La cellule Evidence d'une ligne applied peut porter des pointeurs typés, activés au cas par cas et par ligne, plusieurs par cellule séparés par ;, en laissant intacte la prose environnante. Il y a exactement trois préfixes :

  • file:PATH[:LINE][#SYMBOL] : un fichier du projet, avec optionnellement un numéro de ligne et une sous-chaîne #symbol.
  • test:PATH[::NAME] : un fichier de test, avec optionnellement un ::testName.
  • adr:NNNN : un identifiant d'ADR.

Ce que la garde vérifie pour file: et test:

Pour les pointeurs file: et test:, la garde vérifie, de manière déterministe :

  1. Résolution : le chemin doit résoudre dans l'une des trois bases du projet (parent de la mission, dossier de la mission, dossier du livrable). Un chemin absolu, ou un ../ qui sort de toutes les bases, est rejeté plutôt que résolu : ce n'est pas « une preuve dans votre projet » (le confinement est un test de préfixe normalisé avec un séparateur, de sorte que /a/project-evil n'est jamais compté comme étant sous /a/project). Échec → typed pointer does not resolve.
  2. Fichier régulier, pas un dossier.
  3. Non vide : un fichier sans contenu non blanc échoue avec an empty file is not evidence.
  4. Contient la ligne : si :LINE est fourni et que le fichier a moins de lignes, il échoue.
  5. Contient le symbole : si #SYMBOL est fourni et que la sous-chaîne est absente : symbol "…" not found in the file (moved or renamed? update the pointer).
  6. Contient le nom du test : pour test:…::NAME, le nom doit apparaître dans le fichier.

Pointeurs adr:

Pour adr:NNNN, un ADR portant cet identifiant doit exister dans runward/adr/ ; la correspondance de l'identifiant est ancrée sur une frontière de chiffre, de sorte que adr:1 n'est pas satisfait par ADR-10.

Non-vacuité des chemins nus

Au-delà des pointeurs typés, chaque ligne applied est aussi vérifiée pour sa non-vacuité sur tout jeton de chemin de fichier nu qu'elle mentionne (reconnu par des extensions de code/doc connues) : si un tel jeton résout vers un fichier réel, ce fichier doit être non vide.

Signatures de preuve

Un fichier de règle peut déclarer une regex signature: dans son frontmatter. Quand une règle est signée, sa preuve applied doit pointer vers un fichier dont le contenu correspond à cette regex : cela attrape le « cité, non appliqué », un pointeur visant un fichier qui ne porte pas réellement la forme de la règle. Si aucun fichier pointé n'est présent, ou qu'aucun ne correspond à /sig/i, la ligne échoue.

Deux garde-fous protègent la garde elle-même :

  • une signature dangereuse avec des quantificateurs imbriqués (par exemple (a+)+) qui risque un DoS par backtracking catastrophique est rejetée d'emblée (un filtre conservateur, pas une promesse d'attraper toute regex pathologique) ;
  • une regex invalide dans le fichier de règle est signalée plutôt que levée.

Dérive

driftReport traite les lignes qui utilisent des références de chemin en prose nue (pas des pointeurs typés) : si une ligne applied nomme des jetons de chemin de fichier et qu'aucun ne résout, c'est une dérive et elle échoue. La dérive est bloquante sous --strict. Les lignes qui portent des pointeurs typés sont diagnostiquées par la couche de preuve à la place : un seul diagnostic par ligne, jamais deux.

Sceller : check --freeze

runward check --freeze implique --strict. Le freeze refuse de sceller une garde rouge : s'il y a la moindre lacune de livrable, lacune de conformité ou hook en échec, il affiche « refusing to seal a red gate » et n'écrit rien. Sur une garde entièrement verte, il hache la preuve dans runward/evidence-lock.json.

Ce qui est scellé

collectSealableEvidence parcourt chaque ligne applied de tous les manifestes gardés, collecte chaque fichier de preuve résoluble (issu à la fois des pointeurs typés et des jetons de chemin nus), et enregistre un SHA-256 de ses octets, indexé par chemin relatif à la racine du projet, dans un ordre trié. Le lock est { version: 1, sealedAt, files }, rendu avec un ordre stable de sorte qu'il est idempotent octet pour octet sur une preuve inchangée. Définissez RUNWARD_DRY_RUN=1 pour prévisualiser le sceau sans écrire.

La vérification est activée au choix

La vérification est activée par construction : pas de fichier de lock signifie pas de vérification de sceau (verifyEvidenceLock retourne present: false). Quand un lock existe, à chaque check --strict suivant, chaque fichier scellé est re-haché et comparé. Trois façons qu'un sceau rougisse la garde :

  • Changed : sha256(file) !== hashsealed evidence changed: … — re-read the pointer, confirm the evidence still holds, then re-seal with runward check --freeze.
  • Missing : le fichier scellé a disparu ou n'est plus un fichier régulier.
  • Escaping path : un lock forgé dont la clé est absolue ou ../-échappe le projet est rejeté avant toute lecture, de sorte que le vérificateur ne peut jamais être transformé en un oracle de lecture de fichier arbitraire ou de DoS via /dev/zero. Un JSON invalide dans le lock est aussi signalé, pas levé.

La subtilité du freeze

Une subtilité délibérée : sous --freeze, l'ancien sceau n'est pas vérifié, seulement remplacé, sinon un fichier scellé légitimement modifié rendrait le re-scellage impossible, puisque la violation de sceau rougirait précisément la garde que le freeze exige verte.

Pourquoi un sceau (et pourquoi activé au choix)

Les vérifications de dérive et de pointeur attrapent un pointeur qui cesse de résoudre ; elles n'attrapent pas un pointeur qui résout toujours vers un fichier dont le contenu a silencieusement changé après votre passage. Le sceau SHA-256 comble cette lacune : la garde passe au vert sur les octets exacts sur lesquels vous êtes passé, et rougit le jour où ils changent. Il est activé au choix parce que sceller n'a de sens qu'une fois la garde réellement verte et que vous voulez figer ce passage ; imposer un lock sur chaque mission serait du bruit (ADR-0021).

Preuve comportementale et preuve de tâche refaite

Ce sont deux choses distinctes ; gardez-les séparées.

La preuve comportementale du plancher

La preuve comportementale du plancher est consultative et se situe au-dessus de la garde. runward lit runward/floor.md à la recherche d'une ligne Behavioral proof: <command> et d'un Proof artifact: <path> optionnel. Il signale si l'artefact est présent et frais : frais signifiant que sa mtime est au moins aussi récente que le fichier source le plus récent sous <root>/code/ (et laissé indéfini quand il n'y a pas de dossier code/).

Il est strictement en lecture seule : jamais exécuté, jamais analysé pour un succès/échec : exécuter la suite et juger le résultat reste le travail du code et de l'opérateur. La garde n'affiche cela que lorsque la garde documentaire est déjà verte, avec une note explicite indiquant « runward did not run your code — it is not a runtime. The behavioral proof is your test suite ».

La preuve de tâche refaite

La preuve de tâche refaite est différente : c'est une règle CRITICAL gardée, handover-redone-task-proof, rattachée à la phase handover (impact: CRITICAL, phases: [handover]). Elle est comptabilisée dans le manifeste de conformité de handover.md comme n'importe quelle autre règle, typiquement avec un pointeur typé vers la section de tâche refaite de la note de passation (son bloc « The redone task (the proof) »), et elle est vérifiée par la même couche de preuve ci-dessus.

La doctrine derrière : une passation est prouvée le jour où l'équipe qui reprend refait une vraie tâche de bout en bout, seule, sans le constructeur partant : consignée, datée, pointée. Ainsi la tâche refaite est à l'intérieur de la garde (une règle avec preuve) ; la preuve comportementale du plancher est au-dessus d'elle (présence/fraîcheur consultative).

Aussi au-dessus de la garde : les constats de verify

Une fois la garde documentaire verte, le check vous incite à lancer le workflow verify adversarial de cite-vs-apply (runward/workflows/verify.md). Son artefact de sortie est runward/governance/verify-findings.md ; runward ne signale que sa présence et sa fraîcheur, ne lit jamais un verdict à partir de lui, et il ne bloque jamais la garde. Frais signifie ici plus récent que chaque manifeste gardé : modifiez un manifeste après le passage et les constats deviennent périmés.

Comment l'érosion silencieuse est attrapée

L'« érosion silencieuse » est le cas où la paperasse dit toujours vert mais la réalité sous-jacente a bougé. runward l'attrape de manière déterministe au prochain check --strict :

  • Le fichier cible d'un pointeur est supprimé ou déplacé → la résolution du pointeur typé échoue, ou la dérive se déclenche pour les références en prose.
  • Une fonction est renommée de sorte qu'un #symbol / ::testName n'apparaît plus → la vérification de symbole/nom de test échoue.
  • Un fichier pointé est vidé → la non-vacuité échoue.
  • La forme de la règle est retirée d'un fichier cité → non-correspondance de signature.
  • Les octets d'un fichier scellé changent alors que le pointeur résout toujours → la non-correspondance SHA-256 rougit la garde.
  • Le rattachement de règle est réduit sous son plancher → violation (mapping).
  • Une décision reconstruite est laissée non ratifiée (nom de fichier DRAFT, Status: hypothesis, ou why: UNKNOWN) → la garde échoue tant qu'elle n'est pas ratifiée.
  • La preuve comportementale ou les constats de verify deviennent périmés (plus anciens que le code / un manifeste a changé depuis) → signalés comme stale, consultatif.

Ce que runward ne prétend pas

La garde vérifie la présence et la forme d'une décision tracée : qu'une règle à fort impact est comptabilisée et que son pointeur résout vers un contenu réel, non vide et conforme à la forme. Elle ne juge jamais si le code implémente réellement la règle ; ce jugement reste celui de l'opérateur, à la garde.

La sortie est une preuve d'appui prête pour l'audit qui alimente un programme de conformité (runward compliance <regime>), pas un verdict de conformité : l'acceptation relève de l'auditeur.

runward est déterministe et sans LLM/sans réseau à la garde (le LLM vit dans le harness qui a écrit le code). Il n'exécute jamais votre code ni vos tests, et il n'installe jamais de hooks git et n'écrit jamais dans .git/ : les adaptateurs sont des échantillons inertes que l'opérateur câble de son propre chef (ADR-0012).

Pourquoi ce choix

Pourquoi le sceau SHA-256

Le sceau SHA-256 (check --freeze) existe parce que les vérifications moins coûteuses ratent un mode de défaillance. La dérive et la résolution de pointeur typé attrapent un pointeur qui cesse de résoudre, et la vérification de signature attrape un pointeur visant un fichier dépourvu de la forme de la règle, mais aucune d'elles n'attrape un pointeur qui résout toujours vers un fichier dont le contenu a silencieusement changé après votre passage de la garde. L'alternative rejetée est de faire confiance au fait qu'un pointeur résoluble et non vide reste valide pour toujours ; le sceau fige plutôt les octets exacts sur lesquels vous êtes passé et rougit la garde le jour où ils changent. Il est activé au choix (pas de fichier de lock signifie pas de vérification) parce que sceller n'a de sens qu'une fois la garde réellement verte ; un lock obligatoire sur chaque mission serait du bruit.

Pourquoi manifest --sync n'échafaude que la forme

Séparément, manifest --sync échafaude délibérément seulement la forme et laisse chaque statut vide plutôt que de deviner « applied » : l'alternative rejetée (remplir automatiquement un statut plausible) laisserait la supposition d'un agent passer pour une décision d'opérateur, donc la garde refuse les lignes à statut vide tant qu'un humain n'a pas décidé.

← Docs