0.0.3-alpha.3

This commit is contained in:
2026-09-24 09:21:39 +02:00
parent a8dd8ae349
commit b3230b4da9
12 changed files with 715 additions and 20 deletions

View File

@@ -1 +1 @@
0.0.3-alpha.2
0.0.3-alpha.3

118
deltas/0.0.3/alpha.3.md Normal file
View File

@@ -0,0 +1,118 @@
<!-- file: deltas/0.0.3/alpha.3.md -->
<!-- version: 1 -->
# Delta 0.0.3-alpha.3 — audits de style Rust ciblés
## Baseline et version
Baseline : `0.0.3-alpha.2`, avec `history/0.0.3/alpha.2.md` appliqué et committé. Ce delta MUST NOT être appliqué tant que ce commit n'existe pas.
Version avant application :
```text
0.0.3-alpha.2
```
Version après application :
```text
0.0.3-alpha.3
```
## Objet et REPLAN
Après la gate PASS de `alpha.2`, l'opérateur a signalé les audits Python de style Rust de KSP et Games et demandé leur découpage en scripts plus petits et monotâches. L'examen de leurs `audit_rust_general_rules.py` montre qu'ils combinent blancs, constantes, ordre de modules, headers, rustdoc et règles de façade. Résultat REPLAN : `SPLIT`. Les deux contrôles de style apportant une preuve autonome passent à `alpha.3` ; les manifests Cargo initialement prévus ici passent à `alpha.4`, l'audit des headers Markdown à `alpha.5` et la décision workflow à `alpha.6`.
Ajouter un contrôle facultatif d'espacement des lignes vides et un contrôle facultatif des blocs de constantes racine, avec un helper lexical commun sans politique autonome. Le profil `510` possède leurs conventions optionnelles, la documentation de validation explicite leur adoption et leurs limites ; la matrice répertorie leur test ciblé. Rustfmt reste propriétaire du tri interne qu'il effectue déjà ; Clippy garde ses lints. Aucun script KSP/Games complet ni gate Rust obligatoire n'est copié dans PGR.
## Scope
REPLACE :
```text
VERSION
docs/plans/003-V0_0_3_TEMPLATES_AUDITS_PLAN.md
docs/rules/510-RUST_CARGO.md
docs/templates/001-RUST_CARGO_TOOLING.md
docs/validation/001-VALIDATION_MATRIX.md
```
ADD :
```text
docs/validation/002-RUST_STYLE_AUDITS.md
scripts/_rust_audit_common.py
scripts/audit_rust_blank_lines.py
scripts/audit_rust_const_blocks.py
tests/test_rust_audits.py
deltas/0.0.3/alpha.3.md
```
DELETE : aucun.
Les fichiers complets figurent dans l'archive delta. Aucun history `alpha.3`, changement rétroactif de delta/history, manifest Cargo, suppression ou modification des corpus de référence n'est inclus.
## Limites et prévalidation
Les audits sont read-only, sélectionnent explicitement des fichiers `.rs` et signalent les violations avec chemin et ligne. La prévalidation hors dépôt PGR a porté sur `300` sources KSP et `59` sources Games : l'audit des blancs est propre sur les deux ; l'audit des constantes est propre sur Games mais affiche `17` signaux KSP lorsqu'il est lancé globalement. Des séquences KSP portent un ordre métier ou de schéma ; l'ordre alphabétique ne leur est pas imposé. La sélection du projet doit donc exclure les fichiers ayant une sémantique d'ordre incompatible. Les scripts ne parsèrent pas intégralement Rust et ne remplacent ni Rustfmt ni Clippy.
Cette prévalidation de corpus n'est pas une validation opérateur de la tranche. Le dépôt PGR ne contient ni crate Rust ni fichiers Rust sujets à un audit produit. Les cas de test Python du delta vérifient les contrats réellement livrés, y compris échecs attendus et statut CLI.
## Validation opérateur
Après application sur le commit `alpha.2` :
```bash
test "$(cat VERSION)" = '0.0.3-alpha.3'
git cat-file -e HEAD:history/0.0.3/alpha.2.md
test -f history/0.0.3/alpha.2.md
test -f deltas/0.0.3/alpha.3.md
test ! -e history/0.0.3/alpha.3.md
grep -Fx '<!-- version: 4 -->' docs/plans/003-V0_0_3_TEMPLATES_AUDITS_PLAN.md
grep -F '### alpha.4 — manifests Cargo workspace et crate membre' docs/plans/003-V0_0_3_TEMPLATES_AUDITS_PLAN.md
grep -F '### alpha.5 — audit de headers Markdown' docs/plans/003-V0_0_3_TEMPLATES_AUDITS_PLAN.md
python3 -B -m unittest discover -s tests -p test_rust_audits.py -v
python3 -B scripts/audit_markdown_tables.py \
README.md RULES.md ROADMAP.md CHANGELOG.md docs deltas history prompts
python3 -B scripts/audit_markdown_links.py \
README.md RULES.md ROADMAP.md CHANGELOG.md docs deltas history prompts
python3 -B - <<'PY'
from pathlib import Path
import re
files = [Path(name) for name in ('README.md', 'RULES.md', 'ROADMAP.md', 'CHANGELOG.md')]
for directory in ('docs', 'deltas', 'history', 'prompts'):
files.extend(Path(directory).rglob('*.md'))
files = sorted(set(files))
for path in files:
lines = path.read_text(encoding='utf-8').splitlines()
assert len(lines) >= 2 and lines[0] == f'<!-- file: {path.as_posix()} -->', path
assert re.fullmatch(r'<!-- version: [1-9][0-9]* -->', lines[1]), path
print(f'Markdown header audit: clean ({len(files)} file(s))')
PY
git diff --check
git add VERSION docs/plans/003-V0_0_3_TEMPLATES_AUDITS_PLAN.md \
docs/rules/510-RUST_CARGO.md docs/templates/001-RUST_CARGO_TOOLING.md \
docs/validation/001-VALIDATION_MATRIX.md docs/validation/002-RUST_STYLE_AUDITS.md \
scripts/_rust_audit_common.py scripts/audit_rust_blank_lines.py \
scripts/audit_rust_const_blocks.py tests/test_rust_audits.py \
deltas/0.0.3/alpha.3.md
git diff --cached --check
git status --short
```
Scope attendu avant ajout du history : cinq `REPLACE` et six `ADD` exactement comme ci-dessus. Revue opérateur : vérifier le rôle facultatif des deux scripts, leur dépendance commune, la sélection des fichiers Rust et la priorité de l'ordre sémantique ; confirmer que les manifests Cargo restent prévus en `alpha.4`.
## Transition
### PASS
Produire séparément `history/0.0.3/alpha.3.md` avec les résultats réellement exécutés, l'appliquer et committer cette tranche avant tout delta `alpha.4`.
### FAIL
Consigner le diagnostic dans le history `alpha.3`, puis corriger ce scope dans `alpha.3.fix.1` ou engager un nouveau REPLAN si la séparation des contrôles n'est plus adaptée.

View File

@@ -1,11 +1,11 @@
<!-- file: docs/plans/003-V0_0_3_TEMPLATES_AUDITS_PLAN.md -->
<!-- version: 3 -->
<!-- version: 4 -->
# Plan 0.0.3 — templates initiaux et audits réutilisables
## Statut et sources
Plan de cadrage créé dans `0.0.3-alpha.1` et corrigé dans `0.0.3-alpha.1.fix.1` après sa validation mécanique. `alpha.2` intègre la convention opérateur sur les fichiers d'outillage Rust et matérialise les premiers templates. Les tranches suivantes sont une prévision, non un état validé.
Plan de cadrage créé dans `0.0.3-alpha.1` et corrigé dans `0.0.3-alpha.1.fix.1` après sa validation mécanique. `alpha.2` intègre la convention opérateur sur les fichiers d'outillage Rust et matérialise les premiers templates ; sa gate opérateur est PASS. Après cette gate, la demande de décomposer les audits de style Rust KSP/Games modifie le calendrier prévisionnel. Le résultat REPLAN de `alpha.3` est `SPLIT` : deux audits de style ciblés sont traités d'abord, les manifests Cargo puis les headers Markdown suivent dans des tranches distinctes. Les tranches suivantes restent une prévision, non un état validé.
La source autoritaire est l'archive Gitea fournie `pgr-v0.0.2.zip`, issue selon l'opérateur du tag `v0.0.2`. Son SHA-256 vérifié localement est :
@@ -84,13 +84,21 @@ Les spécifications complètes de skeleton restent réservées à `0.0.4` ; Rust
## Sélection des audits et patterns
Les deux audits existants (`tables`, `liens locaux`) sont les premiers patterns concrets : une propriété chacun, sélection explicite de chemins, lecture seule, messages déterministes et aucune invocation de toolchain. Ils doivent rester séparés.
Les deux audits Markdown existants (`tables`, `liens locaux`) restent séparés : une propriété chacun, sélection explicite des chemins et lecture seule. Le contrôle récurrent des headers Markdown demeure un candidat prioritaire, possédé par `docs/rules/300-DOCUMENTATION.md`, à extraire dans une tranche distincte avec cas positifs et négatifs.
Un contrôle supplémentaire de headers Markdown est candidat prioritaire. La propriété est précise : pour chaque fichier Markdown choisi, comparer `<!-- file: ... -->` à son chemin relatif et vérifier `<!-- version: N -->` avec `N >= 1`. Son owner est `docs/rules/300-DOCUMENTATION.md`, son sujet réel est le corpus de `73` Markdown PGR, et les gates précédents répètent ce contrôle en Python inline. Ni le compilateur ni les deux scripts existants ne le couvrent. Sa matérialisation doit garder la sélection de chemins explicite et tester au moins un header erroné, un header correct et un chemin absent.
La comparaison des scripts Python KSP `0.3.16` et Games `0.3.5` montre que `audit_rust_general_rules.py` (~580 lignes dans chaque corpus) mélange headers et versions Rust, imports, rustdoc, structure des items, blancs, constantes et modules. `audit_rust_export_completeness.py` possède le contrat particulier des façades et réexports ; KSP ajoute `audit_ksp_workspace_rules.py` pour manifests/frontières/tracing, tandis que Games regroupe plusieurs intégrations Web/Android/Gradle dans `audit_distribution_layout.py`. `audit_rust_workspace_rules.py` est un orchestrateur ; il peut rester un lanceur sans devenir propriétaire de ces contrats.
Les archives KSP et Games contiennent les mêmes familles `audit_rust_general_rules.py` et `audit_rust_export_completeness.py`, et un orchestrateur `audit_rust_workspace_rules.py`. Leur ressemblance vient de règles partagées entre ces projets ; les scripts généraux agrègent déjà plusieurs propriétés, telles que headers Rust, placement des imports, rustdoc, ordre d'items et façade publique. KSP ajoute un audit spécifique des dépendances/owners et Games un audit de layout avec des chemins Web/Android et versions Gradle propres au jeu. Un orchestrateur peut rester utile pour une gate, mais ne devient pas un nouvel audit propriétaire de plusieurs règles.
| Propriété | Owner | Suite |
|------------------------------------------|-------------------------|----------------------------------------|
| Ordre interne des `use` et `mod` | Rustfmt | `cargo fmt --all -- --check` |
| Diagnostics du langage et des lints | Rust/Clippy | Outils officiels du projet |
| Espacement des lignes vides | Profil `510` facultatif | `audit_rust_blank_lines.py` |
| Blocs de constantes racine | Profil `510` facultatif | `audit_rust_const_blocks.py` |
| Headers, rustdoc, API, tests, plateforme | Profils locaux | Audits séparés uniquement si justifiés |
Pour PGR, les candidats réutilisables à étudier après les templates Cargo sont : vérification du miroir `VERSION`/`workspace.package.version` lorsque le projet déclare une seule unité de release, vérification de l'héritage explicite des lints dans les crates et éventuel contrôle d'une façade publique uniquement dans un profil qui possède ce contrat. Chacun exigerait un owner, un sujet réel et une propriété isolée ; Cargo/Clippy doivent rester responsables des erreurs qu'ils savent déjà détecter. Aucune de ces idées ne justifie un script Rust spécialisé dans ce fix documentaire. Le contrôle manuel du pairing delta/history reste suffisant à ce stade.
Le balayage préalable des vrais sources `crates/**/src/**/*.rs` donne `300` fichiers KSP et `59` Games. Le contrôle des blancs passe les deux corpus. Un tri naïf de toutes les constantes racine trouve `17` signaux dans KSP et zéro dans Games ; plusieurs listes KSP sont ordonnées par contrat de protocole ou de schéma. Cela interdit de déclarer le tri globalement obligatoire ou de corriger ces fichiers sans revue. Les scripts livrés sont des patrons facultatifs : le propriétaire d'un projet choisit le sous-ensemble de sources concerné, applique `cargo fmt` avant ces audits et conserve l'ordre sémantique là où il existe. Un petit helper lexical partagé masque commentaires et littéraux ; il n'est pas un troisième audit ni un parseur Rust exhaustif. Les limites et le mode d'emploi sont dans `docs/validation/002-RUST_STYLE_AUDITS.md`.
Les autres candidats demeurent conditionnels : miroir `VERSION`/`workspace.package.version` si une release unit est partagée, héritage explicite des lints dans les crates et façade publique seulement lorsqu'un projet possède ce contrat. Aucun audit spécialisé de ces domaines ni de layout Android/Web n'est justifié par le dépôt documentaire PGR lui-même.
## Trajectoire prévisionnelle
@@ -104,17 +112,21 @@ Après le history et le commit de `alpha.1`, documenter la comparaison réelle d
### alpha.2 — outillage Rust/Cargo partagé
Créer les templates `rustfmt.toml`, `clippy.toml` et `.cargo/config.toml` à partir du noyau éprouvé dans KSP et Games. Documenter les responsabilités distinctes : formatage, paramètres Clippy, sortie Cargo hors du dépôt. Le nom du projet, l'édition Rust et les seuils ajustables sont explicités au point de copie ; vérifier syntaxe et chemins relatifs. Les niveaux de lint dans `Cargo.toml` restent à la tranche suivante, qui les reliera au profil Clippy retenu.
Créer les templates `rustfmt.toml`, `clippy.toml` et `.cargo/config.toml` à partir du noyau éprouvé dans KSP et Games. Documenter les responsabilités distinctes : formatage, paramètres Clippy, sortie Cargo hors du dépôt. Le nom du projet, l'édition Rust et les seuils ajustables sont explicités au point de copie ; vérifier syntaxe et chemins relatifs. Les niveaux de lint dans `Cargo.toml` restent à `alpha.4` après le REPLAN, qui les reliera au profil Clippy retenu.
### alpha.3 — manifests Cargo workspace et crate membre
### alpha.3 — audits de style Rust découpés (REPLAN : SPLIT)
Fournir des modèles minimaux de `Cargo.toml` racine et membre, incluant l'héritage des métadonnées et des lints et une explication des variations de scope. Vérifier les exemples sur un workspace d'essai sans recopier les listes de dépendances, versions et chemins de KSP/Games. Si les modèles exigent trop de décisions pour former des fichiers TOML valides, livrer des extraits complets avec procédure de substitution documentée, sans prétendre qu'un placeholder est prêt à compiler.
Suite à la demande post-gate `alpha.2`, isoler deux contrôles réutilisables des scripts composites KSP/Games : espacement des lignes vides et blocs de constantes racine sans ordre sémantique. Posséder leur convention facultative dans le profil `510`, documenter sélection/limites et exécuter des cas ciblés positifs et négatifs. Vérifier sur les corpus réels que les ordres contractuels ne deviennent pas de faux changements imposés. Les manifests initialement prévus ici passent à `alpha.4` : aucune validation anticipée n'est déduite du PASS de `alpha.2`.
### alpha.4 — audit de headers Markdown
### alpha.4 — manifests Cargo workspace et crate membre
Extraire le contrôle récurrent en un script read-only monotâche, documenter sa portée dans la matrice et vérifier des cas positifs et négatifs utiles. Ne pas fusionner cet audit avec les deux audits Markdown existants. Les autres audits candidats restent à l'étude jusqu'à un sujet/owner avéré.
Fournir des modèles minimaux de `Cargo.toml` racine et membre, incluant l'héritage des métadonnées et des lints et une explication des variations de scope. Vérifier les exemples sur un workspace d'essai lorsque la toolchain est disponible, sans recopier les listes de dépendances, versions et chemins de KSP/Games. Si les modèles exigent trop de décisions pour former des fichiers TOML valides, livrer des extraits complets avec procédure de substitution documentée, sans prétendre qu'un placeholder est prêt à compiler.
### alpha.5 — décision sur le template de workflow
### alpha.5 — audit de headers Markdown
Extraire le contrôle récurrent en un script read-only monotâche, documenter sa portée dans la matrice et vérifier des cas positifs et négatifs utiles. Ne pas fusionner cet audit avec les deux audits Markdown existants.
### alpha.6 — décision sur le template de workflow
Factoriser le prompt manuel seulement si la revue des prompts et du workflow confirme un bénéfice net ; sinon constater explicitement son report et passer à la stabilisation sans artefact artificiel.
@@ -132,6 +144,6 @@ Promouvoir la RC validée, produire le history de clôture pré-tag, committer e
## Points de décision et validation
Le plan doit être revu si les manifests Cargo exigent des choix de projet incompatibles avec un modèle minimal, si l'audit de headers ne peut rester fiable et monotâche, si le template de workflow duplique le plan, ou si un autre examen des corpus révèle des contraintes nouvelles. Un REPLAN explicite indique `CONTINUE`, `SPLIT`, `CONTINUE-NEXT-SESSION` ou `ABANDON` ; la durée d'une conversation ne crée pas une nouvelle version.
Le résultat `SPLIT` de `alpha.3` répond à la découverte post-gate de contrôles de style Rust utiles mais mêlés dans deux scripts de référence. Le plan doit encore être revu si les manifests Cargo exigent des choix de projet incompatibles avec un modèle minimal, si l'audit de headers ne peut rester fiable et monotâche, si le template de workflow duplique le plan, ou si un autre examen des corpus révèle des contraintes nouvelles. Un REPLAN explicite indique `CONTINUE`, `SPLIT`, `CONTINUE-NEXT-SESSION` ou `ABANDON` ; la durée d'une conversation ne crée pas une nouvelle version.
La validation `alpha.1` a porté sur l'identité de `VERSION`, la portée du delta, les headers et audits Markdown, l'absence de changement historique rétroactif et le contrôle Git du dépôt de l'opérateur. Ses résultats observés sont conservés dans `history/0.0.3/alpha.1.md`. Le fix documentaire décrit ci-dessus possède sa propre gate et son propre history : le PASS de `alpha.1` ne vaut pas validation anticipée de `alpha.1.fix.1`.

View File

@@ -1,5 +1,5 @@
<!-- file: docs/rules/510-RUST_CARGO.md -->
<!-- version: 1 -->
<!-- version: 2 -->
# Profil Rust / Cargo
@@ -129,7 +129,11 @@ L'édition de ce fichier MUST rester cohérente avec l'édition réellement util
Après une modification Rust, `cargo fmt --all` applique le formatage canonique puis `cargo fmt --all -- --check` vérifie l'état livré. Une modification produite uniquement par le formatter officiel ne crée pas artificiellement une nouvelle révision documentaire d'un fichier si aucun contenu réel n'a changé.
Les micro-règles de mise en page qui ne sont pas communes aux corpus ou qui sont déjà correctement possédées par `rustfmt` ne sont pas élevées automatiquement au niveau PGR.
Le profil de style strict supplémentaire peut être adopté par un projet qui souhaite appliquer les conventions KSP/Games. Dans ce scope choisi, les lignes vides consécutives SHOULD être supprimées, ainsi que les lignes vides à l'intérieur des fonctions/méthodes et des définitions `struct`/`enum`. Ces propriétés ne sont pas entièrement possédées par Rustfmt. L'audit facultatif `scripts/audit_rust_blank_lines.py` contrôle ces deux formes d'espacement sur les seuls fichiers Rust sélectionnés.
Les constantes de module qui constituent un bloc homogène sans ordre sémantique SHOULD être regroupées dans l'ordre `pub`, `pub(crate)`, privé, puis par nom dans chaque visibilité : aucune ligne vide entre constantes d'une même visibilité et exactement une entre niveaux différents. `scripts/audit_rust_const_blocks.py` vérifie cette convention uniquement sur les constantes au niveau racine des fichiers choisis. Son exécution requiert une sélection explicite des fichiers pour lesquels cet ordre est réellement souhaité. Une table de protocole, une séquence de migrations ou tout autre ordre contractuel conserve son ordre propre ; le lecteur ne doit pas réordonner une telle séquence pour faire passer l'audit.
L'ordre des `use`, réexports et déclarations `mod` que Rustfmt sait réordonner dans ses groupes appartient au formatteur ; les modules de test après les modules de production et l'organisation d'une façade relèvent de leur contrat local, avec revue ou audit dédié seulement si celui-ci apporte une preuve fiable. Les règles rustdoc et d'API conservent leurs propriétaires ci-dessus. Les scripts PGR ne prétendent pas parser toutes les macros ou les items Rust : une décision sémantique reste une revue.
## Configuration Cargo et artefacts générés
@@ -188,4 +192,4 @@ Lorsqu'une règle Rust devient objectivement automatisable dans un projet ou ske
Un éventuel wrapper d'orchestration MAY appeler plusieurs audits, mais chaque audit propriétaire doit conserver une responsabilité identifiable et rester read-only.
Aucun nouvel audit Rust n'est imposé par cette tranche à PGR lui-même : le dépôt PGR courant ne contient pas de workspace Rust sur lequel une telle gate fournirait une preuve réelle.
Les deux audits de style disponibles sont documentés dans [`../validation/002-RUST_STYLE_AUDITS.md`](../validation/002-RUST_STYLE_AUDITS.md). Ils restent optionnels et prennent un corpus Rust explicitement sélectionné ; le dépôt PGR lui-même ne contient pas de workspace Rust sur lequel une telle gate fournirait une preuve réelle.

View File

@@ -1,5 +1,5 @@
<!-- file: docs/templates/001-RUST_CARGO_TOOLING.md -->
<!-- version: 1 -->
<!-- version: 2 -->
# Templates d'outillage Rust/Cargo
@@ -11,7 +11,7 @@ Ces trois fichiers fournissent une baseline de configuration issue des conventio
- [`clippy.toml`](../../templates/rust-cargo/clippy.toml) — paramètres de lints (seuils, exceptions de tests, identifiants courts) issus du noyau partagé des deux projets ;
- [`.cargo/config.toml`](../../templates/rust-cargo/.cargo/config.toml) — emplacements des artefacts Cargo hors de la racine source.
Les niveaux `deny`, `allow`, `warn` et `forbid` des lints Rust/Clippy appartiennent au `Cargo.toml` du workspace, selon [`510-RUST_CARGO.md`](../rules/510-RUST_CARGO.md). `clippy.toml` règle des paramètres de lints, pas leur niveau. Les modèles de manifests racine et membre, ainsi que l'héritage `[lints] workspace = true`, sont prévus dans la tranche suivante.
Les niveaux `deny`, `allow`, `warn` et `forbid` des lints Rust/Clippy appartiennent au `Cargo.toml` du workspace, selon [`510-RUST_CARGO.md`](../rules/510-RUST_CARGO.md). `clippy.toml` règle des paramètres de lints, pas leur niveau. Les modèles de manifests racine et membre, ainsi que l'héritage `[lints] workspace = true`, sont prévus en `alpha.4` après l'extraction des audits de style de `alpha.3`.
## Copier et adapter

View File

@@ -1,5 +1,5 @@
<!-- file: docs/validation/001-VALIDATION_MATRIX.md -->
<!-- version: 6 -->
<!-- version: 7 -->
# Matrice de validation PGR
@@ -32,6 +32,12 @@ Un contrôle manuel peut rester dans la matrice lorsqu'aucun audit automatisé f
PGR préfère plusieurs audits simples avec une responsabilité identifiable à un script unique qui agrège des règles indépendantes.
## Contrôles Rust réutilisables, hors gate du dépôt PGR
[`002-RUST_STYLE_AUDITS.md`](002-RUST_STYLE_AUDITS.md) décrit deux audits facultatifs : `audit_rust_blank_lines.py` contrôle l'espacement des lignes vides, `audit_rust_const_blocks.py` contrôle l'ordre et l'espacement des blocs de constantes au niveau racine. Ils acceptent des chemins Rust sélectionnés et partagent un petit support lexical ; leur validation ciblée dans PGR est `python3 -B -m unittest discover -s tests -p test_rust_audits.py`.
Leur disponibilité ne transforme pas PGR en workspace Rust et ne rend pas automatiquement l'ordre alphabétique applicable à chaque fichier d'un projet. Lors d'une adoption, le projet propriétaire sélectionne ses chemins après `cargo fmt` et applique sa propre gate ; Rustfmt et Clippy conservent les contrôles qu'ils possèdent.
## Politique Eclipse actuelle
Les métadonnées Eclipse spécifiques à une machine restent ignorées, notamment `.project`, `.classpath` et les préférences non partagées.

View File

@@ -0,0 +1,34 @@
<!-- file: docs/validation/002-RUST_STYLE_AUDITS.md -->
<!-- version: 1 -->
# Audits facultatifs de style Rust
## Responsabilité et provenance
Les sources KSP `0.3.16` et Games `0.3.5` possèdent chacune un `audit_rust_general_rules.py` qui mêle plusieurs règles. Le découpage PGR fournit deux contrôles ciblés sans copier l'orchestrateur ni les règles propres aux deux projets. Le contrat de style facultatif est dans [`510-RUST_CARGO.md`](../rules/510-RUST_CARGO.md), et la sélection des preuves dans [`400-VALIDATION.md`](../rules/400-VALIDATION.md).
`scripts/audit_rust_blank_lines.py` contrôle les lignes vides consécutives et les lignes vides dans un corps de fonction/méthode, `struct` ou `enum`. `scripts/audit_rust_const_blocks.py` contrôle l'ordre `pub`, `pub(crate)`, privé, le tri naturel des noms dans chaque visibilité et l'espacement entre constantes consécutives au niveau racine du fichier. `scripts/_rust_audit_common.py` partage la sélection des chemins et le masquage des commentaires/littéraux : il ne définit aucune nouvelle politique.
## Adoption et commandes
Dans un projet Rust qui choisit ces conventions, copier les trois fichiers `scripts/*.py` concernés en gardant le helper à côté des deux audits. Adapter les headers `file` au chemin du projet et maintenir une révision de fichier propre à cette copie.
Choisir explicitement les fichiers concernés par chaque contrat. Après formatage :
```bash
cargo fmt --all -- --check
python3 -B scripts/audit_rust_blank_lines.py crates/example/src
python3 -B scripts/audit_rust_const_blocks.py crates/example/src/constants.rs
```
Une invocation sans chemin est une erreur, de même qu'un chemin absent ou aucun fichier Rust sélectionné. Un répertoire sélectionne récursivement les fichiers `.rs` hors répertoires générés habituels. Les audits ne modifient ni les fichiers inspectés ni l'arborescence de build. Les sorties utilisent le chemin et la ligne ; le code de sortie est `0` si propre, `1` pour violation, `2` pour sélection/lecture impossible.
Ne sélectionner pour l'audit des constantes que les fichiers dont les blocs racine sans sémantique d'ordre doivent effectivement suivre ce contrat. Un tri de table de protocole, des migrations, des champs de wire format ou de toute séquence métier peut casser le programme : sa revue appartient au projet. Ne pas utiliser la sortie du script comme justification pour les réordonner.
## Limites mesurées
La prévalidation des sources réels a porté sur `300` fichiers KSP et `59` Games sous `crates/**/src/`. L'audit des lignes vides n'y détecte aucun écart. L'audit de constantes détecte `17` signaux KSP et aucun Games lorsque tous ces fichiers lui sont soumis ; des fichiers KSP contiennent des séquences ordonnées selon le protocole ou le schéma, ce qui rend une gate globale du tri inappropriée. Ces observations ne sont pas une gate de validation opérateur PGR.
L'analyse lexicale masque littéraux et commentaires courants, y compris commentaires imbriqués et chaînes Rust brutes. Elle n'est pas un parseur Rust : les macros élaborées, syntaxe nouvelle, attributs complexes, items associés dans `impl` et constantes dans modules inline n'entrent pas dans le contrat garanti de l'audit des blocs racine. Les lignes vides à l'intérieur d'un littéral multilignes sont exclues. Les cas qui demandent une interprétation sémantique nécessitent la revue humaine ou les outils Rust du projet.
Rustfmt reste propriétaire de l'ordre qu'il réécrit déjà dans les groupes de `use`/`mod`. Clippy et le compilateur restent responsables de leurs diagnostics. La suite `tests/test_rust_audits.py` vérifie les propriétés ciblées avec des cas propres, des violations et des littéraux/commentaires qui contiennent des accolades.

48
history/0.0.3/alpha.3.md Normal file
View File

@@ -0,0 +1,48 @@
<!-- file: history/0.0.3/alpha.3.md -->
<!-- version: 1 -->
# History 0.0.3-alpha.3 — audits de style Rust ciblés
## Statut
PASS
L'opérateur a exécuté la gate prescrite pour `alpha.3` dans son dépôt le `2026-09-24`. Les neuf tests des audits Rust et les contrôles documentaires passent. Ce history constate les preuves fournies pour cette tranche, sans préjuger de la validation des manifests Cargo de `alpha.4`.
## Baseline et version
`git cat-file -e HEAD:history/0.0.3/alpha.2.md` a confirmé la présence dans `HEAD` du history de la tranche précédente. L'opérateur a vérifié `VERSION = 0.0.3-alpha.3`, la présence du delta courant, celle du history précédent et l'absence de `history/0.0.3/alpha.3.md` avant sa livraison.
## Validation opérateur observée
Les recherches sur le plan ont confirmé sa révision `4`, la tranche Cargo prévue en `alpha.4` et l'audit des headers Markdown prévu en `alpha.5`.
La commande `python3 -B -m unittest discover -s tests -p test_rust_audits.py -v` a exécuté `9` cas, tous `ok` ; le résultat final est `OK`. Les contrôles documentaires ont affiché :
```text
Markdown table audit: clean (3 table(s), 83 file(s))
Markdown link audit: clean (59 local link(s), 83 file(s))
Markdown header audit: clean (83 file(s))
```
`git diff --check` et `git diff --cached --check` n'ont affiché aucun diagnostic. Après `git add`, le scope indexé fourni est :
```text
M VERSION
A deltas/0.0.3/alpha.3.md
M docs/plans/003-V0_0_3_TEMPLATES_AUDITS_PLAN.md
M docs/rules/510-RUST_CARGO.md
M docs/templates/001-RUST_CARGO_TOOLING.md
M docs/validation/001-VALIDATION_MATRIX.md
A docs/validation/002-RUST_STYLE_AUDITS.md
A scripts/_rust_audit_common.py
A scripts/audit_rust_blank_lines.py
A scripts/audit_rust_const_blocks.py
A tests/test_rust_audits.py
```
Le collage du terminal intercale le début de la commande de contrôle des headers avant la sortie des tests. Les neuf résultats des tests, les sorties des audits et les commandes Git apparaissent ensuite dans la transcription, sans échec signalé.
## Transition
Appliquer ce history et committer la tranche `alpha.3` avant d'appliquer le delta `alpha.4`. Le commit lui-même n'est pas observé dans la preuve fournie. La suite conforme au plan est la livraison des deux templates `Cargo.toml` et de leur documentation d'adaptation, avec sa propre gate opérateur.

View File

@@ -0,0 +1,144 @@
# file: scripts/_rust_audit_common.py
# version: 1
"""Small shared lexical and file-selection helpers for read-only Rust audits."""
from __future__ import annotations
import pathlib
_IGNORED = frozenset({".git", ".venv", "__pycache__", "target", "builds"})
def selected_files(raw_paths: list[str]) -> tuple[list[pathlib.Path], list[str]]:
"""Select explicit Rust files and directories, without following generated trees."""
files: set[pathlib.Path] = set()
errors: list[str] = []
for raw in raw_paths:
path = pathlib.Path(raw)
if path.is_dir():
files.update(
candidate
for candidate in path.rglob("*.rs")
if candidate.is_file() and not (set(candidate.parts) & _IGNORED)
)
elif path.is_file() and path.suffix == ".rs":
files.add(path)
else:
errors.append(f"unsupported or missing Rust path: {path}")
return sorted(files), errors
def mask_source(text: str) -> tuple[str, set[int]]:
"""Mask comments and literals, preserving offsets and structural punctuation.
Return line numbers whose first character is inside a multiline literal, so
an empty line in a raw string cannot be mistaken for an empty code line.
This is a lexical helper, not a full Rust parser or a compiler substitute.
"""
output = list(text)
protected: set[int] = set()
index = 0
line = 1
state = "code"
block_depth = 0
raw_hashes = 0
while index < len(text):
char = text[index]
nxt = text[index + 1] if index + 1 < len(text) else ""
if char == "\n":
line += 1
if state in {"string", "raw_string"}:
protected.add(line)
if state == "line_comment":
state = "code"
index += 1
continue
if state == "code":
if char == "/" and nxt in {"/", "*"}:
output[index:index + 2] = [" ", " "]
state = "line_comment" if nxt == "/" else "block_comment"
block_depth = 1 if nxt == "*" else 0
index += 2
continue
if char == '"':
output[index] = " "
state = "string"
index += 1
continue
if char == "'":
# Lifetimes have no nearby closing quote; char literals do.
probe = index + 1
escaped = False
found = False
while probe < min(len(text), index + 8) and text[probe] != "\n":
if text[probe] == "'" and not escaped:
found = True
break
escaped = text[probe] == "\\" and not escaped
probe += 1
if found:
output[index] = " "
state = "char"
index += 1
continue
# Also covers r#"..."#, br#"..."# and cr#"..."#.
if char == "r" and (index == 0 or text[index - 1] not in "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_"):
probe = index + 1
while probe < len(text) and text[probe] == "#":
probe += 1
if probe < len(text) and text[probe] == '"':
output[index:probe + 1] = [" "] * (probe + 1 - index)
raw_hashes = probe - index - 1
state = "raw_string"
index = probe + 1
continue
index += 1
continue
if state == "line_comment":
output[index] = " "
elif state == "block_comment":
output[index] = " "
if char == "/" and nxt == "*":
output[index + 1] = " "
block_depth += 1
index += 2
continue
if char == "*" and nxt == "/":
output[index + 1] = " "
block_depth -= 1
if block_depth == 0:
state = "code"
index += 2
continue
elif state in {"string", "char"}:
output[index] = " "
if char == "\\" and index + 1 < len(text):
if text[index + 1] != "\n":
output[index + 1] = " "
index += 2
continue
if char == ('"' if state == "string" else "'"):
state = "code"
elif state == "raw_string":
output[index] = " "
if char == '"' and text.startswith("#" * raw_hashes, index + 1):
output[index + 1:index + 1 + raw_hashes] = [" "] * raw_hashes
index += 1 + raw_hashes
state = "code"
continue
index += 1
return "".join(output), protected
def line_depths(masked: str) -> list[int]:
"""Return brace depth at the start of each line, excluding literals."""
depths: list[int] = []
depth = 0
for line in masked.splitlines():
depths.append(depth)
depth = max(0, depth + line.count("{") - line.count("}"))
return depths

View File

@@ -0,0 +1,83 @@
#!/usr/bin/env python3
# file: scripts/audit_rust_blank_lines.py
# version: 1
"""Audit blank-line spacing in explicitly selected Rust sources."""
from __future__ import annotations
import argparse
import pathlib
import re
import sys
from _rust_audit_common import line_depths, mask_source, selected_files
_BODY = re.compile(r"^\s*(?:(?:pub(?:\([^)]*\))?|async|const|unsafe|extern|default)\s+)*(fn|struct|enum)\b")
def audit_file(path: pathlib.Path) -> list[str]:
"""Check successive empty lines and empty function/struct/enum body lines."""
text = path.read_text(encoding="utf-8")
lines = text.splitlines()
masked, protected = mask_source(text)
masked_lines = masked.splitlines()
depths = line_depths(masked)
errors: list[str] = []
previous_blank = False
pending: tuple[str, int] | None = None
active: tuple[str, int] | None = None
for number, (original, code, depth) in enumerate(zip(lines, masked_lines, depths, strict=True), 1):
blank = not original.strip() and number not in protected
if blank and previous_blank:
errors.append(f"{path}:{number}: consecutive blank Rust source lines")
previous_blank = blank
if active is not None and depth <= active[1]:
active = None
if active is not None and blank:
errors.append(f"{path}:{number}: blank line inside {active[0]} body")
if active is None and pending is None:
match = _BODY.match(code)
if match is not None:
pending = (match.group(1), depth)
if pending is not None:
if "{" in code:
active = pending
pending = None
elif ";" in code:
pending = None
return errors
def main() -> int:
"""Run the read-only spacing audit on explicitly selected Rust files."""
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("paths", nargs="+", help="Rust files or directories to inspect")
args = parser.parse_args()
files, errors = selected_files(args.paths)
if errors or not files:
for error in errors or ["no Rust files selected"]:
print(f"Rust blank-line audit: {error}", file=sys.stderr)
return 2
for path in files:
try:
errors.extend(audit_file(path))
except (OSError, UnicodeError) as error:
print(f"Rust blank-line audit: cannot read {path}: {error}", file=sys.stderr)
return 2
for error in errors:
print(error, file=sys.stderr)
if errors:
print(f"Rust blank-line audit: {len(errors)} error(s) across {len(files)} file(s)", file=sys.stderr)
return 1
print(f"Rust blank-line audit: clean ({len(files)} file(s))")
return 0
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -0,0 +1,109 @@
#!/usr/bin/env python3
# file: scripts/audit_rust_const_blocks.py
# version: 1
"""Audit top-level Rust const blocks for visibility, names and spacing."""
from __future__ import annotations
import argparse
import pathlib
import re
import sys
from _rust_audit_common import line_depths, mask_source, selected_files
_CONST = re.compile(r"^\s*(?:(pub|pub\(crate\))\s+)?const\s+([A-Za-z_][A-Za-z0-9_]*)\s*:")
def _natural_key(value: str) -> tuple[tuple[int, object], ...]:
parts = re.findall(r"[A-Za-z_]+|[0-9]+", value)
return tuple((1, int(part)) if part.isdigit() else (0, part) for part in parts)
def _end_of_const(lines: list[str], start: int) -> int:
"""Find the terminating semicolon outside brackets and initializer braces."""
brackets: list[str] = []
closing = {"}": "{", ")": "(", "]": "["}
for index in range(start, len(lines)):
for char in lines[index]:
if char in "{([":
brackets.append(char)
elif char in "})]" and brackets and brackets[-1] == closing[char]:
brackets.pop()
elif char == ";" and not brackets:
return index
return start
def audit_file(path: pathlib.Path) -> list[str]:
"""Compare adjacent top-level const declarations in homogeneous blocks."""
text = path.read_text(encoding="utf-8")
lines = text.splitlines()
masked, _ = mask_source(text)
code_lines = masked.splitlines()
depths = line_depths(masked)
errors: list[str] = []
previous: tuple[int, int, int, str] | None = None # end, line, visibility rank, name
index = 0
while index < len(lines):
code = code_lines[index]
if depths[index] != 0:
index += 1
continue
match = _CONST.match(code)
if match is not None:
visibility, name = match.groups()
rank = {"pub": 0, "pub(crate)": 1, None: 2}[visibility]
if previous is not None:
end, _, last_rank, last_name = previous
if rank < last_rank:
errors.append(f"{path}:{index + 1}: const visibility must be pub, pub(crate), private")
if rank == last_rank and _natural_key(name) < _natural_key(last_name):
errors.append(f"{path}:{index + 1}: const names out of alphabetical order: {last_name}, {name}")
between = lines[end + 1:index]
if all(not candidate.strip() for candidate in between):
expected = 0 if rank == last_rank else 1
if len(between) != expected:
errors.append(f"{path}:{index + 1}: expected {expected} blank line(s) between const declarations")
end = _end_of_const(code_lines, index)
previous = (end, index + 1, rank, name)
index = end + 1
continue
# Comments, attributes and whitespace attach to the surrounding block.
if code.strip() and not lines[index].lstrip().startswith(("//", "/*", "*", "#[")):
previous = None
index += 1
return errors
def main() -> int:
"""Run the read-only top-level const-block audit."""
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("paths", nargs="+", help="Rust files or directories to inspect")
args = parser.parse_args()
files, errors = selected_files(args.paths)
if errors or not files:
for error in errors or ["no Rust files selected"]:
print(f"Rust const-block audit: {error}", file=sys.stderr)
return 2
for path in files:
try:
errors.extend(audit_file(path))
except (OSError, UnicodeError) as error:
print(f"Rust const-block audit: cannot read {path}: {error}", file=sys.stderr)
return 2
for error in errors:
print(error, file=sys.stderr)
if errors:
print(f"Rust const-block audit: {len(errors)} error(s) across {len(files)} file(s)", file=sys.stderr)
return 1
print(f"Rust const-block audit: clean ({len(files)} file(s))")
return 0
if __name__ == "__main__":
raise SystemExit(main())

137
tests/test_rust_audits.py Normal file
View File

@@ -0,0 +1,137 @@
# file: tests/test_rust_audits.py
# version: 1
"""Targeted contracts for the two optional Rust style audits."""
from __future__ import annotations
import pathlib
import subprocess
import sys
import tempfile
import unittest
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1] / "scripts"))
from audit_rust_blank_lines import audit_file as audit_blanks # noqa: E402
from audit_rust_const_blocks import audit_file as audit_consts # noqa: E402
from _rust_audit_common import selected_files # noqa: E402
class RustAuditTests(unittest.TestCase):
def setUp(self) -> None:
self.temporary = tempfile.TemporaryDirectory()
self.addCleanup(self.temporary.cleanup)
self.root = pathlib.Path(self.temporary.name)
def source(self, text: str) -> pathlib.Path:
path = self.root / "source.rs"
path.write_text(text, encoding="utf-8")
return path
def test_clean_body_and_const_visibility_groups(self) -> None:
path = self.source("""pub const ALPHA: u8 = 1;
pub const BETA: u8 = 2;
pub(crate) const GAMMA: u8 = 3;
const OMEGA: u8 = 4;
fn sample() {
let value = r#"{\n\n}"#;
println!("{value}");
}
""")
self.assertEqual(audit_blanks(path), [])
self.assertEqual(audit_consts(path), [])
def test_blank_body_and_repeated_blank_lines(self) -> None:
path = self.source("""fn sample() {
let value = 1;
println!("{value}");
}
""")
issues = audit_blanks(path)
self.assertTrue(any("consecutive" in issue for issue in issues))
self.assertTrue(any("inside fn body" in issue for issue in issues))
def test_blank_string_and_nested_comment_braces_are_ignored(self) -> None:
path = self.source('''fn sample() {
let data = r#"{
}"#;
/* outer { /* inner } */ outer } */
println!("{data}");
}
''')
self.assertEqual(audit_blanks(path), [])
def test_struct_body_and_tuple_struct(self) -> None:
path = self.source("""struct Payload {
x: u8,
y: u8,
}
struct Pair(u8, u8);
""")
self.assertEqual(len(audit_blanks(path)), 1)
self.assertIn("struct body", audit_blanks(path)[0])
def test_const_order_and_multiline_initializer(self) -> None:
path = self.source("""const BETA: [u8; 2] = [
1,
2,
];
const ALPHA: u8 = 1;
pub const GAMMA: u8 = 3;
""")
issues = audit_consts(path)
self.assertTrue(any("alphabetical" in issue for issue in issues))
self.assertTrue(any("visibility" in issue for issue in issues))
def test_const_spacing(self) -> None:
path = self.source("""pub const ALPHA: u8 = 1;
pub const BETA: u8 = 2;
pub(crate) const GAMMA: u8 = 3;
""")
issues = audit_consts(path)
self.assertEqual(sum("blank line(s)" in issue for issue in issues), 2)
def test_const_function_does_not_join_const_block(self) -> None:
path = self.source("""const ZETA: u8 = 1;
const fn compute() -> u8 {
1
}
const ALPHA: u8 = 2;
""")
self.assertEqual(audit_consts(path), [])
def test_scope_and_missing_path(self) -> None:
self.source("fn sample() {}\n")
selected, errors = selected_files([str(self.root), str(self.root / "absent.rs")])
self.assertEqual(len(selected), 1)
self.assertEqual(len(errors), 1)
def test_cli_status_for_success_violation_and_invalid_selection(self) -> None:
path = self.source("const ALPHA: u8 = 1;\nconst BETA: u8 = 2;\n")
script = pathlib.Path(__file__).resolve().parents[1] / "scripts" / "audit_rust_const_blocks.py"
def run(subject: pathlib.Path) -> subprocess.CompletedProcess[str]:
return subprocess.run([sys.executable, "-B", str(script), str(subject)], capture_output=True, text=True, check=False)
self.assertEqual(run(path).returncode, 0)
path.write_text("const BETA: u8 = 1;\nconst ALPHA: u8 = 2;\n", encoding="utf-8")
self.assertEqual(run(path).returncode, 1)
self.assertEqual(run(self.root / "missing.rs").returncode, 2)
if __name__ == "__main__":
unittest.main()