@cachly-dev/mcp-server 0.10.124 → 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.
- package/dist/index.d.ts +1 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/dist/src/aussetzer.d.ts +67 -0
- package/dist/src/aussetzer.js +82 -0
- package/dist/src/aussetzer.js.map +1 -0
- package/dist/src/autor-abzeichen.d.ts +41 -0
- package/dist/src/autor-abzeichen.js +56 -0
- package/dist/src/autor-abzeichen.js.map +1 -0
- package/dist/src/autor.d.ts +101 -0
- package/dist/src/autor.js +136 -0
- package/dist/src/autor.js.map +1 -0
- package/dist/src/bauabdruck-daten.generiert.json +5 -0
- package/dist/src/bauabdruck.d.ts +38 -0
- package/dist/src/bauabdruck.js +93 -0
- package/dist/src/bauabdruck.js.map +1 -0
- package/dist/src/bedeutung.d.ts +175 -0
- package/dist/src/bedeutung.js +254 -0
- package/dist/src/bedeutung.js.map +1 -0
- package/dist/src/brain-export.d.ts +24 -0
- package/dist/src/brain-export.js +31 -1
- package/dist/src/brain-export.js.map +1 -1
- package/dist/src/deutsch.d.ts +98 -0
- package/dist/src/deutsch.js +223 -0
- package/dist/src/deutsch.js.map +1 -0
- package/dist/src/eingaenge-nachruesten.d.ts +41 -0
- package/dist/src/eingaenge-nachruesten.js +290 -0
- package/dist/src/eingaenge-nachruesten.js.map +1 -0
- package/dist/src/eingaenge.d.ts +135 -0
- package/dist/src/eingaenge.js +228 -0
- package/dist/src/eingaenge.js.map +1 -0
- package/dist/src/geliefert.d.ts +63 -0
- package/dist/src/geliefert.js +114 -0
- package/dist/src/geliefert.js.map +1 -0
- package/dist/src/handlers/advanced.js +7 -3
- package/dist/src/handlers/advanced.js.map +1 -1
- package/dist/src/handlers/brain.d.ts +19 -0
- package/dist/src/handlers/brain.js +536 -48
- package/dist/src/handlers/brain.js.map +1 -1
- package/dist/src/handlers/instances.d.ts +24 -0
- package/dist/src/handlers/instances.js +49 -1
- package/dist/src/handlers/instances.js.map +1 -1
- package/dist/src/handlers/team.js +105 -0
- package/dist/src/handlers/team.js.map +1 -1
- package/dist/src/index.d.ts +20 -0
- package/dist/src/index.js +272 -26
- package/dist/src/index.js.map +1 -1
- package/dist/src/lesson-preview.d.ts +81 -0
- package/dist/src/lesson-preview.js +133 -0
- package/dist/src/lesson-preview.js.map +1 -0
- package/dist/src/rangfolge.d.ts +169 -0
- package/dist/src/rangfolge.js +214 -0
- package/dist/src/rangfolge.js.map +1 -0
- package/dist/src/raumkorrektur.d.ts +100 -0
- package/dist/src/raumkorrektur.js +197 -0
- package/dist/src/raumkorrektur.js.map +1 -0
- package/dist/src/recall-tiefe.d.ts +48 -0
- package/dist/src/recall-tiefe.js +42 -0
- package/dist/src/recall-tiefe.js.map +1 -0
- package/dist/src/rerank.d.ts +0 -14
- package/dist/src/rerank.js +39 -2
- package/dist/src/rerank.js.map +1 -1
- package/dist/src/search.d.ts +56 -17
- package/dist/src/search.js +335 -73
- package/dist/src/search.js.map +1 -1
- package/dist/src/seltenheitsbestand.d.ts +53 -0
- package/dist/src/seltenheitsbestand.js +92 -0
- package/dist/src/seltenheitsbestand.js.map +1 -0
- package/dist/src/spuren.d.ts +97 -0
- package/dist/src/spuren.js +150 -0
- package/dist/src/spuren.js.map +1 -0
- package/dist/src/tools.js +1 -1
- package/dist/src/tools.js.map +1 -1
- package/dist/src/versuch.d.ts +179 -0
- package/dist/src/versuch.js +250 -0
- package/dist/src/versuch.js.map +1 -0
- package/dist/src/vorspann.d.ts +73 -0
- package/dist/src/vorspann.js +146 -0
- package/dist/src/vorspann.js.map +1 -0
- package/dist/src/werkzeug-nutzung.d.ts +62 -0
- package/dist/src/werkzeug-nutzung.js +72 -0
- package/dist/src/werkzeug-nutzung.js.map +1 -0
- package/dist/src/wertbeitrag.d.ts +57 -0
- package/dist/src/wertbeitrag.js +73 -0
- package/dist/src/wertbeitrag.js.map +1 -0
- package/glama.json +1 -1
- package/package.json +12 -4
- package/server.json +2 -2
|
@@ -3,11 +3,69 @@ import { createRequire } from 'node:module';
|
|
|
3
3
|
import { calculateConfidence, confidenceBadge, STRUCTURED_TEMPLATES, CONFIDENCE_WARN_VALUE, CONFIDENCE_STALE_VALUE, CONFIDENCE_WARN_DAYS } from '../confidence.js';
|
|
4
4
|
import { ckgSlug, extractProblemConcept, ckgUpsertNode, ckgUpdateEdge, ckgUpsertPersonNode, ckgUpsertFileNode, ckgRecordCollaboration, ckgUpsertServiceNode } from '../ckg.js';
|
|
5
5
|
import { safeJsonParse, scanKeys } from '../utils.js';
|
|
6
|
+
import { ersparteMinuten, istStarterLektion } from '../wertbeitrag.js';
|
|
7
|
+
import { lessonPreviewLines, trimTo } from '../lesson-preview.js';
|
|
8
|
+
import { meldeEinmal, OHNE_VEKTOREN, OHNE_DIENST, OHNE_FRAGEVEKTOR } from '../aussetzer.js';
|
|
9
|
+
import { versuchStart, neueKennung, wendeZuteilungAn, schliesseVersuchAb } from '../versuch.js';
|
|
10
|
+
/**
|
|
11
|
+
* Preview for one smart_recall hit.
|
|
12
|
+
*
|
|
13
|
+
* WHY THIS EXISTS: the previous version sliced `r.content` — the RAW JSON of the
|
|
14
|
+
* stored record — at 300/400 characters. So the budget was spent on syntax:
|
|
15
|
+
*
|
|
16
|
+
* > {"topic":"node4:einrichtung-contabo-und-fallen","outcome":"success","sev…
|
|
17
|
+
*
|
|
18
|
+
* Roughly the first 80 characters carry no information at all, the reader gets
|
|
19
|
+
* field names instead of facts, and `commands` / `what_failed` never appear
|
|
20
|
+
* because they sit far past the cut. smart_recall is the call this project
|
|
21
|
+
* mandates BEFORE EVERY TASK — it is the worst possible place for that.
|
|
22
|
+
*
|
|
23
|
+
* A lesson is now rendered as a lesson: trimmed prose, its warning when the
|
|
24
|
+
* lesson is critical, and its commands/paths. Anything that is not a lesson
|
|
25
|
+
* (context entries, indexes) keeps a raw preview — but trimmed at a word
|
|
26
|
+
* boundary and with an honest cut marker.
|
|
27
|
+
*/
|
|
28
|
+
export function recallVorschau(key, content, maxChars) {
|
|
29
|
+
if (key.includes('lesson:best:')) {
|
|
30
|
+
const l = safeJsonParse(content, {});
|
|
31
|
+
if (l && typeof l === 'object' && (l.what_worked || l.what_failed || l.commands?.length)) {
|
|
32
|
+
return lessonPreviewLines(l, { maxChars, indent: ' ' });
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
const roh = trimTo(content, maxChars);
|
|
36
|
+
return roh ? [roh] : [];
|
|
37
|
+
}
|
|
6
38
|
import { getRole, ROLE_BADGE, getScopes, lessonVisibleToScope, reviewModeEnabled, storeLessonProposal } from './team.js';
|
|
7
|
-
import { keywordSearch, tokenize, splitMultiQuery, levenshtein, indexVocab as _indexVocab } from '../search.js';
|
|
39
|
+
import { keywordSearch, wortindexEntwerten, tokenize, splitMultiQuery, levenshtein, indexVocab as _indexVocab } from '../search.js';
|
|
8
40
|
import { rerankByQuality, qualityMultiplier, extractLessonQuality } from '../rerank.js';
|
|
9
41
|
import { computeEmbedding, hasEmbedProvider } from '../embeddings.js';
|
|
42
|
+
import { VEKTOR_PRAEFIX, NAME_VEKTOR_PRAEFIX, packe, textFuerVektor, textFuerNamensVektor, Vektorbestand, } from '../bedeutung.js';
|
|
43
|
+
import { schreibeEingaenge, Eingangsbestand, EINGANG_GEWICHT } from '../eingaenge.js';
|
|
44
|
+
import { Seltenheitsbestand } from '../seltenheitsbestand.js';
|
|
45
|
+
import { spurLegen } from '../spuren.js';
|
|
46
|
+
import { bewerteTopf, spreizeImTopf, reichereAn, inhaltsWoerter, grobStamm, GEWICHTE, } from '../rangfolge.js';
|
|
47
|
+
/**
|
|
48
|
+
* Ein Vektorbestand fuer den ganzen Prozess.
|
|
49
|
+
*
|
|
50
|
+
* Er haelt rund 2 MB je 500 Lektionen im Arbeitsspeicher. Die bei jeder Frage
|
|
51
|
+
* neu zu holen waere langsamer als der Wortabgleich, den wir gerade ergaenzen —
|
|
52
|
+
* der Gewinn waere weg, bevor er anfaengt.
|
|
53
|
+
*/
|
|
54
|
+
const vektorbestand = new Vektorbestand();
|
|
55
|
+
/** Dieselbe Mechanik, andere Sicht: die Vektoren der Themennamen. */
|
|
56
|
+
const namensbestand = new Vektorbestand(60_000, NAME_VEKTOR_PRAEFIX);
|
|
57
|
+
/** Die Fehlertext-Eingaenge, je Lektion ihr bester. */
|
|
58
|
+
const eingangsbestand = new Eingangsbestand();
|
|
59
|
+
/** Die Wortstatistik des Bestands — stehend, nicht je Frage neu. */
|
|
60
|
+
const seltenheitsbestand = new Seltenheitsbestand();
|
|
10
61
|
import { upgradeNudge } from '../upgrade-nudge.js';
|
|
62
|
+
import { recallTiefe, TIEFE_VOLL, TIEFE_VOLL_MEHRTHEMIG } from '../recall-tiefe.js';
|
|
63
|
+
import { nutzungInWorten, nutzungsSchluessel, verdichte } from '../werkzeug-nutzung.js';
|
|
64
|
+
import { inWorten as lieferungInWorten, lieferBild, lieferSchluessel, trefferSchluessel } from '../geliefert.js';
|
|
65
|
+
import { COAUTHOR_PREF_KEY, autorHinweis, coautorAus, echterGitLeser, ermittleAutor, mitCoautor, nurMensch } from '../autor.js';
|
|
66
|
+
import { TOOLS } from '../tools.js';
|
|
67
|
+
import { vorspannHinweis } from '../vorspann.js';
|
|
68
|
+
import { autorAbzeichen, fremdanteil } from '../autor-abzeichen.js';
|
|
11
69
|
import { cachlyUrl } from '../cachly-url.js';
|
|
12
70
|
// ── Changelog (shown once per version in session_start) ──────────────────────
|
|
13
71
|
// Resolve the package version at runtime from package.json so the session briefing
|
|
@@ -407,11 +465,15 @@ export const BRAIN_TOOL_NAMES = new Set([
|
|
|
407
465
|
]);
|
|
408
466
|
// ── Free-tier Teaser-Gate ──────────────────────────────────────────────────
|
|
409
467
|
// When a Free-tier Brain crosses its recall limit, smart_recall still delivers
|
|
410
|
-
// the top
|
|
411
|
-
// behind an upgrade teaser ("🔒 N
|
|
468
|
+
// the top hit — the magic moment is sacred — but withholds the long tail
|
|
469
|
+
// behind an upgrade teaser ("🔒 N further relevant lessons withheld"). This is a
|
|
412
470
|
// DEPTH gate (hide the tail), never an access wall: the #1 result is always
|
|
413
471
|
// returned. Usage/limit come from the same authoritative numbers as the Brain
|
|
414
472
|
// Health bar (GET /instances/:id/memory), cached briefly to avoid per-call latency.
|
|
473
|
+
//
|
|
474
|
+
// Diese Zusage stand von 2026-07 bis 2026-08-18 NUR hier im Kommentar, waehrend
|
|
475
|
+
// der Zweig `visibleCount = gateActive ? 0 : 8` null Treffer zurueckgab. Seither
|
|
476
|
+
// ist sie eine Funktion mit Gegenprobe: ../recall-tiefe.ts (GROW-043).
|
|
415
477
|
const UPGRADE_URL = cachlyUrl('/billing', 'upgrade');
|
|
416
478
|
const _recallGateCache = new Map();
|
|
417
479
|
/**
|
|
@@ -480,6 +542,29 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
|
|
|
480
542
|
const { instance_id, topic, outcome, what_worked, what_failed = '', context: ctx = '', severity = 'major', file_paths = [], commands = [], tags = [], depends_on = [], author = '', visibility = 'team', service = '', service_kind = 'service', group = '', } = args;
|
|
481
543
|
const redis = await getConnection(instance_id);
|
|
482
544
|
const ts = new Date().toISOString();
|
|
545
|
+
/*
|
|
546
|
+
* ── Der Autor wird HERGELEITET, nicht erwartet ─────────────────────
|
|
547
|
+
*
|
|
548
|
+
* Gemessen am 19.08.2026: 195 von 493 Lektionen ohne Autor. `author`
|
|
549
|
+
* war ein optionales Feld, das ein Agent bei jedem Aufruf mitschicken
|
|
550
|
+
* sollte — und natuerlich vergisst. Dabei steht der Name in
|
|
551
|
+
* `git config user.name`.
|
|
552
|
+
*
|
|
553
|
+
* Ein Pflichtfeld, das man vergessen kann, ist kein Pflichtfeld.
|
|
554
|
+
*/
|
|
555
|
+
const herkunft = ermittleAutor({
|
|
556
|
+
uebergeben: author,
|
|
557
|
+
umgebung: process.env.CACHLY_AUTHOR,
|
|
558
|
+
git: echterGitLeser(process.env.CACHLY_WORKSPACE ?? process.cwd()),
|
|
559
|
+
});
|
|
560
|
+
// Mitautor nur, wenn ausdruecklich eingeschaltet — Standard ist aus.
|
|
561
|
+
let coautor = '';
|
|
562
|
+
try {
|
|
563
|
+
coautor = coautorAus(await redis.get(`cachly:prefs:${COAUTHOR_PREF_KEY}:${instance_id}`));
|
|
564
|
+
}
|
|
565
|
+
catch { /* eine Einstellung darf das Speichern nie stoppen */ }
|
|
566
|
+
const autorFeld = mitCoautor(herkunft.autor, coautor);
|
|
567
|
+
const autorTipp = autorHinweis(herkunft);
|
|
483
568
|
// ── Lessons-Review workflow (P1-1) ─────────────────────────────────────
|
|
484
569
|
// When the org enabled pending-mode on this (shared) brain, team-visible
|
|
485
570
|
// lessons become proposals awaiting owner/admin approval instead of
|
|
@@ -489,7 +574,7 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
|
|
|
489
574
|
return storeLessonProposal(redis, topic, {
|
|
490
575
|
topic, outcome, what_worked, what_failed, context: ctx, severity,
|
|
491
576
|
file_paths, commands, tags, depends_on,
|
|
492
|
-
...(
|
|
577
|
+
...(autorFeld ? { author: autorFeld } : {}),
|
|
493
578
|
...(service ? { service } : {}),
|
|
494
579
|
...(group ? { group: String(group).toLowerCase().trim() } : {}),
|
|
495
580
|
visibility, recall_count: 0, ts, confidence: 1.0, version: 3,
|
|
@@ -611,7 +696,9 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
|
|
|
611
696
|
commands,
|
|
612
697
|
tags,
|
|
613
698
|
depends_on,
|
|
614
|
-
|
|
699
|
+
// Auch hier der hergeleitete Name — sonst traegt die gespeicherte
|
|
700
|
+
// Lektion selbst keinen Autor, waehrend der Kausalgraph einen hat.
|
|
701
|
+
...(autorFeld ? { author: autorFeld } : {}),
|
|
615
702
|
...(service ? { service } : {}),
|
|
616
703
|
...(group ? { group: String(group).toLowerCase().trim() } : {}),
|
|
617
704
|
visibility,
|
|
@@ -633,11 +720,71 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
|
|
|
633
720
|
await redis.ltrim(listKey, -100, -1);
|
|
634
721
|
await redis.expire(listKey, 90 * 86400);
|
|
635
722
|
// Update best key for success/partial; for failure only update if no success exists
|
|
723
|
+
let bestGeschrieben = false;
|
|
636
724
|
if (outcome === 'success' || outcome === 'partial') {
|
|
637
725
|
await redis.set(`cachly:lesson:best:${topic}`, lesson);
|
|
726
|
+
bestGeschrieben = true;
|
|
638
727
|
}
|
|
639
728
|
else if (!existingRaw) {
|
|
640
729
|
await redis.set(`cachly:lesson:best:${topic}`, lesson);
|
|
730
|
+
bestGeschrieben = true;
|
|
731
|
+
}
|
|
732
|
+
// Der stehende Wortindex kennt die neue Lektion noch nicht.
|
|
733
|
+
//
|
|
734
|
+
// Ohne diese Zeile waere sie bis zu eine Minute lang nicht auffindbar —
|
|
735
|
+
// und genau der Fall "gerade gelernt, sofort gesucht" ist der, bei dem
|
|
736
|
+
// ein Gedaechtnis sich beweisen muss. Der Index wird beim naechsten
|
|
737
|
+
// Recall neu gebaut, nicht hier: Lernen soll nicht auf das Bauen warten.
|
|
738
|
+
if (bestGeschrieben)
|
|
739
|
+
wortindexEntwerten();
|
|
740
|
+
// ── Vektor fuer den Bedeutungsabgleich ─────────────────────────────────
|
|
741
|
+
//
|
|
742
|
+
// Wird beim Schreiben berechnet, nicht beim Suchen. Der Grund steht in
|
|
743
|
+
// Zahlen: die Einbettung kostet rund 280 ms Netz. Einmal je Lektion ist
|
|
744
|
+
// das nichts, bei jeder Frage waere es der halbe Gewinn.
|
|
745
|
+
//
|
|
746
|
+
// Bewusst NICHT abgewartet: wer eine Lektion speichert, soll nicht auf
|
|
747
|
+
// einen fremden Dienst warten. Faellt die Einbettung aus, fehlt der
|
|
748
|
+
// Vektor — und der Bedeutungsabgleich uebergeht diese eine Lektion still.
|
|
749
|
+
// Das ist der Preis dafuer, dass Lernen immer funktioniert, auch ohne
|
|
750
|
+
// Netz. Wie viele Lektionen einen Vektor haben, meldet brain_doctor.
|
|
751
|
+
if (bestGeschrieben && hasEmbedProvider()) {
|
|
752
|
+
void (async () => {
|
|
753
|
+
const gelesen = safeJsonParse(lesson, {});
|
|
754
|
+
try {
|
|
755
|
+
const v = await computeEmbedding(textFuerVektor(gelesen));
|
|
756
|
+
if (v?.length)
|
|
757
|
+
await redis.set(`${VEKTOR_PRAEFIX}${topic}`, packe(v));
|
|
758
|
+
}
|
|
759
|
+
catch { /* kein Vektor ist besser als keine Lektion */ }
|
|
760
|
+
// Der Themenname als eigene Sicht.
|
|
761
|
+
//
|
|
762
|
+
// Er ist kurz und frageaehnlich; eine 60-Zeichen-Frage gegen 60
|
|
763
|
+
// Zeichen zu halten ist symmetrisch, gegen 1376 Zeichen Volltext
|
|
764
|
+
// nicht. Der Sortierer bewertet beide getrennt (naeheText 1,0,
|
|
765
|
+
// naeheThema 0,6). Bis zum 20.08.2026 gab es diese Vektoren nur im
|
|
766
|
+
// Messstand — deshalb war rangfolge.ts auch nie verdrahtet.
|
|
767
|
+
try {
|
|
768
|
+
const nv = await computeEmbedding(textFuerNamensVektor(topic));
|
|
769
|
+
if (nv?.length)
|
|
770
|
+
await redis.set(`${NAME_VEKTOR_PRAEFIX}${topic}`, packe(nv));
|
|
771
|
+
}
|
|
772
|
+
catch { /* siehe oben */ }
|
|
773
|
+
// Die zweite Tuer: woertliche Fehlertexte als eigene Eingaenge.
|
|
774
|
+
//
|
|
775
|
+
// Gemessen am 20.08.2026 an 499 Lektionen mit zwei unabhaengigen
|
|
776
|
+
// 100-Fragen-Saetzen: von fuenf ausprobierten Tuerarten traegt genau
|
|
777
|
+
// DIESE eine. Erstsatz und Uebersetzung brachten null, der Themenname
|
|
778
|
+
// steckt schon im Sortierer. Volle Zahlen und Gegenproben stehen in
|
|
779
|
+
// .agent/cachly/tor0-tor1-ergebnis.md.
|
|
780
|
+
//
|
|
781
|
+
// Genau wie der Vektor: nicht abgewartet, Fehler verschluckt. Eine
|
|
782
|
+
// Lektion muss auch ohne Netz gespeichert werden koennen.
|
|
783
|
+
try {
|
|
784
|
+
await schreibeEingaenge(redis, topic, gelesen, computeEmbedding);
|
|
785
|
+
}
|
|
786
|
+
catch { /* keine Eingaenge ist besser als keine Lektion */ }
|
|
787
|
+
})();
|
|
641
788
|
}
|
|
642
789
|
// Track in decision log for session replay
|
|
643
790
|
try {
|
|
@@ -711,7 +858,7 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
|
|
|
711
858
|
fix_confidence: existEdge.confidence,
|
|
712
859
|
fix_trials: existEdge.trials,
|
|
713
860
|
failure_outcome: outcome,
|
|
714
|
-
reported_by:
|
|
861
|
+
reported_by: autorFeld || 'unknown',
|
|
715
862
|
what_failed: what_failed || '',
|
|
716
863
|
resolved: false,
|
|
717
864
|
}), 'EX', 60 * 60 * 24 * 90);
|
|
@@ -779,14 +926,19 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
|
|
|
779
926
|
// ── Phase 3A: People + File nodes in knowledge graph ──────────────────────
|
|
780
927
|
// Builds the "who knows what" map automatically from author + file_paths.
|
|
781
928
|
// Person → authored → Concept edges power brain_who_knows queries.
|
|
782
|
-
|
|
929
|
+
//
|
|
930
|
+
// Seit dem 19.08.2026 steht hier autorFeld statt author: der Name wird
|
|
931
|
+
// aus git hergeleitet, wenn der Aufrufer keinen mitschickt. Vorher blieb
|
|
932
|
+
// dieser ganze Block bei zwei Fuenfteln aller Lektionen ungenutzt — und
|
|
933
|
+
// damit brain_who_knows leer.
|
|
934
|
+
if (autorFeld) {
|
|
783
935
|
// Agent activity registry (Move 4): track which agents are actively
|
|
784
936
|
// writing to this Brain, keyed by author, with a 1h TTL. Powers
|
|
785
937
|
// brain_conflicts' "who is live" view and multi-agent arbitration.
|
|
786
|
-
redis.set(`cachly:agents:active:${
|
|
938
|
+
redis.set(`cachly:agents:active:${autorFeld}`, JSON.stringify({ author: autorFeld, last_topic: topic, last_outcome: outcome, ts: new Date().toISOString() }), 'EX', 60 * 60).catch(() => { });
|
|
787
939
|
try {
|
|
788
940
|
const domain = topic.split(':')[0] ?? 'unknown';
|
|
789
|
-
const personId = await ckgUpsertPersonNode(redis,
|
|
941
|
+
const personId = await ckgUpsertPersonNode(redis, nurMensch(autorFeld), domain);
|
|
790
942
|
const conceptId = ckgSlug(topic);
|
|
791
943
|
await ckgUpdateEdge(redis, personId, 'authored', conceptId, outcome === 'success', outcome === 'partial');
|
|
792
944
|
for (const fp of file_paths.slice(0, 8)) {
|
|
@@ -811,8 +963,8 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
|
|
|
811
963
|
const conceptId = ckgSlug(topic);
|
|
812
964
|
// concept → affects → service (outcome decides edge confidence)
|
|
813
965
|
await ckgUpdateEdge(redis, conceptId, 'affects', serviceId, outcome === 'success', outcome === 'partial');
|
|
814
|
-
if (
|
|
815
|
-
const personId = `person:${ckgSlug(
|
|
966
|
+
if (autorFeld) {
|
|
967
|
+
const personId = `person:${ckgSlug(nurMensch(autorFeld))}`;
|
|
816
968
|
await ckgUpdateEdge(redis, personId, 'operates', serviceId, true);
|
|
817
969
|
}
|
|
818
970
|
for (const fp of file_paths.slice(0, 8)) {
|
|
@@ -826,12 +978,12 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
|
|
|
826
978
|
// auto-store as a team lesson so knowledge is shared without a separate
|
|
827
979
|
// team_learn call. Tags gain 'team'; the lesson is attributed to the author.
|
|
828
980
|
// This makes team knowledge sharing the default, not the opt-in.
|
|
829
|
-
if (
|
|
981
|
+
if (autorFeld && (outcome === 'success' || outcome === 'partial')) {
|
|
830
982
|
const teamLesson = {
|
|
831
983
|
topic, outcome, what_worked,
|
|
832
984
|
what_failed: what_failed ?? '',
|
|
833
985
|
severity,
|
|
834
|
-
author,
|
|
986
|
+
author: autorFeld,
|
|
835
987
|
file_paths,
|
|
836
988
|
commands,
|
|
837
989
|
tags: [...new Set([...tags, 'team'])],
|
|
@@ -860,14 +1012,22 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
|
|
|
860
1012
|
isUpdate
|
|
861
1013
|
? `♻️ Updated (recall count: ${recallCount} · audit entries: ${auditTrail.length})`
|
|
862
1014
|
: `💡 Recall later with \`recall_best_solution(topic="${topic}")\``,
|
|
863
|
-
(
|
|
864
|
-
? `👥 Auto-shared with team (by _${
|
|
1015
|
+
(autorFeld && (outcome === 'success' || outcome === 'partial'))
|
|
1016
|
+
? `👥 Auto-shared with team (by _${autorFeld}_${herkunft.quelle !== 'uebergeben' ? ` · aus ${herkunft.quelle}` : ''}) — visible in \`team_recall\``
|
|
865
1017
|
: '',
|
|
1018
|
+
autorTipp ?? '',
|
|
866
1019
|
depends_on.length > 0
|
|
867
1020
|
? `🔗 Depends on: ${depends_on.map(d => `\`${d}\``).join(', ')} → trace with \`trace_dependency\``
|
|
868
1021
|
: '',
|
|
869
1022
|
...templateWarnings,
|
|
870
1023
|
...iWasWrongWarning,
|
|
1024
|
+
// GROW-044: Die 100-Zeichen-Regel wird geprueft, nicht geglaubt.
|
|
1025
|
+
// Das Briefing der naechsten Sitzung zeigt je Lektion rund 100
|
|
1026
|
+
// Zeichen; am 16.08.2026 stand die gesuchte Adresse an Zeichen 323,
|
|
1027
|
+
// die Lektion war eingeblendet und der Fehler passierte trotzdem.
|
|
1028
|
+
// Hinweis statt Ablehnung: es gibt Lektionen ohne harte Zahl, und eine
|
|
1029
|
+
// abgelehnte Lektion ist immer schlechter als eine unscharfe.
|
|
1030
|
+
vorspannHinweis(what_worked) ?? '',
|
|
871
1031
|
].filter(l => l !== '').join('\n');
|
|
872
1032
|
}
|
|
873
1033
|
case 'recall_best_solution': {
|
|
@@ -898,9 +1058,12 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
|
|
|
898
1058
|
// A targeted lookup is a recall too, and costs the same one quota unit
|
|
899
1059
|
// as a broad smart_recall — the user asked the brain a question either way.
|
|
900
1060
|
void bumpRecallQuota(redis);
|
|
901
|
-
// Track estimated time saved (30m minor · 60m major · 240m critical)
|
|
902
|
-
|
|
903
|
-
|
|
1061
|
+
// Track estimated time saved (30m minor · 60m major · 240m critical).
|
|
1062
|
+
// Starter-Lektionen zaehlen 0 — siehe wertbeitrag.ts.
|
|
1063
|
+
const savedMins = ersparteMinuten(lesson);
|
|
1064
|
+
if (savedMins > 0) {
|
|
1065
|
+
redis.incrbyfloat(`cachly:stats:time_saved_mins:${instance_id}`, savedMins).catch(() => { });
|
|
1066
|
+
}
|
|
904
1067
|
const sevEmoji = lesson.severity === 'critical' ? '🔴' : lesson.severity === 'major' ? '🟡' : lesson.severity ? '🟢' : '';
|
|
905
1068
|
const auditSummary = (lesson.audit_trail ?? []).length > 1
|
|
906
1069
|
? `_Audit: ${(lesson.audit_trail ?? []).length} changes · stored ${new Date(lesson.ts).toLocaleDateString('de-DE')}_`
|
|
@@ -1006,11 +1169,179 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
|
|
|
1006
1169
|
// ── Layer 1.5: Quality-aware rerank — proven success lessons outrank
|
|
1007
1170
|
// text-similar failed attempts (the moat; see src/rerank.ts + Cachly-Bench). ──
|
|
1008
1171
|
const kwMatches = rerankByQuality(rawMatches);
|
|
1172
|
+
// ── Layer 1.7: Bedeutungsabgleich ──────────────────────────────────────
|
|
1173
|
+
//
|
|
1174
|
+
// Der Wortabgleich findet, was woertlich dasteht. Eine Frage beschreibt
|
|
1175
|
+
// aber ein SYMPTOM, die Lektion eine URSACHE — "Der Deploy haengt beim
|
|
1176
|
+
// Bauen" und "No space left on device" teilen kein Wort und meinen
|
|
1177
|
+
// dasselbe.
|
|
1178
|
+
//
|
|
1179
|
+
// Gemessen am 19.08.2026 an 499 echten Lektionen mit 100 Fragen:
|
|
1180
|
+
//
|
|
1181
|
+
// Verfahren Platz 1 Top 3 Top 10
|
|
1182
|
+
// nur Woerter 21 % 33 % 47 %
|
|
1183
|
+
// nur Bedeutung 40 % 60 % 68 %
|
|
1184
|
+
// gemischt 30/70 39 % 55 % 71 %
|
|
1185
|
+
//
|
|
1186
|
+
// Gemischt wird, nicht ersetzt: ohne Netz gibt es keine Einbettung, und
|
|
1187
|
+
// die ersten zehn Treffer sind gemischt besser als bei beiden einzeln.
|
|
1188
|
+
//
|
|
1189
|
+
// Faellt hier etwas aus, bleibt es beim Wortabgleich. Ein Recall, der
|
|
1190
|
+
// wegen eines fremden Dienstes gar nichts liefert, waere schlimmer als
|
|
1191
|
+
// einer, der schlechter sortiert.
|
|
1192
|
+
//
|
|
1193
|
+
// Jeder Ausstieg hier meldet sich EINMAL je Prozess (siehe
|
|
1194
|
+
// `aussetzer.ts`). Am 20.08.2026 wurde gemessen, dass dieser Pfad in
|
|
1195
|
+
// Produktion monatelang stumm ausstieg: 0 Vektoren bei 506 Lektionen.
|
|
1196
|
+
// Von aussen war das von "alles in Ordnung" nicht zu unterscheiden.
|
|
1197
|
+
let sinnAngewandt = false;
|
|
1198
|
+
const kwGemischt = await (async () => {
|
|
1199
|
+
if (!hasEmbedProvider()) {
|
|
1200
|
+
meldeEinmal(OHNE_DIENST, 'kein Einbettungsdienst eingerichtet — die Suche laeuft nur ueber Woerter');
|
|
1201
|
+
return kwMatches;
|
|
1202
|
+
}
|
|
1203
|
+
try {
|
|
1204
|
+
await vektorbestand.aktualisiere(redis);
|
|
1205
|
+
if (vektorbestand.groesse === 0) {
|
|
1206
|
+
meldeEinmal(OHNE_VEKTOREN, 'keine einzige Lektion hat eine Einbettung — die Suche laeuft nur ueber '
|
|
1207
|
+
+ 'Woerter. Nachruesten mit eingaenge-nachruesten, pruefen mit brain_doctor');
|
|
1208
|
+
return kwMatches;
|
|
1209
|
+
}
|
|
1210
|
+
const frageVektor = await computeEmbedding(query);
|
|
1211
|
+
if (!frageVektor?.length) {
|
|
1212
|
+
meldeEinmal(OHNE_FRAGEVEKTOR, 'die Frage liess sich nicht einbetten — diese Suche laeuft nur ueber Woerter');
|
|
1213
|
+
return kwMatches;
|
|
1214
|
+
}
|
|
1215
|
+
await namensbestand.aktualisiere(redis);
|
|
1216
|
+
await eingangsbestand.aktualisiere(redis);
|
|
1217
|
+
await seltenheitsbestand.aktualisiere(redis);
|
|
1218
|
+
const wortThemen = kwMatches
|
|
1219
|
+
.filter((m) => m.key.startsWith('cachly:lesson:best:'))
|
|
1220
|
+
.map((m) => m.key.slice('cachly:lesson:best:'.length));
|
|
1221
|
+
// ── Die Vorauswahl ────────────────────────────────────────────────
|
|
1222
|
+
//
|
|
1223
|
+
// Woerter (Top 25) vereinigt mit Bedeutung (Top 25). Bei vorhandenen
|
|
1224
|
+
// Eingaengen zaehlt je Lektion ihr BESTER Eingang statt des
|
|
1225
|
+
// Volltextvektors — gemessen am 20.08.2026 hebt das die Findequote@3
|
|
1226
|
+
// von 52 auf 56 Prozent, und mit dem Merkmal unten auf 58.
|
|
1227
|
+
//
|
|
1228
|
+
// Der Preis steht offen im Ergebnisdokument: dieselben Eingaenge
|
|
1229
|
+
// SENKEN die Decke von 89 auf 86 Prozent. Fuer den Nutzer zaehlt die
|
|
1230
|
+
// Findequote — er sieht drei Lektionen, nicht 25.
|
|
1231
|
+
//
|
|
1232
|
+
// WICHTIG: das Maximum ueber ALLE Tueren — Volltext, Themenname UND
|
|
1233
|
+
// Fehlertext. Der erste Versuch am 20.08. hat die Volltext-Auswahl
|
|
1234
|
+
// durch die Eingangs-Auswahl ERSETZT. Das war falsch: nur 399 von 507
|
|
1235
|
+
// Lektionen tragen einen Fehlertext, die uebrigen 108 fielen damit
|
|
1236
|
+
// aus der Vorauswahl. Am echten Bestand gemessen fiel die Trefferlage
|
|
1237
|
+
// von 90 auf 70 Prozent und die Findequote@3 von 50 auf 48.
|
|
1238
|
+
const naeheBesteTuer = (t) => Math.max(vektorbestand.naehe(frageVektor, t), namensbestand.naehe(frageVektor, t), eingangsbestand.besteNaehe(frageVektor, t));
|
|
1239
|
+
const alleThemen = [...seltenheitsbestand.themen()];
|
|
1240
|
+
const sinnThemen = (alleThemen.length > 0
|
|
1241
|
+
? alleThemen
|
|
1242
|
+
: vektorbestand.aehnlichste(frageVektor, 500).map((x) => x.topic))
|
|
1243
|
+
.map((t) => ({ t, n: naeheBesteTuer(t) }))
|
|
1244
|
+
.sort((a, b) => b.n - a.n)
|
|
1245
|
+
.slice(0, 25)
|
|
1246
|
+
.map((x) => x.t);
|
|
1247
|
+
// ── Die Sortierung ───────────────────────────────────────────────
|
|
1248
|
+
//
|
|
1249
|
+
// Bis zum 20.08.2026 stand hier `mischeRangfolgen` (Reciprocal Rank
|
|
1250
|
+
// Fusion). Das war der Grund, warum die ganze Eingangs-Arbeit im
|
|
1251
|
+
// Produkt fast nichts brachte: RRF mischt ueber PLATZIERUNGEN und
|
|
1252
|
+
// kann die Naehe zum besten Eingang nicht als abgestufte Groesse
|
|
1253
|
+
// verwerten.
|
|
1254
|
+
//
|
|
1255
|
+
// frischer Pruefsatz, Findequote@3
|
|
1256
|
+
// RRF, wie ausgeliefert 50 %
|
|
1257
|
+
// RRF + Eingaenge als dritte Quelle 51 %
|
|
1258
|
+
// bewerteTopf 52 %
|
|
1259
|
+
// bewerteTopf + Eingaenge 58 %
|
|
1260
|
+
//
|
|
1261
|
+
// Faellt ein Merkmal aus (kein Namensvektor, kein Eingang), meldet es
|
|
1262
|
+
// -2 und `spreizeImTopf` macht daraus fuer alle dieselbe Null — die
|
|
1263
|
+
// Sortierung faellt dann auf die uebrigen Merkmale zurueck, statt
|
|
1264
|
+
// falsch zu werden.
|
|
1265
|
+
const topf = [...new Set([...wortThemen, ...sinnThemen])];
|
|
1266
|
+
const besteDrei = sinnThemen.slice(0, 3)
|
|
1267
|
+
.map((t) => vektorbestand.rohvektor(t)).filter(Boolean);
|
|
1268
|
+
const angereichert = besteDrei.length ? reichereAn(frageVektor, besteDrei) : frageVektor;
|
|
1269
|
+
const frageWoerter = inhaltsWoerter(query);
|
|
1270
|
+
const statistik = seltenheitsbestand.statistik;
|
|
1271
|
+
const bewertbar = topf.map((t) => ({
|
|
1272
|
+
naeheText: vektorbestand.naehe(frageVektor, t),
|
|
1273
|
+
naeheThema: namensbestand.naehe(frageVektor, t),
|
|
1274
|
+
naeheRueckkopplung: vektorbestand.naehe(angereichert, t),
|
|
1275
|
+
seltenheitsDeckung: statistik
|
|
1276
|
+
? statistik.deckung(frageWoerter, new Set([...inhaltsWoerter(seltenheitsbestand.textVon(t))].map(grobStamm)))
|
|
1277
|
+
: 0,
|
|
1278
|
+
}));
|
|
1279
|
+
let punkte = bewerteTopf(bewertbar, GEWICHTE);
|
|
1280
|
+
if (eingangsbestand.groesse > 0) {
|
|
1281
|
+
const naehen = topf.map((t) => eingangsbestand.besteNaehe(frageVektor, t));
|
|
1282
|
+
const gespreizt = spreizeImTopf(naehen);
|
|
1283
|
+
punkte = punkte.map((pkt, i) => pkt + EINGANG_GEWICHT * gespreizt[i]);
|
|
1284
|
+
}
|
|
1285
|
+
const reihenfolge = topf
|
|
1286
|
+
.map((t, i) => ({ t, p: punkte[i] }))
|
|
1287
|
+
.sort((a, b) => b.p - a.p)
|
|
1288
|
+
.map((x) => x.t);
|
|
1289
|
+
const bekannt = new Map(kwMatches.map((m) => [m.key, m]));
|
|
1290
|
+
// Lektionen, die NUR der Bedeutungsabgleich gefunden hat, muessen
|
|
1291
|
+
// nachgeladen werden — der Wortabgleich hat sie nie angefasst.
|
|
1292
|
+
const sortiert = [];
|
|
1293
|
+
for (const thema of reihenfolge) {
|
|
1294
|
+
const key = `cachly:lesson:best:${thema}`;
|
|
1295
|
+
const da = bekannt.get(key);
|
|
1296
|
+
if (da) {
|
|
1297
|
+
sortiert.push(da);
|
|
1298
|
+
bekannt.delete(key);
|
|
1299
|
+
continue;
|
|
1300
|
+
}
|
|
1301
|
+
const inhalt = await redis.get(key).catch(() => null);
|
|
1302
|
+
if (inhalt) {
|
|
1303
|
+
sortiert.push({ key, content: inhalt, score: 0, matchedWords: [], finalScore: 0, qualityBoost: 1 });
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
// Alles, was kein Thema war (Kontexteintraege, Index), bleibt hinten
|
|
1307
|
+
// erhalten statt still zu verschwinden.
|
|
1308
|
+
sortiert.push(...bekannt.values());
|
|
1309
|
+
sinnAngewandt = true;
|
|
1310
|
+
return sortiert;
|
|
1311
|
+
}
|
|
1312
|
+
catch {
|
|
1313
|
+
return kwMatches;
|
|
1314
|
+
}
|
|
1315
|
+
})();
|
|
1316
|
+
void sinnAngewandt;
|
|
1009
1317
|
// One quota unit for this call, regardless of how many lessons it surfaced.
|
|
1010
1318
|
void bumpRecallQuota(redis);
|
|
1319
|
+
// ── Gedaechtniszellen ────────────────────────────────────────────────
|
|
1320
|
+
//
|
|
1321
|
+
// Diese Frage hat diese Lektionen gefunden — das ist ein bewiesener Weg
|
|
1322
|
+
// zu ihnen. Fuehrt dieselbe Frage ein ZWEITES Mal hierher, wird sie ein
|
|
1323
|
+
// eigener Eingang, und beim naechsten Mal ist der Weg breiter.
|
|
1324
|
+
//
|
|
1325
|
+
// Warum erst beim zweiten Mal: das Signal "wurde benutzt" gibt es nicht.
|
|
1326
|
+
// Der Server sieht, was er ausgeliefert hat, nicht was jemand damit
|
|
1327
|
+
// gemacht hat. Wiederholung IST das, was einen Trampelpfad ausmacht.
|
|
1328
|
+
//
|
|
1329
|
+
// Nur die ersten DREI: das ist, was die Einblendung zeigt. Wer alle
|
|
1330
|
+
// Treffer aufzeichnet, macht aus jedem Streifschuss einen Weg.
|
|
1331
|
+
//
|
|
1332
|
+
// Nicht abgewartet: wer sucht, soll nicht warten, bis das Gedaechtnis
|
|
1333
|
+
// nachgefuehrt ist.
|
|
1334
|
+
if (hasEmbedProvider()) {
|
|
1335
|
+
const gefundeneThemen = kwGemischt
|
|
1336
|
+
.filter((m) => m.key.startsWith('cachly:lesson:best:'))
|
|
1337
|
+
.slice(0, 3)
|
|
1338
|
+
.map((m) => m.key.slice('cachly:lesson:best:'.length));
|
|
1339
|
+
void spurLegen(redis, query, gefundeneThemen, computeEmbedding)
|
|
1340
|
+
.catch(() => { });
|
|
1341
|
+
}
|
|
1011
1342
|
const savedHere = [];
|
|
1012
1343
|
// Exclude archived lessons — they are kept for audit but must not surface in recall.
|
|
1013
|
-
const lessonMatches =
|
|
1344
|
+
const lessonMatches = kwGemischt.filter(m => m.key.startsWith('cachly:lesson:best:'));
|
|
1014
1345
|
let crossAuthorThisCall = 0;
|
|
1015
1346
|
let successRecallsThisCall = 0;
|
|
1016
1347
|
for (const m of lessonMatches.slice(0, 5)) {
|
|
@@ -1024,9 +1355,11 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
|
|
|
1024
1355
|
const recalledAt = new Date().toISOString();
|
|
1025
1356
|
const updated = { ...lesson, recall_count: (lesson.recall_count ?? 0) + 1, verified_at: recalledAt, last_recalled_at: recalledAt };
|
|
1026
1357
|
redis.set(m.key, JSON.stringify(updated)).catch(() => { });
|
|
1027
|
-
|
|
1028
|
-
const savedMins =
|
|
1029
|
-
|
|
1358
|
+
// Starter-Lektionen zaehlen 0 — siehe wertbeitrag.ts.
|
|
1359
|
+
const savedMins = ersparteMinuten(lesson);
|
|
1360
|
+
if (savedMins > 0) {
|
|
1361
|
+
redis.incrbyfloat(`cachly:stats:time_saved_mins:${instance_id}`, savedMins).catch(() => { });
|
|
1362
|
+
}
|
|
1030
1363
|
// Metric 3 (team-knowledge-reuse): a recall of a lesson authored by
|
|
1031
1364
|
// someone OTHER than the requester is cross-author reuse — the value
|
|
1032
1365
|
// only cachly delivers. Track total proven recalls + cross-author ones.
|
|
@@ -1044,11 +1377,16 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
|
|
|
1044
1377
|
}
|
|
1045
1378
|
if (lesson.outcome === 'success')
|
|
1046
1379
|
successRecallsThisCall++;
|
|
1047
|
-
// Surface banner for proven successes (recall_count >= 1 means it's been validated)
|
|
1048
|
-
|
|
1380
|
+
// Surface banner for proven successes (recall_count >= 1 means it's been validated).
|
|
1381
|
+
//
|
|
1382
|
+
// Der Startvorrat kommt hier NICHT ins Banner. "Brain saved you ~8h
|
|
1383
|
+
// here — docker:layer-cache" ist keine Leistung des Gedaechtnisses,
|
|
1384
|
+
// sondern mitgeliefertes Allgemeinwissen. Genau dieses Banner hat
|
|
1385
|
+
// die Zahl im Panel jahrelang glaubwuerdig aussehen lassen.
|
|
1386
|
+
if (lesson.outcome === 'success' && (lesson.recall_count ?? 0) >= 1 && !istStarterLektion(lesson)) {
|
|
1049
1387
|
savedHere.push({
|
|
1050
1388
|
topic: m.key.replace('cachly:lesson:best:', ''),
|
|
1051
|
-
severity:
|
|
1389
|
+
severity: lesson.severity ?? 'major',
|
|
1052
1390
|
recall_count: (lesson.recall_count ?? 0) + 1,
|
|
1053
1391
|
savedMins,
|
|
1054
1392
|
ts: lesson.ts,
|
|
@@ -1098,12 +1436,12 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
|
|
|
1098
1436
|
// Semantic search failed silently — keyword results are enough
|
|
1099
1437
|
}
|
|
1100
1438
|
}
|
|
1101
|
-
const bm25Scores =
|
|
1439
|
+
const bm25Scores = kwGemischt.map(m => m.score);
|
|
1102
1440
|
const bm25Min = bm25Scores.length ? Math.min(...bm25Scores) : 0;
|
|
1103
1441
|
const bm25Range = bm25Scores.length ? (Math.max(...bm25Scores) - bm25Min) || 1 : 1;
|
|
1104
1442
|
const bm25Norm = (s) => (s - bm25Min) / bm25Range;
|
|
1105
1443
|
const hybridMap = new Map();
|
|
1106
|
-
for (const m of
|
|
1444
|
+
for (const m of kwGemischt) {
|
|
1107
1445
|
const n = bm25Norm(m.score);
|
|
1108
1446
|
hybridMap.set(m.key, {
|
|
1109
1447
|
key: m.key, content: m.content, bm25Score: n,
|
|
@@ -1227,14 +1565,49 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
|
|
|
1227
1565
|
return true;
|
|
1228
1566
|
})
|
|
1229
1567
|
.sort((a, b) => b.hybridScore - a.hybridScore);
|
|
1568
|
+
// ── Versuch (Messtechnik, siehe versuch.ts) ───────────────────────────────
|
|
1569
|
+
//
|
|
1570
|
+
// Standardmaessig AUS: `versuchStart()` gibt dann `null` zurueck, und
|
|
1571
|
+
// dieser ganze Block tut nichts — `hybridResults` bleibt unangetastet,
|
|
1572
|
+
// nichts wird nach Redis geschrieben, kein Zeichen im Text aendert sich.
|
|
1573
|
+
// Erst mit `CACHLY_VERSUCH=an` UND gesetztem Salz greift die Zuteilung.
|
|
1574
|
+
const versuchSalz = versuchStart();
|
|
1575
|
+
let versuchKennung = '';
|
|
1576
|
+
let versuchTeil = null;
|
|
1577
|
+
if (versuchSalz !== null) {
|
|
1578
|
+
versuchKennung = neueKennung();
|
|
1579
|
+
const hoechstePunktzahl = hybridResults[0]?.hybridScore ?? 0;
|
|
1580
|
+
const ergebnis = wendeZuteilungAn(hybridResults.map((r) => ({ key: r.key, content: r.content, punktzahl: r.hybridScore })), versuchKennung, versuchSalz, hoechstePunktzahl);
|
|
1581
|
+
if (ergebnis.weggelassenesThema !== undefined) {
|
|
1582
|
+
// HOLD: der bisher beste Treffer wird aus der ausgelieferten Liste
|
|
1583
|
+
// entfernt. Die uebrigen Treffer und ihre Reihenfolge bleiben gleich.
|
|
1584
|
+
hybridResults.splice(0, 1);
|
|
1585
|
+
}
|
|
1586
|
+
else if (ergebnis.markierungGesetzt && hybridResults[0] && ergebnis.treffer[0]) {
|
|
1587
|
+
// DELIVER: derselbe Treffer, aber sein Inhalt traegt jetzt die
|
|
1588
|
+
// Kanarien-Markierung am Ende von `what_worked`.
|
|
1589
|
+
hybridResults[0] = { ...hybridResults[0], content: ergebnis.treffer[0].content };
|
|
1590
|
+
}
|
|
1591
|
+
versuchTeil = {
|
|
1592
|
+
frage: query,
|
|
1593
|
+
hoechstePunktzahl,
|
|
1594
|
+
themen: [], // unten befuellt, sobald feststeht, was wirklich sichtbar ist
|
|
1595
|
+
zulassungsfaehig: ergebnis.zulassungsfaehig,
|
|
1596
|
+
zulassungsGrund: ergebnis.zulassungsGrund,
|
|
1597
|
+
zuteilung: ergebnis.zuteilung,
|
|
1598
|
+
weggelassenesThema: ergebnis.weggelassenesThema,
|
|
1599
|
+
markierungGesetzt: ergebnis.markierungGesetzt,
|
|
1600
|
+
};
|
|
1601
|
+
}
|
|
1230
1602
|
// ── Teaser-Gate: free tier over its recall limit hides the long tail ──────
|
|
1231
1603
|
const recallGate = await getRecallGate(instance_id, apiFetch);
|
|
1232
|
-
//
|
|
1233
|
-
// recall limit, the
|
|
1234
|
-
//
|
|
1235
|
-
//
|
|
1604
|
+
// Depth gate: once a free user is over their (monthly, goodwill-adjusted)
|
|
1605
|
+
// recall limit, the LONG TAIL is withheld — never the top hit. The rule
|
|
1606
|
+
// lives in recall-tiefe.ts with tests (GROW-043), because it used to live
|
|
1607
|
+
// only in a comment while the branch said `? 0 :` and returned nothing.
|
|
1236
1608
|
const gateActive = recallGate.reached;
|
|
1237
|
-
const
|
|
1609
|
+
const tiefe = recallTiefe({ ueberLimit: gateActive, gesamt: hybridResults.length, volleTiefe: TIEFE_VOLL });
|
|
1610
|
+
const visibleCount = tiefe.sichtbar;
|
|
1238
1611
|
// ── Build output ──────────────────────────────────────────────────────────
|
|
1239
1612
|
const lines = [`🧠 **Smart Recall** for: _"${query}"_\n`];
|
|
1240
1613
|
// Warm goodwill note — shown the month a free user is gifted +250 recalls.
|
|
@@ -1288,7 +1661,8 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
|
|
|
1288
1661
|
// Group by sub-query for multi-topic queries
|
|
1289
1662
|
if (subQueries.length > 1) {
|
|
1290
1663
|
const grouped = new Map();
|
|
1291
|
-
|
|
1664
|
+
const tiefeMehrthemig = recallTiefe({ ueberLimit: gateActive, gesamt: hybridResults.length, volleTiefe: TIEFE_VOLL_MEHRTHEMIG });
|
|
1665
|
+
for (const r of hybridResults.slice(0, tiefeMehrthemig.sichtbar)) {
|
|
1292
1666
|
const sq = r.subQuery ?? query;
|
|
1293
1667
|
if (!grouped.has(sq))
|
|
1294
1668
|
grouped.set(sq, []);
|
|
@@ -1307,7 +1681,7 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
|
|
|
1307
1681
|
: r.matchType === 'semantic'
|
|
1308
1682
|
? `sem: ${((r.semScore ?? 0) * 100).toFixed(0)}%, 🎯 semantic`
|
|
1309
1683
|
: `BM25: ${(r.bm25Score ?? 0).toFixed(2)}, matched: ${r.matchedWords?.join(', ')}`;
|
|
1310
|
-
const
|
|
1684
|
+
const vorschau = recallVorschau(r.key, r.content, 300);
|
|
1311
1685
|
lines.push(` **${label}** _(${scorePart})_`);
|
|
1312
1686
|
// Lesbares Brain (docs/produkt/team-puls.md P1): plain-language layer
|
|
1313
1687
|
// stored at lesson:human:<topic> — display-only, ranking untouched.
|
|
@@ -1318,7 +1692,9 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
|
|
|
1318
1692
|
if (h.title)
|
|
1319
1693
|
lines.push(` 📖 **${h.title}** — ${h.summary ?? ''}`);
|
|
1320
1694
|
}
|
|
1321
|
-
|
|
1695
|
+
for (const z of vorschau)
|
|
1696
|
+
lines.push(` > ${z}`);
|
|
1697
|
+
lines.push('');
|
|
1322
1698
|
}
|
|
1323
1699
|
}
|
|
1324
1700
|
const matched = [...grouped.keys()];
|
|
@@ -1342,7 +1718,7 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
|
|
|
1342
1718
|
: r.matchType === 'semantic'
|
|
1343
1719
|
? `sem: ${((r.semScore ?? 0) * 100).toFixed(0)}%, 🎯 semantic`
|
|
1344
1720
|
: `BM25: ${(r.bm25Score ?? 0).toFixed(2)}, matched: ${r.matchedWords?.join(', ')}`;
|
|
1345
|
-
const
|
|
1721
|
+
const vorschau = recallVorschau(r.key, r.content, 400);
|
|
1346
1722
|
lines.push(`**${label}**${authorBadge}${contextBadge} _(${scorePart})_`);
|
|
1347
1723
|
// Lesbares Brain (docs/produkt/team-puls.md P1): plain-language layer.
|
|
1348
1724
|
if (r.key.startsWith('cachly:lesson:best:')) {
|
|
@@ -1352,14 +1728,18 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
|
|
|
1352
1728
|
if (h.title)
|
|
1353
1729
|
lines.push(`📖 **${h.title}** — ${h.summary ?? ''}`);
|
|
1354
1730
|
}
|
|
1355
|
-
|
|
1731
|
+
for (const z of vorschau)
|
|
1732
|
+
lines.push(`> ${z}`);
|
|
1733
|
+
lines.push('');
|
|
1356
1734
|
}
|
|
1357
1735
|
}
|
|
1358
|
-
// ──
|
|
1359
|
-
|
|
1360
|
-
|
|
1736
|
+
// ── Depth-gate footer: the top hit stayed, the tail is withheld ────────
|
|
1737
|
+
// Only speak when something was actually held back — announcing
|
|
1738
|
+
// "0 lessons withheld" would sell a wall that isn't there.
|
|
1739
|
+
if (gateActive && tiefe.zurueckgehalten > 0) {
|
|
1740
|
+
const withheld = tiefe.zurueckgehalten;
|
|
1361
1741
|
lines.push(`🔒 **Free recall limit reached** (${recallGate.used}/${recallGate.limit} this month). ` +
|
|
1362
|
-
|
|
1742
|
+
`Showing your top match; ${withheld} further relevant lesson${withheld !== 1 ? 's are' : ' is'} withheld. ` +
|
|
1363
1743
|
`Your limit resets next month, or unlock your full Brain now → ${UPGRADE_URL}\n`);
|
|
1364
1744
|
}
|
|
1365
1745
|
}
|
|
@@ -1397,6 +1777,15 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
|
|
|
1397
1777
|
lines.push(` • Use \`recall_best_solution("topic")\` for exact topic lookup`);
|
|
1398
1778
|
}
|
|
1399
1779
|
}
|
|
1780
|
+
// Versuch, Abschluss: Kanarienvogel gegen die WIRKLICH zusammengebaute
|
|
1781
|
+
// Antwort pruefen, Markierung entfernen, Protokoll schreiben (schlaegt
|
|
1782
|
+
// das fehl, bleibt die Antwort trotzdem stehen — siehe schreibeTurnProtokoll).
|
|
1783
|
+
if (versuchSalz !== null && versuchTeil) {
|
|
1784
|
+
const themen = hybridResults.slice(0, visibleCount)
|
|
1785
|
+
.filter((r) => r.key.startsWith('cachly:lesson:best:'))
|
|
1786
|
+
.map((r) => r.key.slice('cachly:lesson:best:'.length));
|
|
1787
|
+
return schliesseVersuchAb(redis, versuchKennung, { ...versuchTeil, themen }, lines.join('\n'));
|
|
1788
|
+
}
|
|
1400
1789
|
return lines.join('\n');
|
|
1401
1790
|
}
|
|
1402
1791
|
// ── get_api_status ────────────────────────────────────────────────────────
|
|
@@ -1741,7 +2130,13 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
|
|
|
1741
2130
|
// auto-promote a lesson without any manual step.
|
|
1742
2131
|
{
|
|
1743
2132
|
const CRYSTALLIZE_RECALLS = 5;
|
|
2133
|
+
// Der Startvorrat wird nie zum Gesetz. Eine mitgelieferte Lektion sammelt
|
|
2134
|
+
// Abrufe, weil sie zu allem ein bisschen passt — nicht, weil sie sich
|
|
2135
|
+
// bewaehrt hat. Gemessen am 17.08.2026: Die fuenf obersten "Gesetze"
|
|
2136
|
+
// eines Brains mit 475 Lektionen waren alle aus dem Startvorrat, und die
|
|
2137
|
+
// echten Lektionen standen darunter bei zwei bis fuenf Abrufen.
|
|
1744
2138
|
const laws = lessons
|
|
2139
|
+
.filter(l => !istStarterLektion(l))
|
|
1745
2140
|
.filter(l => l.outcome === 'success' && (l.pinned === true || (l.recall_count ?? 0) >= CRYSTALLIZE_RECALLS))
|
|
1746
2141
|
.sort((a, b) => (b.recall_count ?? 0) - (a.recall_count ?? 0))
|
|
1747
2142
|
.slice(0, 5);
|
|
@@ -1751,7 +2146,8 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
|
|
|
1751
2146
|
const sev = l.severity === 'critical' ? '🔴' : l.severity === 'major' ? '🟡' : '';
|
|
1752
2147
|
const rc = l.recall_count ?? 0;
|
|
1753
2148
|
const why = l.pinned === true ? 'pinned' : `recalled ${rc}×`;
|
|
1754
|
-
|
|
2149
|
+
const body = lessonPreviewLines(l);
|
|
2150
|
+
lines.push(` 🏆${sev} \`${l.topic}\` _(${why})_ — ${body[0] ?? ''}`, ...body.slice(1));
|
|
1755
2151
|
}
|
|
1756
2152
|
lines.push('');
|
|
1757
2153
|
}
|
|
@@ -1841,14 +2237,18 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
|
|
|
1841
2237
|
for (const l of focusLessons.slice(0, 4)) {
|
|
1842
2238
|
const emoji = l.outcome === 'success' ? '✅' : l.outcome === 'partial' ? '⚠️' : '❌';
|
|
1843
2239
|
const sev = l.severity === 'critical' ? '🔴' : l.severity === 'major' ? '🟡' : '';
|
|
1844
|
-
|
|
2240
|
+
const body = lessonPreviewLines(l);
|
|
2241
|
+
lines.push(` ${emoji}${sev} \`${l.topic}\` — ${body[0] ?? ''}`, ...body.slice(1));
|
|
1845
2242
|
}
|
|
1846
2243
|
lines.push('');
|
|
1847
2244
|
}
|
|
1848
2245
|
// Top lessons — sorted by recall_count desc (most-used = most proven value).
|
|
1849
2246
|
// If focus is set, focus-matched lessons already shown above; show remaining here.
|
|
1850
2247
|
if (lessons.length > 0) {
|
|
1851
|
-
|
|
2248
|
+
// Auch hier ohne Startvorrat: "Most valuable" soll zeigen, was DIESES
|
|
2249
|
+
// Team gelernt hat, nicht was mitgeliefert wurde.
|
|
2250
|
+
const byRecall = [...lessons].filter(l => !istStarterLektion(l))
|
|
2251
|
+
.sort((a, b) => (b.recall_count ?? 0) - (a.recall_count ?? 0));
|
|
1852
2252
|
const topRecalled = byRecall.filter(l => (l.recall_count ?? 0) > 0).slice(0, 3);
|
|
1853
2253
|
const remaining = (focusLessons.length > 0 ? lessons.filter(l => !focusLessons.includes(l)) : lessons)
|
|
1854
2254
|
.filter(l => !topRecalled.includes(l))
|
|
@@ -1865,7 +2265,8 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
|
|
|
1865
2265
|
const trust = (nAuthors >= 2 && rc >= 5) || rc >= 10 ? ' 🏆'
|
|
1866
2266
|
: nAuthors >= 2 || rc >= 5 ? ' ✅' : '';
|
|
1867
2267
|
const rct = rc > 1 ? ` _(${rc}× recalled)_` : '';
|
|
1868
|
-
|
|
2268
|
+
const body = lessonPreviewLines(l);
|
|
2269
|
+
lines.push(` ${emoji}${sev}${trust} \`${l.topic}\`${rct} — ${body[0] ?? ''}`, ...body.slice(1));
|
|
1869
2270
|
}
|
|
1870
2271
|
lines.push('');
|
|
1871
2272
|
}
|
|
@@ -1875,7 +2276,8 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
|
|
|
1875
2276
|
for (const l of remaining) {
|
|
1876
2277
|
const emoji = l.outcome === 'success' ? '✅' : l.outcome === 'partial' ? '⚠️' : '❌';
|
|
1877
2278
|
const sev = l.severity === 'critical' ? '🔴' : l.severity === 'major' ? '🟡' : '';
|
|
1878
|
-
|
|
2279
|
+
const body = lessonPreviewLines(l);
|
|
2280
|
+
lines.push(` ${emoji}${sev} \`${l.topic}\` — ${body[0] ?? ''}`, ...body.slice(1));
|
|
1879
2281
|
}
|
|
1880
2282
|
lines.push('');
|
|
1881
2283
|
}
|
|
@@ -2323,9 +2725,19 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
|
|
|
2323
2725
|
const rc = l.recall_count ?? 0;
|
|
2324
2726
|
const rcStr = rc > 0 ? ` (✓ recalled ${rc}×)` : '';
|
|
2325
2727
|
const snippet = l.what_worked.slice(0, 110);
|
|
2326
|
-
|
|
2728
|
+
// GROW-045: `author` wurde bisher nur entgegengenommen, nie benutzt —
|
|
2729
|
+
// obwohl die Werkzeug-Spezifikation "used for team lesson filtering"
|
|
2730
|
+
// verspricht. Dieselbe Regel wie in smart_recall, jetzt aus EINEM Modul.
|
|
2731
|
+
const autor = autorAbzeichen(l.author, author);
|
|
2732
|
+
summaryLines.push(`${sevIcon} ${sevLabel}\`${l.topic}\` — ${snippet}${rcStr}${autor}`);
|
|
2327
2733
|
}
|
|
2328
2734
|
summaryLines.push('');
|
|
2735
|
+
// Die Zahl, die nur ein GETEILTES Gedaechtnis liefert. Ist sie null,
|
|
2736
|
+
// wird nichts gesagt: "0 von Teamkollegen" ist keine Nachricht.
|
|
2737
|
+
const fremde = fremdanteil(selected, author);
|
|
2738
|
+
if (fremde > 0) {
|
|
2739
|
+
summaryLines.push(`👥 ${fremde} of these ${fremde === 1 ? 'was' : 'were'} written by a teammate — the value only a shared brain delivers.`, '');
|
|
2740
|
+
}
|
|
2329
2741
|
if (needsRanking) {
|
|
2330
2742
|
summaryLines.push(`💡 Showing top ${shown} of ${total} lessons · \`session_start\` for full briefing`);
|
|
2331
2743
|
}
|
|
@@ -3169,6 +3581,38 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
|
|
|
3169
3581
|
redis.get(`cachly:stats:time_saved_mins:${instance_id}`).catch(() => null),
|
|
3170
3582
|
redis.smembers(`cachly:stats:reuse_pairs:${instance_id}`).catch(() => []),
|
|
3171
3583
|
]);
|
|
3584
|
+
// ── GROW-046: welche Werkzeuge dieses Brain wirklich benutzt ────────────
|
|
3585
|
+
// Bewusst HIER und nicht in einem eigenen Werkzeug: ein 123. Werkzeug,
|
|
3586
|
+
// um zu messen, dass es zu viele Werkzeuge gibt, waere die Pointe, die
|
|
3587
|
+
// niemand will.
|
|
3588
|
+
// try/catch und nicht nur .catch(): faellt die Methode ganz aus (aelterer
|
|
3589
|
+
// Client, anderer Redis-Aufsatz), wirft schon der AUFRUF, und eine
|
|
3590
|
+
// Zusagen-Absicherung greift dann nicht. Eine Messzeile darf das
|
|
3591
|
+
// Werkzeug nie stoppen.
|
|
3592
|
+
let werkzeugRoh = {};
|
|
3593
|
+
try {
|
|
3594
|
+
werkzeugRoh = (await redis.hgetall(nutzungsSchluessel(instance_id))) ?? {};
|
|
3595
|
+
}
|
|
3596
|
+
catch {
|
|
3597
|
+
werkzeugRoh = {};
|
|
3598
|
+
}
|
|
3599
|
+
const nutzung = verdichte(werkzeugRoh, 10);
|
|
3600
|
+
// ── Die GEMESSENE Ersparnis, neben der gerechneten ─────────────────────
|
|
3601
|
+
// Heinrich am 19.08.2026: "Bei der Schaetzung der Tokenersparnis sollten
|
|
3602
|
+
// wir schauen, dass wir nicht nur schaetzen, denn das nervt Leute sehr
|
|
3603
|
+
// stark." Dies hier ist eine Summe, keine Rechnung: die Laenge dessen,
|
|
3604
|
+
// was das Brain wirklich zurueckgegeben hat. Dieselbe try/catch-Vorsicht
|
|
3605
|
+
// wie oben — eine Messzeile darf das Werkzeug nie stoppen.
|
|
3606
|
+
let lieferRoh = { token: 0, abrufe: 0, treffer: 0 };
|
|
3607
|
+
try {
|
|
3608
|
+
const token = await redis.get(lieferSchluessel(instance_id));
|
|
3609
|
+
const quoten = (await redis.hgetall(trefferSchluessel(instance_id))) ?? {};
|
|
3610
|
+
lieferRoh = { token: token ?? 0, abrufe: quoten.abrufe ?? 0, treffer: quoten.treffer ?? 0 };
|
|
3611
|
+
}
|
|
3612
|
+
catch {
|
|
3613
|
+
// still
|
|
3614
|
+
}
|
|
3615
|
+
const lieferung = lieferBild(lieferRoh);
|
|
3172
3616
|
// ── Metric 1: Time-to-first-recall ──────────────────────────────────────
|
|
3173
3617
|
let ttfrLine;
|
|
3174
3618
|
if (bornAt && firstRecallAt) {
|
|
@@ -3192,6 +3636,36 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
|
|
|
3192
3636
|
const crossAuthor = Number(crossAuthorRaw ?? 0);
|
|
3193
3637
|
const reusePct = recallsTotal > 0 ? Math.round((crossAuthor / recallsTotal) * 100) : 0;
|
|
3194
3638
|
const reuseTarget = reusePct >= 30 ? '🟢 above 30 % target' : recallsTotal === 0 ? '⚪ no recalls yet' : '🟡 below 30 % target';
|
|
3639
|
+
/*
|
|
3640
|
+
* ── Die geschaetzten Stunden stehen nicht mehr im Kundenblick ─────────
|
|
3641
|
+
*
|
|
3642
|
+
* "1.429,5 h total saved not re-researching known fixes" war die
|
|
3643
|
+
* groesste erfundene Zahl, die wir gezeigt haben. Sie ist gerechnet:
|
|
3644
|
+
* Minuten je Lektion nach Schwere, mal Abrufzahl. Der Faktor ist
|
|
3645
|
+
* geraten, und die Zahl waechst mit jedem Abruf mit — auch mit einem,
|
|
3646
|
+
* der nichts gefunden hat.
|
|
3647
|
+
*
|
|
3648
|
+
* Heinrich am 19.08.2026 zur alten Fassung: "Ich tue mich schwer mit den
|
|
3649
|
+
* 1200 h. Das haette vielleicht frueher zugetroffen, zu
|
|
3650
|
+
* Entwicklungszeiten ohne AI, aber heutzutage nicht mehr. Die erscheint
|
|
3651
|
+
* laecherlich hoch."
|
|
3652
|
+
*
|
|
3653
|
+
* Genau so ist es. Die Annahme dahinter — 30 bis 240 Minuten Recherche
|
|
3654
|
+
* je Lektion — ist der Preis von VOR den Assistenten. Heute findet ein
|
|
3655
|
+
* Modell dasselbe in Sekunden. Die Zahl misst eine Welt, die es nicht
|
|
3656
|
+
* mehr gibt.
|
|
3657
|
+
*
|
|
3658
|
+
* FREMDBELEG dazu (Artikel 4278052, METR-Studie): erfahrene Entwickler
|
|
3659
|
+
* FUEHLTEN sich mit KI schneller und waren gemessen 19 Prozent
|
|
3660
|
+
* langsamer. Selbstauskunft und Messung zeigen in entgegengesetzte
|
|
3661
|
+
* Richtungen — eine geschaetzte Zeitersparnis ist die schnellste Art,
|
|
3662
|
+
* Vertrauen fuer alle anderen Zahlen gleich mit zu verlieren.
|
|
3663
|
+
*
|
|
3664
|
+
* Sie wird nicht geloescht, sondern ZURUECKGESTUFT: sie steht weiterhin
|
|
3665
|
+
* da, aber klein, mit ihrer Annahme im selben Satz, und unter der
|
|
3666
|
+
* gemessenen Liefermenge. Eine erfundene Zahl ALLEIN nervt; eine
|
|
3667
|
+
* gemessene daneben traegt sie.
|
|
3668
|
+
*/
|
|
3195
3669
|
const timeSaved = Math.round(Number(timeSavedRaw ?? 0));
|
|
3196
3670
|
const timeSavedHuman = timeSaved < 60 ? `${timeSaved} min` : `${(timeSaved / 60).toFixed(1)} h`;
|
|
3197
3671
|
const lines = [
|
|
@@ -3209,10 +3683,24 @@ export async function handleBrainTool(name, args, getConnection, apiFetch) {
|
|
|
3209
3683
|
reusePairs.length > 0 ? `**${reusePairs.length}** distinct reuse relationship${reusePairs.length !== 1 ? 's' : ''} across the team` : '',
|
|
3210
3684
|
``,
|
|
3211
3685
|
`---`,
|
|
3212
|
-
|
|
3686
|
+
// GEMESSEN zuerst, GESCHAETZT darunter — in dieser Reihenfolge, immer.
|
|
3687
|
+
// Vorher stand die gerechnete Stundenzahl ganz oben und fett; die
|
|
3688
|
+
// einzige Zahl ohne Annahme kam gar nicht vor.
|
|
3689
|
+
`### 📦 Aus dem Brain geliefert _(gemessen, nicht gerechnet)_`,
|
|
3690
|
+
lieferungInWorten(lieferung),
|
|
3691
|
+
``,
|
|
3692
|
+
lieferung.abrufe > 0
|
|
3693
|
+
? `_Daneben eine **Schätzung**: ${timeSavedHuman} nicht neu recherchiert — gerechnet mit 30–240 Min. je Lektion nach Schwere, nicht gemessen. Die Zahl darüber braucht diese Annahme nicht._`
|
|
3694
|
+
: `_Eine **Schätzung** von ${timeSavedHuman} „nicht neu recherchiert" wäre hier gerechnet, nicht gemessen — sie erscheint erst, wenn daneben eine gemessene Zahl steht._`,
|
|
3213
3695
|
recallsTotal === 0
|
|
3214
3696
|
? `_Tip: pass \`author="your-handle"\` to \`smart_recall\` so cross-author reuse can be tracked._`
|
|
3215
3697
|
: `_These numbers compound: every learned lesson and every teammate raises all three._`,
|
|
3698
|
+
``,
|
|
3699
|
+
`### 🔧 Werkzeug-Nutzung _(erst messen, dann zusammenlegen)_`,
|
|
3700
|
+
nutzungInWorten(nutzung, TOOLS.length),
|
|
3701
|
+
...(nutzung.spitze.length > 0
|
|
3702
|
+
? nutzung.spitze.map((z, i) => ` ${String(i + 1).padStart(2)}. \`${z.name}\` — ${z.aufrufe}× (${Math.round(z.anteil * 100)} %)`)
|
|
3703
|
+
: []),
|
|
3216
3704
|
].filter(Boolean);
|
|
3217
3705
|
return lines.join('\n');
|
|
3218
3706
|
}
|