Portabilität: Pfadtrenner, Zeilenenden, Encoding und Locks unter Windows #152

Closed
opened 2026-09-27 08:44:19 +00:00 by torben · 4 comments
Owner

Teilpaket von #140 (D8: Windows nativ, PowerShell 7).

Erledigt (2026-10-01): d0f08d1, Kandidat 8.0.0-beta.19. CI grün: Run 484 (verify und pwsh), Run 485 (release). Lokal vor dem Publish: pytest (1969 bestanden, 3 übersprungen), docs verify und instructions verify grün. Die Handprüfung der Ausgabe auf dem Windows-Zielsystem führt der Nachlauf von #140 („Ausgabe ohne Kodierungsfehler“).

Warum

Der Python-Code setzte an mehreren Stellen POSIX voraus. Im analysierten Lauf (#140) scheiterte instructions verify unter Windows an allen 23 Instruktionen. Weitere Stellen rechneten unter Windows falsch, ohne dass es jemand bemerkt hätte.

Befund (erhoben auf 8.0.0-beta.18)

  • Pfadtrenner. str(<pfad>.relative_to(...)) stand an 15 Stellen außerhalb der Tests, darunter der Selbstreferenz-Vergleich in type_resolver.py, der jede Type-Spec-Validierung brach. Dazu kamen zwei Fehlermeldungen, die einen relativen Pfad erst später in einen String verwandelten.
  • Zeilenenden. Es gab keine .gitattributes. Mit core.autocrlf=true bekam der Bash-Launcher tools/wikitool CRLF (env: 'bash\r').
  • Text-Schreibzugriffe ohne newline="\n" schrieben unter Windows CRLF. Das betraf den Byte-Vergleich der Skill-Kopien und die sha256-Summen in dist upgrade.
  • Encoding beim Lesen. 13 subprocess-Aufrufe mit text=True dekodierten unter Windows mit der Locale (cp1252). Dazu kamen ein read_text() ohne encoding (run_budget._load_state) und sys.stdin in trace_ingest.py.
  • Encoding der eigenen Ausgabe. doctor zeigte unter Copilot (pwsh) und Claude Code (Git Bash) Fu�noten, direkt in einer Konsole dagegen richtig. Python schreibt unter Windows in eine Pipe mit cp1252.
  • Locks. fcntl in run_budget.py und telemetry/writer.py. Unter Windows fiel die Sperre still weg.
  • ripgrep. rg --json liefert unter Windows kb\\comparisons\\COLLECTION.md, auch mit --path-separator / (T3).

Umgesetzt

  1. .gitattributes mit * text=auto eol=lf. Abweichung: raw/ und incoming/ sind -text, damit Quellen byte-genau bleiben (raw/CONTRACT.md, Regel „Immutable“). dist export liefert die Datei aus. Die Renormalisierung des Index änderte nichts.
  2. Pfade: Alle Stellen nutzen .as_posix(), auch die Rückfallzweige und die Fehlermeldungen.
  3. newline="\n" an jedem Text-Schreibzugriff. Abweichung: Es steht direkt am Aufruf, nicht in einem Helfer. Der Guard-Test hält es so oder so, und ein Helfer bräuchte denselben Guard.
  4. encoding="utf-8" an jedem subprocess-Aufruf mit text=True.
  5. chemenu/filelock.py: flock unter POSIX, msvcrt.locking unter Windows. Gesperrt wird ein Byte bei Offset 2³¹−2, weit hinter den Daten, weil Windows-Locks verpflichtend sind und trace_ingest.py die Trace sonst nicht lesen könnte. Bei Konkurrenz wird gewartet, wie bei flock. Die Lock-Datei des Budgets wird mit a geöffnet statt mit w.
  6. search/ripgrep.py: _record_path wandelt \ in /, nur wenn os.sep == "\\". Das Flag kommt nicht hinzu. Die Umwandlung ist verlustfrei, weil kein Windows-Pfadbestandteil und seit #155 kein Seitentitel ein \ enthalten kann.
  7. Guard-Tests in tests/test_portability.py, per AST über tools/chemenu und die Skripte daneben in tools/. Jeder Detektor wird zusätzlich mit seinem Defekt gefüttert, damit ein Guard nicht grün ist, nur weil er nichts findet. Sie schlagen fehl bei:
    • str(...relative_to(...)) und f"{...relative_to(...)}"
    • Text-Schreibzugriffen ohne newline=
    • Erweitert: jedem Text-Lese- oder -Schreibzugriff ohne encoding=
    • subprocess.*(text=True) ohne encoding=, dazu kein from subprocess import
    • import fcntl/msvcrt außerhalb von filelock.py
  8. UTF-8 für die eigene Ausgabe, in tools/run_wikitool.py, der einen Stelle für beide Launcher. Dort stellt reconfigure stdout und stderr vor main() um.
    • Messung auf dem Zielsystem (2026-10-01): Natives pwsh hat [Console]::OutputEncoding = ibm850 und $OutputEncoding = utf-8. Unter Copilot sind beide utf-8. pwsh dekodiert abgefangene Kindprozess-Ausgabe mit [Console]::OutputEncoding, deshalb reicht unter Copilot UTF-8 aus Python. Git Bash reicht die Bytes unverändert durch.
    • Verworfen: PYTHONUTF8=1 in beiden Launchern. Das wären zwei Stellen, und der geänderte open()-Default würde ein fehlendes encoding= verdecken.
    • Mitgenommen: trace_ingest.py liest stdin als UTF-8-Bytes. Ein locale-dekodierter Read scheiterte an Bytes, die in cp1252 undefiniert sind (z. B. Ł = C5 81).
    • Entschieden (Betreiber, 2026-10-01): bleibt vorerst so. Wird tools/wikitool im nativen pwsh-Fenster gepiped (| Select-String), dekodiert pwsh mit ibm850. Das war vorher schon so. Die Alternative wäre, dass wikitool.ps1 [Console]::OutputEncoding für die Dauer des Aufrufs umstellt; sie ist zurückgestellt. Ohne Pipe ist die Ausgabe richtig.

Akzeptanzkriterien

  • tools/wikitool wird mit LF ausgecheckt: git check-attr meldet text: auto und eol: lf (Test test_every_text_file_is_checked_out_with_lf). Der Index ist durchgehend i/lf.
  • rg -n 'str\([^)]*relative_to' tools/chemenu -g '!**/tests/**' findet nichts. Der Guard-Test schlägt bei str(...) und bei f-Strings fehl.
  • Jeder Text-Schreibzugriff unter tools/chemenu und in tools/*.py trägt newline= (Guard-Test).
  • Jeder subprocess-Aufruf mit text=True trägt encoding="utf-8" (Guard-Test).
  • Budget-Zähler und Telemetrie-Writer sperren auch ohne fcntl. Getestet mit ausgeblendetem fcntl-Import und einem Fake-msvcrt, einschließlich Offset, Positionserhalt und Warten bei Konkurrenz. Ein echter Windows-Lock läuft mangels Windows-Runner nicht (T12).
  • Die T3-JSON-Zeile ergibt bei simuliertem Windows-Trenner kb/comparisons/COLLECTION.md. Ein Treffer in derselben Form auf eine Fixture-Seite trägt Schlüssel, Titel, Art, Collection und Summary. Unter POSIX-Trenner bleibt der Pfad unverändert (test_search.py).
  • Unter Linux ist eine Ausgabe mit ß in eine Pipe UTF-8, auch unter simuliertem PYTHONIOENCODING=cp1252 (test_portability.py). Auf dem Windows-Zielsystem zeigt doctor Fußnoten unter Copilot, Claude Code und in beiden Konsolen – diese Handprüfung führt der Nachlauf von #140 („Ausgabe ohne Kodierungsfehler (#152)“), nicht dieses Paket.
  • pytest, docs verify und instructions verify sind grün, lokal und in CI 484.

Restrisiko, nicht in diesem Paket

run_budget.status_command liest den Budget-Zustand ohne Lock. Unter Windows kann os.replace in _save_state scheitern, wenn genau dann ein Leser die Datei offen hat. Das Zeitfenster ist klein. Ein Paket entsteht erst, wenn es im Nachlauf auftritt.

Version

minor – Windows-Fähigkeit, drop-in. Im Kandidaten 8.0.0-beta.19.

Teilpaket von #140 (D8: Windows nativ, PowerShell 7). **Erledigt (2026-10-01):** `d0f08d1`, Kandidat 8.0.0-beta.19. CI grün: Run 484 (`verify` und `pwsh`), Run 485 (`release`). Lokal vor dem Publish: `pytest` (1969 bestanden, 3 übersprungen), `docs verify` und `instructions verify` grün. Die Handprüfung der Ausgabe auf dem Windows-Zielsystem führt der Nachlauf von #140 („Ausgabe ohne Kodierungsfehler“). ## Warum Der Python-Code setzte an mehreren Stellen POSIX voraus. Im analysierten Lauf (#140) scheiterte `instructions verify` unter Windows an allen 23 Instruktionen. Weitere Stellen rechneten unter Windows falsch, ohne dass es jemand bemerkt hätte. ## Befund (erhoben auf 8.0.0-beta.18) - **Pfadtrenner.** `str(<pfad>.relative_to(...))` stand an 15 Stellen außerhalb der Tests, darunter der Selbstreferenz-Vergleich in `type_resolver.py`, der jede Type-Spec-Validierung brach. Dazu kamen zwei Fehlermeldungen, die einen relativen Pfad erst später in einen String verwandelten. - **Zeilenenden.** Es gab keine `.gitattributes`. Mit `core.autocrlf=true` bekam der Bash-Launcher `tools/wikitool` CRLF (`env: 'bash\r'`). - **Text-Schreibzugriffe** ohne `newline="\n"` schrieben unter Windows CRLF. Das betraf den Byte-Vergleich der Skill-Kopien und die sha256-Summen in `dist upgrade`. - **Encoding beim Lesen.** 13 `subprocess`-Aufrufe mit `text=True` dekodierten unter Windows mit der Locale (cp1252). Dazu kamen ein `read_text()` ohne `encoding` (`run_budget._load_state`) und `sys.stdin` in `trace_ingest.py`. - **Encoding der eigenen Ausgabe.** `doctor` zeigte unter Copilot (pwsh) und Claude Code (Git Bash) `Fu�noten`, direkt in einer Konsole dagegen richtig. Python schreibt unter Windows in eine Pipe mit cp1252. - **Locks.** `fcntl` in `run_budget.py` und `telemetry/writer.py`. Unter Windows fiel die Sperre still weg. - **ripgrep.** `rg --json` liefert unter Windows `kb\\comparisons\\COLLECTION.md`, auch mit `--path-separator /` (T3). ## Umgesetzt 1. **`.gitattributes`** mit `* text=auto eol=lf`. **Abweichung:** `raw/` und `incoming/` sind `-text`, damit Quellen byte-genau bleiben (`raw/CONTRACT.md`, Regel „Immutable“). `dist export` liefert die Datei aus. Die Renormalisierung des Index änderte nichts. 2. **Pfade:** Alle Stellen nutzen `.as_posix()`, auch die Rückfallzweige und die Fehlermeldungen. 3. **`newline="\n"`** an jedem Text-Schreibzugriff. **Abweichung:** Es steht direkt am Aufruf, nicht in einem Helfer. Der Guard-Test hält es so oder so, und ein Helfer bräuchte denselben Guard. 4. **`encoding="utf-8"`** an jedem `subprocess`-Aufruf mit `text=True`. 5. **`chemenu/filelock.py`:** `flock` unter POSIX, `msvcrt.locking` unter Windows. Gesperrt wird ein Byte bei Offset 2³¹−2, weit hinter den Daten, weil Windows-Locks verpflichtend sind und `trace_ingest.py` die Trace sonst nicht lesen könnte. Bei Konkurrenz wird gewartet, wie bei `flock`. Die Lock-Datei des Budgets wird mit `a` geöffnet statt mit `w`. 6. **`search/ripgrep.py`:** `_record_path` wandelt `\` in `/`, nur wenn `os.sep == "\\"`. Das Flag kommt nicht hinzu. Die Umwandlung ist verlustfrei, weil kein Windows-Pfadbestandteil und seit #155 kein Seitentitel ein `\` enthalten kann. 7. **Guard-Tests** in `tests/test_portability.py`, per AST über `tools/chemenu` und die Skripte daneben in `tools/`. Jeder Detektor wird zusätzlich mit seinem Defekt gefüttert, damit ein Guard nicht grün ist, nur weil er nichts findet. Sie schlagen fehl bei: - `str(...relative_to(...))` und `f"{...relative_to(...)}"` - Text-Schreibzugriffen ohne `newline=` - **Erweitert:** jedem Text-Lese- oder -Schreibzugriff ohne `encoding=` - `subprocess.*(text=True)` ohne `encoding=`, dazu kein `from subprocess import` - `import fcntl`/`msvcrt` außerhalb von `filelock.py` 8. **UTF-8 für die eigene Ausgabe**, in `tools/run_wikitool.py`, der einen Stelle für beide Launcher. Dort stellt `reconfigure` stdout und stderr vor `main()` um. - **Messung auf dem Zielsystem (2026-10-01):** Natives pwsh hat `[Console]::OutputEncoding` = `ibm850` und `$OutputEncoding` = `utf-8`. Unter Copilot sind beide `utf-8`. pwsh dekodiert abgefangene Kindprozess-Ausgabe mit `[Console]::OutputEncoding`, deshalb reicht unter Copilot UTF-8 aus Python. Git Bash reicht die Bytes unverändert durch. - **Verworfen:** `PYTHONUTF8=1` in beiden Launchern. Das wären zwei Stellen, und der geänderte `open()`-Default würde ein fehlendes `encoding=` verdecken. - **Mitgenommen:** `trace_ingest.py` liest stdin als UTF-8-Bytes. Ein locale-dekodierter Read scheiterte an Bytes, die in cp1252 undefiniert sind (z. B. `Ł` = C5 81). - **Entschieden (Betreiber, 2026-10-01): bleibt vorerst so.** Wird `tools/wikitool` im nativen pwsh-Fenster gepiped (`| Select-String`), dekodiert pwsh mit `ibm850`. Das war vorher schon so. Die Alternative wäre, dass `wikitool.ps1` `[Console]::OutputEncoding` für die Dauer des Aufrufs umstellt; sie ist zurückgestellt. Ohne Pipe ist die Ausgabe richtig. ## Akzeptanzkriterien - [x] `tools/wikitool` wird mit LF ausgecheckt: `git check-attr` meldet `text: auto` und `eol: lf` (Test `test_every_text_file_is_checked_out_with_lf`). Der Index ist durchgehend `i/lf`. - [x] `rg -n 'str\([^)]*relative_to' tools/chemenu -g '!**/tests/**'` findet nichts. Der Guard-Test schlägt bei `str(...)` und bei f-Strings fehl. - [x] Jeder Text-Schreibzugriff unter `tools/chemenu` und in `tools/*.py` trägt `newline=` (Guard-Test). - [x] Jeder `subprocess`-Aufruf mit `text=True` trägt `encoding="utf-8"` (Guard-Test). - [x] Budget-Zähler und Telemetrie-Writer sperren auch ohne `fcntl`. Getestet mit ausgeblendetem `fcntl`-Import und einem Fake-`msvcrt`, einschließlich Offset, Positionserhalt und Warten bei Konkurrenz. Ein echter Windows-Lock läuft mangels Windows-Runner nicht (T12). - [x] Die T3-JSON-Zeile ergibt bei simuliertem Windows-Trenner `kb/comparisons/COLLECTION.md`. Ein Treffer in derselben Form auf eine Fixture-Seite trägt Schlüssel, Titel, Art, Collection und Summary. Unter POSIX-Trenner bleibt der Pfad unverändert (`test_search.py`). - [x] Unter Linux ist eine Ausgabe mit `ß` in eine Pipe UTF-8, auch unter simuliertem `PYTHONIOENCODING=cp1252` (`test_portability.py`). ~~Auf dem Windows-Zielsystem zeigt `doctor` `Fußnoten` unter Copilot, Claude Code und in beiden Konsolen~~ – diese Handprüfung führt der Nachlauf von #140 („Ausgabe ohne Kodierungsfehler (#152)“), nicht dieses Paket. - [x] `pytest`, `docs verify` und `instructions verify` sind grün, lokal und in CI 484. ## Restrisiko, nicht in diesem Paket `run_budget.status_command` liest den Budget-Zustand ohne Lock. Unter Windows kann `os.replace` in `_save_state` scheitern, wenn genau dann ein Leser die Datei offen hat. Das Zeitfenster ist klein. Ein Paket entsteht erst, wenn es im Nachlauf auftritt. ## Version minor – Windows-Fähigkeit, drop-in. Im Kandidaten 8.0.0-beta.19.
torben added the prio/plannedsize/Marea/distributionkind/build labels 2026-09-27 08:44:19 +00:00
Author
Owner

Changelog: T3 aus #140 eingearbeitet (2026-09-30): --path-separator / wirkt nicht auf rg --json.

  • Punkt 6: normalisiert jetzt in Python beim Parsen, nur unter Windows. Die Invariante, dass dabei nichts verloren geht, stützt sich auf die Titelregel aus #155. Das Flag entfällt.
  • Neues Kriterium: ein Test mit den JSON-Zeilen aus T3.
  • Hinweis: Die Zeilennummern im Befund sind Stand dd885db und werden vor dem Umbau neu erhoben.
  • Abhängigkeit auf T3 entfernt; alle Punkte sind startbar.
**Changelog:** T3 aus #140 eingearbeitet (2026-09-30): `--path-separator /` wirkt nicht auf `rg --json`. - **Punkt 6:** normalisiert jetzt in Python beim Parsen, nur unter Windows. Die Invariante, dass dabei nichts verloren geht, stützt sich auf die Titelregel aus #155. Das Flag entfällt. - **Neues Kriterium:** ein Test mit den JSON-Zeilen aus T3. - **Hinweis:** Die Zeilennummern im Befund sind Stand `dd885db` und werden vor dem Umbau neu erhoben. - **Abhängigkeit auf T3** entfernt; alle Punkte sind startbar.
Author
Owner

Changelog: Befund aus der Handprüfung von #151 auf dem Windows-Zielsystem (2026-10-01).

  • Neu: Die eigene Ausgabe von wikitool kommt unter Copilot (pwsh) und unter Claude Code (Git Bash) falsch kodiert an: Fu�noten in doctor. Direkt in einem Konsolenfenster ist sie richtig. Dazu kommen Punkt 8 im Umfang (UTF-8 an einer Stelle, Kodierung von pwsh zuerst messen) und ein Kriterium auf dem Zielsystem.
  • Korrigiert: Den Pfad zu rg liefert schon .wikitool-tools.json aus #151. Der Satz „bis dahin weiter aus PATH“ in Punkt 6 und die Abhängigkeit auf #151 sind entfallen.
**Changelog:** Befund aus der Handprüfung von #151 auf dem Windows-Zielsystem (2026-10-01). - **Neu:** Die eigene Ausgabe von `wikitool` kommt unter Copilot (pwsh) und unter Claude Code (Git Bash) falsch kodiert an: `Fu�noten` in `doctor`. Direkt in einem Konsolenfenster ist sie richtig. Dazu kommen Punkt 8 im Umfang (UTF-8 an einer Stelle, Kodierung von pwsh zuerst messen) und ein Kriterium auf dem Zielsystem. - **Korrigiert:** Den Pfad zu `rg` liefert schon `.wikitool-tools.json` aus #151. Der Satz „bis dahin weiter aus PATH“ in Punkt 6 und die Abhängigkeit auf #151 sind entfallen.
Author
Owner

Changelog (2026-10-01): Punkt 8 gemessen und entschieden, Umbau begonnen.

  • Punkt 8: UTF-8 wird in tools/run_wikitool.py gesetzt. Grundlage ist die Messung: natives pwsh ibm850/utf-8, pwsh unter Copilot utf-8/utf-8. PYTHONUTF8 in den Launchern ist verworfen.
  • Offen (D-neu): Wird tools/wikitool … | Select-String im nativen pwsh-Fenster gepiped, dekodiert pwsh mit ibm850 – vorher wie nachher falsch. Möglich wäre: wikitool.ps1 setzt [Console]::OutputEncoding für die Dauer des Aufrufs auf UTF-8 und stellt es danach wieder her. Entscheidung beim Operator.
  • Abweichungen vom Umfang:
    • .gitattributes setzt raw/ und incoming/ auf -text, damit Quellen byte-genau bleiben (Unveränderlichkeit laut raw/CONTRACT.md).
    • newline= steht direkt am Aufruf, nicht in einem Helfer. Der Guard-Test hält das ohnehin, und ein Helfer bräuchte denselben Guard.
    • Mitgenommen: Der Guard prüft auch encoding= an jedem Text-Lese- und -Schreibzugriff. Dabei kam eine Fundstelle zutage (run_budget._load_state).
    • Mitgenommen: trace_ingest.py liest stdin als UTF-8-Bytes. Ein locale-dekodierter Read scheiterte unter Windows an Bytes, die in cp1252 undefiniert sind.
  • Restrisiko, nicht in diesem Paket: run_budget.status_command liest den Budget-Zustand ohne Lock. Unter Windows kann os.replace scheitern, wenn genau dann ein Leser die Datei offen hat. Das Zeitfenster ist klein.
**Changelog (2026-10-01):** Punkt 8 gemessen und entschieden, Umbau begonnen. - **Punkt 8:** UTF-8 wird in `tools/run_wikitool.py` gesetzt. Grundlage ist die Messung: natives pwsh `ibm850`/`utf-8`, pwsh unter Copilot `utf-8`/`utf-8`. `PYTHONUTF8` in den Launchern ist verworfen. - **Offen (D-neu):** Wird `tools/wikitool … | Select-String` im nativen pwsh-Fenster gepiped, dekodiert pwsh mit `ibm850` – vorher wie nachher falsch. Möglich wäre: `wikitool.ps1` setzt `[Console]::OutputEncoding` für die Dauer des Aufrufs auf UTF-8 und stellt es danach wieder her. Entscheidung beim Operator. - **Abweichungen vom Umfang:** - `.gitattributes` setzt `raw/` und `incoming/` auf `-text`, damit Quellen byte-genau bleiben (Unveränderlichkeit laut `raw/CONTRACT.md`). - `newline=` steht direkt am Aufruf, nicht in einem Helfer. Der Guard-Test hält das ohnehin, und ein Helfer bräuchte denselben Guard. - Mitgenommen: Der Guard prüft auch `encoding=` an jedem Text-Lese- und -Schreibzugriff. Dabei kam eine Fundstelle zutage (`run_budget._load_state`). - Mitgenommen: `trace_ingest.py` liest stdin als UTF-8-Bytes. Ein locale-dekodierter Read scheiterte unter Windows an Bytes, die in cp1252 undefiniert sind. - **Restrisiko, nicht in diesem Paket:** `run_budget.status_command` liest den Budget-Zustand ohne Lock. Unter Windows kann `os.replace` scheitern, wenn genau dann ein Leser die Datei offen hat. Das Zeitfenster ist klein.
Author
Owner

Changelog: abgeschlossen (2026-10-01). Body auf den Endzustand gebracht.

  • d0f08d1 (8.0.0-beta.19), CI 484/485 grün.
  • Die native pwsh-Pipe bleibt laut Betreiber vorerst so.
  • Die Handprüfung auf dem Zielsystem (Fußnoten) liegt beim Nachlauf von #140.
**Changelog:** abgeschlossen (2026-10-01). Body auf den Endzustand gebracht. - `d0f08d1` (8.0.0-beta.19), CI 484/485 grün. - Die native pwsh-Pipe bleibt laut Betreiber vorerst so. - Die Handprüfung auf dem Zielsystem (`Fußnoten`) liegt beim Nachlauf von #140.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: torben/chemenu#152