diff --git a/VERSION b/VERSION index bb2e50d..061b399 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.0.5-alpha.4 +0.0.5-alpha.5 diff --git a/deltas/0.0.5/alpha.5.md b/deltas/0.0.5/alpha.5.md new file mode 100644 index 0000000..a11ad3a --- /dev/null +++ b/deltas/0.0.5/alpha.5.md @@ -0,0 +1,205 @@ + + + +# Delta 0.0.5-alpha.5 — audit Rust de façade/export completeness + +## Baseline et version + +Baseline attendue : commit opérateur complet de `0.0.5-alpha.4`, réalisé après application de `history/0.0.5/alpha.4.md` avec statut `PASS`. + +Le hash de ce futur commit n'est pas anticipé. + +Version avant application : + +```text +0.0.5-alpha.4 +``` + +Version après application : + +```text +0.0.5-alpha.5 +``` + +## Rôle de la tranche + +`alpha.5` extrait un audit Rust facultatif réellement réutilisable à partir du corpus KSP/Games, mais corrige avant généralisation une limite découverte dans le script historique : Games organise ses crates sous plusieurs niveaux et n'était pas réellement couvert par le glob `crates/*/Cargo.toml`. + +La tranche reste limitée à la façade crate-root et aux chemins same-crate canoniques. Elle ne copie pas `audit_rust_general_rules.py`, ne commence pas Tauri et ne transforme pas PGR en workspace Rust. + +## Scope + +REPLACE : + +```text +VERSION +docs/000-README.md +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 +``` + +ADD : + +```text +docs/validation/003-RUST_EXPORT_COMPLETENESS.md +scripts/audit_rust_export_completeness.py +tests/test_rust_export_completeness.py +deltas/0.0.5/alpha.5.md +``` + +DELETE : aucun. + +Le history `alpha.4` appartient au commit baseline et n'est pas inclus dans l'archive du présent delta. + +## Décisions matérialisées + +### Audit monotâche + +Le nouveau script possède exactement deux responsabilités : + +```text +RUST-API-201 + façade crate-root absente pour un élément public, ou crate-public clairement partagé + +RUST-IMPORT-201 + chemin `crate::module::Item` alors qu'un réexport explicite fournit déjà `crate::Item` +``` + +Les conventions des tests unitaires séparés du script historique ne sont pas reprises. Le contrôle d'un symbole `crate::X` inexistant n'est pas repris non plus, car le compilateur Rust possède déjà la résolution de noms. + +### Découverte des crates + +La découverte des `Cargo.toml` devient récursive et accepte les crates imbriquées. Les arbres générés usuels (`target`, `build`, `builds`, `dist`, `node_modules`, etc.) restent ignorés. + +Le module fichier `foo/mod.rs` est interprété comme `foo`, et les réexports crate-root simples, groupés ou aliasés couverts par les tests sont reconnus. + +### Adoption facultative + +Le contrat général Rust reste un `SHOULD`. Un projet existant peut commencer avec `--report-only`, traiter les écarts ou exceptions locales, puis rendre l'audit bloquant seulement après décision explicite. + +L'audit est documenté dans `docs/validation/003-RUST_EXPORT_COMPLETENESS.md` et référencé par le skeleton Rust/Cargo sans devenir obligatoire. + +### Mesure de réconciliation + +La prévalidation générateur sur les baselines autoritaires de `alpha.1` observe : + +```text +KSP 0.3.16 22 crate(s) 0 candidate(s) +Games 0.3.5 23 crate(s) 2 candidate(s) +``` + +Le résultat Games antérieur `0 candidate(s)` provenait d'une découverte de crates incomplète. Les deux candidats actuels ne sont pas traités comme des défauts historiques, car Games n'est pas encore gouverné par PGR. + +## Validation opérateur + +Après application du history `PASS` de `alpha.4`, commit/push de cette tranche, puis application du présent delta : + +```bash +test "$(cat VERSION)" = '0.0.5-alpha.5' +test -f history/0.0.5/alpha.4.md +test -f deltas/0.0.5/alpha.5.md +test -f scripts/audit_rust_export_completeness.py +test -f tests/test_rust_export_completeness.py +test -f docs/validation/003-RUST_EXPORT_COMPLETENESS.md +test ! -e history/0.0.5/alpha.5.md + +grep -A2 '^## Statut$' history/0.0.5/alpha.4.md | grep -Fx 'PASS' +grep -Fx '# version: 1' scripts/audit_rust_export_completeness.py +grep -Fx '# version: 1' tests/test_rust_export_completeness.py +grep -Fx '' docs/validation/003-RUST_EXPORT_COMPLETENESS.md +grep -Fx '' docs/rules/510-RUST_CARGO.md +grep -Fx '' docs/skeletons/002-RUST_CARGO_WORKSPACE.md +grep -Fx '' docs/validation/001-VALIDATION_MATRIX.md + +python3 -B -m unittest discover -s tests -p 'test_rust_export_completeness.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.4.md').read_text(encoding='utf-8') +assert re.search(r'## Statut\s+PASS\s', history) + +script = Path('scripts/audit_rust_export_completeness.py').read_text(encoding='utf-8') +for phrase in ( + 'def crate_roots(', + 'root.rglob("Cargo.toml")', + 'RUST-API-201', + 'RUST-IMPORT-201', + '--report-only', + '--summary-only', +): + assert phrase in script, phrase +for forbidden in ('RUST-IMPORT-202', 'RUST-IMPORT-203', 'RUST-IMPORT-204', 'RUST-IMPORT-205'): + assert forbidden not in script, forbidden + +validation = Path('docs/validation/003-RUST_EXPORT_COMPLETENESS.md').read_text(encoding='utf-8') +for phrase in ( + '22` crates KSP', + '23` crates Games', + '0 candidate(s)', + '2 candidate(s)', + 'ne sont pas qualifiés de défauts rétroactifs', +): + assert phrase in validation, phrase + +plan = Path('docs/plans/005-V0_0_5_RECONCILIATION_PLAN.md').read_text(encoding='utf-8') +assert 'KSP = 0 candidate(s) / 22 crates' in plan +assert 'Games = 2 candidate(s) / 23 crates' in plan + +print('0.0.5 alpha.5 Rust façade audit: clean (recursive crate discovery, narrow ownership, regression coverage)') +PY + +git diff --check + +git add VERSION docs/000-README.md \ + 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/003-RUST_EXPORT_COMPLETENESS.md \ + scripts/audit_rust_export_completeness.py \ + tests/test_rust_export_completeness.py \ + deltas/0.0.5/alpha.5.md + +git diff --cached --check +git status --short +``` + +La sortie Git attendue avant ajout du history `alpha.5` est exactement : + +```text +M VERSION +A deltas/0.0.5/alpha.5.md +M docs/000-README.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 +A docs/validation/003-RUST_EXPORT_COMPLETENESS.md +A scripts/audit_rust_export_completeness.py +A tests/test_rust_export_completeness.py +``` + +## Transition + +### PASS + +Créer/appliquer `history/0.0.5/alpha.5.md`, committer/pousser `0.0.5-alpha.5`, puis poursuivre à `0.0.5-alpha.6` pour le premier profil framework Tauri. + +### FAIL + +Rester dans `0.0.5-alpha.5.fix.M` si le défaut concerne l'audit, ses tests, sa documentation ou l'intégration Rust/Cargo. Ne pas absorber le profil Tauri dans un fix. diff --git a/docs/000-README.md b/docs/000-README.md index 8a98b78..e0c222c 100644 --- a/docs/000-README.md +++ b/docs/000-README.md @@ -1,5 +1,5 @@ - + # Documentation PGR @@ -26,6 +26,7 @@ Les templates techniques copiables sous `templates/` restent séparés des instr - [`studies/001-KSP_GAMES_RECONCILIATION.md`](studies/001-KSP_GAMES_RECONCILIATION.md) — inventaire comparatif des baselines KSP/Games et décisions de généralisation, maintien local ou différé. - [`templates/004-FILE_FAMILY_CONTRACTS.md`](templates/004-FILE_FAMILY_CONTRACTS.md) — pattern facultatif pour expliciter les responsabilités et règles de modification de familles de fichiers locales lorsque la complexité le justifie. - [`templates/005-RELEASE_PLAN.md`](templates/005-RELEASE_PLAN.md) — pattern de plan de release avec clôture/transition explicites et revue d'horizon proportionnée. +- [`validation/003-RUST_EXPORT_COMPLETENESS.md`](validation/003-RUST_EXPORT_COMPLETENESS.md) — contrat, provenance et limites de l'audit Rust facultatif de façade crate-root. ## Points d'entrée de 0.0.4 @@ -36,4 +37,4 @@ Les templates techniques copiables sous `templates/` restent séparés des instr - [`skeletons/002-RUST_CARGO_WORKSPACE.md`](skeletons/002-RUST_CARGO_WORKSPACE.md) — specialization Rust/Cargo workspace composable avec la fondation et les cinq templates existants ; - [`templates/001-RUST_CARGO_TOOLING.md`](templates/001-RUST_CARGO_TOOLING.md) et [`templates/002-CARGO_MANIFESTS.md`](templates/002-CARGO_MANIFESTS.md) — adaptation des cinq fichiers Rust/Cargo copiables ; - [`templates/003-NEW_SESSION_PROMPT.md`](templates/003-NEW_SESSION_PROMPT.md) — spécification du message manuel après une stable publiée ; -- [`validation/001-VALIDATION_MATRIX.md`](validation/001-VALIDATION_MATRIX.md) et [`validation/002-RUST_STYLE_AUDITS.md`](validation/002-RUST_STYLE_AUDITS.md) — gates PGR et limites des audits de style Rust facultatifs. +- [`validation/001-VALIDATION_MATRIX.md`](validation/001-VALIDATION_MATRIX.md), [`validation/002-RUST_STYLE_AUDITS.md`](validation/002-RUST_STYLE_AUDITS.md) et [`validation/003-RUST_EXPORT_COMPLETENESS.md`](validation/003-RUST_EXPORT_COMPLETENESS.md) — gates PGR et limites des audits Rust facultatifs. diff --git a/docs/plans/005-V0_0_5_RECONCILIATION_PLAN.md b/docs/plans/005-V0_0_5_RECONCILIATION_PLAN.md index 0e563b7..94d674c 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 @@ -84,6 +84,10 @@ Cette tranche ne commence pas l'audit Rust ni le profil Tauri. Extraire le noyau réellement commun de `audit_rust_export_completeness.py`, l'aligner avec `510-RUST_CARGO.md`, documenter son caractère facultatif et ses limites, et ajouter des tests positifs/négatifs. +La réconciliation a révélé que le script historique ne découvrait que `crates/*/Cargo.toml` : il couvrait les `22` crates KSP mais aucune des `23` crates Games imbriquées sous des sous-familles. La variante PGR doit donc corriger la découverte récursive avant toute généralisation. Elle doit également abandonner les sous-règles historiques de tests séparés et la duplication de résolution de noms déjà possédée par le compilateur. + +Mesure de référence avec le scope PGR retenu : `KSP = 0 candidate(s) / 22 crates`, `Games = 2 candidate(s) / 23 crates`. Les deux signaux Games sont une observation de réconciliation, pas une non-conformité rétroactive. + Cette tranche ne réimporte pas `audit_rust_general_rules.py` en bloc. Toute règle déjà possédée par Rustfmt, Clippy, le compilateur ou les audits PGR existants reste chez son owner actuel. ### alpha.6 — premier profil framework : Tauri diff --git a/docs/rules/510-RUST_CARGO.md b/docs/rules/510-RUST_CARGO.md index 1f983eb..9d06fdf 100644 --- a/docs/rules/510-RUST_CARGO.md +++ b/docs/rules/510-RUST_CARGO.md @@ -1,5 +1,5 @@ - + # Profil Rust / Cargo @@ -192,4 +192,8 @@ 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. -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. +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é. + +[`../validation/003-RUST_EXPORT_COMPLETENESS.md`](../validation/003-RUST_EXPORT_COMPLETENESS.md) documente séparément un audit facultatif de façade crate-root. Lorsqu'un projet choisit ce contrat strict, les éléments `pub` des modules fichiers et les éléments `pub(crate)` clairement partagés hors de leur module SHOULD être réexportés explicitement au crate-root ; lorsqu'un tel réexport existe, les chemins internes SHOULD employer `crate::Item` plutôt que `crate::module::Item`. + +Cet audit de façade ne rend pas la règle obligatoire pour tous les projets Rust et ne remplace pas le compilateur. Le dépôt PGR lui-même ne contient pas de workspace Rust sur lequel une telle gate fournirait une preuve runtime ou Cargo réelle. diff --git a/docs/skeletons/002-RUST_CARGO_WORKSPACE.md b/docs/skeletons/002-RUST_CARGO_WORKSPACE.md index 65021c5..e3c7b85 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 @@ -57,7 +57,7 @@ templates/rust-cargo/Cargo.toml templates/rust-cargo/crates/project-core-lib/Cargo.toml ``` -[`../validation/002-RUST_STYLE_AUDITS.md`](../validation/002-RUST_STYLE_AUDITS.md) est seulement une option supplémentaire. Les scripts `audit_rust_blank_lines.py`, `audit_rust_const_blocks.py` et `_rust_audit_common.py` ne sont copiés que si le projet choisit explicitement les conventions correspondantes et sait quels fichiers Rust doivent être audités. +[`../validation/002-RUST_STYLE_AUDITS.md`](../validation/002-RUST_STYLE_AUDITS.md) et [`../validation/003-RUST_EXPORT_COMPLETENESS.md`](../validation/003-RUST_EXPORT_COMPLETENESS.md) sont seulement des options supplémentaires. Les scripts Rust PGR ne sont copiés que si le projet choisit explicitement les conventions correspondantes. Le helper `_rust_audit_common.py` peut être partagé entre ces audits. ## Décisions destinataire avant copie @@ -72,7 +72,7 @@ Avant de considérer la specialization comme instanciée, résoudre explicitemen 7. les niveaux de lints hérités, les éventuelles exceptions locales et tout besoin réel d'`unsafe` dans un scope étroit ; 8. les paramètres `rustfmt.toml` et `clippy.toml` à conserver ou adapter ; 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 audits de style Rust facultatifs réellement adoptés et leur corpus exact ; +10. les audits Rust facultatifs réellement adoptés, dont éventuellement le contrat de façade crate-root, et leur scope exact ; 11. 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. @@ -127,11 +127,11 @@ 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 de style facultatifs +### Audits Rust facultatifs -Si le projet adopte les conventions documentées dans [`../validation/002-RUST_STYLE_AUDITS.md`](../validation/002-RUST_STYLE_AUDITS.md), copier les trois scripts concernés et adapter leurs headers à leurs chemins locaux. +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. -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. +Si le projet adopte le contrat de façade décrit dans [`../validation/003-RUST_EXPORT_COMPLETENESS.md`](../validation/003-RUST_EXPORT_COMPLETENESS.md), copier `audit_rust_export_completeness.py` avec `_rust_audit_common.py`. Une adoption d'un projet existant SHOULD commencer en `--report-only`, résoudre les écarts ou exceptions locales, puis seulement rendre l'audit bloquant. Ne pas copier ces scripts uniquement parce qu'ils existent dans PGR. @@ -150,7 +150,7 @@ cargo clippy --workspace --all-targets --all-features -- -D warnings Ajouter des tests ciblés pour les crates qui possèdent déjà un comportement à vérifier. Un `cargo build` n'est nécessaire que lorsqu'un artefact de build doit réellement être produit. -Si les audits de style facultatifs ont été sélectionnés, les exécuter après le formatage sur les seuls chemins concernés. +Si des audits Rust facultatifs ont été sélectionnés, exécuter les audits de style après formatage sur les seuls chemins concernés et l'audit de façade sur la racine du projet une fois son contrat explicitement adopté. La validation doit aussi confirmer : diff --git a/docs/studies/001-KSP_GAMES_RECONCILIATION.md b/docs/studies/001-KSP_GAMES_RECONCILIATION.md index 53b239a..84de778 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 @@ -133,6 +133,12 @@ Décision : classer ce contrôle `COMMON`, mais ne pas copier le script tel quel 3. fournir des tests positifs/négatifs ciblés et documenter les limites du parsing lexical ; 4. éviter de réintroduire les règles déjà possédées par Rustfmt, Clippy ou les deux audits de style PGR existants. +#### Mesure alpha.5 + +La relecture du script historique révèle une limite importante : sa découverte `crates/*/Cargo.toml` couvre les `22` crates KSP placées directement sous `crates/`, mais aucune des `23` crates Games imbriquées sous `crates/apps`, `crates/common`, `crates/engines`, etc. Le code des deux scripts est bien commun, mais le résultat historique Games `0 candidate(s)` n'était pas une preuve d'audit des crates réelles. + +La variante PGR corrige la découverte récursive et réduit le scope aux deux propriétés effectivement possédées par `510-RUST_CARGO.md` : complétude de la façade crate-root et canonicalisation d'un chemin interne lorsqu'un réexport explicite existe. Avec cette variante, KSP donne `0 candidate(s)` sur `22` crates et Games `2 candidate(s)` sur `23` crates. Ces deux signaux Games ne sont pas des non-conformités rétroactives ; ils deviennent des points de décision lors d'une future adoption PGR. + Le grand `audit_rust_general_rules.py` des deux corpus reste `DEFER` comme source d'extraction, pas comme script à importer. Il mélange plusieurs responsabilités : headers, rustdoc, imports, structure de façade, blancs, constantes et règles locales ; Games y ajoute une exception FFI Android étroite. PGR doit continuer à extraire des audits monotâches seulement lorsqu'une propriété commune apporte une preuve non redondante. L'orchestrateur `audit_rust_workspace_rules.py` est un pattern d'ergonomie utile mais ne possède aucune règle. PGR peut documenter comment composer des audits sélectionnés dans un projet destinataire sans fournir un lanceur universel qui supposerait les mêmes scripts partout. diff --git a/docs/validation/001-VALIDATION_MATRIX.md b/docs/validation/001-VALIDATION_MATRIX.md index 86bfd58..0f15895 100644 --- a/docs/validation/001-VALIDATION_MATRIX.md +++ b/docs/validation/001-VALIDATION_MATRIX.md @@ -1,5 +1,5 @@ - + # Matrice de validation PGR @@ -41,6 +41,8 @@ PGR préfère plusieurs audits simples avec une responsabilité identifiable à 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. +[`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. + ## 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. diff --git a/docs/validation/003-RUST_EXPORT_COMPLETENESS.md b/docs/validation/003-RUST_EXPORT_COMPLETENESS.md new file mode 100644 index 0000000..6f75fcf --- /dev/null +++ b/docs/validation/003-RUST_EXPORT_COMPLETENESS.md @@ -0,0 +1,83 @@ + + + +# Audit facultatif de façade Rust et export completeness + +## Responsabilité + +`scripts/audit_rust_export_completeness.py` contrôle un contrat Rust optionnel plus strict que les garanties du compilateur : les éléments publics durables d'un module fichier doivent être exposés par une façade explicite au crate-root, et le code de la même crate doit employer cette façade lorsqu'un réexport correspondant existe. + +Le propriétaire normatif reste [`../rules/510-RUST_CARGO.md`](../rules/510-RUST_CARGO.md). Le script ne définit pas une architecture à lui seul et ne doit être adopté que par un projet qui choisit ce contrat de façade. + +L'audit émet seulement deux familles de diagnostics : + +```text +RUST-API-201 + élément `pub`, ou élément `pub(crate)` clairement consommé hors de son module fichier, + sans réexport explicite correspondant au crate-root + +RUST-IMPORT-201 + chemin interne long `crate::module::Item` alors que le crate-root expose déjà `crate::Item` +``` + +Il ne vérifie pas les règles générales de style, les lignes vides, l'ordre des constantes, la rustdoc, les lints Cargo/Clippy, la sûreté FFI ou le contenu métier. + +## Provenance et correction du corpus + +KSP `0.3.16` et Games `0.3.5` contiennent un `audit_rust_export_completeness.py` dont le corps est identique à l'exception de sa révision de fichier. Cette convergence justifie l'extraction du concept, mais pas une copie littérale. + +Le script historique découvre uniquement `crates/*/Cargo.toml`. Cette hypothèse couvre les `22` crates KSP situées directement sous `crates/`, mais elle ne couvre aucune des `23` crates Games organisées sous des sous-familles telles que `crates/apps/`, `crates/common/` ou `crates/engines/`. Un résultat historique `0 candidate(s)` dans Games ne constitue donc pas une preuve que ses crates avaient réellement été inspectées par cet audit. + +La variante PGR corrige ce point en découvrant récursivement les packages Cargo possédant `src/lib.rs` ou `src/main.rs`, tout en ignorant les arbres générés usuels. Elle gère également correctement un module fichier `mod.rs` et les réexports explicites simples, groupés ou aliasés. + +La réconciliation générateur réalisée pendant `0.0.5-alpha.5` donne avec la nouvelle découverte : + +```text +KSP 0.3.16 22 crate(s) 0 candidate(s) +Games 0.3.5 23 crate(s) 2 candidate(s) +``` + +Les deux candidats Games sont des constantes `pub(crate)` du smoke WebTransport navigateur consommées depuis d'autres modules sans façade crate-root. Games n'étant pas encore gouverné par PGR, ces signaux ne sont pas qualifiés de défauts rétroactifs ; ils montrent simplement ce qu'une adoption future de ce contrat devrait décider ou corriger. + +## Ce qui n'est pas repris du script historique + +PGR n'importe pas les diagnostics historiques liés aux tests unitaires séparés (`super::...`, références non qualifiées) : leur validité dépend du layout et du contrat local des modules de test, et la baseline Games ne prouve pas une convergence opérationnelle sur ce point. + +PGR n'importe pas non plus le contrôle d'un `crate::Symbol` non résolu. Le compilateur Rust est déjà propriétaire de la résolution de noms ; dupliquer cette erreur dans un audit Python n'apporte pas une preuve supplémentaire proportionnée. + +L'audit PGR reste donc monotâche : façade crate-root et canonicalisation des chemins pour les seuls symboles qu'il sait relier explicitement. + +## Adoption + +Pour adopter cette convention dans un projet Rust, copier : + +```text +scripts/_rust_audit_common.py +scripts/audit_rust_export_completeness.py +``` + +puis adapter les headers de fichiers au dépôt destinataire. + +Commande canonique : + +```bash +python3 -B scripts/audit_rust_export_completeness.py --root . +``` + +Le code de sortie est `0` lorsque le corpus est propre, `1` lorsqu'au moins un candidat est trouvé et `2` lorsque la racine est invalide, illisible ou ne contient aucune crate Rust détectable. `--report-only` permet une phase d'inventaire sans bloquer la gate ; `--summary-only` réduit la sortie aux comptes. + +Une adoption progressive peut donc commencer par : + +```bash +python3 -B scripts/audit_rust_export_completeness.py --root . --report-only +``` + +puis devenir bloquante une fois les exceptions ou écarts locaux explicitement résolus. + +## Limites + +L'analyse est lexicale et non un parseur Rust complet. Elle vise les déclarations `pub`/`pub(crate)` au niveau racine de modules fichiers et les réexports explicites du crate-root. Les macros générant des items, attributs élaborés, modules inline complexes, `#[path]` non standard et formes de réexport imbriquées au-delà du contrat testé peuvent nécessiter une revue humaine ou les outils Rust du projet. + +Un élément `pub(crate)` n'est exigé à la façade que lorsqu'une consommation externe au fichier est clairement détectée par un chemin `crate::module::Item` ou `crate::Item`. L'absence de diagnostic ne prouve donc pas toute la sémantique de visibilité de la crate. + +L'audit ne remplace ni `cargo check`, ni Clippy, ni rustdoc. Il complète uniquement une convention architecturale explicitement choisie. diff --git a/history/0.0.5/alpha.5.md b/history/0.0.5/alpha.5.md new file mode 100644 index 0000000..772e176 --- /dev/null +++ b/history/0.0.5/alpha.5.md @@ -0,0 +1,105 @@ + + + +# History 0.0.5-alpha.5 — audit Rust de façade/export completeness + +## Statut + +FAIL + +La gate mécanique de `0.0.5-alpha.5` exécutée le `2026-09-25` est propre, mais la revue fonctionnelle qui suit révèle que le contrat livré est incomplet par rapport à la convention Rust que PGR doit pouvoir porter : l'audit détecte `crate::module::Item`, mais ne détecte pas les références non qualifiées ou autrement non canoniques à un item partagé déjà exposé au crate-root, notamment dans son propre module de déclaration. + +Le statut global reste donc `FAIL`. Les résultats mécaniques ne sont pas annulés ; ils prouvent que l'implémentation livrée fonctionne selon son contrat écrit, tandis que la revue révèle que ce contrat est trop faible. + +## Baseline et version + +La tranche part du commit opérateur de `0.0.5-alpha.4` : + +```text +defc9ca 0.0.5-alpha.4 +``` + +Le push vers `origin/master` a réussi avant l'application de `alpha.5`. + +La version contrôlée après application du delta est : + +```text +0.0.5-alpha.5 +``` + +`history/0.0.5/alpha.4.md`, `deltas/0.0.5/alpha.5.md`, `scripts/audit_rust_export_completeness.py`, `tests/test_rust_export_completeness.py` et `docs/validation/003-RUST_EXPORT_COMPLETENESS.md` sont présents. `history/0.0.5/alpha.5.md` était absent pendant la gate. + +## Validation opérateur observée + +L'opérateur a fourni les sorties du `2026-09-25` depuis `~/Projects/pgr`, à `00:03:49`–`00:08:36`. + +Les contrôles de présence, statut et révision ont réussi. La suite ciblée a exécuté `9` tests, tous réussis : + +```text +Ran 9 tests in 0.161s + +OK +``` + +La suite Python globale a exécuté `28` tests, tous réussis : + +```text +Ran 28 tests in 0.468s + +OK +``` + +Les audits documentaires affichent : + +```text +Markdown header audit: clean (140 file(s)) +Markdown table audit: clean (6 table(s), 140 file(s)) +Markdown link audit: clean (137 local link(s), 140 file(s)) +``` + +Le contrôle structurel de la tranche affiche : + +```text +0.0.5 alpha.5 Rust façade audit: clean (recursive crate discovery, narrow ownership, regression coverage) +``` + +`git diff --check` et `git diff --cached --check` n'ont produit aucun diagnostic. Le scope Git observé est exactement celui du delta `alpha.5`. + +## Défaut fonctionnel découvert après la gate + +La convention recherchée est plus stricte que le contrat implémenté : lorsqu'un item `pub` ou `pub(crate)` est partagé et exposé par la façade crate-root, ses références intra-crate doivent utiliser le chemin canonique `crate::Item`, y compris depuis le module propriétaire. + +Le script `version: 1` détecte seulement le sous-cas : + +```text +crate::module::Item +``` + +lorsque `crate::Item` existe déjà. Il laisse notamment passer : + +```text +Item +self::Item +super::Item +module::Item +``` + +ainsi que les noms nus dans des signatures, `impl`, constructeurs ou appels situés dans le module de déclaration. Le compilateur et Clippy ne possèdent pas cette convention de style/architecture : une seule consommation correcte par `crate::Item` peut suffire à rendre le réexport utilisé sans révéler les autres accès non canoniques. + +Ce manque est dans le scope exact de `alpha.5`; il doit donc être corrigé par `alpha.5.fix.1`, sans commencer Tauri. + +## Résultat + +`0.0.5-alpha.5` est enregistré `FAIL` malgré une gate mécanique verte, car l'acceptation fonctionnelle de l'audit est incomplète. + +## Transition + +Appliquer le présent history puis committer/pousser `0.0.5-alpha.5`. + +La correction suivante est : + +```text +0.0.5-alpha.5.fix.1 +``` + +Elle doit étendre l'audit de façade pour imposer réellement la forme canonique intra-crate `crate::Item` aux items partagés, ajouter les régressions correspondantes et corriger la documentation/mesure de réconciliation. Aucun scope Tauri ne doit être absorbé. diff --git a/scripts/audit_rust_export_completeness.py b/scripts/audit_rust_export_completeness.py new file mode 100644 index 0000000..0dfef90 --- /dev/null +++ b/scripts/audit_rust_export_completeness.py @@ -0,0 +1,292 @@ +#!/usr/bin/env python3 +# file: scripts/audit_rust_export_completeness.py +# version: 1 + +"""Audit crate-root façade completeness and canonical same-crate paths.""" + +from __future__ import annotations + +import argparse +import dataclasses +import pathlib +import re +import sys + +from _rust_audit_common import line_depths, mask_source + +_IGNORED_DIRECTORY_NAMES = frozenset({ + ".git", + ".venv", + "__pycache__", + "build", + "builds", + "dist", + "node_modules", + "target", +}) + + +@dataclasses.dataclass(frozen=True) +class Candidate: + """One façade or same-crate path violation candidate.""" + + code: str + path: str + line: int + message: str + + +@dataclasses.dataclass(frozen=True) +class Declaration: + """One module-level public or crate-public declaration.""" + + module: str + name: str + visibility: str + kind: str + path: pathlib.Path + line: int + + +def _is_ignored(path: pathlib.Path, root: pathlib.Path) -> bool: + """Return whether a path belongs to a generated or local-only tree.""" + + try: + relative = path.relative_to(root) + except ValueError: + return True + return bool(set(relative.parts) & _IGNORED_DIRECTORY_NAMES) + + +def crate_roots(root: pathlib.Path) -> list[pathlib.Path]: + """Discover Rust package roots recursively below one project root.""" + + crates: set[pathlib.Path] = set() + for manifest in root.rglob("Cargo.toml"): + if _is_ignored(manifest, root): + continue + crate = manifest.parent + if (crate / "src/lib.rs").is_file() or (crate / "src/main.rs").is_file(): + crates.add(crate) + return sorted(crates) + + +def crate_root_source(crate: pathlib.Path) -> pathlib.Path | None: + """Return the library or binary crate-root source when present.""" + + library = crate / "src/lib.rs" + if library.is_file(): + return library + binary = crate / "src/main.rs" + if binary.is_file(): + return binary + return None + + +def module_path(crate: pathlib.Path, path: pathlib.Path) -> str: + """Return the file-based module path represented by one Rust source file.""" + + relative = path.relative_to(crate / "src").with_suffix("") + parts = list(relative.parts) + if parts and parts[-1] == "mod": + parts.pop() + return "::".join(parts) + + +def declaration_candidates(crate: pathlib.Path, path: pathlib.Path) -> list[Declaration]: + """Return root-level public or crate-public declarations from one module file.""" + + text = path.read_text(encoding="utf-8") + lines = text.splitlines() + masked, _ = mask_source(text) + depths = line_depths(masked) + pattern = re.compile( + r"^\s*(pub(?:\(crate\))?)\s+" + r"(?:(?:async|unsafe|const)\s+)*" + r"(const|static|type|struct|enum|trait|union|fn)\s+" + r"([A-Za-z_][A-Za-z0-9_]*)" + ) + found: list[Declaration] = [] + for line_number, line in enumerate(lines, 1): + if line_number - 1 >= len(depths) or depths[line_number - 1] != 0: + continue + match = pattern.match(line) + if match is None: + continue + found.append( + Declaration( + module=module_path(crate, path), + name=match.group(3), + visibility=match.group(1), + kind=match.group(2), + path=path, + line=line_number, + ) + ) + return found + + +def _parse_export_item(module: str, item: str) -> tuple[tuple[str, str], str] | None: + """Parse one simple explicit item from a crate-root re-export.""" + + stripped = item.strip() + if not stripped or stripped == "*" or "{" in stripped or "}" in stripped: + return None + alias_match = re.fullmatch( + r"([A-Za-z_][A-Za-z0-9_]*)(?:\s+as\s+([A-Za-z_][A-Za-z0-9_]*))?", + stripped, + ) + if alias_match is None: + return None + name = alias_match.group(1) + alias = alias_match.group(2) or name + return (module, name), alias + + +def root_exports(crate: pathlib.Path) -> dict[tuple[str, str], str]: + """Return explicit crate-root re-exports keyed by source module and symbol.""" + + crate_root = crate_root_source(crate) + exports: dict[tuple[str, str], str] = {} + if crate_root is None: + return exports + + text = crate_root.read_text(encoding="utf-8") + pattern = re.compile( + r"^\s*pub(?:\(crate\))?\s+use\s+(?:self|crate)::(.*?);", + re.MULTILINE | re.DOTALL, + ) + for match in pattern.finditer(text): + body = " ".join(part.strip() for part in match.group(1).splitlines()) + if "{" in body and body.endswith("}"): + prefix, group = body.split("{", 1) + module = prefix.rstrip().rstrip(":") + for item in group[:-1].split(","): + parsed = _parse_export_item(module, item) + if parsed is not None: + key, alias = parsed + exports[key] = alias + continue + if "::" not in body: + continue + module, item = body.rsplit("::", 1) + parsed = _parse_export_item(module, item) + if parsed is not None: + key, alias = parsed + exports[key] = alias + return exports + + +def externally_used(crate: pathlib.Path, declaration: Declaration) -> bool: + """Return whether a crate-public item is clearly used outside its module file.""" + + direct = f"crate::{declaration.module}::{declaration.name}" + root_direct = f"crate::{declaration.name}" + for path in sorted(crate.rglob("*.rs")): + if _is_ignored(path, crate) or path.resolve() == declaration.path.resolve(): + continue + masked, _ = mask_source(path.read_text(encoding="utf-8")) + if direct in masked or root_direct in masked: + return True + return False + + +def audit_crate(workspace: pathlib.Path, crate: pathlib.Path) -> list[Candidate]: + """Audit one Rust package for the selected strict façade contract.""" + + crate_root = crate_root_source(crate) + if crate_root is None: + return [] + exports = root_exports(crate) + candidates: list[Candidate] = [] + + for path in sorted((crate / "src").rglob("*.rs")): + if path == crate_root or _is_ignored(path, crate): + continue + for declaration in declaration_candidates(crate, path): + required = declaration.visibility == "pub" or externally_used(crate, declaration) + if required and (declaration.module, declaration.name) not in exports: + reason = "public item" if declaration.visibility == "pub" else "crate-public item used outside its module" + candidates.append( + Candidate( + code="RUST-API-201", + path=path.relative_to(workspace).as_posix(), + line=declaration.line, + message=f"{reason} `{declaration.name}` requires an explicit crate-root re-export", + ) + ) + + long_path = re.compile(r"\bcrate::([A-Za-z_][A-Za-z0-9_:]*)::([A-Za-z_][A-Za-z0-9_]*)") + for path in sorted(crate.rglob("*.rs")): + if path == crate_root or _is_ignored(path, crate): + continue + relative = path.relative_to(workspace).as_posix() + masked, _ = mask_source(path.read_text(encoding="utf-8")) + for line_number, line in enumerate(masked.splitlines(), 1): + for match in long_path.finditer(line): + alias = exports.get((match.group(1), match.group(2))) + if alias is None: + continue + candidates.append( + Candidate( + code="RUST-IMPORT-201", + path=relative, + line=line_number, + message=( + f"long same-crate path `crate::{match.group(1)}::{match.group(2)}` " + f"must use crate-root `crate::{alias}`" + ), + ) + ) + return candidates + + +def audit_workspace(root: pathlib.Path) -> tuple[list[pathlib.Path], list[Candidate]]: + """Return discovered crates and all façade candidates below one project root.""" + + crates = crate_roots(root) + candidates = [candidate for crate in crates for candidate in audit_crate(root, crate)] + candidates.sort(key=lambda candidate: (candidate.code, candidate.path, candidate.line, candidate.message)) + return crates, candidates + + +def main() -> int: + """Run the export-completeness audit.""" + + parser = argparse.ArgumentParser() + parser.add_argument("--root", default=".") + parser.add_argument("--report-only", action="store_true") + parser.add_argument("--summary-only", action="store_true") + arguments = parser.parse_args() + + workspace = pathlib.Path(arguments.root).resolve() + if not workspace.is_dir(): + sys.stderr.write(f"Rust export completeness audit: invalid root: {workspace}\n") + return 2 + + try: + crates, candidates = audit_workspace(workspace) + except (OSError, UnicodeError) as error: + sys.stderr.write(f"Rust export completeness audit: unable to read selected workspace: {error}\n") + return 2 + + if not crates: + sys.stderr.write("Rust export completeness audit: no Rust crate root discovered\n") + return 2 + + counts: dict[str, int] = {} + for candidate in candidates: + counts[candidate.code] = counts.get(candidate.code, 0) + 1 + + sys.stdout.write(f"Rust export completeness audit: {len(candidates)} candidate(s) across {len(crates)} crate(s)\n") + for code in sorted(counts): + sys.stdout.write(f"{code}: {counts[code]}\n") + if not arguments.summary_only: + for candidate in candidates: + sys.stdout.write(f"{candidate.code} {candidate.path}:{candidate.line}: {candidate.message}\n") + + return 0 if arguments.report_only or not candidates else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/test_rust_export_completeness.py b/tests/test_rust_export_completeness.py new file mode 100644 index 0000000..a0aeed7 --- /dev/null +++ b/tests/test_rust_export_completeness.py @@ -0,0 +1,132 @@ +# file: tests/test_rust_export_completeness.py +# version: 1 + +"""Regression tests for the optional Rust crate-root façade audit.""" + +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_export_completeness import audit_workspace # noqa: E402 + + +class RustExportCompletenessTests(unittest.TestCase): + def setUp(self) -> None: + self.temporary = tempfile.TemporaryDirectory() + self.addCleanup(self.temporary.cleanup) + self.root = pathlib.Path(self.temporary.name) + + def crate(self, relative: str, root_source: str, files: dict[str, str]) -> pathlib.Path: + crate = self.root / relative + (crate / "src").mkdir(parents=True) + (crate / "Cargo.toml").write_text( + '[package]\nname = "fixture"\nversion = "0.1.0"\nedition = "2024"\n', + encoding="utf-8", + ) + (crate / "src/lib.rs").write_text(root_source, encoding="utf-8") + for name, text in files.items(): + path = crate / name + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(text, encoding="utf-8") + return crate + + def candidates(self) -> list[object]: + _, candidates = audit_workspace(self.root) + return candidates + + def test_nested_crate_layout_is_discovered(self) -> None: + self.crate("crates/common/example-lib", "mod api;\npub use self::api::Thing;\n", {"src/api.rs": "pub struct Thing;\n"}) + crates, candidates = audit_workspace(self.root) + self.assertEqual(len(crates), 1) + self.assertEqual(candidates, []) + + def test_missing_public_reexport_is_rejected(self) -> None: + self.crate("crates/example-lib", "mod api;\n", {"src/api.rs": "pub struct Thing;\n"}) + candidates = self.candidates() + self.assertEqual([candidate.code for candidate in candidates], ["RUST-API-201"]) + + def test_external_crate_public_item_requires_reexport(self) -> None: + self.crate( + "crates/example-lib", + "mod api;\nmod consumer;\n", + { + "src/api.rs": "pub(crate) const VALUE: u8 = 1;\n", + "src/consumer.rs": "pub(crate) fn value() -> u8 { return crate::api::VALUE; }\n", + }, + ) + self.assertTrue(any(candidate.code == "RUST-API-201" for candidate in self.candidates())) + + def test_unused_crate_public_item_does_not_force_reexport(self) -> None: + self.crate("crates/example-lib", "mod api;\n", {"src/api.rs": "pub(crate) const VALUE: u8 = 1;\n"}) + self.assertEqual(self.candidates(), []) + + def test_exported_item_requires_crate_root_path(self) -> None: + self.crate( + "crates/example-lib", + "mod api;\nmod consumer;\npub use self::api::Thing;\n", + { + "src/api.rs": "pub struct Thing;\n", + "src/consumer.rs": "pub(crate) fn make() -> crate::api::Thing { return crate::api::Thing; }\n", + }, + ) + candidates = self.candidates() + self.assertTrue(any(candidate.code == "RUST-IMPORT-201" for candidate in candidates)) + + def test_grouped_and_aliased_reexports_are_understood(self) -> None: + self.crate( + "crates/example-lib", + "mod api;\npub use self::api::{First, Second as PublicSecond};\n", + {"src/api.rs": "pub struct First;\npub struct Second;\n"}, + ) + self.assertEqual(self.candidates(), []) + + def test_mod_rs_maps_to_parent_module(self) -> None: + self.crate( + "crates/example-lib", + "mod api;\npub use self::api::Thing;\n", + {"src/api/mod.rs": "pub struct Thing;\n"}, + ) + self.assertEqual(self.candidates(), []) + + def test_comments_and_literals_do_not_create_long_path_candidates(self) -> None: + self.crate( + "crates/example-lib", + "mod api;\nmod consumer;\npub use self::api::Thing;\n", + { + "src/api.rs": "pub struct Thing;\n", + "src/consumer.rs": 'pub(crate) fn text() -> &\'static str { return "crate::api::Thing"; } // crate::api::Thing\n', + }, + ) + self.assertEqual(self.candidates(), []) + + def test_cli_status_for_clean_violation_report_only_and_missing_crates(self) -> None: + script = pathlib.Path(__file__).resolve().parents[1] / "scripts" / "audit_rust_export_completeness.py" + + def run(root: pathlib.Path, *extra: str) -> subprocess.CompletedProcess[str]: + return subprocess.run( + [sys.executable, "-B", str(script), "--root", str(root), *extra], + capture_output=True, + text=True, + check=False, + ) + + empty = self.root / "empty" + empty.mkdir() + self.assertEqual(run(empty).returncode, 2) + + self.crate("crates/example-lib", "mod api;\npub use self::api::Thing;\n", {"src/api.rs": "pub struct Thing;\n"}) + self.assertEqual(run(self.root).returncode, 0) + + (self.root / "crates/example-lib/src/lib.rs").write_text("mod api;\n", encoding="utf-8") + self.assertEqual(run(self.root).returncode, 1) + self.assertEqual(run(self.root, "--report-only").returncode, 0) + + +if __name__ == "__main__": + unittest.main()