plan-kopie: Zieldatei ohne Endung über ## Zieldateien, sonst Befund statt stillem Fehlziel
Anlass: Plankritik Paket R1 im Edge-Stack, Befunde 1 und 5. Die Überschrift "Stelle 30 — `host/logrotate/edge-traefik`" setzte kein Ziel; der Block "Die ganze Datei ist neu" überschrieb tests/systemd/test-units.sh mit Exit 0. - ordne() in plan_materialisiere.py, von plan_bloecke.py importiert: Ziel ist der letzte PFAD-Treffer, ohne ihn der letzte Token, der unter ## Zieldateien steht (absolut relativ zu --projekt bzw. zum Ordner vor docs/plans). - Überschrift ohne Ziel, die nach einer Datei aussieht (relativer Pfad mit /, Datei in Kopie/Projekt, Dateiname eines Eintrags): jeder schreibende Block danach ist Befund, Exit 2, nichts geschrieben. - plan_ziel_treiber.py: 33 Fälle; Ursprung 22 rot, jetzt 33 grün; 21 eingebaute Fehler gefangen. - Vorher/nachher über 2 223 Plandateien: Abweichung nur R1 task-01 und Trading Task 90, beide jetzt ins richtige Ziel. - LIESMICH und planer.md nachgezogen. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
07675bf541
commit
5cbe75c5e3
5 files changed
+433
-21
No files matched your search
@@ -3,7 +3,24 @@
|
||||
Koordinator-Werkzeug (entstanden 2026-09-17, Trading Plattform Teil 3b). Es tut etwas und beurteilt
|
||||
nichts; das Urteil faellt danach mit Suite, Mutanten und welle_vergleich.py.
|
||||
Geltungsbereich:
|
||||
- Zieldatei ist der letzte Pfad in Backticks in einer Ueberschrift (## bis ####) oberhalb des Blocks.
|
||||
- Zieldatei ist der letzte Pfad in Backticks in einer Ueberschrift (## bis ####) oberhalb des Blocks, den
|
||||
PFAD erkennt: mit Schraegstrich und Endung, ohne Schraegstrich nur mit py, ts, tsx, toml, md, css, json.
|
||||
Erkennt PFAD in der Ueberschrift nichts, ist Ziel der letzte Token, der (ohne "./") genau so unter
|
||||
"## Zieldateien" derselben Taskdatei steht (seit 2026-10-17): So wird eine Datei ohne Endung
|
||||
(`host/logrotate/edge-traefik`, `Dockerfile`, `.gitignore`) planbar. Ein absoluter Eintrag zaehlt
|
||||
relativ zum Ordner vor "docs/plans" im Pfad des Planordners; ohne diesen zaehlen nur relative Eintraege.
|
||||
Ordner, Muster mit "*" und Pfade mit ".." unter Zieldateien sind kein Ziel.
|
||||
- Befund statt stillem Ziel (seit 2026-10-17): Nennt eine Ueberschrift kein Ziel, aber einen Token, der
|
||||
nach einer Datei aussieht, ist jeder schreibende Block danach ein Befund, auch unter Ueberschriften
|
||||
ohne Pfad, bis eine Ueberschrift mit Ziel kommt; nichts wird geschrieben. Nach einer Datei sehen aus:
|
||||
ein relativer Pfad mit Schraegstrich und ein Name ohne Schraegstrich, der direkt in der Kopie als Datei
|
||||
liegt oder Dateiname eines Eintrags unter Zieldateien ist. Vorher ging der Block still ins vorige Ziel.
|
||||
Kein Befund sind Ordner (hinten "/"), absolute Pfade (vorn "/"), Token mit Leerraum und Bezeichner
|
||||
ohne gleichnamige Datei: Bestehende Plaene nennen sie in Ueberschriften als Kontext und meinen das
|
||||
vorige Ziel. Nicht gesehen: eine neue Datei ohne Endung, ohne Schraegstrich und ohne Eintrag unter
|
||||
Zieldateien, ein absoluter Pfad ohne Endung und eine Ueberschrift, die neben einem PFAD-Treffer eine
|
||||
Datei ohne Endung nennt (dann gilt der PFAD-Treffer). Gleiche Regel in plan_bloecke.py (dort mit
|
||||
--projekt statt Kopie und Planordner); Regressionsnachweis: plan_ziel_treiber.py.
|
||||
- Dateien, die "## Zieldateien" als "(gelöscht)" fuehrt, werden in der Kopie geloescht.
|
||||
- Bloecke mit Einrueckung (Befehle in Listen) und Prosa-Vorgaben ("wird: ein neuer Abschnitt ...")
|
||||
werden uebergangen: Ein Doku-Task mit frei zu formulierendem Text laesst sich nicht anwenden.
|
||||
@@ -21,26 +38,117 @@ Exit 0 ohne Befund, 2 mit Befund.
|
||||
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from pathlib import Path, PureWindowsPath
|
||||
|
||||
UEBERSCHRIFT = re.compile(r"^#{2,4} ")
|
||||
PFAD = re.compile(r"`([^`\s]+/[^`\s]+\.[A-Za-z0-9]+|[^`\s/]+\.(?:py|ts|tsx|toml|md|css|json))`")
|
||||
TOKEN = re.compile(r"`([^`\s]+)`")
|
||||
# Eintrag unter "## Zieldateien", dieselbe Form wie in welle_vergleich.py
|
||||
ZIEL = re.compile(r"^- `(.+?)`(?:\s*\((.*?)\))?")
|
||||
|
||||
|
||||
def wurzeln(planordner: Path) -> list[PureWindowsPath]:
|
||||
"""Der Projektordner vor "docs/plans" im Pfad des Planordners, wie uebergeben und aufgeloest."""
|
||||
aus = []
|
||||
for pfad in (planordner.absolute(), planordner.resolve()):
|
||||
teile = pfad.parts
|
||||
for i in range(len(teile) - 2, 0, -1):
|
||||
if teile[i] == "docs" and teile[i + 1] == "plans":
|
||||
aus.append(PureWindowsPath(*teile[:i]))
|
||||
break
|
||||
return aus
|
||||
|
||||
|
||||
def zieldateien(zeilen: list[str], wurzeln: list[PureWindowsPath]) -> set[str]:
|
||||
"""Relative Pfade unter "## Zieldateien"; ein absoluter zaehlt relativ zu einer der wurzeln.
|
||||
|
||||
Ordner (hinten "/"), Muster mit "*" und Pfade mit ".." sind kein Ziel.
|
||||
"""
|
||||
drin, menge = False, set()
|
||||
for zeile in zeilen:
|
||||
if zeile.startswith("## "):
|
||||
drin = zeile.startswith("## Zieldateien")
|
||||
continue
|
||||
treffer = ZIEL.match(zeile) if drin else None
|
||||
if not treffer or treffer.group(1).endswith(("/", "\\")) or "*" in treffer.group(1):
|
||||
continue
|
||||
pfad = PureWindowsPath(treffer.group(1))
|
||||
if pfad.drive or pfad.root:
|
||||
relativ = None
|
||||
for wurzel in wurzeln:
|
||||
try:
|
||||
relativ = pfad.relative_to(wurzel)
|
||||
break
|
||||
except ValueError:
|
||||
continue
|
||||
if relativ is None:
|
||||
continue
|
||||
pfad = relativ
|
||||
if ".." not in pfad.parts:
|
||||
menge.add(pfad.as_posix())
|
||||
return menge
|
||||
|
||||
|
||||
def ist_datei(ordner: Path | None, name: str) -> bool:
|
||||
"""Liegt `name` als Datei direkt im ordner? Nie fuer einen Namen mit Laufwerk oder Wurzel."""
|
||||
if ordner is None or PureWindowsPath(name).drive or PureWindowsPath(name).root:
|
||||
return False
|
||||
try:
|
||||
return (ordner / name).is_file()
|
||||
except (OSError, ValueError):
|
||||
return False
|
||||
|
||||
|
||||
def ordne(zeile: str, geplant: set[str], ordner: Path | None) -> tuple[str | None, list[str]]:
|
||||
"""Zieldatei einer Ueberschrift und die Token, die nach einer Datei aussehen, ohne erkannt zu sein.
|
||||
|
||||
Ziel ist der letzte PFAD-Treffer, ohne ihn der letzte Token, der (ohne "./") unter "## Zieldateien"
|
||||
steht. Ohne Ziel gelten als offen: relative Pfade mit Schraegstrich (kein Ordner, nicht absolut) und
|
||||
Namen ohne Schraegstrich, die direkt im ordner als Datei liegen oder Dateiname eines Eintrags sind.
|
||||
Nachbesserung 2026-10-17 (Plankritik Paket R1 im Edge-Stack, Befunde 1 und 5): "Stelle 30 —
|
||||
`host/logrotate/edge-traefik`" setzte kein Ziel, der Block "Die ganze Datei ist neu" ueberschrieb
|
||||
`tests/systemd/test-units.sh` mit Exit 0; `Dockerfile` ging ebenso still ins vorige Ziel. `config/`
|
||||
und `/proc` stehen in bestehenden Plaenen ueber Bloecken, die das vorige Ziel meinen.
|
||||
"""
|
||||
treffer = PFAD.findall(zeile)
|
||||
if treffer:
|
||||
return treffer[-1], []
|
||||
token = TOKEN.findall(zeile)
|
||||
ziele = [PureWindowsPath(t).as_posix() for t in token if PureWindowsPath(t).as_posix() in geplant]
|
||||
if ziele:
|
||||
return ziele[-1], []
|
||||
namen = {eintrag.rsplit("/", 1)[-1] for eintrag in geplant}
|
||||
offen = [t for t in token if ("/" in t and not t.startswith("/") and not t.endswith("/"))
|
||||
or ("/" not in t and (t in namen or ist_datei(ordner, t)))]
|
||||
return None, offen
|
||||
|
||||
|
||||
def schreibt(marke: str) -> bool:
|
||||
"""Marken, nach denen main() in die Zieldatei schreibt; bei einer neuen Marke hier mitziehen."""
|
||||
return (marke == "wird:" or "Die ganze Datei ist neu" in marke or "vollständig ersetzt" in marke
|
||||
or marke.startswith("Häng diesen Block ans Ende"))
|
||||
|
||||
|
||||
def normal(text: str) -> str:
|
||||
return text.replace("\r\n", "\n").replace("\r", "\n")
|
||||
|
||||
|
||||
def bloecke(taskdatei: Path):
|
||||
def bloecke(taskdatei: Path, kopie: Path | None = None):
|
||||
zeilen = normal(taskdatei.read_text(encoding="utf-8")).split("\n")
|
||||
geplant = zieldateien(zeilen, wurzeln(taskdatei.parent))
|
||||
ziel = None
|
||||
# (Zeile, Token) der letzten Ueberschrift ohne Ziel, die nach einer Datei aussieht; None, sobald ein Ziel kommt
|
||||
ungeklaert = None
|
||||
i = 0
|
||||
while i < len(zeilen):
|
||||
zeile = zeilen[i]
|
||||
if UEBERSCHRIFT.match(zeile):
|
||||
treffer = PFAD.findall(zeile)
|
||||
if treffer:
|
||||
ziel = treffer[-1]
|
||||
# Der Generator laeuft verzahnt mit main(): Die Kopie traegt hier schon die Bloecke davor
|
||||
neu, offen = ordne(zeile, geplant, kopie)
|
||||
if neu:
|
||||
ziel, ungeklaert = neu, None
|
||||
elif offen:
|
||||
ungeklaert = (i + 1, offen)
|
||||
if zeile.lstrip().startswith("```"):
|
||||
einzug = len(zeile) - len(zeile.lstrip())
|
||||
kern = zeile.lstrip()
|
||||
@@ -58,7 +166,8 @@ def bloecke(taskdatei: Path):
|
||||
while k >= 0 and not zeilen[k].strip():
|
||||
k -= 1
|
||||
marke = zeilen[k].strip() if k >= 0 else ""
|
||||
yield {"ziel": ziel, "marke": marke, "zeile": start + 1, "text": "\n".join(inhalt), "einzug": einzug}
|
||||
yield {"ziel": ziel, "marke": marke, "zeile": start + 1, "text": "\n".join(inhalt), "einzug": einzug,
|
||||
"ungeklaert": ungeklaert}
|
||||
i += 1
|
||||
|
||||
|
||||
@@ -114,7 +223,7 @@ def main() -> int:
|
||||
taskdatei = plan / name
|
||||
alt = None
|
||||
zaehler = {"neu": 0, "ersetzt": 0, "angehaengt": 0, "uebergangen": 0}
|
||||
for block in bloecke(taskdatei):
|
||||
for block in bloecke(taskdatei, kopie):
|
||||
marke, ziel = block["marke"], block["ziel"]
|
||||
if block["einzug"] > 0:
|
||||
zaehler["uebergangen"] += 1
|
||||
@@ -122,6 +231,14 @@ def main() -> int:
|
||||
if marke == "Aus:" or marke.startswith("Aus, Zeile"):
|
||||
alt = block
|
||||
continue
|
||||
if block["ungeklaert"] and schreibt(marke):
|
||||
zeile_u, pfade = block["ungeklaert"]
|
||||
print(f"BEFUND {name}:{block['zeile']}: Ueberschrift in Zeile {zeile_u} nennt "
|
||||
f"{', '.join(f'`{p}`' for p in pfade)} und keine erkannte Zieldatei (fehlt sie unter ## Zieldateien?); "
|
||||
f"der Block ginge nach {ziel or 'keinem Ziel'}, nichts geschrieben")
|
||||
befunde += 1
|
||||
alt = None
|
||||
continue
|
||||
if marke == "wird:":
|
||||
if alt is None or ziel is None:
|
||||
print(f"BEFUND {name}:{block['zeile']}: wird-Block ohne Aus-Block oder ohne Zieldatei")
|
||||
|
||||
Reference in new issue
Block a user