// Shared planning math. Machine profiles are accepted for legacy callers but ignored. import { parseDuration, parseRangeMidpoint, } from "./time.js?v=__ASSET_VERSION__"; import { YELLOW_RATIO, SANITY_BANDS, } from "./reference-data.js?v=__ASSET_VERSION__"; /** All durations are seconds; null means the plan cannot yet be computed. * The optional legacy profile argument is intentionally ignored. */ export function computeLedger(plan, machineProfile = null) { const f = plan.fields ?? {}; const isBlend = f["2.1"] === "blend"; const warnings = []; const pace = 1; // Roaster history and overrides never modify a plan. const l1 = parseRangeMidpoint(isBlend ? f["2.4"] : f["1.4"]); const l2 = parseDuration(f["1.6"]) ?? 0; const l3 = parseDuration(f["5.6"]) ?? 0; const l4 = parseDuration(f["6.4"]) ?? 0; const baseA = l1 === null ? null : l1 + l2 + l3 + l4; const A = baseA !== null && baseA > 0 ? baseA : null; if (baseA !== null && A === null) warnings.push( "First crack must be after charge; check timing adjustments.", ); const yellow = A === null ? null : Math.round(A * YELLOW_RATIO); const maillard = A === null ? null : A - yellow; const l7 = parseRangeMidpoint(f["4.3"]); const l8 = parseDuration(isBlend ? f["2.5"] : f["3.2"]) ?? 0; const l9 = parseDuration(f["1.7"]) ?? 0; const rawC = l7 === null ? null : l7 + l8 + l9; const explicitC = parseDuration(f["4.4"]); const suggestedC = rawC !== null && f["4.1"] === "light" ? Math.max(75, rawC) : rawC; const invalidOverride = f["4.4"] && explicitC === null; if (invalidOverride) warnings.push( "Development override is invalid; use m:ss with seconds below 60.", ); const candidateC = invalidOverride ? null : (explicitC ?? suggestedC); const C = candidateC !== null && candidateC > 0 ? candidateC : null; if (explicitC === null && rawC !== suggestedC) warnings.push( "Development modifiers reduced the light-roast suggestion below 1:15. Using a 1:15 starting target; enter a development override to change it.", ); if (candidateC !== null && C === null) warnings.push("Development must be positive."); // rows 5 and 6 (yellow/Maillard) are NEVER summed here const D = A === null || C === null ? null : A + C; if (l1 === null) warnings.push( "No cultivar/blend anchor (field 1.4 or 2.4) — first crack cannot be computed.", ); if (l7 === null && explicitC === null) warnings.push( "No development base (field 4.3) — development cannot be computed.", ); const pct = (part) => (D && D > 0 && part !== null ? (part / D) * 100 : null); if (pct(C) !== null && (pct(C) < 12 || pct(C) > 20)) warnings.push( "Development ratio is outside the 12–20% planning range. Review color, aroma and the cup; DTR alone does not determine roast quality.", ); const density = Number(f["5.2"]); if (f["5.2"] && Number.isFinite(density) && (density < 300 || density > 1000)) warnings.push( "Check density units: enter g/L, not g/mL. For example, 0.7614 g/mL = 761.4 g/L. No automatic correction was made.", ); const checks = { drying: { ...passCheck(pct(yellow), SANITY_BANDS.drying), informational: true, }, maillard: { ...passCheck(pct(maillard), SANITY_BANDS.maillard), informational: true, }, dtr: passCheck(pct(C), SANITY_BANDS.dtr), ceiling: { valueS: C, maxS: SANITY_BANDS.ceilingS, pass: C === null ? null : C < SANITY_BANDS.ceilingS, }, }; return { isBlend, lines: { l1, l2, l3, l4, l7, l8, l9 }, pace, subtotalA: baseA, // lines 1-4 summed, before pace — exposed so the UI/print sheet can show // the multiplication step explicitly instead of jumping straight to A with no visible way to // reconstruct it from the printed lines (see the header comment above). A, yellow, maillard, rawC, C, D, checks, warnings, }; } function passCheck(pct, band) { return { pct, lo: band.lo, hi: band.hi, pass: pct === null ? null : pct >= band.lo && pct <= band.hi, }; }