diff --git a/README.de.md b/README.de.md index ed8d566..0866248 100644 --- a/README.de.md +++ b/README.de.md @@ -127,6 +127,12 @@ und holt ihn ins Bild — bei **allen**, die das Dokument ansehen. Das kann ein Cursor nicht. Erkannt nur als alleinstehendes Token, `Achtung!!!` bleibt also ein gewöhnliches Label. Die Marke bleibt stehen, bis jemand sie löscht. +**Eine Git-Historie für einen geteilten Plan:** `tools/pull-doc +` holt das Dokument (auch ein `?live=`-Link funktioniert) und committet +es in das git-Worktree der Datei — Datum, Titel und Version in der Nachricht, +kein Commit ohne Änderung. Per Cron aufgerufen archiviert sich der Plan von +selbst; siehe `docs/DECISIONS.md` D88. + Das Backend einrichten: siehe [backend/README.md](backend/README.md) und `docs/DECISIONS.md` D76. diff --git a/README.md b/README.md index a26f86c..9d4fca1 100644 --- a/README.md +++ b/README.md @@ -121,6 +121,12 @@ something a cursor cannot do. Recognised only as a standalone token, so `Careful!!!` stays an ordinary label. It stays in the text until someone deletes it. +**A git history for a shared plan:** `tools/pull-doc ` +fetches the document (a `?live=` link works too) and commits it into the file's +git worktree — date, title and version in the message, and no commit when +nothing changed. Run it from cron and the plan archives itself; see +`docs/DECISIONS.md` D88. + Setting up the backend: see [backend/README.md](backend/README.md) and `docs/DECISIONS.md` D76. diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 03131c9..70a61b7 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -19,6 +19,7 @@ reverse. ## 2026-08-27 +- A `pull-doc` script fetches a shared document from the backend and commits it into a git worktree — dated commits, none when nothing changed, so a cron gives the plan a git history - A benefit rating per node recorded as planned: a 0-9 digit inside the size token (`M/9`), and a third path mode that picks alternatives by benefit over cost — spelled out in SPEC §11 before it is built - A people bar below the diagram shows each `@person` with their share of the open work on the cheapest path — plus an entry for what is assigned to nobody - Tapping a person filters the diagram to their nodes: everything else folds, their packages stand collapsed as `▸ n`, their pills turn teal — a view-only lens that never writes into the text diff --git a/docs/DECISIONS.md b/docs/DECISIONS.md index e771d8b..a785dad 100644 --- a/docs/DECISIONS.md +++ b/docs/DECISIONS.md @@ -7525,3 +7525,53 @@ anna-Pillen petrol, Text byte-identisch. Bens Linse hält annas Backend Text nicht; Ausschalten stellt den textdefinierten Zustand wieder her; Unzugewiesen-Linse klappt genau die getaggten Pakete zu. Tag-loses Dokument: keine Leiste. 538 Tests, davon 7 neue in `tests/lens.test.js`. + +## D88 — pull-doc: ein Server-Dokument in ein git-Worktree holen und committen +Gewünscht: ein Script im `tools`-Verzeichnis, das eine Backend-URL und einen +Zielpfad nimmt (der Zielpfad muss in einem git-Worktree liegen), das Dokument +herunterlädt und mit einem Datums-Kommentar committet. Das ist die dritte +Alternative unter „Git as the shared store" (`#col.git.pull`): zwischen der +Datei im Repo (`#col.git.pr`) und dem Backend, das jede Änderung committet +(`#col.git.auto`), steht der Cron-fähige Archiv-Schritt — ein geteilter Plan +(D76) bekommt so eine Git-Historie, ohne dass der Server git können muss. + +**Eigenes Script `tools/pull-doc`, nicht hinter `remote`.** `remote` ist die +Vordertür für alles, was **auf dem Server** passiert (D77/CLAUDE.md); dieses +Werkzeug läuft lokal — es holt per HTTP und committet ins lokale Worktree. + +**Auch der Editor-Link wird genommen.** Was ein Mensch in der Hand hat, ist +meist der geteilte `?live=`-Link, nicht die API-Adresse — der Parameter wird +erkannt und prozent-dekodiert ausgepackt (dieselbe Höflichkeit wie bei den +Pad-Adressen in D31: die URL aus der Adresszeile genügt). + +**Unverändert heißt: kein Commit, Exit 0.** Ohne diese Regel füllte ein Cron +die Historie mit leeren Bewegungen bzw. bräche bei `--allow-empty`-losem +`git commit` fehl. Verglichen wird gegen die Datei auf der Platte. + +**Byte-getreu geschrieben, von Python, nicht über eine Shell-Variable.** Eine +Kommandosubstitution verschluckt abschließende Zeilenumbrüche — der Inhalt +liefe also durch die Shell nicht unverfälscht. Das JSON liest und schreibt +deshalb ein `python3`-Einzeiler (Datei direkt, `newline=""`); die Wahl fiel +auf python3 statt jq/node, weil es der eine Interpreter ist, der auf +Entwickler-Rechnern wie auf dem Zielserver (D76-Nachtrag 1) sicher vorliegt. +Nachgemessen: 29 Bytes ohne angehängtes Newline kommen als 29 Bytes an. + +**Committet wird mit Pfadangabe** (`git commit -m … -- `): Andere +vorgemerkte Änderungen im Worktree bleiben unangetastet — nachgemessen mit +einer gestagten Fremd-Datei, die nach dem Lauf weiterhin gestagt und nicht im +Commit war. Die Nachricht nennt Datum, Titel und Server-Version +(`Werkbaum-Stand vom 2026-08-27 11:53 — „Titel" (Version 8)`). + +**Fehler sind laut und räumen auf:** kein Worktree, Verzeichnis fehlt, +Abruf gescheitert, Antwort kein Dokument — je eigene Meldung, Exit ≠ 0, +Temp-Datei weg (Trap). Die Worktree-Prüfung lehnt auch das `.git`-Verzeichnis +selbst ab. + +**Nachgemessen** gegen einen lokalen Mock-Server in einem +Wegwerf-Worktree: Erstabruf committet genau die eine Datei; unveränderter +zweiter Lauf meldet „kein Commit" (Exit 0, Commit-Zahl unverändert); +geänderter Inhalt ergibt den zweiten Commit; der prozent-kodierte +`?live=`-Link wird ausgepackt; Nicht-Worktree, 404 und Nicht-Dokument-JSON +enden mit klarer Meldung und ohne Rückstände. Der echte Backend-Abruf ist +derselbe GET wie im Mock — dieselbe Werkzeuggrenze wie überall: Der native +Serverbetrieb ist über D76/D77 abgedeckt. diff --git a/docs/examples/werkbaum.werkbaum b/docs/examples/werkbaum.werkbaum index d05ecaa..fc2b90e 100644 --- a/docs/examples/werkbaum.werkbaum +++ b/docs/examples/werkbaum.werkbaum @@ -140,8 +140,9 @@ " same instant is the expensive one. - [^] #col.point: A shared pointer: !!! focuses a node for everyone (XS) - [-] #col.pad: Borrow an Etherpad — it merges, we render (L) %% built, then removed, see D78 - - [ ] #col.git: Git as the shared store (L) + - [~] #col.git: Git as the shared store (L) | [ ] #col.git.pr: A file in a repository, changed by pull request (S) %% works today, no code + | [x] #col.git.pull: A script pulls the server document and commits it (S) %% tools/pull-doc, dated commits — see D88 | [?] #col.git.auto: The backend commits every change (L) :#be.scaffold - [?] #col.git.hist: History and restore (M) - [?] #col.git.diff: Diff between two versions (S) @@ -886,6 +887,11 @@ go through a pull request, a link renders it. For a plan that changes weekly rather than per second this is often the better answer. +#col.git.pull + The tools/pull-doc script fetches a server document (a ?live= link works too) + and commits it into a git worktree — date, title and version in the message, + no commit when nothing changed. Run from cron, a shared plan archives itself. + #col.git.auto The server turns every change into a commit. History, restore and branches follow from that, with commit granularity as the open question. diff --git a/tools/pull-doc b/tools/pull-doc new file mode 100755 index 0000000..769090f --- /dev/null +++ b/tools/pull-doc @@ -0,0 +1,92 @@ +#!/usr/bin/env bash +# +# Werkbaum — pull-doc: ein Server-Dokument in ein git-Worktree holen und +# committen. Gedacht als Archiv-Schritt (auch per Cron): Der Notationstext +# eines geteilten Plans (D76) bekommt so eine Git-Historie. +# +# Verwendung: +# pull-doc +# +# Die Dokument-Adresse des Backends +# (https://…/api/v1/documents/) — oder gleich der +# geteilte Editor-Link (…?live=); der Parameter +# wird erkannt und ausgepackt. +# Datei, in die der Notationstext geschrieben wird. Sie muss +# in einem git-Worktree liegen. Committet wird nur diese +# eine Datei; die Commit-Nachricht nennt Datum, Titel und +# Version des Server-Stands. +# +# Unverändert heißt: kein Commit — der Lauf meldet es und endet mit 0. +# Geschrieben wird byte-getreu, was der Server hält (der Text ist das +# führende Datenformat, D14); andere vorgemerkte Änderungen im Worktree +# bleiben unangetastet (Commit mit Pfadangabe). +# +# Siehe docs/DECISIONS.md D88. + +set -euo pipefail + +usage(){ sed -n '2,22p' "$0" | sed 's/^# \{0,1\}//'; } + +case "${1:-}" in -h|--help) usage; exit 0;; esac +[ $# -eq 2 ] || { echo "Fehler: genau zwei Argumente erwartet (URL und Zieldatei)." >&2; echo >&2; usage >&2; exit 2; } + +url="$1"; target="$2" + +command -v python3 >/dev/null || { echo "Fehler: python3 wird gebraucht (JSON lesen)." >&2; exit 2; } +command -v curl >/dev/null || { echo "Fehler: curl wird gebraucht." >&2; exit 2; } + +# Ein geteilter Editor-Link traegt die Dokument-Adresse als ?live=…-Parameter — +# auspacken (samt Prozent-Dekodierung), sonst gilt die URL selbst. +url="$(python3 - "$url" <<'PY' +import sys, urllib.parse +u = sys.argv[1] +q = urllib.parse.parse_qs(urllib.parse.urlsplit(u).query) +print(q["live"][0] if "live" in q else u) +PY +)" +case "$url" in http://*|https://*) ;; *) echo "Fehler: keine http(s)-URL: $url" >&2; exit 2;; esac + +# Ziel pruefen: Verzeichnis muss existieren und in einem git-Worktree liegen. +[ -d "$target" ] && { echo "Fehler: $target ist ein Verzeichnis." >&2; exit 2; } +dir="$(dirname -- "$target")"; base="$(basename -- "$target")" +[ -d "$dir" ] || { echo "Fehler: Verzeichnis $dir gibt es nicht." >&2; exit 2; } +if [ "$(git -C "$dir" rev-parse --is-inside-work-tree 2>/dev/null)" != "true" ]; then + echo "Fehler: $dir liegt in keinem git-Worktree." >&2; exit 2 +fi +if [ "$(git -C "$dir" rev-parse --is-inside-git-dir 2>/dev/null)" = "true" ]; then + echo "Fehler: $dir liegt im .git-Verzeichnis." >&2; exit 2 +fi + +tmp_json="$(mktemp)"; tmp_doc="$dir/.$base.pull-doc.$$" +trap 'rm -f "$tmp_json" "$tmp_doc"' EXIT + +curl -fsS --max-time 30 -H 'Accept: application/json' -o "$tmp_json" "$url" \ + || { echo "Fehler: Abruf gescheitert: $url" >&2; exit 1; } + +# JSON lesen; den Inhalt schreibt python selbst (byte-getreu — eine +# Shell-Variable verschluckte abschliessende Zeilenumbrueche). +meta="$(python3 - "$tmp_json" "$tmp_doc" <<'PY' +import io, json, sys +try: + with io.open(sys.argv[1], encoding="utf-8") as f: + doc = json.load(f) + content, title, version = doc["content"], doc["title"], doc["version"] +except Exception as e: + sys.exit(f"Fehler: Antwort ist kein Werkbaum-Dokument ({e})") +with io.open(sys.argv[2], "w", encoding="utf-8", newline="") as f: + f.write(content) +print(f"{title}\t{version}") +PY +)" +title="${meta%$'\t'*}"; version="${meta##*$'\t'}" + +if [ -f "$target" ] && cmp -s "$tmp_doc" "$target"; then + echo "Unverändert: „$title“ (Version $version) — kein Commit." + exit 0 +fi + +mv -- "$tmp_doc" "$target" +msg="Werkbaum-Stand vom $(date '+%Y-%m-%d %H:%M') — „$title“ (Version $version)" +git -C "$dir" add -- "$base" +git -C "$dir" commit -q -m "$msg" -- "$base" +echo "Committet $(git -C "$dir" rev-parse --short HEAD): $msg"