Add per-user learned machine profile; fix methodology honesty issues
Test and deploy / test-and-deploy (push) Successful in 1m26s
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:
@@ -8,6 +8,7 @@ import { parseAlog } from "./alog.js";
|
||||
import { listAlogLibrary, readAlogFromLibrary } from "./alog-library.js";
|
||||
import { sendMail } from "./mailer.js";
|
||||
import { coerceSession, computeTotalScore, blankSession } from "../shared/cupping.js";
|
||||
import { computeMachineProfile } from "../shared/learn.js";
|
||||
|
||||
const hash = (value) => crypto.createHash("sha256").update(value).digest("hex");
|
||||
const token = () => crypto.randomBytes(32).toString("base64url");
|
||||
@@ -723,6 +724,21 @@ export function createApp({ db, root, env = process.env } = {}) {
|
||||
},
|
||||
);
|
||||
|
||||
// Learned per-user machine profile (pace factor + temperature bands) from this account's own
|
||||
// completed roasts — see shared/learn.js. Deliberately reads every one of the user's plans
|
||||
// rather than paginating: a personal roast log tops out at low hundreds of rows, and this is
|
||||
// the only place that number gets reduced, so there's nothing to cache incrementally against.
|
||||
app.get("/api/machine-profile", requireAuth, async (req, res, next) => {
|
||||
try {
|
||||
const rows = (
|
||||
await db.query("SELECT plan FROM roast_plans WHERE user_id=$1", [req.user.id])
|
||||
).rows;
|
||||
res.json({ ok: true, profile: computeMachineProfile(rows.map((r) => r.plan)) });
|
||||
} catch (e) {
|
||||
next(e);
|
||||
}
|
||||
});
|
||||
|
||||
// ─── Inventory ─────────────────────────────────────────────────────────
|
||||
// Strict on purpose: JSON.stringify silently turns a client-side NaN (e.g. a non-numeric
|
||||
// weight/cost typed into a field that isn't really constrained to digits) into null, which
|
||||
@@ -850,6 +866,46 @@ export function createApp({ db, root, env = process.env } = {}) {
|
||||
}
|
||||
},
|
||||
);
|
||||
// The auto-refine-carry-forward feature: the most recent past roast against this lot whose
|
||||
// "One change next batch" note was actually filled in, so the planner can offer it as a
|
||||
// starting ± Refine value instead of the user retyping their own prior conclusion. Reads
|
||||
// inventory.lotId straight off the plan JSONB — no join needed, and scoping to this user's own
|
||||
// roast_plans is what keeps this ownership-safe regardless of whether :id even belongs to them.
|
||||
app.get(
|
||||
"/api/inventory/:id/last-refine",
|
||||
requireAuth,
|
||||
requireUuidParam("id"),
|
||||
async (req, res, next) => {
|
||||
try {
|
||||
const lot = (
|
||||
await db.query(
|
||||
"SELECT id FROM green_bean_lots WHERE id=$1 AND user_id=$2",
|
||||
[req.params.id, req.user.id],
|
||||
)
|
||||
).rows[0];
|
||||
if (!lot) return res.status(404).json({ ok: false, code: "not_found" });
|
||||
const row = (
|
||||
await db.query(
|
||||
`SELECT plan->'afterRoast'->>'oneChange' AS one_change,
|
||||
plan->'fields'->>'0.1' AS plan_title, updated_at
|
||||
FROM roast_plans
|
||||
WHERE user_id=$1 AND plan->'inventory'->>'lotId'=$2
|
||||
AND coalesce(plan->'afterRoast'->>'oneChange','')<>''
|
||||
ORDER BY updated_at DESC LIMIT 1`,
|
||||
[req.user.id, req.params.id],
|
||||
)
|
||||
).rows[0];
|
||||
res.json({
|
||||
ok: true,
|
||||
refine: row
|
||||
? { oneChange: row.one_change, planTitle: row.plan_title, atIso: row.updated_at }
|
||||
: null,
|
||||
});
|
||||
} catch (e) {
|
||||
next(e);
|
||||
}
|
||||
},
|
||||
);
|
||||
app.put(
|
||||
"/api/inventory/:id",
|
||||
requireAuth,
|
||||
|
||||
Reference in New Issue
Block a user