kimal3 0.2.0 → 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.
@@ -42,6 +42,18 @@ import { createHash } from "node:crypto";
42
42
  * ⛔ Die Werkbank-`CLAUDE.md` steht NICHT hier. Sie traegt einen Namen im
43
43
  * Ordner und gehoert einem Menschen (Spec §4).
44
44
  */
45
+ /**
46
+ * Die oberste `CLAUDE.md` des Firmen-Ordners -- die einzige verwaltete Datei,
47
+ * die den Firmennamen im Klartext traegt (seit A-11) UND den Weg zum zweiten
48
+ * Menschen mitgeht (sie steht in der Positiv-Liste der `.gitignore`).
49
+ * Deshalb ist sie das Erkennungszeichen in `firmennameAusClaudeMd`.
50
+ *
51
+ * ⚠️ Sie ist zugleich der erste Eintrag von `VERWALTETE_DATEIEN` darunter --
52
+ * dass beide dieselbe Datei meinen, prueft `check:cli`. Dort haengt auch die
53
+ * Pruefung, dass die ECHTE Vorlage ihre Ueberschrift noch so schreibt, wie
54
+ * `firmennameAusClaudeMd` sie liest.
55
+ */
56
+ export const FIRMEN_CLAUDE_MD = "CLAUDE.md";
45
57
  export const VERWALTETE_DATEIEN = [
46
58
  "CLAUDE.md",
47
59
  "kontext/kurzprofil.md",
@@ -117,6 +129,83 @@ export const LEER = "leer";
117
129
  export const FORMAT_AKTUELL = 1;
118
130
  /** Der leere Merkzettel -- weder Datei-Eintraege noch eine bekannte Firma. */
119
131
  const LEERER_MERKZETTEL = { firma_id: null, dateien: {} };
132
+ /**
133
+ * Den Firmennamen aus der obersten `CLAUDE.md` lesen -- ihre erste Zeile
134
+ * lautet `# CLAUDE.md — <Firma> · KI-Arbeitsplatz`.
135
+ *
136
+ * 🔴 **Warum es diese zweite Quelle ueberhaupt braucht** (Befund 2 der DRITTEN
137
+ * Review-Runde, 2026-08-27, und er trifft die Reparatur der zweiten):
138
+ * `firmenAbgleich` verglich zuerst nur die IDs aus den Merkzetteln. Der
139
+ * Merkzettel steht aber in der `.gitignore` der Firmenvorlage -- bewusst, er
140
+ * ist Maschinenzustand. **Genau der Fall, fuer den die Wache gebaut wurde, hat
141
+ * ihn deshalb nie:** Wer den Firmenordner als Kopie oder ueber das Repo der
142
+ * Firma bekommt -- also der zweite Mensch, fuer den `werkbank neu` da ist --
143
+ * hat kein `.kimal3-stand.json`. `firma_id` war `null`, die Antwort lautete
144
+ * `unbekannt`, und die Wache liess durch.
145
+ * *Eine Wache, die ihr Erkennungszeichen aus einer Datei liest, die den Weg
146
+ * zum Kunden gar nicht mitgeht, leuchtet fuer immer gruen.*
147
+ *
148
+ * 🔑 Die `CLAUDE.md` geht den Weg mit: Sie steht ausdruecklich in der
149
+ * Positiv-Liste der `.gitignore` (`!/CLAUDE.md`), und seit A-11 traegt sie den
150
+ * echten Firmennamen statt eines Platzhalters.
151
+ *
152
+ * ⚠️ Ein unersetzter `[FIRMA]`-Platzhalter zaehlt als **kein** Name: Er steht
153
+ * genau dann da, wenn der Server die Firma nicht kannte -- also im
154
+ * Vordruck-Fall, in dem es nichts zu vergleichen gibt.
155
+ */
156
+ export function firmennameAusClaudeMd(inhalt) {
157
+ if (inhalt === null)
158
+ return null;
159
+ const treffer = /^#\s*CLAUDE\.md\s*—\s*(.+?)\s*·\s*KI-Arbeitsplatz\s*$/m.exec(inhalt);
160
+ if (treffer === null)
161
+ return null;
162
+ const name = treffer[1].trim();
163
+ return name === "" || name === "[FIRMA]" ? null : name;
164
+ }
165
+ /**
166
+ * 🔑 **ZWEI Erkennungszeichen, in dieser Reihenfolge -- die ID schlaegt den
167
+ * Namen.** Zwei Firmen duerfen gleich heissen (dafuer gibt es `--firma-id`),
168
+ * aber nie dieselbe ID haben. Wo beide Seiten eine ID kennen, ist die Antwort
169
+ * also sicher; der Name ist der Rueckfall fuer den geklonten Ordner, der
170
+ * keinen Merkzettel hat.
171
+ *
172
+ * ⚠️ **Der Name ist die schwaechere Auskunft und wird nur gefragt, wenn die
173
+ * ID schweigt** -- nicht zusaetzlich. Sonst widersprechen sich zwei Quellen
174
+ * bei zwei gleichnamigen Firmen, und die staerkere verloere.
175
+ */
176
+ export function firmenAbgleich(lokal, geliefert) {
177
+ if (lokal.id !== null && geliefert.id !== null) {
178
+ return lokal.id === geliefert.id ? { art: "passt" } : { art: "andere-firma" };
179
+ }
180
+ if (lokal.name !== null && geliefert.name !== null) {
181
+ if (lokal.name === geliefert.name)
182
+ return { art: "passt" };
183
+ /*
184
+ 🔴 **EIN VERSCHIEDENER NAME IST EIN VERDACHT, KEIN BEWEIS** (Befund 1
185
+ der vierten Review-Runde, 2026-08-27 -- und er trifft die Reparatur der
186
+ dritten).
187
+
188
+ Die `CLAUDE.md` ist ausdruecklich eine Datei, die der Kunde AENDERN
189
+ darf: `kontext runter` sagt ihm woertlich „Ich fasse nichts an -- du
190
+ hast lokal geaendert". Schreibt eine Admin in die erste Zeile den vollen
191
+ Firmennamen („Nordwind Handel GmbH"), waehrend die App noch „Nordwind"
192
+ fuehrt, und legt den Ordner ins Firmen-Repo, dann liest ihr Kollege beim
193
+ Klonen einen Namen, der nicht zum gelieferten passt.
194
+
195
+ Mit `andere-firma` waere er damit HART GESPERRT worden -- mit einem Satz,
196
+ der falsch ist („eine andere Firma") und keinen gangbaren Ausweg nennt.
197
+ *Und gesperrt worden waere genau der Mensch, fuer den `werkbank neu`
198
+ ueberhaupt gebaut ist.*
199
+
200
+ 🔑 Deshalb `zweifel`: Der Name ist das SCHWAECHERE Erkennungszeichen und
201
+ bekommt die schwaechere Folge. Sperren darf nur, was sicher ist -- die
202
+ ID. *Wer auf einen Verdacht hin zusperrt, schadet oefter dem Unschuldigen
203
+ als dem Fall, den er treffen wollte.*
204
+ */
205
+ return { art: "zweifel", hier: lokal.name, gewaehlt: geliefert.name };
206
+ }
207
+ return { art: "unbekannt" };
208
+ }
120
209
  /**
121
210
  * Kurz-Pruefsumme des Inhalts. Zwoelf Hexzeichen aus SHA-256.
122
211
  *
@@ -333,24 +422,27 @@ export function bekannterStandAus(merkzettel) {
333
422
  * vergessenes Argument ist das nicht: Wer diese Funktion aufruft, muss sich
334
423
  * entscheiden, nicht stillschweigend `null` bekommen.
335
424
  *
336
- * 🔴 TASK 2: `vorlagenPruefsummen` ist das vierte, LETZTE und OPTIONALE
337
- * Argument (Default `{}`) -- bewusst ans Ende gehaengt, nicht dazwischen. Der
338
- * einzige heutige Aufrufer (`ausliefern.ts`) reicht die echten
339
- * Vordruck-Pruefsummen noch nicht durch (das wiederum durchzuziehen ist Sache
340
- * einer spaeteren Aufgabe -- diese hier baut nur die Rechnung, s. Auftrag);
341
- * ein PFLICHT-Argument haette genau diesen bestehenden Aufruf sofort zum
342
- * Kompilierfehler gemacht, und diese Aufgabe darf ausschliesslich in dieser
343
- * Datei und in `check-kontext.mts` etwas aendern. Fehlt eine Pruefsumme fuer
344
- * eine Datei, traegt ihr `vorlage`-Feld `LEER` -- derselbe Sicherheitswert
345
- * wie beim ungepflegten `stand` zwei Zeilen weiter unten, aus demselben
346
- * Grund: *wer nichts weiss, behauptet nichts.*
425
+ * 🔴 TASK 2: `vorlagenPruefsummen` traegt je Datei den Fingerabdruck UNSERER
426
+ * VORLAGE -- nicht den des ausgelieferten Inhalts (das ist `pruef`). Fehlt
427
+ * eine Pruefsumme fuer eine Datei, traegt ihr `vorlage`-Feld `LEER` --
428
+ * derselbe Sicherheitswert wie beim ungepflegten `stand` zwei Zeilen weiter
429
+ * unten, aus demselben Grund: *wer nichts weiss, behauptet nichts.*
430
+ *
431
+ * 🔴 **PUNKT A-1 (2026-08-26): DIESES ARGUMENT IST JETZT PFLICHT.** Es war
432
+ * optional, mit Default `{}` -- und genau deshalb hat es ein halbes Jahr lang
433
+ * niemand uebergeben. `ausliefern.ts` rief die Funktion mit drei Werten auf,
434
+ * jeder ausgelieferte Merkzettel trug bei allen sechs Dateien
435
+ * `"vorlage": "leer"`, und die Lage `vorlage-neuer` unten konnte in der
436
+ * Wirklichkeit nie entstehen: *keine fehlende Zeile, kein Compiler-Fehler,
437
+ * kein roter Waechter.* Ein Pflicht-Argument ist die einzige Absicherung, die
438
+ * auch den erwischt, der diesen Kommentar nie liest.
347
439
  *
348
440
  * 🔑 `format` wird HIER, beim NEU-Bauen einer Lieferung, immer gesetzt --
349
441
  * anders als beim blossen LESEN einer womoeglich alten Datei
350
442
  * (`leseMerkzettel` oben). Jede Lieferung, die durch diese Funktion entsteht,
351
443
  * ist per Definition die aktuelle Fassung.
352
444
  */
353
- export function baueMerkzettel(dateien, staende, firmenId, vorlagenPruefsummen = {}) {
445
+ export function baueMerkzettel(dateien, staende, firmenId, vorlagenPruefsummen) {
354
446
  const raus = {};
355
447
  for (const pfad of VERWALTETE_DATEIEN) {
356
448
  const datei = dateien.find((d) => d.pfad === pfad);
@@ -454,15 +546,25 @@ export function merkzettelNachHoch(vomServer, lokal, gesendet) {
454
546
  * Firmen-Zeile -- in beiden Faellen wuerde ein sang- und klangloses
455
547
  * Ueberschreiben echte Arbeit verlieren, also wird "beides".
456
548
  *
457
- * 🔴 `aktuelleVorlage` ist OPTIONAL, NICHT weil die dritte Dimension optional
458
- * waere, sondern weil das Verdrahten mit echten Vordruck-Pruefsummen an den
459
- * Aufrufstellen (`kontext-runter.ts`) NICHT zu dieser Aufgabe gehoert (s.
460
- * Auftrag: "Bau die Lage, nicht ihre Ausgabe") -- und diese Aufgabe darf
461
- * ausschliesslich in dieser Datei und in `check-kontext.mts` etwas aendern.
462
- * Bleibt das Argument weg, verhaelt sich diese Funktion BUCHSTAeBLICH wie vor
463
- * dieser Aufgabe: `vorlage-neuer` kann dann gar nicht entstehen. Das ist kein
464
- * Kompromiss an der Rechnung, sondern die Rechnung UND der bestehende,
465
- * unveraenderte Aufrufer bleiben beide wahr.
549
+ * 🔴 **PUNKT A-1 (2026-08-26): `aktuelleVorlage` IST JETZT PFLICHT.** Es war
550
+ * optional -- mit der Begruendung, das Verdrahten gehoere zu einer spaeteren
551
+ * Aufgabe. Zurueckgekommen ist danach nie jemand: `kontext-runter.ts` rief
552
+ * diese Funktion ohne den Wert auf, `vorlage-neuer` konnte in der
553
+ * Wirklichkeit nicht entstehen, und der Kunde las weiter „Dein Firmenkontext
554
+ * ist aktuell". *Die Rechnung war gebaut, geprueft, dokumentiert -- und tot.*
555
+ * Wer den Wert jetzt weglaesst, bekommt einen Kompilierfehler.
556
+ *
557
+ * ⚠️ **Ein Merkzettel-Eintrag OHNE `vorlage` gilt als „unbekannt", und
558
+ * unbekannt zaehlt als GEAeNDERT.** Solche Eintraege liegen echt auf Platten:
559
+ * Sie stammen von einer Auslieferung, bevor es dieses Feld gab. Der Vergleich
560
+ * `undefined !== aktuelleVorlage` ist wahr, es entsteht also einmalig
561
+ * `vorlage-neuer` bzw. `beides` -- dieselbe Antwort wie bei einem fehlenden
562
+ * Eintrag, aus demselben Grund: *wer nichts weiss, sichert, bevor er
563
+ * ueberschreibt.* Die Gegenrichtung waere schlimmer: Wuerde „unbekannt" als
564
+ * „unveraendert" gelesen, bekaeme der Kunde die Verbesserung nicht -- und der
565
+ * naechste Lauf haette obendrein einen Merkzettel, der den Server-Text
566
+ * beschreibt, waehrend die alte Datei danebenliegt, und meldete faelschlich
567
+ * „du hast lokal geaendert".
466
568
  */
467
569
  export function lageFuerDatei(a) {
468
570
  if (a.lokalerInhalt === a.serverInhalt)
@@ -472,19 +574,76 @@ export function lageFuerDatei(a) {
472
574
  return "server-neuer";
473
575
  const lokalGeaendert = a.eintrag === null ? true : pruefsumme(a.lokalerInhalt) !== a.eintrag.pruef;
474
576
  const serverNeuer = a.eintrag === null ? true : a.eintrag.stand !== a.serverStand;
475
- const vorlageNeuer = a.aktuelleVorlage === undefined
476
- ? false
477
- : a.eintrag === null
478
- ? true
479
- : a.eintrag.vorlage !== a.aktuelleVorlage;
577
+ const vorlageNeuer = a.eintrag === null ? true : a.eintrag.vorlage !== a.aktuelleVorlage;
578
+ /*
579
+ 🔴 **`vorlageNeuer` zaehlt hier BEWUSST nicht mit** (Review-Befund 1 zu
580
+ Punkt A-1, 2026-08-26 -- gemessen, nicht vermutet).
581
+
582
+ Hier stand einen Nachmittag lang `serverNeuer || vorlageNeuer`. Die Folge:
583
+ Wer seine Datei lokal geaendert hatte, um sie spaeter mit `kimal3 kontext
584
+ hoch` zu schicken, verlor beim naechsten `runter` seinen Arbeitsstand,
585
+ sobald WIR irgendeine Frage umformuliert hatten -- gesichert zwar, aber
586
+ aus der Datei verschwunden, und ohne dass er danach gefragt haette.
587
+ *Eine Verbesserung an unserem Text darf nicht die Arbeit von jemandem
588
+ wegraeumen, der gerade an seiner sitzt.*
589
+
590
+ 🔑 Der richtige Weg ist der, den `lokal-geaendert` ohnehin nennt: erst
591
+ hochschicken, dann herunterladen. Dann liegen seine Antworten auf dem
592
+ Server, der Stand aendert sich, und der naechste `runter` bringt seine
593
+ Antworten UND unsere neuen Fragen -- zusammengefuehrt statt eines gegen
594
+ das andere. Wer nie hochschickt, behaelt seine Datei; sie gehoert ihm.
595
+
596
+ ⚠️ `vorlage-neuer` unten bleibt davon unberuehrt. Genau dafuer wurde A-1
597
+ gebaut, und es ist der haeufige Fall: eine Datei, die unangetastet so
598
+ dasteht, wie wir sie geliefert haben.
599
+ */
480
600
  if (lokalGeaendert)
481
- return serverNeuer || vorlageNeuer ? "beides" : "lokal-geaendert";
601
+ return serverNeuer ? "beides" : "lokal-geaendert";
482
602
  if (serverNeuer)
483
603
  return "server-neuer";
484
604
  if (vorlageNeuer)
485
605
  return "vorlage-neuer";
486
606
  return "schon-aktuell";
487
607
  }
608
+ /**
609
+ * Dieselbe Entscheidung wie {@link lageFuerDatei} -- aber gefuettert aus den
610
+ * ZWEI Merkzetteln, zwischen denen `kimal3 kontext runter` steht: dem auf der
611
+ * Platte und dem aus der Antwort des Servers.
612
+ *
613
+ * 🔴 **WARUM ES DIESE FUNKTION GIBT (Punkt A-1, 2026-08-26).** Der eigentliche
614
+ * Fehler war nicht die Rechnung -- die stimmte. Er war, WOHER ihre Eingaben
615
+ * kommen: `kontext-runter.ts` gab ihr den Vordruck-Abdruck gar nicht erst mit.
616
+ * Genau dieser Schritt ("aus welchem der beiden Merkzettel kommt welcher
617
+ * Wert?") liess sich von keiner Wache sehen, weil er mitten im Befehl wohnte
618
+ * -- zwischen `fetch` und Dateisystem, wo `check:kontext` nie hinkommt.
619
+ *
620
+ * 🔴 **DIE ZUORDNUNG, DIE HIER ABGESICHERT WIRD -- sie ist nicht offensichtlich
621
+ * und in beide Richtungen falsch machbar:**
622
+ * - `eintrag` kommt aus dem LOKALEN Merkzettel. Er sagt, was beim letzten
623
+ * Holen galt: die eine Seite des Vergleichs.
624
+ * - `serverStand` UND `aktuelleVorlage` kommen aus dem Merkzettel DES SERVERS.
625
+ * Sie sagen, was heute gilt: die andere Seite.
626
+ *
627
+ * ⚠️ **Nimmt man beide Seiten aus derselben Datei, vergleicht die Rechnung
628
+ * eine Zahl mit sich selbst und findet NIE einen Unterschied** -- kein Fehler,
629
+ * keine Meldung, der Kunde liest „Dein Firmenkontext ist aktuell". Gemessen:
630
+ * Mit `aktuelleVorlage` aus dem lokalen Merkzettel blieb die verbesserte
631
+ * Datei liegen, und der naechste Lauf behauptete obendrein „du hast lokal
632
+ * geaendert". *Das ist derselbe Schaden wie vorher, nur eine Zeile weiter.*
633
+ *
634
+ * 🔑 Fehlt die Datei im Merkzettel des Servers, gilt fuer beide Werte `LEER`
635
+ * -- „wir wissen nichts", derselbe Rueckfall wie ueberall in dieser Datei.
636
+ */
637
+ export function lageAusMerkzetteln(a) {
638
+ const vomServer = a.vomServer.dateien[a.pfad] ?? null;
639
+ return lageFuerDatei({
640
+ lokalerInhalt: a.lokalerInhalt,
641
+ serverInhalt: a.serverInhalt,
642
+ eintrag: a.lokal.dateien[a.pfad] ?? null,
643
+ serverStand: vomServer?.stand ?? LEER,
644
+ aktuelleVorlage: vomServer?.vorlage ?? LEER,
645
+ });
646
+ }
488
647
  /** `kontext/vorher-2026-08-21` */
489
648
  export function sicherungsOrdner(datum) {
490
649
  return `${SICHERUNG_BASIS}/vorher-${datum}`;
@@ -564,6 +723,102 @@ export function arbeitsplatzAus(kandidaten) {
564
723
  Antworten an der Stelle von `EINSETZ_MARKE_DREI_SAETZE` ein, und dass DIESE
565
724
  Marke woertlich in der Vorlage steht, prueft `check:arbeitsplatz`.
566
725
  */
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
+ };
567
822
  /**
568
823
  * Was ein Mensch lesen soll, wenn `kimal3 kontext runter` Dateien in Ruhe
569
824
  * laesst, weil er sie lokal geaendert hat -- je nach Rolle und je nach DATEI.
@@ -587,30 +842,29 @@ export function arbeitsplatzAus(kandidaten) {
587
842
  * Verhalten eines Befehls wohnte im Befehl selbst, wo keine Vorlagen-Wache je
588
843
  * hinsieht.* `check:kontext` prueft jetzt genau diese Zuordnung.
589
844
  *
590
- * ⚠️ ECHTE UMLAUTE -- das liest ein Kunde. Die ASCII-Umschrift dieser Datei
591
- * gilt nur fuer Kommentare.
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.
592
857
  */
593
858
  export function ratNachUnberuehrt(unberuehrt, rolle, adminName) {
594
- const zeilen = [];
595
- // Fuer alles, was `kontext hoch` wirklich tragen kann.
596
- if (unberuehrt.some((p) => FIRMEN_TEXTE.some((t) => t.pfad === p))) {
597
- if (rolle === "admin") {
598
- zeilen.push(" Deine Fassung ist die neuere? Dann schick sie hoch: kimal3 kontext hoch");
599
- }
600
- else {
601
- const wer = adminName ? ` an ${adminName}` : " an euren Firmen-Admin";
602
- zeilen.push(" Deine Fassung bleibt stehen — hochschicken darf nur der Firmen-Admin.\n" +
603
- ` Gib deine Ergänzung${wer} weiter; von dort geht sie in der App unter\n` +
604
- " Firma → Kontext oder mit `kimal3 kontext hoch` an alle.");
605
- }
606
- }
607
- // Und fuer die eine Datei, die er nicht tragen kann.
608
- if (unberuehrt.includes("CLAUDE.md")) {
609
- zeilen.push(" Die `CLAUDE.md` kann `kimal3 kontext hoch` nicht mitnehmen — der Befehl schickt nur\n" +
610
- " die fünf Dateien aus `kontext/`. Die drei Sätze in dieser Datei kommen aus\n" +
611
- " `kontext/kurzprofil.md`, der Firmenname aus der App: Ändere sie dort, dann holt\n" +
612
- " der nächste `kimal3 kontext runter` sie hierher.\n" +
613
- " Alles Übrige darin ist unsere Vorlage — dafür gibt es keinen Weg zurück.");
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]);
614
865
  }
615
- return zeilen;
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));
616
870
  }
package/dist/nachricht.js CHANGED
@@ -62,6 +62,21 @@ function teile(v) {
62
62
  }
63
63
  return zahlen;
64
64
  }
65
+ /**
66
+ * Laesst sich das ueberhaupt als Versionsstand lesen?
67
+ *
68
+ * 🔴 **Gebraucht, weil `istAelter` unten bei Muell `false` sagt** -- und
69
+ * `false` heisst dort „nicht aelter", was ein Aufrufer als „neu genug" liest.
70
+ * Fuer den Update-Hinweis ist das richtig („im Zweifel schweigen"), fuer eine
71
+ * Faehigkeits-Frage (`cli/src/faehigkeiten.ts`) ist es genau verkehrt: Dort
72
+ * wuerde ein unlesbarer Kopf zur Auskunft „die Fassung kann alles".
73
+ *
74
+ * *Dieselbe Antwort `false` bedeutet an zwei Stellen Verschiedenes -- deshalb
75
+ * muss die Frage „ist das ueberhaupt eine Nummer" getrennt gestellt werden.*
76
+ */
77
+ export function istVersionsstand(v) {
78
+ return teile(v) !== null;
79
+ }
65
80
  /**
66
81
  * Ist `a` aelter als `b`? Zahl fuer Zahl, nicht als Text.
67
82
  *
@@ -100,9 +115,73 @@ export function heuteAus(d) {
100
115
  return new Intl.DateTimeFormat("sv-SE", { timeZone: "Europe/Berlin" }).format(d);
101
116
  }
102
117
  /**
103
- * Die ganze Entscheidung, als Funktion von vier Werten.
118
+ * Die Kennung eines FIRMENKONTEXT-Hinweises faengt hiermit an, und dahinter
119
+ * steht der Server-Stand, den er meint (`kontext-2026-08-26T09:00:00Z`).
120
+ *
121
+ * 🔑 Sie steht als eigener Name da, weil sie von ZWEI Seiten gebraucht wird:
122
+ * `firmenHinweisAus` baut die Kennung, `hinweisErledigt` liest sie wieder
123
+ * auseinander. Zwei getippte `"kontext-"` waeren zwei Wahrheiten -- und die
124
+ * zweite verrottete lautlos, weil ein Hinweis, der nicht mehr erkannt wird,
125
+ * einfach wieder erscheint, statt einen Fehler zu machen.
126
+ */
127
+ export const KONTEXT_HINWEIS_PRAEFIX = "kontext-";
128
+ /**
129
+ * Ist der ANLASS dieses Hinweises inzwischen erledigt?
130
+ *
131
+ * 🔴 DER FALL, DER DIESE FUNKTION RECHTFERTIGT (offener Punkt A-4, gemessen
132
+ * am 2026-08-26): Direkt nach `kimal3 kontext runter` stand da
133
+ *
134
+ * Dein Firmenkontext ist aktuell.
135
+ * Nordwind hat den Firmenkontext aktualisiert. kimal3 kontext runter
136
+ *
137
+ * -- die zweite Zeile riet zu genau dem Befehl, der gerade gelaufen war. Kein
138
+ * Rechenfehler: Die CLI schickt ihren Stand VOR dem Herunterladen mit, der
139
+ * Server vergleicht ihn mit seinem eigenen und haengt den Hinweis an. Bis der
140
+ * Hinweis ausgegeben wird, hat der Befehl den lokalen Stand laengst
141
+ * nachgezogen. *Die Meldung kam einen Aufruf zu spaet.*
142
+ *
143
+ * Die Regel dagegen: **Ein Hinweis, dessen Stand der lokale Merkzettel JETZT
144
+ * traegt, ist erledigt und wird nicht ausgegeben.**
145
+ *
146
+ * ⚠️ NUR Kontext-Hinweise. Die globale Nachricht aus der Datenbank traegt eine
147
+ * UUID als Kennung und darf durch diese Regel NIE verstummen -- sie hat mit
148
+ * dem Firmenkontext nichts zu tun, und ein Kanal, der sich versehentlich selbst
149
+ * stilllegt, faellt niemandem auf.
150
+ *
151
+ * ⚠️ `null` und leer heissen "wir wissen es nicht", nicht "erledigt": Wer den
152
+ * lokalen Stand nicht kennt, darf keinen Hinweis unterdruecken. Im Zweifel
153
+ * zeigen -- eine Zeile zu viel ist harmlos, eine verschluckte Zeile nicht.
154
+ */
155
+ export function hinweisErledigt(hinweisId, lokalerStandJetzt) {
156
+ if (!hinweisId)
157
+ return false;
158
+ if (!hinweisId.startsWith(KONTEXT_HINWEIS_PRAEFIX))
159
+ return false;
160
+ const gemeinterStand = hinweisId.slice(KONTEXT_HINWEIS_PRAEFIX.length);
161
+ if (gemeinterStand === "")
162
+ return false;
163
+ if (lokalerStandJetzt === null || lokalerStandJetzt === "")
164
+ return false;
165
+ return gemeinterStand === lokalerStandJetzt;
166
+ }
167
+ /**
168
+ * Die ganze Entscheidung, als Funktion von fuenf Werten.
169
+ *
170
+ * 🔴 `lokalerKontextStandJetzt` IST ABSICHTLICH PFLICHT, nicht optional
171
+ * (A-4). Am 2026-08-26 hat dieses Projekt gemessen, was ein optionales
172
+ * Argument anrichtet: `lageFuerDatei` konnte erkennen, dass sich unsere
173
+ * Vorlage geaendert hat -- die Aufrufstelle uebergab den noetigen Wert nie,
174
+ * und weil das Argument optional war, fehlte keine Zeile, meckerte kein
175
+ * Compiler und wurde kein Waechter rot. Die Rechnung war gebaut, geprueft,
176
+ * dokumentiert und in der Wirklichkeit tot. *Ein Pflicht-Argument ist die
177
+ * einzige Verdrahtung, die der Compiler fuer einen mitdenkt.*
178
+ *
179
+ * 🔑 UND ER MUSS FRISCH SEIN. Der Wert, den die CLI im Kopf mitschickt, wird
180
+ * VOR dem Netzaufruf gelesen; dieser hier gehoert an das ENDE des Befehls.
181
+ * Wer denselben Wert zweimal benutzt, baut den Fehler wieder ein, gegen den
182
+ * `hinweisErledigt` geschrieben ist.
104
183
  */
105
- export function sollZeigen(h, eigeneVersion, gesehen, heute) {
184
+ export function sollZeigen(h, eigeneVersion, gesehen, heute, lokalerKontextStandJetzt) {
106
185
  if (!h.id)
107
186
  return false;
108
187
  if (!h.text || h.text.trim() === "")
@@ -110,6 +189,11 @@ export function sollZeigen(h, eigeneVersion, gesehen, heute) {
110
189
  // Leeres Feld = alle sehen es. Sonst nur, wer aelter ist als die Grenze.
111
190
  if (h.gilt_unter_version && !istAelter(eigeneVersion, h.gilt_unter_version))
112
191
  return false;
192
+ // Der Anlass ist erledigt -- der Befehl, zu dem geraten wird, ist gerade
193
+ // gelaufen. Bewusst VOR der Tagessperre: Ein Hinweis, der gar nicht mehr
194
+ // gilt, soll auch nicht den einen Platz des Tages verbrauchen.
195
+ if (hinweisErledigt(h.id, lokalerKontextStandJetzt))
196
+ return false;
113
197
  // Hoechstens einmal am Tag -- auch bei zwanzig Befehlen.
114
198
  if (gesehen[h.id] === heute)
115
199
  return false;
@@ -242,7 +326,7 @@ export function firmenHinweisAus(lokalerStand, serverStand, firmenName) {
242
326
  if (lokalerStand === serverStand)
243
327
  return null;
244
328
  return {
245
- id: `kontext-${serverStand}`,
329
+ id: `${KONTEXT_HINWEIS_PRAEFIX}${serverStand}`,
246
330
  text: `${firmenName} hat den Firmenkontext aktualisiert. kimal3 kontext runter`,
247
331
  gilt_unter_version: null,
248
332
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kimal3",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Trag deine KI-Projekte dort ein, wo sie entstehen: im Terminal.",
5
5
  "type": "module",
6
6
  "bin": {
package/skill/SKILL.md CHANGED
@@ -43,6 +43,21 @@ Das ist meist richtig, aber nicht immer schön (`s.mueller@…` wird zu `werkban
43
43
  **Frag den Menschen nach seinem Vornamen und gib ihn mit**, statt es dem Server zu überlassen —
44
44
  er heißt selten so, wie seine Adresse aussieht.
45
45
 
46
+ 🔴 **Gehört der Mensch zu MEHREREN Firmen, fragen beide Befehle nach — mit `--firma`.**
47
+ Sie legen dann nichts an, sondern nennen die Firmen als fertige Zeilen zum Kopieren. Das ist
48
+ kein Fehler, sondern die Rückfrage: Der Ordner trägt den Namen der Firma, und raten darf der
49
+ Befehl ihn nicht.
50
+
51
+ ```
52
+ kimal3 arbeitsplatz neu sabine --firma "Nordwind Handel GmbH"
53
+ kimal3 werkbank neu sabine --firma "Nordwind Handel GmbH"
54
+ kimal3 werkbank neu sabine --firma-id "…" # falls zwei Firmen GLEICH heissen
55
+ ```
56
+
57
+ ⚠️ Der Rückgabewert ist dann `1` und die Absage steht auf der Fehlerausgabe. **Nimm eine der
58
+ angebotenen Zeilen, statt es unverändert zu wiederholen** — ohne `--firma` kommt dieselbe
59
+ Rückfrage noch einmal.
60
+
46
61
  ⚠️ **`werkbank neu` verlangt ein eigenes Pro-Konto.** Die Route prüft den Plan **pro Person**
47
62
  (`konto_plan`), nicht über die Firmen-Mitgliedschaft. Ein Kollege ohne eigenes Pro bekommt
48
63
  Rückgabewert `4` — dann **nicht** wiederholen und nicht raten, sondern genau das sagen: Sein
@@ -106,10 +121,19 @@ Stelle.
106
121
  Anker, Frage, Hinweis. Sind alle beantwortet, sagt er das in einem Satz, und du bist fertig.
107
122
  Der Befehl liest nur; er braucht weder Netz noch Anmeldung.
108
123
  2. **Die Fragen EINZELN stellen.** Eine Frage, die Antwort abwarten, dann die nächste. Keine
109
- Liste mit siebzehn Punkten und kein Fragebogen am Stück — es sind Fragen über die Firma
110
- eines Menschen, und die beantwortet er nicht in einem Rutsch.
124
+ lange Liste und kein Fragebogen am Stück — es sind Fragen über die Firma eines Menschen, und
125
+ die beantwortet er nicht in einem Rutsch.
111
126
  3. **Die Antwort in die genannte Datei schreiben** — unter ihre `## `-Überschrift, über der der
112
127
  Anker steht. Die Hinweiszeilen mit `>` gehören uns; **lass sie stehen und schreib darunter.**
128
+
129
+ 🔴 **Die Pfade zählen ab dem Ordner, den `kontext fragen` als `Arbeitsplatz:` nennt — nicht
130
+ ab deinem Arbeitsverzeichnis.** Der Normalfall ist, dass beides auseinanderfällt:
131
+ `kimal3 arbeitsplatz neu` rät ausdrücklich, in die **Werkbank** zu wechseln und dort Claude
132
+ zu starten; die fünf Dateien liegen dann eine Ebene höher. **Setz jeden Pfad an den
133
+ genannten Arbeitsplatz an, bevor du schreibst.**
134
+ ⚠️ Wer das übergeht, legt eine **neue** Datei am falschen Ort an. `kimal3 kontext hoch` liest
135
+ weiter die echten Dateien und schickt leere Antworten hoch — *beide Seiten melden Erfolg, und
136
+ die Arbeit des Menschen ist trotzdem weg.*
113
137
  4. **`kimal3 kontext hoch`** — schickt alle fünf Dateien an die Firma. **Nur der Firmen-Admin
114
138
  darf das.**
115
139
 
@@ -124,12 +148,24 @@ darüberkopiert wurde —, bricht `kontext hoch` ab und schickt gar nichts, auch
124
148
  nicht** (Rückgabewert `1`). Das ist **kein** „noch einmal versuchen": Sag dem Menschen, welche
125
149
  Datei gemeint ist, und hol sie mit `kimal3 kontext runter` zurück.
126
150
 
127
- 🔴 **Die eine Regel, die du sonst falsch machst: Rückgabewert `1` bei `kontext hoch` heißt „du
128
- bist kein Admin dieser Firma" — nicht „noch einmal versuchen".** Das ist eine andere `1` als die
129
- üblichen Eingabefehler weiter unten. Ein zweiter Versuch ändert nichts, weil sich an der Rolle
130
- nichts ändert. **Sag dem Menschen, dass er kein Admin ist, und nenne, an wen er sich wendet** —
131
- lief vorher `kontext runter`, kennt dessen Ausgabe den Admin-Namen bereits; sonst frag, wer in
132
- der Firma Admin ist.
151
+ 🔴 **Bei `kontext hoch` sagt die Zahl `1` allein GAR NICHTS — die Meldung sagt es. LIES SIE.**
152
+
153
+ ⚠️ **Hier stand bis zum 2026-08-28: „Rückgabewert `1` heißt ‚du bist kein Admin dieser
154
+ Firma'".** Das war falsch und gefährlich. `kontext hoch` hat **sechs** verschiedene Ausgänge
155
+ auf dieser einen Zahl (`cli/src/befehle/kontext-hoch.ts`): unbekannte Option · kein
156
+ Arbeitsplatz gefunden · eine Datei nicht lesbar · eine Datei ohne Fragen · „jemand anderes hat
157
+ geändert" (Ausweg `--ueberschreiben`) · mehrere Firmen (Ausweg `--firma`) — und die Rolle.
158
+
159
+ 🔴 **Der teuerste davon steht in `kontext-hoch.ts` (Merkzettel-Zweig) und ist ein ERFOLG:** *„Die Firma hat
160
+ deine Änderungen gespeichert — nur der lokale Merkzettel ließ sich nicht schreiben."* Auch das
161
+ gibt `1` zurück. Wer die Zahl als „kein Admin" deutet, sagt einem Menschen, der gerade alle
162
+ Fragen beantwortet und **erfolgreich hochgeschickt** hat, er solle sich an seinen Admin wenden.
163
+ *Ein Erfolg, als Absage vorgelesen — und er tippt danach alles noch einmal.*
164
+
165
+ **Also:** Nimm den Satz, den der Befehl geschrieben hat, und sag dem Menschen, was er bedeutet.
166
+ Geht es wirklich um die Rolle, nenne den Admin — lief vorher `kontext runter`, kennt dessen
167
+ Ausgabe den Namen bereits; sonst frag, wer in der Firma Admin ist. Ein zweiter Versuch hilft nur
168
+ dort, wo die Meldung einen Ausweg nennt (`--ueberschreiben`, `--firma`), sonst nie.
133
169
 
134
170
  ⚠️ **Beide Befehle brauchen wie `werkbank neu` ein eigenes Pro-Konto** — ohne das kommt
135
171
  Rückgabewert `4`, dann **nicht wiederholen**.