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.
- package/README.md +3 -0
- package/dist/api.js +77 -0
- package/dist/befehle/kontext-fragen.js +165 -0
- package/dist/befehle/kontext-gemeinsam.js +171 -0
- package/dist/befehle/kontext-hoch.js +203 -0
- package/dist/befehle/kontext-runter.js +254 -0
- package/dist/befehle-katalog.js +49 -0
- package/dist/fragen-lesen.js +178 -0
- package/dist/kimal3.js +9 -0
- package/dist/kontext-dateien.js +248 -0
- package/dist/kontext-stand.js +616 -0
- package/dist/nachricht.js +51 -0
- package/package.json +1 -1
- package/skill/SKILL.md +70 -11
|
@@ -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
|
+
}
|
package/dist/befehle-katalog.js
CHANGED
|
@@ -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
|
}
|