Rebuild printable roast plans and remove machine-based adjustments
Test and deploy / test-and-deploy (push) Successful in 2m28s

This commit is contained in:
snowspeeder committed 2026-09-05 14:56:11 -04:00
1 parent e9f2393c96
commit 824a964f9d
30 files changed
+1721 -908

No files matched your search

+61 -33
View File
@@ -1,42 +1,31 @@
// Browser-safe. THE ONLY implementation of the worksheet's time-ledger math.
// Mirrors manual-roast-planner.html's worksheet box 6 (lines 1-9, totals A/C/D)
// and box 7 (the four sanity checks) exactly. Imported by both server and browser.
//
// Two changes from the original worksheet, made 2026-07-31 after a methodology review (see
// shared/learn.js's header): first crack is now scaled by a multiplicative machine-pace factor —
// the operator's own 14-roast data showed a ~13% batch-size increase stretching first crack ~40%,
// a scaling relationship a flat added-seconds guess can't represent. Pace is applied to the WHOLE
// pre-pace sum, INCLUDING line 4 (± batch-size correction), not just lines 1-3: shared/learn.js
// fits pace from each historical roast's l1+l2+l3+l4 against what actually happened, so applying
// it only to l1+l2+l3 here and adding l4 back afterwards would double-count whatever that
// historical l4 was already compensating for. And the drying/Maillard sanity checks are now
// marked `informational`: because yellow is DERIVED as YELLOW_RATIO × A rather than entered
// independently, drying% and Maillard% are algebraic consequences of the DTR check, not
// independent validations — whenever DTR passes, both are mathematically guaranteed to also pass.
// Only DTR and the development ceiling test anything a plan could actually fail.
// 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__";
import { parseDuration, parseRangeMidpoint } from "./time.js?v=__ASSET_VERSION__";
import { YELLOW_RATIO, SANITY_BANDS } from "./reference-data.js?v=__ASSET_VERSION__";
/**
* @param {import("./fields.js").Plan} plan
* @param {import("./learn.js").computeMachineProfile extends (...a:any)=>infer R ? R : never} [machineProfile]
* optional learned per-user profile from shared/learn.js; omitted or a profile with no learned
* data behaves exactly as before (pace 1.0 — the reference/original worksheet behavior).
* @returns ledger result — every time value in seconds, null when not computable.
*/
/** 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 = machineProfile?.pace?.value ?? 1;
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 ? null : Math.round(baseA * pace);
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;
@@ -44,18 +33,56 @@ export function computeLedger(plan, machineProfile = null) {
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 C = l7 === null ? null : l7 + l8 + l9; // rows 5 and 6 (yellow/Maillard) are NEVER summed here
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) warnings.push("No development base (field 4.3) — development cannot be computed.");
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 },
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,
@@ -74,6 +101,7 @@ export function computeLedger(plan, machineProfile = null) {
A,
yellow,
maillard,
rawC,
C,
D,
checks,