Add per-user learned machine profile; fix methodology honesty issues
Test and deploy / test-and-deploy (push) Successful in 1m26s

Replaces the flat additive batch-size correction with a per-user
multiplicative pace factor learned from each account's own logged
roasts (shared/learn.js, GET /api/machine-profile), plus a lot-scoped
"last refine" auto-suggestion. Also fixes the reference data being
framed as "your own roasts" on a now-multi-user product, and
reclassifies the drying/Maillard sanity checks as informational since
they're algebraically derived from the DTR check rather than
independent (yellow = 0.56 x first crack, not entered separately).

Bug fixes found by an adversarial Opus review of the first pass:
- Unicode minus sign (U+2212) broke duration parsing against the
  app's own generated refine-suggestion text
- Printed sanity-checks table still showed a bare pass/fail glyph for
  the now-informational drying/Maillard rows
- Printed time-ledger box didn't show the pace multiplication step,
  so it stopped reconciling by hand once pace != 1
- Field 6.4 (manual batch correction) was double-counted: excluded
  from the learned-pace fit but added back after the multiplication
- Learned pace had no outlier rejection or hard clamp
- computeLedger had no test coverage
- Batch-size help copy overstated what the pace factor models (it's
  a single blanket ratio, not conditioned on batch weight)

A follow-up Opus pass also caught the per-user profile cache
surviving logout/account-switch in a shared browser; fixed by
sweeping it alongside the existing plan-draft cleanup.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
2026-07-31 12:40:03 -04:00
co-authored by Claude Sonnet 5
parent 0a0356b86e
commit 2237c199c1
14 changed files with 999 additions and 83 deletions
+58 -11
View File
@@ -499,7 +499,17 @@
>0 the first time you roast this coffee. Afterwards,
carry "One change next batch" here — this is how the
plan learns.</span
></label
>
<div class="refine-suggestion hidden" id="refine-suggestion">
<span id="refine-suggestion-text"></span>
<button
type="button"
class="ghost-btn small hidden"
id="btn-apply-refine"
>
Apply
</button>
</div></label
>
<label class="field" data-fid="1.7"
><span class="field-label">± Dev modifier</span
@@ -1242,6 +1252,7 @@
Drying + Maillard + Development = the whole roast. Cultivar
sets first crack; process and level set development.
</p>
<p class="dock-note machine-profile-note" id="machine-profile-note"></p>
<div class="ledger-group">
<div class="ledger-row">
@@ -1270,6 +1281,16 @@
title="225 g lands first crack ~1:302:00 later than 200 g"
/>
</div>
<div class="ledger-row ledger-subtotal">
<span class="l-fid"></span
><span class="l-label">Subtotal (before pace)</span
><output class="l-val" data-out="subtotalA"></output>
</div>
<div class="ledger-row ledger-pace">
<span class="l-fid"></span
><span class="l-label">× Machine pace</span
><output class="l-val" data-out="pace-factor">×1.00</output>
</div>
</div>
<div class="ledger-total hero">
<span>First crack</span><output data-out="A"></output>
@@ -1325,19 +1346,22 @@
<div class="check-tile unknown" data-pass="drying">
<span class="check-name">Drying share</span>
<output class="check-value" data-out="check-drying"></output>
<span class="check-band">target 4351%</span>
<span class="check-band">informational, target 4351%</span>
<span class="check-why"
>high = late yellow, browning starved · low = rushed
before internal pressure built</span
>Derived from the development ratio (yellow is 0.56 ×
first crack, not entered independently) — this can't fail
on its own. High = late yellow, browning starved · low =
rushed before internal pressure built.</span
>
</div>
<div class="check-tile unknown" data-pass="maillard">
<span class="check-name">Maillard share</span>
<output class="check-value" data-out="check-maillard"></output>
<span class="check-band">target 3339%</span>
<span class="check-band">informational, target 3339%</span>
<span class="check-why"
>low = too small a browning window for sweetness and
complexity</span
>Same derivation as drying share — informational, not an
independent check. Low = too small a browning window for
sweetness and complexity.</span
>
</div>
<div class="check-tile unknown" data-pass="dtr">
@@ -1878,6 +1902,22 @@
/><span class="pv"></span>
</td>
</tr>
<tr class="row">
<td class="ln"></td>
<td class="desc">Subtotal (before pace)</td>
<td class="val"><output data-out="subtotalA"></output></td>
</tr>
<tr class="row">
<td class="ln"></td>
<td class="desc">
<span class="op">×</span> Machine pace
<span class="sub"
>learned from your own roasts, else ×1.00
(reference)</span
>
</td>
<td class="val"><output data-out="pace-factor">×1.00</output></td>
</tr>
<tr class="total">
<td class="ln">A</td>
<td class="desc"><strong>FIRST CRACK</strong></td>
@@ -1964,16 +2004,16 @@
</thead>
<tbody>
<tr>
<td>Drying share</td>
<td>Drying share (informational)</td>
<td class="num">4351 %</td>
<td class="num">
<output data-out="check-drying"></output>
</td>
<td class="num passcell unknown" data-pass="drying"></td>
<td>high = late yellow · low = rushed</td>
<td>can't fail on its own — see * below</td>
</tr>
<tr>
<td>Maillard share</td>
<td>Maillard share (informational)</td>
<td class="num">3339 %</td>
<td class="num">
<output data-out="check-maillard"></output>
@@ -1982,7 +2022,7 @@
class="num passcell unknown"
data-pass="maillard"
></td>
<td>low = too little browning</td>
<td>can't fail on its own — see * below</td>
</tr>
<tr>
<td>Development ratio</td>
@@ -2004,6 +2044,13 @@
</tr>
</tbody>
</table>
<p class="wsfoot">
* Drying share and Maillard share are always in-band
whenever Development ratio passes — yellow is calculated as
a fixed 56% of first crack, not measured independently, so
these two rows can't fail on their own. Only Development
ratio and Development ceiling are real pass/fail checks.
</p>
</div>
</div>
</section>