kimal3 0.1.6 β†’ 0.2.1

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,305 @@
1
+ import { zerlege } from "../argumente.js";
2
+ import { ruf } from "../api.js";
3
+ import { heuteAus } from "../nachricht.js";
4
+ import { istUnsichererPfad } from "../arbeitsplatz-dateien.js";
5
+ import { LEER, VERWALTETE_DATEIEN, alsMerkzettel, sicherungsOrdner, sicherungsZiel, lageAusMerkzetteln, ratNachUnberuehrt, } from "../kontext-stand.js";
6
+ import { leseEineDatei, leseMerkzettelDatei, schreibeMerkzettelDatei, schreibeVerwalteteDatei, sichereDatei, } from "../kontext-dateien.js";
7
+ import { ERLAUBT, absage, arbeitsplatzOderAbsage } from "./kontext-gemeinsam.js";
8
+ /**
9
+ * Holt den Firmenkontext von der App und legt ihn in den verwalteten Dateien
10
+ * des gefundenen Arbeitsplatzes ab -- `CLAUDE.md` und die fuenf Dateien in
11
+ * `kontext/`.
12
+ *
13
+ * πŸ”΄ **`kontext/kurzprofil.md` ist seit Ruling K-24 (Task 7) dabei.** Sie wurde
14
+ * beim ERSTEN Ausliefern gefuellt und danach nie wieder aufgefrischt -- waehrend
15
+ * sie selbst dem Kunden zusicherte, sie werde β€žzentral in der App gepflegt".
16
+ *
17
+ * πŸ”΄ **R2 (Spec Β§14): Dieser Befehl fragt NIE `git status`.** Er entscheidet
18
+ * ausschliesslich ueber den Merkzettel (`.kimal3-stand.json`), egal ob der
19
+ * Ordner ein Git-Repo ist oder nicht -- sonst gaebe es zwei Verhalten fuer
20
+ * denselben Befehl, und das schwerer erklaerbare traefe ausgerechnet den, der
21
+ * Git benutzt.
22
+ *
23
+ * β›” **Unterhalb von `werkbank-<name>/` wird nichts geschrieben** (Spec Β§4).
24
+ * Dieser Befehl kennt genau die Pfade aus `VERWALTETE_DATEIEN`, und in keinem
25
+ * davon steht ein Personenname.
26
+ *
27
+ * πŸ” **Ruling T9-2:** Die Gegenrichtung, `kontextHoch`, wohnt seit dem Split
28
+ * von `kontext.ts` in `kontext-hoch.ts`. Beide teilen sich Fundregel,
29
+ * Firmenwahl-Absage und die `--firma`/`--firma-id`-Optionen ueber
30
+ * `kontext-gemeinsam.ts`.
31
+ */
32
+ const HILFE = `
33
+ kimal3 kontext runter [--firma "Name" | --firma-id "…"]
34
+
35
+ Holt den Firmenkontext (CLAUDE.md + die fuenf Dateien in kontext/) und legt
36
+ ihn in deinem KI-Arbeitsplatz ab. Was du selbst geaendert hast, fasst der
37
+ Befehl nicht an -- er sagt dir, wie du es einbringst.
38
+
39
+ --firma "Name" Noetig, wenn du zu mehreren Firmen gehoerst.
40
+ Die Absage nennt dir die Namen als fertige Befehle.
41
+
42
+ --firma-id "…" Fuer den Fall, dass zwei deiner Firmen GLEICH heissen.
43
+ Auch diese Absage nennt dir fertige Befehle.
44
+ `;
45
+ export async function kontextRunter(argv) {
46
+ if (argv.includes("--help") || argv.includes("-h")) {
47
+ console.log(HILFE);
48
+ return 0;
49
+ }
50
+ const zerlegt = zerlege(argv, ERLAUBT);
51
+ if (!zerlegt.ok) {
52
+ console.error(zerlegt.meldung);
53
+ return 1;
54
+ }
55
+ // πŸ“Œ Seit Task 7 gemeinsam mit `kontext hoch` und `kontext fragen`
56
+ // (`arbeitsplatzOderAbsage`). Hier standen dieselben vier Zeilen abgetippt;
57
+ // bei drei Aufrufern ist das keine Wiederholung mehr, sondern eine Falle --
58
+ // wer eine Zeile schaerft, schaerft eine von dreien.
59
+ const ordner = arbeitsplatzOderAbsage();
60
+ if (ordner === null)
61
+ return 1;
62
+ const wunsch = zerlegt.werte.optionen.firma;
63
+ const wunschId = zerlegt.werte.optionen["firma-id"];
64
+ const suche = new URLSearchParams();
65
+ if (wunsch !== undefined)
66
+ suche.set("firma", wunsch);
67
+ if (wunschId !== undefined)
68
+ suche.set("firma-id", wunschId);
69
+ const pfad = suche.toString() ? `/api/cli/kontext?${suche.toString()}` : "/api/cli/kontext";
70
+ const antwort = await ruf(pfad, { methode: "GET", mitAusweis: true });
71
+ if (antwort.status !== 200) {
72
+ return absage(antwort, "kontext runter");
73
+ }
74
+ // πŸ”΄ Ein `200` mit unlesbarem Rumpf ist KEIN Erfolg -- dasselbe Muster wie
75
+ // in `firma-daten.ts`. `ruf` verschluckt Parse-Fehler zu `null`.
76
+ if (antwort.koerper === null || typeof antwort.koerper !== "object") {
77
+ console.error("Die Antwort war nicht lesbar. Bitte gleich noch einmal.");
78
+ return 3;
79
+ }
80
+ const daten = alsKontextAntwort(antwort.koerper);
81
+ if (daten === null) {
82
+ console.error("Die Antwort war nicht lesbar. Bitte gleich noch einmal.");
83
+ return 3;
84
+ }
85
+ const lokalerMerkzettel = leseMerkzettelDatei(ordner);
86
+ const heute = heuteAus(new Date());
87
+ const geschrieben = [];
88
+ // Punkt A-1: die Teilmenge von `geschrieben`, bei der NICHT die Firma etwas
89
+ // geaendert hat, sondern wir unsere eigene Vorlage. Der Mensch soll den
90
+ // Unterschied lesen koennen -- sonst wundert er sich, warum sich eine Datei
91
+ // aendert, an der seine Firma nachweislich nichts getan hat.
92
+ const vorlageErneuert = [];
93
+ const gesicherte = [];
94
+ const unberuehrt = [];
95
+ for (const verwalteterPfad of VERWALTETE_DATEIEN) {
96
+ const serverDatei = daten.dateien.find((d) => d.pfad === verwalteterPfad);
97
+ if (!serverDatei) {
98
+ console.error(`Die Antwort enthaelt keine Datei β€ž${verwalteterPfad}". Bitte melden.`);
99
+ return 3;
100
+ }
101
+ // Zweites Netz -- s. Kommentar an `schreibeVerwalteteDatei`: `pfad` kommt
102
+ // hier aus der eigenen Iteration, nicht roh vom Server, trotzdem geprueft.
103
+ if (istUnsichererPfad(serverDatei.pfad)) {
104
+ console.error(`Die Antwort enthaelt einen unsicheren Pfad: β€ž${serverDatei.pfad}". Bitte melden.`);
105
+ return 3;
106
+ }
107
+ const lokalGelesen = leseEineDatei(ordner, verwalteterPfad);
108
+ if (!lokalGelesen.ok) {
109
+ console.error(lokalGelesen.meldung);
110
+ return 1;
111
+ }
112
+ /*
113
+ πŸ”΄ **DIE BEIDEN MERKZETTEL GEHEN ALS GANZES HINEIN -- Punkt A-1,
114
+ 2026-08-26.** Hier standen vorher drei Zeilen, die den Eintrag, den
115
+ Firmenstand und den Vordruck-Abdruck einzeln aus den beiden Dateien
116
+ herausklaubten. Genau dieser Schritt war die Blindstelle: Der
117
+ Vordruck-Abdruck wurde nie herausgeklaubt, das Argument war optional,
118
+ und deshalb bekam ein Kunde mit einer bestehenden Datei Verbesserungen
119
+ an unserem Fragenkatalog NIE zu sehen -- der Befehl meldete stattdessen
120
+ β€žDein Firmenkontext ist aktuell".
121
+
122
+ πŸ”‘ Die Zuordnung β€žwas kommt aus welchem Merkzettel" wohnt jetzt in
123
+ `lageAusMerkzetteln` (`kontext-stand.ts`), wo `check:kontext` sie messen
124
+ kann. *Was gesichert sein muss, gehoert auf die reine Seite.*
125
+ */
126
+ const lage = lageAusMerkzetteln({
127
+ pfad: verwalteterPfad,
128
+ lokalerInhalt: lokalGelesen.inhalt,
129
+ serverInhalt: serverDatei.inhalt,
130
+ lokal: lokalerMerkzettel,
131
+ vomServer: daten.merkzettel,
132
+ });
133
+ if (lage === "beides") {
134
+ // `lokalGelesen.inhalt` ist hier garantiert eine Zeichenkette:
135
+ // `lageFuerDatei` liefert "beides" nie, wenn `lokalerInhalt === null`.
136
+ const sicherung = sichereDatei(ordner, verwalteterPfad, lokalGelesen.inhalt, heute);
137
+ if (!sicherung.ok) {
138
+ console.error(sicherung.meldung);
139
+ return 1;
140
+ }
141
+ // Ruling T8-2: `sichereDatei` ueberschreibt nie und ueberspringt nie --
142
+ // `nebenVorhandener` sagt nur, ob HEUTE schon eine andere Sicherung
143
+ // dieser Datei existiert. Beide Faelle sind echte Erfolge und werden
144
+ // unten bewusst UNTERSCHIEDLICH gemeldet (Β§8).
145
+ gesicherte.push({
146
+ pfad: verwalteterPfad,
147
+ ziel: sicherung.ziel,
148
+ nebenVorhandener: sicherung.nebenVorhandener,
149
+ });
150
+ }
151
+ /*
152
+ πŸ”΄ **`vorlage-neuer` schreibt wie `server-neuer` -- und sichert bewusst
153
+ NICHT** (Punkt A-1, 2026-08-26). Diese Lage heisst: Die Datei steht noch
154
+ Zeichen fuer Zeichen so da, wie wir sie zuletzt geliefert haben (die
155
+ Pruefsumme im Merkzettel passt), die Firma hat nichts geaendert -- nur
156
+ WIR haben unsere Frage besser formuliert. Es gibt hier also nichts von
157
+ einem Menschen zu verlieren. Haette er die Datei angefasst, waere die
158
+ Lage `beides` und die Sicherung liefe eine Bedingung weiter oben.
159
+
160
+ ⚠️ Bis zu diesem Punkt fiel `vorlage-neuer` durch ALLE drei `if`s: nicht
161
+ geschrieben, nicht gesichert, nicht gemeldet. Der Fall konnte bisher nie
162
+ entstehen, weil die Kette dahinter tot war -- ein Zweig, den niemand
163
+ vermisst hat, weil ihn niemand erreichen konnte.
164
+ */
165
+ if (lage === "server-neuer" || lage === "beides" || lage === "vorlage-neuer") {
166
+ const geschriebenErg = schreibeVerwalteteDatei(ordner, verwalteterPfad, serverDatei.inhalt);
167
+ if (!geschriebenErg.ok) {
168
+ console.error(geschriebenErg.meldung);
169
+ return 1;
170
+ }
171
+ geschrieben.push(verwalteterPfad);
172
+ if (lage === "vorlage-neuer")
173
+ vorlageErneuert.push(verwalteterPfad);
174
+ }
175
+ if (lage === "lokal-geaendert") {
176
+ unberuehrt.push(verwalteterPfad);
177
+ }
178
+ }
179
+ /*
180
+ πŸ”΄ Der Merkzettel des SERVERS wird geschrieben -- unabhaengig davon, ob in
181
+ diesem Lauf ueberhaupt etwas geaendert wurde. Er traegt fuer JEDE der fuenf
182
+ Dateien den Stand, der zum jetzt ausgelieferten Inhalt passt (Β§7b), auch
183
+ fuer die, die "schon aktuell" waren oder lokal geaendert blieben -- ein
184
+ lokal geaenderter Eintrag bleibt so beim naechsten Lauf ERKENNBAR lokal
185
+ geaendert, statt durch einen fehlenden Merkzettel-Eintrag neu "sicher" zu
186
+ werden.
187
+
188
+ πŸ”΄ Task 10a: `firma_id` kommt ausdruecklich aus `daten.firma.id` -- der
189
+ Firma, die diese Antwort tatsaechlich beantwortet hat (`waehleFirma` in
190
+ der Route), nicht aus dem Merkzettel-Feld selbst. Wer zu MEHREREN Firmen
191
+ gehoert, soll im Ordner von Firma A niemals lesen "B hat sich geaendert".
192
+
193
+ πŸ”΄ **Punkt A-1: MIT DIESER ZEILE HAELT DER MERKZETTEL DEN NEUEN
194
+ VORLAGEN-FINGERABDRUCK FEST.** `daten.merkzettel.dateien` kommt aus der
195
+ Antwort des Servers und traegt je Datei ihr `vorlage` (`alsMerkzettel`
196
+ verlangt es ausdruecklich). Waere es der LOKALE Merkzettel, staende hier
197
+ weiter der Abdruck von gestern -- und der naechste Lauf meldete denselben
198
+ Unterschied noch einmal, Tag fuer Tag. *Der Teil, den man leicht
199
+ uebersieht: Eine Verbesserung, die jeden Tag neu gemeldet wird, ist
200
+ dieselbe Sorte Fehlalarm wie eine, die nie ankommt.*
201
+ */
202
+ const merkzettelZumSchreiben = { firma_id: daten.firma.id, dateien: daten.merkzettel.dateien };
203
+ const merkzettelSchreiben = schreibeMerkzettelDatei(ordner, merkzettelZumSchreiben);
204
+ if (!merkzettelSchreiben.ok) {
205
+ console.error(merkzettelSchreiben.meldung);
206
+ return 1;
207
+ }
208
+ // ---- Ausgabe --------------------------------------------------------
209
+ // πŸ”΄ Ruling T8-2 (Fix-Runde 1): ZWEI ehrlich verschiedene Faelle, keine
210
+ // Pauschalmeldung mehr. "Frisch gesichert" und "zusaetzlich neben einer
211
+ // schon vorhandenen Sicherung von heute" sind beide echte Erfolge -- aber
212
+ // verschiedene, und der Mensch muss wissen, WAS mit seinem Text passiert
213
+ // ist. Nie ein Satz, der eine Sicherung behauptet, wo keine (mehr)
214
+ // entstanden ist (der urspruengliche Befund).
215
+ //
216
+ // US-7 bleibt erfuellt: Die Meldung steht VOR der "aktualisiert"-Zeile
217
+ // (vor dem Ergebnis) und nennt den Ordner. Die eigentliche Reihenfolge
218
+ // (erst sichern, dann ueberschreiben) erzwingt schon der Code oben.
219
+ if (gesicherte.length > 0) {
220
+ console.log(`Ich sichere deine lokale Fassung, bevor ich sie ersetze -- Ordner: ${sicherungsOrdner(heute)}`);
221
+ for (const g of gesicherte) {
222
+ if (g.nebenVorhandener) {
223
+ const bisherige = sicherungsZiel(heute, g.pfad).split("/").pop();
224
+ const neue = g.ziel.split("/").pop();
225
+ console.log(` β€ž${g.pfad}": Von heute gibt es schon eine Sicherung (${bisherige}) -- die bleibt ` +
226
+ `unveraendert stehen. Deine neuere Fassung sichere ich zusaetzlich als ${neue}, ` +
227
+ "bevor ich die Datei im Ordner ersetze.");
228
+ }
229
+ else {
230
+ console.log(` β€ž${g.pfad}" -> ${g.ziel.split("/").pop()}`);
231
+ }
232
+ }
233
+ }
234
+ if (geschrieben.length > 0) {
235
+ console.log(`βœ“ aktualisiert: ${geschrieben.join(", ")}`);
236
+ }
237
+ // πŸ”΄ Punkt A-1: Eine eigene Zeile, kein stilles Mitschreiben. β€žAktualisiert"
238
+ // heisst fuer den Leser sonst automatisch β€žmeine Firma hat etwas gesagt" --
239
+ // und dann sucht er in der App eine Aenderung, die es dort nicht gibt.
240
+ if (vorlageErneuert.length > 0) {
241
+ console.log(` Davon aus unserer verbesserten Vorlage: ${vorlageErneuert.join(", ")}`);
242
+ console.log(" Diese Dateien standen noch genau so da, wie wir sie geliefert hatten β€” es ist nichts\n" +
243
+ " von dir verloren gegangen. HΓ€ttest du darin etwas geΓ€ndert, hΓ€tte ich vorher gesichert.");
244
+ }
245
+ // Β§8: Ein Fehlerfall darf nie denselben Namen tragen wie ein Erfolgsfall --
246
+ // "nichts geschrieben" heisst hier DREI verschiedene Dinge, und die Ausgabe
247
+ // muss sie unterscheiden, nicht nur die Rueckgabe (alle drei geben 0).
248
+ if (unberuehrt.length > 0) {
249
+ console.log(`Ich fasse nichts an -- du hast lokal geaendert: ${unberuehrt.join(", ")}`);
250
+ // πŸ”΄ Der Rat haengt an der DATEI, nicht nur an der Rolle -- s.
251
+ // `ratNachUnberuehrt` (kontext-stand.ts), Review-Befund F-3.
252
+ for (const zeile of ratNachUnberuehrt(unberuehrt, daten.rolle, daten.adminName)) {
253
+ console.log(zeile);
254
+ }
255
+ }
256
+ if (geschrieben.length === 0 && unberuehrt.length === 0) {
257
+ const alleLeer = VERWALTETE_DATEIEN.every((p) => (daten.merkzettel.dateien[p]?.stand ?? LEER) === LEER);
258
+ if (alleLeer) {
259
+ console.log("Deine Firma hat noch nichts hinterlegt. Bis dahin bleiben die Vordrucke stehen.");
260
+ }
261
+ else {
262
+ console.log("Dein Firmenkontext ist aktuell.");
263
+ }
264
+ }
265
+ return 0;
266
+ }
267
+ /** Prueft die Antwort-Form, ohne `any` -- eine falsche Form ist unlesbar, keine Ausnahme. */
268
+ function alsKontextAntwort(koerper) {
269
+ if (!koerper || typeof koerper !== "object")
270
+ return null;
271
+ const k = koerper;
272
+ // πŸ”΄ Task 10a: `firma.id` ist die Quelle, aus der `kontextRunter` gleich
273
+ // den lokalen Merkzettel stempelt -- s. Kommentar an der Schreibstelle.
274
+ const firmaRoh = k.firma;
275
+ if (!firmaRoh || typeof firmaRoh !== "object")
276
+ return null;
277
+ const firmaId = firmaRoh.id;
278
+ if (typeof firmaId !== "string" || firmaId.trim() === "")
279
+ return null;
280
+ const rolle = k.rolle;
281
+ if (rolle !== "admin" && rolle !== "member")
282
+ return null;
283
+ const adminName = k.admin_name;
284
+ if (adminName !== null && typeof adminName !== "string")
285
+ return null;
286
+ const dateienRoh = k.dateien;
287
+ if (!Array.isArray(dateienRoh))
288
+ return null;
289
+ const dateien = [];
290
+ for (const eintrag of dateienRoh) {
291
+ if (!eintrag || typeof eintrag !== "object")
292
+ return null;
293
+ const p = eintrag.pfad;
294
+ const i = eintrag.inhalt;
295
+ if (typeof p !== "string" || p.trim() === "")
296
+ return null;
297
+ if (typeof i !== "string")
298
+ return null;
299
+ dateien.push({ pfad: p, inhalt: i });
300
+ }
301
+ const merkzettel = alsMerkzettel(k.merkzettel);
302
+ if (merkzettel === null)
303
+ return null;
304
+ return { firma: { id: firmaId }, rolle, adminName, dateien, merkzettel };
305
+ }
@@ -4,22 +4,21 @@ import { fileURLToPath } from "node:url";
4
4
  import { join } from "node:path";
5
5
  import { STEMPEL_MARKE } from "../nachricht.js";
6
6
  import { eigeneVersion } from "../version.js";
7
- /**
8
- * Namen, die frueher einmal ausgeliefert wurden und heute nicht mehr.
9
- *
10
- * πŸ”‘ **Wofuer das da ist** (Review-Befund, 2026-08-18): Die Befehle landen im
11
- * GEMEINSAMEN Ordner `~/.claude/commands/` und werden dort nur ueberschrieben,
12
- * nie entfernt. Wird ein Befehl spaeter umbenannt oder gestrichen, ueberlebt
13
- * die alte Datei jedes `skill install` und bietet weiter Anweisungen an, die
14
- * auf einen nicht mehr vorhandenen Unterbefehl zeigen. `SKILL.md` kann das
15
- * nicht passieren -- es ist EINE Datei, die ueberschrieben wird.
16
- *
17
- * ⚠️ Warum eine Liste und nicht "alles loeschen, was wir nicht ausliefern":
18
- * Der Ordner gehoert dem Menschen. Ein `k3*.md`, das er selbst angelegt hat,
19
- * duerfen wir nicht wegraeumen. **Wer einen Befehl umbenennt oder entfernt,
20
- * traegt den alten Namen hier ein** -- sonst bleibt er beim Nutzer stehen.
21
- */
22
- const VERALTETE_BEFEHLE = [];
7
+ export const VERALTETE_BEFEHLE = [
8
+ /*
9
+ πŸ”΄ **Der ERSTE Eintrag ueberhaupt** (2026-08-28). `/k3firma` heisst jetzt
10
+ `/k3analyse`, weil der Befehl Zahlen holt und daraus eine Frage beantwortet
11
+ -- das ist eine Auswertung, und der alte Name sagte nur, WORUEBER sie geht.
12
+ Ohne diese Zeile staenden beide Dateien nebeneinander und taeten dasselbe;
13
+ nur wuerde `k3firma.md` nie wieder gepflegt.
14
+
15
+ ⚠️ Damit laeuft `raeumeVeralteteAuf` zum ersten Mal scharf. Der Kommentar
16
+ an der Funktion sagt es selbst: Die Schleife ist nie gelaufen. Deshalb
17
+ gehoert zu diesem Eintrag eine Rot-Probe in BEIDE Richtungen -- eine Datei
18
+ MIT unserer Marke muss verschwinden, eine OHNE muss liegen bleiben.
19
+ */
20
+ { alt: "k3firma.md", ersetztDurch: "k3analyse.md" },
21
+ ];
23
22
  /**
24
23
  * Woran wir unsere eigenen Befehlsdateien erkennen.
25
24
  *
@@ -344,7 +343,42 @@ function slashBefehleInstallieren() {
344
343
  console.error(`Die Slash-Befehle liessen sich nicht alle schreiben: ${String(fehler)}`);
345
344
  gestoert = true;
346
345
  }
347
- raeumeVeralteteAuf(zielOrdner, VERALTETE_BEFEHLE);
346
+ /*
347
+ πŸ”΄ **NUR aufraeumen, wenn das Schreiben durchlief** (Review-Runde 2,
348
+ 2026-08-28). Der Aufruf stand ausserhalb des `try` und lief deshalb auch
349
+ nach einem Fehler -- mit einer boesen Folge: `k3firma.md` wird geloescht,
350
+ obwohl `k3analyse.md` womoeglich nie geschrieben wurde. Der Mensch haette
351
+ danach **keinen von beiden** Befehlen.
352
+
353
+ ⚠️ Und der Fall ist naeher, als er klingt: Die Dateien werden alphabetisch
354
+ kopiert, `k3analyse.md` ist die ZWEITE. Ein Rechteproblem oder eine volle
355
+ Platte genau dort genuegt.
356
+
357
+ πŸ”‘ Die sichere Richtung ist eindeutig: Lieber traegt jemand den alten
358
+ Befehl eine Runde laenger mit sich herum, als dass er ohne dasteht. Ein
359
+ zweiter Lauf holt beides nach -- der Rueckgabewert sagt ihm das ohnehin.
360
+ */
361
+ /*
362
+ πŸ”΄ **Nachgeschaerft in Runde 3.** Die erste Fassung hiess `if (!gestoert)`
363
+ und deckte nur EINE der drei Arten ab, auf die eine Datei nicht landet:
364
+ `gestoert` (eine Ausnahme beim Kopieren) -- aber nicht `unlesbar` und nicht
365
+ `uebersprungen` (dort liegt eine Datei OHNE unsere Marke, die wir bewusst
366
+ stehen lassen). In beiden Faellen waere `k3firma.md` geloescht worden,
367
+ obwohl `k3analyse.md` nie geschrieben wurde.
368
+
369
+ πŸ”‘ Die Bedingung trifft jetzt die Absicht direkt: **Ein alter Name wird nur
370
+ entfernt, wenn sein Ersatz wirklich liegt.** Steht kein Ersatz in der
371
+ Tabelle, gilt der Eintrag als ersatzlos gestrichen und darf immer weg.
372
+ */
373
+ /*
374
+ ⚠️ **Beide Schreibweisen, genau wie `raeumeVeralteteAuf`** (Review-Runde 4).
375
+ `VERALTETE_BEFEHLE` nimmt `"k3alt"` UND `"k3alt.md"`; `geschrieben` traegt
376
+ immer den Dateinamen. Ohne die Angleichung schluege der Vergleich fehl, und
377
+ ein Eintrag ohne `.md` bliebe fuer immer liegen.
378
+ */
379
+ const alsDatei = (n) => (n.endsWith(".md") ? n : `${n}.md`);
380
+ const ersatzLiegt = VERALTETE_BEFEHLE.filter((e) => e.ersetztDurch === null || geschrieben.includes(alsDatei(e.ersetztDurch))).map((e) => e.alt);
381
+ raeumeVeralteteAuf(zielOrdner, ersatzLiegt);
348
382
  return {
349
383
  geschrieben: geschrieben.map((n) => n.replace(/\.md$/, "")),
350
384
  uebersprungen: uebersprungen.map((n) => n.replace(/\.md$/, "")),
@@ -1,7 +1,10 @@
1
1
  import { existsSync, statSync } from "node:fs";
2
2
  import { resolve } from "node:path";
3
- import { dateienSchreiben, istUnsichererOrdnername, vorlageHolen, werkbankOrdner, } from "../arbeitsplatz-dateien.js";
3
+ import { FIRMEN_OPTIONEN, dateienSchreiben, istUnsichererOrdnername, vorlageHolen, werkbankOrdner, } from "../arbeitsplatz-dateien.js";
4
+ import { alsBefehlszeile, alsWort, trenneNamenUndOptionen, zerlege } from "../argumente.js";
4
5
  import { werkbankOrdnerAus, werkbankUmbenennen } from "../werkbank-name.js";
6
+ import { FIRMEN_CLAUDE_MD, MERKZETTEL_PFAD, firmenAbgleich, firmennameAusClaudeMd, leseMerkzettel, } from "../kontext-stand.js";
7
+ import { leseEineDatei, leseMerkzettelDatei } from "../kontext-dateien.js";
5
8
  /**
6
9
  * Legt **nur eine Werkbank** an β€” in einem Firmen-Ordner, der schon steht.
7
10
  *
@@ -54,7 +57,7 @@ import { werkbankOrdnerAus, werkbankUmbenennen } from "../werkbank-name.js";
54
57
  * darf ein lokaler Befehl nicht ΓΌberschreiben.
55
58
  */
56
59
  const HILFE = `
57
- kimal3 werkbank neu [dein-vorname]
60
+ kimal3 werkbank neu [dein-vorname] [--firma "Name" | --firma-id "…"]
58
61
 
59
62
  Legt eine eigene Werkbank in einem bestehenden Firmen-Ordner an.
60
63
  Die Firmenebene (kontext/, CLAUDE.md, README.md) wird NICHT angefasst.
@@ -63,6 +66,12 @@ const HILFE = `
63
66
  wird "werkbank-sabine". Ohne Angabe steht dort der Name,
64
67
  den der Server aus deiner Anmelde-Adresse gebildet hat.
65
68
 
69
+ --firma "Name" Noetig, wenn du zu mehreren Firmen gehoerst.
70
+ Die Absage nennt dir die Namen als fertige Befehle.
71
+
72
+ --firma-id "…" Fuer den Fall, dass zwei deiner Firmen GLEICH heissen.
73
+ Auch diese Absage nennt dir fertige Befehle.
74
+
66
75
  πŸ”΄ Bricht ab, wenn der Zielordner schon existiert und NICHT leer ist.
67
76
 
68
77
  Du hast noch gar keinen Firmen-Ordner?
@@ -94,7 +103,7 @@ const HILFE = `
94
103
  * Eingestaendnis, dass man es nicht weiss -- hier weiss man es.*
95
104
  */
96
105
  function hinweisFuer(ordnerName) {
97
- const ziel = ordnerName.includes(" ") ? `"${ordnerName}"` : ordnerName;
106
+ const ziel = alsWort(ordnerName);
98
107
  return (" Ist das DEINE Werkbank? Dann steht sie schon -- geh einfach hinein:\n" +
99
108
  ` cd ${ziel}\n` +
100
109
  " Bist du es nicht, nimm deinen eigenen Vornamen:\n" +
@@ -116,37 +125,58 @@ export async function werkbankNeu(argv) {
116
125
  `werkbank-werkbank-sabine` zu bauen. *Alte Anleitungen ueberleben laenger
117
126
  als der Code, den sie beschreiben.*
118
127
  */
119
- const nameArg = argv[0];
128
+ /*
129
+ πŸ”΄ **Erst schneiden, dann zerlegen** -- wortgleich zum Zwilling in
130
+ `arbeitsplatz.ts` (Punkt A-3, 2026-08-26). Dieser Befehl hat ein Wort OHNE
131
+ `--` (den Vornamen), und `zerlege` lehnt genau das ab.
132
+ */
133
+ const { woerter, optionen: optionsArgv } = trenneNamenUndOptionen(argv);
134
+ const nameArg = woerter[0];
120
135
  if (nameArg !== undefined && nameArg.trim() === "") {
121
136
  console.error(`Ein leerer Name ist kein gueltiger Name.\n${HILFE}`);
122
137
  return 1;
123
138
  }
124
- if (nameArg !== undefined && nameArg.startsWith("--")) {
125
- console.error(`Die Option β€ž${nameArg}" kenne ich nicht.\n${HILFE}`);
126
- return 1;
127
- }
128
- if (argv.length > 1) {
139
+ if (woerter.length > 1) {
129
140
  /*
130
141
  πŸ”΄ **Der Vorschlag darf keine Optionen einsammeln** (Review-Runde 5 zu
131
142
  PR #25). Hier stand `argv.join("-")` -- aus `werkbank neu sabine --force`
132
- wurde damit der Rat `werkbank neu sabine---force`. Die `--`-Wache ein paar
133
- Zeilen hoeher sieht nur `argv[0]`.
143
+ wurde damit der Rat `werkbank neu sabine---force`.
134
144
  *Ein Rat, der ein falsches Kommando erzeugt, ist schlimmer als keiner* --
135
145
  dieselbe Klasse wie die Absagen aus Runde 1 und 2, nur kleiner.
136
146
 
137
147
  Deshalb: Sieht irgendein Wort wie eine Option aus, gibt es gar keinen
138
148
  Vorschlag. Ein Mensch, der `--force` tippt, sucht keine Schreibhilfe fuer
139
149
  seinen Vornamen.
150
+
151
+ ⚠️ Seit dem 2026-08-26 stehen die `--`-Optionen schon gar nicht mehr in
152
+ `woerter` (der Schnitt oben). Der Filter bleibt trotzdem: Er faengt das
153
+ EINFACHE `-` (`-force`), das kein Schnitt-Zeichen ist.
140
154
  */
141
- const woerter = argv.filter((w) => !w.startsWith("-"));
142
- const vorschlag = woerter.length === argv.length && woerter.length > 1
155
+ const einfacheWoerter = woerter.filter((w) => !w.startsWith("-"));
156
+ /*
157
+ πŸ”΄ **Die getippten Optionen reisen mit** (Review-Befund 3, 2026-08-26).
158
+ Ohne sie riet die Zeile zu einem Befehl, der bei einem Menschen in zwei
159
+ Firmen sofort wieder in dieselbe Absage laeuft -- s. `alsBefehlszeile`.
160
+ */
161
+ const optionsAnhang = optionsArgv.length === 0 ? "" : ` ${alsBefehlszeile(optionsArgv)}`;
162
+ const vorschlag = einfacheWoerter.length === woerter.length
143
163
  ? `Besteht dein Name aus mehreren Woertern? Dann schreib ihn zusammen:\n` +
144
- ` kimal3 werkbank neu ${woerter.join("-")}\n`
164
+ ` kimal3 werkbank neu ${einfacheWoerter.join("-")}${optionsAnhang}\n`
145
165
  : "";
146
- console.error(`Mit β€ž${argv[1]}" kann ich hier nichts anfangen -- nur EIN Name ist erlaubt.\n` +
166
+ console.error(`Mit β€ž${woerter[1]}" kann ich hier nichts anfangen -- nur EIN Name ist erlaubt.\n` +
147
167
  `${vorschlag}${HILFE}`);
148
168
  return 1;
149
169
  }
170
+ // πŸ”‘ Erst NACH den Namens-Absagen -- s. Zwilling in `arbeitsplatz.ts`.
171
+ const zerlegt = zerlege(optionsArgv, FIRMEN_OPTIONEN);
172
+ if (!zerlegt.ok) {
173
+ console.error(`${zerlegt.meldung}\n${HILFE}`);
174
+ return 1;
175
+ }
176
+ const wunsch = {
177
+ firma: zerlegt.werte.optionen.firma,
178
+ firmaId: zerlegt.werte.optionen["firma-id"],
179
+ };
150
180
  // `null` heisst: nach dem Umschreiben blieb kein Zeichen uebrig, das in einem
151
181
  // Ordnernamen stehen darf. Ein Mensch hat hier getippt und verdient eine
152
182
  // Antwort -- der Server schweigt im selben Fall und bleibt beim Vordruck.
@@ -161,7 +191,17 @@ export async function werkbankNeu(argv) {
161
191
  return 1;
162
192
  }
163
193
  }
164
- const holen = await vorlageHolen();
194
+ /*
195
+ πŸ”΄ **HIER ENTSCHEIDET SICH, OB UEBERHAUPT ETWAS ENTSTEHT** (Punkt A-3) --
196
+ dieselbe Stelle wie beim Zwilling: Bei mehreren Firmen ohne Angabe kommt
197
+ `409`, dieser Aufruf endet mit `1`, und `dateienSchreiben` weiter unten
198
+ wird nie erreicht.
199
+ */
200
+ // πŸ”‘ Der getippte Vorname reist in der Absage mit -- s. Zwilling.
201
+ const befehlsname = nameArg === undefined
202
+ ? "werkbank neu"
203
+ : `werkbank neu ${alsWort(nameArg)}`;
204
+ const holen = await vorlageHolen(befehlsname, wunsch);
165
205
  if (!holen.ok)
166
206
  return holen.wert;
167
207
  const daten = holen.daten;
@@ -305,6 +345,77 @@ export async function werkbankNeu(argv) {
305
345
  console.error(" verschiedene Firmenordner mit verschiedenen Regeln darin.");
306
346
  return 1;
307
347
  }
348
+ /*
349
+ πŸ”΄ **STEHT DIE GELIEFERTE FIRMA UEBERHAUPT HIER?** (Befund 1 der zweiten
350
+ Review-Runde, 2026-08-27.)
351
+
352
+ Bis hierher ist geprueft, dass NEBEN uns eine Firmenebene liegt -- aber
353
+ nicht, ob es DIE Firma ist, deren Namen die Lieferung gleich in die
354
+ `werkbank-<name>/CLAUDE.md` schreibt. Seit A-11 ist das keine Kosmetik
355
+ mehr: Dort stand frueher fuer jede Firma derselbe Platzhalter, jetzt steht
356
+ dort ein Name, den Claude bei jedem Start liest. Die ganze Fallgeschichte
357
+ haengt an `firmenAbgleich` in `../kontext-stand.js`.
358
+
359
+ πŸ”‘ **Der Vergleich laeuft ueber die IDs im Merkzettel** -- der des Ordners,
360
+ in dem wir stehen, gegen den, der gerade geliefert wurde. Beide Seiten
361
+ kennen ihre Firma aus derselben Quelle, es wird nichts geraten.
362
+
363
+ πŸ”΄ **UND ER STEHT VOR `dateienSchreiben`.** Ein Abbruch hier hinterlaesst
364
+ keine halbe Werkbank -- dieselbe Haltung wie in `kontextHoch`
365
+ (β€žich schicke nichts Halbes hochβ€œ).
366
+ */
367
+ const inhaltAus = (pfad) => daten.dateien.find((d) => d.pfad === pfad)?.inhalt ?? null;
368
+ const hierGelesen = leseEineDatei(process.cwd(), FIRMEN_CLAUDE_MD);
369
+ const abgleich = firmenAbgleich({
370
+ id: leseMerkzettelDatei(process.cwd()).firma_id,
371
+ name: firmennameAusClaudeMd(hierGelesen.ok ? hierGelesen.inhalt : null),
372
+ }, {
373
+ id: leseMerkzettel(inhaltAus(MERKZETTEL_PFAD)).firma_id,
374
+ name: firmennameAusClaudeMd(inhaltAus(FIRMEN_CLAUDE_MD)),
375
+ });
376
+ /*
377
+ πŸ”΄ **DER VERDACHT WARNT, ER SPERRT NICHT** (Befund 1 der vierten
378
+ Review-Runde). Steht der Name nur deshalb anders da, weil jemand die
379
+ `CLAUDE.md` seiner Firma bearbeitet hat -- was er ausdruecklich darf --,
380
+ waere eine Absage falsch UND ausweglos. Die Zeile hier sagt, was sie
381
+ sieht, und laesst den Menschen entscheiden.
382
+ */
383
+ if (abgleich.art === "zweifel") {
384
+ console.error(`Achtung: Dieser Ordner nennt als Firma β€ž${abgleich.hier}",`);
385
+ console.error(`du hast aber β€ž${abgleich.gewaehlt}" gewaehlt.`);
386
+ console.error("Ich lege die Werkbank trotzdem an -- vielleicht heisst die Firma bei euch");
387
+ console.error("nur anders geschrieben. Stimmt es nicht, loesch den neuen Ordner wieder.");
388
+ console.error("");
389
+ }
390
+ if (abgleich.art === "andere-firma") {
391
+ /*
392
+ πŸ”΄ **ZWEI LAGEN, ZWEI SAETZE** (Befund 3 der dritten Review-Runde). Hier
393
+ stand ein Rueckfall `?? "die genannte Firma"`, und der war erreichbar:
394
+ Wer zu genau EINER Firma gehoert, tippt gar kein `--firma`. Steht er im
395
+ Ordner einer anderen Firma, las er woertlich *β€žDieser Ordner gehoert zu
396
+ einer ANDEREN Firma als β€šdie genannte Firma'."* -- ein Satz, der nichts
397
+ benennt und wie ein Fehler im Programm aussieht.
398
+ *Ein Platzhalter in einer Meldung ist ein Satz, den niemand geschrieben
399
+ hat.*
400
+ */
401
+ const genannt = wunsch.firma ?? wunsch.firmaId;
402
+ if (genannt === undefined) {
403
+ console.error("Dieser Ordner gehoert zu einer anderen Firma als der, zu der du gehoerst.");
404
+ console.error("Ich lege hier keine Werkbank an, die einen fremden Firmennamen traegt --");
405
+ console.error("sie wuerde Claude bei jedem Start die falsche Firma nennen.");
406
+ console.error("");
407
+ console.error(" Deine Werkbank gehoert in den Ordner DEINER Firma. Geh dorthin und");
408
+ console.error(" versuch es dort noch einmal.");
409
+ return 1;
410
+ }
411
+ console.error(`Dieser Ordner gehoert zu einer ANDEREN Firma als β€ž${genannt}".`);
412
+ console.error("Ich lege hier keine Werkbank an, die einen fremden Firmennamen traegt --");
413
+ console.error("sie wuerde Claude bei jedem Start die falsche Firma nennen.");
414
+ console.error("");
415
+ console.error(` Soll die Werkbank fuer β€ž${genannt}" sein? Dann gehoert sie in DEREN Ordner.`);
416
+ console.error(" Soll sie fuer die Firma hier sein? Dann nimm deren Namen.");
417
+ return 1;
418
+ }
308
419
  /*
309
420
  πŸ”‘ Der Zielordner ist der Werkbank-Name -- entweder der, den der Server
310
421
  schon gesetzt hat (`werkbank`, aus der Anmelde-Adresse gebildet), oder der
@@ -351,7 +462,7 @@ export async function werkbankNeu(argv) {
351
462
  console.log(" Die Firmenebene ist unberuehrt geblieben.");
352
463
  console.log("");
353
464
  console.log(" Weiter geht's:");
354
- console.log(` cd ${ordnerName.includes(" ") ? `"${ordnerName}"` : ordnerName}`);
465
+ console.log(` cd ${alsWort(ordnerName)}`);
355
466
  console.log(" claude");
356
467
  console.log(' dann "Lass uns einrichten" tippen');
357
468
  return 0;