kimal3 0.2.10 → 0.3.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.
Files changed (66) hide show
  1. package/README.md +137 -2
  2. package/dist/api.js +16 -7
  3. package/dist/arbeitsplatz-dateien.js +23 -19
  4. package/dist/argumente.js +25 -97
  5. package/dist/befehle/arbeitsplatz.js +50 -68
  6. package/dist/befehle/firma-daten.js +24 -26
  7. package/dist/befehle/kontext-fragen.js +57 -47
  8. package/dist/befehle/kontext-gemeinsam.js +84 -40
  9. package/dist/befehle/kontext-hoch.js +40 -46
  10. package/dist/befehle/kontext-runter.js +70 -51
  11. package/dist/befehle/login.js +23 -9
  12. package/dist/befehle/logout.js +9 -4
  13. package/dist/befehle/projekt-liste.js +24 -29
  14. package/dist/befehle/projekt-neu.js +25 -31
  15. package/dist/befehle/skill-install.js +78 -49
  16. package/dist/befehle/skill-tausch.js +28 -0
  17. package/dist/befehle/wache.js +12 -6
  18. package/dist/befehle/werkbank.js +143 -104
  19. package/dist/befehle-katalog-en.js +185 -0
  20. package/dist/befehle-katalog.js +11 -50
  21. package/dist/befehlswoerter.js +69 -0
  22. package/dist/einstellungen-mischen.js +2 -2
  23. package/dist/einstellungen-schreiben.js +8 -5
  24. package/dist/fehler.js +5 -5
  25. package/dist/firmen-liste.js +8 -5
  26. package/dist/format.js +41 -0
  27. package/dist/fragen-lesen.js +3 -3
  28. package/dist/hilfe.js +24 -0
  29. package/dist/kimal3.js +77 -71
  30. package/dist/kontext-dateien.js +30 -23
  31. package/dist/kontext-hoch-zeile.js +5 -3
  32. package/dist/kontext-rat.js +151 -0
  33. package/dist/kontext-stand.js +79 -210
  34. package/dist/nachricht.js +39 -15
  35. package/dist/projekt-neu-firma.js +9 -9
  36. package/dist/projekt-zeilen.js +40 -54
  37. package/dist/slash-befehle.js +36 -0
  38. package/dist/sprache.js +114 -0
  39. package/dist/texte/allgemein.js +99 -0
  40. package/dist/texte/arbeitsplatz.js +135 -0
  41. package/dist/texte/kontext-hoch.js +128 -0
  42. package/dist/texte/kontext.js +171 -0
  43. package/dist/texte/projekte.js +126 -0
  44. package/dist/texte/skill.js +87 -0
  45. package/dist/texte/wache.js +36 -0
  46. package/dist/texte/werkbank.js +161 -0
  47. package/dist/wache-rechnung.js +27 -40
  48. package/dist/werkbank-name.js +28 -10
  49. package/dist/wort.js +98 -0
  50. package/package.json +3 -3
  51. package/skill/SKILL.md +9 -9
  52. package/skill/commands/k3.md +7 -4
  53. package/skill/commands/k3feierabend.md +99 -97
  54. package/skill/commands/k3kontext.md +1 -1
  55. package/skill/commands/k3kurs.md +6 -5
  56. package/skill/commands/k3start.md +2 -1
  57. package/skill/en/SKILL.md +451 -0
  58. package/skill/en/commands/k3.md +106 -0
  59. package/skill/en/commands/k3analyse.md +59 -0
  60. package/skill/en/commands/k3context.md +92 -0
  61. package/skill/en/commands/k3course.md +54 -0
  62. package/skill/en/commands/k3list.md +26 -0
  63. package/skill/en/commands/k3project.md +50 -0
  64. package/skill/en/commands/k3start.md +47 -0
  65. package/skill/en/commands/k3wrapup.md +99 -0
  66. package/dist/zz-kaputt.js +0 -1
package/dist/kimal3.js CHANGED
@@ -16,10 +16,14 @@ import { BASIS_URL, leseAnmeldung } from "./anmeldung.js";
16
16
  import { gemerkterHinweis, lokalerKontextStandJetzt, NichtAngemeldet } from "./api.js";
17
17
  import { leseGesehen, merkeGesehen } from "./gesehen.js";
18
18
  import { ausfallMeldung, hinweisAnhaengen } from "./wache-rechnung.js";
19
+ import { WACHE_TEXTE } from "./texte/wache.js";
19
20
  import { leseWacheDatei, sitzungJetzt, wacheAusgefallen } from "./wache-schalter.js";
20
- import { formatiere, heuteAus, skillMeldung, skillZustand, sollZeigen, } from "./nachricht.js";
21
+ import { formatiere, heuteAus, skillZustand, sollZeigen, } from "./nachricht.js";
21
22
  import { eigeneVersion } from "./version.js";
22
- import { hilfeText } from "./befehle-katalog.js";
23
+ import { sprache } from "./sprache.js";
24
+ import { ALLGEMEIN_TEXTE } from "./texte/allgemein.js";
25
+ import { hilfeText } from "./hilfe.js";
26
+ import { erkenneBefehl } from "./befehlswoerter.js";
23
27
  import { arbeitsplatzNeu } from "./befehle/arbeitsplatz.js";
24
28
  import { werkbankNeu } from "./befehle/werkbank.js";
25
29
  import { firmaDaten } from "./befehle/firma-daten.js";
@@ -135,7 +139,9 @@ function gegenUnbehandelteKanalfehlerAbsichern(kanal, meldenAuf) {
135
139
  return;
136
140
  schonGemeldet = true;
137
141
  try {
138
- meldenAuf.write(`kimal3: Ausgabekanal-Fehler -- ${fehler.message}\n`);
142
+ // 🌐 Teil 3: die Sprache wird erst JETZT gelesen (zur Schreibzeit); der Empfaenger selbst
143
+ // bleibt auf Modulebene vor der ersten Zeile angemeldet.
144
+ meldenAuf.write(ALLGEMEIN_TEXTE[sprache()].outputChannelError(fehler.message));
139
145
  }
140
146
  catch {
141
147
  // Auch der Fehlerkanal kann kaputt sein -- dann bleibt wenigstens der
@@ -149,7 +155,6 @@ gegenUnbehandelteKanalfehlerAbsichern(process.stderr, null);
149
155
  // `befehle-katalog.ts` und wird von dort AUCH im Pro-Bereich der App angezeigt.
150
156
  // Zwei Listen ueber dieselben Befehle driften — die eine bekaeme den naechsten
151
157
  // Befehl, die andere nicht, und keiner der beiden Orte wuesste davon.
152
- const HILFE = hilfeText();
153
158
  /**
154
159
  * Hilfe plus Zustand.
155
160
  *
@@ -189,8 +194,9 @@ function versionsZeilen() {
189
194
  // Zeile zeigt einem Menschen etwas an, und da ist "unbekannt" die ehrliche
190
195
  // Antwort. Der andere Aufrufer RECHNET mit dem Wert und braucht dort das
191
196
  // Gegenteil -- er will lieber gar keine Nummer als eine erfundene.
192
- const version = eigeneVersion() ?? "unbekannt";
193
- return ` kimal3 ${version}\n spricht mit ${BASIS_URL}`;
197
+ const t = ALLGEMEIN_TEXTE[sprache()];
198
+ const version = eigeneVersion() ?? t.versionUnknown;
199
+ return t.versionLines(version, BASIS_URL);
194
200
  }
195
201
  /**
196
202
  * Welcher Update-Hinweis soll ans Ende -- oder keiner?
@@ -254,12 +260,12 @@ function hinweisText() {
254
260
  // verrottet lautlos (Befund der vierten Review-Runde).
255
261
  const datei = skillDatei();
256
262
  const inhalt = existsSync(datei) ? readFileSync(datei, "utf8") : null;
257
- if (skillZustand(inhalt, eigene) === "veraltet") {
263
+ if (skillZustand(inhalt, eigene, sprache()) === "veraltet") {
258
264
  // Auch dieser Hinweis nur einmal am Tag, unter einer festen Kennung.
259
265
  // Sie kann mit keiner Nachrichten-UUID kollidieren.
260
266
  const gesehen = leseGesehen();
261
267
  if (gesehen["skill-veraltet"] !== heute) {
262
- return { text: skillMeldung(), kennung: "skill-veraltet", tag: heute };
268
+ return { text: WACHE_TEXTE[sprache()].skillOutdated, kennung: "skill-veraltet", tag: heute };
263
269
  }
264
270
  }
265
271
  /*
@@ -281,7 +287,7 @@ function hinweisText() {
281
287
  // Auch dieser Hinweis hoechstens einmal am Tag, unter fester Kennung.
282
288
  const gesehen = leseGesehen();
283
289
  if (gesehen["wache-ausgefallen"] !== heute) {
284
- return { text: ausfallMeldung(), kennung: "wache-ausgefallen", tag: heute };
290
+ return { text: ausfallMeldung(sprache()), kennung: "wache-ausgefallen", tag: heute };
285
291
  }
286
292
  }
287
293
  }
@@ -463,17 +469,61 @@ function endeMitHinweis(code) {
463
469
  * nicht mehr Auskunft, sondern **weniger Behauptung**.
464
470
  */
465
471
  function zustandsZeile() {
472
+ const t = ALLGEMEIN_TEXTE[sprache()];
466
473
  const a = leseAnmeldung();
467
- return a
468
- ? ` ● Ausweis hinterlegt für ${a.email}\n` +
469
- " (ob er noch gilt, zeigt erst der nächste Befehl)"
470
- : " ● Nicht angemeldet. → kimal3 login";
474
+ return a ? t.credentialsStored(a.email) : t.notSignedIn;
475
+ }
476
+ /** `null` = der Rest passt nicht zu diesem Befehl -> "kenne ich nicht", wie bisher. */
477
+ async function fuehreAus(name, rest) {
478
+ const allein = rest.length === 0;
479
+ switch (name) {
480
+ case "login": return allein ? login() : null;
481
+ case "logout": return allein ? logout() : null;
482
+ case "skill install": return allein ? skillInstall() : null;
483
+ case "projekt neu": return projektNeu(rest);
484
+ case "projekt liste": return allein ? projektListe() : null;
485
+ // ⚠️ Rest wird durchgereicht, nicht auf Gleichheit geprueft: `firma daten --firma X` gehoert dazu.
486
+ case "firma daten": return firmaDaten(rest);
487
+ case "arbeitsplatz neu": return arbeitsplatzNeu(rest);
488
+ case "werkbank neu": return werkbankNeu(rest);
489
+ case "kontext runter": return kontextRunter(rest);
490
+ case "kontext hoch": return kontextHoch(rest);
491
+ case "kontext fragen": return kontextFragen(rest);
492
+ case "wache an": return allein ? wacheSchalten(true) : null;
493
+ case "wache aus": return allein ? wacheSchalten(false) : null;
494
+ case "wache":
495
+ // 🔴 Der Startbefehl aus `~/.claude/settings.json`, den `skill install` eintraegt (T3-5:
496
+ // eingetragen wird immer `kimal3 wache --hook`; `kimal3 watch --hook` laeuft genauso).
497
+ // Er verpackt die Meldung anders -- Begruendung an `wache()`.
498
+ // ⚠️ Ein Mensch tippt das nie; der Katalog nennt ihn deshalb auch nicht.
499
+ if (rest.length === 1 && rest[0] === "--hook")
500
+ return wache(true);
501
+ /*
502
+ 🔴 NACHSICHT GEGENUEBER FLAGGEN, DIE ES HIER NOCH NICHT GIBT.
503
+ Der Eintrag in `~/.claude/settings.json` ueberlebt jede Neuinstallation --
504
+ auch eine, die eine AELTERE Fassung bringt. Steht dort eines Tages
505
+ `wache --irgendwas`, das diese Fassung nicht kennt, faellt der Aufruf ohne
506
+ diese Zeile in den Unbekannt-Zweig: die volle Hilfe bei JEDEM Claude-Start,
507
+ Rueckgabewert 1, und `merkeSitzung` wird nie erreicht -- woraufhin
508
+ `wacheAusgefallen` bei jedem weiteren Befehl einen Rat gibt, der nichts
509
+ behebt. Aus einem fehlenden Zusatz wird so ein lauter Dauerschaden.
510
+ 🔑 Warum die MENSCHEN-Form die richtige Rueckfallebene ist: Sie endet
511
+ normal mit 0, ihre Meldung kommt also an. Die JSON-Form waere fuer einen
512
+ Menschen, der aus Neugier eine Flagge tippt, nur Kauderwelsch.
513
+ ⚠️ Fuer bereits veroeffentlichte Fassungen ist das nicht mehr zu retten --
514
+ `0.2.3` kennt `--hook` nicht. Diese Zeile sorgt dafuer, dass die naechste
515
+ Flagge die Falle nicht erneut stellt. (Befund der Durchsicht, 2026-09-09.)
516
+ */
517
+ if (rest.every((a) => a.startsWith("--")))
518
+ return wache(false);
519
+ return null;
520
+ }
471
521
  }
472
522
  async function haupt() {
473
523
  const argv = process.argv.slice(2);
474
524
  const wort = argv.join(" ");
475
525
  if (argv.length === 0 || wort === "--help" || wort === "-h") {
476
- console.log(HILFE);
526
+ console.log(hilfeText(sprache()));
477
527
  console.log(zustandsZeile() + "\n");
478
528
  return 0;
479
529
  }
@@ -482,61 +532,17 @@ async function haupt() {
482
532
  console.log(zustandsZeile() + "\n");
483
533
  return 0;
484
534
  }
485
- if (wort === "login")
486
- return login();
487
- if (wort === "logout")
488
- return logout();
489
- if (argv[0] === "projekt" && argv[1] === "neu")
490
- return projektNeu(argv.slice(2));
491
- if (wort === "projekt liste")
492
- return projektListe();
493
- // ⚠️ Praefix-Vergleich wie bei `projekt neu` und NICHT `wort === "firma daten"`:
494
- // `wort` enthaelt die Optionen mit, `firma daten --firma X` fiele bei einem
495
- // Gleichheits-Vergleich durch und meldete "Den Befehl kenne ich nicht".
496
- if (argv[0] === "firma" && argv[1] === "daten")
497
- return firmaDaten(argv.slice(2));
498
- if (wort === "skill install")
499
- return skillInstall();
500
- if (argv[0] === "arbeitsplatz" && argv[1] === "neu")
501
- return arbeitsplatzNeu(argv.slice(2));
502
- if (argv[0] === "werkbank" && argv[1] === "neu")
503
- return werkbankNeu(argv.slice(2));
504
- if (argv[0] === "kontext" && argv[1] === "runter")
505
- return kontextRunter(argv.slice(2));
506
- if (argv[0] === "kontext" && argv[1] === "hoch")
507
- return kontextHoch(argv.slice(2));
508
- if (argv[0] === "kontext" && argv[1] === "fragen")
509
- return kontextFragen(argv.slice(2));
510
- if (wort === "wache")
511
- return wache(false);
512
- // 🔴 Der Startbefehl aus `~/.claude/settings.json`, den `skill install`
513
- // eintraegt. Er verpackt die Meldung anders -- Begruendung an `wache()`.
514
- // ⚠️ Ein Mensch tippt das nie; der Katalog nennt ihn deshalb auch nicht.
515
- if (wort === "wache --hook")
516
- return wache(true);
517
- if (wort === "wache aus")
518
- return wacheSchalten(false);
519
- if (wort === "wache an")
520
- return wacheSchalten(true);
521
- /*
522
- 🔴 NACHSICHT GEGENUEBER FLAGGEN, DIE ES HIER NOCH NICHT GIBT.
523
- Der Eintrag in `~/.claude/settings.json` ueberlebt jede Neuinstallation --
524
- auch eine, die eine AELTERE Fassung bringt. Steht dort eines Tages
525
- `wache --irgendwas`, das diese Fassung nicht kennt, faellt der Aufruf ohne
526
- diese Zeile in den Unbekannt-Zweig: die volle Hilfe bei JEDEM Claude-Start,
527
- Rueckgabewert 1, und `merkeSitzung` wird nie erreicht -- woraufhin
528
- `wacheAusgefallen` bei jedem weiteren Befehl einen Rat gibt, der nichts
529
- behebt. Aus einem fehlenden Zusatz wird so ein lauter Dauerschaden.
530
- 🔑 Warum die MENSCHEN-Form die richtige Rueckfallebene ist: Sie endet
531
- normal mit 0, ihre Meldung kommt also an. Die JSON-Form waere fuer einen
532
- Menschen, der aus Neugier eine Flagge tippt, nur Kauderwelsch.
533
- ⚠️ Fuer bereits veroeffentlichte Fassungen ist das nicht mehr zu retten --
534
- `0.2.3` kennt `--hook` nicht. Diese Zeile sorgt dafuer, dass die naechste
535
- Flagge die Falle nicht erneut stellt. (Befund der Durchsicht, 2026-09-09.)
536
- */
537
- if (argv[0] === "wache" && argv.slice(1).every((a) => a.startsWith("--")))
538
- return wache(false);
539
- console.error(`Den Befehl „${wort}" kenne ich nicht.\n${HILFE}`);
535
+ // 🌐 Teil 3 (T3-3): ein Verteiler fuer beide Sprachen. Was heute nur ALLEIN gilt
536
+ // (`login`, `projekt liste`, `wache an` …), gilt auch weiterhin nur allein -- `kimal3 login
537
+ // extra` bleibt unbekannt. Der grosse Kommentar zur Nachsicht bei `wache --irgendwas` steht
538
+ // unten an `fuehreAus` und gilt unveraendert.
539
+ const erkannt = erkenneBefehl(argv);
540
+ if (erkannt) {
541
+ const code = await fuehreAus(erkannt.name, erkannt.rest);
542
+ if (code !== null)
543
+ return code;
544
+ }
545
+ console.error(`${ALLGEMEIN_TEXTE[sprache()].unknownCommand(wort)}${hilfeText(sprache())}`);
540
546
  return 1;
541
547
  }
542
548
  haupt()
@@ -564,10 +570,10 @@ haupt()
564
570
  // Node laeuft aus, sobald die Kanaele leer sind.
565
571
  .catch((f) => {
566
572
  if (f instanceof NichtAngemeldet) {
567
- console.error("Bitte zuerst `kimal3 login` ausführen.");
573
+ console.error(ALLGEMEIN_TEXTE[sprache()].signInFirst);
568
574
  process.exitCode = 2;
569
575
  return;
570
576
  }
571
- console.error("kimal3 ist gerade nicht erreichbar.");
577
+ console.error(ALLGEMEIN_TEXTE[sprache()].unreachable);
572
578
  process.exitCode = 3;
573
579
  });
@@ -1,7 +1,9 @@
1
- import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
1
+ import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
2
2
  import { dirname, join, resolve } from "node:path";
3
3
  import { istUnsichererPfad } from "./arbeitsplatz-dateien.js";
4
- import { MERKZETTEL_PFAD, VERWALTETE_DATEIEN, arbeitsplatzAus, leseMerkzettel, schreibeMerkzettel, sicherungsZiel, } from "./kontext-stand.js";
4
+ import { sprache as menschSprache } from "./sprache.js";
5
+ import { KONTEXT_TEXTE } from "./texte/kontext.js";
6
+ import { MERKZETTEL_PFAD, verwalteteDateien, arbeitsplatzAus, ORDNER_NAMEN, leseMerkzettel, schreibeMerkzettel, sicherungsZiel, } from "./kontext-stand.js";
5
7
  /**
6
8
  * Das Dateisystem hinter `kimal3 kontext runter` -- Ordner finden, die
7
9
  * verwalteten Dateien lesen, sichern, schreiben, den Merkzettel lesen und
@@ -23,13 +25,17 @@ import { MERKZETTEL_PFAD, VERWALTETE_DATEIEN, arbeitsplatzAus, leseMerkzettel, s
23
25
  * hier nie helfen kann. Ein Schreibfehler (Platte voll, `EACCES`) gibt
24
26
  * deshalb `1` zurueck, nicht `3`.
25
27
  */
26
- /** Ist `pfad` (relativ zu `wurzel`) ein lesbarer ORDNER? Unlesbar zaehlt als "nein". */
27
- function istLesbarerOrdner(pfad) {
28
+ /**
29
+ * Die Namen der Eintraege (ohne Unterordner) in `pfad`. Fehlt der Ordner oder
30
+ * ist er unlesbar -> leere Liste: Ein Ordner, in den ich nicht hineinsehe,
31
+ * zaehlt wie einer ohne unsere Dateien (`arbeitsplatzAus`, Final-Review Fund 1).
32
+ */
33
+ function namenIn(pfad) {
28
34
  try {
29
- return existsSync(pfad) && statSync(pfad).isDirectory();
35
+ return readdirSync(pfad, { withFileTypes: true }).filter((e) => !e.isDirectory()).map((e) => e.name);
30
36
  }
31
37
  catch {
32
- return false;
38
+ return [];
33
39
  }
34
40
  }
35
41
  /** Ist `pfad` (relativ zu `wurzel`) eine lesbare DATEI? Unlesbar zaehlt als "nein". */
@@ -59,8 +65,9 @@ export function findeArbeitsplatz(start) {
59
65
  for (let i = 0; i < 200; i++) {
60
66
  kandidaten.push({
61
67
  pfad: aktuell,
62
- hatKontext: istLesbarerOrdner(join(aktuell, "kontext")),
63
- hatClaude: istLesbareDatei(join(aktuell, "CLAUDE.md")),
68
+ kontextNamen: namenIn(join(aktuell, ORDNER_NAMEN.de.kontext)),
69
+ contextNamen: namenIn(join(aktuell, ORDNER_NAMEN.en.kontext)),
70
+ hatClaude: istLesbareDatei(join(aktuell, "CLAUDE.md")), // kein-anzeigetext: Dateiname
64
71
  });
65
72
  const eltern = dirname(aktuell);
66
73
  if (eltern === aktuell)
@@ -81,7 +88,7 @@ export function leseEineDatei(ordner, pfad) {
81
88
  if (!statSync(ort).isFile()) {
82
89
  return {
83
90
  ok: false,
84
- meldung: `„${pfad}" existiert, ist aber keine Datei -- ich fasse das nicht an. Bitte pruefen.`,
91
+ meldung: KONTEXT_TEXTE[menschSprache()].notAFile(pfad),
85
92
  };
86
93
  }
87
94
  return { ok: true, inhalt: readFileSync(ort, "utf8") };
@@ -89,8 +96,7 @@ export function leseEineDatei(ordner, pfad) {
89
96
  catch (fehler) {
90
97
  return {
91
98
  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.",
99
+ meldung: KONTEXT_TEXTE[menschSprache()].cannotReach(pfad, fehler instanceof Error ? fehler.message : String(fehler)),
94
100
  };
95
101
  }
96
102
  }
@@ -120,7 +126,7 @@ export function schreibeMerkzettelDatei(ordner, m) {
120
126
  catch (fehler) {
121
127
  return {
122
128
  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.`,
129
+ meldung: KONTEXT_TEXTE[menschSprache()].trackingFailed(fehler instanceof Error ? fehler.message : String(fehler)),
124
130
  };
125
131
  }
126
132
  }
@@ -180,7 +186,8 @@ function nummeriertesZiel(basisZielRel, nummer) {
180
186
  return [...teile, `${name}-${nummer}${endung}`].join("/");
181
187
  }
182
188
  /**
183
- * Die lokale Fassung von `pfad` nach `kontext/vorher-<datum>/` sichern --
189
+ * Die lokale Fassung von `pfad` nach `kontext/vorher-<datum>/` (englisch
190
+ * `context/before-<datum>/`) sichern --
184
191
  * FLACH, mit dem Basisnamen (`sicherungsZiel`), oder -- ist der schon belegt
185
192
  * -- unter einem durchnummerierten Namen DANEBEN (`naechstesFreiesSicherungsziel`,
186
193
  * Ruling T8-2).
@@ -194,8 +201,8 @@ function nummeriertesZiel(basisZielRel, nummer) {
194
201
  * Ausgabe unterscheiden (§8: kein Fehlerfall traegt den Namen eines
195
202
  * Erfolgsfalls, und hier sind es zwei verschiedene ECHTE Erfolgsfaelle).
196
203
  */
197
- export function sichereDatei(ordner, pfad, inhalt, datum) {
198
- const basisZielRel = sicherungsZiel(datum, pfad);
204
+ export function sichereDatei(ordner, pfad, inhalt, datum, sprache) {
205
+ const basisZielRel = sicherungsZiel(sprache, datum, pfad);
199
206
  const istBelegt = (zielRel) => existsSync(join(ordner, ...zielRel.split("/")));
200
207
  const zielRel = naechstesFreiesSicherungsziel(basisZielRel, istBelegt);
201
208
  const ort = join(ordner, ...zielRel.split("/"));
@@ -207,7 +214,7 @@ export function sichereDatei(ordner, pfad, inhalt, datum) {
207
214
  catch (fehler) {
208
215
  return {
209
216
  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.`,
217
+ meldung: KONTEXT_TEXTE[menschSprache()].backupFailed(pfad, fehler instanceof Error ? fehler.message : String(fehler)),
211
218
  };
212
219
  }
213
220
  }
@@ -215,9 +222,9 @@ export function sichereDatei(ordner, pfad, inhalt, datum) {
215
222
  * Eine der verwalteten Dateien schreiben.
216
223
  *
217
224
  * 🔴 **Positivliste, keine Verbotsliste** (Auflage aus dem Brief): `pfad` muss
218
- * WOERTLICH einer der Eintraege aus `VERWALTETE_DATEIEN` sein, sonst wird
225
+ * WOERTLICH einer der Eintraege aus `verwalteteDateien(sprache)` sein, sonst wird
219
226
  * NICHTS geschrieben. Diese Funktion bekommt `pfad` im echten Ablauf immer aus
220
- * der eigenen Iteration ueber `VERWALTETE_DATEIEN` (nie roh aus der Server-
227
+ * der eigenen Iteration ueber `verwalteteDateien(sprache)` (nie roh aus der Server-
221
228
  * Antwort) -- die Pruefung hier ist trotzdem ausdruecklich verlangt und bleibt
222
229
  * als zweites Netz stehen, falls sich der Aufrufer einmal aendert.
223
230
  *
@@ -226,12 +233,12 @@ export function sichereDatei(ordner, pfad, inhalt, datum) {
226
233
  * einer Server-Antwort, und eine Pruefung, die nur dort gilt, wo man sie
227
234
  * gerade braucht, ist keine Pruefung dieser Funktion.
228
235
  */
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.` };
236
+ export function schreibeVerwalteteDatei(ordner, pfad, inhalt, sprache) {
237
+ if (!verwalteteDateien(sprache).includes(pfad)) {
238
+ return { ok: false, meldung: KONTEXT_TEXTE[menschSprache()].notManaged(pfad) };
232
239
  }
233
240
  if (istUnsichererPfad(pfad)) {
234
- return { ok: false, meldung: `„${pfad}" ist kein sicherer Pfad -- wird nicht geschrieben. Bitte melden.` };
241
+ return { ok: false, meldung: KONTEXT_TEXTE[menschSprache()].notSafePath(pfad) };
235
242
  }
236
243
  const ort = join(ordner, ...pfad.split("/"));
237
244
  try {
@@ -242,7 +249,7 @@ export function schreibeVerwalteteDatei(ordner, pfad, inhalt) {
242
249
  catch (fehler) {
243
250
  return {
244
251
  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.`,
252
+ meldung: KONTEXT_TEXTE[menschSprache()].writeFailed(pfad, fehler instanceof Error ? fehler.message : String(fehler)),
246
253
  };
247
254
  }
248
255
  }
@@ -1,11 +1,13 @@
1
+ import { KONTEXT_HOCH_TEXTE } from "./texte/kontext-hoch.js";
1
2
  /**
2
3
  * Die Erfolgszeile von `kimal3 kontext hoch` -- als reine Rechnung, damit
3
4
  * `check:cli` sie prueft. Sie nennt, was sich BEI DER FIRMA geaendert hat,
4
5
  * nicht, was gesendet wurde (B-e, 29.08.: fuenf Dateien genannt, zwei
5
6
  * geaendert, eine davon ohne Zutun des Menschen).
6
7
  */
7
- export function hochZeile(uebernommen) {
8
+ export function hochZeile(uebernommen, s) {
9
+ const t = KONTEXT_HOCH_TEXTE[s];
8
10
  if (uebernommen.length === 0)
9
- return "✓ nichts geändert — alles war schon so";
10
- return `✓ geändert bei der Firma: ${uebernommen.join(", ")}`;
11
+ return t.nothingChanged;
12
+ return t.changedAtCompany(uebernommen.join(", "));
11
13
  }
@@ -0,0 +1,151 @@
1
+ import { befehlIn } from "./befehlswoerter.js";
2
+ import { KONTEXT_TEXTE } from "./texte/kontext.js";
3
+ import { FIRMEN_CLAUDE_MD, ORDNER_NAMEN, firmenTexte, verwalteteDateien } from "./kontext-stand.js";
4
+ /*
5
+ Der Rat nach `kimal3 kontext runter`, wenn Dateien in Ruhe gelassen wurden (Teil 3, Task 10).
6
+
7
+ 🔑 **Umgezogen aus `kontext-stand.ts`.** Diese Datei hat die Sätze des Rats verlassen -- sie
8
+ gehören jetzt in `texte/kontext.ts`, in beiden Sprachen --, und `kontext-stand.ts` darf nichts
9
+ importieren (die App liest sie über den Bundler). Was die Logik angeht, ist alles geblieben:
10
+ welche Art Rat eine Datei bekommt, in welcher Reihenfolge, und dass jede verwaltete Datei einen
11
+ bekommt. Die Kommentare darunter stammen unverändert von dort.
12
+
13
+ 🔴 **ZWEI Sprachen, zwei Pflichtwerte** (Messung h): `ordner` bestimmt die PFADE im Rat
14
+ (`context/profile.md` heißt so, weil der Ordner so heißt), `sprache` bestimmt die SÄTZE und die
15
+ Befehlswörter darin. Bis Teil 3 war der Rat immer deutsch -- im englischen Ordner mit englischen
16
+ Pfaden --, weil das Programm nur Deutsch sprach; die Sprache des Menschen kennt er erst seit
17
+ jetzt. Beide Werte sind Pflicht, kein Vorgabewert: Wer nur einen nennt, bekommt die zweite
18
+ Sprache nicht still dazu.
19
+ */
20
+ /**
21
+ * Die ARTEN von Rat, die es gibt -- **in der Reihenfolge, in der sie im
22
+ * Terminal stehen.**
23
+ *
24
+ * 🔑 Diese Liste ist zugleich die Quelle des Typs {@link RatArt}. Wer eine
25
+ * dritte Art erfindet, muss sie hier eintragen -- und bekommt danach vom
26
+ * Compiler gesagt, dass {@link RAT_TEXT} noch keinen Text dafuer hat. *Eine
27
+ * Reihenfolge, die nebenher den Typ traegt, kann nicht unvollstaendig sein.*
28
+ *
29
+ * ⚠️ Die Reihenfolge ist nicht Geschmack: `kimal3 kontext runter` druckt seit
30
+ * Befund F-3 erst den Rat fuer die Firmen-Texte und darunter den fuer unsere
31
+ * Vorlage. Wer sie dreht, dreht die Ausgabe beim Kunden.
32
+ */
33
+ const RAT_REIHENFOLGE = ["hochschickbar", "nicht-hochschickbar"];
34
+ /**
35
+ * 🔴 **PUNKT A-7 (2026-08-26): DIESE TABELLE IST DIE ABSICHERUNG, NICHT NUR
36
+ * EINE AUFRAEUMAKTION.**
37
+ *
38
+ * Bis hierher zaehlte {@link ratNachUnberuehrt} ihre Faelle auf: ein `if` fuer
39
+ * „irgendetwas aus den Firmen-Texten" und ein zweites fuer den woertlichen
40
+ * Dateinamen `"CLAUDE.md"`. {@link verwalteteDateien} hat aber SECHS
41
+ * Eintraege, und eine kuenftige SIEBTE waere durch beide `if`s gefallen --
42
+ * ohne Fehler, ohne Meldung, ohne Rat. *Der Mensch haette gelesen, dass seine
43
+ * Datei in Ruhe gelassen wurde, und nirgends, was er jetzt tun soll.*
44
+ *
45
+ * 🔑 **Teil 2 (2026-10-01): aus der Tabelle `RAT_ART_JE_DATEI` wurde diese
46
+ * Funktion.** Die Tabelle hatte als Schluessel die sechs deutschen Pfade --
47
+ * mit `context/company.md` daneben haette sie zwei Saetze Schluessel
48
+ * gebraucht, die nur gemeinsam stimmen. Die Regel dahinter war immer nur eine:
49
+ * Die Firmen-`CLAUDE.md` traegt `kontext hoch` nicht, alles andere schon.
50
+ *
51
+ * ⚠️ **Was dabei verloren ging, ehrlich:** Der `Record` war eine Absicherung
52
+ * des Compilers -- eine siebte verwaltete Datei haette ihm einen Schluessel
53
+ * zu wenig gegeben. Jetzt bekommt eine siebte Datei still den Rat
54
+ * `hochschickbar`. Das ist nie „kein Rat" (der Fall, gegen den A-7 gebaut
55
+ * war), aber es waere der falsche, wenn die siebte Datei ebenfalls nicht
56
+ * hochgeschickt werden kann. Halten tut das jetzt allein die Pruefung in
57
+ * `check:kontext` unten.
58
+ *
59
+ * ⚠️ **Sie sagt dasselbe wie {@link firmenTexte}, und das ist eine
60
+ * Doppelung** -- gehalten wird sie von einer Pruefung in `check:kontext`
61
+ * („der `hoch`-Rat geht an genau die Dateien, die `kontext hoch` wirklich
62
+ * traegt"). Die konnte vor A-7 gar nicht rot werden, weil der Rat SELBST aus
63
+ * der Liste der Firmen-Texte entstand; jetzt sind es zwei Quellen, und ihr
64
+ * Auseinanderlaufen ist messbar.
65
+ */
66
+ function ratArt(pfad) {
67
+ return pfad === FIRMEN_CLAUDE_MD ? "nicht-hochschickbar" : "hochschickbar";
68
+ }
69
+ /**
70
+ * Der Wortlaut je Art und Rolle.
71
+ *
72
+ * 🔑 Die Saetze selbst stehen seit Teil 3 in `texte/kontext.ts` (mit echten Umlauten -- das
73
+ * liest ein Kunde).
74
+ *
75
+ * 🔑 `wer` ist die fertige Anrede (` an Lena` oder ` an euren Firmen-Admin`, englisch ` to Lena`),
76
+ * damit der Rueckfall auf den Sammelbegriff an EINER Stelle steht und nicht in
77
+ * jedem Zweig neu. Ruling T7-1: nie `null` im Klartext.
78
+ */
79
+ const RAT_TEXT = {
80
+ hochschickbar: (rolle, wer, _namen, t, b) => (rolle === "admin" ? t.adviceUploadAdmin(b.hoch) : t.adviceUploadMember(wer, b.hoch)),
81
+ /*
82
+ 🔴 **Auch dieser Zweig kennt die ROLLE -- Punkt A-5, 2026-08-26.** Er tat
83
+ es bis dahin nicht und riet jedem dasselbe: „Aendere sie dort", also im
84
+ Formular unter Firma → Kontext. Ein MITGLIED kann das nicht. `kimal3
85
+ kontext hoch` weist es ab, und den Firmenkontext der Firma darf es in der
86
+ App nicht schreiben. *Ein Rat, der die Lage nicht aendern kann, ist keiner*
87
+ -- er schickt einen Menschen an eine Tuer, vor der er keinen Schluessel
88
+ hat, und laesst ihn den Fehler bei sich suchen.
89
+
90
+ 🔑 Was das Mitglied stattdessen liest: WAS es weitergeben soll (die drei
91
+ Saetze aus `kontext/kurzprofil.md`) und AN WEN -- genau die Form, die der
92
+ Zweig fuer die fuenf `kontext/`-Dateien oben schon hat.
93
+ */
94
+ "nicht-hochschickbar": (rolle, wer, namen, t, b) => rolle === "admin"
95
+ ? t.adviceFileAdmin(namen.kontext, namen.kurzprofil, b.hoch, b.runter)
96
+ : t.adviceFileMember(namen.kontext, namen.kurzprofil, wer, b.hoch),
97
+ };
98
+ /**
99
+ * Was ein Mensch lesen soll, wenn `kimal3 kontext runter` Dateien in Ruhe
100
+ * laesst, weil er sie lokal geaendert hat -- je nach Rolle und je nach DATEI.
101
+ *
102
+ * 🔴 **DAS IST FUND F, ZUM ZWEITEN MAL -- und diesmal die echte Fassung
103
+ * (Review-Befund F-3, 2026-08-25).** Der Rat lautete fuer die ganze Liste auf
104
+ * einmal: „Deine Fassung ist die neuere? Dann schick sie hoch: kimal3 kontext
105
+ * hoch". Fuer `CLAUDE.md` ist dieser Satz eine Sackgasse: Sie steht in
106
+ * {@link verwalteteDateien} und landet deshalb in der Liste -- aber
107
+ * `kontext hoch` liest ausschliesslich die fuenf Dateien aus
108
+ * {@link firmenTexte} und meldet danach Erfolg, ohne sie. *Der Mensch tippt
109
+ * den geratenen Befehl, sieht einen Haken, und seine Aenderung steht weiter
110
+ * allein auf seiner Platte.*
111
+ *
112
+ * 🔑 Die ausgelieferte `CLAUDE.md` laedt diese Bearbeitung foermlich ein: Sie
113
+ * sagt selbst, aendern muesse man sie nur, wenn sich die Firma aendert.
114
+ *
115
+ * 🔴 **Der Rat wohnt hier und nicht im Befehl, damit ihn eine Wache sehen
116
+ * kann.** Die spurious Fassung desselben Fehlers (`merkzettelNachHoch`) war
117
+ * behoben, die echte lag vier Runden lang offen -- *ein Satz ueber das
118
+ * Verhalten eines Befehls wohnte im Befehl selbst, wo keine Vorlagen-Wache je
119
+ * hinsieht.* `check:kontext` prueft jetzt genau diese Zuordnung.
120
+ *
121
+ * 🔴 **PUNKT A-7 (2026-08-26): DER RAT WIRD ERZEUGT, NICHT AUFGEZAEHLT.** Diese
122
+ * Funktion hatte zwei `if`s -- eines fuer „irgendetwas aus
123
+ * {@link firmenTexte}", eines fuer den woertlichen Namen `"CLAUDE.md"`. Eine
124
+ * siebte verwaltete Datei waere durch beide gefallen und haette schweigend gar
125
+ * keinen Rat bekommen. Jetzt laeuft die Schleife ueber
126
+ * {@link verwalteteDateien} selbst und fragt {@link ratArt} nach der Art --
127
+ * jede Datei bekommt einen Rat.
128
+ *
129
+ * ⚠️ **Eine ART, ein Satz -- auch wenn drei ihrer Dateien betroffen sind.** Das
130
+ * war vorher so (ein `if` feuerte einmal) und bleibt so: Der Rat spricht ueber
131
+ * den Weg, nicht ueber die einzelne Datei; dreimal derselbe Absatz waere
132
+ * Laerm.
133
+ */
134
+ export function ratNachUnberuehrt(unberuehrt, rolle, adminName, ordner, sprache) {
135
+ const t = KONTEXT_TEXTE[sprache];
136
+ // Ruling T7-1: kein `null` im Klartext, sondern der Sammelbegriff.
137
+ const wer = adminName ? t.toPerson(adminName) : t.toCompanyAdmin;
138
+ // Befehlswoerter in der Sprache des Satzes (befehlIn), nicht in der des Ordners.
139
+ const befehle = { hoch: befehlIn("kontext hoch", sprache), runter: befehlIn("kontext runter", sprache) }; // kein-anzeigetext: Befehlsschlüssel
140
+ const betroffeneArten = new Set();
141
+ for (const pfad of verwalteteDateien(ordner)) {
142
+ if (unberuehrt.includes(pfad))
143
+ betroffeneArten.add(ratArt(pfad));
144
+ }
145
+ // 🔑 Die Reihenfolge kommt aus RAT_REIHENFOLGE, NICHT aus der Einfuegereihen-
146
+ // folge des `Set` -- sonst haenge die Ausgabe beim Kunden davon ab, in
147
+ // welcher Reihenfolge verwalteteDateien(ordner) zufaellig steht.
148
+ // Die PFADE im Rat heissen so, wie sie in DIESEM Ordner heissen (L-19) -- die SAETZE folgen dem Menschen.
149
+ const namen = { kontext: ORDNER_NAMEN[ordner].kontext, kurzprofil: firmenTexte(ordner)[0].pfad };
150
+ return RAT_REIHENFOLGE.filter((art) => betroffeneArten.has(art)).map((art) => RAT_TEXT[art](rolle, wer, namen, t, befehle));
151
+ }