Prefill: classify unknown cultivars into a reference group so the ledger still computes
Test and deploy / test-and-deploy (push) Successful in 59s

The extraction schema gains cultivarGroup (typica/bourbon/ethiopian/
hybrid, classified by the LLM from coffee genetics); when the named
variety isn't in the cultivar table, deriveFields now falls back to the
group's first-crack anchor (number still comes only from the reference
table) instead of leaving 1.4 blank and breaking every downstream
calculation. deriveFields exported + unit-tested.

Co-Authored-By: Claude Fable 5 <[email protected]>
This commit is contained in:
Shane Maynard
2026-08-09 08:03:46 -04:00
co-authored by Claude Fable 5
parent f781930832
commit c130efb180
2 changed files with 79 additions and 3 deletions
+18 -3
View File
@@ -24,6 +24,7 @@ Schema:
{
"coffeeName": string|null,
"cultivar": string|null, // e.g. "Bourbon", "Gesha" — the single named variety, if any
"cultivarGroup": string|null, // one of: "typica","bourbon","ethiopian","hybrid" — the variety's genetic/roast-behavior family. Use your knowledge of coffee genetics to classify the named variety even when it is obscure (e.g. Wush Wush -> "ethiopian", Villa Sarchi -> "bourbon", Ruiru 11 -> "hybrid"). null only if no variety is stated or you genuinely cannot classify it
"origin": string|null, // country/region as stated
"producer": string|null,
"process": string|null, // one of: "washed","yellow-honey","red-black-honey","natural","anaerobic" — pick the closest match, or null
@@ -132,8 +133,9 @@ function parseModelJson(raw) {
}
/** Deterministic mapping from extracted page facts -> worksheet field IDs. The model never
* supplies a time/duration directly — every number here comes from shared/reference-data.js. */
function deriveFields(extracted) {
* supplies a time/duration directly — every number here comes from shared/reference-data.js.
* Exported for unit tests. */
export function deriveFields(extracted) {
const fields = {};
const provenance = {};
const warnings = [];
@@ -159,7 +161,20 @@ function deriveFields(extracted) {
set("1.7", formatSigned(row.devModS), "cultivar-table");
} else {
set("1.1", extracted.cultivar, "page");
warnings.push(`Cultivar "${extracted.cultivar}" is not in the reference table — fields 1.2/1.4/1.5/1.7 left blank; pick the nearest group by hand.`);
// Unknown cultivar: fall back to its GROUP's first-crack anchor so the ledger still
// computes, instead of leaving 1.4 blank and silently breaking every downstream number.
// The group comes from the model's genetics classification, but the NUMBER still only
// ever comes from the reference table.
const group = findGroup(extracted.cultivarGroup);
const groupAnchorS = group ? parseRangeMidpoint(group.fcAnchor) : null;
if (group && groupAnchorS !== null) {
set("1.2", group.key, "group-table");
set("1.4", formatDuration(groupAnchorS), "group-table");
set("1.7", "+0:00", "group-table");
warnings.push(`Cultivar "${extracted.cultivar}" is not in the reference table — using the ${group.label} group anchor (${group.fcAnchor}) as the first-crack target. Adjust field 1.4 if you know this variety behaves differently.`);
} else {
warnings.push(`Cultivar "${extracted.cultivar}" is not in the reference table — fields 1.2/1.4/1.5/1.7 left blank; pick the nearest group by hand.`);
}
}
}