@cachly-dev/mcp-server 0.10.129 → 0.10.131

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (223) hide show
  1. package/README.md +2 -2
  2. package/dist/index.js +1 -7
  3. package/dist/packages/telegram-notify/client.js +2 -75
  4. package/dist/src/ambient-cli.js +7 -253
  5. package/dist/src/ambient-deps.js +1 -55
  6. package/dist/src/ambient-hooks.js +5 -278
  7. package/dist/src/ambient-ledger.js +6 -95
  8. package/dist/src/ambient-memory.js +1 -67
  9. package/dist/src/ambient-recall.js +1 -221
  10. package/dist/src/aussetzer.js +1 -225
  11. package/dist/src/auth.js +9 -140
  12. package/dist/src/autor-abzeichen.js +1 -56
  13. package/dist/src/autor.js +1 -136
  14. package/dist/src/bauabdruck.js +1 -93
  15. package/dist/src/bedeutung.js +2 -323
  16. package/dist/src/bench/cachly-bench.js +4 -249
  17. package/dist/src/bench/external-corpus.js +4 -135
  18. package/dist/src/bench/fixtures.js +1 -225
  19. package/dist/src/brain-export.js +16 -119
  20. package/dist/src/brain-watch-hook.js +8 -86
  21. package/dist/src/cachly-url.js +1 -21
  22. package/dist/src/ckg.js +1 -108
  23. package/dist/src/cls-hook.js +8 -94
  24. package/dist/src/confidence.js +1 -50
  25. package/dist/src/credentials.js +1 -85
  26. package/dist/src/deutsch.js +1 -223
  27. package/dist/src/doctor.js +2 -248
  28. package/dist/src/editor.js +1 -63
  29. package/dist/src/eingaenge-nachruesten.js +1 -290
  30. package/dist/src/eingaenge.js +2 -260
  31. package/dist/src/embeddings.js +13 -223
  32. package/dist/src/feldreparatur.js +3 -113
  33. package/dist/src/first-contact.js +4 -108
  34. package/dist/src/first-recall-proof.js +4 -41
  35. package/dist/src/funnel-milestones.js +2 -48
  36. package/dist/src/geliefert.js +1 -114
  37. package/dist/src/handlers/advanced.js +38 -537
  38. package/dist/src/handlers/brain.js +69 -4397
  39. package/dist/src/handlers/cache.js +59 -638
  40. package/dist/src/handlers/context.js +17 -182
  41. package/dist/src/handlers/fedbrain.js +40 -1436
  42. package/dist/src/handlers/instances.js +16 -201
  43. package/dist/src/handlers/roadmap.js +13 -175
  44. package/dist/src/handlers/share.js +19 -530
  45. package/dist/src/handlers/syndicate.js +38 -904
  46. package/dist/src/handlers/team.js +89 -1865
  47. package/dist/src/handlers/viz.js +3 -148
  48. package/dist/src/index.js +321 -4281
  49. package/dist/src/interferenz.js +1 -141
  50. package/dist/src/kostprobe.js +7 -125
  51. package/dist/src/lesson-preview.js +2 -133
  52. package/dist/src/namespace.js +1 -128
  53. package/dist/src/notifier.js +1 -6
  54. package/dist/src/rangfolge-stellschrauben.js +1 -0
  55. package/dist/src/rangfolge.js +1 -249
  56. package/dist/src/raumkorrektur.js +1 -197
  57. package/dist/src/recall-tiefe.js +1 -42
  58. package/dist/src/rerank.js +1 -154
  59. package/dist/src/resonanz.js +1 -140
  60. package/dist/src/search.js +1 -2104
  61. package/dist/src/seltenheitsbestand.js +1 -92
  62. package/dist/src/speicherlage.js +1 -84
  63. package/dist/src/spuren.js +1 -150
  64. package/dist/src/starter-corpus.js +1 -165
  65. package/dist/src/telemetry-types.js +0 -13
  66. package/dist/src/tools.js +10 -2465
  67. package/dist/src/toolspecs.js +1 -121
  68. package/dist/src/upgrade-nudge.js +1 -29
  69. package/dist/src/utils.js +1 -89
  70. package/dist/src/versuch.js +1 -264
  71. package/dist/src/vorspann.js +1 -146
  72. package/dist/src/wartung/vektoren-verdichten.js +1 -200
  73. package/dist/src/werkzeug-nutzung.js +1 -72
  74. package/dist/src/wertbeitrag.js +1 -203
  75. package/dist/src/wirkungsspur.js +1 -0
  76. package/glama.json +1 -1
  77. package/package.json +8 -7
  78. package/server.json +3 -3
  79. package/dist/index.d.ts +0 -1
  80. package/dist/index.js.map +0 -1
  81. package/dist/packages/telegram-notify/client.d.ts +0 -19
  82. package/dist/packages/telegram-notify/client.js.map +0 -1
  83. package/dist/src/ambient-cli.d.ts +0 -101
  84. package/dist/src/ambient-cli.js.map +0 -1
  85. package/dist/src/ambient-deps.d.ts +0 -26
  86. package/dist/src/ambient-deps.js.map +0 -1
  87. package/dist/src/ambient-hooks.d.ts +0 -137
  88. package/dist/src/ambient-hooks.js.map +0 -1
  89. package/dist/src/ambient-ledger.d.ts +0 -19
  90. package/dist/src/ambient-ledger.js.map +0 -1
  91. package/dist/src/ambient-memory.d.ts +0 -12
  92. package/dist/src/ambient-memory.js.map +0 -1
  93. package/dist/src/ambient-recall.d.ts +0 -127
  94. package/dist/src/ambient-recall.js.map +0 -1
  95. package/dist/src/aussetzer.d.ts +0 -156
  96. package/dist/src/aussetzer.js.map +0 -1
  97. package/dist/src/auth.d.ts +0 -43
  98. package/dist/src/auth.js.map +0 -1
  99. package/dist/src/autor-abzeichen.d.ts +0 -41
  100. package/dist/src/autor-abzeichen.js.map +0 -1
  101. package/dist/src/autor.d.ts +0 -101
  102. package/dist/src/autor.js.map +0 -1
  103. package/dist/src/bauabdruck-daten.generiert.json +0 -5
  104. package/dist/src/bauabdruck.d.ts +0 -38
  105. package/dist/src/bauabdruck.js.map +0 -1
  106. package/dist/src/bedeutung.d.ts +0 -168
  107. package/dist/src/bedeutung.js.map +0 -1
  108. package/dist/src/bench/cachly-bench.d.ts +0 -61
  109. package/dist/src/bench/cachly-bench.js.map +0 -1
  110. package/dist/src/bench/external-corpus.d.ts +0 -44
  111. package/dist/src/bench/external-corpus.js.map +0 -1
  112. package/dist/src/bench/fixtures.d.ts +0 -37
  113. package/dist/src/bench/fixtures.js.map +0 -1
  114. package/dist/src/brain-export.d.ts +0 -64
  115. package/dist/src/brain-export.js.map +0 -1
  116. package/dist/src/brain-watch-hook.d.ts +0 -21
  117. package/dist/src/brain-watch-hook.js.map +0 -1
  118. package/dist/src/cachly-url.d.ts +0 -68
  119. package/dist/src/cachly-url.js.map +0 -1
  120. package/dist/src/ckg.d.ts +0 -54
  121. package/dist/src/ckg.js.map +0 -1
  122. package/dist/src/cls-hook.d.ts +0 -15
  123. package/dist/src/cls-hook.js.map +0 -1
  124. package/dist/src/confidence.d.ts +0 -19
  125. package/dist/src/confidence.js.map +0 -1
  126. package/dist/src/credentials.d.ts +0 -37
  127. package/dist/src/credentials.js.map +0 -1
  128. package/dist/src/deutsch.d.ts +0 -98
  129. package/dist/src/deutsch.js.map +0 -1
  130. package/dist/src/doctor.d.ts +0 -47
  131. package/dist/src/doctor.js.map +0 -1
  132. package/dist/src/editor.d.ts +0 -9
  133. package/dist/src/editor.js.map +0 -1
  134. package/dist/src/eingaenge-nachruesten.d.ts +0 -41
  135. package/dist/src/eingaenge-nachruesten.js.map +0 -1
  136. package/dist/src/eingaenge.d.ts +0 -159
  137. package/dist/src/eingaenge.js.map +0 -1
  138. package/dist/src/embeddings.d.ts +0 -48
  139. package/dist/src/embeddings.js.map +0 -1
  140. package/dist/src/feldreparatur.d.ts +0 -58
  141. package/dist/src/feldreparatur.js.map +0 -1
  142. package/dist/src/first-contact.d.ts +0 -56
  143. package/dist/src/first-contact.js.map +0 -1
  144. package/dist/src/first-recall-proof.d.ts +0 -19
  145. package/dist/src/first-recall-proof.js.map +0 -1
  146. package/dist/src/funnel-milestones.d.ts +0 -5
  147. package/dist/src/funnel-milestones.js.map +0 -1
  148. package/dist/src/geliefert.d.ts +0 -63
  149. package/dist/src/geliefert.js.map +0 -1
  150. package/dist/src/handlers/advanced.d.ts +0 -6
  151. package/dist/src/handlers/advanced.js.map +0 -1
  152. package/dist/src/handlers/brain.d.ts +0 -41
  153. package/dist/src/handlers/brain.js.map +0 -1
  154. package/dist/src/handlers/cache.d.ts +0 -6
  155. package/dist/src/handlers/cache.js.map +0 -1
  156. package/dist/src/handlers/context.d.ts +0 -6
  157. package/dist/src/handlers/context.js.map +0 -1
  158. package/dist/src/handlers/fedbrain.d.ts +0 -18
  159. package/dist/src/handlers/fedbrain.js.map +0 -1
  160. package/dist/src/handlers/instances.d.ts +0 -33
  161. package/dist/src/handlers/instances.js.map +0 -1
  162. package/dist/src/handlers/roadmap.d.ts +0 -6
  163. package/dist/src/handlers/roadmap.js.map +0 -1
  164. package/dist/src/handlers/share.d.ts +0 -8
  165. package/dist/src/handlers/share.js.map +0 -1
  166. package/dist/src/handlers/syndicate.d.ts +0 -6
  167. package/dist/src/handlers/syndicate.js.map +0 -1
  168. package/dist/src/handlers/team.d.ts +0 -45
  169. package/dist/src/handlers/team.js.map +0 -1
  170. package/dist/src/handlers/viz.d.ts +0 -6
  171. package/dist/src/handlers/viz.js.map +0 -1
  172. package/dist/src/index.d.ts +0 -93
  173. package/dist/src/index.js.map +0 -1
  174. package/dist/src/interferenz.d.ts +0 -111
  175. package/dist/src/interferenz.js.map +0 -1
  176. package/dist/src/kostprobe.d.ts +0 -25
  177. package/dist/src/kostprobe.js.map +0 -1
  178. package/dist/src/lesson-preview.d.ts +0 -81
  179. package/dist/src/lesson-preview.js.map +0 -1
  180. package/dist/src/namespace.d.ts +0 -20
  181. package/dist/src/namespace.js.map +0 -1
  182. package/dist/src/notifier.d.ts +0 -5
  183. package/dist/src/notifier.js.map +0 -1
  184. package/dist/src/rangfolge.d.ts +0 -205
  185. package/dist/src/rangfolge.js.map +0 -1
  186. package/dist/src/raumkorrektur.d.ts +0 -100
  187. package/dist/src/raumkorrektur.js.map +0 -1
  188. package/dist/src/recall-tiefe.d.ts +0 -48
  189. package/dist/src/recall-tiefe.js.map +0 -1
  190. package/dist/src/rerank.d.ts +0 -48
  191. package/dist/src/rerank.js.map +0 -1
  192. package/dist/src/resonanz.d.ts +0 -85
  193. package/dist/src/resonanz.js.map +0 -1
  194. package/dist/src/search.d.ts +0 -153
  195. package/dist/src/search.js.map +0 -1
  196. package/dist/src/seltenheitsbestand.d.ts +0 -53
  197. package/dist/src/seltenheitsbestand.js.map +0 -1
  198. package/dist/src/speicherlage.d.ts +0 -51
  199. package/dist/src/speicherlage.js.map +0 -1
  200. package/dist/src/spuren.d.ts +0 -97
  201. package/dist/src/spuren.js.map +0 -1
  202. package/dist/src/starter-corpus.d.ts +0 -28
  203. package/dist/src/starter-corpus.js.map +0 -1
  204. package/dist/src/telemetry-types.d.ts +0 -87
  205. package/dist/src/telemetry-types.js.map +0 -1
  206. package/dist/src/tools.d.ts +0 -2933
  207. package/dist/src/tools.js.map +0 -1
  208. package/dist/src/toolspecs.d.ts +0 -43
  209. package/dist/src/toolspecs.js.map +0 -1
  210. package/dist/src/upgrade-nudge.d.ts +0 -15
  211. package/dist/src/upgrade-nudge.js.map +0 -1
  212. package/dist/src/utils.d.ts +0 -34
  213. package/dist/src/utils.js.map +0 -1
  214. package/dist/src/versuch.d.ts +0 -187
  215. package/dist/src/versuch.js.map +0 -1
  216. package/dist/src/vorspann.d.ts +0 -73
  217. package/dist/src/vorspann.js.map +0 -1
  218. package/dist/src/wartung/vektoren-verdichten.d.ts +0 -50
  219. package/dist/src/wartung/vektoren-verdichten.js.map +0 -1
  220. package/dist/src/werkzeug-nutzung.d.ts +0 -62
  221. package/dist/src/werkzeug-nutzung.js.map +0 -1
  222. package/dist/src/wertbeitrag.d.ts +0 -148
  223. package/dist/src/wertbeitrag.js.map +0 -1
@@ -1,249 +1 @@
1
- /**
2
- * Der zusammengesetzte Sortierer.
3
- *
4
- * ── Was hier drinsteckt, und was NICHT ──────────────────────────────────────
5
- *
6
- * Am 19.08.2026 wurden an 499 echten Lektionen mit 100 Fragen ein Dutzend
7
- * Verfahren durchgemessen. Die meisten haben nichts gebracht. Sie stehen hier,
8
- * damit sie niemand ein zweites Mal baut:
9
- *
10
- * Symptom-Sicht (what_failed als eigener Vektor) 17 % statt 40 % — das Feld
11
- * beschreibt bei uns nicht das Symptom, sondern was vergeblich versucht wurde
12
- * Schwerpunkt abziehen 41 % — im Rauschen
13
- * Hauptrichtungen entfernen (1, 3, 6 Stueck) 37–41 % — im Rauschen
14
- * Nabenabzug (CSLS-artig) 41 % — im Rauschen
15
- * Text in Stuecke schneiden nicht gebaut: gemessen deckt
16
- * das beste Stueck genauso viel ab wie der ganze Text (Verhaeltnis 1,00),
17
- * und es liegt bei 86 von 96 Fragen ohnehin am Anfang
18
- *
19
- * Was BLEIBT, sind vier Bestandteile — kreuzweise geprueft, also auf der einen
20
- * Haelfte der Fragen eingestellt und auf der anderen gemessen, in beide
21
- * Richtungen:
22
- *
23
- * P@1 MRR
24
- * nur Bedeutung 40 % 51,3 %
25
- * zusammengesetzt 51 % 57,0 %
26
- *
27
- * ── Die vier Bestandteile ───────────────────────────────────────────────────
28
- *
29
- * 1. NAEHE ZUM GANZEN TEXT (Sicht A). Der Grundstock.
30
- *
31
- * 2. NAEHE ZUM THEMENNAMEN (Sicht C). Der Name ist kurz wie eine Frage — der
32
- * Vergleich ist damit symmetrisch, waehrend eine 60-Zeichen-Frage gegen
33
- * 1376 Zeichen Text es nicht ist. Allein liefert diese Sicht nur 26 %, im
34
- * Verbund traegt sie verlaesslich (Gewicht 0,6 in beiden Richtungen).
35
- *
36
- * 3. RUECKKOPPLUNG. Die ersten drei Treffer sind meistens ungefaehr richtig.
37
- * Ihre Vektoren werden zu einem Viertel unter die Frage gemischt, dann wird
38
- * erneut verglichen. Kostet nichts — keine neue Einbettung, nur eine
39
- * Mittelung vorhandener Zahlen.
40
- *
41
- * 4. SELTENHEITS-UEBERLAPPUNG. Welcher Anteil der SELTENEN Fragewoerter kommt
42
- * im Text vor. Ohne Laengennormierung, ohne unscharfe Treffer, ohne
43
- * Bigramme.
44
- *
45
- * Das ist der unbequeme Befund: dieses eine Verhaeltnis bekam beim Einstellen
46
- * das Gewicht 1,0 bis 1,6 — unser ausgefeiltes BM25 mit Laengennormierung,
47
- * Bigramm-Naehe, unscharfen Treffern und Frischebonus bekam NULL. Nicht weil
48
- * Woerter nichts taugen, sondern weil das ganze Beiwerk beim Sortieren
49
- * daneben misst.
50
- *
51
- * ── Warum innerhalb des Topfes normiert wird ────────────────────────────────
52
- *
53
- * Ueber den ganzen Bestand liegen alle Aehnlichkeiten zwischen 0,42 und 0,61 —
54
- * ein sehr schmales Band, weil alle Vektoren eine gemeinsame Grundrichtung
55
- * teilen (gemessen: 69,8 Prozent). Unter fuenfzig Kandidaten spreizt eine
56
- * Normierung auf 0 bis 1 genau die Unterschiede auf, um die es geht. Das ist
57
- * dasselbe Ziel wie beim Schwerpunkt-Abziehen — nur dort, wo es wirkt.
58
- */
59
- import { kosinus } from './bedeutung.js';
60
- /** Wörter eines Textes, umlautfrei, ohne Kurzwörter. */
61
- export function inhaltsWoerter(text) {
62
- return new Set(text.toLowerCase()
63
- .replace(/ä/g, 'ae').replace(/ö/g, 'oe').replace(/ü/g, 'ue').replace(/ß/g, 'ss')
64
- .split(/[^a-z0-9]+/)
65
- .filter((w) => w.length > 3));
66
- }
67
- /**
68
- * Es gibt hier KEINE Stammbildung — und das ist gemessen, nicht vergessen.
69
- *
70
- * Naheliegend wäre, `Abhaengigkeiten` und `Abhaengigkeit` auf einen Nenner zu
71
- * bringen. Drei Varianten wurden am 19.08.2026 an 499 Lektionen mit 100 Fragen
72
- * kreuzweise durchgemessen:
73
- *
74
- * ohne Stammbildung P@1 50 % MRR 56,4 %
75
- * erste 6 Zeichen P@1 47 % MRR 54,7 %
76
- * erste 8 Zeichen P@1 47 % MRR 54,7 %
77
- *
78
- * Jede Form von Kürzen kostet drei Punkte. Der Grund liegt daran, was diese
79
- * Suche trägt: seltene Fachwörter. `fail2ban`, `stampede`, `timestamptz`,
80
- * `keycloak` entscheiden eine Frage praktisch allein — und jede Verschmelzung
81
- * verwischt genau sie. `container` und `contains` werden zu demselben Wort,
82
- * und aus zwei seltenen wird ein häufiges.
83
- *
84
- * Beugung einzufangen lohnt sich also nur, wo die häufigen Wörter zählen. Hier
85
- * zählen die seltenen.
86
- *
87
- * (Die Wortform-Behandlung in deutsch.ts bleibt davon unberührt — sie wirkt im
88
- * Wortabgleich, der die Vorauswahl macht, nicht in dieser Bewertung.)
89
- */
90
- export function grobStamm(w) {
91
- return w;
92
- }
93
- /**
94
- * Die Seltenheit jedes Wortes im Bestand — einmal berechnet, oft benutzt.
95
- *
96
- * Trifft eine Frage das Wort "nicht", sagt das nichts. Trifft sie "fail2ban",
97
- * ist die Sache praktisch entschieden. Ohne diese Gewichtung sind beide gleich
98
- * viel wert.
99
- */
100
- export class Seltenheit {
101
- df = new Map();
102
- anzahl = 0;
103
- /** @param texte je ein Text pro Datensatz */
104
- constructor(texte) {
105
- this.anzahl = texte.length;
106
- for (const t of texte) {
107
- for (const w of inhaltsWoerter(t))
108
- this.df.set(grobStamm(w), (this.df.get(grobStamm(w)) ?? 0) + 1);
109
- }
110
- }
111
- wert(wort) {
112
- return Math.log((this.anzahl + 1) / ((this.df.get(grobStamm(wort)) ?? 0) + 1));
113
- }
114
- /**
115
- * Welcher Anteil der seltenen Fragewörter kommt im Text vor.
116
- *
117
- * Der Nenner ist die Summe über ALLE Fragewörter — ein fehlendes seltenes
118
- * Wort zieht damit den Wert herunter. Fehlende Belege zählen also mit, nicht
119
- * nur vorhandene.
120
- */
121
- deckung(frageWoerter, textWoerter) {
122
- let gesamt = 0;
123
- let getroffen = 0;
124
- for (const w of frageWoerter) {
125
- const g = this.wert(w);
126
- gesamt += g;
127
- if (textWoerter.has(grobStamm(w)))
128
- getroffen += g;
129
- }
130
- return gesamt > 0 ? getroffen / gesamt : 0;
131
- }
132
- }
133
- /** Auf 0 bis 1 spreizen — innerhalb des Topfes, nicht über den Bestand. */
134
- export function spreizeImTopf(werte) {
135
- const gueltig = werte.filter((x) => Number.isFinite(x) && x > -2);
136
- if (gueltig.length === 0)
137
- return werte.map(() => 0);
138
- let min = Infinity;
139
- let max = -Infinity;
140
- for (const x of gueltig) {
141
- if (x < min)
142
- min = x;
143
- if (x > max)
144
- max = x;
145
- }
146
- if (max === min)
147
- return werte.map(() => 0.5);
148
- return werte.map((x) => (x > -2 ? (x - min) / (max - min) : 0));
149
- }
150
- /**
151
- * Die Frage mit ihren besten Treffern anreichern.
152
- *
153
- * Die Frage benutzt Alltagsworte, die Lektionen Fachworte. Die ersten Treffer
154
- * schlagen diese Brücke: ihre Vektoren zeigen dorthin, wo die Antwort liegt.
155
- *
156
- * Das Risiko ist bekannt und begrenzt: sind die ersten Treffer falsch, zieht
157
- * die Anreicherung in die falsche Richtung. Deshalb bleibt die ursprüngliche
158
- * Frage mit drei Vierteln stehen — gemessen war das besser als jede stärkere
159
- * Beimischung.
160
- */
161
- export function reichereAn(frage, besteTreffer, anteil = 0.25) {
162
- if (besteTreffer.length === 0)
163
- return frage;
164
- const d = frage.length;
165
- const m = new Array(d).fill(0);
166
- for (const v of besteTreffer) {
167
- if (v.length !== d)
168
- continue;
169
- for (let i = 0; i < d; i++)
170
- m[i] += v[i] / besteTreffer.length;
171
- }
172
- return frage.map((x, i) => (1 - anteil) * x + anteil * m[i]);
173
- }
174
- /**
175
- * Die Gewichte.
176
- *
177
- * Kreuzweise ermittelt: auf der einen Hälfte der Fragen eingestellt, auf der
178
- * anderen gemessen, in beide Richtungen. Die drei Werte hier waren in BEIDEN
179
- * Richtungen stabil. Zwei weitere Merkmale — Überlappung mit dem Themennamen
180
- * und Qualitätsmerkmale — schwankten zwischen den Richtungen und sind deshalb
181
- * NICHT dabei: was nur in einer Hälfte hilft, ist eine Eigenschaft dieser
182
- * Hälfte, keine des Verfahrens.
183
- */
184
- export const GEWICHTE = {
185
- /** Nähe zum ganzen Text. Der Grundstock, deshalb 1. */
186
- text: 1,
187
- /** Nähe zum Themennamen. In beiden Richtungen 0,6. */
188
- thema: 0.6,
189
- /** Angereicherte Frage gegen den ganzen Text. In beiden Richtungen 0,3. */
190
- rueckkopplung: 0.3,
191
- /** Anteil der seltenen Fragewörter im Text. 1,0 bis 1,6 — Mitte genommen. */
192
- seltenheit: 1.3,
193
- };
194
- /**
195
- * Punktzahl je Kandidat. Höher ist besser.
196
- *
197
- * Erwartet den GANZEN Topf auf einmal, weil die Normierung ihn braucht — eine
198
- * Punktzahl für einen einzelnen Kandidaten gibt es nicht und kann es nicht
199
- * geben.
200
- */
201
- export function bewerteTopf(topf, gewichte = GEWICHTE) {
202
- const t = spreizeImTopf(topf.map((k) => k.naeheText));
203
- const th = spreizeImTopf(topf.map((k) => k.naeheThema));
204
- const r = spreizeImTopf(topf.map((k) => k.naeheRueckkopplung));
205
- return topf.map((k, i) => gewichte.text * t[i]
206
- + gewichte.thema * th[i]
207
- + gewichte.rueckkopplung * r[i]
208
- + gewichte.seltenheit * k.seltenheitsDeckung);
209
- }
210
- /**
211
- * Punktzahl je Kandidat — STRENGE Fassung: Merkmale multiplizieren statt addieren.
212
- *
213
- * ── Das Vorbild ─────────────────────────────────────────────────────────────
214
- *
215
- * Kinetisches Korrekturlesen (Hopfield 1974, T-Zell-Rezeptoren): Biochemie
216
- * erreicht Fehlerraten weit unter dem, was ein einzelner Bindungsschritt
217
- * hergibt, indem sie Kandidaten durch eine KETTE von Pruefschritten schickt.
218
- * Bei jedem Schritt kann der Kandidat abfallen; die Trennschaerfe der Schritte
219
- * MULTIPLIZIERT sich, statt sich zu mitteln.
220
- *
221
- * ── Der Unterschied zur Addition ────────────────────────────────────────────
222
- *
223
- * In der Summe (bewerteTopf) kann ein Kandidat totale Schwaeche in einem
224
- * Merkmal mit Staerke in einem anderen voll ausgleichen. Im Produkt nicht:
225
- * eine Null in einem Merkmal drueckt die Gesamtnote, egal wie gut der Rest
226
- * ist. Das Produkt verlangt STIMMIGKEIT ueber alle Merkmale — die Summe
227
- * verlangt nur Masse.
228
- *
229
- * `basis` stellt die Strenge ein: grosse Basis naehert sich der Summe an,
230
- * kleine Basis macht eine Null toedlich. Rechnerisch ist das eine gewichtete
231
- * Summe im Logarithmus — der Unterschied zur Addition ist die begrenzte
232
- * Ausgleichbarkeit, nicht die Buchhaltung.
233
- *
234
- * Ergebnis der Messung vom 21.08.2026 steht in src/bench/fusion-vergleich.ts.
235
- */
236
- export function bewerteTopfStreng(topf, gewichte = GEWICHTE, basis = 0.3) {
237
- const t = spreizeImTopf(topf.map((k) => k.naeheText));
238
- const th = spreizeImTopf(topf.map((k) => k.naeheThema));
239
- const r = spreizeImTopf(topf.map((k) => k.naeheRueckkopplung));
240
- return topf.map((k, i) => gewichte.text * Math.log(basis + t[i])
241
- + gewichte.thema * Math.log(basis + th[i])
242
- + gewichte.rueckkopplung * Math.log(basis + r[i])
243
- + gewichte.seltenheit * Math.log(basis + k.seltenheitsDeckung));
244
- }
245
- /** Bequemlichkeit: Kosinus mit Lücken-Behandlung. */
246
- export function naeheOderLuecke(frage, vektor) {
247
- return frage && vektor && vektor.length > 0 ? kosinus(frage, vektor) : -2;
248
- }
249
- //# sourceMappingURL=rangfolge.js.map
1
+ import{kosinus as i}from"./bedeutung.js";function p(n){return new Set(n.toLowerCase().replace(/ä/g,"ae").replace(/ö/g,"oe").replace(/ü/g,"ue").replace(/ß/g,"ss").split(/[^a-z0-9]+/).filter(e=>e.length>3))}function g(n){return n}class m{df=new Map;anzahl=0;constructor(e){this.anzahl=e.length;for(const s of e)for(const r of p(s))this.df.set(r,(this.df.get(r)??0)+1)}wert(e){return Math.log((this.anzahl+1)/((this.df.get(e)??0)+1))}deckung(e,s){let r=0,l=0;for(const a of e){const o=this.wert(a);r+=o,s.has(a)&&(l+=o)}return r>0?l/r:0}besterZeuge(e,s){let r=0;const l=Math.log(this.anzahl+1);if(l<=0)return 0;for(const a of e){if(!s.has(a))continue;const o=this.wert(a)/l;o>r&&(r=o)}return r}}function u(n){const e=n.filter(t=>Number.isFinite(t)&&t>-2);if(e.length===0)return n.map(()=>0);let s=1/0,r=-1/0;for(const t of e)t<s&&(s=t),t>r&&(r=t);if(r===s)return n.map(()=>.5);const l=n.map(t=>Number.isFinite(t)&&t>-2?(t-s)/(r-s):-1);let a=0;for(const t of l)t>=0&&(a+=t);const o=a/e.length;return l.map(t=>t>=0?t:o)}function k(n,e,s=.25){if(e.length===0)return n;const r=n.length,l=new Array(r).fill(0);for(const a of e)if(a.length===r)for(let o=0;o<r;o++)l[o]+=a[o]/e.length;return n.map((a,o)=>(1-s)*a+s*l[o])}const c={text:1,thema:0,rueckkopplung:.3,seltenheit:1.3,zeuge:.5};function x(n,e=c){const s=u(n.map(t=>t.naeheText)),r=u(n.map(t=>t.naeheThema)),l=u(n.map(t=>t.naeheRueckkopplung)),a=u(n.map(t=>t.besterZeuge??0)),o=e.zeuge??0;return n.map((t,h)=>e.text*s[h]+e.thema*r[h]+e.rueckkopplung*l[h]+e.seltenheit*t.seltenheitsDeckung+o*a[h])}function z(n,e=c,s=.3){const r=u(n.map(o=>o.naeheText)),l=u(n.map(o=>o.naeheThema)),a=u(n.map(o=>o.naeheRueckkopplung));return n.map((o,t)=>e.text*Math.log(s+r[t])+e.thema*Math.log(s+l[t])+e.rueckkopplung*Math.log(s+a[t])+e.seltenheit*Math.log(s+o.seltenheitsDeckung))}function d(n,e){return n&&e&&e.length>0?i(n,e):-2}export{c as GEWICHTE,m as Seltenheit,x as bewerteTopf,z as bewerteTopfStreng,g as grobStamm,p as inhaltsWoerter,d as naeheOderLuecke,k as reichereAn,u as spreizeImTopf};
@@ -1,197 +1 @@
1
- /**
2
- * Raumkorrektur: den Einbettungsraum brauchbar machen, bevor man darin sucht.
3
- *
4
- * ── Drei Übel, drei Gegenmittel ─────────────────────────────────────────────
5
- *
6
- * Ein Einbettungsraum ist nicht neutral. Er hat Eigenschaften, die das Sortieren
7
- * verderben — alle drei am 19.08.2026 an 499 echten Lektionen gemessen:
8
- *
9
- * 1. GEMEINSAME GRUNDRICHTUNG (Anisotropie). Ein durchschnittlicher Vektor
10
- * zeigt zu 69,8 Prozent in Richtung des Schwerpunkts aller Vektoren. Dieser
11
- * Anteil ist in JEDEM Vergleich derselbe und sagt daher nichts — er hebt nur
12
- * alle Werte an und drückt die Unterschiede zusammen. Gemessen liegen alle
13
- * Ähnlichkeiten zwischen 0,42 und 0,61.
14
- *
15
- * 2. RICHTUNGEN, DIE NUR SCHREIBSTIL SIND. Nach dem Schwerpunkt bleiben ein
16
- * paar Hauptrichtungen übrig, die keine Bedeutung tragen, sondern Textsorte:
17
- * "ist eine Fehlermeldung", "enthält Codeschnipsel", "ist auf Deutsch".
18
- * Sie sind stark und lenken die Rangfolge.
19
- *
20
- * 3. NABEN. Manche Vektoren liegen einfach nahe an vielem. Sie erscheinen als
21
- * Treffer für Fragen, mit denen sie nichts zu tun haben. Belegt: die Lektion
22
- * `forge:arbeitskopie-nach-dem-merge-sofort-abraeumen` stand bei "Ich beende
23
- * den Prozess, kurz darauf ist er neu da" UND bei "Wenn der Zwischenspeicher
24
- * abläuft, schlagen alle Anfragen durch" auf Platz 1. Zwei Fragen ohne
25
- * Berührungspunkt, dieselbe falsche Antwort.
26
- *
27
- * ── Die Gegenmittel ─────────────────────────────────────────────────────────
28
- *
29
- * Zu 1 und 2: Schwerpunkt abziehen und die stärksten verbleibenden
30
- * Hauptrichtungen entfernen. Was danach übrig bleibt, ist das, worin sich die
31
- * Texte inhaltlich unterscheiden.
32
- *
33
- * Zu 3: jedem Vektor seine eigene "Geselligkeit" ausrechnen — wie nah liegt er
34
- * im Mittel an seinen nächsten Nachbarn. Wer überall nah dran ist, bekommt
35
- * einen Abzug. Das ist die Idee hinter CSLS aus der Wortübersetzung, hier auf
36
- * den Fall übertragen, dass zur Bauzeit keine Fragen vorliegen: die Nachbarn
37
- * sind die anderen Lektionen.
38
- *
39
- * Entscheidend für den Betrieb: ALLES DAVON WIRD EINMAL AUSGERECHNET. Zur
40
- * Fragezeit bleibt ein Skalarprodukt und eine Subtraktion. Die Rangfolge wird
41
- * besser, ohne dass jemand länger wartet.
42
- */
43
- /** Skalarprodukt. */
44
- export function punktprodukt(a, b) {
45
- let s = 0;
46
- for (let i = 0; i < a.length; i++)
47
- s += a[i] * b[i];
48
- return s;
49
- }
50
- export function laenge(v) {
51
- return Math.sqrt(punktprodukt(v, v));
52
- }
53
- /** Auf Länge 1 bringen. Ein Nullvektor bleibt, was er ist. */
54
- export function normiere(v) {
55
- const l = laenge(v);
56
- return l > 1e-12 ? v.map((x) => x / l) : v.slice();
57
- }
58
- /** Der Schwerpunkt — die gemeinsame Grundrichtung aller Vektoren. */
59
- export function schwerpunkt(vs) {
60
- const d = vs[0]?.length ?? 0;
61
- const m = new Array(d).fill(0);
62
- for (const v of vs)
63
- for (let i = 0; i < d; i++)
64
- m[i] += v[i];
65
- for (let i = 0; i < d; i++)
66
- m[i] /= Math.max(1, vs.length);
67
- return m;
68
- }
69
- /**
70
- * Die stärksten Hauptrichtungen, über Potenziteration mit Deflation.
71
- *
72
- * Warum von Hand statt einer Bibliothek: es sind rund zwanzig Zeilen, läuft
73
- * einmal, und eine Abhängigkeit für zwanzig Zeilen ist eine Abhängigkeit, die
74
- * jemand pflegen muss.
75
- *
76
- * Die Vektoren müssen bereits zentriert sein — sonst ist die erste
77
- * Hauptrichtung nur der Schwerpunkt, den wir gerade abgezogen haben.
78
- */
79
- export function hauptrichtungen(zentriert, anzahl, schritte = 30) {
80
- const d = zentriert[0]?.length ?? 0;
81
- const rest = zentriert.map((v) => v.slice());
82
- const aus = [];
83
- for (let k = 0; k < anzahl; k++) {
84
- // Startvektor: fest, nicht zufällig — sonst liefert derselbe Bestand bei
85
- // jedem Lauf leicht andere Richtungen, und niemand kann eine Messung
86
- // wiederholen.
87
- let u = new Array(d).fill(0).map((_, i) => Math.sin(i * (k + 1) * 0.7));
88
- u = normiere(u);
89
- for (let s = 0; s < schritte; s++) {
90
- const neu = new Array(d).fill(0);
91
- for (const v of rest) {
92
- const p = punktprodukt(v, u);
93
- for (let i = 0; i < d; i++)
94
- neu[i] += p * v[i];
95
- }
96
- const l = laenge(neu);
97
- if (l < 1e-12)
98
- break;
99
- for (let i = 0; i < d; i++)
100
- neu[i] /= l;
101
- u = neu;
102
- }
103
- aus.push(u);
104
- // Deflation: den gefundenen Anteil aus allen Vektoren entfernen, damit die
105
- // nächste Iteration eine ANDERE Richtung findet.
106
- for (const v of rest) {
107
- const p = punktprodukt(v, u);
108
- for (let i = 0; i < d; i++)
109
- v[i] -= p * u[i];
110
- }
111
- }
112
- return aus;
113
- }
114
- /** Was von einem Vektor bleibt, wenn Schwerpunkt und Hauptrichtungen weg sind. */
115
- export function bereinige(v, mitte, richtungen) {
116
- const w = v.map((x, i) => x - mitte[i]);
117
- for (const u of richtungen) {
118
- const p = punktprodukt(w, u);
119
- for (let i = 0; i < w.length; i++)
120
- w[i] -= p * u[i];
121
- }
122
- return normiere(w);
123
- }
124
- /**
125
- * Die "Geselligkeit" jedes Vektors: mittlere Ähnlichkeit zu seinen k nächsten
126
- * Nachbarn im selben Bestand.
127
- *
128
- * Ein hoher Wert heißt: dieser Vektor liegt in einer dichten Gegend und ist
129
- * vielem nah. Solche Naben erscheinen als Treffer für Fragen, mit denen sie
130
- * nichts zu tun haben — nicht weil sie passen, sondern weil sie überall
131
- * passen.
132
- *
133
- * Der Wert wird EINMAL berechnet und beim Suchen abgezogen. Kosten zur
134
- * Fragezeit: eine Subtraktion.
135
- */
136
- export function geselligkeit(normierte, k = 10) {
137
- const n = normierte.length;
138
- const aus = new Array(n).fill(0);
139
- for (let i = 0; i < n; i++) {
140
- // Die k besten Ähnlichkeiten sammeln, ohne alles zu sortieren.
141
- const beste = [];
142
- for (let j = 0; j < n; j++) {
143
- if (i === j)
144
- continue;
145
- const s = punktprodukt(normierte[i], normierte[j]);
146
- if (beste.length < k) {
147
- beste.push(s);
148
- if (beste.length === k)
149
- beste.sort((a, b) => a - b);
150
- }
151
- else if (s > beste[0]) {
152
- beste[0] = s;
153
- beste.sort((a, b) => a - b);
154
- }
155
- }
156
- aus[i] = beste.length ? beste.reduce((a, b) => a + b, 0) / beste.length : 0;
157
- }
158
- return aus;
159
- }
160
- export function baueRaum(roh, optionen = {}) {
161
- const anzahlRichtungen = optionen.richtungen ?? 3;
162
- const nachbarn = optionen.nachbarn ?? 10;
163
- const beta = optionen.beta ?? 0.5;
164
- const da = roh.filter(Boolean);
165
- const mitte = schwerpunkt(da);
166
- const zentriert = da.map((v) => v.map((x, i) => x - mitte[i]));
167
- const richtungen = anzahlRichtungen > 0 ? hauptrichtungen(zentriert, anzahlRichtungen) : [];
168
- // Bereinigte Vektoren in der ursprünglichen Reihenfolge — Lücken bleiben
169
- // Lücken, damit ein fehlender Vektor nicht still zu einem Nullvektor wird,
170
- // der zu allem gleich weit entfernt ist.
171
- const vektoren = [];
172
- const gueltigeIndizes = [];
173
- for (const [i, v] of roh.entries()) {
174
- if (!v) {
175
- vektoren.push([]);
176
- continue;
177
- }
178
- vektoren.push(bereinige(v, mitte, richtungen));
179
- gueltigeIndizes.push(i);
180
- }
181
- const abzug = new Array(roh.length).fill(0);
182
- const nurGueltige = gueltigeIndizes.map((i) => vektoren[i]);
183
- const g = geselligkeit(nurGueltige, nachbarn);
184
- gueltigeIndizes.forEach((idx, k) => { abzug[idx] = g[k]; });
185
- return { mitte, richtungen, vektoren, abzug, beta };
186
- }
187
- /**
188
- * Bewertet eine Frage gegen alle Lektionen des Raums.
189
- *
190
- * `2 * cos - abzug` ist die Form aus CSLS. Der Faktor 2 sorgt dafür, dass der
191
- * Abzug die Ähnlichkeit dämpft und nicht überstimmt.
192
- */
193
- export function bewerteImRaum(raum, frage) {
194
- const f = bereinige(frage, raum.mitte, raum.richtungen);
195
- return raum.vektoren.map((v, i) => v.length === 0 ? -2 : 2 * punktprodukt(f, v) - raum.beta * raum.abzug[i]);
196
- }
197
- //# sourceMappingURL=raumkorrektur.js.map
1
+ function b(t,o){let r=0;for(let n=0;n<t.length;n++)r+=t[n]*o[n];return r}function x(t){return Math.sqrt(b(t,t))}function w(t){const o=x(t);return o>1e-12?t.map(r=>r/o):t.slice()}function y(t){const o=t[0]?.length??0,r=new Array(o).fill(0);for(const n of t)for(let e=0;e<o;e++)r[e]+=n[e];for(let n=0;n<o;n++)r[n]/=Math.max(1,t.length);return r}function A(t,o,r=30){const n=t[0]?.length??0,e=t.map(c=>c.slice()),s=[];for(let c=0;c<o;c++){let i=new Array(n).fill(0).map((f,l)=>Math.sin(l*(c+1)*.7));i=w(i);for(let f=0;f<r;f++){const l=new Array(n).fill(0);for(const a of e){const m=b(a,i);for(let g=0;g<n;g++)l[g]+=m*a[g]}const u=x(l);if(u<1e-12)break;for(let a=0;a<n;a++)l[a]/=u;i=l}s.push(i);for(const f of e){const l=b(f,i);for(let u=0;u<n;u++)f[u]-=l*i[u]}}return s}function k(t,o,r){const n=t.map((e,s)=>e-o[s]);for(const e of r){const s=b(n,e);for(let c=0;c<n.length;c++)n[c]-=s*e[c]}return w(n)}function z(t,o=10){const r=t.length,n=new Array(r).fill(0);for(let e=0;e<r;e++){const s=[];for(let c=0;c<r;c++){if(e===c)continue;const i=b(t[e],t[c]);s.length<o?(s.push(i),s.length===o&&s.sort((f,l)=>f-l)):i>s[0]&&(s[0]=i,s.sort((f,l)=>f-l))}n[e]=s.length?s.reduce((c,i)=>c+i,0)/s.length:0}return n}function M(t,o={}){const r=o.richtungen??3,n=o.nachbarn??10,e=o.beta??.5,s=t.filter(Boolean),c=y(s),i=s.map(h=>h.map((p,d)=>p-c[d])),f=r>0?A(i,r):[],l=[],u=[];for(const[h,p]of t.entries()){if(!p){l.push([]);continue}l.push(k(p,c,f)),u.push(h)}const a=new Array(t.length).fill(0),m=u.map(h=>l[h]),g=z(m,n);return u.forEach((h,p)=>{a[h]=g[p]}),{mitte:c,richtungen:f,vektoren:l,abzug:a,beta:e}}function R(t,o){const r=k(o,t.mitte,t.richtungen);return t.vektoren.map((n,e)=>n.length===0?-2:2*b(r,n)-t.beta*t.abzug[e])}export{M as baueRaum,k as bereinige,R as bewerteImRaum,z as geselligkeit,A as hauptrichtungen,x as laenge,w as normiere,b as punktprodukt,y as schwerpunkt};
@@ -1,42 +1 @@
1
- /**
2
- * Wie viele Treffer zeigt `smart_recall` — und wie viele haelt es zurueck?
3
- *
4
- * Eigenes Modul, weil hier ein Versprechen eingeloest wird, das vorher nur im
5
- * Kommentar stand. Der Kommentar am Teaser-Gate sagte seit jeher:
6
- *
7
- * "the magic moment is sacred ... never an access wall: the #1 result is
8
- * always returned"
9
- *
10
- * Der Code sagte `visibleCount = gateActive ? 0 : 8` — also NULL Treffer,
11
- * sobald ein Frei-Konto sein Monatskontingent ueberschritten hat. Der Nutzer
12
- * las die Ueberschrift "Results (23 — hybrid)", darunter keine einzige Zeile
13
- * und dann eine Kaufaufforderung. Das sieht nicht aus wie eine Rechnung,
14
- * sondern wie ein kaputtes Werkzeug — besonders bei Ambient Recall, das bei
15
- * jedem Prompt feuert und das Kontingent in Tagen aufbraucht.
16
- *
17
- * Fehlerklasse: eine Zusage an zwei Orten gepflegt (Kommentar und Zweig).
18
- * Deshalb ist die Regel jetzt eine Funktion mit Tests, nicht ein Satz Prosa.
19
- */
20
- /** Der eine Treffer, den auch ein ueberzogenes Frei-Konto immer sieht. */
21
- export const TIEFE_UEBER_LIMIT = 1;
22
- /** Volle Tiefe bei einer einzelnen Frage. */
23
- export const TIEFE_VOLL = 8;
24
- /** Volle Tiefe, wenn die Frage in Unterfragen zerfaellt (mehr Themen, mehr Platz). */
25
- export const TIEFE_VOLL_MEHRTHEMIG = 12;
26
- /**
27
- * Nie eine Mauer, immer eine Bremse: ueber dem Limit bleibt genau der beste
28
- * Treffer sichtbar, der Rest wird gezaehlt und benannt.
29
- *
30
- * Bei null Treffern gibt es nichts zu zeigen und nichts zurueckzuhalten — ein
31
- * leeres Ergebnis darf nie als "hinter der Bezahlschranke" erscheinen, sonst
32
- * verkauft die Meldung etwas, das gar nicht existiert.
33
- */
34
- export function recallTiefe({ ueberLimit, gesamt, volleTiefe = TIEFE_VOLL }) {
35
- const vorhanden = Number.isFinite(gesamt) && gesamt > 0 ? Math.floor(gesamt) : 0;
36
- if (vorhanden === 0)
37
- return { sichtbar: 0, zurueckgehalten: 0 };
38
- const obergrenze = ueberLimit ? TIEFE_UEBER_LIMIT : Math.max(0, Math.floor(volleTiefe));
39
- const sichtbar = Math.min(vorhanden, obergrenze);
40
- return { sichtbar, zurueckgehalten: ueberLimit ? vorhanden - sichtbar : 0 };
41
- }
42
- //# sourceMappingURL=recall-tiefe.js.map
1
+ const c=1,I=8,a=12;function h({ueberLimit:e,gesamt:t,volleTiefe:o=8}){const r=Number.isFinite(t)&&t>0?Math.floor(t):0;if(r===0)return{sichtbar:0,zurueckgehalten:0};const E=e?1:Math.max(0,Math.floor(o)),n=Math.min(r,E);return{sichtbar:n,zurueckgehalten:e?r-n:0}}export{c as TIEFE_UEBER_LIMIT,I as TIEFE_VOLL,a as TIEFE_VOLL_MEHRTHEMIG,h as recallTiefe};
@@ -1,154 +1 @@
1
- /**
2
- * Quality-aware reranking for Brain recall.
3
- *
4
- * The raw BM25+ keyword search (src/search.ts) ranks purely by *textual* relevance.
5
- * That is necessary but not sufficient: when an agent asks "how do I fix the flaky
6
- * deploy?", a *failed* attempt with similar words can out-rank the *proven success*
7
- * lesson that actually solves it. Anthropic's flat-file memory has exactly this
8
- * problem — it reads files in order, with no notion of which lesson is trustworthy.
9
- *
10
- * This module is Cachly's structural advantage made concrete: we re-rank text
11
- * matches by the *quality* of the underlying lesson — outcome, confidence,
12
- * proven-ness (recall_count), severity and human review (endorsements) — so the
13
- * lesson most likely to help surfaces first. A flat-file memory has none of these
14
- * signals; a senior-confirmed, ten-times-recalled success reads identically to a
15
- * one-off failed attempt.
16
- *
17
- * It is intentionally pure (no I/O): the lesson JSON already travels inside each
18
- * KeywordMatch.content for `cachly:lesson:best:*` keys, so reranking needs no extra
19
- * Redis round-trips. This keeps it fast and trivially benchmarkable (see
20
- * src/bench/cachly-bench.ts).
21
- */
22
- import { safeJsonParse } from './utils.js';
23
- // ── Tunable weights (kept conservative; validated by Cachly-Bench) ─────────────
24
- const OUTCOME_WEIGHT = {
25
- success: 1.0,
26
- partial: 0.8,
27
- failure: 0.55,
28
- };
29
- const OUTCOME_DEFAULT = 0.75; // unknown / not a lesson with an outcome
30
- const SEVERITY_WEIGHT = {
31
- critical: 1.15,
32
- major: 1.05,
33
- minor: 1.0,
34
- };
35
- const SEVERITY_DEFAULT = 1.0;
36
- // Governance: a human-confirmed lesson is more trustworthy than one proven only
37
- // by automated recall. A senior review weighs more than a peer review; multiple
38
- // endorsements add a small, saturating bump on top.
39
- const REVIEW_LEVEL_WEIGHT = {
40
- senior: 1.25,
41
- peer: 1.1,
42
- };
43
- // Clamp so a strong text match is never fully buried by quality, and a weak text
44
- // match is never catapulted to the top purely on quality.
45
- // MAX lowered 2.5→1.8: on larger corpora a high-quality but weakly-matching lesson
46
- // could overtake a strongly-matching relevant one and push it out of the top-k.
47
- // 1.8 keeps the home-corpus Precision@1 lift while improving external-corpus
48
- // Precision@1 + MRR (validated by src/bench/external-corpus.ts).
49
- const MIN_MULTIPLIER = 0.5;
50
- const MAX_MULTIPLIER = 1.8;
51
- const LESSON_KEY_PREFIX = 'cachly:lesson:best:';
52
- /**
53
- * Compute the quality multiplier for a single lesson.
54
- * Returns 1.0 (neutral) for documents that carry no quality signal (e.g. context).
55
- */
56
- export function qualityMultiplier(lesson) {
57
- if (!lesson || (lesson.outcome === undefined && lesson.confidence === undefined
58
- && lesson.recall_count === undefined && lesson.reviewed_by === undefined
59
- && lesson.endorsements === undefined)) {
60
- return 1.0;
61
- }
62
- // 1. Outcome — a proven success is what the agent actually wants.
63
- const outcome = OUTCOME_WEIGHT[lesson.outcome ?? ''] ?? OUTCOME_DEFAULT;
64
- // 2. Confidence — maps 0..1 → 0.7..1.3 so it nudges, not dominates.
65
- const conf = typeof lesson.confidence === 'number' ? Math.max(0, Math.min(1, lesson.confidence)) : 0.6;
66
- const confidenceFactor = 0.7 + 0.6 * conf;
67
- // 3. Proven-ness — a lesson recalled many times has stood the test of use.
68
- // Log-scaled so it saturates: rc=0→1.0, rc=5→~1.18, rc=50→~1.30.
69
- const rc = Math.max(0, lesson.recall_count ?? 0);
70
- const provenBoost = 1 + Math.min(0.3, Math.log1p(rc) / 13);
71
- // 4. Severity — when two lessons tie, surface the one guarding the worse failure.
72
- const severity = SEVERITY_WEIGHT[lesson.severity ?? ''] ?? SEVERITY_DEFAULT;
73
- // 5. Governance — a human-confirmed lesson outranks an unreviewed one.
74
- // Base weight from the review level, plus a small saturating bump for
75
- // additional endorsements (rc-style log curve, capped at +0.1).
76
- const reviewBase = lesson.reviewed_by
77
- ? (REVIEW_LEVEL_WEIGHT[lesson.review_level ?? 'peer'] ?? REVIEW_LEVEL_WEIGHT.peer)
78
- : 1.0;
79
- const extraEndorse = Math.max(0, (lesson.endorsements ?? 0) - 1);
80
- const reviewBoost = reviewBase * (1 + Math.min(0.1, Math.log1p(extraEndorse) / 20));
81
- const m = outcome * confidenceFactor * provenBoost * severity * reviewBoost;
82
- return Math.max(MIN_MULTIPLIER, Math.min(MAX_MULTIPLIER, m));
83
- }
84
- /** Extract the quality signal from a match, if its content is a stored lesson. */
85
- export function extractLessonQuality(match) {
86
- if (!match.key.startsWith(LESSON_KEY_PREFIX))
87
- return null;
88
- return safeJsonParse(match.content, null);
89
- }
90
- /**
91
- * Rerank BM25 keyword matches by lesson quality.
92
- *
93
- * BM25 scores are compressed with a 0.3-power root before quality is applied.
94
- * Compression is the key: a distractor lesson whose `what_failed` contains the
95
- * exact query phrase can score 5–6× higher in BM25 than the correct success lesson.
96
- * A linear quality multiplier of ±40% cannot overcome that gap; after ^0.3
97
- * compression the ratio shrinks to ~1.5×, well within the quality range.
98
- *
99
- * The formula compressed_score * (0.4 + 0.6 * boost) gives quality 60% weight
100
- * on the compressed axis. A failure lesson (boost ≈ 0.5) retains 70% of its
101
- * compressed score; a senior-confirmed success (boost ≈ 1.8) gets 148%.
102
- * Ties are broken by the raw BM25 score (stable, deterministic).
103
- */
104
- /**
105
- * Wie stark Qualitaetsmerkmale die Rangfolge bewegen duerfen.
106
- *
107
- * Bleibt bei 0.6 — dem Wert von vorher. Die Messreihe vom 19.08.2026 zeigt,
108
- * dass dieser Wert auf echten Daten NICHTS kostet: bei ungestauchter Relevanz
109
- * liefern 0.6, 0.3 und 0.15 dasselbe Ergebnis (30 Prozent auf Platz 1).
110
- *
111
- * Das ist der Grund, hier nichts zu aendern. Der Schaden kam allein von der
112
- * Stauchung darunter. Wer zwei Dinge gleichzeitig verstellt, weiss hinterher
113
- * nicht, welches gewirkt hat.
114
- */
115
- const QUALITAETS_GEWICHT = 0.6;
116
- export function rerankByQuality(matches) {
117
- if (matches.length === 0)
118
- return [];
119
- const reranked = matches.map(m => {
120
- // ── Die Relevanz wird NICHT mehr plattgedrueckt ─────────────────────────
121
- //
122
- // Hier stand `Math.pow(score, 0.3)`. Das war Absicht: die Relevanz wurde
123
- // zusammengestaucht, damit die Qualitaetsmerkmale (Zuversicht, Abnahme,
124
- // Zustimmungen) die Rangfolge mitbestimmen koennen. Aus einem
125
- // zehnfachen Relevanzvorsprung wurde ein doppelter.
126
- //
127
- // Auf dem Pruefstand mit 17 Lektionen war das ein Gewinn: 92,3 statt
128
- // 69,2 Prozent. Genau diese Zahl steht seit Monaten auf der Landingpage.
129
- //
130
- // Auf 498 echten Lektionen, am 19.08.2026 gemessen, ist es der Verlust:
131
- //
132
- // Stauchung echter Bestand Pruefstand
133
- // ^0.3 15 % 92,3 %
134
- // ^0.6 25 % 76,9 %
135
- // ^1.0 (keine) 30 % 69,2 %
136
- //
137
- // Der gesamte gemessene Vorteil auf dem Pruefstand IST der Schaden auf
138
- // echten Daten. Bei 17 Lektionen ist der richtige Treffer so eindeutig,
139
- // dass er auch gestaucht gewinnt. Bei 498 ist die Relevanz das knappe
140
- // Gut, und wer sie staucht, wirft weg, was er hat.
141
- const compressed = Math.max(0, m.score);
142
- const lesson = extractLessonQuality(m);
143
- const boost = qualityMultiplier(lesson);
144
- // Qualitaet wirkt unveraendert auf die UNGESTAUCHTE Relevanz. Damit kann
145
- // sie einen knappen Textvorsprung noch drehen (eine belegte Loesung schlaegt
146
- // einen etwas besser passenden Fehlversuch), einen deutlichen aber nicht
147
- // mehr.
148
- const finalScore = compressed * (1 - QUALITAETS_GEWICHT + QUALITAETS_GEWICHT * boost);
149
- return { ...m, finalScore, qualityBoost: boost };
150
- });
151
- reranked.sort((a, b) => (b.finalScore - a.finalScore) || (b.score - a.score));
152
- return reranked;
153
- }
154
- //# sourceMappingURL=rerank.js.map
1
+ import{safeJsonParse as m}from"./utils.js";const M={success:1,partial:.8,failure:.55},_=.75,l={critical:1.15,major:1.05,minor:1},p=1,a={senior:1.25,peer:1.1},I=.5,T=1.8,h="cachly:lesson:best:";function y(e){if(!e||e.outcome===void 0&&e.confidence===void 0&&e.recall_count===void 0&&e.reviewed_by===void 0&&e.endorsements===void 0)return 1;const o=M[e.outcome??""]??_,n=.7+.6*(typeof e.confidence=="number"?Math.max(0,Math.min(1,e.confidence)):.6),c=Math.max(0,e.recall_count??0),r=1+Math.min(.3,Math.log1p(c)/13),i=l[e.severity??""]??p,u=e.reviewed_by?a[e.review_level??"peer"]??a.peer:1,f=Math.max(0,(e.endorsements??0)-1),E=u*(1+Math.min(.1,Math.log1p(f)/20)),d=o*n*r*i*E;return Math.max(I,Math.min(T,d))}function L(e){return e.key.startsWith(h)?m(e.content,null):null}const s=.6;function v(e){if(e.length===0)return[];const o=e.map(t=>{const n=Math.max(0,t.score),c=L(t),r=y(c),i=n*(1-s+s*r);return{...t,finalScore:i,qualityBoost:r}});return o.sort((t,n)=>n.finalScore-t.finalScore||n.score-t.score),o}export{L as extractLessonQuality,y as qualityMultiplier,v as rerankByQuality};