mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-10 05:12:14 +00:00
MenstrualCycle rows were only ever written by the one-tap period boundary. Every other way of recording bleeding wrote a day-log and stopped there: the log sheet's flow chips, the bulk drain, an Apple Health export full of menstrual-flow samples. The engine reads cycle rows, so a person could log every period they had and the module still held that none had happened. No cycle length, no forecast, an empty chart, and an offer to log a first period. An Apple Health user who never found the one-tap button had the whole module dead. A first bleeding day now opens the cycle it belongs to, through one shared seam in the day-log write helper, so the single capture, the bulk drain and the Apple Health fold all infer a start the same way and the one-tap boundary's own write lands on a date that already has a cycle, which the rule declines. The rule is conservative, because a false start corrupts every length derived after it. Spotting does not open a cycle: it is as often the tail or the herald of a period as its first day. Bleeding flagged as between periods does not, since that flag exists to say so. Neither does a bleeding day within the hard physiological minimum of an existing start, which is what keeps day three of a period, or a forgotten day two, from starting a cycle of its own without needing a contiguity check. A genuinely short cycle is not inferred from flow alone; the one-tap start still opens one on any date. An edit is symmetric. Setting a flow opens the cycle; clearing it, or flagging it as between periods, removes the cycle that day opened and re-opens the one before it. A cycle that an edit can create and not undo leaves the record holding a period nothing on the day says happened. - the period route's start arm and the flow inference now share one boundary helper, so a start means the same thing however it is entered - back-dating re-anchors both neighbours, as the one-tap start does - integration: eleven cases through the real routes, of which five are about what must NOT open a cycle
68 lines
2.6 KiB
TypeScript
68 lines
2.6 KiB
TypeScript
/**
|
|
* The rule that decides whether a logged bleeding day starts a cycle.
|
|
*
|
|
* It is the one piece of the flow-to-cycle inference that is a judgement
|
|
* rather than bookkeeping, and a wrong yes is worse than a wrong no: a false
|
|
* cycle start corrupts every length the engine derives after it, while a
|
|
* missed one leaves the person exactly where they were before, with the
|
|
* one-tap start still available on any date.
|
|
*/
|
|
import { describe, it, expect } from "vitest";
|
|
|
|
import { bleedOpensCycle } from "../cycle-boundaries";
|
|
import { HARD_CYCLE_MIN } from "../types";
|
|
import { addDays } from "../day-math";
|
|
|
|
const DAY = "2026-06-01";
|
|
|
|
describe("bleedOpensCycle", () => {
|
|
it("opens the first cycle a record ever has", () => {
|
|
expect(bleedOpensCycle("MEDIUM", false, null, DAY)).toBe(true);
|
|
expect(bleedOpensCycle("HEAVY", false, null, DAY)).toBe(true);
|
|
expect(bleedOpensCycle("LIGHT", false, null, DAY)).toBe(true);
|
|
});
|
|
|
|
it("declines a day that records no bleeding", () => {
|
|
expect(bleedOpensCycle(null, false, null, DAY)).toBe(false);
|
|
expect(bleedOpensCycle("NONE", false, null, DAY)).toBe(false);
|
|
});
|
|
|
|
it("declines spotting", () => {
|
|
// As often the tail or the herald of a period as its first day, and the
|
|
// person has a chip for a real flow when it is one.
|
|
expect(bleedOpensCycle("SPOTTING", false, null, DAY)).toBe(false);
|
|
});
|
|
|
|
it("declines bleeding the person flagged as between periods", () => {
|
|
expect(bleedOpensCycle("HEAVY", true, null, DAY)).toBe(false);
|
|
});
|
|
|
|
it("declines a bleeding day inside the current cycle's plausible span", () => {
|
|
// Day two, day three, and a day somebody forgot and entered late: all of
|
|
// them are this period, not a new one.
|
|
for (const offset of [1, 2, 4, HARD_CYCLE_MIN - 1]) {
|
|
expect(bleedOpensCycle("HEAVY", false, addDays(DAY, -offset), DAY)).toBe(
|
|
false,
|
|
);
|
|
}
|
|
});
|
|
|
|
it("declines a bleeding day on a date a cycle already starts", () => {
|
|
// What the one-tap boundary's own day-log write hits.
|
|
expect(bleedOpensCycle("MEDIUM", false, DAY, DAY)).toBe(false);
|
|
});
|
|
|
|
it("opens the next cycle at the hard physiological minimum and beyond", () => {
|
|
expect(
|
|
bleedOpensCycle("MEDIUM", false, addDays(DAY, -HARD_CYCLE_MIN), DAY),
|
|
).toBe(true);
|
|
expect(bleedOpensCycle("MEDIUM", false, addDays(DAY, -28), DAY)).toBe(true);
|
|
});
|
|
|
|
it("declines when the nearest cycle start is AFTER the day", () => {
|
|
// Never happens through the resolver (it queries `startDate <= date`), but
|
|
// a negative gap must not read as a large one.
|
|
expect(bleedOpensCycle("HEAVY", false, addDays(DAY, 10), DAY)).toBe(false);
|
|
});
|
|
});
|