diff --git a/docs/DECISIONS.md b/docs/DECISIONS.md index 059d5ef..f2a4794 100644 --- a/docs/DECISIONS.md +++ b/docs/DECISIONS.md @@ -649,9 +649,19 @@ Nutzerdaten. (Die erste Fassung hieß durch einen Tippfehler „Werkbank"; ausgelieferte ist, damit eine eigene Umbenennung stehen bleibt.) Der Reset (D22) setzt jetzt **beide** mitgelieferten Dokumente auf ihren Auslieferungsstand zurück; eigene Dokumente bleiben weiterhin unangetastet. -**Offen:** Der Text wird nach dem Anlegen nicht mehr aktualisiert. Erscheint -später eine neuere Fassung des Plans, sieht sie nur, wer das Dokument löscht und -zurücksetzt oder den `?sourceUrl=`-Link öffnet. Ein Ausbau könnte in -`werkbaum-seeded` statt '1' eine Versionsnummer ablegen und ein **unverändertes** -Dokument nachziehen (bearbeitete nie) — dieselbe Adoptions-Regel wie beim -Beispiel in D22. +**Nachziehen bei neuer Fassung (nachgereicht).** Zuerst wurde der Text nur +einmalig angelegt — eine spätere Ergänzung des Plans erreichte niemanden mehr. +Das fiel sofort auf, als der Plan um den Abschnitt „gemeinsam arbeiten" +(ROADMAP) wuchs. `werkbaum-seeded` hält deshalb nicht mehr '1', sondern den +**Fingerabdruck** (FNV-1a) der zuletzt ausgelieferten Fassung. Beim Laden gilt: + +- kein Merker → Dokument einmalig anlegen (auch für Bestandsnutzer); +- Merker ≠ aktueller Fingerabdruck **und** der Text des Nutzers hat noch genau + den gemerkten Fingerabdruck → Text nachziehen; +- Text **verändert** → nie anfassen (dieselbe Adoptions-Regel wie beim Beispiel + in D22: nur Unverändertes wird adoptiert); +- Dokument gelöscht → bleibt gelöscht. + +Der Altwert `'1'` aus der ersten Fassung sagt nichts über den Textstand; dort +wird bewusst nichts überschrieben, nur der Merker ersetzt. Wer aus dieser kurzen +Zwischenfassung kommt, holt den aktuellen Stand über den Reset. diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 1a1e2c6..48390ac 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -44,6 +44,46 @@ in laufender Entwicklung, Integrationsfähigkeit erklärtes Ziel — noch zu jung als Plattform-Ziel. Companion-App so schneiden, dass ein späterer Umzug Taiga → Tenzu nur den API-Adapter betrifft. +## Gemeinsam an einem Diagramm arbeiten +Ziel: mehrere Personen ändern denselben Plan. Drei Stufen, aufeinander +aufbauend — die dritte lohnt nur, wenn wirklich gleichzeitig gearbeitet wird. + +**1. Lesen teilen — vorhanden.** `?sourceUrl=` (D23) zeigt eine entfernte +Textdatei an; die Quelle wird anderswo gepflegt. Einbahnstraße, aber ohne +jeden Server. + +**2. Asynchron ändern über Git — heute schon möglich, ohne eine Zeile Code.** +Die `.werkbaum`-Datei (D24) liegt im Repository, wird per Pull Request geändert +und per `?sourceUrl=` angezeigt. Weil der Plan **Text** ist (D14), sind +`git diff` und `git blame` tatsächlich lesbar („wer hat *Payment provider* auf +`[!]` gesetzt?"). Für einen Plan, der sich pro Woche und nicht pro Sekunde +ändert, ist das oft die passendere Antwort — mit Review und Historie obendrauf. +Ausbau: Das Backend (D13) legt jede Änderung als Commit ab und bekommt +Historie, Wiederherstellung und Verzweigungen für Szenarien geschenkt. + +*Git direkt aus dem Browser* ist technisch möglich (`isomorphic-git` spricht das +HTTP-Smart-Protokoll), taugt aber nicht als Live-Sync: Es synchronisiert auf +Zuruf statt fortlaufend, braucht wegen fehlender CORS-Header einen Proxy (SSH +scheidet im Browser ohnehin aus), und ein Konflikt landet als +`<<<<<<<`-Markierung mitten in der Notation — der Parser sähe kaputte Zeilen. + +**3. Live gemeinsam tippen — offen.** Der **Transport** ist das kleinere +Problem (WebSocket; SSE + POST oder WebRTC als Alternativen). Die eigentliche +Frage ist, was passiert, wenn zwei Personen **dieselbe Zeile** ändern: Naives +„jede Änderung sofort in beide Richtungen" führt zu Textsprüngen unter dem +Cursor und verlorenen Zeichen. Erprobte Antworten sind **Operational +Transformation** (Google Docs) und **CRDTs**. Für Werkbaum fällt die Wahl +leicht, weil D14 das Format auf puren Text festgelegt hat: Ein Text-CRDT +(`Y.Text` in Yjs, oder Loro) passt ohne eigene Merge-Logik darauf; Cursor und +Anwesenheit fallen als Beigabe ab. **Eigene Merge-Algorithmen sind hier kein +Betätigungsfeld** — das Problem ist gelöst. + +Offene Punkte vor einer Entscheidung: Yjs wäre die **erste +Laufzeit-Abhängigkeit** überhaupt (CLAUDE: nicht ohne Rückfrage); wie CRDT- +Zustand und Git-Historie zusammenspielen (Commit-Granularität — nicht jeder +Tastendruck ein Commit); und wer bei einem Backend eigentlich was darf +(Rechte, siehe „Accounts" im Werkbaum-Beispielplan). + ## Kleinere Ideen - Deterministische Pastellfarbe pro `@name` (Personen wiedererkennen). - Sichtbare Anmerkungen am Knoten (eigene Syntax, getrennt von `%%`). diff --git a/docs/examples/example-werkbaum.werkbaum b/docs/examples/example-werkbaum.werkbaum index 051f503..bf035f9 100644 --- a/docs/examples/example-werkbaum.werkbaum +++ b/docs/examples/example-werkbaum.werkbaum @@ -58,6 +58,21 @@ - [?] Accounts and permissions (L) | [?] Single user, one token (S) | [?] Log in with OIDC (L) + - [?] Working together on one plan (XL) + - [ ] Git as the shared store (L) + | [ ] A file in a repository, changed by pull request (S) %% works today, no code + | [?] The backend commits every change (L) + - [?] History and restore (M) + - [?] Diff between two versions (S) + - [?] Who changed this line (S) + - [?] A branch per scenario (S) + - [?] Live editing, several people at once (XL) + - [ ] Transport over a websocket (S) %% the easy half + - [!] Merging simultaneous edits (L) %% the actual work + | [?] Text CRDT — the plan is plain text, so it fits (L) + | [?] Operational transformation (XL) + - [?] Cursors and who else is here (S) + - [-] A merge algorithm of our own (XL) %% solved problem, do not reinvent - [?] Mermaid plugin (XL) - [!] A layout engine of its own (XL) %% measure, place, route — the real work - [ ] Measure node sizes (M) diff --git a/frontend/CLAUDE.md b/frontend/CLAUDE.md index 2061bb5..6b7b339 100644 --- a/frontend/CLAUDE.md +++ b/frontend/CLAUDE.md @@ -110,9 +110,12 @@ verworfene Elemente. Quelle sind ES-Module unter `src/`; `index.html` ist der `../../docs/examples/example-werkbaum.werkbaum?raw` — die Beispieldatei ist damit **Build-Eingabe**, Umbenennen/Verschieben bricht den Build (Zugriff außerhalb des Roots erlaubt `server.fs.allow:['..']`). `seedShippedDocs()` legt - es **einmalig** an (auch für Bestandsnutzer) und merkt sich das in - `werkbaum-seeded`; ohne den Merker käme ein gelöschtes Dokument bei jedem Laden - zurück. `resetToDefaults()` setzt beide mitgelieferten Dokumente zurück. + es **einmalig** an (auch für Bestandsnutzer); ohne den Merker `werkbaum-seeded` + käme ein gelöschtes Dokument bei jedem Laden zurück. Der Merker hält den + **Fingerabdruck** der ausgelieferten Fassung: Ändert sich die Datei, wird der + Text nur nachgezogen, wenn der Nutzer ihn **nicht** bearbeitet hat. Wer die + Beispieldatei ändert, ändert damit das mitgelieferte Dokument mit. + `resetToDefaults()` setzt beide mitgelieferten Dokumente **und** den Merker. - Umbenennen ist **inline** (kein `window.prompt` — in manchen Browser-Kontexten unterdrückt): `renameDoc()` setzt `renamingId`, `renderDocMenu()` rendert dann ein `` (Enter = `commitRename`, Esc = `cancelRename`, diff --git a/frontend/src/app.js b/frontend/src/app.js index cd8cb42..dff8efb 100644 --- a/frontend/src/app.js +++ b/frontend/src/app.js @@ -1340,18 +1340,33 @@ function persistDocs(){ if(d) localStorage.setItem(LS_SRC, d.text); /* Spiegel für Fallback/Migration */ }catch(_){} } -/* „Werkbank" (D27) genau EINMAL anlegen — auch für Bestandsnutzer, die schon - eine Dokumentenliste haben. Der Merker `LS_SEEDED` sorgt dafür, dass ein - bewusst gelöschtes Dokument nicht bei jedem Laden zurückkehrt; ein späterer - Ausbau kann dort eine Versionsnummer statt '1' ablegen. */ +/* Kurzer, stabiler Fingerabdruck (FNV-1a) — dient nur dem Vergleich „ist das + noch der ausgelieferte Text?"; keine kryptografische Anforderung. */ +function fingerprint(s){ + let h = 0x811c9dc5; + for(let i = 0; i < s.length; i++){ h ^= s.charCodeAt(i); h = Math.imul(h, 0x01000193); } + return (h >>> 0).toString(36); +} + +/* Mitgeliefertes Dokument „Werkbaum" (D27) anlegen bzw. nachziehen. In + `LS_SEEDED` steht der Fingerabdruck der zuletzt ausgelieferten Fassung: + - kein Merker -> Dokument einmalig anlegen (auch für Bestandsnutzer); + - neue Fassung -> Text nur ersetzen, wenn der Nutzer ihn NICHT geändert hat + (sein Fingerabdruck also noch dem gemerkten entspricht); + - gelöscht -> bleibt gelöscht (der Merker verhindert die Wiederkehr). + Der Altwert '1' aus der ersten Fassung sagt nichts über den Textstand, dort + wird bewusst nichts angefasst — nur der Merker wird ersetzt. */ function seedShippedDocs(){ - let seeded = null; - try{ seeded = localStorage.getItem(LS_SEEDED); }catch(_){} - if(seeded) return; - if(!docs.some(d => d.id === WERKBAUM_ID)){ - docs.push({ id: WERKBAUM_ID, name: WERKBAUM_NAME, text: WERKBAUM_DOC }); + const fp = fingerprint(WERKBAUM_DOC); + let seen = null; + try{ seen = localStorage.getItem(LS_SEEDED); }catch(_){} + const doc = docs.find(d => d.id === WERKBAUM_ID); + if(!seen){ + if(!doc) docs.push({ id: WERKBAUM_ID, name: WERKBAUM_NAME, text: WERKBAUM_DOC }); + } else if(seen !== '1' && seen !== fp && doc && fingerprint(doc.text) === seen){ + doc.text = WERKBAUM_DOC; } - try{ localStorage.setItem(LS_SEEDED, '1'); }catch(_){} + try{ localStorage.setItem(LS_SEEDED, fp); }catch(_){} } /* Aus dem localStorage laden; bei fehlender Dokumentenliste den bestehenden @@ -2065,6 +2080,9 @@ function resetToDefaults(){ }; reseed(EXAMPLE_ID, EXAMPLE_NAME, INITIAL, true); reseed(WERKBAUM_ID, WERKBAUM_NAME, WERKBAUM_DOC, false); + /* Merker auf den jetzt ausgelieferten Stand setzen (D27) — sonst hielte ein + Altwert die spätere Nachzieh-Logik davon ab, den Text je zu aktualisieren. */ + try{ localStorage.setItem(LS_SEEDED, fingerprint(WERKBAUM_DOC)); }catch(_){} activeId = EXAMPLE_ID; persistDocs(); logUpdate('🔄 Mitgelieferte Dokumente und Einstellungen zurückgesetzt');