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é.
Par où entrer
Cette page a grandi avec la couche qu'elle décrit : elle couvre maintenant ce que la porte lit et ce que le verdict devient. Selon ce que vous êtes en train de faire :
| Vous voulez… | Allez à |
|---|---|
| écrire une preuve dans un manifeste | le manifeste, puis les pointeurs typés |
| savoir ce qu'un pointeur vérifie vraiment — un test, un scan, une couverture, un lint, un SBOM | la profondeur dépend de ce que vous visez |
| comprendre pourquoi une ligne rougit | dérive, signatures de preuve |
| figer un passage au vert | sceller |
| remettre le verdict à un auditeur, à une pull request, à un moteur de politique | ce que le verdict devient |
| brancher ce verdict chez cosign, Kosli, JFrog ou Kyverno | docs/interop.md (dépôt), et l'étage release pour la ligne de partage |
| lier une spec à ses artefacts livrés | ce que le verdict devient, section spec-check |
| savoir ce que la porte ne dit pas | ce que runward ne prétend pas |
Si vous lisez d'un bout à l'autre, l'ordre ci-dessous est celui de la chaîne : le manifeste déclare, le pointeur résout, le sceau fige, le verdict sort.
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 dansrunward/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 :
- 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-eviln'est jamais compté comme étant sous/a/project). Échec →typed pointer does not resolve. - Fichier régulier, pas un dossier.
- Non vide : un fichier sans contenu non blanc échoue avec
an empty file is not evidence. - Contient la ligne : si
:LINEest fourni et que le fichier a moins de lignes, il échoue. - Porte le symbole : si
#SYMBOLest fourni, il doit être présent à une frontière d'identifiant —#guardFieldsn'est plus satisfait par un fichier qui ne contient queguardFieldsLegacy. C'était une sous-chaîne jusqu'à la 0.34.0, et un identifiant renommé restait vert tant que l'ancien nom était un fragment du nouveau : exactement le cas que le message d'erreur nommait déjà. Les symboles non-identifiants (opérateurs, noms pointés ou entre guillemets) gardent la sémantique de sous-chaîne exacte, une frontière d'identifiant n'y ayant aucun sens. - Porte le nom du test : pour
test:…::NAME, voir juste en dessous — ce que « porter » veut dire dépend de ce que le fichier est.
La profondeur dépend de ce que vous visez
Un pointeur ne dit pas seulement où regarder, il dit à quelle profondeur. Et la profondeur que la garde peut honnêtement atteindre dépend de la nature du fichier visé : dans un fichier ordinaire, un nom est un nom ; dans un rapport d'outil, c'est un résultat enregistré, et s'en tenir au nom serait accepter comme preuve exactement ce que l'outil a marqué rouge.
runward reconnaît le type du fichier par ses marqueurs structurels, jamais par son extension — un .xml peut être n'importe quoi. Ce que « résout » signifie, cas par cas :
| Ce que vous visez | Ce que le pointeur nomme | « Résout » veut dire | Ce qui est refusé |
|---|---|---|---|
| un fichier ordinaire | #SYMBOL |
présent à une frontière d'identifiant | absent, ou fragment d'un identifiant plus long |
| un rapport JUnit committé | ::NAME (ou ::CLASSE::NAME) |
le cas est enregistré vert | un cas rouge, erreuré ou skippé ; un cas absent. Toutes les occurrences du nom sont examinées, et un seul rouge rougit le verdict |
| un scan SARIF committé | #ruleId |
la règle est connue du scan et sans finding ouvert | des findings ouverts ; une règle que le scan n'a jamais vérifiée ; un JSON illisible |
| un rapport de couverture committé (lcov ou Cobertura) | #chemin/du/fichier.ts |
le fichier est mesuré et quelque chose l'a exercé | mesuré mais zéro ligne couverte ; jamais mesuré |
| un rapport ESLint committé | #chemin/du/fichier.ts |
le fichier est linté et sans finding de sévérité erreur | des erreurs enregistrées ; un fichier jamais linté. Un avertissement (sévérité 1) ne rougit pas |
| un SBOM CycloneDX committé | #pkg:npm/nom@1.2.3 ou #nom@1.2.3 |
le composant est déclaré dans l'inventaire | une autre version ; un composant absent ; un nom nu sans version, refusé plutôt que résolu |
| un ADR | adr:NNNN |
l'ADR existe et sa décision est ratifiée | un template, un ADR vide, une décision rejetée ou non ratifiée |
La logique est la même partout, et c'est celle de « un fichier vide n'est pas une preuve », déclinée : un test rouge n'est pas une preuve, un scan avec des findings ouverts n'est pas une preuve, un fichier que rien n'exerce ne prouve pas que la règle y a été appliquée. Un rapport committé n'est jamais exécuté : runward lit ce que votre CI a déposé, il ne lance ni votre suite ni votre scanner.
La SCA n'a volontairement pas d'adaptateur à elle : un constat de vulnérabilité est le travail de l'adaptateur SARIF, et tout outil de SCA qui vaut d'être cité émet du SARIF. Un troisième format de findings serait de la surface sans capacité derrière.
Deux conséquences à connaître.
Le nom nu est refusé dans un SBOM. #lodash passerait avec n'importe quelle version que l'inventaire porte — or « la dépendance est épinglée » est précisément la revendication qu'un tel pointeur sert d'habitude à étayer. Nommez le purl exact, ou nom@version ; le message d'erreur porte la correction.
Et sur un fichier SARIF, #ruleId ne retombe jamais sur la recherche de sous-chaîne. L'identifiant de règle est dans le JSON précisément parce qu'il y a des findings — une recherche de sous-chaîne verdirait donc le cas exactement rouge.
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) !== hash→sealed 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).
Ce que le verdict devient
Jusqu'ici, cette page décrit ce que la garde lit. L'autre moitié, c'est ce qu'elle émet — parce qu'un verdict qui ne quitte pas la machine où il a été rendu ne prouve rien à personne d'autre. Toutes ces sorties sont des fichiers : runward écrit, vous décidez de ce qui en est fait. Aucune ne signe, aucune ne publie, aucune n'appelle le réseau.
| Commande | Ce qui sort | À quoi ça sert |
|---|---|---|
check --json |
le verdict, contrat machine stable | piloter la garde depuis un agent ou une CI |
check --attest |
un Statement in-toto non signé, dont l'empreinte de sujet lie le verdict à l'état exact de la mission | remettre le verdict à quelqu'un d'autre ; le signer avec votre clé |
check --sarif |
un log SARIF 2.1.0 | chaque lacune devient une annotation sur la ligne de manifeste qui la porte, dans la pull request |
check --vsa --resource-uri <uri> |
une SLSA Verification Summary Attestation | un moteur de politique (Kyverno, OPA) admet ou refuse un déploiement sans rien apprendre du vocabulaire runward |
bundle <artefacts…> |
un manifeste in-toto liant plusieurs artefacts par SHA-256 brut | une provenance unique à remettre à un assesseur |
verify <attestation> |
le verdict re-dérivé depuis l'arbre courant | vérifier soi-même, hors ligne, des mois plus tard |
runward verify est la pièce qui rend le reste opposable : il recalcule l'empreinte de l'état de mission et le verdict depuis l'arbre présent. Un arbre qui a dérivé et un prédicat trafiqué échouent tous les deux, bruyamment. Sans réseau, sans racine de confiance, sans clé — donc si l'attestation porte une signature, elle est comptée et jamais vérifiée : ancrer une signature demande un trousseau que runward n'a pas, et le dire serait revendiquer plus que l'outil ne peut. C'est cosign verify-blob-attestation qui fait cette moitié-là. Si l'attestation a été produite par une version antérieure, l'écart est nommé, pour qu'une évolution du verdict ne soit jamais confondue avec une falsification.
À côté, runward spec-check applique la même grammaire de pointeurs à un tout autre objet : une spec (Spec Kit, OpenSpec, BMAD — markdown, format-agnostique). Chaque critère d'acceptation doit être lié à un artefact livré qui résout, à la profondeur que son pointeur déclare. Donnez-lui un bundle — plusieurs fichiers, ou le répertoire de la feature — et il vérifie en plus la cohérence interne du delta : tout identifiant de critère que le bundle référence doit être déclaré quelque part. tasks.md qui implémente AC7 quand la spec déclare AC1 à AC5 est cassé, quoi qu'AC7 ait voulu dire. Liaison et intégrité des références ; jamais un jugement sur le fait que le critère soit sémantiquement satisfait.
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/::testNamen'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, ouwhy: 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é.