fix: recalibrate Neapolitan yeast for v1.1
This commit is contained in:
@@ -2,6 +2,8 @@
|
||||
|
||||
Pizzateig-Kalkulator als statische Single-Page-App.
|
||||
|
||||
Aktuelle Version: **v1.1**
|
||||
|
||||
- Bäckerprozent
|
||||
- Mehl-Datenbank
|
||||
- Vorteig
|
||||
|
||||
+12
-9
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||||
<title>TEIG//TERMINAL — Pizzateig-Kalkulator</title>
|
||||
<title>TEIG//TERMINAL v1.1 — Pizzateig-Kalkulator</title>
|
||||
<meta name="description" content="Pizzateig-Rechner mit Bäckerprozent, Mehl-Datenbank, Vorteig und Rückwärts-Zeitplan.">
|
||||
<meta name="color-scheme" content="dark">
|
||||
<style>
|
||||
@@ -304,7 +304,7 @@ footer b{color:var(--txt)}
|
||||
<header>
|
||||
<h1 class="brand">TEIG<em>//</em>TERMINAL</h1>
|
||||
<p class="tag">Pizzateig-Kalkulator · Bäckerprozent · Vorteig · Rückwärts-Zeitplan</p>
|
||||
<div class="status"><span class="dot"></span><span id="statusline">System bereit</span></div>
|
||||
<div class="status"><span class="dot"></span><span id="statusline">System bereit · v1.1</span></div>
|
||||
</header>
|
||||
|
||||
<div class="quick-summary noprint" id="quickSummary" aria-label="Kurzüberblick"></div>
|
||||
@@ -553,10 +553,10 @@ const FLOURS = [
|
||||
{g:"Spezial", id:"custom", n:"Eigenes Mehl / Blend", w:"—", pr:0, hyd:[50,90], reife:[0,120], vt:true, note:"Keine Prüfung — du weißt, was du tust."}
|
||||
];
|
||||
|
||||
/* ---------- Stile (tf = Teigdichte in g/cm²) ---------- */
|
||||
/* ---------- Stile (tf = Teigdichte in g/cm², yeastK = Frischhefe-Referenz) ---------- */
|
||||
const STYLES = {
|
||||
napo: {n:"Napoletana", ball:250, hyd:62, salt:2.8, oil:0, sugar:0, tf:0.36, mode:"w"},
|
||||
contemp: {n:"Contemporanea", ball:280, hyd:70, salt:2.8, oil:0, sugar:0, tf:0.39, mode:"w"},
|
||||
napo: {n:"Napoletana", ball:250, hyd:62, salt:2.8, oil:0, sugar:0, tf:0.36, mode:"w", yeastK:1.5},
|
||||
contemp: {n:"Contemporanea", ball:280, hyd:70, salt:2.8, oil:0, sugar:0, tf:0.39, mode:"w", yeastK:1.7},
|
||||
ny: {n:"New York", ball:300, hyd:63, salt:2.0, oil:2.5, sugar:1.5, tf:0.31, mode:"w"},
|
||||
tonda: {n:"Römisch tonda", ball:200, hyd:57, salt:2.2, oil:4, sugar:0, tf:0.26, mode:"w"},
|
||||
teglia: {n:"Teglia / Blech", ball:800, hyd:78, salt:2.2, oil:2, sugar:0, tf:0.55, mode:"p"},
|
||||
@@ -582,7 +582,7 @@ const SEQ = {
|
||||
warmcold: {n:"Warm → Kalt", fridge:true,
|
||||
note:"Hybrid: erst Stockgare bei Raumtemperatur, dann in den Kühlschrank, kurz vor dem Backen aufwärmen. Sehr verbreitet, braucht wenig Hefe — aber der Teig kühlt langsam durch und gärt im Kühlschrank wärmer als gedacht."},
|
||||
coldwarm: {n:"Kalt → Warm", fridge:true,
|
||||
note:"Lehmann-Methode: direkt nach dem Kneten kalt stellen, die Gare passiert im Kühlschrank, die warme Zeit danach dient dem Handling. Am besten planbar, braucht deutlich mehr Hefe."}
|
||||
note:"Direkt nach dem Kneten kalt stellen, anschließend in warmer Stückgare ausreifen lassen. Gut planbar und besonders für längere Führungen geeignet; die warme Endgare macht die Ballen sichtbar aktiv und dehnbar."}
|
||||
};
|
||||
const STAGLIO = {
|
||||
before:"Ballen vor der Kälte",
|
||||
@@ -670,7 +670,7 @@ function calc(s){
|
||||
const pfFrac= hasPf ? s.pfPct/100 : 0;
|
||||
|
||||
// --- Triebmittel in % vom Mehl ---
|
||||
let fresh = 6/U; // Frischhefe-Referenz
|
||||
let fresh = (st.yeastK ?? 6)/U; // stilbezogene Frischhefe-Referenz
|
||||
fresh *= 1 + Math.max(0, s.salt-2.5)*0.06; // Salz bremst
|
||||
if (hasPf) fresh *= (1 - 0.8*pfFrac); // Vorteig bringt schon Trieb
|
||||
fresh = clamp(fresh, 0.01, 6);
|
||||
@@ -873,7 +873,7 @@ function applySimpleFermentation(){
|
||||
|
||||
S.seq = "coldwarm";
|
||||
S.staglio = "after";
|
||||
S.roomH = doughH <= 18 ? 2 : 3;
|
||||
S.roomH = 4;
|
||||
S.fridgeH = Math.max(4, Math.round(doughH - S.roomH));
|
||||
const total = S.roomH + S.fridgeH;
|
||||
S.plan = total <= 18 ? "night" : total <= 30 ? "d24" : total <= 54 ? "d48" : "d72";
|
||||
@@ -1003,6 +1003,8 @@ function renderWarnings(f,r){
|
||||
if (r.hasPf && !f.vt)
|
||||
w.push(["",`${f.n} ist für Vorteige eher schwach. Poolish und Biga verlangen mehr Backstärke.`]);
|
||||
}
|
||||
if ((S.style==="napo" || S.style==="contemp") && S.seq==="room" && S.roomH<4)
|
||||
w.push(["",`<b>Schnellteig:</b> Unter 4 h braucht neapolitanischer Teig vergleichsweise viel Hefe und entwickelt weniger Aroma. Wenn möglich, mindestens 4–8 h einplanen.`]);
|
||||
if (S.fridgeT<4) w.push(["",`Unter 4 °C stellt die Hefe die Arbeit fast ein. 5–6 °C sind für die Kühlschrankgare der bessere Bereich.`]);
|
||||
if (S.seq==="warmcold" && S.staglio==="after" && S.fridgeH>0 && r.tcCold > S.fridgeH*0.4)
|
||||
w.push(["",`Die Masse braucht rund ${Math.round(r.tcCold)} h zum Durchkühlen — bei ${S.fridgeH} h Kaltgare gärt der Kern also die halbe Zeit deutlich wärmer als ${n1(S.fridgeT)} °C. Teig flach drücken oder auf mehrere Behälter aufteilen, sonst wird das Ergebnis schwer wiederholbar.`]);
|
||||
@@ -1010,7 +1012,8 @@ function renderWarnings(f,r){
|
||||
w.push(["",`Bei der Lehmann-Methode ist die Gare im Kühlschrank abgeschlossen. ${n1(S.roomH)} h warm danach sind viel — der Teig übergärt leicht. 0,5 bis 2 h reichen, um ihn dehnbar zu bekommen.`]);
|
||||
if (S.seq==="coldwarm" && S.roomH<0.5)
|
||||
w.push(["",`Ohne Aufwärmzeit ist der Teig sehr elastisch und wehrt sich beim Öffnen. Ab etwa 10 °C Kerntemperatur wird er dehnbar.`]);
|
||||
if (!r.isSour && r.yPct<0.03) w.push(["",`Die Hefemenge ist rechnerisch winzig. Ohne Feinwaage mit 0,01 g Auflösung wird das Raten — kürzere Gare oder Vorteig sind praktikabler.`]);
|
||||
const yeastG = r.hasPf ? r.pf.yeast+r.main.yeast : r.main.yeast;
|
||||
if (!r.isSour && yeastG<0.5) w.push(["",`Nur ${yeastG.toFixed(2).replace(".",",")} g Hefe: Für reproduzierbare Ergebnisse eine Feinwaage mit 0,01 g Auflösung verwenden oder die Hefe in Wasser lösen und anteilig dosieren.`]);
|
||||
if (!r.isSour && r.yPct>3) w.push(["",`Sehr viel Hefe. Der Teig geht schnell, aber flach im Geschmack. Mehr Zeit gibt mehr Aroma.`]);
|
||||
if (r.hasPf && r.main.water<0)
|
||||
w.push(["bad",`Der Vorteig enthält mehr Wasser als das ganze Rezept hergibt. Vorteig-Anteil senken oder Hydration erhöhen.`]);
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const vm = require('node:vm');
|
||||
|
||||
const html = fs.readFileSync(path.join(__dirname, '..', 'index.html'), 'utf8');
|
||||
|
||||
function between(start, end) {
|
||||
const from = html.indexOf(start);
|
||||
const to = html.indexOf(end, from);
|
||||
assert.ok(from >= 0 && to > from, `Quellbereich fehlt: ${start} … ${end}`);
|
||||
return html.slice(from, to);
|
||||
}
|
||||
|
||||
const core = between('const STYLES =', '/* ============================================================\n ZEITPLAN');
|
||||
const simple = between('function applySimpleFermentation()', 'function initUI()');
|
||||
const warnings = between('function renderWarnings(', 'function renderRecipe(');
|
||||
const fixedNow = 1_700_000_000_000;
|
||||
class FixedDate extends Date {
|
||||
static now() { return fixedNow; }
|
||||
}
|
||||
const warningHost = { innerHTML: '' };
|
||||
const sandbox = {
|
||||
Date: FixedDate,
|
||||
console,
|
||||
$: id => id === 'warns' ? warningHost : {},
|
||||
pct: x => `${x} %`,
|
||||
n1: x => String(x),
|
||||
};
|
||||
vm.createContext(sandbox);
|
||||
vm.runInContext(`${core}\n${simple}\n${warnings}\nglobalThis.api={STYLES,DEFAULT,calc,applySimpleFermentation,renderWarnings,setState:s=>S=s,getState:()=>S};`, sandbox);
|
||||
const { STYLES, DEFAULT, calc, applySimpleFermentation, renderWarnings, setState, getState } = sandbox.api;
|
||||
|
||||
function state(overrides = {}) {
|
||||
return { ...DEFAULT, pf: 'none', yeast: 'fresh', balls: 4, ball: 250, mode: 'w', ...overrides };
|
||||
}
|
||||
|
||||
// Napoletana und Contemporanea werden bewusst niedriger kalibriert.
|
||||
assert.equal(STYLES.napo.yeastK, 1.5);
|
||||
assert.equal(STYLES.contemp.yeastK, 1.7);
|
||||
|
||||
// 8 h warme Napoletana bei 20 °C: rund 0,19 % Frischhefe statt bisher 0,76 %.
|
||||
const napo8 = calc(state({ style: 'napo', seq: 'room', roomH: 8, roomT: 20, fridgeH: 0 }));
|
||||
assert.ok(Math.abs(napo8.yPct - 0.190875) < 1e-9, `unerwartet: ${napo8.yPct}%`);
|
||||
assert.ok(napo8.main.yeast > 1.1 && napo8.main.yeast < 1.2, `unerwartet: ${napo8.main.yeast} g`);
|
||||
|
||||
// Nicht neu kalibrierte Stile behalten den bisherigen Referenzfaktor 6.
|
||||
const ny8 = calc(state({ style: 'ny', salt: 2, seq: 'room', roomH: 8, roomT: 20, fridgeH: 0 }));
|
||||
assert.equal(ny8.yPct, 0.75);
|
||||
|
||||
// Lange Führung im Einfach-Modus endet mit 4 h warmer Stückgare.
|
||||
setState(state({ uxMode: 'simple', bake: fixedNow + 24.75 * 3600e3 }));
|
||||
applySimpleFermentation();
|
||||
assert.equal(getState().seq, 'coldwarm');
|
||||
assert.equal(getState().roomH, 4);
|
||||
assert.equal(getState().fridgeH, 20);
|
||||
|
||||
setState(state({ uxMode: 'simple', bake: fixedNow + 48.75 * 3600e3 }));
|
||||
applySimpleFermentation();
|
||||
assert.equal(getState().roomH, 4);
|
||||
assert.equal(getState().fridgeH, 44);
|
||||
|
||||
// Unter 4 h wird Napoletana ausdrücklich als Schnellteig gekennzeichnet.
|
||||
const napo3State = state({ style: 'napo', seq: 'room', roomH: 3, roomT: 20, fridgeH: 0 });
|
||||
setState(napo3State);
|
||||
const napo3 = calc(napo3State);
|
||||
renderWarnings({ n: 'Testmehl', reife: [0, 120], hyd: [50, 90], vt: true }, napo3);
|
||||
assert.match(warningHost.innerHTML, /Schnellteig/);
|
||||
|
||||
// Kleine absolute Hefemengen nennen die Feinwaage, auch wenn der Prozentwert über 0,03 % liegt.
|
||||
const napo24State = state({ style: 'napo', seq: 'room', roomH: 24, roomT: 20, fridgeH: 0 });
|
||||
setState(napo24State);
|
||||
const napo24 = calc(napo24State);
|
||||
assert.ok(napo24.main.yeast < 0.5 && napo24.yPct > 0.03);
|
||||
renderWarnings({ n: 'Testmehl', reife: [0, 120], hyd: [50, 90], vt: true }, napo24);
|
||||
assert.match(warningHost.innerHTML, /Feinwaage/);
|
||||
|
||||
console.log('yeast calibration regression tests: OK');
|
||||
Reference in New Issue
Block a user