kimal3 0.1.6 β†’ 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,254 @@
1
+ import { zerlege } from "../argumente.js";
2
+ import { ruf } from "../api.js";
3
+ import { heuteAus } from "../nachricht.js";
4
+ import { istUnsichererPfad } from "../arbeitsplatz-dateien.js";
5
+ import { LEER, VERWALTETE_DATEIEN, alsMerkzettel, sicherungsOrdner, sicherungsZiel, lageFuerDatei, ratNachUnberuehrt, } from "../kontext-stand.js";
6
+ import { leseEineDatei, leseMerkzettelDatei, schreibeMerkzettelDatei, schreibeVerwalteteDatei, sichereDatei, } from "../kontext-dateien.js";
7
+ import { ERLAUBT, absage, arbeitsplatzOderAbsage } from "./kontext-gemeinsam.js";
8
+ /**
9
+ * Holt den Firmenkontext von der App und legt ihn in den verwalteten Dateien
10
+ * des gefundenen Arbeitsplatzes ab -- `CLAUDE.md` und die fuenf Dateien in
11
+ * `kontext/`.
12
+ *
13
+ * πŸ”΄ **`kontext/kurzprofil.md` ist seit Ruling K-24 (Task 7) dabei.** Sie wurde
14
+ * beim ERSTEN Ausliefern gefuellt und danach nie wieder aufgefrischt -- waehrend
15
+ * sie selbst dem Kunden zusicherte, sie werde β€žzentral in der App gepflegt".
16
+ *
17
+ * πŸ”΄ **R2 (Spec Β§14): Dieser Befehl fragt NIE `git status`.** Er entscheidet
18
+ * ausschliesslich ueber den Merkzettel (`.kimal3-stand.json`), egal ob der
19
+ * Ordner ein Git-Repo ist oder nicht -- sonst gaebe es zwei Verhalten fuer
20
+ * denselben Befehl, und das schwerer erklaerbare traefe ausgerechnet den, der
21
+ * Git benutzt.
22
+ *
23
+ * β›” **Unterhalb von `werkbank-<name>/` wird nichts geschrieben** (Spec Β§4).
24
+ * Dieser Befehl kennt genau die Pfade aus `VERWALTETE_DATEIEN`, und in keinem
25
+ * davon steht ein Personenname.
26
+ *
27
+ * πŸ” **Ruling T9-2:** Die Gegenrichtung, `kontextHoch`, wohnt seit dem Split
28
+ * von `kontext.ts` in `kontext-hoch.ts`. Beide teilen sich Fundregel,
29
+ * Firmenwahl-Absage und die `--firma`/`--firma-id`-Optionen ueber
30
+ * `kontext-gemeinsam.ts`.
31
+ */
32
+ const HILFE = `
33
+ kimal3 kontext runter [--firma "Name" | --firma-id "…"]
34
+
35
+ Holt den Firmenkontext (CLAUDE.md + die fuenf Dateien in kontext/) und legt
36
+ ihn in deinem KI-Arbeitsplatz ab. Was du selbst geaendert hast, fasst der
37
+ Befehl nicht an -- er sagt dir, wie du es einbringst.
38
+
39
+ --firma "Name" Noetig, wenn du zu mehreren Firmen gehoerst.
40
+ Die Absage nennt dir die Namen als fertige Befehle.
41
+
42
+ --firma-id "…" Fuer den Fall, dass zwei deiner Firmen GLEICH heissen.
43
+ Auch diese Absage nennt dir fertige Befehle.
44
+ `;
45
+ export async function kontextRunter(argv) {
46
+ if (argv.includes("--help") || argv.includes("-h")) {
47
+ console.log(HILFE);
48
+ return 0;
49
+ }
50
+ const zerlegt = zerlege(argv, ERLAUBT);
51
+ if (!zerlegt.ok) {
52
+ console.error(zerlegt.meldung);
53
+ return 1;
54
+ }
55
+ // πŸ“Œ Seit Task 7 gemeinsam mit `kontext hoch` und `kontext fragen`
56
+ // (`arbeitsplatzOderAbsage`). Hier standen dieselben vier Zeilen abgetippt;
57
+ // bei drei Aufrufern ist das keine Wiederholung mehr, sondern eine Falle --
58
+ // wer eine Zeile schaerft, schaerft eine von dreien.
59
+ const ordner = arbeitsplatzOderAbsage();
60
+ if (ordner === null)
61
+ return 1;
62
+ const wunsch = zerlegt.werte.optionen.firma;
63
+ const wunschId = zerlegt.werte.optionen["firma-id"];
64
+ const suche = new URLSearchParams();
65
+ if (wunsch !== undefined)
66
+ suche.set("firma", wunsch);
67
+ if (wunschId !== undefined)
68
+ suche.set("firma-id", wunschId);
69
+ const pfad = suche.toString() ? `/api/cli/kontext?${suche.toString()}` : "/api/cli/kontext";
70
+ const antwort = await ruf(pfad, { methode: "GET", mitAusweis: true });
71
+ if (antwort.status !== 200) {
72
+ return absage(antwort, "kontext runter");
73
+ }
74
+ // πŸ”΄ Ein `200` mit unlesbarem Rumpf ist KEIN Erfolg -- dasselbe Muster wie
75
+ // in `firma-daten.ts`. `ruf` verschluckt Parse-Fehler zu `null`.
76
+ if (antwort.koerper === null || typeof antwort.koerper !== "object") {
77
+ console.error("Die Antwort war nicht lesbar. Bitte gleich noch einmal.");
78
+ return 3;
79
+ }
80
+ const daten = alsKontextAntwort(antwort.koerper);
81
+ if (daten === null) {
82
+ console.error("Die Antwort war nicht lesbar. Bitte gleich noch einmal.");
83
+ return 3;
84
+ }
85
+ const lokalerMerkzettel = leseMerkzettelDatei(ordner);
86
+ const heute = heuteAus(new Date());
87
+ const geschrieben = [];
88
+ const gesicherte = [];
89
+ const unberuehrt = [];
90
+ for (const verwalteterPfad of VERWALTETE_DATEIEN) {
91
+ const serverDatei = daten.dateien.find((d) => d.pfad === verwalteterPfad);
92
+ if (!serverDatei) {
93
+ console.error(`Die Antwort enthaelt keine Datei β€ž${verwalteterPfad}". Bitte melden.`);
94
+ return 3;
95
+ }
96
+ // Zweites Netz -- s. Kommentar an `schreibeVerwalteteDatei`: `pfad` kommt
97
+ // hier aus der eigenen Iteration, nicht roh vom Server, trotzdem geprueft.
98
+ if (istUnsichererPfad(serverDatei.pfad)) {
99
+ console.error(`Die Antwort enthaelt einen unsicheren Pfad: β€ž${serverDatei.pfad}". Bitte melden.`);
100
+ return 3;
101
+ }
102
+ const lokalGelesen = leseEineDatei(ordner, verwalteterPfad);
103
+ if (!lokalGelesen.ok) {
104
+ console.error(lokalGelesen.meldung);
105
+ return 1;
106
+ }
107
+ const eintrag = lokalerMerkzettel.dateien[verwalteterPfad] ?? null;
108
+ const serverStand = daten.merkzettel.dateien[verwalteterPfad]?.stand ?? LEER;
109
+ const lage = lageFuerDatei({
110
+ lokalerInhalt: lokalGelesen.inhalt,
111
+ serverInhalt: serverDatei.inhalt,
112
+ eintrag,
113
+ serverStand,
114
+ });
115
+ if (lage === "beides") {
116
+ // `lokalGelesen.inhalt` ist hier garantiert eine Zeichenkette:
117
+ // `lageFuerDatei` liefert "beides" nie, wenn `lokalerInhalt === null`.
118
+ const sicherung = sichereDatei(ordner, verwalteterPfad, lokalGelesen.inhalt, heute);
119
+ if (!sicherung.ok) {
120
+ console.error(sicherung.meldung);
121
+ return 1;
122
+ }
123
+ // Ruling T8-2: `sichereDatei` ueberschreibt nie und ueberspringt nie --
124
+ // `nebenVorhandener` sagt nur, ob HEUTE schon eine andere Sicherung
125
+ // dieser Datei existiert. Beide Faelle sind echte Erfolge und werden
126
+ // unten bewusst UNTERSCHIEDLICH gemeldet (Β§8).
127
+ gesicherte.push({
128
+ pfad: verwalteterPfad,
129
+ ziel: sicherung.ziel,
130
+ nebenVorhandener: sicherung.nebenVorhandener,
131
+ });
132
+ }
133
+ if (lage === "server-neuer" || lage === "beides") {
134
+ const geschriebenErg = schreibeVerwalteteDatei(ordner, verwalteterPfad, serverDatei.inhalt);
135
+ if (!geschriebenErg.ok) {
136
+ console.error(geschriebenErg.meldung);
137
+ return 1;
138
+ }
139
+ geschrieben.push(verwalteterPfad);
140
+ }
141
+ if (lage === "lokal-geaendert") {
142
+ unberuehrt.push(verwalteterPfad);
143
+ }
144
+ }
145
+ /*
146
+ πŸ”΄ Der Merkzettel des SERVERS wird geschrieben -- unabhaengig davon, ob in
147
+ diesem Lauf ueberhaupt etwas geaendert wurde. Er traegt fuer JEDE der fuenf
148
+ Dateien den Stand, der zum jetzt ausgelieferten Inhalt passt (Β§7b), auch
149
+ fuer die, die "schon aktuell" waren oder lokal geaendert blieben -- ein
150
+ lokal geaenderter Eintrag bleibt so beim naechsten Lauf ERKENNBAR lokal
151
+ geaendert, statt durch einen fehlenden Merkzettel-Eintrag neu "sicher" zu
152
+ werden.
153
+
154
+ πŸ”΄ Task 10a: `firma_id` kommt ausdruecklich aus `daten.firma.id` -- der
155
+ Firma, die diese Antwort tatsaechlich beantwortet hat (`waehleFirma` in
156
+ der Route), nicht aus dem Merkzettel-Feld selbst. Wer zu MEHREREN Firmen
157
+ gehoert, soll im Ordner von Firma A niemals lesen "B hat sich geaendert".
158
+ */
159
+ const merkzettelZumSchreiben = { firma_id: daten.firma.id, dateien: daten.merkzettel.dateien };
160
+ const merkzettelSchreiben = schreibeMerkzettelDatei(ordner, merkzettelZumSchreiben);
161
+ if (!merkzettelSchreiben.ok) {
162
+ console.error(merkzettelSchreiben.meldung);
163
+ return 1;
164
+ }
165
+ // ---- Ausgabe --------------------------------------------------------
166
+ // πŸ”΄ Ruling T8-2 (Fix-Runde 1): ZWEI ehrlich verschiedene Faelle, keine
167
+ // Pauschalmeldung mehr. "Frisch gesichert" und "zusaetzlich neben einer
168
+ // schon vorhandenen Sicherung von heute" sind beide echte Erfolge -- aber
169
+ // verschiedene, und der Mensch muss wissen, WAS mit seinem Text passiert
170
+ // ist. Nie ein Satz, der eine Sicherung behauptet, wo keine (mehr)
171
+ // entstanden ist (der urspruengliche Befund).
172
+ //
173
+ // US-7 bleibt erfuellt: Die Meldung steht VOR der "aktualisiert"-Zeile
174
+ // (vor dem Ergebnis) und nennt den Ordner. Die eigentliche Reihenfolge
175
+ // (erst sichern, dann ueberschreiben) erzwingt schon der Code oben.
176
+ if (gesicherte.length > 0) {
177
+ console.log(`Ich sichere deine lokale Fassung, bevor ich sie ersetze -- Ordner: ${sicherungsOrdner(heute)}`);
178
+ for (const g of gesicherte) {
179
+ if (g.nebenVorhandener) {
180
+ const bisherige = sicherungsZiel(heute, g.pfad).split("/").pop();
181
+ const neue = g.ziel.split("/").pop();
182
+ console.log(` β€ž${g.pfad}": Von heute gibt es schon eine Sicherung (${bisherige}) -- die bleibt ` +
183
+ `unveraendert stehen. Deine neuere Fassung sichere ich zusaetzlich als ${neue}, ` +
184
+ "bevor ich die Datei im Ordner ersetze.");
185
+ }
186
+ else {
187
+ console.log(` β€ž${g.pfad}" -> ${g.ziel.split("/").pop()}`);
188
+ }
189
+ }
190
+ }
191
+ if (geschrieben.length > 0) {
192
+ console.log(`βœ“ aktualisiert: ${geschrieben.join(", ")}`);
193
+ }
194
+ // Β§8: Ein Fehlerfall darf nie denselben Namen tragen wie ein Erfolgsfall --
195
+ // "nichts geschrieben" heisst hier DREI verschiedene Dinge, und die Ausgabe
196
+ // muss sie unterscheiden, nicht nur die Rueckgabe (alle drei geben 0).
197
+ if (unberuehrt.length > 0) {
198
+ console.log(`Ich fasse nichts an -- du hast lokal geaendert: ${unberuehrt.join(", ")}`);
199
+ // πŸ”΄ Der Rat haengt an der DATEI, nicht nur an der Rolle -- s.
200
+ // `ratNachUnberuehrt` (kontext-stand.ts), Review-Befund F-3.
201
+ for (const zeile of ratNachUnberuehrt(unberuehrt, daten.rolle, daten.adminName)) {
202
+ console.log(zeile);
203
+ }
204
+ }
205
+ if (geschrieben.length === 0 && unberuehrt.length === 0) {
206
+ const alleLeer = VERWALTETE_DATEIEN.every((p) => (daten.merkzettel.dateien[p]?.stand ?? LEER) === LEER);
207
+ if (alleLeer) {
208
+ console.log("Deine Firma hat noch nichts hinterlegt. Bis dahin bleiben die Vordrucke stehen.");
209
+ }
210
+ else {
211
+ console.log("Dein Firmenkontext ist aktuell.");
212
+ }
213
+ }
214
+ return 0;
215
+ }
216
+ /** Prueft die Antwort-Form, ohne `any` -- eine falsche Form ist unlesbar, keine Ausnahme. */
217
+ function alsKontextAntwort(koerper) {
218
+ if (!koerper || typeof koerper !== "object")
219
+ return null;
220
+ const k = koerper;
221
+ // πŸ”΄ Task 10a: `firma.id` ist die Quelle, aus der `kontextRunter` gleich
222
+ // den lokalen Merkzettel stempelt -- s. Kommentar an der Schreibstelle.
223
+ const firmaRoh = k.firma;
224
+ if (!firmaRoh || typeof firmaRoh !== "object")
225
+ return null;
226
+ const firmaId = firmaRoh.id;
227
+ if (typeof firmaId !== "string" || firmaId.trim() === "")
228
+ return null;
229
+ const rolle = k.rolle;
230
+ if (rolle !== "admin" && rolle !== "member")
231
+ return null;
232
+ const adminName = k.admin_name;
233
+ if (adminName !== null && typeof adminName !== "string")
234
+ return null;
235
+ const dateienRoh = k.dateien;
236
+ if (!Array.isArray(dateienRoh))
237
+ return null;
238
+ const dateien = [];
239
+ for (const eintrag of dateienRoh) {
240
+ if (!eintrag || typeof eintrag !== "object")
241
+ return null;
242
+ const p = eintrag.pfad;
243
+ const i = eintrag.inhalt;
244
+ if (typeof p !== "string" || p.trim() === "")
245
+ return null;
246
+ if (typeof i !== "string")
247
+ return null;
248
+ dateien.push({ pfad: p, inhalt: i });
249
+ }
250
+ const merkzettel = alsMerkzettel(k.merkzettel);
251
+ if (merkzettel === null)
252
+ return null;
253
+ return { firma: { id: firmaId }, rolle, adminName, dateien, merkzettel };
254
+ }
@@ -84,6 +84,55 @@ export const BEFEHLE = [
84
84
  "Gib deinen Vornamen mit β€” der Ordner trΓ€gt ihn: aus sabine wird werkbank-sabine. Ohne Angabe bildet der Server ihn aus deiner Anmelde-Adresse. So sieht man spΓ€ter, wem welche Werkbank gehΓΆrt.",
85
85
  ],
86
86
  },
87
+ /*
88
+ πŸ”΄ Auch diese zwei Eintraege stehen direkt NEBEN `werkbank neu` und nicht
89
+ am Ende der Liste, aus demselben Grund wie oben bei `arbeitsplatz neu`:
90
+ Wer hier liest, sucht den naechsten Schritt, nicht das Alphabet. Wer
91
+ einen KI-Arbeitsplatz gerade angelegt oder betreten hat, fragt sich als
92
+ Naechstes, wie der Firmenkontext (CLAUDE.md + kontext/*.md) aktuell
93
+ bleibt -- nicht, wie man ein Projekt eintraegt.
94
+ */
95
+ {
96
+ befehl: "kimal3 kontext runter",
97
+ beschreibung: "Den Firmenkontext holen β€” CLAUDE.md und die fΓΌnf Dateien in kontext/",
98
+ wer: "du",
99
+ rhythmus: "wann immer die Firma etwas geΓ€ndert haben kΓΆnnte",
100
+ tippen: "! kimal3 kontext runter",
101
+ terminal: "kimal3 kontext runter",
102
+ details: [
103
+ "Holt CLAUDE.md und die fΓΌnf Dateien in kontext/ von deiner Firma und legt sie in deinem KI-Arbeitsplatz ab. Was du selbst lokal geΓ€ndert hast, fasst er nicht an β€” er sagt dir, wie du es einbringst.",
104
+ "Hast du eine Datei lokal geΓ€ndert UND hat die Firma sie inzwischen auch geΓ€ndert, sichert er deine Fassung zuerst nach kontext/vorher-<datum>/, bevor er sie ersetzt β€” nichts geht verloren.",
105
+ "Geklappt hat es, wenn er die aktualisierten Dateien nennt oder β€žDein Firmenkontext ist aktuellβ€œ sagt.",
106
+ "⚠️ Wie werkbank neu braucht er ein eigenes Pro-Konto. Ohne das kommt RΓΌckgabewert 4 β€” dann nicht wiederholen.",
107
+ ],
108
+ },
109
+ {
110
+ befehl: "kimal3 kontext fragen",
111
+ beschreibung: "Zeigen, welche Fragen ΓΌber deine Firma noch unbeantwortet sind",
112
+ wer: "du",
113
+ rhythmus: "bevor ihr den Firmenkontext ausfΓΌllt β€” und wann immer du wissen willst, was fehlt",
114
+ tippen: "! kimal3 kontext fragen",
115
+ terminal: "kimal3 kontext fragen",
116
+ details: [
117
+ "Liest die fΓΌnf Dateien in kontext/ und gibt jede Frage aus, unter der noch nichts steht β€” mit Datei, Frage und Hinweis. Sind alle beantwortet, sagt er das in einem Satz.",
118
+ "Er liest nur: kein Netz, keine Anmeldung, kein Schreiben. Deshalb ist er der richtige erste Schritt β€” Claude stellt dir die Fragen dann einzeln und trΓ€gt deine Antworten in die Dateien ein.",
119
+ "Danach schickt kimal3 kontext hoch sie an die Firma. Das darf nur der Admin; alle anderen geben ihre Antworten an ihn weiter.",
120
+ ],
121
+ },
122
+ {
123
+ befehl: "kimal3 kontext hoch",
124
+ beschreibung: "Deine lokalen Γ„nderungen an den fΓΌnf Firmen-Texten hochschicken",
125
+ wer: "du",
126
+ rhythmus: "nachdem du kontext/*.md bearbeitet hast β€” nur als Admin",
127
+ tippen: "! kimal3 kontext hoch",
128
+ terminal: "kimal3 kontext hoch",
129
+ details: [
130
+ "Schickt alle fΓΌnf Firmen-Texte an deine Firma β€” die Dateien in kontext/ (kurzprofil.md, firma.md, marke.md, systeme.md, sicherheit.md). Damit kommen sie beim nΓ€chsten kontext runter auch bei den Kolleginnen und Kollegen an.",
131
+ "πŸ”΄ Nur der Firmen-Admin darf das. Bist du keiner, kommt RΓΌckgabewert 1 β€” das ist hier kein β€žnochmal versuchenβ€œ, sondern β€žgib deine ErgΓ€nzung an den Admin weiterβ€œ.",
132
+ "Sieht eine der fΓΌnf Dateien nicht mehr aus wie ihre Frageliste β€” etwa weil etwas Fremdes darΓΌberkopiert wurde β€”, bricht er ab und schickt gar nichts, auch die vier anderen nicht. kimal3 kontext runter holt die Datei zurΓΌck.",
133
+ "Geklappt hat es, wenn er β€žβœ“ hochgeschicktβ€œ mit den Dateinamen meldet.",
134
+ ],
135
+ },
87
136
  {
88
137
  befehl: "kimal3 projekt neu …",
89
138
  beschreibung: "Ein Projekt ins Erfolgsprofil eintragen",
@@ -0,0 +1,178 @@
1
+ /*
2
+ Die Fragen aus einer Kontext-Datei lesen -- die reine Rechnung hinter
3
+ `kimal3 kontext fragen` und hinter dem Abbruch von `kimal3 kontext hoch`.
4
+
5
+ πŸ”‘ WOHER DIE FRAGEN KOMMEN -- die eine Entscheidung dieser Datei:
6
+ **Aus der Datei selbst, nicht aus einem Katalog.** Jede ausgelieferte
7
+ Kontext-Datei traegt ihre Fragen bereits vollstaendig bei sich: einen Anker
8
+ (`<!-- k3:marke_anrede -->`), darunter die Frage als `## `-Ueberschrift,
9
+ darunter unsere Hinweiszeilen mit `>`, darunter die Antwort des Menschen.
10
+ Das Format beschreibt sich selbst.
11
+
12
+ πŸ”΄ WARUM DAS NICHT NUR BEQUEM, SONDERN DAS EINZIG EHRLICHE IST: Die CLI kann
13
+ nichts aus `src/` importieren (eigenes Paket, eigenes tsconfig -- s. Kopf von
14
+ `kontext-stand.ts`). Ein Katalog in `cli/src/` waere eine ZWEITE Fassung der
15
+ 17 Fragen, und sie friert in dem Moment ein, in dem `npm publish` laeuft: Der
16
+ Kunde hat dann eine CLI von gestern und Dateien von heute. Die Datei auf
17
+ seiner Platte kommt dagegen IMMER vom Server -- sie ist die einzige Quelle,
18
+ die nicht gegenueber dem abweichen kann, was er wirklich vor sich hat.
19
+
20
+ ⚠️ DIESE DATEI IMPORTIERT NICHTS -- dieselbe Auflage wie `kontext-stand.ts`.
21
+ Nur so kann `scripts/check-kontext.mts` sie neben `FRAGEN` (aus der App) in
22
+ denselben Lauf holen und beide gegeneinander messen.
23
+
24
+ πŸ“Œ Der Preis, ausgesprochen: Wer den ANKER einer Frage aus seiner Datei
25
+ loescht, die Ueberschrift aber stehen laesst, hat eine Frage, die hier nicht
26
+ mehr auftaucht. Die App faengt diesen Fall ab (sie erkennt die Frage am
27
+ Wortlaut) und setzt den Anker beim naechsten Speichern wieder ein. Die
28
+ Ausgabe von `kontext fragen` sagt deshalb "Fragen in deinen Dateien" und
29
+ nicht "alle Fragen deiner Firma" -- sie behauptet nur, was sie messen kann.
30
+ */
31
+ /**
32
+ * `<!-- k3:anrede -->` bzw. `<!-- k3:unwoerter optional -->`.
33
+ *
34
+ * ⚠️ Woertlich dasselbe Muster wie `ANKER_MUSTER` in
35
+ * `src/app/firma/kontext-zerlegen.ts`. Es steht hier ein zweites Mal, weil
36
+ * diese Datei nichts importieren darf (s. Kopf) -- dass beide dasselbe meinen,
37
+ * misst `check:kontext`, indem er die ECHTEN Vorlagen durch beide Seiten
38
+ * schickt und die Ergebnisse vergleicht.
39
+ */
40
+ const ANKER_MUSTER = /^<!--\s*k3:([a-z0-9_]+)(\s+optional)?\s*-->$/;
41
+ function istHinweis(zeile) {
42
+ return zeile.trimStart().startsWith(">");
43
+ }
44
+ function istUeberschrift(zeile) {
45
+ return zeile.startsWith("## ");
46
+ }
47
+ /**
48
+ * Alle Fragen, die in `text` stehen -- in der Reihenfolge der Datei.
49
+ *
50
+ * Der Durchgang ist zeilenweise und haelt drei Dinge auseinander:
51
+ * 1. **Anker** -- er merkt sich, wessen Frage als Naechstes kommt.
52
+ * 2. **Ueberschrift** -- steht ein Anker offen, beginnt hier seine Frage.
53
+ * 3. **Darunter** -- die unmittelbar folgenden `>`-Zeilen sind UNSER Hinweis,
54
+ * alles Weitere bis zur naechsten Ueberschrift ist die Antwort.
55
+ *
56
+ * πŸ”‘ DER HINWEIS ZAEHLT NUR UNMITTELBAR UNTER DER UEBERSCHRIFT -- genau wie in
57
+ * `scripts/arbeitsplatz-inhalt.mts`, wo derselbe Katalog fuer die App entsteht.
58
+ * Sonst kaeme eine `>`-Zeile, die ein Mensch mitten in seine Antwort setzt,
59
+ * als unser Hinweis heraus.
60
+ *
61
+ * πŸ”΄ AUS DER ANTWORT FALLEN TROTZDEM ALLE `>`-ZEILEN HERAUS, auch spaetere --
62
+ * das ist die Regel, nach der auch die App zerlegt (`zerlege`). Ein Zitatblock
63
+ * gehoert uns; ginge er als Antwort durch, stuende unser eigener Satz als
64
+ * Aussage der Firma in der Datenbank.
65
+ *
66
+ * ⚠️ Ein Anker OHNE Ueberschrift darunter zaehlt nicht -- er wird vom naechsten
67
+ * Anker oder von der naechsten Ueberschrift ueberschrieben. *Ein Anker, der auf
68
+ * eine Ueberschrift drei Absaetze weiter zeigt, ist kein Anker, sondern ein
69
+ * Zufall, der aussieht wie Absicht.*
70
+ */
71
+ export function fragenAus(text) {
72
+ const zeilen = text.split(/\r?\n/);
73
+ const fragen = [];
74
+ let offenerAnker = null;
75
+ let aktuell = null;
76
+ let hinweisNochOffen = false;
77
+ const hinweise = [];
78
+ const antwort = [];
79
+ /** Die laufende Frage abschliessen -- Hinweis und Antwort festschreiben. */
80
+ const schliesse = () => {
81
+ if (aktuell === null)
82
+ return;
83
+ aktuell.hinweis = hinweise.join(" ").trim();
84
+ aktuell.antwort = antwort.join("\n").trim();
85
+ aktuell = null;
86
+ hinweise.length = 0;
87
+ antwort.length = 0;
88
+ };
89
+ for (const zeile of zeilen) {
90
+ const treffer = ANKER_MUSTER.exec(zeile.trim());
91
+ if (treffer) {
92
+ schliesse();
93
+ offenerAnker = { anker: treffer[1], pflicht: !treffer[2] };
94
+ continue;
95
+ }
96
+ if (istUeberschrift(zeile)) {
97
+ schliesse();
98
+ if (offenerAnker) {
99
+ aktuell = {
100
+ anker: offenerAnker.anker,
101
+ ueberschrift: zeile.slice(3).trim(),
102
+ hinweis: "",
103
+ pflicht: offenerAnker.pflicht,
104
+ antwort: "",
105
+ };
106
+ hinweisNochOffen = true;
107
+ fragen.push(aktuell);
108
+ }
109
+ offenerAnker = null;
110
+ continue;
111
+ }
112
+ /*
113
+ πŸ”΄ **RULING K-14: EIN ANKER, UNTER DEM NICHT UNMITTELBAR SEINE FRAGE
114
+ STEHT, ZAEHLT NICHT.** Leerzeilen sind erlaubt, Text nicht. Ohne diese
115
+ drei Zeilen machte ein verwaister Anker die naechstbeste Ueberschrift zur
116
+ Frage -- und `kimal3 kontext fragen` stellte einem Menschen eine Frage,
117
+ die niemand geschrieben hat. Dieselbe Regel wie im Erzeuger
118
+ (`frageZeileNach` in `scripts/arbeitsplatz-inhalt.mts`).
119
+
120
+ ⚠️ Die App ist an dieser Stelle NACHSICHTIGER: `zerlege` haengt einen
121
+ verwaisten Anker an die naechste Ueberschrift, wie weit sie auch weg ist.
122
+ Der Unterschied ist gewollt und folgenlos, solange keine ausgelieferte
123
+ Datei einen verwaisten Anker hat -- und genau das prueft
124
+ `check:arbeitsplatz` fuer jede Vorlage.
125
+ */
126
+ if (offenerAnker && zeile.trim() !== "")
127
+ offenerAnker = null;
128
+ if (aktuell === null)
129
+ continue;
130
+ if (istHinweis(zeile)) {
131
+ // Nur der Block DIREKT unter der Ueberschrift ist unser Hinweis;
132
+ // spaetere `>`-Zeilen fallen weg, gehoeren aber auch nicht zur Antwort.
133
+ if (hinweisNochOffen)
134
+ hinweise.push(zeile.trimStart().slice(1).trim());
135
+ continue;
136
+ }
137
+ // ⚠️ SCHON EINE LEERZEILE BEENDET DEN HINWEISBLOCK -- woertlich dieselbe
138
+ // Grenze wie im Erzeuger (`katalogAus` in `scripts/arbeitsplatz-inhalt.mts`).
139
+ // Zoege sie hier weiter, kaeme aus einer Datei mit einem Zitat weiter unten
140
+ // ein anderer Hinweis heraus als in der App, und `check:kontext` haette
141
+ // recht, wenn er rot wird.
142
+ hinweisNochOffen = false;
143
+ antwort.push(zeile);
144
+ }
145
+ schliesse();
146
+ return fragen;
147
+ }
148
+ /**
149
+ * Sieht dieser Text aus wie die Datei, die er sein soll?
150
+ *
151
+ * πŸ”΄ **DER ABBRUCH VON `kimal3 kontext hoch` HAENGT AN DIESER ZEILE** (Spec,
152
+ * Abschnitt 11). Ohne sie schickt der Befehl JEDEN Dateiinhalt hoch, den er an
153
+ * der Stelle findet. Die App zerlegt ihn dann gegen den Katalog DIESER Datei,
154
+ * findet keine einzige Antwort -- und legt den kompletten Text als "Zusatz"
155
+ * ab. Die Firma haette danach einen Zusatz statt eines Firmentextes, und die
156
+ * CLI haette dazu einen Haken gemeldet. *Ein Fehlerfall darf nie denselben
157
+ * Namen tragen wie ein Erfolgsfall.*
158
+ *
159
+ * πŸ”‘ **WORAN ERKANNT WIRD: an mindestens einem Anker, der zu DIESER Datei
160
+ * gehoert.** Die Anker sind je Datei benannt (`marke_anrede`, `firma_angebot`,
161
+ * `kurz_kunden` …), und `FIRMEN_TEXTE` (`kontext-stand.ts`) fuehrt je Datei
162
+ * ihren Praefix. Damit faellt nicht nur die voellig fremde Datei durch, sondern
163
+ * auch der haeufigere Unfall: `marke.md` ueber `firma.md` kopiert. Ein blosses
164
+ * "irgendein Anker steht drin" haette genau den durchgewunken.
165
+ *
166
+ * ⚠️ **Dass die Praefixe wirklich passen, ist keine Verabredung, sondern eine
167
+ * Messung:** `check:kontext` haelt jeden Anker aus `FRAGEN` (der App) gegen den
168
+ * Praefix, den `FIRMEN_TEXTE` fuer seine Datei nennt.
169
+ *
170
+ * πŸ“Œ **Der Preis, ausgesprochen:** Wer ALLE Anker einer Datei loescht und nur
171
+ * die Ueberschriften stehen laesst, bekommt hier ein Nein, obwohl die App den
172
+ * Text noch am Wortlaut erkennen wuerde. Das ist die sichere Seite: Es geht
173
+ * nichts verloren, die Meldung sagt warum, und `kimal3 kontext runter` stellt
174
+ * die Datei wieder her. Die andere Seite waere der stille Datenverlust oben.
175
+ */
176
+ export function traegtFragenVon(text, ankerPraefix) {
177
+ return fragenAus(text).some((f) => f.anker.startsWith(ankerPraefix));
178
+ }
package/dist/kimal3.js CHANGED
@@ -21,6 +21,9 @@ import { hilfeText } from "./befehle-katalog.js";
21
21
  import { arbeitsplatzNeu } from "./befehle/arbeitsplatz.js";
22
22
  import { werkbankNeu } from "./befehle/werkbank.js";
23
23
  import { firmaDaten } from "./befehle/firma-daten.js";
24
+ import { kontextFragen } from "./befehle/kontext-fragen.js";
25
+ import { kontextRunter } from "./befehle/kontext-runter.js";
26
+ import { kontextHoch } from "./befehle/kontext-hoch.js";
24
27
  import { login } from "./befehle/login.js";
25
28
  import { logout } from "./befehle/logout.js";
26
29
  import { projektListe } from "./befehle/projekt-liste.js";
@@ -279,6 +282,12 @@ async function haupt() {
279
282
  return arbeitsplatzNeu(argv.slice(2));
280
283
  if (argv[0] === "werkbank" && argv[1] === "neu")
281
284
  return werkbankNeu(argv.slice(2));
285
+ if (argv[0] === "kontext" && argv[1] === "runter")
286
+ return kontextRunter(argv.slice(2));
287
+ if (argv[0] === "kontext" && argv[1] === "hoch")
288
+ return kontextHoch(argv.slice(2));
289
+ if (argv[0] === "kontext" && argv[1] === "fragen")
290
+ return kontextFragen(argv.slice(2));
282
291
  console.error(`Den Befehl β€ž${wort}" kenne ich nicht.\n${HILFE}`);
283
292
  return 1;
284
293
  }