kimal3 0.1.5 β†’ 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,248 @@
1
+ import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
2
+ import { dirname, join, resolve } from "node:path";
3
+ import { istUnsichererPfad } from "./arbeitsplatz-dateien.js";
4
+ import { MERKZETTEL_PFAD, VERWALTETE_DATEIEN, arbeitsplatzAus, leseMerkzettel, schreibeMerkzettel, sicherungsZiel, } from "./kontext-stand.js";
5
+ /**
6
+ * Das Dateisystem hinter `kimal3 kontext runter` -- Ordner finden, die
7
+ * verwalteten Dateien lesen, sichern, schreiben, den Merkzettel lesen und
8
+ * schreiben.
9
+ *
10
+ * πŸ”΄ **Ruling T8-1: Die reine Fundregel `arbeitsplatzAus` wird hier NICHT noch
11
+ * einmal geschrieben** -- sie steht seit Task 1 in `kontext-stand.ts` und wird
12
+ * hier nur VERBRAUCHT (`findeArbeitsplatz` unten fuellt sie mit echten
13
+ * Kandidaten vom Dateisystem). Zwei Fassungen derselben Fundregel waeren
14
+ * dieselbe zweite Wahrheit, die dieses Projekt schon bei `werkbankAusDateien`
15
+ * teuer bezahlt hat.
16
+ *
17
+ * ⚠️ **Dieselben Netze wie in `arbeitsplatz-dateien.ts`**, aus denselben,
18
+ * teuer bezahlten Gruenden: `existsSync`/`statSync`/`readFileSync` gehoeren
19
+ * in ein EIGENES `try` -- ein `EACCES` fluege sonst bis in den Auffang-Block
20
+ * von `kimal3.ts`, der "kimal3 ist gerade nicht erreichbar" druckt und `3`
21
+ * zurueckgibt. Der Mensch suchte den Fehler dann beim Server, obwohl er auf
22
+ * seiner eigenen Platte sitzt -- und `3` heisst "gleich noch einmal", was
23
+ * hier nie helfen kann. Ein Schreibfehler (Platte voll, `EACCES`) gibt
24
+ * deshalb `1` zurueck, nicht `3`.
25
+ */
26
+ /** Ist `pfad` (relativ zu `wurzel`) ein lesbarer ORDNER? Unlesbar zaehlt als "nein". */
27
+ function istLesbarerOrdner(pfad) {
28
+ try {
29
+ return existsSync(pfad) && statSync(pfad).isDirectory();
30
+ }
31
+ catch {
32
+ return false;
33
+ }
34
+ }
35
+ /** Ist `pfad` (relativ zu `wurzel`) eine lesbare DATEI? Unlesbar zaehlt als "nein". */
36
+ function istLesbareDatei(pfad) {
37
+ try {
38
+ return existsSync(pfad) && statSync(pfad).isFile();
39
+ }
40
+ catch {
41
+ return false;
42
+ }
43
+ }
44
+ /**
45
+ * Von `start` aufwaerts nach dem Arbeitsplatz suchen -- die Kandidaten kommen
46
+ * von UNTEN NACH OBEN, `start` zuerst.
47
+ *
48
+ * πŸ”‘ Diese Funktion FUELLT nur die Kandidatenliste vom Dateisystem; die
49
+ * eigentliche Entscheidung ("welcher Ordner gewinnt") trifft `arbeitsplatzAus`
50
+ * in `kontext-stand.ts` (Ruling P-4/T8-1).
51
+ *
52
+ * Die Schleife hat eine Sicherheitsgrenze (200 Ebenen): `dirname()` konvergiert
53
+ * auf jedem realen Dateisystem zur Wurzel, aber ein Netzlaufwerk mit einem
54
+ * kaputten Elternverweis soll trotzdem nie eine Endlosschleife erzeugen.
55
+ */
56
+ export function findeArbeitsplatz(start) {
57
+ const kandidaten = [];
58
+ let aktuell = resolve(start);
59
+ for (let i = 0; i < 200; i++) {
60
+ kandidaten.push({
61
+ pfad: aktuell,
62
+ hatKontext: istLesbarerOrdner(join(aktuell, "kontext")),
63
+ hatClaude: istLesbareDatei(join(aktuell, "CLAUDE.md")),
64
+ });
65
+ const eltern = dirname(aktuell);
66
+ if (eltern === aktuell)
67
+ break;
68
+ aktuell = eltern;
69
+ }
70
+ return arbeitsplatzAus(kandidaten);
71
+ }
72
+ /**
73
+ * Eine der verwalteten Dateien lesen -- fehlend wird zu `inhalt: null`,
74
+ * NICHT zu einem Fehler (ein frisch geklonter Ordner hat oft noch keine).
75
+ */
76
+ export function leseEineDatei(ordner, pfad) {
77
+ const ort = join(ordner, ...pfad.split("/"));
78
+ try {
79
+ if (!existsSync(ort))
80
+ return { ok: true, inhalt: null };
81
+ if (!statSync(ort).isFile()) {
82
+ return {
83
+ ok: false,
84
+ meldung: `β€ž${pfad}" existiert, ist aber keine Datei -- ich fasse das nicht an. Bitte pruefen.`,
85
+ };
86
+ }
87
+ return { ok: true, inhalt: readFileSync(ort, "utf8") };
88
+ }
89
+ catch (fehler) {
90
+ return {
91
+ ok: false,
92
+ meldung: `Ich komme an β€ž${pfad}" nicht heran: ${fehler instanceof Error ? fehler.message : String(fehler)}. ` +
93
+ "Das liegt an diesem Rechner, nicht am Server -- Rechte pruefen.",
94
+ };
95
+ }
96
+ }
97
+ /**
98
+ * Den lokalen Merkzettel lesen. Unlesbar oder nicht vorhanden -> leeres
99
+ * Objekt, GENAU wie `leseMerkzettel(null)` -- der sichere Fall aus Β§7b: "wer
100
+ * nichts weiss, loescht nichts". Eine unlesbare Datei fuehrt also zur
101
+ * Sicherung vor dem Ueberschreiben, nie zum stillen Verlust.
102
+ */
103
+ export function leseMerkzettelDatei(ordner) {
104
+ try {
105
+ const ort = join(ordner, MERKZETTEL_PFAD);
106
+ const roh = existsSync(ort) ? readFileSync(ort, "utf8") : null;
107
+ return leseMerkzettel(roh);
108
+ }
109
+ catch {
110
+ return leseMerkzettel(null);
111
+ }
112
+ }
113
+ /** Den Merkzettel schreiben -- MIT dem Stand, der zum ausgelieferten Inhalt passt. */
114
+ export function schreibeMerkzettelDatei(ordner, m) {
115
+ try {
116
+ const ort = join(ordner, MERKZETTEL_PFAD);
117
+ writeFileSync(ort, schreibeMerkzettel(m), "utf8");
118
+ return { ok: true };
119
+ }
120
+ catch (fehler) {
121
+ return {
122
+ ok: false,
123
+ meldung: `Der Merkzettel liess sich nicht schreiben: ${fehler instanceof Error ? fehler.message : String(fehler)}. Das liegt an diesem Rechner, nicht am Server -- Platz und Rechte pruefen.`,
124
+ };
125
+ }
126
+ }
127
+ /**
128
+ * Der naechste NICHT belegte Sicherungspfad -- reine Rechnung, kein
129
+ * Dateisystem. `istBelegt(pfad)` sagt, ob dieser Pfad schon existiert (der
130
+ * Aufrufer entscheidet, WIE das geprueft wird); diese Funktion zaehlt vom
131
+ * Basisnamen aus hoch, bis eine freie Stelle gefunden ist.
132
+ *
133
+ * πŸ”΄ **Ruling T8-2 (Review-Fund 1, Fix-Runde 1).** Vorher gab `sichereDatei`
134
+ * bei einem belegten Ziel `{ uebersprungen: true }` zurueck und schrieb dabei
135
+ * GAR NICHTS -- eine zweite Aenderung am selben Tag verschwand beim
136
+ * anschliessenden Ueberschreiben spurlos, waehrend die Ausgabe im Befehl
137
+ * "gesichert" behauptete. **Diese Funktion darf deshalb NIE einen belegten
138
+ * Pfad als Endergebnis zurueckgeben und NIE einen Pfad, der schon existiert,
139
+ * unkommentiert ueberschreiben lassen** -- sie liefert IMMER einen Pfad, der
140
+ * (laut `istBelegt`) frei ist.
141
+ *
142
+ * ⚠️ **Die erste Sicherung eines Tages -- der Basisname -- bleibt fuer immer
143
+ * unangetastet.** Das ist US-7 woertlich: "Laeuft der Befehl am selben Tag
144
+ * ein zweites Mal, wird die erste Sicherung NICHT ueberschrieben." Wer diese
145
+ * Funktion vereinfacht ("dann eben doch den Basisnamen nehmen, wenn `nummer`
146
+ * gross wird") loescht genau die Sicherung, die US-7 ausdruecklich schuetzt.
147
+ * Eine dritte, vierte, ... Aenderung bekommt stattdessen einen weiteren,
148
+ * eigenen Namen daneben -- nie einen schon vergebenen.
149
+ */
150
+ export function naechstesFreiesSicherungsziel(basisZielRel, istBelegt) {
151
+ if (!istBelegt(basisZielRel))
152
+ return basisZielRel;
153
+ for (let nummer = 2; nummer <= 1000; nummer++) {
154
+ const kandidat = nummeriertesZiel(basisZielRel, nummer);
155
+ if (!istBelegt(kandidat))
156
+ return kandidat;
157
+ }
158
+ /*
159
+ Praktisch unerreichbar (1000 Sicherungen derselben Datei am selben Tag).
160
+ Diese reine Funktion wirft nicht -- sie liefert den letzten geprueften
161
+ Namen, und der Aufrufer (`sichereDatei`) trifft beim tatsaechlichen
162
+ Schreiben erneut auf `existsSync` und meldet dort einen echten Fehler,
163
+ statt dass eine reine Rechnung selbst eine Ausnahme auslΓΆst.
164
+ */
165
+ return nummeriertesZiel(basisZielRel, 1000);
166
+ }
167
+ /**
168
+ * `kontext/vorher-2026-08-21/firma.md` + 2 -> `.../firma-2.md`.
169
+ *
170
+ * Nur der DATEINAME bekommt die Nummer, nicht der Ordner -- alle
171
+ * verwalteten Dateien landen weiter FLACH im selben `vorher-<datum>/`
172
+ * (unveraendert gegenueber `sicherungsZiel`).
173
+ */
174
+ function nummeriertesZiel(basisZielRel, nummer) {
175
+ const teile = basisZielRel.split("/");
176
+ const dateiname = teile.pop() ?? "";
177
+ const punkt = dateiname.lastIndexOf(".");
178
+ const name = punkt === -1 ? dateiname : dateiname.slice(0, punkt);
179
+ const endung = punkt === -1 ? "" : dateiname.slice(punkt);
180
+ return [...teile, `${name}-${nummer}${endung}`].join("/");
181
+ }
182
+ /**
183
+ * Die lokale Fassung von `pfad` nach `kontext/vorher-<datum>/` sichern --
184
+ * FLACH, mit dem Basisnamen (`sicherungsZiel`), oder -- ist der schon belegt
185
+ * -- unter einem durchnummerierten Namen DANEBEN (`naechstesFreiesSicherungsziel`,
186
+ * Ruling T8-2).
187
+ *
188
+ * πŸ”΄ **Es wird NIE uebersprungen und NIE ueberschrieben.** Beides waere eine
189
+ * stille Datenvernichtung: Ueberspringen ohne Kopie loescht die zweite
190
+ * Aenderung eines Tages beim anschliessenden Ueberschreiben der Arbeitsdatei;
191
+ * Ueberschreiben der ersten Sicherung verstoesst woertlich gegen US-7.
192
+ * `nebenVorhandener: true` im Ergebnis sagt dem Aufrufer, dass HEUTE schon
193
+ * eine andere Sicherung derselben Datei existiert -- er muss das in der
194
+ * Ausgabe unterscheiden (Β§8: kein Fehlerfall traegt den Namen eines
195
+ * Erfolgsfalls, und hier sind es zwei verschiedene ECHTE Erfolgsfaelle).
196
+ */
197
+ export function sichereDatei(ordner, pfad, inhalt, datum) {
198
+ const basisZielRel = sicherungsZiel(datum, pfad);
199
+ const istBelegt = (zielRel) => existsSync(join(ordner, ...zielRel.split("/")));
200
+ const zielRel = naechstesFreiesSicherungsziel(basisZielRel, istBelegt);
201
+ const ort = join(ordner, ...zielRel.split("/"));
202
+ try {
203
+ mkdirSync(dirname(ort), { recursive: true });
204
+ writeFileSync(ort, inhalt, "utf8");
205
+ return { ok: true, ziel: zielRel, nebenVorhandener: zielRel !== basisZielRel };
206
+ }
207
+ catch (fehler) {
208
+ return {
209
+ ok: false,
210
+ meldung: `Sichern von β€ž${pfad}" ist fehlgeschlagen: ${fehler instanceof Error ? fehler.message : String(fehler)}. Das liegt an diesem Rechner, nicht am Server -- Platz und Rechte pruefen.`,
211
+ };
212
+ }
213
+ }
214
+ /**
215
+ * Eine der verwalteten Dateien schreiben.
216
+ *
217
+ * πŸ”΄ **Positivliste, keine Verbotsliste** (Auflage aus dem Brief): `pfad` muss
218
+ * WOERTLICH einer der Eintraege aus `VERWALTETE_DATEIEN` sein, sonst wird
219
+ * NICHTS geschrieben. Diese Funktion bekommt `pfad` im echten Ablauf immer aus
220
+ * der eigenen Iteration ueber `VERWALTETE_DATEIEN` (nie roh aus der Server-
221
+ * Antwort) -- die Pruefung hier ist trotzdem ausdruecklich verlangt und bleibt
222
+ * als zweites Netz stehen, falls sich der Aufrufer einmal aendert.
223
+ *
224
+ * ⚠️ `istUnsichererPfad` zusaetzlich, aus demselben Grund wie in
225
+ * `dateienSchreiben` (`arbeitsplatz-dateien.ts`): Ein Pfad kommt letztlich aus
226
+ * einer Server-Antwort, und eine Pruefung, die nur dort gilt, wo man sie
227
+ * gerade braucht, ist keine Pruefung dieser Funktion.
228
+ */
229
+ export function schreibeVerwalteteDatei(ordner, pfad, inhalt) {
230
+ if (!VERWALTETE_DATEIEN.includes(pfad)) {
231
+ return { ok: false, meldung: `β€ž${pfad}" ist keine verwaltete Datei -- wird nicht geschrieben. Bitte melden.` };
232
+ }
233
+ if (istUnsichererPfad(pfad)) {
234
+ return { ok: false, meldung: `β€ž${pfad}" ist kein sicherer Pfad -- wird nicht geschrieben. Bitte melden.` };
235
+ }
236
+ const ort = join(ordner, ...pfad.split("/"));
237
+ try {
238
+ mkdirSync(dirname(ort), { recursive: true });
239
+ writeFileSync(ort, inhalt, "utf8");
240
+ return { ok: true };
241
+ }
242
+ catch (fehler) {
243
+ return {
244
+ ok: false,
245
+ meldung: `Schreiben von β€ž${pfad}" ist fehlgeschlagen: ${fehler instanceof Error ? fehler.message : String(fehler)}. Das liegt an diesem Rechner, nicht am Server -- Platz und Rechte pruefen.`,
246
+ };
247
+ }
248
+ }