From c331da5114517e045f7809d3958cec62bffc229d Mon Sep 17 00:00:00 2001 From: SinuS Von SifriduS Date: Fri, 25 Sep 2026 17:41:26 +0200 Subject: [PATCH] 0.0.5-alpha.8 --- VERSION | 2 +- deltas/0.0.5/alpha.8.md | 248 +++++++++++++++++++ docs/plans/005-V0_0_5_RECONCILIATION_PLAN.md | 16 +- docs/rules/510-RUST_CARGO.md | 19 +- docs/skeletons/002-RUST_CARGO_WORKSPACE.md | 17 +- docs/studies/001-KSP_GAMES_RECONCILIATION.md | 19 +- docs/validation/001-VALIDATION_MATRIX.md | 6 +- docs/validation/002-RUST_STYLE_AUDITS.md | 73 ++++-- history/0.0.5/alpha.8.md | 125 ++++++++++ scripts/audit_rust_item_spacing.py | 168 +++++++++++++ scripts/audit_rust_module_order.py | 87 +++++++ scripts/audit_rust_reexport_blocks.py | 88 +++++++ scripts/audit_rust_trait_import_blocks.py | 74 ++++++ tests/test_rust_audits.py | 127 +++++++++- 14 files changed, 1034 insertions(+), 35 deletions(-) create mode 100644 deltas/0.0.5/alpha.8.md create mode 100644 history/0.0.5/alpha.8.md create mode 100755 scripts/audit_rust_item_spacing.py create mode 100755 scripts/audit_rust_module_order.py create mode 100755 scripts/audit_rust_reexport_blocks.py create mode 100755 scripts/audit_rust_trait_import_blocks.py diff --git a/VERSION b/VERSION index 6e97c5e..70c89dd 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.0.5-alpha.7.fix.1 +0.0.5-alpha.8 diff --git a/deltas/0.0.5/alpha.8.md b/deltas/0.0.5/alpha.8.md new file mode 100644 index 0000000..411d4b1 --- /dev/null +++ b/deltas/0.0.5/alpha.8.md @@ -0,0 +1,248 @@ + + + +# Delta 0.0.5-alpha.8 — formatage Rust structurel et audits monotâches + +## Baseline et version + +Baseline attendue : commit opérateur complet de `0.0.5-alpha.7.fix.1`, réalisé après application de `history/0.0.5/alpha.7.fix.1.md` avec statut `PASS`. + +Version après application : + +```text +0.0.5-alpha.8 +``` + +## Mission + +Fermer le chantier de réconciliation du formatage Rust structurel sans importer les anciens `audit_rust_general_rules.py` composites de KSP/Games. + +La tranche attribue chaque propriété à un owner identifiable : Rustfmt lorsqu'il sait réellement réécrire la forme, audits PGR monotâches lorsqu'une structure stable n'est pas possédée par l'outil officiel, et revue humaine lorsque l'ordre peut être métier/protocolaire. + +## Scope + +REPLACE : + +```text +VERSION +docs/plans/005-V0_0_5_RECONCILIATION_PLAN.md +docs/rules/510-RUST_CARGO.md +docs/skeletons/002-RUST_CARGO_WORKSPACE.md +docs/studies/001-KSP_GAMES_RECONCILIATION.md +docs/validation/001-VALIDATION_MATRIX.md +docs/validation/002-RUST_STYLE_AUDITS.md +tests/test_rust_audits.py +``` + +ADD : + +```text +deltas/0.0.5/alpha.8.md +scripts/audit_rust_item_spacing.py +scripts/audit_rust_module_order.py +scripts/audit_rust_reexport_blocks.py +scripts/audit_rust_trait_import_blocks.py +``` + +DELETE : aucun. + +## Décisions de formatage + +Pour une nouvelle baseline Rust contrôlée sélectionnant le profil strict : + +- les lignes vides consécutives et les lignes vides dans fonctions/méthodes/`struct`/`enum` sont interdites ; +- deux items Rust distincts voisins d'un même scope sont séparés par exactement une ligne vide lorsque rien d'intermédiaire ne possède lui-même l'espacement ; +- les blocs compacts `const` restent sous un owner séparé et ne sont pas interprétés par l'audit d'espacement général ; +- les modules de production précèdent les modules de tests et chaque groupe est alphabétique lorsque cet ordre est sans sémantique ; +- les `use` non exportés autorisés par le profil strict restent dans un bloc module-level compact avant les déclarations métier ; +- les `pub use` précèdent les `pub(crate) use`, sans ligne vide interne et avec exactement une ligne vide entre les deux visibilités lorsqu'elles coexistent ; +- Rustfmt reste l'owner du tri intra-bloc des `use`, réexports et modules qu'il sait réécrire ; les audits PGR ne créent pas un tri concurrent. + +PGR conserve comme fortes conventions de lisibilité le rapprochement d'un `struct` et de ses `impl`, ainsi que la préférence des éléments visibles avant les helpers privés lorsque cela clarifie le fichier. Aucun audit lexical n'impose cependant un ordre global `enum → struct → impl → fn` ou un tri visible/privé susceptible d'écraser une organisation métier. + +## Audits ajoutés + +Quatre contrôles read-only monotâches sont extraits : + +```text +audit_rust_item_spacing.py + séparation exacte des items distincts voisins + +audit_rust_module_order.py + ordre production/tests et ordre alphabétique des modules + +audit_rust_trait_import_blocks.py + placement/contiguïté des use non exportés + +audit_rust_reexport_blocks.py + ordre de visibilité et espacement des blocs de réexports +``` + +Ils utilisent `_rust_audit_common.py` pour la sélection et le masquage lexical. Ils ne tentent pas de remplacer un parser Rust ni de déterminer la nature sémantique d'un import. + +Les audits déjà existants gardent leurs responsabilités : + +```text +audit_rust_blank_lines.py + blancs consécutifs / blancs dans certains corps + +audit_rust_const_blocks.py + consts sélectionnées : visibilité, ordre, espacement + +audit_rust_export_completeness.py + façade crate-root et chemins canoniques +``` + +## Mesure croisée + +Les quatre nouveaux audits ont été exécutés pendant la préparation sur tous les fichiers Rust sous `crates/` des baselines autoritaires : + +```text +KSP 0.3.16 603 file(s) item/module/trait-import/reexport: clean +Games 0.3.5 87 file(s) item/module/trait-import/reexport: clean +``` + +`audit_rust_blank_lines.py` est également propre sur ces `603` et `87` fichiers. + +En revanche, `audit_rust_const_blocks.py` exécuté aveuglément sur l'ensemble produit `18` signaux KSP et `0` Games. Certains signaux correspondent à des tables/schémas à ordre intentionnel. Il reste donc un audit à sélection explicite par fichier et ne devient pas une gate globale. + +Ces mesures servent à qualifier la réutilisabilité des règles PGR ; elles ne constituent pas une validation opérateur de KSP/Games. + +## Horizon documenté sans engagement + +La revue d'horizon du plan conserve `0.1.0` comme seule prochaine release engagée, mais mémorise trois candidats sans numéro ni engagement : + +1. profils techniques composables par langage/framework/target/plateforme et préoccupation transverse, notamment observability/logging ; +2. profil documentaire/spécification, avec `saselang-bible` comme corpus pilote possible ; +3. audit de généricité de PGR pour des projets hors développement logiciel. + +Ces candidats ne deviennent ni `Study`, ni `ROADMAP`, ni release plan tant qu'une décision explicite ne les promeut pas. + +## Hors scope + +Cette tranche ne : + +- crée pas de parser Rust général ; +- réimporte pas `audit_rust_general_rules.py` ; +- impose pas un ordre alphabétique à une séquence sémantique ; +- crée pas d'audit dédié `static/type` uniquement pour la symétrie ; +- modifie pas les contrats de suffixes/packaging/tracing fermés par `alpha.7.fix.1` ; +- commence pas le skeleton Tauri réservé à `alpha.9` ; +- transforme pas les candidats d'horizon en versions promises. + +## Validation opérateur + +Après application du history `PASS` de `alpha.7.fix.1`, commit/push de cette tranche, puis application du présent delta : + +```bash +test "$(cat VERSION)" = '0.0.5-alpha.8' +test -f history/0.0.5/alpha.7.fix.1.md +test -f deltas/0.0.5/alpha.8.md +test -f scripts/audit_rust_item_spacing.py +test -f scripts/audit_rust_module_order.py +test -f scripts/audit_rust_reexport_blocks.py +test -f scripts/audit_rust_trait_import_blocks.py +test ! -e scripts/audit_rust_general_rules.py +test ! -e history/0.0.5/alpha.8.md + +grep -A2 '^## Statut$' history/0.0.5/alpha.7.fix.1.md | grep -Fx 'PASS' +grep -Fx '' docs/rules/510-RUST_CARGO.md +grep -Fx '' docs/skeletons/002-RUST_CARGO_WORKSPACE.md +grep -Fx '' docs/studies/001-KSP_GAMES_RECONCILIATION.md +grep -Fx '' docs/validation/001-VALIDATION_MATRIX.md +grep -Fx '' docs/validation/002-RUST_STYLE_AUDITS.md +grep -Fx '' docs/plans/005-V0_0_5_RECONCILIATION_PLAN.md +grep -Fx '# version: 2' tests/test_rust_audits.py +grep -Fx '# version: 1' scripts/audit_rust_item_spacing.py +grep -Fx '# version: 1' scripts/audit_rust_module_order.py +grep -Fx '# version: 1' scripts/audit_rust_reexport_blocks.py +grep -Fx '# version: 1' scripts/audit_rust_trait_import_blocks.py + +python3 -B -m unittest discover -s tests -p 'test_rust_audits.py' -v +python3 -B -m unittest discover -s tests -v + +python3 -B scripts/audit_markdown_headers.py \ + README.md RULES.md ROADMAP.md CHANGELOG.md docs deltas history prompts +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 + +history = Path("history/0.0.5/alpha.7.fix.1.md").read_text(encoding="utf-8") +assert re.search(r"## Statut\s+PASS\s", history) + +rules = Path("docs/rules/510-RUST_CARGO.md").read_text(encoding="utf-8") +for phrase in ( + "Le profil structurel strict fait partie du défaut des nouvelles baselines Rust contrôlées.", + "audit_rust_item_spacing.py", + "audit_rust_module_order.py", + "audit_rust_trait_import_blocks.py", + "audit_rust_reexport_blocks.py", + "PGR n'impose toutefois pas un ordre global automatique `enum → struct → impl → fn`", +): + assert phrase in rules, phrase + +validation = Path("docs/validation/002-RUST_STYLE_AUDITS.md").read_text(encoding="utf-8") +for phrase in ( + "603` fichiers KSP `0.3.16`", + "87` fichiers Games `0.3.5`", + "18` signaux KSP", + "0` Games", + "Rustfmt reste l'owner de l'ordre intra-bloc", +): + assert phrase in validation, phrase + +plan = Path("docs/plans/005-V0_0_5_RECONCILIATION_PLAN.md").read_text(encoding="utf-8") +for phrase in ( + "quatre extractions monotâches complètent le profil", + "maturation de profils techniques composables", + "`saselang-bible` comme corpus pilote possible", + "audit du noyau PGR hors développement logiciel", + "### alpha.9 — décision de skeleton Tauri pour projets contrôlés", +): + assert phrase in plan, phrase + +print("0.0.5 alpha.8 Rust structure audit: clean (owners split, four monotask audits, semantic order protected, horizon candidates recorded)") +PY + +git diff --check + +git add VERSION \ + docs/plans/005-V0_0_5_RECONCILIATION_PLAN.md \ + docs/rules/510-RUST_CARGO.md \ + docs/skeletons/002-RUST_CARGO_WORKSPACE.md \ + docs/studies/001-KSP_GAMES_RECONCILIATION.md \ + docs/validation/001-VALIDATION_MATRIX.md \ + docs/validation/002-RUST_STYLE_AUDITS.md \ + scripts/audit_rust_item_spacing.py \ + scripts/audit_rust_module_order.py \ + scripts/audit_rust_reexport_blocks.py \ + scripts/audit_rust_trait_import_blocks.py \ + tests/test_rust_audits.py \ + deltas/0.0.5/alpha.8.md + +git diff --cached --check +git status --short +``` + +La sortie Git attendue avant ajout du history est exactement : + +```text +M VERSION +A deltas/0.0.5/alpha.8.md +M docs/plans/005-V0_0_5_RECONCILIATION_PLAN.md +M docs/rules/510-RUST_CARGO.md +M docs/skeletons/002-RUST_CARGO_WORKSPACE.md +M docs/studies/001-KSP_GAMES_RECONCILIATION.md +M docs/validation/001-VALIDATION_MATRIX.md +M docs/validation/002-RUST_STYLE_AUDITS.md +A scripts/audit_rust_item_spacing.py +A scripts/audit_rust_module_order.py +A scripts/audit_rust_reexport_blocks.py +A scripts/audit_rust_trait_import_blocks.py +M tests/test_rust_audits.py +``` diff --git a/docs/plans/005-V0_0_5_RECONCILIATION_PLAN.md b/docs/plans/005-V0_0_5_RECONCILIATION_PLAN.md index 30e751b..0c116a9 100644 --- a/docs/plans/005-V0_0_5_RECONCILIATION_PLAN.md +++ b/docs/plans/005-V0_0_5_RECONCILIATION_PLAN.md @@ -1,5 +1,5 @@ - + # Plan 0.0.5 — réconciliation réelle avec KSP et sasedev-games @@ -120,9 +120,11 @@ La gate mécanique `alpha.7` est propre, mais la revue fonctionnelle post-gate m ### alpha.8 — formatage Rust structurel et audits monotâches -Réconcilier les règles KSP de blocs homogènes, visibilité, ordre alphabétique, espacement, modules et réexports avec les conventions plus légères de Games. Décider précisément ce qui appartient à `rustfmt`, aux audits PGR existants (`blank_lines`, `const_blocks`) ou à un nouvel audit monotâche justifié. +La réconciliation des anciens audits composites KSP/Games attribue désormais chaque propriété à un owner précis. Rustfmt reste propriétaire du formatage et du tri qu'il sait réellement réécrire dans un bloc contigu. Les audits PGR existants conservent `blank_lines` et `const_blocks`; quatre extractions monotâches complètent le profil : `item_spacing`, `module_order`, `trait_import_blocks` et `reexport_blocks`. -La tranche ne doit pas créer un parser Rust général ni imposer un ordre alphabétique lorsqu'un ordre métier/protocolaire est sémantique. Elle peut conclure qu'aucun nouveau script n'est nécessaire pour une propriété déjà correctement possédée. +La mesure sur l'ensemble des sources `crates/**/*.rs` est propre pour les quatre nouveaux audits dans KSP `0.3.16` (`603` fichiers) et Games `0.3.5` (`87` fichiers). `blank_lines` est également propre sur ces corpus. `const_blocks` reste explicitement opt-in : son exécution globale remonte `18` signaux KSP, dont des tables/schémas à ordre intentionnel, contre `0` Games. + +Le profil strict des nouvelles baselines contrôlées retient donc les règles mécaniques de lignes vides, séparation exacte des items distincts, modules production/tests, bloc de traits et blocs de réexports. PGR n'automatise pas un ordre global `enum → struct → impl → fn`, ni un tri visible/privé lorsque la structure métier peut être plus informative. Aucun parser Rust général ni audit composite n'est introduit. ### alpha.9 — décision de skeleton Tauri pour projets contrôlés @@ -167,6 +169,14 @@ Pour PGR, la prochaine release engagée est `0.1.0 — première baseline réuti Ses sorties autorisées seront : étendre la roadmap avec des objectifs réellement décidés, maintenir l'horizon ouvert avec un prochain déclencheur, différer/mettre en pause la trajectoire ou constater qu'un objectif macro est accompli. +Trois **candidats d'horizon**, sans numéro de release réservé ni engagement de réalisation, sont maintenant identifiés pour cette revue : + +1. maturation de profils techniques composables par langage, framework, target/plateforme et préoccupation transverse — par exemple observability/logging, Web/TypeScript/Vite/Sass, Android, WASM, networking ou persistence — toujours à partir de corpus réels avant normalisation ; +2. profil de projet documentaire/spécification, avec `saselang-bible` comme corpus pilote possible pour tester gouvernance normative, terminologie, décisions, exemples, compatibilité et traduction lorsque la documentation est elle-même le produit ; +3. audit du noyau PGR hors développement logiciel afin d'identifier ce qui est réellement universel et ce qui doit rester dans un profil `software`, sans inventer les règles métier de domaines spécialisés. + +Ces sujets restent au stade `horizon candidat`. Ils ne deviennent `Study`, `ROADMAP` ou plan de release qu'après décision explicite selon `300-DOCUMENTATION.md`. + ## Points de décision Un REPLAN est requis si : diff --git a/docs/rules/510-RUST_CARGO.md b/docs/rules/510-RUST_CARGO.md index dffd44e..aa47f15 100644 --- a/docs/rules/510-RUST_CARGO.md +++ b/docs/rules/510-RUST_CARGO.md @@ -1,5 +1,5 @@ - + # Profil Rust / Cargo @@ -220,12 +220,23 @@ 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é. -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. +Le profil structurel strict fait partie du défaut des nouvelles baselines Rust contrôlées. Un projet externe, historique ou imposé peut documenter un override borné ; cet override ne réduit pas le profil canonique des nouveaux projets. L'owner reste partagé entre `rustfmt` pour ce qu'il sait réécrire de manière fiable et des audits read-only monotâches pour les propriétés de structure qu'il ne possède pas. -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. +Les lignes vides consécutives MUST être supprimées. Aucune ligne vide n'est conservée à l'intérieur du corps d'une fonction ou méthode, ni à l'intérieur d'une définition `struct` ou `enum`. `scripts/audit_rust_blank_lines.py` contrôle ces deux propriétés sur les fichiers Rust sélectionnés. -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. +Deux items Rust distincts et voisins d'un même scope — fonctions/méthodes, `struct`, `enum`, `union`, `trait`, blocs `impl` ou changements de catégorie/visibilité — MUST être séparés par exactement une ligne vide lorsqu'aucune syntaxe intermédiaire ne possède elle-même l'espacement. `scripts/audit_rust_item_spacing.py` contrôle cette séparation sans tenter d'interpréter les macros ou de posséder les blocs compacts `const`/`static`/`type`. +Les déclarations compactes d'un même type et d'une même visibilité SHOULD former un bloc sans ligne vide lorsque l'ordre n'a pas de signification sémantique. Pour les constantes de module sélectionnées, l'ordre canonique est `pub`, `pub(crate)`, privé, puis le nom dans chaque visibilité ; exactement une ligne vide sépare deux niveaux de visibilité. `scripts/audit_rust_const_blocks.py` possède uniquement ce contrat `const` au niveau racine. Il MUST NOT être appliqué aveuglément à une table de protocole, une séquence de migrations, un schéma, une table de dispatch ou toute autre série dont l'ordre est contractuel. Les blocs `static`/`type` peuvent suivre la même présentation par convention locale, mais PGR ne leur ajoute pas encore un audit dédié uniquement pour atteindre une symétrie théorique. + +Les déclarations `mod` de production d'un même fichier MUST précéder les modules de tests conditionnels ; les deux groupes sont ordonnés alphabétiquement lorsque l'ordre ne porte aucune sémantique locale. `scripts/audit_rust_module_order.py` contrôle cette propriété. `rustfmt` reste propriétaire de l'ordre qu'il réécrit déjà à l'intérieur d'un bloc contigu. + +Les `use` non exportés autorisés par le profil strict — donc normalement des imports de traits — restent au niveau du module, avant les déclarations Rust métier et dans un bloc contigu sans ligne vide interne. `scripts/audit_rust_trait_import_blocks.py` contrôle uniquement cette disposition ; il ne prétend pas déterminer lexicalement si le symbole importé est effectivement un trait. L'ordre intra-bloc reste celui de `cargo fmt --all`. + +Au crate-root, les réexports `pub use` constituent un bloc avant les `pub(crate) use`. Aucune ligne vide n'est admise à l'intérieur d'un bloc de même visibilité et exactement une ligne vide sépare les deux niveaux lorsqu'ils existent tous deux. `scripts/audit_rust_reexport_blocks.py` contrôle cette structure sans imposer un second tri : l'ordre intra-bloc reste celui produit par Rustfmt. Les rustdocs adjacentes restent possédées par les règles d'API, pas par cet audit de forme. + +Lorsqu'un `struct` et ses `impl` forment une unité locale, les `impl` SHOULD suivre la structure ; de même, les éléments visibles SHOULD généralement précéder les helpers privés lorsque cela rend l'API plus lisible. PGR n'impose toutefois pas un ordre global automatique `enum → struct → impl → fn` : un ordre métier, protocolaire ou pédagogique peut être plus important. Cette propriété reste une revue tant qu'un audit fiable ne peut pas distinguer la structure informative d'un ordre sémantique. + +Les scripts PGR restent des analyseurs lexicaux ciblés, pas un parser Rust général. Une macro complexe, un item généré ou une syntaxe nouvelle peut nécessiter une revue humaine ou l'owner Rust officiel. Une règle déjà possédée par `rustfmt`, rustc ou Clippy MUST NOT être réimplémentée uniquement pour multiplier les gates. ## Configuration Cargo et artefacts générés `.cargo/config.toml` est le propriétaire naturel d'une configuration Cargo partagée par le projet lorsqu'une telle configuration est nécessaire. diff --git a/docs/skeletons/002-RUST_CARGO_WORKSPACE.md b/docs/skeletons/002-RUST_CARGO_WORKSPACE.md index 9a97a09..c2fcaaf 100644 --- a/docs/skeletons/002-RUST_CARGO_WORKSPACE.md +++ b/docs/skeletons/002-RUST_CARGO_WORKSPACE.md @@ -1,5 +1,5 @@ - + # Skeleton — Rust/Cargo workspace @@ -74,8 +74,9 @@ Avant de considérer la specialization comme instanciée, résoudre explicitemen 9. l'emplacement des artefacts Cargo et l'identifiant unique utilisé dans `.cargo/config.toml` si cette politique externe au dépôt est retenue ; 10. les frontières `-api` / `-lib` réellement justifiées, les packages exécutables distincts de leurs bibliothèques et toute exception framework à cette séparation ; 11. l'ownership du logging/tracing : simple exécutable local ou crate transverse commune lorsque plusieurs runtimes partagent subscriber, sinks ou policy ; -12. les exceptions éventuelles au profil Rust strict ; pour une nouvelle baseline contrôlée, la façade crate-root et les chemins explicites sont sélectionnés par défaut, tandis que les audits de style supplémentaires restent décidés séparément ; -13. les validations Cargo réellement nécessaires au code et aux manifests créés. +12. les exceptions éventuelles au profil Rust strict ; pour une nouvelle baseline contrôlée, la façade crate-root, les chemins explicites et les conventions structurelles sans ordre métier sont sélectionnés par défaut ; +13. les fichiers pour lesquels un ordre mécanique des blocs `const` est réellement applicable, sans englober les tables/protocoles/schémas ordonnés ; +14. les validations Cargo réellement nécessaires au code et aux manifests créés. Les valeurs `0.1.0`, `2024`, `project-name`, `project-core-lib`, les seuils Clippy et les chemins de build présents dans les templates sont des exemples ou baselines à revoir. Ils ne deviennent jamais des valeurs universelles par simple copie. @@ -133,13 +134,15 @@ L'édition de `rustfmt.toml` doit rester cohérente avec le workspace. Les seuil Décider explicitement si `Cargo.lock` est versionné selon le type de produit, le mode de publication et le besoin de reproductibilité. La présence ou l'absence du lockfile dans un projet source historique ne constitue pas une règle suffisante pour le nouveau workspace. -### Audits Rust facultatifs +### Audits Rust ciblés -Si le projet adopte les conventions documentées dans [`../validation/002-RUST_STYLE_AUDITS.md`](../validation/002-RUST_STYLE_AUDITS.md), copier les scripts de style concernés et adapter leurs headers à leurs chemins locaux. La sélection des fichiers doit rester explicite. En particulier, l'audit des blocs de constantes ne doit jamais réordonner une table de protocole, une séquence de migrations ou toute autre structure dont l'ordre possède une sémantique propre. +Pour une nouvelle baseline contrôlée qui adopte le profil strict, [`../validation/002-RUST_STYLE_AUDITS.md`](../validation/002-RUST_STYLE_AUDITS.md) fournit des contrôles monotâches pour les propriétés structurelles que Rustfmt ne possède pas complètement : lignes vides, séparation des items, ordre des modules, bloc d'imports de traits et blocs de réexports. Copier seulement les scripts réellement retenus et adapter leurs headers aux chemins locaux. -Pour une nouvelle baseline contrôlée, le contrat de façade décrit dans [`../validation/003-RUST_EXPORT_COMPLETENESS.md`](../validation/003-RUST_EXPORT_COMPLETENESS.md) appartient au profil strict ; `audit_rust_export_completeness.py` avec `_rust_audit_common.py` SHOULD être adopté afin de contrôler les propriétés que `rustc` et Clippy ne possèdent pas. Une adoption d'un projet existant SHOULD commencer en `--report-only`, résoudre les écarts ou exceptions locales, puis seulement rendre l'audit bloquant. +L'audit `audit_rust_const_blocks.py` reste à sélection explicite par fichier : il ne doit jamais réordonner une table de protocole, une séquence de migrations, un schéma ou toute structure dont l'ordre possède une sémantique propre. Les blocs `static`/`type` peuvent reprendre localement la même présentation sans qu'un audit supplémentaire soit créé par symétrie. -Les audits de style de `002-RUST_STYLE_AUDITS.md` restent séparés et ne sont copiés que lorsque leurs conventions sont retenues. Aucun script n'est copié uniquement parce qu'il existe dans PGR. +Le contrat de façade décrit dans [`../validation/003-RUST_EXPORT_COMPLETENESS.md`](../validation/003-RUST_EXPORT_COMPLETENESS.md) appartient également au profil strict ; `audit_rust_export_completeness.py` avec `_rust_audit_common.py` SHOULD être adopté afin de contrôler les propriétés que `rustc` et Clippy ne possèdent pas. Une adoption d'un projet existant SHOULD commencer en `--report-only`, résoudre les écarts ou exceptions locales, puis seulement rendre l'audit bloquant. + +Aucun script n'est copié uniquement parce qu'il existe dans PGR. Si un autre owner fiable possède déjà la propriété ou si l'ordre est sémantique, la revue ou l'outil officiel reste préférable. ## Validation initiale diff --git a/docs/studies/001-KSP_GAMES_RECONCILIATION.md b/docs/studies/001-KSP_GAMES_RECONCILIATION.md index 61c121a..2cb5eca 100644 --- a/docs/studies/001-KSP_GAMES_RECONCILIATION.md +++ b/docs/studies/001-KSP_GAMES_RECONCILIATION.md @@ -1,5 +1,5 @@ - + # Étude de réconciliation PGR / KSP / sasedev-games @@ -232,6 +232,23 @@ La décision PGR est donc : Cette décision est cohérente avec le principe PGR historique qui distingue déjà version canonique, miroirs techniques, compteurs et versions de schéma au lieu d'exiger `tous les champs == VERSION`. +#### Décision `alpha.8` — formatage structurel et audits monotâches + +La comparaison des `audit_rust_general_rules.py` KSP/Games confirme que leur grand script mélangeait plusieurs owners qui peuvent être séparés proprement. PGR possédait déjà `blank_lines` et `const_blocks`; la tranche extrait quatre propriétés supplémentaires sans copier les contrôles de headers, FFI, rustdoc, façade ou imports déjà possédés ailleurs. + +Les nouveaux audits ciblés sont : + +- `item_spacing` : exactement une ligne vide entre items distincts voisins d'un même scope ; les blocs compacts `const/static/type` homogènes sont délégués afin d'éviter les faux positifs sémantiques ; +- `module_order` : modules de production avant les modules de tests, ordre alphabétique de chaque groupe ; +- `trait_import_blocks` : `use` non exportés au niveau module, avant les déclarations et sans ligne vide interne ; la nature réelle du trait n'est pas inférée lexicalement ; +- `reexport_blocks` : bloc `pub use` avant `pub(crate) use`, aucun blanc interne et exactement un blanc entre niveaux ; Rustfmt reste owner du tri intra-bloc. + +La mesure croisée sur tous les fichiers `crates/**/*.rs` donne `603` fichiers KSP et `87` Games. Les quatre nouveaux audits sont propres sur les deux corpus, de même que `blank_lines`. L'audit `const_blocks` produit en revanche `18` signaux KSP lorsqu'il est appliqué aveuglément et `0` Games ; certains signaux KSP correspondent à des tables ou schémas dont l'ordre est intentionnel. Cette différence confirme que `const_blocks` doit rester opt-in sur des fichiers explicitement sélectionnés. + +PGR ne reprend pas automatiquement `RUST-FMT-110` du script KSP (item après `return` comme suspicion d'accolade déplacée), ni un ordre global `enum → struct → impl → fn`, ni l'idée que tous les éléments visibles doivent mécaniquement précéder les privés. Ces propriétés peuvent dépendre de la structure métier ; un audit lexical général créerait plus de faux positifs qu'il n'apporterait de preuve. La convention `struct` suivi de ses `impl` reste une forte préférence de lisibilité, pas un tri universel automatisé. + +Cette extraction ferme le besoin identifié pour `alpha.8` sans créer de parser Rust général. + ### Web / Vite / WASM Les corpus et la clarification opérateur permettent désormais de séparer trois choses : diff --git a/docs/validation/001-VALIDATION_MATRIX.md b/docs/validation/001-VALIDATION_MATRIX.md index 0f15895..1fc8dc4 100644 --- a/docs/validation/001-VALIDATION_MATRIX.md +++ b/docs/validation/001-VALIDATION_MATRIX.md @@ -1,5 +1,5 @@ - + # Matrice de validation PGR @@ -37,9 +37,9 @@ PGR préfère plusieurs audits simples avec une responsabilité identifiable à ## 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`. +[`002-RUST_STYLE_AUDITS.md`](002-RUST_STYLE_AUDITS.md) décrit les audits monotâches de structure Rust : lignes vides, blocs `const`, séparation des items, ordre des modules, disposition des imports de traits et blocs de réexports. 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 -v`. -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. +Pour une nouvelle baseline contrôlée, les propriétés sans ordre métier peuvent faire partie du profil strict. L'audit `const_blocks` reste explicitement sélectionné, car une table de protocole, un schéma ou une séquence normative ne doit jamais être réordonné pour satisfaire un tri mécanique. Leur disponibilité ne transforme pas PGR en workspace Rust ; le projet propriétaire exécute ces audits après `cargo fmt`, et Rustfmt/Clippy/rustc conservent les contrôles qu'ils possèdent. [`003-RUST_EXPORT_COMPLETENESS.md`](003-RUST_EXPORT_COMPLETENESS.md) décrit séparément `audit_rust_export_completeness.py`, audit facultatif de façade crate-root. Sa validation ciblée dans PGR est `python3 -B -m unittest discover -s tests -p test_rust_export_completeness.py -v`. L'audit est destiné aux projets Rust qui choisissent explicitement ce contrat ; PGR n'exécute pas ce script sur son propre dépôt comme gate, car il ne constitue pas lui-même un workspace Rust. diff --git a/docs/validation/002-RUST_STYLE_AUDITS.md b/docs/validation/002-RUST_STYLE_AUDITS.md index b33f45b..4ee6660 100644 --- a/docs/validation/002-RUST_STYLE_AUDITS.md +++ b/docs/validation/002-RUST_STYLE_AUDITS.md @@ -1,34 +1,79 @@ - + -# Audits facultatifs de style Rust +# Audits monotâches de structure 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). +Les sources KSP `0.3.16` et Games `0.3.5` possèdent chacune un `audit_rust_general_rules.py` composite. `0.0.3` avait déjà extrait deux propriétés ciblées ; `0.0.5-alpha.8` termine la réconciliation des propriétés de forme communes sans importer cet orchestrateur monolithique ni ses règles locales. -`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. +Le contrat normatif est dans [`510-RUST_CARGO.md`](../rules/510-RUST_CARGO.md). Les scripts disponibles sont read-only et chacun possède une seule famille de preuve : + +```text +audit_rust_blank_lines.py + lignes vides consécutives et lignes vides dans fn/méthode/struct/enum + +audit_rust_const_blocks.py + ordre visibilité/nom et espacement des blocs const explicitement sélectionnés + +audit_rust_item_spacing.py + exactement une ligne vide entre items Rust distincts voisins d'un même scope + +audit_rust_module_order.py + modules de production avant tests, ordre alphabétique de chaque groupe + +audit_rust_trait_import_blocks.py + bloc de use non exportés au module, avant les déclarations et sans ligne vide interne + +audit_rust_reexport_blocks.py + bloc pub use avant pub(crate) use, espacement exact entre/à l'intérieur des blocs +``` + +`scripts/_rust_audit_common.py` partage seulement la sélection de fichiers et le masquage lexical des commentaires/littéraux. Il ne définit aucune politique. `audit_rust_export_completeness.py` reste séparé dans [`003-RUST_EXPORT_COMPLETENESS.md`](003-RUST_EXPORT_COMPLETENESS.md), car il possède la façade et les chemins canoniques plutôt que le formatage. ## 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 : +Pour une nouvelle baseline Rust contrôlée qui sélectionne le profil strict, les audits sans interprétation métier peuvent être exécutés sur les sources concernées après Rustfmt : ```bash cargo fmt --all -- --check -python3 -B scripts/audit_rust_blank_lines.py crates/example/src +python3 -B scripts/audit_rust_blank_lines.py crates +python3 -B scripts/audit_rust_item_spacing.py crates +python3 -B scripts/audit_rust_module_order.py crates +python3 -B scripts/audit_rust_trait_import_blocks.py crates +python3 -B scripts/audit_rust_reexport_blocks.py crates +``` + +L'audit des constantes reste différent : sélectionner seulement les fichiers dont les blocs `const` doivent réellement suivre l'ordre mécanique PGR. Exemple : + +```bash 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. +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 sources ni l'arborescence de build. Le code de sortie est `0` si propre, `1` pour violation et `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. +`audit_rust_trait_import_blocks.py` ne prouve pas qu'un `use` importe réellement un trait ; cette sémantique appartient au profil et à la revue/compilation. `audit_rust_reexport_blocks.py` ne trie pas les symboles exportés. Dans les deux cas, Rustfmt reste l'owner de l'ordre intra-bloc lorsque les lignes sont contiguës. -## Limites mesurées +`audit_rust_item_spacing.py` ne possède pas l'espacement des blocs compacts homogènes `const`/`static`/`type` : il les délègue afin de ne pas transformer une séparation sémantique entre deux grandes tables en faux positif. L'audit spécialisé `const_blocks` reste opt-in sur des fichiers explicitement choisis. -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. +## Mesure croisée KSP / Games -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. +La prévalidation `alpha.8` a exécuté les quatre nouveaux audits sur l'ensemble des fichiers Rust sous `crates/` des archives fournies : `603` fichiers KSP `0.3.16` et `87` fichiers Games `0.3.5`. Les quatre contrôles sont propres sur les deux corpus : -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. +```text + KSP 0.3.16 Games 0.3.5 +item spacing clean / 603 clean / 87 +module order clean / 603 clean / 87 +trait import blocks clean / 603 clean / 87 +re-export blocks clean / 603 clean / 87 +``` + +L'audit des lignes vides est également propre sur les `603` et `87` fichiers. En revanche, l'audit `const_blocks` appliqué aveuglément à tout le corpus produit `18` signaux KSP et `0` Games. Certains signaux KSP concernent des tables/schémas dont l'ordre est intentionnel, ce qui confirme qu'il ne doit pas devenir une gate globale par défaut. Ces mesures sont des preuves de réconciliation PGR, pas une validation opérateur de KSP/Games. + +## Limites et owners + +Les analyses masquent les commentaires et littéraux courants, y compris chaînes brutes et commentaires imbriqués, mais ne constituent pas un parseur Rust. Elles n'essaient pas de comprendre les expansions de macros, l'ordre métier, les protocoles ou les dépendances sémantiques entre items. + +PGR ne crée pas d'audit automatique pour imposer un ordre global des catégories `enum`, `struct`, `impl`, fonctions ou helpers privés. Le rapprochement `struct` puis `impl`, et la préférence des éléments visibles avant les helpers privés, restent des conventions de lisibilité soumises à la sémantique locale. + +Rustfmt possède le formatage et les tris qu'il sait réellement réécrire ; rustc et Clippy conservent leurs diagnostics. Les tests PGR de ces scripts vivent dans `tests/test_rust_audits.py`. diff --git a/history/0.0.5/alpha.8.md b/history/0.0.5/alpha.8.md new file mode 100644 index 0000000..a2e362a --- /dev/null +++ b/history/0.0.5/alpha.8.md @@ -0,0 +1,125 @@ + + + +# History 0.0.5-alpha.8 — formatage Rust structurel et audits monotâches + +## Statut + +PASS + +La gate opérateur de `0.0.5-alpha.8` exécutée le `2026-09-25` valide la fermeture du chantier de formatage Rust structurel : les quatre audits monotâches ajoutés sont couverts par leurs tests, la suite globale PGR est propre, les audits Markdown passent et le contrôle structurel confirme la séparation des owners ainsi que la conservation des ordres sémantiques hors automatisation. + +Aucun correctif `alpha.8` n'est requis. + +## Baseline et version + +La tranche part du commit opérateur de `0.0.5-alpha.7.fix.1` : + +```text +ecd9de4 0.0.5-alpha.7.fix.1 +``` + +Le push vers `origin/master` a réussi avant l'application de `alpha.8`. + +La version contrôlée après application du delta est : + +```text +0.0.5-alpha.8 +``` + +`history/0.0.5/alpha.7.fix.1.md` est présent avec statut `PASS`, `deltas/0.0.5/alpha.8.md` est présent et `history/0.0.5/alpha.8.md` était absent pendant la gate, conformément au workflow. + +## Validation opérateur observée + +L'opérateur a fourni les sorties du `2026-09-25` depuis `~/Projects/pgr`, à `17:22:08`–`17:24:18`. + +Les contrôles de révision ont confirmé : + +```text +docs/rules/510-RUST_CARGO.md version 7 +docs/skeletons/002-RUST_CARGO_WORKSPACE.md version 5 +docs/studies/001-KSP_GAMES_RECONCILIATION.md version 10 +docs/validation/001-VALIDATION_MATRIX.md version 12 +docs/validation/002-RUST_STYLE_AUDITS.md version 2 +docs/plans/005-V0_0_5_RECONCILIATION_PLAN.md version 11 +tests/test_rust_audits.py version 2 +``` + +Les quatre nouveaux audits sont présents en version `1` : + +```text +scripts/audit_rust_item_spacing.py +scripts/audit_rust_module_order.py +scripts/audit_rust_reexport_blocks.py +scripts/audit_rust_trait_import_blocks.py +``` + +La suite ciblée Rust a exécuté `20` tests, tous réussis : + +```text +Ran 20 tests in 0.287s + +OK +``` + +La suite Python globale a exécuté `48` tests, tous réussis : + +```text +Ran 48 tests in 0.710s + +OK +``` + +Les audits documentaires sont propres : + +```text +Markdown header audit: clean (158 file(s)) +Markdown table audit: clean (7 table(s), 158 file(s)) +Markdown link audit: clean (152 local link(s), 158 file(s)) +``` + +Le contrôle structurel spécifique affiche : + +```text +0.0.5 alpha.8 Rust structure audit: clean (owners split, four monotask audits, semantic order protected, horizon candidates recorded) +``` + +`git diff --check` et `git diff --cached --check` n'ont produit aucun diagnostic. + +Le scope Git observé avant ajout du présent history est exactement : + +```text +M VERSION +A deltas/0.0.5/alpha.8.md +M docs/plans/005-V0_0_5_RECONCILIATION_PLAN.md +M docs/rules/510-RUST_CARGO.md +M docs/skeletons/002-RUST_CARGO_WORKSPACE.md +M docs/studies/001-KSP_GAMES_RECONCILIATION.md +M docs/validation/001-VALIDATION_MATRIX.md +M docs/validation/002-RUST_STYLE_AUDITS.md +A scripts/audit_rust_item_spacing.py +A scripts/audit_rust_module_order.py +A scripts/audit_rust_reexport_blocks.py +A scripts/audit_rust_trait_import_blocks.py +M tests/test_rust_audits.py +``` + +## Résultat + +Le profil Rust strict possède maintenant des owners mécaniques séparés pour les blancs, blocs de constantes sélectionnés, espacement des items, ordre des modules, blocs de traits, réexports et façade crate-root. Aucun `audit_rust_general_rules.py` composite n'est réintroduit. + +Les séquences métier ou protocolaires restent protégées contre un tri automatique injustifié. `audit_rust_const_blocks.py` demeure explicitement opt-in lorsque l'ordre des constantes peut être sémantique. + +Les trois candidats d'horizon discutés pendant la tranche sont conservés sans numéro de release ni engagement : maturation des profils techniques composables, projets documentaires/spécifications et généricité de PGR hors développement logiciel. + +## Transition + +Appliquer le présent history puis committer et pousser `0.0.5-alpha.8`. + +La tranche suivante est : + +```text +0.0.5-alpha.9 +``` + +Elle doit décider si un skeleton Tauri pour projets contrôlés peut être matérialisé sans faire du skeleton un propriétaire implicite des choix Web encore dépourvus de profils PGR. Si les owners Web restent insuffisants, la décision doit être explicitement différée plutôt que représentée par un skeleton décoratif. diff --git a/scripts/audit_rust_item_spacing.py b/scripts/audit_rust_item_spacing.py new file mode 100755 index 0000000..d2d3156 --- /dev/null +++ b/scripts/audit_rust_item_spacing.py @@ -0,0 +1,168 @@ +#!/usr/bin/env python3 +# file: scripts/audit_rust_item_spacing.py +# version: 1 + +"""Audit exact spacing between mechanically detectable Rust items.""" + +from __future__ import annotations + +import argparse +import dataclasses +import pathlib +import re +import sys + +from _rust_audit_common import line_depths, mask_source, selected_files + + +@dataclasses.dataclass(frozen=True) +class RustItem: + """One mechanically detected Rust item and its source span.""" + + kind: str + visibility: str + start: int + declaration: int + end: int + depth: int + braced: bool + + +def _visibility(stripped: str) -> str: + if stripped.startswith("pub(crate) "): + return "pub(crate)" + if stripped.startswith("pub "): + return "pub" + return "private" + + +def _kind(stripped: str) -> str | None: + prefix = r"(?:(?:pub|pub\(crate\))\s+)?" + qualifiers = r"(?:(?:async|const|unsafe)\s+)*" + if re.match(rf"^{prefix}{qualifiers}fn\b", stripped): + return "fn" + for kind in ("struct", "enum", "union", "trait", "impl", "const", "static", "type"): + if re.match(rf"^{prefix}{qualifiers}{kind}\b", stripped): + return kind + return None + + +def _leading_line(lines: list[str], declaration: int) -> int: + cursor = declaration - 2 + while cursor >= 0: + stripped = lines[cursor].strip() + if stripped.startswith("///") or stripped.startswith("#["): + cursor -= 1 + continue + break + return cursor + 2 + + +def _end_line(masked_lines: list[str], depths: list[int], declaration: int, kind: str) -> tuple[int, bool]: + start_depth = depths[declaration - 1] + if kind in {"const", "static", "type"}: + brackets: list[str] = [] + closing = {"}": "{", ")": "(", "]": "["} + for line_index in range(declaration - 1, len(masked_lines)): + for char in masked_lines[line_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 line_index + 1, False + return declaration, False + body_started = False + for line_index in range(declaration - 1, len(masked_lines)): + masked_line = masked_lines[line_index] + if not body_started and ";" in masked_line and "{" not in masked_line: + return line_index + 1, False + if "{" in masked_line: + body_started = True + if body_started: + end_depth = depths[line_index] + masked_line.count("{") - masked_line.count("}") + if end_depth == start_depth: + return line_index + 1, True + return declaration, body_started + + +def _items(lines: list[str], masked_lines: list[str], depths: list[int]) -> list[RustItem]: + result: list[RustItem] = [] + for number, line in enumerate(lines, 1): + stripped = line.strip() + kind = _kind(stripped) + if kind is None: + continue + end, braced = _end_line(masked_lines, depths, number, kind) + result.append(RustItem(kind, _visibility(stripped), _leading_line(lines, number), number, end, depths[number - 1], braced)) + return result + + +def _parent(item: RustItem, items: list[RustItem]) -> RustItem | None: + parents = [candidate for candidate in items if candidate.braced and candidate.declaration < item.declaration <= candidate.end and candidate.depth < item.depth] + if not parents: + return None + return max(parents, key=lambda candidate: candidate.depth) + + +def audit_file(path: pathlib.Path) -> list[str]: + """Require exact separation between adjacent items in one Rust scope.""" + + text = path.read_text(encoding="utf-8") + lines = text.splitlines() + masked, _ = mask_source(text) + masked_lines = masked.splitlines() + depths = line_depths(masked) + items = _items(lines, masked_lines, depths) + siblings: dict[tuple[int, int] | None, list[RustItem]] = {} + for item in items: + parent = _parent(item, items) + key = None if parent is None else (parent.declaration, parent.end) + siblings.setdefault(key, []).append(item) + + errors: list[str] = [] + for group in siblings.values(): + group.sort(key=lambda item: item.declaration) + for previous, current in zip(group, group[1:]): + between = lines[previous.end:current.start - 1] + # Unknown syntax or a macro between items owns its own layout; do not infer across it. + if any(candidate.strip() for candidate in between): + continue + blank_count = sum(1 for candidate in between if not candidate.strip()) + compact = previous.kind == current.kind and previous.kind in {"const", "static", "type"} and previous.visibility == current.visibility + if compact: + # Compact declaration blocks have a dedicated owner when a project selects one. + continue + if blank_count != 1: + errors.append(f"{path}:{current.start}: expected exactly one blank line between Rust items; found {blank_count}") + return errors + + +def main() -> int: + """Run the read-only item-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 item-spacing 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 item-spacing audit: cannot read {path}: {error}", file=sys.stderr) + return 2 + for error in errors: + print(error, file=sys.stderr) + if errors: + print(f"Rust item-spacing audit: {len(errors)} error(s) across {len(files)} file(s)", file=sys.stderr) + return 1 + print(f"Rust item-spacing audit: clean ({len(files)} file(s))") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/audit_rust_module_order.py b/scripts/audit_rust_module_order.py new file mode 100755 index 0000000..1d2c13d --- /dev/null +++ b/scripts/audit_rust_module_order.py @@ -0,0 +1,87 @@ +#!/usr/bin/env python3 +# file: scripts/audit_rust_module_order.py +# version: 1 + +"""Audit top-level Rust module ordering with test modules last.""" + +from __future__ import annotations + +import argparse +import pathlib +import re +import sys + +from _rust_audit_common import line_depths, mask_source, selected_files + + +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 audit_file(path: pathlib.Path) -> list[str]: + """Require production modules before test modules and alphabetic order within each group.""" + + text = path.read_text(encoding="utf-8") + lines = text.splitlines() + masked, _ = mask_source(text) + depths = line_depths(masked) + modules: list[tuple[int, bool, str]] = [] + for number, line in enumerate(lines, 1): + if depths[number - 1] != 0: + continue + match = re.match(r"^mod\s+([A-Za-z_][A-Za-z0-9_]*)\s*(?:;|\{)", line.strip()) + if match is None: + continue + cursor = number - 2 + is_test = False + while cursor >= 0 and lines[cursor].strip().startswith("#["): + if "cfg(test)" in lines[cursor].replace(" ", ""): + is_test = True + cursor -= 1 + modules.append((number, is_test, match.group(1))) + + production = [(number, name) for number, is_test, name in modules if not is_test] + tests = [(number, name) for number, is_test, name in modules if is_test] + errors: list[str] = [] + if production and tests and min(number for number, _ in tests) < max(number for number, _ in production): + errors.append(f"{path}:{min(number for number, _ in tests)}: test modules must follow production modules") + for group, label in ((production, "production"), (tests, "test")): + names = [name for _, name in group] + expected = sorted(names, key=_natural_key) + if names != expected: + for (number, name), wanted in zip(group, expected, strict=True): + if name != wanted: + errors.append(f"{path}:{number}: {label} module declarations are not alphabetically ordered") + break + return errors + + +def main() -> int: + """Run the read-only module-order 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 module-order 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 module-order audit: cannot read {path}: {error}", file=sys.stderr) + return 2 + for error in errors: + print(error, file=sys.stderr) + if errors: + print(f"Rust module-order audit: {len(errors)} error(s) across {len(files)} file(s)", file=sys.stderr) + return 1 + print(f"Rust module-order audit: clean ({len(files)} file(s))") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/audit_rust_reexport_blocks.py b/scripts/audit_rust_reexport_blocks.py new file mode 100755 index 0000000..d216193 --- /dev/null +++ b/scripts/audit_rust_reexport_blocks.py @@ -0,0 +1,88 @@ +#!/usr/bin/env python3 +# file: scripts/audit_rust_reexport_blocks.py +# version: 1 + +"""Audit crate-root Rust re-export block visibility 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 + +_EXPORT = re.compile(r"^(pub(?:\(crate\))?)\s+use\s+") + + +def audit_file(path: pathlib.Path) -> list[str]: + """Require public re-exports before crate-public re-exports and compact blocks.""" + + text = path.read_text(encoding="utf-8") + lines = text.splitlines() + masked, _ = mask_source(text) + depths = line_depths(masked) + exports: list[tuple[int, str]] = [] + for number, line in enumerate(lines, 1): + if depths[number - 1] != 0: + continue + match = _EXPORT.match(line.strip()) + if match is None: + continue + visibility = "pub(crate)" if match.group(1) == "pub(crate)" else "pub" + exports.append((number, visibility)) + + errors: list[str] = [] + crate_seen = False + previous_by_visibility: dict[str, int] = {} + for number, visibility in exports: + if visibility == "pub(crate)": + crate_seen = True + elif crate_seen: + errors.append(f"{path}:{number}: pub use block must precede pub(crate) use block") + previous = previous_by_visibility.get(visibility) + if previous is not None: + between = lines[previous:number - 1] + if any(not candidate.strip() for candidate in between): + errors.append(f"{path}:{number}: {visibility} use block contains an empty line") + previous_by_visibility[visibility] = number + + public = [number for number, visibility in exports if visibility == "pub"] + crate_public = [number for number, visibility in exports if visibility == "pub(crate)"] + if public and crate_public: + transition = lines[public[-1]:crate_public[0] - 1] + blank_count = sum(1 for candidate in transition if not candidate.strip()) + if blank_count != 1: + errors.append(f"{path}:{crate_public[0]}: pub use and pub(crate) use blocks require exactly one blank line; found {blank_count}") + return errors + + +def main() -> int: + """Run the read-only re-export-block audit on explicitly selected Rust files.""" + + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("paths", nargs="+", help="Rust crate-root 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 re-export-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 re-export-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 re-export-block audit: {len(errors)} error(s) across {len(files)} file(s)", file=sys.stderr) + return 1 + print(f"Rust re-export-block audit: clean ({len(files)} file(s))") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/audit_rust_trait_import_blocks.py b/scripts/audit_rust_trait_import_blocks.py new file mode 100755 index 0000000..c46dec4 --- /dev/null +++ b/scripts/audit_rust_trait_import_blocks.py @@ -0,0 +1,74 @@ +#!/usr/bin/env python3 +# file: scripts/audit_rust_trait_import_blocks.py +# version: 1 + +"""Audit placement and contiguity of module-level Rust trait-import blocks.""" + +from __future__ import annotations + +import argparse +import pathlib +import re +import sys + +from _rust_audit_common import line_depths, mask_source, selected_files + +_DECLARATION = re.compile(r"^(?:pub(?:\(crate\))?\s+)?(?:async\s+|const\s+|unsafe\s+)*(?:const|static|type|struct|enum|trait|union|fn|impl)\b") +_USE = re.compile(r"^use\s+") + + +def audit_file(path: pathlib.Path) -> list[str]: + """Require ordinary module-level uses before declarations and in one compact block.""" + + text = path.read_text(encoding="utf-8") + lines = text.splitlines() + masked, _ = mask_source(text) + depths = line_depths(masked) + seen_declaration = False + uses: list[int] = [] + errors: list[str] = [] + for number, line in enumerate(lines, 1): + if depths[number - 1] != 0: + continue + stripped = line.strip() + if _DECLARATION.match(stripped): + seen_declaration = True + if _USE.match(stripped): + uses.append(number) + if seen_declaration: + errors.append(f"{path}:{number}: module-level trait import must precede Rust declarations") + for previous, current in zip(uses, uses[1:]): + between = lines[previous:current - 1] + if any(not candidate.strip() for candidate in between): + errors.append(f"{path}:{current}: module-level trait import block contains an empty line") + return errors + + +def main() -> int: + """Run the read-only trait-import-block 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 trait-import-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 trait-import-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 trait-import-block audit: {len(errors)} error(s) across {len(files)} file(s)", file=sys.stderr) + return 1 + print(f"Rust trait-import-block audit: clean ({len(files)} file(s))") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/test_rust_audits.py b/tests/test_rust_audits.py index 2186f58..39aff7f 100644 --- a/tests/test_rust_audits.py +++ b/tests/test_rust_audits.py @@ -1,7 +1,7 @@ # file: tests/test_rust_audits.py -# version: 1 +# version: 2 -"""Targeted contracts for the two optional Rust style audits.""" +"""Targeted contracts for the optional monotask Rust structure audits.""" from __future__ import annotations @@ -15,6 +15,10 @@ 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 audit_rust_item_spacing import audit_file as audit_item_spacing # noqa: E402 +from audit_rust_module_order import audit_file as audit_module_order # noqa: E402 +from audit_rust_reexport_blocks import audit_file as audit_reexports # noqa: E402 +from audit_rust_trait_import_blocks import audit_file as audit_trait_imports # noqa: E402 from _rust_audit_common import selected_files # noqa: E402 @@ -132,6 +136,125 @@ const ALPHA: u8 = 2; self.assertEqual(run(path).returncode, 1) self.assertEqual(run(self.root / "missing.rs").returncode, 2) + def test_item_spacing_between_distinct_items(self) -> None: + path = self.source("""struct Alpha {} +struct Beta {} + +fn sample() {} +""") + issues = audit_item_spacing(path) + self.assertTrue(any("exactly one blank line" in issue for issue in issues)) + + def test_item_spacing_delegates_compact_declaration_blocks(self) -> None: + path = self.source("""const ALPHA: u8 = 1; + +const BETA: u8 = 2; + +static CACHE: u8 = 0; +static READY: u8 = 1; + +type First = u8; +type Second = u16; +""") + self.assertEqual(audit_item_spacing(path), []) + + def test_item_spacing_inside_impl(self) -> None: + path = self.source("""struct Sample {} + +impl Sample { + fn alpha() {} + fn beta() {} +} +""") + self.assertTrue(any("exactly one blank line" in issue for issue in audit_item_spacing(path))) + + def test_item_spacing_does_not_infer_across_macro(self) -> None: + path = self.source("""struct Alpha {} +macro_rules! generated { () => {}; } +struct Beta {} +""") + self.assertEqual(audit_item_spacing(path), []) + + def test_module_order_production_and_tests(self) -> None: + path = self.source("""mod beta; +mod alpha; +#[cfg(test)] +mod tests; +""") + issues = audit_module_order(path) + self.assertTrue(any("production" in issue for issue in issues)) + path.write_text("#[cfg(test)]\nmod tests;\nmod alpha;\n", encoding="utf-8") + self.assertTrue(any("test modules must follow" in issue for issue in audit_module_order(path))) + + def test_module_order_clean_with_path_attribute(self) -> None: + path = self.source("""mod alpha; +mod beta; + +#[cfg(test)] +#[path = "unit_tests/mod.rs"] +mod unit_tests; +""") + self.assertEqual(audit_module_order(path), []) + + def test_reexport_blocks_visibility_and_spacing(self) -> None: + path = self.source("""pub(crate) use self::internal::Internal; + +pub use self::public::Public; +""") + issues = audit_reexports(path) + self.assertTrue(any("must precede" in issue for issue in issues)) + path.write_text("pub use self::public::Alpha;\n\npub use self::public::Beta;\n", encoding="utf-8") + self.assertTrue(any("contains an empty line" in issue for issue in audit_reexports(path))) + + def test_reexport_blocks_clean_with_rustdocs(self) -> None: + path = self.source("""/// Alpha. +pub use self::public::Alpha; +/// Beta. +pub use self::public::Beta; + +/// Internal. +pub(crate) use self::internal::Internal; +""") + self.assertEqual(audit_reexports(path), []) + + def test_trait_import_block_layout(self) -> None: + path = self.source("""use alpha::AlphaTrait; + +use beta::BetaTrait; + +struct Sample {} +""") + self.assertTrue(any("contains an empty line" in issue for issue in audit_trait_imports(path))) + path.write_text("struct Sample {}\n\nuse alpha::AlphaTrait;\n", encoding="utf-8") + self.assertTrue(any("must precede" in issue for issue in audit_trait_imports(path))) + + def test_trait_import_block_clean_and_exports_ignored(self) -> None: + path = self.source("""mod alpha; + +use alpha::AlphaTrait; +use beta::BetaTrait; + +struct Sample {} + +pub use self::alpha::Alpha; +""") + self.assertEqual(audit_trait_imports(path), []) + + def test_new_audit_cli_statuses(self) -> None: + path = self.source("mod alpha;\nmod beta;\n") + root = pathlib.Path(__file__).resolve().parents[1] / "scripts" + scripts = ( + root / "audit_rust_item_spacing.py", + root / "audit_rust_module_order.py", + root / "audit_rust_reexport_blocks.py", + root / "audit_rust_trait_import_blocks.py", + ) + for script in scripts: + result = subprocess.run([sys.executable, "-B", str(script), str(path)], capture_output=True, text=True, check=False) + self.assertEqual(result.returncode, 0, (script, result.stderr)) + missing = subprocess.run([sys.executable, "-B", str(script), str(self.root / "missing.rs")], capture_output=True, text=True, check=False) + self.assertEqual(missing.returncode, 2, (script, missing.stderr)) + if __name__ == "__main__": unittest.main()