kimal3 0.2.9 → 0.3.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.
Files changed (67) hide show
  1. package/README.md +135 -0
  2. package/dist/api.js +16 -7
  3. package/dist/arbeitsplatz-dateien.js +62 -30
  4. package/dist/argumente.js +25 -97
  5. package/dist/befehle/arbeitsplatz.js +51 -68
  6. package/dist/befehle/firma-daten.js +24 -26
  7. package/dist/befehle/kontext-fragen.js +62 -44
  8. package/dist/befehle/kontext-gemeinsam.js +86 -43
  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 +118 -88
  19. package/dist/befehle-katalog-en.js +185 -0
  20. package/dist/befehle-katalog.js +10 -49
  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 +42 -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 +68 -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/wache-schalter.js +40 -5
  49. package/dist/werkbank-name.js +28 -10
  50. package/dist/wort.js +98 -0
  51. package/package.json +27 -27
  52. package/skill/SKILL.md +18 -10
  53. package/skill/commands/k3.md +1 -1
  54. package/skill/commands/k3feierabend.md +99 -97
  55. package/skill/commands/k3kontext.md +2 -2
  56. package/skill/commands/k3kurs.md +6 -5
  57. package/skill/commands/k3start.md +2 -1
  58. package/skill/en/SKILL.md +451 -0
  59. package/skill/en/commands/k3.md +106 -0
  60. package/skill/en/commands/k3analyse.md +59 -0
  61. package/skill/en/commands/k3context.md +92 -0
  62. package/skill/en/commands/k3course.md +54 -0
  63. package/skill/en/commands/k3list.md +26 -0
  64. package/skill/en/commands/k3project.md +50 -0
  65. package/skill/en/commands/k3start.md +47 -0
  66. package/skill/en/commands/k3wrapup.md +99 -0
  67. package/dist/zz-kaputt.js +0 -1
@@ -48,20 +48,20 @@ import { createHash } from "node:crypto";
48
48
  * Menschen mitgeht (sie steht in der Positiv-Liste der `.gitignore`).
49
49
  * Deshalb ist sie das Erkennungszeichen in `firmennameAusClaudeMd`.
50
50
  *
51
- * ⚠️ Sie ist zugleich der erste Eintrag von `VERWALTETE_DATEIEN` darunter --
51
+ * ⚠️ Sie ist zugleich der erste Eintrag von `verwalteteDateien(sprache)` darunter --
52
52
  * dass beide dieselbe Datei meinen, prueft `check:cli`. Dort haengt auch die
53
53
  * Pruefung, dass die ECHTE Vorlage ihre Ueberschrift noch so schreibt, wie
54
54
  * `firmennameAusClaudeMd` sie liest.
55
55
  */
56
- export const FIRMEN_CLAUDE_MD = "CLAUDE.md";
57
- export const VERWALTETE_DATEIEN = [
58
- "CLAUDE.md",
59
- "kontext/kurzprofil.md",
60
- "kontext/firma.md",
61
- "kontext/marke.md",
62
- "kontext/systeme.md",
63
- "kontext/sicherheit.md",
64
- ];
56
+ export const FIRMEN_CLAUDE_MD = "CLAUDE.md"; // kein-anzeigetext: Dateiname
57
+ export const ORDNER_NAMEN = {
58
+ de: { wurzel: "KI-Arbeitsplatz", kontext: "kontext", kopf: "KI-Arbeitsplatz", sicherung: "vorher" }, // kein-anzeigetext: Ordnername (Wortliste Teil 2)
59
+ en: { wurzel: "AI-Workplace", kontext: "context", kopf: "AI Workplace", sicherung: "before" }, // kein-anzeigetext: Ordnername (Wortliste Teil 2)
60
+ };
61
+ const DATEINAMEN = {
62
+ de: { drei_saetze: "kurzprofil.md", firma_md: "firma.md", marke_md: "marke.md", systeme_md: "systeme.md", sicherheit_md: "sicherheit.md" },
63
+ en: { drei_saetze: "profile.md", firma_md: "company.md", marke_md: "brand.md", systeme_md: "systems.md", sicherheit_md: "security.md" },
64
+ };
65
65
  /**
66
66
  * Die FUENF Texte, die eine Firma selbst pflegt -- welche Datei welches Feld
67
67
  * der App fuellt, wie sie dort heisst, und woran man sie wiedererkennt.
@@ -75,12 +75,14 @@ export const VERWALTETE_DATEIEN = [
75
75
  *
76
76
  * 🔴 **Sie ist NICHT der Fragenkatalog.** Hier stehen fuenf Dateinamen, fuenf
77
77
  * Feldnamen, fuenf Anzeigenamen und fuenf Praefixe -- kein einziger Fragetext.
78
- * Die 17 Fragen wohnen genau einmal, in `arbeitsplatz/vorlage/`, und erreichen
78
+ * Die Fragen wohnen genau einmal, in der Vorlage (`arbeitsplatz/vorlage/`), und erreichen
79
79
  * die CLI ueber die ausgelieferte Datei selbst (s. `fragen-lesen.ts`).
80
80
  *
81
- * ⚠️ **`titel` ist ANZEIGETEXT und traegt deshalb echte Umlaute** -- ein Mensch
82
- * liest ihn im Abbruch von `kontext hoch` („Diese Datei sieht nicht aus wie
83
- * …"). Die ASCII-Umschrift dieser Datei gilt nur fuer Kommentare.
81
+ * ⚠️ **`titel` ist hier nur noch der deutsche ABGLEICHWERT** (`kontext-felder.ts`, `check:kontext`
82
+ * halten ihn gegen die App). Was ein Mensch im Terminal liest -- im Abbruch von `kontext hoch`
83
+ * („Diese Datei sieht nicht aus wie …") und hinter jedem Pfad in `kontext fragen` --, steht seit
84
+ * Teil 3 in `KONTEXT_TEXTE[sprache].dateiTitel` (`texte/kontext.ts`), in der Sprache des Menschen
85
+ * (W-7). Diese Datei darf nichts importieren, deshalb kam der Satz nicht hierher.
84
86
  *
85
87
  * 🔴 **`ankerPraefix` ist keine Verabredung, sondern eine gemessene Zusage.**
86
88
  * `check:kontext` haelt jeden Anker aus `FRAGEN` (der App) gegen den Praefix
@@ -92,14 +94,27 @@ export const VERWALTETE_DATEIEN = [
92
94
  * Zuordnungen.** Die App darf diese Datei lesen, die CLI aber nicht die
93
95
  * App -- deshalb steht die Tabelle hier und die Uebereinstimmung ist eine
94
96
  * Pruefung in `check:kontext`, keine Hoffnung.
97
+ *
98
+ * 🌐 **Teil 2:** Der `pfad` haengt an der Firmensprache (`kontext/firma.md` oder
99
+ * `context/company.md`); Feld, Titel und Anker sind in beiden Sprachen gleich.
100
+ * Reihenfolge = die von heute (kurzprofil, firma, marke, systeme, sicherheit) --
101
+ * Merkzettel und Terminal-Ausgaben bleiben damit fuer deutsche Ordner Zeichen fuer
102
+ * Zeichen gleich. `titel` bleibt deutsch und ist nur der Abgleichwert (s. o.).
95
103
  */
96
- export const FIRMEN_TEXTE = [
97
- { pfad: "kontext/kurzprofil.md", feld: "drei_saetze", titel: "Die Firma in drei Sätzen", ankerPraefix: "kurz_" },
98
- { pfad: "kontext/firma.md", feld: "firma_md", titel: "Die Firma", ankerPraefix: "firma_" },
99
- { pfad: "kontext/marke.md", feld: "marke_md", titel: "Die Marke", ankerPraefix: "marke_" },
100
- { pfad: "kontext/systeme.md", feld: "systeme_md", titel: "Die Systeme", ankerPraefix: "systeme_" },
101
- { pfad: "kontext/sicherheit.md", feld: "sicherheit_md", titel: "Sicherheit", ankerPraefix: "sicher_" },
104
+ const TEXTE = [
105
+ { feld: "drei_saetze", titel: "Die Firma in drei Sätzen", ankerPraefix: "kurz_" }, // kein-anzeigetext: Abgleichwert, Anzeige über KONTEXT_TEXTE.dateiTitel (W-7)
106
+ { feld: "firma_md", titel: "Die Firma", ankerPraefix: "firma_" }, // kein-anzeigetext: Abgleichwert, Anzeige über KONTEXT_TEXTE.dateiTitel (W-7)
107
+ { feld: "marke_md", titel: "Die Marke", ankerPraefix: "marke_" }, // kein-anzeigetext: Abgleichwert, Anzeige über KONTEXT_TEXTE.dateiTitel (W-7)
108
+ { feld: "systeme_md", titel: "Die Systeme", ankerPraefix: "systeme_" }, // kein-anzeigetext: Abgleichwert, Anzeige über KONTEXT_TEXTE.dateiTitel (W-7)
109
+ { feld: "sicherheit_md", titel: "Sicherheit", ankerPraefix: "sicher_" }, // kein-anzeigetext: Abgleichwert, Anzeige über KONTEXT_TEXTE.dateiTitel (W-7)
102
110
  ];
111
+ export function firmenTexte(sprache) {
112
+ return TEXTE.map((t) => ({ ...t, pfad: `${ORDNER_NAMEN[sprache].kontext}/${DATEINAMEN[sprache][t.feld]}` }));
113
+ }
114
+ /** Alle Dateien, die der Merkzettel fuehrt: die Firmen-`CLAUDE.md` zuerst, dann die fuenf Texte. */
115
+ export function verwalteteDateien(sprache) {
116
+ return [FIRMEN_CLAUDE_MD, ...firmenTexte(sprache).map((t) => t.pfad)];
117
+ }
103
118
  /**
104
119
  * Der Merkzettel liegt im WURZELVERZEICHNIS des Arbeitsplatzes, eine Ebene
105
120
  * ueber `kontext/`.
@@ -110,8 +125,6 @@ export const FIRMEN_TEXTE = [
110
125
  * stiller Falschaussage, die niemand bemerkt, bis sie schadet.
111
126
  */
112
127
  export const MERKZETTEL_PFAD = ".kimal3-stand.json";
113
- /** Wo die Sicherungen hinkommen. Siehe die Frage am Kopf des Bauplans. */
114
- export const SICHERUNG_BASIS = "kontext";
115
128
  /**
116
129
  * Der Stand einer Firma, die nichts gepflegt hat.
117
130
  *
@@ -153,10 +166,16 @@ const LEERER_MERKZETTEL = { firma_id: null, dateien: {} };
153
166
  * genau dann da, wenn der Server die Firma nicht kannte -- also im
154
167
  * Vordruck-Fall, in dem es nichts zu vergleichen gibt.
155
168
  */
169
+ // Teil 2: beide Koepfe aus ORDNER_NAMEN (`KI-Arbeitsplatz`, `AI Workplace`), maskiert fuer den Ausdruck.
170
+ const KOPF_MUSTER = new RegExp(
171
+ // kein-anzeigetext: Suchmuster für die Überschrift der Firmen-CLAUDE.md, kein Satz
172
+ `^#\\s*CLAUDE\\.md\\s*—\\s*(.+?)\\s*·\\s*(?:${Object.values(ORDNER_NAMEN)
173
+ .map((n) => n.kopf.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))
174
+ .join("|")})\\s*$`, "m");
156
175
  export function firmennameAusClaudeMd(inhalt) {
157
176
  if (inhalt === null)
158
177
  return null;
159
- const treffer = /^#\s*CLAUDE\.md\s*—\s*(.+?)\s*·\s*KI-Arbeitsplatz\s*$/m.exec(inhalt);
178
+ const treffer = KOPF_MUSTER.exec(inhalt);
160
179
  if (treffer === null)
161
180
  return null;
162
181
  const name = treffer[1].trim();
@@ -390,9 +409,9 @@ export function alsMerkzettel(wert) {
390
409
  * die Datei ohne jeden Import ausser `node:crypto`, ist der einzige Weg, der
391
410
  * `api.ts` nicht in einen Kreisimport mit sich selbst zwingt.
392
411
  */
393
- export function bekannterStandAus(merkzettel) {
412
+ export function bekannterStandAus(merkzettel, sprache) {
394
413
  const staende = new Set();
395
- for (const pfad of VERWALTETE_DATEIEN) {
414
+ for (const pfad of verwalteteDateien(sprache)) {
396
415
  const stand = merkzettel.dateien[pfad]?.stand;
397
416
  if (stand !== undefined && stand !== LEER)
398
417
  staende.add(stand);
@@ -442,9 +461,9 @@ export function bekannterStandAus(merkzettel) {
442
461
  * (`leseMerkzettel` oben). Jede Lieferung, die durch diese Funktion entsteht,
443
462
  * ist per Definition die aktuelle Fassung.
444
463
  */
445
- export function baueMerkzettel(dateien, staende, firmenId, vorlagenPruefsummen) {
464
+ export function baueMerkzettel(dateien, staende, firmenId, vorlagenPruefsummen, sprache) {
446
465
  const raus = {};
447
- for (const pfad of VERWALTETE_DATEIEN) {
466
+ for (const pfad of verwalteteDateien(sprache)) {
448
467
  const datei = dateien.find((d) => d.pfad === pfad);
449
468
  if (!datei)
450
469
  continue;
@@ -466,6 +485,10 @@ export function baueMerkzettel(dateien, staende, firmenId, vorlagenPruefsummen)
466
485
  * schreibt keine einzige Datei. Fuer die fuenf gesendeten Texte geht das
467
486
  * trotzdem auf: Was der Server zurueckgibt, entsteht aus dem, was die CLI ihm
468
487
  * gerade geschickt hat, und dasselbe steht auf der Platte.
488
+ * ⚠️ Seit dem 25.09. (Luecke L12) gilt das nur noch, weil der SERVER es
489
+ * sicherstellt: Er liefert einen um neue Fragen ergaenzten Text aus, traegt im
490
+ * Merkzettel aber die Pruefsumme des GESENDETEN ein -- `merkzettelFuerHochAntwort`
491
+ * in `src/lib/cli/merkzettel-hoch.ts`. Wer dort etwas aendert, bricht diese Zeile.
469
492
  *
470
493
  * ⚠️ **Fuer die Firmen-`CLAUDE.md` geht es NICHT auf, seit Ruling K-6.** Sie
471
494
  * wird nicht mehr gesendet, aber der Server BAUT sie neu (er setzt die frischen
@@ -495,9 +518,9 @@ export function baueMerkzettel(dateien, staende, firmenId, vorlagenPruefsummen)
495
518
  * 🔑 `format` und `firma_id` kommen weiterhin vom SERVER: Beide sagen nichts
496
519
  * ueber den Inhalt einer Datei, und bei der Firma ist der Server die Wahrheit.
497
520
  */
498
- export function merkzettelNachHoch(vomServer, lokal, gesendet) {
521
+ export function merkzettelNachHoch(vomServer, lokal, gesendet, sprache) {
499
522
  const dateien = {};
500
- for (const pfad of VERWALTETE_DATEIEN) {
523
+ for (const pfad of verwalteteDateien(sprache)) {
501
524
  const eintrag = gesendet.includes(pfad) ? vomServer.dateien[pfad] : lokal.dateien[pfad];
502
525
  if (eintrag)
503
526
  dateien[pfad] = eintrag;
@@ -644,9 +667,9 @@ export function lageAusMerkzetteln(a) {
644
667
  aktuelleVorlage: vomServer?.vorlage ?? LEER,
645
668
  });
646
669
  }
647
- /** `kontext/vorher-2026-08-21` */
648
- export function sicherungsOrdner(datum) {
649
- return `${SICHERUNG_BASIS}/vorher-${datum}`;
670
+ /** `kontext/vorher-2026-08-21` (deutsch) oder `context/before-2026-08-21` (englisch). */
671
+ export function sicherungsOrdner(sprache, datum) {
672
+ return `${ORDNER_NAMEN[sprache].kontext}/${ORDNER_NAMEN[sprache].sicherung}-${datum}`;
650
673
  }
651
674
  /**
652
675
  * Wohin die lokale Fassung gesichert wird.
@@ -657,43 +680,23 @@ export function sicherungsOrdner(datum) {
657
680
  * Basisnamen haben. Eine kuenftige sechste faellt damit auf, bevor sie eine
658
681
  * Sicherung ueberschreibt.
659
682
  */
660
- export function sicherungsZiel(datum, pfad) {
683
+ export function sicherungsZiel(sprache, datum, pfad) {
661
684
  const name = pfad.split("/").pop() ?? pfad;
662
- return `${sicherungsOrdner(datum)}/${name}`;
685
+ return `${sicherungsOrdner(sprache, datum)}/${name}`;
663
686
  }
664
- /**
665
- * Von welchem Ordner aus `kimal3 kontext runter` ueberhaupt wissen kann,
666
- * welche Firma gemeint ist -- **ohne** das Dateisystem selbst anzufassen.
667
- *
668
- * 🔑 WARUM DIESE FUNKTION HIER UND NICHT ERST IN EINER SPAETEREN AUFGABE
669
- * WOHNT: Diese Datei importiert bewusst nur `node:crypto` (s. Kopf). Die
670
- * Fundregel selbst braucht kein Dateisystem -- nur eine Liste von Kandidaten,
671
- * die der Aufrufer (spaeter) mit `readdirSync`/`existsSync` gefuellt hat.
672
- * Die eigentliche Suche gehoert deshalb woanders hin (zieht Umgebung und
673
- * Heimverzeichnis hoch); ein Waechter fuer eine reine Pfadregel soll das
674
- * nicht mit auslösen.
675
- *
676
- * Fundregel: Vom aktuellen Verzeichnis aufwaerts suchen, bis ein Ordner
677
- * gefunden ist, der `kontext/` UND `CLAUDE.md` enthaelt.
678
- *
679
- * 🔴 DER ERSTE TREFFER GEWINNT, NICHT DER OBERSTE. Die Kandidaten kommen von
680
- * UNTEN NACH OBEN (aktuelles Verzeichnis zuerst) -- wer in
681
- * `Kunden/ARNO/KI-Arbeitsplatz` steht, meint ARNO, nicht `Kunden/`. Wuerde
682
- * stattdessen der oberste Treffer gewinnen, faende der Befehl bei jedem
683
- * verschachtelten Arbeitsplatz den falschen.
684
- *
685
- * 🔑 BEIDES MUSS STIMMEN. Ein Ordner mit `kontext/`, aber ohne `CLAUDE.md`,
686
- * ist kein Arbeitsplatz -- sonst traefe der Befehl irgendeinen Ordner, in dem
687
- * jemand zufaellig etwas "kontext" genannt hat.
688
- *
689
- * Kein Treffer -> `null`.
690
- */
691
687
  export function arbeitsplatzAus(kandidaten) {
688
+ const traegtTexte = (namen, sprache) => Object.values(DATEINAMEN[sprache]).some((n) => namen.includes(n));
692
689
  for (const k of kandidaten) {
693
- if (k.hatKontext && k.hatClaude)
694
- return k.pfad;
690
+ const de = traegtTexte(k.kontextNamen, "de");
691
+ const en = traegtTexte(k.contextNamen, "en");
692
+ if (!k.hatClaude || (!de && !en))
693
+ continue;
694
+ // Spec 3.2: beide Namen nebeneinander ist ein Fehler, den das Programm MELDET, statt still einen zu waehlen.
695
+ if (de && en)
696
+ return { art: "beide", pfad: k.pfad };
697
+ return { art: "gefunden", pfad: k.pfad, sprache: en ? "en" : "de" };
695
698
  }
696
- return null;
699
+ return { art: "keiner" };
697
700
  }
698
701
  /*
699
702
  🪦 **Hier wohnten `DREI_SAETZE_UEBERSCHRIFT`, der Typ `DreiSaetzeSchnitt` und
@@ -714,7 +717,7 @@ export function arbeitsplatzAus(kandidaten) {
714
717
  Fragengeruest gezeigt -- mit den Saetzen der Firma unsichtbar dahinter.
715
718
 
716
719
  🔑 **Was ihre Aufgabe uebernommen hat:** `kontext/kurzprofil.md` ist seither
717
- eine Datei wie die vier anderen. Sie steht in {@link FIRMEN_TEXTE} und wird
720
+ eine Datei wie die vier anderen. Sie steht in {@link firmenTexte} und wird
718
721
  gelesen, hochgeschickt und nachgezogen wie `marke.md` auch. *Fuenf
719
722
  gleichrangige Dateien, kein Sonderfall, keine Extra-Meldung.*
720
723
 
@@ -723,148 +726,3 @@ export function arbeitsplatzAus(kandidaten) {
723
726
  Antworten an der Stelle von `EINSETZ_MARKE_DREI_SAETZE` ein, und dass DIESE
724
727
  Marke woertlich in der Vorlage steht, prueft `check:arbeitsplatz`.
725
728
  */
726
- /**
727
- * Die ARTEN von Rat, die es gibt -- **in der Reihenfolge, in der sie im
728
- * Terminal stehen.**
729
- *
730
- * 🔑 Diese Liste ist zugleich die Quelle des Typs {@link RatArt}. Wer eine
731
- * dritte Art erfindet, muss sie hier eintragen -- und bekommt danach vom
732
- * Compiler gesagt, dass {@link RAT_TEXT} noch keinen Text dafuer hat. *Eine
733
- * Reihenfolge, die nebenher den Typ traegt, kann nicht unvollstaendig sein.*
734
- *
735
- * ⚠️ Die Reihenfolge ist nicht Geschmack: `kimal3 kontext runter` druckt seit
736
- * Befund F-3 erst den Rat fuer die Firmen-Texte und darunter den fuer unsere
737
- * Vorlage. Wer sie dreht, dreht die Ausgabe beim Kunden.
738
- */
739
- const RAT_REIHENFOLGE = ["hochschickbar", "nicht-hochschickbar"];
740
- /**
741
- * 🔴 **PUNKT A-7 (2026-08-26): DIESE TABELLE IST DIE ABSICHERUNG, NICHT NUR
742
- * EINE AUFRAEUMAKTION.**
743
- *
744
- * Bis hierher zaehlte {@link ratNachUnberuehrt} ihre Faelle auf: ein `if` fuer
745
- * „irgendetwas aus {@link FIRMEN_TEXTE}" und ein zweites fuer den woertlichen
746
- * Dateinamen `"CLAUDE.md"`. {@link VERWALTETE_DATEIEN} hat aber SECHS
747
- * Eintraege, und eine kuenftige SIEBTE waere durch beide `if`s gefallen --
748
- * ohne Fehler, ohne Meldung, ohne Rat. *Der Mensch haette gelesen, dass seine
749
- * Datei in Ruhe gelassen wurde, und nirgends, was er jetzt tun soll.*
750
- *
751
- * 🔑 **Warum ein `Record` und keine zweite Liste:** Der Schluesseltyp ist die
752
- * Vereinigung aller Pfade aus {@link VERWALTETE_DATEIEN}. Eine siebte Datei
753
- * dort weitet ihn -- und dieses Objekt hat dann einen Schluessel zu wenig,
754
- * *also uebersetzt die CLI nicht mehr*. Das ist die einzige Absicherung, die
755
- * auch den erwischt, der diesen Kommentar nie liest.
756
- *
757
- * ⚠️ **Warum die Zuordnung nicht IN {@link VERWALTETE_DATEIEN} steht**, wo sie
758
- * inhaltlich hingehoerte: Diese Liste ist an sechs Stellen ausserhalb dieser
759
- * Datei eine reine Pfadliste (`ausliefern.ts`, `route.ts`,
760
- * `kontext-runter.ts`, `kontext-dateien.ts`, zwei Waechter). Aus ihr eine
761
- * Liste von Objekten zu machen, haette all das angefasst -- fuer denselben
762
- * Schutz, den der `Record` hier ohne einen einzigen fremden Eingriff gibt.
763
- *
764
- * ⚠️ **Sie sagt dasselbe wie {@link FIRMEN_TEXTE}, und das ist eine
765
- * Doppelung** -- gehalten wird sie von einer Pruefung in `check:kontext`
766
- * („der `hoch`-Rat geht an genau die Dateien, die `kontext hoch` wirklich
767
- * traegt"). Die konnte vor diesem Umbau gar nicht rot werden, weil der Rat
768
- * SELBST aus {@link FIRMEN_TEXTE} entstand; jetzt sind es zwei Tabellen, und
769
- * ihr Auseinanderlaufen ist messbar.
770
- */
771
- const RAT_ART_JE_DATEI = {
772
- "CLAUDE.md": "nicht-hochschickbar",
773
- "kontext/kurzprofil.md": "hochschickbar",
774
- "kontext/firma.md": "hochschickbar",
775
- "kontext/marke.md": "hochschickbar",
776
- "kontext/systeme.md": "hochschickbar",
777
- "kontext/sicherheit.md": "hochschickbar",
778
- };
779
- /**
780
- * Der Wortlaut je Art und Rolle.
781
- *
782
- * ⚠️ ECHTE UMLAUTE -- das liest ein Kunde. Die ASCII-Umschrift dieser Datei
783
- * gilt nur fuer Kommentare.
784
- *
785
- * 🔑 `wer` ist die fertige Anrede (` an Lena` oder ` an euren Firmen-Admin`),
786
- * damit der Rueckfall auf den Sammelbegriff an EINER Stelle steht und nicht in
787
- * jedem Zweig neu. Ruling T7-1: nie `null` im Klartext.
788
- */
789
- const RAT_TEXT = {
790
- hochschickbar: (rolle, wer) => rolle === "admin"
791
- ? " Deine Fassung ist die neuere? Dann schick sie hoch: kimal3 kontext hoch"
792
- : " Deine Fassung bleibt stehen — hochschicken darf nur der Firmen-Admin.\n" +
793
- ` Gib deine Ergänzung${wer} weiter; von dort geht sie in der App unter\n` +
794
- " Firma → Kontext oder mit `kimal3 kontext hoch` an alle.",
795
- /*
796
- 🔴 **Auch dieser Zweig kennt die ROLLE -- Punkt A-5, 2026-08-26.** Er tat
797
- es bis dahin nicht und riet jedem dasselbe: „Aendere sie dort", also im
798
- Formular unter Firma → Kontext. Ein MITGLIED kann das nicht. `kimal3
799
- kontext hoch` weist es ab, und den Firmenkontext der Firma darf es in der
800
- App nicht schreiben. *Ein Rat, der die Lage nicht aendern kann, ist keiner*
801
- -- er schickt einen Menschen an eine Tuer, vor der er keinen Schluessel
802
- hat, und laesst ihn den Fehler bei sich suchen.
803
-
804
- 🔑 Was das Mitglied stattdessen liest: WAS es weitergeben soll (die drei
805
- Saetze aus `kontext/kurzprofil.md`) und AN WEN -- genau die Form, die der
806
- Zweig fuer die fuenf `kontext/`-Dateien oben schon hat.
807
- */
808
- "nicht-hochschickbar": (rolle, wer) => rolle === "admin"
809
- ? " Die `CLAUDE.md` kann `kimal3 kontext hoch` nicht mitnehmen — der Befehl schickt nur\n" +
810
- " die fünf Dateien aus `kontext/`. Die drei Sätze in dieser Datei kommen aus\n" +
811
- " `kontext/kurzprofil.md`, der Firmenname aus der App: Ändere sie dort, dann holt\n" +
812
- " der nächste `kimal3 kontext runter` sie hierher.\n" +
813
- " Alles Übrige darin ist unsere Vorlage — dafür gibt es keinen Weg zurück."
814
- : " Die `CLAUDE.md` kann `kimal3 kontext hoch` nicht mitnehmen — der Befehl schickt nur\n" +
815
- " die fünf Dateien aus `kontext/`. Deine Änderung bleibt stehen.\n" +
816
- " Die drei Sätze in dieser Datei kommen aus `kontext/kurzprofil.md`, und die ändert\n" +
817
- ` nur der Firmen-Admin. Gib deinen Wunsch${wer} weiter; von dort geht\n` +
818
- " `kontext/kurzprofil.md` in der App unter Firma → Kontext oder mit\n" +
819
- " `kimal3 kontext hoch` an alle.\n" +
820
- " Alles Übrige darin ist unsere Vorlage — dafür gibt es keinen Weg zurück.",
821
- };
822
- /**
823
- * Was ein Mensch lesen soll, wenn `kimal3 kontext runter` Dateien in Ruhe
824
- * laesst, weil er sie lokal geaendert hat -- je nach Rolle und je nach DATEI.
825
- *
826
- * 🔴 **DAS IST FUND F, ZUM ZWEITEN MAL -- und diesmal die echte Fassung
827
- * (Review-Befund F-3, 2026-08-25).** Der Rat lautete fuer die ganze Liste auf
828
- * einmal: „Deine Fassung ist die neuere? Dann schick sie hoch: kimal3 kontext
829
- * hoch". Fuer `CLAUDE.md` ist dieser Satz eine Sackgasse: Sie steht in
830
- * {@link VERWALTETE_DATEIEN} und landet deshalb in der Liste -- aber
831
- * `kontext hoch` liest ausschliesslich die fuenf Dateien aus
832
- * {@link FIRMEN_TEXTE} und meldet danach Erfolg, ohne sie. *Der Mensch tippt
833
- * den geratenen Befehl, sieht einen Haken, und seine Aenderung steht weiter
834
- * allein auf seiner Platte.*
835
- *
836
- * 🔑 Die ausgelieferte `CLAUDE.md` laedt diese Bearbeitung foermlich ein: Sie
837
- * sagt selbst, aendern muesse man sie nur, wenn sich die Firma aendert.
838
- *
839
- * 🔴 **Der Rat wohnt hier und nicht im Befehl, damit ihn eine Wache sehen
840
- * kann.** Die spurious Fassung desselben Fehlers (`merkzettelNachHoch`) war
841
- * behoben, die echte lag vier Runden lang offen -- *ein Satz ueber das
842
- * Verhalten eines Befehls wohnte im Befehl selbst, wo keine Vorlagen-Wache je
843
- * hinsieht.* `check:kontext` prueft jetzt genau diese Zuordnung.
844
- *
845
- * 🔴 **PUNKT A-7 (2026-08-26): DER RAT WIRD ERZEUGT, NICHT AUFGEZAEHLT.** Diese
846
- * Funktion hatte zwei `if`s -- eines fuer „irgendetwas aus
847
- * {@link FIRMEN_TEXTE}", eines fuer den woertlichen Namen `"CLAUDE.md"`. Eine
848
- * siebte verwaltete Datei waere durch beide gefallen und haette schweigend gar
849
- * keinen Rat bekommen. Jetzt laeuft die Schleife ueber
850
- * {@link VERWALTETE_DATEIEN} selbst und schlaegt die Art in
851
- * {@link RAT_ART_JE_DATEI} nach -- eine Datei ohne Zuordnung uebersetzt nicht.
852
- *
853
- * ⚠️ **Eine ART, ein Satz -- auch wenn drei ihrer Dateien betroffen sind.** Das
854
- * war vorher so (ein `if` feuerte einmal) und bleibt so: Der Rat spricht ueber
855
- * den Weg, nicht ueber die einzelne Datei; dreimal derselbe Absatz waere
856
- * Laerm.
857
- */
858
- export function ratNachUnberuehrt(unberuehrt, rolle, adminName) {
859
- // Ruling T7-1: kein `null` im Klartext, sondern der Sammelbegriff.
860
- const wer = adminName ? ` an ${adminName}` : " an euren Firmen-Admin";
861
- const betroffeneArten = new Set();
862
- for (const pfad of VERWALTETE_DATEIEN) {
863
- if (unberuehrt.includes(pfad))
864
- betroffeneArten.add(RAT_ART_JE_DATEI[pfad]);
865
- }
866
- // 🔑 Die Reihenfolge kommt aus RAT_REIHENFOLGE, NICHT aus der Einfuegereihen-
867
- // folge des `Set` -- sonst haenge die Ausgabe beim Kunden davon ab, in
868
- // welcher Reihenfolge VERWALTETE_DATEIEN zufaellig steht.
869
- return RAT_REIHENFOLGE.filter((art) => betroffeneArten.has(art)).map((art) => RAT_TEXT[art](rolle, wer));
870
- }
package/dist/nachricht.js CHANGED
@@ -8,7 +8,7 @@
8
8
  * Die Raender (Datei, Netz, Uhr) wohnen in gesehen.ts, api.ts und kimal3.ts.
9
9
  */
10
10
  /** Der Stempel, den `kimal3 skill install` ans Ende der SKILL.md schreibt. */
11
- export const STEMPEL_MARKE = "kimal3:erzeugt version=";
11
+ export const STEMPEL_MARKE = "kimal3:erzeugt version="; // kein-anzeigetext: Stempel in der ausgelieferten Datei
12
12
  /**
13
13
  * Zieht den Hinweis aus einer Server-Antwort -- oder gibt `null` zurueck.
14
14
  *
@@ -119,7 +119,7 @@ export function heuteAus(d) {
119
119
  * steht der Server-Stand, den er meint (`kontext-2026-08-26T09:00:00Z`).
120
120
  *
121
121
  * 🔑 Sie steht als eigener Name da, weil sie von ZWEI Seiten gebraucht wird:
122
- * `firmenHinweisAus` baut die Kennung, `hinweisErledigt` liest sie wieder
122
+ * `kontextHinweisAus` baut die Kennung, `hinweisErledigt` liest sie wieder
123
123
  * auseinander. Zwei getippte `"kontext-"` waeren zwei Wahrheiten -- und die
124
124
  * zweite verrottete lautlos, weil ein Hinweis, der nicht mehr erkannt wird,
125
125
  * einfach wieder erscheint, statt einen Fehler zu machen.
@@ -251,34 +251,58 @@ export function stempelAus(inhalt) {
251
251
  const treffer = [...inhalt.matchAll(/kimal3:erzeugt version=([0-9][0-9.]*)/g)];
252
252
  return treffer.length > 0 ? treffer[treffer.length - 1][1] : null;
253
253
  }
254
+ /** Die Stempelzeile -- Deutsch Zeichen fuer Zeichen wie vor Teil 4 (T4-4), Englisch mit Zusatz. */
255
+ export function stempelZeile(version, sprache) {
256
+ return sprache === "en"
257
+ ? `<!-- ${STEMPEL_MARKE}${version} sprache=en -->` // kein-anzeigetext: Stempel in der ausgelieferten Datei
258
+ : `<!-- ${STEMPEL_MARKE}${version} -->`; // kein-anzeigetext: Stempel in der ausgelieferten Datei
259
+ }
260
+ /**
261
+ * Die Sprache des LETZTEN Stempels (wie `stempelAus`: ein Beispiel im Text darf nicht gewinnen).
262
+ * Ohne Zusatz -- und bei jedem anderen Zusatz als genau `en` -- Deutsch (T4-4).
263
+ */
264
+ export function stempelSpracheAus(inhalt) {
265
+ const treffer = [...inhalt.matchAll(/kimal3:erzeugt version=[0-9][0-9.]*(?: sprache=([a-z]+))?/g)];
266
+ return treffer.length > 0 && treffer[treffer.length - 1][1] === "en" ? "en" : "de";
267
+ }
254
268
  /**
255
269
  * Die DREI Zustaende aus US-4/US-5 -- und sie sind ausdruecklich drei, nicht
256
270
  * zwei:
257
271
  *
258
- * | Was liegt dort | Antwort |
259
- * |-----------------------|------------|
260
- * | Datei fehlt | still | <- nie installiert
261
- * | Datei ohne Stempel | veraltet | <- 0.1.4 und aelter
262
- * | Stempel aelter | veraltet |
263
- * | Stempel gleich/neuer | still | <- niemand wird rueckwaerts gedraengt
272
+ * | Was liegt dort | Antwort |
273
+ * |-------------------------------------|------------|
274
+ * | Datei fehlt | still | <- nie installiert
275
+ * | Datei ohne Stempel | veraltet | <- 0.1.4 und aelter
276
+ * | Stempel-Sprache ≠ Lauf-Sprache | veraltet | <- Teil 4, T4-3
277
+ * | Stempel aelter | veraltet |
278
+ * | Stempel gleich/neuer | still | <- niemand wird rueckwaerts gedraengt
264
279
  *
265
280
  * ⚠️ Die ersten beiden sehen im Dateisystem fast gleich aus und bedeuten das
266
281
  * Gegenteil. `null` heisst hier "es gibt keine Datei", NICHT "leer".
267
282
  */
268
- export function skillZustand(inhalt, eigeneVersion) {
283
+ export function skillZustand(inhalt, eigeneVersion, sprache) {
269
284
  if (inhalt === null)
270
285
  return "still";
271
286
  const stempel = stempelAus(inhalt);
272
287
  if (stempel === null)
273
288
  return "veraltet";
289
+ if (stempelSpracheAus(inhalt) !== sprache)
290
+ return "veraltet";
274
291
  return istAelter(stempel, eigeneVersion) ? "veraltet" : "still";
275
292
  }
276
293
  /**
277
- * ⚠️ Sie nennt den Befehl, sie FUEHRT IHN NICHT AUS. Ein Befehl darf nicht
278
- * mehr tun, als sein Name verspricht -- die Regel steht schon in kimal3.ts.
294
+ * Muss `kimal3 login` Claudes Anleitung umstellen? (Gesamt-Review Teil 4, Important 1)
295
+ *
296
+ * 🔑 NUR die Sprache entscheidet, nicht die Fassung: Auf `/pro` laeuft `skill install` VOR dem Login --
297
+ * ohne Merkzettel in der Sprache des Betriebssystems. Nach dem Login kennt das Programm die Kontosprache;
298
+ * weicht der Stempel ab, stellt der Login um. Eine aeltere Fassung in der RICHTIGEN Sprache meldet weiter
299
+ * die taegliche Pruefung (`skillZustand`) -- der Login schreibt nichts, wozu er keinen Anlass hat.
300
+ *
301
+ * ⚠️ Ohne Datei oder ohne Stempel: `false`. Eine Datei ohne Stempel ist von vor 0.1.5 oder nicht von uns --
302
+ * die fasst der Login nicht an (Lagen-Tabelle L3, L8 in docs/qa/2026-10-02-mehrsprachig-teil-4-messungen.md).
279
303
  */
280
- export function skillMeldung() {
281
- return "\n → Deine kimal3-Skilldateien sind aelter als die CLI. kimal3 skill install\n";
304
+ export function skillTauschNoetig(inhalt, sprache) {
305
+ return inhalt !== null && stempelAus(inhalt) !== null && stempelSpracheAus(inhalt) !== sprache;
282
306
  }
283
307
  /**
284
308
  * Zerlegt den Kopf `X-Kimal3-Kontext-Stand`: `<firmen-id>:<stand>` (Task 10).
@@ -322,12 +346,12 @@ export function kontextStandAusKopf(wert) {
322
346
  * 🔑 US-8: `firmenName` steht IM TEXT, nicht nur die ID -- wer in zwei Firmen
323
347
  * ist, muss lesen koennen, WELCHE gemeint ist.
324
348
  */
325
- export function firmenHinweisAus(lokalerStand, serverStand, firmenName) {
349
+ export function kontextHinweisAus(lokalerStand, serverStand, text) {
326
350
  if (lokalerStand === serverStand)
327
351
  return null;
328
352
  return {
329
353
  id: `${KONTEXT_HINWEIS_PRAEFIX}${serverStand}`,
330
- text: `${firmenName} hat den Firmenkontext aktualisiert. kimal3 kontext runter`,
354
+ text,
331
355
  gilt_unter_version: null,
332
356
  };
333
357
  }
@@ -1,4 +1,6 @@
1
- import { alsBefehlszeile } from "./argumente.js";
1
+ import { alsBefehlszeile, schalterOhneWert } from "./argumente.js";
2
+ import { schalterIn } from "./befehlswoerter.js";
3
+ import { PROJEKTE_TEXTE } from "./texte/projekte.js";
2
4
  /**
3
5
  * Der Firmenwunsch bei `projekt neu` -- herausgeloest aus `befehle/projekt-neu.ts`,
4
6
  * damit `check:cli` ihn als reine Rechnung pruefen kann (Final-Review F-1).
@@ -21,10 +23,8 @@ import { alsBefehlszeile } from "./argumente.js";
21
23
  * gewacht wurde.
22
24
  */
23
25
  export function ohneFirmaAusArgv(argv) {
24
- return {
25
- rest: argv.filter((a) => a !== "--ohne-firma"),
26
- ohneFirma: argv.includes("--ohne-firma"),
27
- };
26
+ const { gesetzt, rest } = schalterOhneWert(argv, "ohne-firma");
27
+ return { rest, ohneFirma: gesetzt };
28
28
  }
29
29
  /**
30
30
  * Schritt 2, nach `zerlege`: aus Firmenname, Firmen-ID und dem Schalter den
@@ -38,11 +38,11 @@ export function ohneFirmaAusArgv(argv) {
38
38
  * CLI die widerspruechliche Eingabe VORHER ab -- wie beim fehlenden `--titel`
39
39
  * daneben.
40
40
  */
41
- export function firmenKoerperAus(firma, firmaId, ohneFirma) {
41
+ export function firmenKoerperAus(firma, firmaId, ohneFirma, s) {
42
42
  if (ohneFirma && (firma || firmaId)) {
43
43
  return {
44
44
  ok: false,
45
- meldung: 'Entweder --ohne-firma ODER --firma/--firma-id -- nenn nur eines von beiden.',
45
+ meldung: PROJEKTE_TEXTE[s].companyConflict,
46
46
  };
47
47
  }
48
48
  return { ok: true, koerper: { firma: firma ?? "", firma_id: firmaId ?? "", ohne_firma: ohneFirma } };
@@ -63,12 +63,12 @@ export function firmenKoerperAus(firma, firmaId, ohneFirma) {
63
63
  * `o` an (s. `ohneFirmaAusArgv` oben) und braucht deshalb keinen eigenen
64
64
  * Ausschluss.
65
65
  */
66
- export function befehlszeileOhneFirma(o) {
66
+ export function befehlszeileOhneFirma(o, s) {
67
67
  const teile = [];
68
68
  for (const [name, wert] of Object.entries(o)) {
69
69
  if (name === "firma" || name === "firma-id")
70
70
  continue;
71
- teile.push(`--${name}`, wert);
71
+ teile.push(`--${schalterIn(name, s)}`, wert);
72
72
  }
73
73
  return alsBefehlszeile(teile);
74
74
  }