@cachly-dev/mcp-server 0.10.125 → 0.10.126

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,179 @@
1
+ /**
2
+ * Messtechnik fuer den Rueckfall-Versuch (dev.to "rules-before-the-run").
3
+ *
4
+ * ── Worum es geht ────────────────────────────────────────────────────────────
5
+ *
6
+ * Der Artikel sagt zu: wir messen, ob eine abgerufene Lektion, die WIRKLICH bis
7
+ * ins Modell kommt, den Rueckfall senkt (dieselbe Lektion muss kein zweites Mal
8
+ * geschrieben werden). Diese Datei baut nur das Messgeraet. Sie startet nichts.
9
+ *
10
+ * Ohne den Schalter `CACHLY_VERSUCH=an` (siehe `versuchStart`) aendert sich am
11
+ * Verhalten des Recall-Pfads NICHTS — kein Zurueckhalten, kein Schreiben nach
12
+ * Redis, keine Markierung im Text. Das ist keine Nebenbedingung, das ist der
13
+ * ganze Zweck: der Versuch soll erst beginnen, wenn Heinrich ihn startet.
14
+ *
15
+ * ── Der Kanarienvogel ────────────────────────────────────────────────────────
16
+ *
17
+ * Zu pruefen ist nicht, ob eine Lektion ABGERUFEN wurde. Zu pruefen ist, ob ihr
18
+ * Text VOLLSTAENDIG in der Antwort landet, die das Modell sieht. Anlass: eine
19
+ * Lektion trug die gesuchte Adresse an Zeichen 323 von `what_worked`, die
20
+ * Anzeige schnitt bei 100 Zeichen ab (siehe lesson-preview.ts). Die Lektion war
21
+ * eingeblendet, der Fehler passierte trotzdem — genau der Zustand, den ein
22
+ * "wurde abgerufen"-Zaehler nie sieht.
23
+ *
24
+ * Die Markierung sitzt deshalb am Ende von `what_worked` — dem Feld, an dem der
25
+ * Anlass hing, und dem Feld, das `lessonPreviewLines` als Erstes und am
26
+ * grosszuegigsten kuerzt. Sie besteht aus acht Zero-Width-Space-Zeichen
27
+ * (U+200B): unsichtbar in jedem Renderer (Terminal, Markdown, HTML), und sie
28
+ * uebersteht sowohl `flat()`s Leerzeichen-Kollaps als auch `String#trim()` —
29
+ * beide fassen nur "richtige" Leerzeichen an, U+200B gehoert zu keiner der
30
+ * beiden Kategorien. Acht Wiederholungen, damit ein einzelnes zufaelliges
31
+ * Zero-Width-Zeichen in echtem Nutzertext die Markierung nicht vortaeuscht.
32
+ *
33
+ * WICHTIG: die Markierung wird NICHT roh ans Ende des JSON-Strings gehaengt.
34
+ * Das wuerde `JSON.parse` in `recallVorschau` brechen und den Fallback-Pfad
35
+ * (rohes `trimTo` ueber das GANZE JSON-Objekt) erzwingen — einen Pfad, den
36
+ * echte, unmarkierte Lektionen nie durchlaufen. Das Ergebnis waere ein
37
+ * Fehlalarm, kein Messwert. Die Markierung wird darum in die geparste
38
+ * `what_worked`-Zeichenkette eingefuegt, das Objekt bleibt gueltiges JSON, und
39
+ * der Kanarienvogel durchlaeuft exakt den Pfad, den eine echte Lektion auch
40
+ * durchlaeuft.
41
+ *
42
+ * Geprueft wird am Ende die WIRKLICH zusammengebaute Antwort (`lines.join`),
43
+ * nicht eine Nebenrechnung — siehe `schliesseVersuchAb`. Die Markierung wird
44
+ * danach aus dem Text entfernt, bevor er den Nutzer erreicht.
45
+ */
46
+ import type { Redis } from 'ioredis';
47
+ export declare const VERSUCH_ENV = "CACHLY_VERSUCH";
48
+ export declare const VERSUCH_SALZ_ENV = "CACHLY_VERSUCH_SALZ";
49
+ /** Grund fuer `meldeEinmal`: Versuch eingeschaltet, aber kein Salz gesetzt. */
50
+ export declare const OHNE_SALZ = "versuch-ohne-salz";
51
+ /**
52
+ * Der einzige Einstiegspunkt fuer die Frage "soll der Versuch jetzt laufen?".
53
+ *
54
+ * Gibt das Salz zurueck, wenn ja — sonst `null`. Zwei Faelle enden bei `null`:
55
+ * der Schalter steht nicht auf "an", oder er steht auf "an", aber das Salz
56
+ * fehlt. Der zweite Fall meldet sich einmal je Prozess nach stderr (ueber
57
+ * `meldeEinmal`) und verhaelt sich danach wie der erste — still weiterlaufen
58
+ * waere die Fehlerklasse, gegen die `aussetzer.ts` gebaut wurde.
59
+ */
60
+ export declare function versuchStart(): string | null;
61
+ /**
62
+ * Eine neue, stabile Kennung je Recall-Aufruf.
63
+ *
64
+ * Im Recall-Pfad gibt es keine vorhandene Anfrage- oder Turn-Kennung, die man
65
+ * verlaengern koennte — `requester` ist ein Autorenname, `cachly:session:current`
66
+ * haelt Zustand je INSTANZ, nicht je Aufruf. Darum: Zeitstempel (sortierbar,
67
+ * in einer Zeile lesbar) plus vier zufaellige Byte, damit zwei Aufrufe in
68
+ * derselben Millisekunde nicht kollidieren.
69
+ */
70
+ export declare function neueKennung(): string;
71
+ export type Zuteilung = 'HOLD' | 'DELIVER';
72
+ /**
73
+ * Reine Funktion, ohne Netz testbar: Kennung + Salz -> HOLD oder DELIVER.
74
+ *
75
+ * Berechnung exakt wie oeffentlich angekuendigt: Kennung und Salz aneinander-
76
+ * haengen, SHA-256, die ersten VIER Hex-Ziffern als Zahl lesen, modulo 100.
77
+ * Unter 50 heisst HOLD, sonst DELIVER.
78
+ */
79
+ export declare function zuteilung(kennung: string, salz: string): Zuteilung;
80
+ /** Ohne einen Treffer ueber dieser Punktzahl gibt es nichts zu messen. */
81
+ export declare const ZULASSUNGS_SCHWELLE = 0;
82
+ /** Gibt es ueberhaupt einen Treffer, der ueber der Schwelle liegt? */
83
+ export declare function istZulassungsfaehig(hoechstePunktzahl: number, schwelle?: number): boolean;
84
+ /** Der Grund, warum eine Anfrage NICHT zulassungsfaehig ist — oder `undefined`. */
85
+ export declare function zulassungsGrund(hoechstePunktzahl: number, schwelle?: number): string | undefined;
86
+ /**
87
+ * Acht Zero-Width-Spaces. Siehe Dateikopf fuer die Begruendung der Wahl.
88
+ *
89
+ * Ueber String.fromCharCode, nicht als roher Unicode-Buchstabe im
90
+ * Quelltext — ein unsichtbares Zeichen zwischen Anfuehrungszeichen ist beim
91
+ * Lesen und in `git diff` nicht von "kein Zeichen" zu unterscheiden.
92
+ */
93
+ export declare const KANARIEN_MARKIERUNG: string;
94
+ /**
95
+ * Setzt die Kanarien-Markierung ans Ende des `what_worked`-Feldes.
96
+ *
97
+ * `inhalt` ist der rohe Redis-Wert, wie ihn `recallVorschau` bekommt — meist
98
+ * das JSON einer Lektion, manchmal (Kontext-Eintraege, Index) einfacher Text.
99
+ * Ist es eine Lektion mit `what_worked`, wandert die Markierung dorthin und
100
+ * das Objekt bleibt gueltiges JSON (Begruendung siehe Dateikopf). Sonst wird
101
+ * roh ans Ende angehaengt — das ist fuer Nicht-Lektionen korrekt, weil
102
+ * `recallVorschau` sie ohnehin nur als Rohtext kuerzt.
103
+ */
104
+ export declare function markiereDatensatz(inhalt: string, markierung?: string): string;
105
+ /**
106
+ * Ueberlebt die Markierung in der fertig zusammengebauten Antwort?
107
+ *
108
+ * Ueberlebt sie: der ganze Datensatz kam durch. Fehlt sie: er wurde gekuerzt.
109
+ */
110
+ export declare function kanarienUeberlebt(fertigeAntwort: string, markierung?: string): boolean;
111
+ /** Entfernt die Markierung, bevor der Text den Nutzer erreicht. */
112
+ export declare function entferneMarkierung(text: string, markierung?: string): string;
113
+ /** Redis-Schluessel einer Lektion -> ihr Themenname (ohne Praefix). */
114
+ export declare function themaVon(key: string): string;
115
+ /** Hash der Frage — die Frage selbst wird nie gespeichert (Zusage des Artikels). */
116
+ export declare function hashFrage(frage: string): string;
117
+ /** Redis-Schluessel fuer den Protokoll-Eintrag einer Kennung. */
118
+ export declare function turnSchluessel(kennung: string): string;
119
+ export interface VersuchTurnProtokoll {
120
+ ts: string;
121
+ frageHash: string;
122
+ hoechstePunktzahl: number;
123
+ /** Themenschluessel der zurueckgegebenen (tatsaechlich gezeigten) Lektionen. */
124
+ themen: string[];
125
+ zulassungsfaehig: boolean;
126
+ zulassungsGrund?: string;
127
+ zuteilung: Zuteilung;
128
+ /** Nur bei HOLD gesetzt: das Thema, das weggelassen wurde. */
129
+ weggelassenesThema?: string;
130
+ kanarienBestanden: boolean;
131
+ }
132
+ /**
133
+ * Schreibt den Turn-Eintrag. Schlaegt das fehl, wird NICHTS geworfen — der
134
+ * Nutzer wartet auf eine Antwort, nicht auf ein Protokoll.
135
+ */
136
+ export declare function schreibeTurnProtokoll(redis: Redis, kennung: string, protokoll: VersuchTurnProtokoll): Promise<void>;
137
+ /** Ein Kandidat aus der schon sortierten Trefferliste von smart_recall. */
138
+ export interface VersuchTreffer {
139
+ key: string;
140
+ content: string;
141
+ punktzahl: number;
142
+ }
143
+ export interface ZuteilungsErgebnis {
144
+ /** Die neue Trefferliste — bei HOLD fehlt der bisher beste Treffer. */
145
+ treffer: VersuchTreffer[];
146
+ zuteilung: Zuteilung;
147
+ zulassungsfaehig: boolean;
148
+ zulassungsGrund?: string;
149
+ /** War DELIVER UND es gab einen Treffer, der die Markierung tragen konnte. */
150
+ markierungGesetzt: boolean;
151
+ weggelassenesThema?: string;
152
+ }
153
+ /**
154
+ * Wendet Zulassung + Zuteilung auf eine Trefferliste an. Reine Funktion,
155
+ * mutiert `treffer` nicht.
156
+ *
157
+ * Ohne zulassungsfaehigen Treffer passiert nichts — Zuteilung wird trotzdem
158
+ * berechnet und protokolliert, aber es gibt nichts zurueckzuhalten oder zu
159
+ * markieren. Bei HOLD verschwindet der beste Treffer aus der Liste, die
160
+ * uebrigen bleiben unveraendert. Bei DELIVER traegt der beste Treffer danach
161
+ * die Kanarien-Markierung.
162
+ */
163
+ export declare function wendeZuteilungAn(treffer: readonly VersuchTreffer[], kennung: string, salz: string, hoechstePunktzahl: number, schwelle?: number): ZuteilungsErgebnis;
164
+ export interface VersuchProtokollTeil {
165
+ frage: string;
166
+ hoechstePunktzahl: number;
167
+ themen: string[];
168
+ zulassungsfaehig: boolean;
169
+ zulassungsGrund?: string;
170
+ zuteilung: Zuteilung;
171
+ weggelassenesThema?: string;
172
+ markierungGesetzt: boolean;
173
+ }
174
+ /**
175
+ * Letzter Schritt: prueft den Kanarienvogel gegen die WIRKLICH zusammen-
176
+ * gebaute Antwort, entfernt die Markierung, schreibt das Protokoll (fire-and-
177
+ * forget) und gibt den bereinigten Text zurueck — den, den der Nutzer sieht.
178
+ */
179
+ export declare function schliesseVersuchAb(redis: Redis, kennung: string, teil: VersuchProtokollTeil, fertigeAntwort: string): string;
@@ -0,0 +1,250 @@
1
+ /**
2
+ * Messtechnik fuer den Rueckfall-Versuch (dev.to "rules-before-the-run").
3
+ *
4
+ * ── Worum es geht ────────────────────────────────────────────────────────────
5
+ *
6
+ * Der Artikel sagt zu: wir messen, ob eine abgerufene Lektion, die WIRKLICH bis
7
+ * ins Modell kommt, den Rueckfall senkt (dieselbe Lektion muss kein zweites Mal
8
+ * geschrieben werden). Diese Datei baut nur das Messgeraet. Sie startet nichts.
9
+ *
10
+ * Ohne den Schalter `CACHLY_VERSUCH=an` (siehe `versuchStart`) aendert sich am
11
+ * Verhalten des Recall-Pfads NICHTS — kein Zurueckhalten, kein Schreiben nach
12
+ * Redis, keine Markierung im Text. Das ist keine Nebenbedingung, das ist der
13
+ * ganze Zweck: der Versuch soll erst beginnen, wenn Heinrich ihn startet.
14
+ *
15
+ * ── Der Kanarienvogel ────────────────────────────────────────────────────────
16
+ *
17
+ * Zu pruefen ist nicht, ob eine Lektion ABGERUFEN wurde. Zu pruefen ist, ob ihr
18
+ * Text VOLLSTAENDIG in der Antwort landet, die das Modell sieht. Anlass: eine
19
+ * Lektion trug die gesuchte Adresse an Zeichen 323 von `what_worked`, die
20
+ * Anzeige schnitt bei 100 Zeichen ab (siehe lesson-preview.ts). Die Lektion war
21
+ * eingeblendet, der Fehler passierte trotzdem — genau der Zustand, den ein
22
+ * "wurde abgerufen"-Zaehler nie sieht.
23
+ *
24
+ * Die Markierung sitzt deshalb am Ende von `what_worked` — dem Feld, an dem der
25
+ * Anlass hing, und dem Feld, das `lessonPreviewLines` als Erstes und am
26
+ * grosszuegigsten kuerzt. Sie besteht aus acht Zero-Width-Space-Zeichen
27
+ * (U+200B): unsichtbar in jedem Renderer (Terminal, Markdown, HTML), und sie
28
+ * uebersteht sowohl `flat()`s Leerzeichen-Kollaps als auch `String#trim()` —
29
+ * beide fassen nur "richtige" Leerzeichen an, U+200B gehoert zu keiner der
30
+ * beiden Kategorien. Acht Wiederholungen, damit ein einzelnes zufaelliges
31
+ * Zero-Width-Zeichen in echtem Nutzertext die Markierung nicht vortaeuscht.
32
+ *
33
+ * WICHTIG: die Markierung wird NICHT roh ans Ende des JSON-Strings gehaengt.
34
+ * Das wuerde `JSON.parse` in `recallVorschau` brechen und den Fallback-Pfad
35
+ * (rohes `trimTo` ueber das GANZE JSON-Objekt) erzwingen — einen Pfad, den
36
+ * echte, unmarkierte Lektionen nie durchlaufen. Das Ergebnis waere ein
37
+ * Fehlalarm, kein Messwert. Die Markierung wird darum in die geparste
38
+ * `what_worked`-Zeichenkette eingefuegt, das Objekt bleibt gueltiges JSON, und
39
+ * der Kanarienvogel durchlaeuft exakt den Pfad, den eine echte Lektion auch
40
+ * durchlaeuft.
41
+ *
42
+ * Geprueft wird am Ende die WIRKLICH zusammengebaute Antwort (`lines.join`),
43
+ * nicht eine Nebenrechnung — siehe `schliesseVersuchAb`. Die Markierung wird
44
+ * danach aus dem Text entfernt, bevor er den Nutzer erreicht.
45
+ */
46
+ import { randomBytes, createHash } from 'node:crypto';
47
+ import { meldeEinmal } from './aussetzer.js';
48
+ // ── Der Schalter ─────────────────────────────────────────────────────────────
49
+ export const VERSUCH_ENV = 'CACHLY_VERSUCH';
50
+ export const VERSUCH_SALZ_ENV = 'CACHLY_VERSUCH_SALZ';
51
+ /** Grund fuer `meldeEinmal`: Versuch eingeschaltet, aber kein Salz gesetzt. */
52
+ export const OHNE_SALZ = 'versuch-ohne-salz';
53
+ /**
54
+ * Der einzige Einstiegspunkt fuer die Frage "soll der Versuch jetzt laufen?".
55
+ *
56
+ * Gibt das Salz zurueck, wenn ja — sonst `null`. Zwei Faelle enden bei `null`:
57
+ * der Schalter steht nicht auf "an", oder er steht auf "an", aber das Salz
58
+ * fehlt. Der zweite Fall meldet sich einmal je Prozess nach stderr (ueber
59
+ * `meldeEinmal`) und verhaelt sich danach wie der erste — still weiterlaufen
60
+ * waere die Fehlerklasse, gegen die `aussetzer.ts` gebaut wurde.
61
+ */
62
+ export function versuchStart() {
63
+ if (process.env[VERSUCH_ENV] !== 'an')
64
+ return null;
65
+ const salz = process.env[VERSUCH_SALZ_ENV];
66
+ if (!salz) {
67
+ meldeEinmal(OHNE_SALZ, `${VERSUCH_ENV}=an, aber ${VERSUCH_SALZ_ENV} fehlt — der Versuch verhaelt sich wie "aus"`);
68
+ return null;
69
+ }
70
+ return salz;
71
+ }
72
+ // ── Turn-Kennung ─────────────────────────────────────────────────────────────
73
+ /**
74
+ * Eine neue, stabile Kennung je Recall-Aufruf.
75
+ *
76
+ * Im Recall-Pfad gibt es keine vorhandene Anfrage- oder Turn-Kennung, die man
77
+ * verlaengern koennte — `requester` ist ein Autorenname, `cachly:session:current`
78
+ * haelt Zustand je INSTANZ, nicht je Aufruf. Darum: Zeitstempel (sortierbar,
79
+ * in einer Zeile lesbar) plus vier zufaellige Byte, damit zwei Aufrufe in
80
+ * derselben Millisekunde nicht kollidieren.
81
+ */
82
+ export function neueKennung() {
83
+ const zeit = new Date().toISOString().replace(/[-:.]/g, '').slice(0, 15); // 20260820T171523
84
+ const zufall = randomBytes(4).toString('hex');
85
+ return `${zeit}-${zufall}`;
86
+ }
87
+ /**
88
+ * Reine Funktion, ohne Netz testbar: Kennung + Salz -> HOLD oder DELIVER.
89
+ *
90
+ * Berechnung exakt wie oeffentlich angekuendigt: Kennung und Salz aneinander-
91
+ * haengen, SHA-256, die ersten VIER Hex-Ziffern als Zahl lesen, modulo 100.
92
+ * Unter 50 heisst HOLD, sonst DELIVER.
93
+ */
94
+ export function zuteilung(kennung, salz) {
95
+ const hash = createHash('sha256').update(`${kennung}${salz}`).digest('hex');
96
+ const ersteVier = hash.slice(0, 4);
97
+ const zahl = parseInt(ersteVier, 16) % 100;
98
+ return zahl < 50 ? 'HOLD' : 'DELIVER';
99
+ }
100
+ // ── Zulassung ────────────────────────────────────────────────────────────────
101
+ /** Ohne einen Treffer ueber dieser Punktzahl gibt es nichts zu messen. */
102
+ export const ZULASSUNGS_SCHWELLE = 0;
103
+ /** Gibt es ueberhaupt einen Treffer, der ueber der Schwelle liegt? */
104
+ export function istZulassungsfaehig(hoechstePunktzahl, schwelle = ZULASSUNGS_SCHWELLE) {
105
+ return hoechstePunktzahl > schwelle;
106
+ }
107
+ /** Der Grund, warum eine Anfrage NICHT zulassungsfaehig ist — oder `undefined`. */
108
+ export function zulassungsGrund(hoechstePunktzahl, schwelle = ZULASSUNGS_SCHWELLE) {
109
+ if (istZulassungsfaehig(hoechstePunktzahl, schwelle))
110
+ return undefined;
111
+ return `hoechste Punktzahl ${hoechstePunktzahl.toFixed(3)} liegt nicht ueber der Schwelle ${schwelle}`;
112
+ }
113
+ // ── Kanarienvogel ────────────────────────────────────────────────────────────
114
+ /**
115
+ * Acht Zero-Width-Spaces. Siehe Dateikopf fuer die Begruendung der Wahl.
116
+ *
117
+ * Ueber String.fromCharCode, nicht als roher Unicode-Buchstabe im
118
+ * Quelltext — ein unsichtbares Zeichen zwischen Anfuehrungszeichen ist beim
119
+ * Lesen und in `git diff` nicht von "kein Zeichen" zu unterscheiden.
120
+ */
121
+ export const KANARIEN_MARKIERUNG = String.fromCharCode(0x200b).repeat(8);
122
+ /**
123
+ * Setzt die Kanarien-Markierung ans Ende des `what_worked`-Feldes.
124
+ *
125
+ * `inhalt` ist der rohe Redis-Wert, wie ihn `recallVorschau` bekommt — meist
126
+ * das JSON einer Lektion, manchmal (Kontext-Eintraege, Index) einfacher Text.
127
+ * Ist es eine Lektion mit `what_worked`, wandert die Markierung dorthin und
128
+ * das Objekt bleibt gueltiges JSON (Begruendung siehe Dateikopf). Sonst wird
129
+ * roh ans Ende angehaengt — das ist fuer Nicht-Lektionen korrekt, weil
130
+ * `recallVorschau` sie ohnehin nur als Rohtext kuerzt.
131
+ */
132
+ export function markiereDatensatz(inhalt, markierung = KANARIEN_MARKIERUNG) {
133
+ try {
134
+ const objekt = JSON.parse(inhalt);
135
+ if (typeof objekt.what_worked === 'string') {
136
+ objekt.what_worked = `${objekt.what_worked}${markierung}`;
137
+ return JSON.stringify(objekt);
138
+ }
139
+ }
140
+ catch {
141
+ // kein JSON — Markierung ans Ende des Rohtexts, siehe unten
142
+ }
143
+ return `${inhalt}${markierung}`;
144
+ }
145
+ /**
146
+ * Ueberlebt die Markierung in der fertig zusammengebauten Antwort?
147
+ *
148
+ * Ueberlebt sie: der ganze Datensatz kam durch. Fehlt sie: er wurde gekuerzt.
149
+ */
150
+ export function kanarienUeberlebt(fertigeAntwort, markierung = KANARIEN_MARKIERUNG) {
151
+ return fertigeAntwort.includes(markierung);
152
+ }
153
+ /** Entfernt die Markierung, bevor der Text den Nutzer erreicht. */
154
+ export function entferneMarkierung(text, markierung = KANARIEN_MARKIERUNG) {
155
+ return text.split(markierung).join('');
156
+ }
157
+ // ── Themenschluessel ─────────────────────────────────────────────────────────
158
+ const LEKTION_PRAEFIX = 'cachly:lesson:best:';
159
+ /** Redis-Schluessel einer Lektion -> ihr Themenname (ohne Praefix). */
160
+ export function themaVon(key) {
161
+ return key.startsWith(LEKTION_PRAEFIX) ? key.slice(LEKTION_PRAEFIX.length) : key;
162
+ }
163
+ // ── Protokoll je Turn ────────────────────────────────────────────────────────
164
+ /** Hash der Frage — die Frage selbst wird nie gespeichert (Zusage des Artikels). */
165
+ export function hashFrage(frage) {
166
+ return createHash('sha256').update(frage).digest('hex');
167
+ }
168
+ const TURN_TTL_SEKUNDEN = 90 * 24 * 60 * 60; // 90 Tage — der Versuch dauert 14
169
+ /** Redis-Schluessel fuer den Protokoll-Eintrag einer Kennung. */
170
+ export function turnSchluessel(kennung) {
171
+ return `cachly:versuch:turn:${kennung}`;
172
+ }
173
+ /**
174
+ * Schreibt den Turn-Eintrag. Schlaegt das fehl, wird NICHTS geworfen — der
175
+ * Nutzer wartet auf eine Antwort, nicht auf ein Protokoll.
176
+ */
177
+ export async function schreibeTurnProtokoll(redis, kennung, protokoll) {
178
+ try {
179
+ await redis.set(turnSchluessel(kennung), JSON.stringify(protokoll), 'EX', TURN_TTL_SEKUNDEN);
180
+ }
181
+ catch {
182
+ // Ein fehlendes Protokoll ist besser als eine gescheiterte Suche.
183
+ }
184
+ }
185
+ /**
186
+ * Wendet Zulassung + Zuteilung auf eine Trefferliste an. Reine Funktion,
187
+ * mutiert `treffer` nicht.
188
+ *
189
+ * Ohne zulassungsfaehigen Treffer passiert nichts — Zuteilung wird trotzdem
190
+ * berechnet und protokolliert, aber es gibt nichts zurueckzuhalten oder zu
191
+ * markieren. Bei HOLD verschwindet der beste Treffer aus der Liste, die
192
+ * uebrigen bleiben unveraendert. Bei DELIVER traegt der beste Treffer danach
193
+ * die Kanarien-Markierung.
194
+ */
195
+ export function wendeZuteilungAn(treffer, kennung, salz, hoechstePunktzahl, schwelle = ZULASSUNGS_SCHWELLE) {
196
+ const zulassungsfaehig = istZulassungsfaehig(hoechstePunktzahl, schwelle);
197
+ const grund = zulassungsGrund(hoechstePunktzahl, schwelle);
198
+ const entscheidung = zuteilung(kennung, salz);
199
+ if (!zulassungsfaehig || treffer.length === 0) {
200
+ return {
201
+ treffer: [...treffer],
202
+ zuteilung: entscheidung,
203
+ zulassungsfaehig,
204
+ zulassungsGrund: grund,
205
+ markierungGesetzt: false,
206
+ };
207
+ }
208
+ const [bester, ...rest] = treffer;
209
+ if (entscheidung === 'HOLD') {
210
+ return {
211
+ treffer: rest,
212
+ zuteilung: entscheidung,
213
+ zulassungsfaehig,
214
+ zulassungsGrund: grund,
215
+ markierungGesetzt: false,
216
+ weggelassenesThema: themaVon(bester.key),
217
+ };
218
+ }
219
+ const markiert = { ...bester, content: markiereDatensatz(bester.content) };
220
+ return {
221
+ treffer: [markiert, ...rest],
222
+ zuteilung: entscheidung,
223
+ zulassungsfaehig,
224
+ zulassungsGrund: grund,
225
+ markierungGesetzt: true,
226
+ };
227
+ }
228
+ /**
229
+ * Letzter Schritt: prueft den Kanarienvogel gegen die WIRKLICH zusammen-
230
+ * gebaute Antwort, entfernt die Markierung, schreibt das Protokoll (fire-and-
231
+ * forget) und gibt den bereinigten Text zurueck — den, den der Nutzer sieht.
232
+ */
233
+ export function schliesseVersuchAb(redis, kennung, teil, fertigeAntwort) {
234
+ const kanarienBestanden = teil.markierungGesetzt ? kanarienUeberlebt(fertigeAntwort) : false;
235
+ const bereinigt = entferneMarkierung(fertigeAntwort);
236
+ const protokoll = {
237
+ ts: new Date().toISOString(),
238
+ frageHash: hashFrage(teil.frage),
239
+ hoechstePunktzahl: teil.hoechstePunktzahl,
240
+ themen: teil.themen,
241
+ zulassungsfaehig: teil.zulassungsfaehig,
242
+ zulassungsGrund: teil.zulassungsGrund,
243
+ zuteilung: teil.zuteilung,
244
+ weggelassenesThema: teil.weggelassenesThema,
245
+ kanarienBestanden,
246
+ };
247
+ void schreibeTurnProtokoll(redis, kennung, protokoll);
248
+ return bereinigt;
249
+ }
250
+ //# sourceMappingURL=versuch.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"versuch.js","sourceRoot":"","sources":["../../src/versuch.ts"],"names":[],"mappings":"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"}
package/glama.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "$schema": "https://glama.ai/mcp/schemas/server.json",
3
- "version": "0.10.125",
3
+ "version": "0.10.126",
4
4
  "maintainers": [
5
5
  "HeinrichNebula"
6
6
  ],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cachly-dev/mcp-server",
3
- "version": "0.10.125",
3
+ "version": "0.10.126",
4
4
  "mcpName": "io.github.cachly-dev/mcp-server",
5
5
  "description": "Your AI forgets everything between sessions. cachly fixes that permanently: 122 MCP tools for Claude Code, Cursor, Copilot & Windsurf. Learns from every bug fix, commit and CI run. Arrives pre-briefed. Free tier forever, EU servers, GDPR.",
6
6
  "type": "module",
package/server.json CHANGED
@@ -6,13 +6,13 @@
6
6
  "url": "https://github.com/cachly-dev/cachly-mcp",
7
7
  "source": "github"
8
8
  },
9
- "version": "0.10.125",
9
+ "version": "0.10.126",
10
10
  "packages": [
11
11
  {
12
12
  "registryType": "npm",
13
13
  "name": "@cachly-dev/mcp-server",
14
14
  "identifier": "@cachly-dev/mcp-server",
15
- "version": "0.10.125",
15
+ "version": "0.10.126",
16
16
  "transport": {
17
17
  "type": "stdio"
18
18
  }