Files
roast_command_center/shared/ledger.js
T
snowspeeder 824a964f9d
Test and deploy / test-and-deploy (push) Successful in 2m28s
Rebuild printable roast plans and remove machine-based adjustments
2026-09-05 14:56:11 -04:00

120 lines
4.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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,
};
}