Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions document/06-traps.md
Original file line number Diff line number Diff line change
Expand Up @@ -718,3 +718,28 @@ The general shape: **a rule with three call sites and no name has no place to
add a case to.** When a third value joins a two-value decision, the first thing
to look for is the other spellings of that decision. `grep` for the ternary,
not for the function, because there is no function yet, which is the problem.

## A parameter with a default that every caller leaves out

Today read **Stock: 0 doses** for a compound with a full pack of sixty tablets
on the shelf.

`stockFor` takes a container and defaults it to `"vial"`, which was right when
a vial was the only thing there was. Spray bottles added the parameter; tablets
added a third value for it. All three call sites went on omitting it, so every
figure on every screen counted vials and nothing else. Nothing failed, nothing
warned, and the zero looked like an empty shelf rather than like a question
that was never asked.

Same shape as the container the log form picks a dose from, one file over. A
default is a decision made once and inherited silently by everybody who does
not know a decision is being made.

The rule: **when a parameter gains a value, grep the call sites that omit it,
not the ones that pass it.** The callers that already pass something have been
thought about. The ones relying on the default have not, and they are invisible
in a search for the parameter's name.

`needsReconstitution` was the same sentence one line further down: with the
container finally arriving, it had to stop being true for a pack, which has
nothing to make up.
16 changes: 13 additions & 3 deletions src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ import {
startOfLocalDay,
unloggedDoseTimes,
} from "@/lib/calc/schedule";
import { daysOfSupplyForProtocol, vialConcentration } from "@/lib/calc/inventory";
import { containerForDose, daysOfSupplyForProtocol, vialConcentration } from "@/lib/calc/inventory";
import { routeHasSite, siteChoices, suggestSite } from "@/lib/calc/sites";
import {
currentStreak,
Expand Down Expand Up @@ -236,7 +236,17 @@ export default function NowPage() {

const targetMcg = scheduledDoseMcg(protocol, now);
const due = dueStatus(protocol, now, { lastLoggedAt });
const stock = stockFor(vials, protocol.peptideId, targetMcg, now);
/*
* From the container this compound is actually taken out of. Left to its
* default this counted vials only, so a protocol on tablets read
* "0 doses" with a full pack on the shelf.
*/
const stock = stockFor(
vials,
protocol.peptideId,
targetMcg,
now,
containerForDose(peptide?.preparation, protocol.route));

/**
* What "100% of a single-dose peak" is measured against.
Expand Down Expand Up @@ -957,7 +967,7 @@ export default function NowPage() {
<span className="text-[var(--ink)]">
{track.lastLoggedAt ? relativeTime(track.lastLoggedAt, now) : t("now_never")}
</span>
{track.lastLog?.site && (
{track.lastLog?.site && routeHasSite(track.lastLog.route) && (
<span className="text-[var(--ink)]">
{" · "}
{siteLabel(track.lastLog.site)}
Expand Down
9 changes: 7 additions & 2 deletions src/app/stock/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { useSyringeScale } from "@/components/DoseMarks";
import {
diluentAfterTopUp,
groupSealedVials,
containerForDose,
marksFromVial,
openPack,
stockFor,
Expand Down Expand Up @@ -170,14 +171,18 @@ export default function StockPage() {
if (hit) return hit;

const p = protocols.find((x) => x.active && x.peptideId === peptideId);
const container = containerForDose(findPeptide(custom, peptideId)?.preparation, p?.route ?? "subcutaneous");
const out: SupplyOutlook = p
? supplyOutlook(stockFor(vials, peptideId, scheduledDoseMcg(p, now), now), p, now)
? supplyOutlook(
stockFor(vials, peptideId, scheduledDoseMcg(p, now), now, container),
p,
now)
: { kind: "unknown" };

cache.set(peptideId, out);
return out;
};
}, [protocols, vials, now]);
}, [protocols, vials, custom, now]);

if (!hydrated) {
return <div className="py-20 text-center text-[14px] text-[var(--faint)]">{t("loading")}</div>;
Expand Down
2 changes: 1 addition & 1 deletion src/components/LogDoseSheet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -326,7 +326,7 @@ export function LogDoseSheet({

// What the stock looks like once this dose is taken. This is the number the
// user actually wants: how many more of these are left.
const stock = stockFor(vials, peptideId, doseMcg, at);
const stock = stockFor(vials, peptideId, doseMcg, at, container);
const willDeplete = !skipped && !!vialId && doseMcg > 0;
const dosesAfter = Math.max(0, stock.dosesRemaining - (willDeplete ? 1 : 0));
const vialLeftAfter = vial
Expand Down
32 changes: 32 additions & 0 deletions src/lib/calc/inventory.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -938,3 +938,35 @@ describe("opening a pack", () => {
expect(pickVialForDose(vials, "klow", 10_000, NOW, "pack")?.id).toBe("started");
});
});

/*
* The container has to be asked for. It defaults to a vial, and a screen that
* leaves it out counts nothing for a compound sold as tablets: Today read
* "0 doses" with a full pack on the shelf.
*/
describe("stockFor and the container", () => {
const pack = (over: Partial<Vial> = {}) =>
vial({ id: "p", container: "pack", mgPerTablet: 10, strengthMg: 600, state: "sealed", ...over });

it("counts nothing for a pack when asked about vials", () => {
expect(stockFor([pack()], "klow", 10_000, NOW).dosesRemaining).toBe(0);
});

it("counts the pack when asked about packs", () => {
const s = stockFor([pack()], "klow", 10_000, NOW, "pack");
expect(s.dosesRemaining).toBe(60);
expect(s.availableMcg).toBe(600_000);
});

/* Nothing to make up, so nobody is told to reach for the water. */
it("never asks for a pack to be reconstituted", () => {
expect(stockFor([pack()], "klow", 10_000, NOW, "pack").needsReconstitution).toBe(false);
expect(stockFor([vial({ id: "v" })], "klow", 10_000, NOW).needsReconstitution).toBe(true);
});

it("leaves a vial and a pack of the same compound out of each other's count", () => {
const both = [pack(), vial({ id: "v", strengthMg: 10 })];
expect(stockFor(both, "klow", 10_000, NOW, "pack").dosesRemaining).toBe(60);
expect(stockFor(both, "klow", 10_000, NOW).dosesRemaining).toBe(1);
});
});
7 changes: 6 additions & 1 deletion src/lib/calc/inventory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -405,7 +405,12 @@ export function stockFor(
openCount,
dosesRemaining: per(availableMcg),
dosesInOpenVials: per(openMcg),
needsReconstitution: openCount === 0 && sealedCount > 0,
/*
* Only ever true of a vial. A pack and a spray bottle have nothing to make
* up, so telling their owner to reach for the water would be advice about
* a container they are not holding.
*/
needsReconstitution: container === "vial" && openCount === 0 && sealedCount > 0,
dosesExpired: per(expiredMcg),
};
}
Expand Down
18 changes: 15 additions & 3 deletions src/lib/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import {
reconcileVials,
returnToVial,
stockFor as computeStock,
type ContainerKind,
vialConcentration,
vialExpired,
vialFractionRemaining,
Expand Down Expand Up @@ -966,9 +967,20 @@ export function vialStatus(vial: Vial, nowMs = Date.now()): VialStatus {
};
}

/** Doses of a peptide still available across every usable vial. */
export function stockFor(vials: Vial[], peptideId: string, doseMcg: number, nowMs = Date.now()) {
return computeStock(vials, peptideId, doseMcg, nowMs);
/**
* Doses of a peptide still available across every usable container.
*
* The container has to be passed, not defaulted, wherever the answer is shown
* to somebody: the default is a vial, and a caller that leaves it out counts
* nothing for a compound that comes as tablets or lives in a spray bottle.
*/
export function stockFor(
vials: Vial[],
peptideId: string,
doseMcg: number,
nowMs = Date.now(),
container: ContainerKind = "vial") {
return computeStock(vials, peptideId, doseMcg, nowMs, container);
}

export { vialCapacityMcg, pickVialForDose } from "./calc/inventory";
Loading