0.1.4-alpha.2.fix.1

This commit is contained in:
2026-09-29 13:36:20 +02:00
parent ed794d66a0
commit 9b140e85b3
8 changed files with 549 additions and 15 deletions

View File

@@ -1 +1 @@
0.1.4-alpha.2 0.1.4-alpha.2.fix.1

View File

@@ -0,0 +1,209 @@
<!-- file: deltas/0.1.4/alpha.2.fix.1.md -->
<!-- version: 1 -->
# Delta 0.1.4-alpha.2.fix.1 — hygiène EOF et prévention automatique
## Diagnostic
La gate opérateur de `0.1.4-alpha.2` a validé les `48/48` tests Python, les trois audits Markdown et le contrôle normatif Tauri, puis a échoué sur les deux contrôles Git d'hygiène :
```text
docs/studies/002-TAURI_SKELETON_DECISION.md:299: new blank line at EOF.
```
Le même défaut a été signalé par :
```text
git diff --check
git diff --cached --check
```
`history/0.1.4/alpha.2.md` enregistre donc `FAIL`. Le contenu normatif Tauri de `alpha.2` reste conservé ; le présent fix corrige uniquement l'hygiène mécanique et la lacune de prévention qui a permis la réapparition d'un défaut déjà rencontré historiquement.
Version après application :
```text
0.1.4-alpha.2.fix.1
```
## Objet
Corriger la ligne vide surnuméraire à EOF de l'étude Tauri et faire de cette classe de défaut une propriété automatiquement contrôlée avant les futures livraisons PGR.
Le correctif introduit un audit monotâche read-only au lieu d'ajouter une vérification ad hoc dans chaque delta.
## Scope
REPLACE :
```text
VERSION
docs/rules/210-WORKFLOW.md
docs/studies/002-TAURI_SKELETON_DECISION.md
docs/validation/001-VALIDATION_MATRIX.md
```
ADD :
```text
deltas/0.1.4/alpha.2.fix.1.md
scripts/audit_text_hygiene.py
tests/test_text_hygiene.py
```
Les autres décisions de `alpha.2`, notamment `docs/rules/710-TAURI.md`, le plan `010` et `docs/000-README.md`, MUST rester inchangées.
## Correction EOF
`docs/studies/002-TAURI_SKELETON_DECISION.md` conserve son contenu et sa révision interne `3`. La seule modification de ce fichier est mécanique : sa terminaison passe de deux caractères LF successifs à exactement un LF final.
Aucune décision Tauri n'est modifiée par cette correction.
## Audit `text_hygiene`
`scripts/audit_text_hygiene.py` possède quatre propriétés mécaniques sur les fichiers texte sélectionnés :
```text
LF uniquement
aucun whitespace terminal
newline finale obligatoire
exactement une newline finale, donc aucune ligne vide supplémentaire à EOF
```
L'audit reste read-only, accepte des fichiers ou répertoires explicites et ignore les répertoires générés usuels. Il ne remplace pas un formatter, un parser Markdown ou `git diff --check`.
`tests/test_text_hygiene.py` couvre au minimum :
- fichier propre avec exactement un LF final ;
- ligne vide supplémentaire à EOF ;
- newline finale absente ;
- CRLF ;
- espace ou tabulation terminale ;
- exclusion d'un répertoire généré ;
- sélection invalide.
## Contrat de workflow
`docs/rules/210-WORKFLOW.md` passe de la révision `7` à `8` et rend explicite que le générateur doit contrôler l'hygiène mécanique avant livraison, plutôt que déléguer sa découverte à la gate opérateur.
`docs/validation/001-VALIDATION_MATRIX.md` passe de la révision `14` à `15` et ajoute `PGR-TEXT-HYGIENE-001` comme audit faible coût déclenché lorsqu'un fichier texte gouverné est modifié.
Le contrôle Git reste conservé comme filet indépendant.
## Gate opérateur
Après application puis commit/push du history `FAIL` de `alpha.2`, appliquer ce delta et exécuter :
```bash
cat VERSION
test -f history/0.1.4/alpha.2.md
grep -A2 '^## Statut$' history/0.1.4/alpha.2.md | grep -Fx 'FAIL'
test -f deltas/0.1.4/alpha.2.fix.1.md
test ! -e history/0.1.4/alpha.2.fix.1.md
test -f scripts/audit_text_hygiene.py
test -f tests/test_text_hygiene.py
python3 -B -m unittest discover -s tests -v
python3 -B scripts/audit_text_hygiene.py \
VERSION README.md RULES.md ROADMAP.md CHANGELOG.md \
docs deltas history prompts scripts tests templates .gitignore .settings
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
assert Path('VERSION').read_text(encoding='utf-8').strip() == '0.1.4-alpha.2.fix.1'
history = Path('history/0.1.4/alpha.2.md').read_text(encoding='utf-8')
assert re.search(r'## Statut\s+FAIL', history)
study_path = Path('docs/studies/002-TAURI_SKELETON_DECISION.md')
study = study_path.read_bytes()
assert study.endswith(b'\n')
assert not study.endswith(b'\n\n')
assert study_path.read_text(encoding='utf-8').startswith(
'<!-- file: docs/studies/002-TAURI_SKELETON_DECISION.md -->\n'
'<!-- version: 3 -->'
)
workflow = Path('docs/rules/210-WORKFLOW.md').read_text(encoding='utf-8')
assert workflow.startswith(
'<!-- file: docs/rules/210-WORKFLOW.md -->\n'
'<!-- version: 8 -->'
)
for phrase in (
'## Hygiène mécanique des artefacts texte',
'exactement une seule newline finale',
'scripts/audit_text_hygiene.py',
'Un générateur MUST corriger un défaut mécanique détectable avant livraison',
):
assert phrase in workflow, phrase
matrix = Path('docs/validation/001-VALIDATION_MATRIX.md').read_text(encoding='utf-8')
assert matrix.startswith(
'<!-- file: docs/validation/001-VALIDATION_MATRIX.md -->\n'
'<!-- version: 15 -->'
)
assert '`PGR-TEXT-HYGIENE-001`' in matrix
assert 'exactement une seule newline finale' in matrix
assert Path('scripts/audit_text_hygiene.py').exists()
assert Path('tests/test_text_hygiene.py').exists()
for path, version in (
('docs/rules/710-TAURI.md', '7'),
('docs/plans/010-V0_1_4_TAURI_REEVALUATION_PLAN.md', '2'),
('docs/000-README.md', '42'),
):
lines = Path(path).read_text(encoding='utf-8').splitlines()
assert lines[1] == f'<!-- version: {version} -->', (path, lines[1])
assert not Path('docs/skeletons/004-TAURI.md').exists()
assert not Path('history/0.1.4/alpha.2.fix.1.md').exists()
print(
'0.1.4 alpha.2.fix.1 hygiene audit: clean '
'(EOF corrected, reusable text hygiene audit active, Tauri decisions preserved)'
)
PY
git diff --check
git add -A
git diff --cached --check
git status --short
```
Le scope indexé attendu est :
```text
M VERSION
A deltas/0.1.4/alpha.2.fix.1.md
M docs/rules/210-WORKFLOW.md
M docs/studies/002-TAURI_SKELETON_DECISION.md
M docs/validation/001-VALIDATION_MATRIX.md
A scripts/audit_text_hygiene.py
A tests/test_text_hygiene.py
```
L'ordre d'affichage peut varier ; l'ensemble des chemins doit être identique.
## Transition
### PASS
Produire `history/0.1.4/alpha.2.fix.1.md`, commit/push sous l'identité `0.1.4-alpha.2.fix.1`, puis poursuivre avec `alpha.3` sur cette baseline propre. Les gates suivantes de `0.1.4` doivent sélectionner `PGR-TEXT-HYGIENE-001` pour les fichiers texte modifiés.
### FAIL
Enregistrer le résultat réel dans `history/0.1.4/alpha.2.fix.1.md` et poursuivre sous `alpha.2.fix.N`. Ne pas commencer `alpha.3` tant que l'hygiène texte et le scope du correctif ne sont pas `PASS`.

View File

@@ -1,5 +1,5 @@
<!-- file: docs/rules/210-WORKFLOW.md --> <!-- file: docs/rules/210-WORKFLOW.md -->
<!-- version: 7 --> <!-- version: 8 -->
# Workflow artifact rules # Workflow artifact rules
@@ -163,6 +163,16 @@ Les deux archives restent séparées. Cette optimisation de flux n'autorise jama
Le delta suivant SHOULD NOT être anticipé lorsqu'une décision opérateur, un REPLAN non résolu, un abandon/supersession, une preuve externe post-tag ou toute autre dépendance peut encore modifier sa baseline ou son scope. Le delta suivant SHOULD NOT être anticipé lorsqu'une décision opérateur, un REPLAN non résolu, un abandon/supersession, une preuve externe post-tag ou toute autre dépendance peut encore modifier sa baseline ou son scope.
## Hygiène mécanique des artefacts texte
Les fichiers texte de gouvernance ajoutés ou remplacés MUST être livrés avec des fins de ligne LF, sans whitespace terminal et avec exactement une seule newline finale. Une ligne vide supplémentaire à EOF constitue un défaut de livraison même si le contenu sémantique du document est inchangé.
Avant remise d'un delta ou d'un history, le générateur MUST contrôler cette propriété sur les fichiers texte concernés. Le dépôt PGR fournit `scripts/audit_text_hygiene.py` comme audit read-only monotâche réutilisable ; il contrôle les CR, le whitespace terminal, l'absence de newline finale et les lignes vides terminales surnuméraires.
`git diff --check` et `git diff --cached --check` restent obligatoires lorsqu'ils sont sélectionnés par la gate Git applicable. Ils constituent un filet indépendant et ne remplacent pas l'audit d'hygiène lorsque celui-ci est disponible dans le projet.
Un générateur MUST corriger un défaut mécanique détectable avant livraison plutôt que compter sur la gate opérateur pour le découvrir. Si la gate opérateur détecte malgré tout ce défaut, le résultat réel reste `FAIL` et la correction suit le workflow `.fix.N`.
## Commit de tranche ## Commit de tranche
Le commit d'une tranche doit représenter l'état complet de cette tranche, y compris son history. Le commit d'une tranche doit représenter l'état complet de cette tranche, y compris son history.

View File

@@ -296,4 +296,3 @@ Le réexamen ne démontre toujours pas le besoin de créer en `alpha.2` :
- un owner iOS, serveur Rust ou deployment/operations par proximité. - un owner iOS, serveur Rust ou deployment/operations par proximité.
Ces décisions restent à réévaluer après matérialisation de la specification, conformément au plan `0.1.4`. Ces décisions restent à réévaluer après matérialisation de la specification, conformément au plan `0.1.4`.

View File

@@ -1,5 +1,5 @@
<!-- file: docs/validation/001-VALIDATION_MATRIX.md --> <!-- file: docs/validation/001-VALIDATION_MATRIX.md -->
<!-- version: 14 --> <!-- version: 15 -->
# Matrice de validation PGR # Matrice de validation PGR
@@ -13,20 +13,23 @@ Les catégories et règles de sélection sont définies dans [`../rules/400-VALI
## Contrôles actifs ## Contrôles actifs
| ID | Catégorie | Commande / contrôle | Prérequis | Trigger | Phase | Coût | Acteur | | ID | Catégorie | Commande / contrôle | Prérequis | Trigger | Phase | Coût | Acteur |
|---------------------|-----------|--------------------------------------------------------------------------------------|------------------------|---------------------------------|----------------------|--------|-----------------| |------------------------|-----------|--------------------------------------------------------------------------------------|------------------------|---------------------------------|----------------------|--------|-----------------|
| `PGR-GIT-001` | `CHECK` | `git diff --check` ou `git diff --cached --check` selon l'état de l'index | Git | toute modification suivie | alpha/beta/RC/stable | faible | opérateur | | `PGR-GIT-001` | `CHECK` | `git diff --check` ou `git diff --cached --check` selon l'état de l'index | Git | toute modification suivie | alpha/beta/RC/stable | faible | opérateur |
| `PGR-VERSION-001` | `CHECK` | comparer `VERSION` à la version attendue par le delta courant | fichier `VERSION` | changement de tranche/version | alpha/beta/RC/stable | faible | opérateur | | `PGR-TEXT-HYGIENE-001` | `AUDIT` | `python3 -B scripts/audit_text_hygiene.py <paths...>` | Python 3 | fichier texte gouverné modifié | alpha/beta/RC/stable | faible | opérateur/CI |
| `PGR-LAYOUT-001` | `AUDIT` | comparer les fichiers ajoutés/remplacés/supprimés au scope déclaré du delta | scope du delta | changement de structure | alpha/beta/RC/stable | faible | opérateur/revue | | `PGR-VERSION-001` | `CHECK` | comparer `VERSION` à la version attendue par le delta courant | fichier `VERSION` | changement de tranche/version | alpha/beta/RC/stable | faible | opérateur |
| `PGR-DOC-001` | `AUDIT` | vérifier chemins, rôles et responsabilités des documents touchés | fichiers documentaires | modification documentaire | alpha/beta/RC/stable | faible | opérateur/revue | | `PGR-LAYOUT-001` | `AUDIT` | comparer les fichiers ajoutés/remplacés/supprimés au scope déclaré du delta | scope du delta | changement de structure | alpha/beta/RC/stable | faible | opérateur/revue |
| `PGR-MD-HEADER-001` | `AUDIT` | `python3 -B scripts/audit_markdown_headers.py <paths...>` | Python 3 | Markdown gouverné modifié | alpha/beta/RC/stable | faible | opérateur/CI | | `PGR-DOC-001` | `AUDIT` | vérifier chemins, rôles et responsabilités des documents touchés | fichiers documentaires | modification documentaire | alpha/beta/RC/stable | faible | opérateur/revue |
| `PGR-MD-TABLE-001` | `AUDIT` | `python3 -B scripts/audit_markdown_tables.py <paths...>` | Python 3 | table Markdown ajoutée/modifiée | alpha/beta/RC/stable | faible | opérateur/CI | | `PGR-MD-HEADER-001` | `AUDIT` | `python3 -B scripts/audit_markdown_headers.py <paths...>` | Python 3 | Markdown gouverné modifié | alpha/beta/RC/stable | faible | opérateur/CI |
| `PGR-NAV-001` | `AUDIT` | `python3 -B scripts/audit_markdown_links.py <paths...>` | Python 3 | navigation ou réconciliation | alpha/beta/RC/stable | faible | opérateur/CI | | `PGR-MD-TABLE-001` | `AUDIT` | `python3 -B scripts/audit_markdown_tables.py <paths...>` | Python 3 | table Markdown ajoutée/modifiée | alpha/beta/RC/stable | faible | opérateur/CI |
| `PGR-SCOPE-001` | `AUDIT` | vérifier que les documents vivants pointent vers les propriétaires normatifs actuels | documents vivants | réconciliation de phase | beta/RC/stable | faible | revue | | `PGR-NAV-001` | `AUDIT` | `python3 -B scripts/audit_markdown_links.py <paths...>` | Python 3 | navigation ou réconciliation | alpha/beta/RC/stable | faible | opérateur/CI |
| `PGR-STATUS-001` | `CHECK` | `git status --short` | Git | avant fermeture d'une tranche | alpha/beta/RC/stable | faible | opérateur | | `PGR-SCOPE-001` | `AUDIT` | vérifier que les documents vivants pointent vers les propriétaires normatifs actuels | documents vivants | réconciliation de phase | beta/RC/stable | faible | revue |
| `PGR-STATUS-001` | `CHECK` | `git status --short` | Git | avant fermeture d'une tranche | alpha/beta/RC/stable | faible | opérateur |
Un contrôle manuel peut rester dans la matrice lorsqu'aucun audit automatisé fiable n'existe encore. PGR SHOULD automatiser uniquement les propriétés stables et objectivement contrôlables. Un contrôle manuel peut rester dans la matrice lorsqu'aucun audit automatisé fiable n'existe encore. PGR SHOULD automatiser uniquement les propriétés stables et objectivement contrôlables.
`PGR-TEXT-HYGIENE-001` est un contrôle mécanique indépendant du sens documentaire. Sur les fichiers texte sélectionnés, il exige des fins de ligne LF, interdit le whitespace terminal et exige exactement une seule newline finale ; une ligne vide supplémentaire à EOF est donc rejetée avant commit. Il complète les contrôles Git au lieu de les remplacer.
`PGR-MD-TABLE-001` reste volontairement monotâche : il contrôle uniquement la structure et l'alignement source des tableaux Markdown. Il couvre les tableaux Markdown ordinaires ainsi que les exemples placés dans des fences explicitement `markdown` ou `md`. Les autres blocs fenced restent ignorés afin de ne pas interpréter comme Markdown des sorties terminal, extraits de code ou tableaux ASCII d'un autre langage. `PGR-MD-TABLE-001` reste volontairement monotâche : il contrôle uniquement la structure et l'alignement source des tableaux Markdown. Il couvre les tableaux Markdown ordinaires ainsi que les exemples placés dans des fences explicitement `markdown` ou `md`. Les autres blocs fenced restent ignorés afin de ne pas interpréter comme Markdown des sorties terminal, extraits de code ou tableaux ASCII d'un autre langage.
`PGR-MD-HEADER-001` contrôle seulement les deux premières lignes des fichiers Markdown sélectionnés : chemin relatif depuis la racine et révision interne positive. La revue `PGR-DOC-001` reste responsable du rôle documentaire et de la pertinence réelle d'un incrément de révision. `PGR-MD-HEADER-001` contrôle seulement les deux premières lignes des fichiers Markdown sélectionnés : chemin relatif depuis la racine et révision interne positive. La revue `PGR-DOC-001` reste responsable du rôle documentaire et de la pertinence réelle d'un incrément de révision.

View File

@@ -0,0 +1,94 @@
<!-- file: history/0.1.4/alpha.2.fix.1.md -->
<!-- version: 1 -->
# History 0.1.4-alpha.2.fix.1 — hygiène texte rétablie
## Statut
PASS
La gate opérateur de `0.1.4-alpha.2.fix.1` exécutée le `2026-09-29` est propre. Le défaut de ligne vide surnuméraire à EOF est corrigé, l'audit réutilisable d'hygiène texte est actif et les décisions Tauri validées en `alpha.2` restent inchangées.
## Baseline opérateur
La tranche `0.1.4-alpha.2` en échec a été committée puis publiée avant application du fix :
```text
commit alpha.2 ed794d6
push 7f47558..ed794d6 master -> master
```
La version observée pendant la gate du fix est :
```text
0.1.4-alpha.2.fix.1
```
`history/0.1.4/alpha.2.md` est présent avec le statut `FAIL`, tandis que `history/0.1.4/alpha.2.fix.1.md` était absent pendant la gate.
## Tests Python
La suite complète a affiché :
```text
Ran 55 tests in 0.890s
OK
```
Soit `55/55` tests `PASS`, dont les sept cas ciblés du nouvel audit `text_hygiene`.
## Audit d'hygiène texte
Le nouvel audit a affiché :
```text
Text hygiene audit: clean (297 file(s))
```
Il confirme sur le corpus sélectionné :
- fins de ligne LF ;
- absence de whitespace terminal ;
- newline finale présente ;
- absence de ligne vide terminale surnuméraire.
## Audits Markdown
Les audits documentaires sont propres :
```text
Markdown header audit: clean (270 file(s))
Markdown table audit: clean (11 table(s), 270 file(s))
Markdown link audit: clean (344 local link(s), 270 file(s))
```
## Hygiène Git et scope observé
`git diff --check` puis `git diff --cached --check` n'ont produit aucun diagnostic.
Le scope indexé observé est exactement :
```text
M VERSION
A deltas/0.1.4/alpha.2.fix.1.md
M docs/rules/210-WORKFLOW.md
M docs/studies/002-TAURI_SKELETON_DECISION.md
M docs/validation/001-VALIDATION_MATRIX.md
A scripts/audit_text_hygiene.py
A tests/test_text_hygiene.py
```
La révision interne de l'étude Tauri reste `3` : seule sa terminaison de fichier a été corrigée.
## Observation de livraison post-gate
Après la gate, l'opérateur a constaté que les ZIP remis pour `alpha.2.history` et `alpha.2.fix.1.delta` contenaient à tort un répertoire englobant `pgr/`. Le contenu appliqué au dépôt est correct et la gate présente reste `PASS`, mais ce défaut de packaging doit être corrigé dans les archives remises et explicitement interdit par le contrat de workflow avant les livraisons suivantes.
Cette observation ne modifie pas les fichiers validés par le fix ; elle constitue un durcissement du mécanisme de livraison à intégrer dès `alpha.3`.
## Décision
`0.1.4-alpha.2.fix.1` est validée `PASS`.
`alpha.3` peut matérialiser `docs/skeletons/004-TAURI.md` sur la baseline Tauri réconciliée. La tranche doit également clarifier que les entrées d'une archive delta/history sont relatives directement à la racine du dépôt, sans répertoire englobant `<project>/`, afin que l'application canonique `unzip -o <archive> -d .` reste correcte.

View File

@@ -0,0 +1,136 @@
#!/usr/bin/env python3
# file: scripts/audit_text_hygiene.py
# version: 1
"""Validate mechanical hygiene of selected governed text files."""
from __future__ import annotations
import argparse
import pathlib
import sys
_IGNORED_DIRECTORIES = frozenset({".git", ".idea", ".venv", "__pycache__", "dist", "node_modules", "target"})
_TEXT_SUFFIXES = frozenset(
{
".cfg",
".conf",
".css",
".gradle",
".html",
".ini",
".js",
".json",
".jsx",
".kts",
".md",
".prefs",
".properties",
".py",
".rs",
".scss",
".sh",
".toml",
".ts",
".tsx",
".txt",
".xml",
".yaml",
".yml",
}
)
_TEXT_NAMES = frozenset({".gitignore", "VERSION"})
def _is_governed_text(path: pathlib.Path) -> bool:
return path.name in _TEXT_NAMES or path.suffix.lower() in _TEXT_SUFFIXES
def _selected_files(raw_paths: list[str], root: pathlib.Path) -> tuple[list[pathlib.Path], list[str]]:
files: set[pathlib.Path] = set()
errors: list[str] = []
for raw_path in raw_paths:
path = pathlib.Path(raw_path)
if path.is_dir() or path.is_file():
if not path.resolve().is_relative_to(root):
errors.append(f"Text hygiene audit: path outside project root: {path}")
continue
if path.is_dir():
candidates = (
candidate
for candidate in path.rglob("*")
if candidate.is_file()
and _is_governed_text(candidate)
and not any(part in _IGNORED_DIRECTORIES for part in candidate.parts)
)
elif path.is_file() and _is_governed_text(path):
candidates = iter((path,))
else:
errors.append(f"Text hygiene audit: unsupported or missing path: {path}")
continue
for candidate in candidates:
canonical = candidate.resolve()
if not canonical.is_relative_to(root):
errors.append(f"Text hygiene audit: path outside project root: {candidate}")
else:
files.add(canonical)
return sorted(files), errors
def _audit_file(path: pathlib.Path, root: pathlib.Path) -> list[str]:
relative = path.relative_to(root).as_posix()
data = path.read_bytes()
errors: list[str] = []
if b"\r" in data:
errors.append(f"{relative}: CR byte found; governed text must use LF line endings")
if not data.endswith(b"\n"):
errors.append(f"{relative}: EOF: missing final LF")
elif data.endswith(b"\n\n"):
errors.append(f"{relative}: EOF: extra blank line; exactly one final LF is required")
for line_number, line in enumerate(data.split(b"\n")[:-1], start=1):
if line.endswith((b" ", b"\t")):
errors.append(f"{relative}:{line_number}: trailing whitespace")
return errors
def main() -> int:
"""Run the read-only text hygiene audit from the project root."""
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("paths", nargs="+", help="Governed text files or directories to audit")
args = parser.parse_args()
root = pathlib.Path.cwd().resolve()
files, selection_errors = _selected_files(args.paths, root)
if selection_errors or not files:
for error in selection_errors or ["Text hygiene audit: no governed text files selected"]:
print(error, file=sys.stderr)
return 2
errors: list[str] = []
for path in files:
try:
errors.extend(_audit_file(path, root))
except OSError as error:
print(f"Text hygiene audit: cannot read {path}: {error}", file=sys.stderr)
return 2
for error in errors:
print(error, file=sys.stderr)
if errors:
print(f"Text hygiene audit: {len(errors)} error(s) across {len(files)} file(s)", file=sys.stderr)
return 1
print(f"Text hygiene audit: clean ({len(files)} file(s))")
return 0
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -0,0 +1,83 @@
# file: tests/test_text_hygiene.py
# version: 1
"""Exercise the governed text hygiene audit's real file and CLI contracts."""
from __future__ import annotations
import pathlib
import subprocess
import sys
import tempfile
import unittest
_SCRIPT = pathlib.Path(__file__).resolve().parents[1] / "scripts/audit_text_hygiene.py"
class TextHygieneAuditTests(unittest.TestCase):
def setUp(self) -> None:
temporary = tempfile.TemporaryDirectory(prefix="pgr-text-hygiene-")
self.addCleanup(temporary.cleanup)
self.root = pathlib.Path(temporary.name)
def source(self, name: str, contents: bytes) -> pathlib.Path:
path = self.root / name
path.parent.mkdir(parents=True, exist_ok=True)
path.write_bytes(contents)
return path
def run_audit(self, *paths: str) -> subprocess.CompletedProcess[str]:
return subprocess.run(
[sys.executable, "-B", str(_SCRIPT), *paths],
cwd=self.root,
capture_output=True,
text=True,
check=False,
)
def test_exactly_one_final_lf_is_clean(self) -> None:
self.source("README.md", b"line one\nline two\n")
result = self.run_audit("README.md")
self.assertEqual(result.returncode, 0, result.stderr)
self.assertIn("clean (1 file(s))", result.stdout)
def test_extra_blank_line_at_eof_is_rejected(self) -> None:
self.source("docs/page.md", b"content\n\n")
result = self.run_audit("docs/page.md")
self.assertEqual(result.returncode, 1)
self.assertIn("extra blank line", result.stderr)
def test_missing_final_lf_is_rejected(self) -> None:
self.source("VERSION", b"1.2.3")
result = self.run_audit("VERSION")
self.assertEqual(result.returncode, 1)
self.assertIn("missing final LF", result.stderr)
def test_crlf_is_rejected(self) -> None:
self.source("config.toml", b"value = true\r\n")
result = self.run_audit("config.toml")
self.assertEqual(result.returncode, 1)
self.assertIn("CR byte found", result.stderr)
def test_trailing_space_and_tab_are_rejected(self) -> None:
self.source("docs/page.md", b"first \nsecond\t\n")
result = self.run_audit("docs/page.md")
self.assertEqual(result.returncode, 1)
self.assertIn("docs/page.md:1: trailing whitespace", result.stderr)
self.assertIn("docs/page.md:2: trailing whitespace", result.stderr)
def test_generated_directory_is_skipped(self) -> None:
self.source("docs/visible.md", b"clean\n")
self.source("docs/target/generated.md", b"bad\n\n")
result = self.run_audit("docs")
self.assertEqual(result.returncode, 0, result.stderr)
self.assertIn("clean (1 file(s))", result.stdout)
def test_missing_path_is_a_selection_error(self) -> None:
result = self.run_audit("missing.md")
self.assertEqual(result.returncode, 2)
self.assertIn("unsupported or missing path", result.stderr)
if __name__ == "__main__":
unittest.main()