Wie geht's dir?: fester Kern, wechselnde Fragen -- und die leere Runde

Dein Auftrag von vorher, zusammen mit zwei Saetzen, die sich zu
widersprechen schienen: "viel mehr Fragen" und "kurz und knapp".

1. DER FEHLER, DEN ICH DABEI GEFUNDEN HABE, WAR GROESSER.

Eine Runde galt als vollstaendig, sobald zu jeder Frage IRGENDEIN Stand
vorlag. 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 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 hier verboten ist: eine, die etwas Falsches behauptet, und
der man glaubt. Bei DIESEM Gegenstand -- dem Befinden eines Menschen --
waere das der schlechteste denkbare Ort dafuer.

DIE VORHANDENE PRUEFUNG HAT DEN FEHLER NICHT GEFUNDEN, SIE HAT IHN
FESTGESCHRIEBEN. Dort stand woertlich `ok(zweite.code === 201, "eine
zweite Runde geht auch sofort")` -- gruen, und ein Beleg fuer genau das
Gegenteil dessen, was sie pruefen sollte. Sie aenderte eine Antwort von
zwoelf und war zufrieden.

Ab jetzt zaehlt nur, was NACH der letzten Runde gesetzt wurde. Die alte
Antwort bleibt sichtbar -- sie ist nicht falsch, nur alt -- und traegt,
solange eine Runde faellig ist, den Hinweis "Das war deine Antwort beim
letzten Mal". Der Knopf zum Abschliessen ist weg, bis bestaetigt wurde.

2. FUENF FESTE FRAGEN, DREI WECHSELNDE.

Kern sind die, bei denen ein Ausfall teuer waere: die beiden
meistgenannten Gruende fuers Aufhoeren (zu wenig Zeit, Streit im Team),
die Erholung, die Sicherheit vor Anfeindung und der beste
Fruehindikator ("kann ich mir vorstellen, in ein paar Monaten noch
dabei zu sein"). Eine davon nur jede dritte Runde zu fragen hiesse, die
Antwort im Schnitt sechs Wochen spaeter zu bekommen.

Alle anderen rotieren, drei je Runde. Damit bleibt eine Runde bei acht
Fragen -- die Quellen sagen fuenf bis fuenfzehn, unter zwei Minuten --
und der Katalog kann WACHSEN, ohne dass eine Runde laenger wird. Das
ist der eigentliche Gewinn und die Aufloesung deines Widerspruchs.

Gerechnet, nicht gewuerfelt: aus der Zahl der bisherigen Runden. Zufall
hiesse, dass bei jedem Neuladen etwas anderes dasteht. Und weil sieben
Zusatzfragen und drei Plaetze nicht glatt aufgehen, wird der Versatz
erhoeht, falls er je einen gemeinsamen Teiler mit der Laenge haette --
sonst kaemen nur zwei feste Haelften dran, und der Rest nie.

3. WAS DARAUS FOLGTE, OHNE DASS ES IM PLAN STAND.

- `rhythmus()` zaehlte gegen ALLE Fragen. Mit der Rotation waere das nie
  wieder erfuellt gewesen: Jede Runde haette sich selbst fuer
  unvollstaendig erklaert und die Seite haette taeglich gefragt.
  Jetzt gegen den Kern, der in jeder Runde enthalten ist.
- Die Auswertung sieht ALLE zwoelf Fragen, nicht die acht dieser Runde.
  Der Verlauf hoert nicht auf, wenn eine Frage pausiert.
- Der Verlauf zeigt jede Frage MIT Geschichte (nach zwei Runden elf von
  zwoelf), nicht die der aktuellen Runde. Leere Felder heissen jetzt
  ausdruecklich "war nicht dran", nicht "keine Antwort".
- "seit 2 Runden" heisst jetzt "die letzten 2 Male". Mit der Rotation
  war die alte Formulierung eine Behauptung ueber eine Runde, in der
  gar nicht gefragt wurde.

4. ZWEI DINGE, DIE KEIN ZAHLENVERGLEICH GEFUNDEN HAT -- nur das
   Ansehen des Bildschirmfotos, und beide waren wahr und trotzdem
   irrefuehrend:

- 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 acht Fragen zaehlt, wo zwoelf im Katalog
  stehen, haelt vier fuer verschwunden.
- "Das war deine Antwort beim letzten Mal" stand an JEDER Frage,
  unmittelbar nachdem man sie beantwortet hatte.

Beide haben jetzt eine Pruefung, inklusive Gegenprobe ueber eine
zurueckdatierte Runde.

GEMESSEN: pruef-befinden 100/0 (vorher 75 -- die Zahl ist gestiegen,
nicht gefallen), pruef-entwicklung 43/0, pruef-push-ziel 10/0,
pruef-werdegang 95/0.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
2026-09-19 10:33:07 +02:00
co-authored by Claude Opus 5
parent 19e9f471d3
commit 6c10ef0600
42 changed files with 1085 additions and 499 deletions
+216 -23
View File
@@ -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.";
}