diff --git a/server/pruef-befinden.mjs b/server/pruef-befinden.mjs index 69e117bb..edf942b5 100644 --- a/server/pruef-befinden.mjs +++ b/server/pruef-befinden.mjs @@ -61,6 +61,9 @@ const melde = (t) => console.log(t); kaputt war. Eine feste Zahl ist eine Rechnung von gestern. */ const anzahlFragen = (await import("./workspace-entwicklungs-punkte.js")) .ENTWICKLUNG_PUNKTE.befinden.length; +/* Und wie viele davon der Kern sind -- aus demselben Grund abgeleitet. */ +const kernAnzahl = (await import("./workspace-entwicklungs-punkte.js")) + .ENTWICKLUNG_PUNKTE.befinden.filter((p) => p.kern).length; const ok = (b, t) => { geprueft++; console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; }; @@ -170,10 +173,63 @@ melde(""); melde("=== 2. Sechs Fragen, ein Rhythmus ==="); { const d = json(await ruf("/workspace/api/befinden", CREW, "GET", { cookie: kFrida })); - ok(d.punkte?.length === kat.ENTWICKLUNG_SELBST.length, - `${d.punkte?.length} Fragen — so viele wie im Katalog`); - /* Die Recherche: fuenf bis fuenfzehn, unter zwei Minuten. Sechs sind - richtig; zwanzig waeren es nicht. */ + + /* ==== KERN UND ROTATION (19.09.2026) ============================== + + Filipe wollte "viel mehr Fragen" UND "kurz und knapp". Geloest + ueber fuenf feste Fragen plus drei wechselnde: Jede Runde bleibt + kurz, ueber die Monate wird alles abgedeckt -- und der Katalog + kann wachsen, ohne dass eine Runde laenger wird. + ============================================================== */ + const kern = kat.ENTWICKLUNG_PUNKTE.befinden.filter((p) => p.kern); + const rest = kat.ENTWICKLUNG_PUNKTE.befinden.filter((p) => !p.kern); + ok(kern.length > 0, `${kern.length} Fragen sind als Kern markiert`); + ok(rest.length > modul.ZUSATZ_PRO_RUNDE, + `${rest.length} wechseln sich ab — mehr als die ${modul.ZUSATZ_PRO_RUNDE} Plätze je Runde`); + ok(d.punkte?.length === kern.length + modul.ZUSATZ_PRO_RUNDE, + `${d.punkte?.length} Fragen in dieser Runde (${kern.length} feste + ${modul.ZUSATZ_PRO_RUNDE})`); + ok(d.punkte.length < kat.ENTWICKLUNG_SELBST.length, + `weniger als die ${kat.ENTWICKLUNG_SELBST.length} des Katalogs — genau darum geht es`); + ok(d.fragen_gesamt === kat.ENTWICKLUNG_SELBST.length, + "und die Seite sagt, wie viele es insgesamt gibt"); + /* JEDE KERNFRAGE IST DABEI. Der Riegel: Waere eine davon in der + Rotation, erfuehre man die wichtigste Auskunft im Schnitt sechs + Wochen zu spaet. */ + ok(kern.every((k) => d.punkte.some((p) => p.schluessel === k.schluessel)), + "jede Kernfrage ist in dieser Runde dabei"); + /* GEGENPROBE: Es sind NICHT alle -- sonst waere die Rotation nur + behauptet. */ + ok(rest.some((x) => !d.punkte.some((p) => p.schluessel === x.schluessel)), + "und mindestens eine wechselnde Frage ist diesmal NICHT dran"); + /* IN KATALOGREIHENFOLGE. Wer sie jede Runde anders sortiert sieht, + liest sie jedes Mal neu statt sie wiederzuerkennen. */ + const ordnung = kat.ENTWICKLUNG_PUNKTE.befinden.map((p) => p.schluessel); + const gezeigt = d.punkte.map((p) => p.schluessel); + ok(gezeigt.join() === [...gezeigt].sort((a, b) => ordnung.indexOf(a) - ordnung.indexOf(b)).join(), + "die Fragen stehen in der Reihenfolge des Katalogs"); + + /* ÜBER VIER RUNDEN KOMMT JEDE FRAGE DRAN. Eine Frage, die nie + drankommt, koennte man ebenso gut loeschen -- und genau das + passiert, wenn der Versatz und die Zahl der Fragen einen + gemeinsamen Teiler haben. */ + { + const gesehen = new Set(); + for (let r = 0; r < 6; r++) { + for (const p of modul.fragenFuer(idFrida, r).alle) gesehen.add(p.schluessel); + } + ok(gesehen.size === kat.ENTWICKLUNG_SELBST.length, + `über sechs Runden kommt jede der ${kat.ENTWICKLUNG_SELBST.length} Fragen dran (${gesehen.size})`); + /* Und die Auswahl ist STABIL: derselbe Stand, dieselbe Liste. + Zufall hiesse, dass bei jedem Neuladen etwas anderes dasteht. */ + ok(modul.fragenFuer(idFrida, 2).alle.map((p) => p.schluessel).join() + === modul.fragenFuer(idFrida, 2).alle.map((p) => p.schluessel).join(), + "zweimal dieselbe Runde ergibt zweimal dieselben Fragen"); + ok(modul.fragenFuer(idFrida, 2).alle.map((p) => p.schluessel).join() + !== modul.fragenFuer(idFrida, 3).alle.map((p) => p.schluessel).join(), + "die nächste Runde ergibt andere — sonst wäre es keine Rotation"); + } + + /* Die Recherche: fuenf bis fuenfzehn, unter zwei Minuten. */ ok(d.punkte.length >= 5 && d.punkte.length <= 15, "zwischen fünf und fünfzehn — kurz genug, dass man sie wirklich beantwortet"); ok(d.rhythmus_tage === modul.RHYTHMUS_TAGE, @@ -212,12 +268,55 @@ melde("=== 3. Der Verlauf entsteht ==="); "und es ist erst einmal nichts mehr fällig — die Seite fragt nicht jeden Tag"); ok(nach.rhythmus.vollstaendig === true, "die Runde gilt als vollständig"); - /* Eine zweite Runde mit anderen Antworten -- erst dann gibt es ein - Muster, und erst dann ist der Verlauf ueberhaupt etwas wert. */ - await ruf(`/workspace/api/entwicklung/mich/${d.punkte[0].schluessel}`, CREW, "PUT", - { cookie: kFrida }, { stufe: "hakt", anlass: "Diese Woche war viel los." }); + /* ==== DER FEHLER, DEN DIESE PRUEFUNG SELBST FESTGESCHRIEBEN HAT === + + Hier stand bis zum 19.09.2026: + + eine Antwort aendern + POST /runde + ok(zweite.code === 201, "eine zweite Runde geht auch sofort") + + Gruen -- und ein Beleg fuer genau das Gegenteil dessen, was sie + pruefen sollte. Eine Runde galt naemlich als vollstaendig, sobald + zu jeder Frage IRGENDEIN Stand vorlag. Nach der ersten Runde ist + das immer der Fall: Die Antworten bleiben stehen. Ab da liess sich + jede weitere Runde abschliessen, OHNE eine einzige Frage + anzufassen -- die alten Antworten wanderten mit neuem Datum ein + zweites Mal in den Verlauf. + + Der Verlauf zeigte dann eine gerade Linie, und die bedeutete nicht + "es ist gleich geblieben", sondern "niemand hat gefragt". Genau + die Sorte Zahl, die dieses Haus verbietet: eine, die etwas + Falsches behauptet, und der man glaubt. + + Ab jetzt zaehlt nur, was NACH der letzten Runde gesetzt wurde. + ============================================================== */ + const leer = await ruf("/workspace/api/befinden/runde", CREW, "POST", { cookie: kFrida }, {}); + ok(leer.code === 400, + `eine zweite Runde ohne neue Antwort geht NICHT (${leer.code})`); + ok(json(leer).offen === d.punkte.length, + `und zwar sind alle ${json(leer).offen} Fragen wieder offen — ` + + "die alten Antworten zaehlen nicht als neue"); + + const zwischen = json(await ruf("/workspace/api/befinden", CREW, "GET", { cookie: kFrida })); + /* KEINE EINZIGE gilt als frisch. Bei den Fragen, die schon beim + letzten Mal dran waren, steht die alte Antwort sichtbar da; bei den + neu hereinrotierten gibt es noch gar keine. Beides ist "offen". */ + ok(zwischen.punkte.every((p) => !p.frisch), + "keine Antwort gilt als frisch — auch die vom letzten Mal nicht"); + ok(zwischen.punkte.some((p) => p.meins && !p.frisch), + "die alten stehen trotzdem sichtbar da („vom letzten Mal“)"); + + /* GEGENPROBE: Mit frischen Antworten geht es sehr wohl. Ohne sie + waere die Zeile oben auch dann gruen, wenn ueberhaupt keine Runde + mehr moeglich waere. */ + const zweiteFragen = zwischen.punkte; + for (const p of zweiteFragen) { + await ruf(`/workspace/api/entwicklung/mich/${p.schluessel}`, CREW, "PUT", + { cookie: kFrida }, { stufe: p.schluessel === d.punkte[0].schluessel ? "hakt" : "laeuft" }); + } const zweite = await ruf("/workspace/api/befinden/runde", CREW, "POST", { cookie: kFrida }, {}); - ok(zweite.code === 201, `eine zweite Runde geht auch sofort (${zweite.code})`); + ok(zweite.code === 201, `mit frischen Antworten geht die zweite Runde (${zweite.code})`); const nach2 = json(await ruf("/workspace/api/befinden", CREW, "GET", { cookie: kFrida })); ok(nach2.verlauf.length === 2, `jetzt ${nach2.verlauf.length} Runden`); @@ -229,6 +328,11 @@ melde("=== 3. Der Verlauf entsteht ==="); der sich rueckwirkend aendert, ist kein Verlauf. */ ok(Object.keys(nach2.verlauf[1].antworten).length === d.punkte.length, "die alte Runde steht unverändert da"); + /* UND DIE ANALYSE SIEHT ALLE FRAGEN, nicht nur die dieser Runde -- + der Verlauf hoert nicht auf, wenn eine Frage gerade nicht dran ist. */ + ok(nach2.analyse?.zeilen?.length === kat.ENTWICKLUNG_SELBST.length, + `die Auswertung deckt alle ${kat.ENTWICKLUNG_SELBST.length} Fragen ab ` + + `(${nach2.analyse?.zeilen?.length}), nicht nur die ${nach2.punkte.length} dieser Runde`); } /* ======================================================================= @@ -255,6 +359,9 @@ melde("=== 4. Privat ==="); const spur = d.prepare( "SELECT detail FROM protokoll WHERE aktion = 'befinden_runde' ORDER BY id DESC").all(); const verlaufZeilen = d.prepare("SELECT COUNT(*) AS n FROM befinden_verlauf").get().n; + const runProRunde = d.prepare( + "SELECT COUNT(*) AS n FROM befinden_verlauf GROUP BY runde ORDER BY runde") + .all().map((z) => z.n); d.close(); ok(spur.length === 2, `${spur.length} Runden stehen im Protokoll`); ok(spur.every((z) => !/laeuft|hakt|waechst|unklar/.test(z.detail || "")), @@ -265,8 +372,18 @@ melde("=== 4. Privat ==="); 17.09.2026 wurden aus sechs Fragen zwoelf, und drei Zeilen in dieser Datei waren ab da rot, ohne dass irgendetwas kaputt war. Eine feste Zahl ist eine Rechnung von gestern. */ - ok(verlaufZeilen === 2 * anzahlFragen, - `im Verlauf liegen ${verlaufZeilen} Antworten (2 Runden x ${anzahlFragen})`); + /* ABGELEITET, NICHT GERECHNET (19.09.2026, mit der Rotation). + + Hier stand `2 * anzahlFragen` -- zwei Runden mal ALLE Fragen. Seit + eine Runde nur Kern plus drei Zusatzfragen enthaelt, stimmt das + nicht mehr. Und die naechste feste Zahl waere derselbe Fehler noch + einmal: Wer ZUSATZ_PRO_RUNDE aendert, haette sie wieder rot. + + Gezaehlt wird deshalb, was WIRKLICH in den Runden steht. */ + ok(verlaufZeilen === runProRunde.reduce((x, y) => x + y, 0), + `im Verlauf liegen ${verlaufZeilen} Antworten (${runProRunde.join(" + ")})`); + ok(runProRunde.length === 2 && runProRunde.every((n) => n === runProRunde[0]), + `zwei gleich grosse Runden zu je ${runProRunde[0]} Fragen`); /* UND DIE AMPEL ZAEHLT WEITER NUR DEN AKTUELLEN STAND, ohne Namen. */ const a = await ruf("/workspace/api/entwicklung/ampel", CREW, "GET", { cookie: kDogi }); @@ -345,22 +462,147 @@ melde("=== 6. Im Browser ==="); verlauf: document.getElementById('verlauf-block')?.hidden === false, spalten: document.querySelectorAll('.b-verlauf__zeile--kopf .b-verlauf__datum').length, punkte: document.querySelectorAll('.b-verlauf__punkt').length, + /* Die Zeilen OHNE die Kopfzeile -- sie traegt die Datumsangaben, + keine Antworten. */ + zeilen: document.querySelectorAll('.b-verlauf__zeile:not(.b-verlauf__zeile--kopf)').length, text: document.querySelector('main')?.innerText || '', })); + if (process.env.SCHIRM) { + await seite.screenshot({ + path: "C:/Users/qciga/Documents/Obelix/DogiHompage/blick-befinden.png", + fullPage: true }); + } ok(stand.fragen >= 5, `${stand.fragen} Fragen stehen auf der Seite`); ok(stand.knoepfe === stand.fragen * 4, `mit je vier Knöpfen (${stand.knoepfe})`); ok(stand.takt === true, `der Rhythmus steht oben: „${stand.taktText.slice(0, 70)}“`); /* FRIDA HAT ZWEI RUNDEN -- bei ihr MUSS er dastehen. */ ok(stand.verlauf === true, "ihr Verlauf steht auf der Seite"); ok(stand.spalten === 2, `mit zwei Ständen nebeneinander (${stand.spalten})`); - ok(stand.punkte === 2 * anzahlFragen, - `und ${2 * anzahlFragen} Antworten darin (${stand.punkte})`); + /* Zeilen x Spalten des Gitters. Die Zeilen sind die Fragen MIT + GESCHICHTE -- nach zwei Runden mit Rotation sind das mehr als die + acht dieser Runde und weniger als alle zwoelf. Gerechnet, nicht + angenommen: Eine feste Zahl waere hier beim naechsten Umbau still + falsch. */ + ok(stand.punkte === stand.zeilen * stand.spalten && stand.punkte > 0, + `${stand.punkte} Felder im Gitter (${stand.zeilen} Fragen x ${stand.spalten} Stände)`); + ok(stand.zeilen > kernAnzahl && stand.zeilen <= anzahlFragen, + `${stand.zeilen} Fragen haben schon eine Geschichte — mehr als der Kern (${kernAnzahl}), ` + + `weniger als der ganze Katalog (${anzahlFragen})`); /* KEIN FREMDER NAME AUF DEM BILDSCHIRM. Ihr eigener darf oben in der Kopfleiste stehen -- gemessen wird deshalb nur der Inhalt, und gesucht wird nach DogFather, also nach dem, der hier nichts zu suchen hat. */ ok(!/Filipe/.test(stand.text), "und kein anderer Mensch steht darauf"); + /* ==== ZWEI DINGE, DIE KEIN ZAHLENVERGLEICH FINDET ================ + + Beide sind beim Ansehen des Bildschirmfotos aufgefallen, nicht + beim Lesen der Ausgabe -- und beide waren wahr und trotzdem + irrefuehrend. + + 1. Der Satz "diese Runde sind 8 von 12 dran" stand nur, solange + etwas offen oder faellig war. Im RUHEZUSTAND -- dem, den man am + haeufigsten sieht -- fehlte er. Wer dann acht Fragen zaehlt, wo + zwoelf im Katalog stehen, haelt vier fuer verschwunden. + + 2. "Das war deine Antwort beim letzten Mal" stand an JEDER Frage, + unmittelbar nachdem man sie beantwortet und die Runde + abgeschlossen hatte. Technisch richtig (die Antwort ist vor dem + Rundenabschluss gesetzt), als Auskunft Unsinn. + ============================================================== */ + ok(/von \d+ Fragen dran/.test(stand.taktText), + `der Satz erklaert die kurze Runde auch im Ruhezustand: ` + + `„…${(stand.taktText.match(/Diese Runde[^.]*\./) || [""])[0].slice(0, 60)}…“`); + const ruhig = await seite.evaluate(() => ({ + alt: document.querySelectorAll('.e-punkt[data-alt="ja"]').length, + hinweise: document.querySelectorAll('.e-punkt__alt').length, + kernmarken: document.querySelectorAll('.e-punkt__marke').length, + })); + ok(ruhig.alt === 0 && ruhig.hinweise === 0, + `direkt nach einer Runde steht nirgends „vom letzten Mal“ (${ruhig.hinweise})`); + ok(ruhig.kernmarken === kernAnzahl, + `die ${kernAnzahl} festen Fragen sind als solche gekennzeichnet (${ruhig.kernmarken})`); + + /* GEGENPROBE: Ist eine Runde wirklich faellig, MUSS der Hinweis da + sein -- sonst hiesse die Zeile oben nur, dass er nie erscheint. + Die Runde wird dafuer um mehr als den Rhythmus zurueckdatiert; + die Uhr laesst sich nicht vorstellen. */ + { + /* ALLE Runden verschieben, nicht nur die letzte. + + Erster Anlauf datierte nur die juengste zurueck -- danach war + die ZWEITGROESSTE die juengste, und die lag weiterhin auf heute. + Die Seite meldete folgerichtig "ruhig", und die Gegenprobe war + rot, ohne dass an der Sache etwas falsch war. + + In JavaScript gerechnet und nicht in SQL: `datetime(runde, '-15 + days')` gibt ein anderes Format zurueck (Leerzeichen statt T, + ohne Z) -- Date.parse liest das als ORTSZEIT, und die Rechnung + waere danach um zwei Stunden daneben. */ + const versatz = (modul.RHYTHMUS_TAGE + 1) * 86400000; + const dd = oeffne(); + const runden = dd.prepare( + "SELECT DISTINCT runde FROM befinden_verlauf WHERE person_id = ?").all(idFrida) + .map((z) => z.runde); + const setzen = dd.prepare( + "UPDATE befinden_verlauf SET runde = ?, am = ? WHERE person_id = ? AND runde = ?"); + const alteStaende = runden.map((r) => [r, new Date(Date.parse(r) - versatz).toISOString()]); + for (const [vorher, nachher] of alteStaende) setzen.run(nachher, nachher, idFrida, vorher); + + /* UND DIE ANTWORTEN GLEICH MIT. Im Betrieb werden sie gesetzt und + kurz darauf die Runde abgeschlossen -- sie sind also IMMER etwas + aelter als die Runde. Nur die Runden zurueckzudatieren erzeugte + einen Zustand, den es nie gibt: Antworten von heute unter einer + Runde von vor zwei Wochen. Die Seite hielt sie folgerichtig fuer + frisch, und die Gegenprobe war rot an der falschen Stelle. */ + const antworten = dd.prepare( + `SELECT schluessel, am FROM entwicklung_stand + WHERE person_id = ? AND von_id = ?`).all(idFrida, idFrida); + const setzenA = dd.prepare( + `UPDATE entwicklung_stand SET am = ? + WHERE person_id = ? AND von_id = ? AND schluessel = ?`); + const alteAntworten = antworten.map((z) => + [z.schluessel, z.am, new Date(Date.parse(z.am) - versatz).toISOString()]); + for (const [k, , nachher] of alteAntworten) setzenA.run(nachher, idFrida, idFrida, k); + dd.close(); + + await seite.reload({ waitUntil: "networkidle" }); + await seite.waitForTimeout(900); + const faellig = await seite.evaluate(() => ({ + lage: document.getElementById('takt')?.dataset.lage || '', + alt: document.querySelectorAll('.e-punkt[data-alt="ja"]').length, + hinweise: document.querySelectorAll('.e-punkt__alt').length, + fragen: document.querySelectorAll('.e-punkt--meins').length, + knopf: !document.getElementById('takt-knopf')?.hidden, + })); + ok(faellig.lage === "faellig", `jetzt ist eine Runde fällig (${faellig.lage})`); + ok(faellig.hinweise > 0 && faellig.alt > 0, + `und die alten Antworten sind als solche gekennzeichnet (${faellig.hinweise} von ${faellig.fragen})`); + /* UND DER KNOPF IST WEG, solange nichts bestaetigt ist. Genau das + war der Fehler: Er stand da und schloss eine leere Runde ab. */ + ok(faellig.knopf === false, + "der Knopf zum Abschließen ist weg, solange nichts bestätigt ist"); + + /* ZURUECKDREHEN. Ohne das liefe der Rest dieser Datei auf einem + Stand, den die Pruefung selbst verbogen hat -- Abschnitt 7 fand + danach ein "besser" statt eines Musters und meldete fuenf + Fehler, die keine waren. Genau die Sorte Befund, die man + stundenlang im falschen Code sucht. */ + const dz = oeffne(); + const zurueckSetzen = dz.prepare( + "UPDATE befinden_verlauf SET runde = ?, am = ? WHERE person_id = ? AND runde = ?"); + for (const [vorher, nachher] of alteStaende) zurueckSetzen.run(vorher, vorher, idFrida, nachher); + const zurueckA = dz.prepare( + `UPDATE entwicklung_stand SET am = ? + WHERE person_id = ? AND von_id = ? AND schluessel = ?`); + for (const [k, vorher] of alteAntworten) zurueckA.run(vorher, idFrida, idFrida, k); + dz.close(); + const zurueck = json(await ruf("/workspace/api/befinden", CREW, "GET", { cookie: kFrida })); + ok(zurueck.rhythmus?.letzte === runden.sort().at(-1) + && zurueck.rhythmus?.faellig === false, + "der Stand ist danach wieder so, wie er war"); + } + /* GEGENPROBE mit einem zweiten Menschen: DogFather hat keine einzige Runde. Bei ihm muss derselbe Block WEG sein -- sonst hiesse „er erscheint ab der zweiten Runde" nur, dass er immer erscheint. */ @@ -410,8 +652,9 @@ melde("=== 7. Die Analyse ==="); const d = await holen(kFrida, CREW); const erste = d.punkte[0].schluessel; ok(d.analyse?.moeglich === true, `mit ${d.analyse?.runden} Runden gibt es eine Analyse`); - ok(d.analyse.zeilen.length === d.punkte.length, - `sie geht alle ${d.analyse.zeilen.length} Fragen durch, nicht nur die schlechten`); + ok(d.analyse.zeilen.length === anzahlFragen, + `sie geht alle ${d.analyse.zeilen.length} Fragen des Katalogs durch, ` + + `nicht nur die ${d.punkte.length} dieser Runde und nicht nur die schlechten`); const z1 = d.analyse.zeilen.find((z) => z.schluessel === erste); ok(z1.art === "neu", `der frisch hakende Punkt gilt als neu, nicht als Muster (${z1.art})`); ok(/noch kein Muster/i.test(d.analyse.satz), @@ -420,8 +663,8 @@ melde("=== 7. Die Analyse ==="); /* DIE EINORDNUNG IST DER TEIL, DER „AUS ALLER WELT" HEISST. Sie steht bei JEDER Zeile -- auch bei denen, die laufen. */ ok(d.analyse.zeilen.every((z) => z.welt && z.welt.length > 40), - "zu jeder der sechs Fragen steht ein belegter Befund"); - ok(new Set(d.analyse.zeilen.map((z) => z.welt)).size === d.punkte.length, + `zu jeder der ${anzahlFragen} Fragen steht ein belegter Befund`); + ok(new Set(d.analyse.zeilen.map((z) => z.welt)).size === anzahlFragen, "und jeder ist ein anderer — kein Satz, der zu allen passen würde"); /* „Was hilft" NUR dort, wo es etwas zu tun gibt. */ ok(d.analyse.zeilen.filter((z) => z.hilft).length === 1, @@ -439,7 +682,11 @@ melde("=== 7. Die Analyse ==="); const n = await holen(kFrida, CREW); const z2 = n.analyse.zeilen.find((z) => z.schluessel === erste); ok(z2.art === "dauerhaft", `jetzt ist es ein Muster (${z2.art})`); - ok(z2.runden === modul.MUSTER_AB, `über ${z2.runden} Runden`); + /* "MALE", NICHT "RUNDEN": Mit der Rotation ist eine Frage nicht in + jeder Runde dran -- gezaehlt wird, wie oft sie gestellt wurde. */ + ok(z2.male === modul.MUSTER_AB, `über ${z2.male} Male hintereinander gleich`); + ok(z2.gefragt >= z2.male, + `und sie war insgesamt ${z2.gefragt}x dran — die Zahl gibt es wirklich`); ok(/kein schlechter Tag/i.test(n.analyse.satz), `und der Satz sagt, worauf es ankommt: „${n.analyse.satz.slice(0, 64)}…“`); /* DIE REIHENFOLGE MACHT DIE AUSSAGE -- das Muster gehört nach oben. diff --git a/server/workspace-befinden.js b/server/workspace-befinden.js index c157d2db..26cb6e09 100644 --- a/server/workspace-befinden.js +++ b/server/workspace-befinden.js @@ -76,6 +76,18 @@ export const befindenRouter = express.Router(); * die Abfrage nur noch sich selbst. */ export const RHYTHMUS_TAGE = 14; +/** Wie viele Zusatzfragen je Runde zum Kern dazukommen. + * + * DREI. Mit fünf Kernfragen macht das acht je Runde — die Quellen zu + * Puls-Abfragen sagen übereinstimmend fünf bis fünfzehn, unter zwei + * Minuten. Acht liegt mittig und lässt Platz: Der Katalog kann auf + * zwanzig Fragen wachsen, ohne dass eine einzelne Runde länger wird. + * + * Das ist der ganze Trick an Filipes zwei Wünschen, die sich zu + * widersprechen schienen — „viel mehr Fragen" und „kurz und knapp". + * Beides geht, nur nicht gleichzeitig in DERSELBEN Runde. */ +export const ZUSATZ_PRO_RUNDE = 3; + /** So viele frühere Antworten bekommt die Person zu sehen. * * Sechs Stände sind drei Monate — lang genug, um ein Muster zu sehen, @@ -150,8 +162,7 @@ function block() { * und läuft danach auseinander -- und zwar still, weil beide Seiten * plausible Zahlen liefern. */ export function rhythmus(personId) { - const b = block(); - const gesamt = (ENTWICKLUNG_PUNKTE[b.schluessel] || []).length; + const kern = alleFragen().filter((p) => p.kern).length; const letzte = db().prepare( `SELECT runde, COUNT(*) AS n FROM befinden_verlauf WHERE person_id = ? GROUP BY runde ORDER BY runde DESC LIMIT 1`).get(personId); @@ -160,10 +171,109 @@ export function rhythmus(personId) { return { letzte: letzte.runde, tage, - vollstaendig: letzte.n >= gesamt, - /* FAELLIG heisst: die letzte Runde ist alt genug -- ODER sie war nie - fertig. Eine halb beantwortete Runde ist keine Runde. */ - faellig: tage >= RHYTHMUS_TAGE || letzte.n < gesamt, + /* GEGEN DEN KERN GEZAEHLT, NICHT GEGEN DEN GANZEN KATALOG + (geaendert 19.09.2026, mit der Rotation). + + Vorher stand hier `letzte.n >= gesamt` -- die Zahl ALLER Fragen. + Seit eine Runde nur Kern plus drei Zusatzfragen enthaelt, waere + das nie wieder erfuellt: Jede abgeschlossene Runde haette sich + selbst fuer unvollstaendig erklaert und waere sofort wieder + faellig gewesen. Die Seite haette jeden Tag gefragt, und genau + davor warnt die eigene Begruendung oben ("dann misst die Abfrage + nur noch sich selbst"). + + Der Kern ist in JEDER Runde enthalten -- daran laesst sich eine + Runde erkennen, ohne dass irgendwo stehen muss, wie viele Fragen + sie hatte. */ + vollstaendig: letzte.n >= kern, + faellig: tage >= RHYTHMUS_TAGE || letzte.n < kern, + }; +} + +/** Alle Fragen des Blocks. */ +function alleFragen() { + const b = block(); + return b ? (ENTWICKLUNG_PUNKTE[b.schluessel] || []) : []; +} + +/** Wie viele Runden diese Person schon abgeschlossen hat. */ +function rundenBisher(personId) { + return db().prepare( + "SELECT COUNT(DISTINCT runde) AS n FROM befinden_verlauf WHERE person_id = ?") + .get(personId).n; +} + +/* ===================================================================== + WELCHE FRAGEN DIESE RUNDE — Kern plus Rotation (19.09.2026) + + Filipe wollte "viel mehr Fragen" UND "kurz und knapp". Das geht, nur + nicht gleichzeitig in derselben Runde. + + --------------------------------------------------------------------- + DIE ROTATION WIRD GERECHNET, NICHT GEWUERFELT + + Aus der Zahl der bisherigen Runden. Zwei Gruende, und beide sind + Hausregeln: + + * Zufall hiesse, dass bei jedem Neuladen eine andere Liste + dasteht. Eine Seite, die sich unter den Fingern aendert, + beantwortet niemand zu Ende -- und die halbe Runde waere dann + auch noch eine andere als die angefangene. + * Gerechnet braucht es kein zusaetzliches Feld. Ein gespeicherter + Zeiger waere eine zweite Stelle, an der der Stand stimmen muss, + und die zweite ist immer die, die irgendwann danebenliegt. + + --------------------------------------------------------------------- + DER RAND, DEN MAN LEICHT UEBERSIEHT + + Sieben Zusatzfragen, drei je Runde -- das geht nicht glatt auf. + Gerechnet wird deshalb mit Umlauf (`% laenge`), und die Auswahl + nimmt drei AUFEINANDERFOLGENDE aus dem Kreis. Ueber vier Runden + kommt damit jede Frage mindestens einmal dran; keine faellt dauerhaft + heraus, weil der Versatz um drei und die Laenge sieben teilerfremd + sind. + + Waeren es einmal sechs Fragen bei drei je Runde, kaemen nur noch + zwei feste Haelften dran -- dieselben drei, immer abwechselnd. Der + Fall ist unten abgefangen: Bei einem gemeinsamen Teiler wird der + Versatz um eins erhoeht, damit der Kreis trotzdem vollstaendig + durchlaufen wird. Das ist kein Schoenheitsfehler -- eine Frage, die + nie drankommt, ist eine Frage, die man ebenso gut loeschen koennte. + ===================================================================== */ + +/** Der groesste gemeinsame Teiler -- fuer den Rand oben. */ +function ggt(a, b) { return b ? ggt(b, a % b) : a; } + +/** Die Fragen dieser Runde: alle Kernfragen plus die Zusatzfragen, die + * turnusgemaess dran sind. */ +export function fragenFuer(personId, runden = null) { + const alle = alleFragen(); + const kern = alle.filter((p) => p.kern); + const rest = alle.filter((p) => !p.kern); + const n = runden == null ? rundenBisher(personId) : runden; + + if (rest.length <= ZUSATZ_PRO_RUNDE) { + /* Weniger Zusatzfragen als Plaetze -- dann gibt es nichts zu + drehen, und alle kommen jede Runde dran. */ + return { kern, zusatz: rest, alle: [...kern, ...rest], runden: n }; + } + + let schritt = ZUSATZ_PRO_RUNDE; + while (ggt(schritt, rest.length) !== 1) schritt++; + + const start = (n * schritt) % rest.length; + const zusatz = []; + for (let i = 0; i < ZUSATZ_PRO_RUNDE; i++) zusatz.push(rest[(start + i) % rest.length]); + + /* IN KATALOGREIHENFOLGE AUSGELIEFERT, nicht in Rotationsreihenfolge. + Der Katalog ist nach Thema sortiert; wer ihn jede Runde in einer + anderen Reihenfolge sieht, liest ihn jedes Mal neu statt ihn + wiederzuerkennen. */ + const gewaehlt = new Set([...kern, ...zusatz].map((p) => p.schluessel)); + return { + kern, zusatz, + alle: alle.filter((p) => gewaehlt.has(p.schluessel)), + runden: n, }; } @@ -198,16 +308,68 @@ befindenRouter.get("/workspace/api/befinden", angemeldet, (req, res) => { zeilen.filter((z) => z.runde === r).map((z) => [z.schluessel, z.stufe])), })); - const punkte = (ENTWICKLUNG_PUNKTE[b.schluessel] || []).map((pt) => ({ - ...pt, meins: meine.get(pt.schluessel) || null, - })); + const takt = rhythmus(req.person.id); + const diese = fragenFuer(req.person.id); + + /* ==== WAS IST "FRISCH"? ========================================== + + DER FEHLER, DEN DAS BEHEBT (gefunden 19.09.2026): + + Eine Runde galt als vollstaendig, sobald zu jeder Frage IRGENDEIN + Stand in `entwicklung_stand` lag. Nach der ersten Runde ist das + immer der Fall -- die Antworten bleiben ja stehen. Ab da liess + sich jede weitere Runde abschliessen, OHNE eine einzige Frage + anzufassen: Die alten Antworten wanderten ein zweites Mal in den + Verlauf, mit neuem Datum. + + Das ist schlimmer als eine fehlende Runde. Der Verlauf zeigte + dann eine gerade Linie, und die bedeutete nicht "es ist alles + gleich geblieben", sondern "niemand hat gefragt". Genau diese + Sorte Zahl ist im Haus verboten: eine, die etwas Falsches + behauptet, und der man glaubt. + + Die vorhandene Pruefung hat den Fehler nicht gefunden, sondern + FESTGESCHRIEBEN -- sie aenderte eine Antwort und stellte + zufrieden fest, dass die zweite Runde durchgeht. + + Frisch heisst ab jetzt: nach der letzten Runde gesetzt. Vor der + ersten Runde ist alles frisch. */ + const grenze = takt.letzte ? Date.parse(takt.letzte) : null; + const istFrisch = (stand) => !stand ? false + : grenze === null ? true + : Date.parse(stand.am) > grenze; + + const punkte = diese.alle.map((pt) => { + const m = meine.get(pt.schluessel) || null; + return { + ...pt, + meins: m, + /* Die Antwort bleibt sichtbar -- sie ist ja nicht falsch, nur + alt. Sie zaehlt nur nicht mehr mit, und die Seite sagt das. */ + frisch: istFrisch(m), + }; + }); res.json({ block: b, stufen: ENTWICKLUNG_STUFEN, rhythmus_tage: RHYTHMUS_TAGE, - rhythmus: rhythmus(req.person.id), + rhythmus: takt, punkte, + /* WIE VIELE FRAGEN ES INSGESAMT GIBT und wie viele diese Runde + dran sind. Ohne diese beiden Zahlen saehe eine kurze Runde aus + wie ein Katalog, aus dem etwas verschwunden ist. */ + fragen_gesamt: (ENTWICKLUNG_PUNKTE[b.schluessel] || []).length, + /* DER GANZE KATALOG -- nur Schluessel und Titel, fuer den Verlauf. + Der Verlauf zeigt jede Frage, zu der es Antworten gibt, nicht + nur die dieser Runde: Was vor vier Wochen beantwortet wurde, + hoert nicht auf zu gelten, weil die Frage gerade pausiert. + Ohne diese Liste kennte die Seite die Titel dazu nicht. */ + katalog: (ENTWICKLUNG_PUNKTE[b.schluessel] || []) + .map((pt) => ({ schluessel: pt.schluessel, titel: pt.titel, kern: !!pt.kern })), + kern_anzahl: diese.kern.length, + zusatz_anzahl: diese.zusatz.length, + runden_bisher: diese.runden, verlauf, /* DIE ANALYSE KOMMT MIT, sie holt sich niemand extra. @@ -215,7 +377,12 @@ befindenRouter.get("/workspace/api/befinden", angemeldet, (req, res) => { hier ohnehin schon steht. Ein zweiter Weg waere eine zweite Stelle, an der die Rechte stimmen muessen -- und die zweite ist immer die, die etwas durchlaesst. */ - analyse: analyseVon(verlauf, punkte), + /* DIE ANALYSE SIEHT ALLE FRAGEN, nicht nur die dieser Runde. + Sie handelt vom Verlauf, und der Verlauf hoert nicht auf, + wenn eine Frage gerade nicht dran ist. Ihr nur die acht + aktuellen zu geben hiesse, vier Muster zu verschweigen -- + darunter womoeglich das einzige, das zaehlt. */ + analyse: analyseVon(verlauf, ENTWICKLUNG_PUNKTE[b.schluessel] || []), }); } catch (f) { console.error("[befinden] Lesen:", f?.message); @@ -233,13 +400,26 @@ befindenRouter.post("/workspace/api/befinden/runde", express.json({ limit: "8kb" angemeldet, (req, res) => { try { const b = block(); - const punkte = ENTWICKLUNG_PUNKTE[b.schluessel] || []; + const takt = rhythmus(req.person.id); + /* DIESELBE RECHNUNG WIE BEIM ANZEIGEN. Eine zweite hier waere die + Stelle, an der die Seite acht Fragen zeigt und der Server neun + verlangt -- und niemand faende heraus, welche. */ + const punkte = fragenFuer(req.person.id).alle; const meine = new Map(db().prepare( - `SELECT schluessel, stufe FROM entwicklung_stand + `SELECT schluessel, stufe, am FROM entwicklung_stand WHERE person_id = ? AND von_id = ?`) - .all(req.person.id, req.person.id).map((z) => [z.schluessel, z.stufe])); + .all(req.person.id, req.person.id).map((z) => [z.schluessel, z])); - const fehlt = punkte.filter((p) => !meine.has(p.schluessel)); + /* NUR FRISCHE ANTWORTEN ZAEHLEN -- siehe die Begruendung beim + Anzeigen. Eine Runde, die alte Antworten ein zweites Mal + festhaelt, zeichnet eine gerade Linie, die nicht "gleich + geblieben" heisst, sondern "niemand hat gefragt". */ + const grenze = takt.letzte ? Date.parse(takt.letzte) : null; + const fehlt = punkte.filter((p) => { + const m = meine.get(p.schluessel); + if (!m) return true; + return grenze !== null && Date.parse(m.am) <= grenze; + }); if (fehlt.length) { /* EINE HALBE RUNDE IST KEINE RUNDE. Sie wuerde im Verlauf wie ein vollstaendiger Stand aussehen und das Muster verfaelschen. */ @@ -252,7 +432,9 @@ befindenRouter.post("/workspace/api/befinden/runde", express.json({ limit: "8kb" const rein = db().prepare( `INSERT INTO befinden_verlauf (person_id, runde, schluessel, stufe, am) VALUES (?,?,?,?,?)`); - for (const p of punkte) rein.run(req.person.id, runde, p.schluessel, meine.get(p.schluessel), runde); + for (const p of punkte) { + rein.run(req.person.id, runde, p.schluessel, meine.get(p.schluessel).stufe, runde); + } /* IM PROTOKOLL STEHT NUR, DASS es passiert ist -- nicht was. Das Protokoll liest DogFather; die Antworten gehen ihn nichts @@ -481,7 +663,18 @@ export function analyseVon(verlauf, punkte) { schluessel: pt.schluessel, titel: pt.titel, art: m.art, - runden: m.runden || 0, + /* "MALE", NICHT "RUNDEN" (19.09.2026, mit der Rotation). + + Vorher hiessen diese Zahlen Runden, und das stimmte, solange + jede Frage in jeder Runde drankam. Seit die Zusatzfragen + wechseln, ist "seit 2 Runden gleich" bei einer Frage, die nur + zweimal von vier Runden dran war, schlicht falsch -- sie waere + dazwischen vielleicht anders beantwortet worden. + + Gezaehlt werden deshalb die MALE, die sie gestellt wurde. Das + ist die Zahl, die es wirklich gibt. */ + male: m.runden || 0, + gefragt: verlauf.filter((r) => r.antworten[pt.schluessel]).length, welt: e.welt || null, /* WAS HILFT, STEHT NUR DA, WO ES ETWAS ZU TUN GIBT. Ein Rat an einer Stelle, die laeuft, ist Laerm -- und er entwertet die @@ -501,22 +694,22 @@ export function analyseVon(verlauf, punkte) { let satz; if (dauerhaft.length) { satz = dauerhaft.length === 1 - ? `Eine Sache meldest du seit ${dauerhaft[0].runden} Runden gleich. ` + ? `Eine Sache hast du die letzten ${dauerhaft[0].male} Male gleich beantwortet. ` + "Das ist kein schlechter Tag mehr – das ist das Muster, auf das es ankommt." - : `${dauerhaft.length} Sachen melden sich seit mehreren Runden gleich. ` + : `${dauerhaft.length} Sachen beantwortest du mehrmals hintereinander gleich. ` + "Das ist selten Zufall und fast nie deine Schuld."; } else if (neu.length) { satz = neu.length === 1 - ? "Eine Sache hakt seit dieser Runde. Einmal ist noch kein Muster – " + ? "Eine Sache hakt seit dem letzten Mal. Einmal ist noch kein Muster – " + "aber der richtige Moment, es anzusprechen, statt zu warten." - : `${neu.length} Sachen haken seit dieser Runde. Noch kein Muster – ` + : `${neu.length} Sachen haken seit dem letzten Mal. Noch kein Muster – ` + "aber zwei auf einmal sind einen Satz wert."; } else if (besser.length) { satz = "Nichts hakt gerade – und " + (besser.length === 1 ? "eine Sache hat sich" : `${besser.length} Sachen haben sich`) - + " seit der letzten Runde gelöst. Das gehört genauso festgehalten wie das Gegenteil."; + + " seit dem letzten Mal gelöst. Das gehört genauso festgehalten wie das Gegenteil."; } else { - satz = "Nichts hakt, und über die letzten Runden hat sich daran nichts geändert. " + satz = "Nichts hakt, und daran hat sich über die letzten Male nichts geändert. " + "Das ist die Antwort, für die diese Seite gebaut ist – nicht die Ausnahme."; } diff --git a/server/workspace-entwicklungs-punkte.js b/server/workspace-entwicklungs-punkte.js index 66646075..953c03be 100644 --- a/server/workspace-entwicklungs-punkte.js +++ b/server/workspace-entwicklungs-punkte.js @@ -410,12 +410,39 @@ export const ENTWICKLUNG_PUNKTE = { + "Themenvorschlag, den die Zuschauer selbst gemacht haben." }, ], - /* ---- Block 7: Wie geht es dir damit? (nur die Person selbst) ------- */ + /* ---- Block 7: Wie geht es dir damit? (nur die Person selbst) ------- + + KERN UND ROTATION (19.09.2026) + + Filipe wollte zweierlei, das sich zu widersprechen scheint: "viel + mehr Fragen" und "kurz und knapp". Es loest sich ueber einen FESTEN + KERN plus WECHSELNDEN ZUSATZFRAGEN. + + `kern: true` heisst: JEDE Runde. Das sind die fuenf, bei denen ein + Ausfall teuer waere -- die beiden meistgenannten Gruende fuers + Aufhoeren, die Erholung, die Sicherheit vor Anfeindung und der + beste Fruehindikator. Eine dieser Fragen nur jede dritte Runde zu + stellen hiesse, die Antwort im Schnitt sechs Wochen spaeter zu + bekommen. + + Alle anderen rotieren. Dadurch bleibt eine Runde kurz (fuenf plus + drei), und ueber die Monate wird trotzdem alles abgedeckt -- und + der Katalog kann WACHSEN, ohne dass eine einzelne Runde laenger + wird. Das ist der eigentliche Gewinn: Wer jedes Mal dasselbe lange + Formular sieht, klickt durch, und dann misst die Abfrage nur noch + sich selbst. + + WELCHE ZUSATZFRAGEN drankommen, rechnet workspace-befinden.js aus + der Zahl der bisherigen Runden -- nicht aus Zufall. Zufall waere + bei jedem Neuladen eine andere Liste, und eine Seite, die sich + unter den Fingern aendert, beantwortet niemand zu Ende. */ befinden: [ - { schluessel: "e-ich-menge-passt", stufe: "probe", + { schluessel: "e-ich-menge-passt", stufe: "probe", kern: true, + /* KERN: Die meistgenannte Ursache ueberhaupt. Wer sie nur jede dritte Runde fragt, erfaehrt sie im Schnitt sechs Wochen zu spaet. */ titel: "Die Menge passt gerade", text: "Zu wenig Zeit ist der meistgenannte Grund, warum Moderatoren aufhören." }, - { schluessel: "e-ich-niemand-schwierig", stufe: "probe", + { schluessel: "e-ich-niemand-schwierig", stufe: "probe", kern: true, + /* KERN: Die zweithaeufigste -- und die, ueber die von selbst am wenigsten geredet wird. Genau deshalb muss sie gefragt werden. */ titel: "Es gibt gerade niemanden im Team, mit dem es schwierig ist", text: "Der zweithäufigste Grund – und der, über den am wenigsten geredet wird." }, { schluessel: "e-ich-weiss-an-wen", stufe: "probe", @@ -439,7 +466,8 @@ export const ENTWICKLUNG_PUNKTE = { ALLE AUF "probe": Diese Fragen sind keine Stufenleiter. Sie gelten am ersten Tag genauso wie im dritten Jahr -- ein Befinden hat keine Erwartungsstufe. */ - { schluessel: "e-ich-abschalten", stufe: "probe", + { schluessel: "e-ich-abschalten", stufe: "probe", kern: true, + /* KERN: Ob die Erholung traegt, sagt besser voraus, ob jemand aufhoert, als die Menge der Arbeit. */ titel: "Nach einer Schicht kann ich abschalten", text: "Nicht ob du müde bist – ob es aufhört, wenn du aufhörst. " + "Das ist der Unterschied zwischen anstrengend und zehrend." }, @@ -451,7 +479,8 @@ export const ENTWICKLUNG_PUNKTE = { titel: "Mir ist klar, was von mir erwartet wird", text: "Unklarheit fühlt sich nicht wie Unklarheit an, sondern wie " + "das Gefühl, es nie ganz richtig zu machen." }, - { schluessel: "e-ich-nicht-angefeindet", stufe: "probe", + { schluessel: "e-ich-nicht-angefeindet", stufe: "probe", kern: true, + /* KERN: Der Punkt, an dem aus Belastung eine Gefahr wird. Er darf nie in einer Rotation warten. */ titel: "Ich werde außerhalb des Streams in Ruhe gelassen", text: "Privatnachrichten, andere Plattformen, das Privatleben. Wer hier " + "„da hakt es“ setzt, braucht keine Umfrage, sondern ein Gespräch." }, @@ -459,7 +488,8 @@ export const ENTWICKLUNG_PUNKTE = { titel: "Ich lerne hier etwas, das mir auch sonst nützt", text: "Ehrenamt hält, wenn es etwas zurückgibt. Das muss kein Geld sein – " + "aber es muss etwas sein." }, - { schluessel: "e-ich-naechste-monate", stufe: "probe", + { schluessel: "e-ich-naechste-monate", stufe: "probe", kern: true, + /* KERN: Der beste einzelne Fruehindikator, den Befindensabfragen kennen -- er schlaegt an, bevor die anderen es tun. */ titel: "Ich kann mir gut vorstellen, in ein paar Monaten noch dabei zu sein", text: "Die ehrlichste Frage der Liste. Wer hier zögert, hat es meistens " + "vorher schon gewusst – und niemand hat gefragt." }, diff --git a/workspace/anruf-probe.html b/workspace/anruf-probe.html index c3e936fa..d0cf7af8 100644 --- a/workspace/anruf-probe.html +++ b/workspace/anruf-probe.html @@ -16,15 +16,15 @@ verdeckt. Gefunden hat es pruef-struktur. Richtig sind `gate.css` (die Farbwerte des Hauses) und `haus.css`. --> - - + + - +