4 Commits
Author SHA1 Message Date
Alexander Heldt 5a08fb4510 Let a meal say what kind of food it was
Grams alone put dry and fresh in the same total, so the log could not show that
fresh had been creeping up or that a soft stomach followed a switch. A meal can
now carry a kind the user names themselves.

The whole thing is optional, and that constraint shaped most of it. "No kind"
is a real value rather than a missing one: it is what every meal already logged
carries, so nothing needed migrating; it is always offered in the picker; and
with no kinds defined the picker, the legend and the split are all absent, so
the app is byte-for-byte the one it was for anyone who never wants this. The
checks cover that case specifically, because it is the one nobody would notice
breaking.

Kinds are a third synced collection beside events and exercises, with the same
contract — uuid ids, per-item last-write-wins, tombstoned deletes — so renaming
a kind updates the meals logged as it, and deleting one leaves them readable
under the name the tombstone kept. FoodKindStore duplicates ExerciseStore
closely; Store and ExerciseStore were already near-twins, so a third in that
shape is this file's pattern and leaves two working collections untouched.
Folding all three into one store over a table name is the tidier end state and
a separate job.

Two decisions worth naming. The default kind is a flag on the kind rather than
a profile field: the profile is last-write-wins across the whole row, and this
codebase already carries a special case for pedigree_id because that dropped a
value once — per-item LWW means two devices that each choose a default resolve
to the newer instead. And each kind keeps a colorIndex fixed at creation, so
deleting one never repaints the charts of the kinds around it.

The bars stack by kind with a line fitted per kind. Each line sits at that
kind's own daily amount rather than at the top of its segment: the segment's
height is what the kind ate, but its position is an accident of what is stacked
beneath it. So a line can cross a segment it does not belong to — dashed and in
the kind's colour, with the figures named underneath either way.

One sentence per kind would grow with the list, so only kinds whose move beats
their own scatter get one and the rest fold into a clause. Both tests are ones
foodTrend already applied; nothing new is being claimed.

A guest labels a meal with a kind that exists but cannot add, rename or delete
one, exactly as with the exercise library.
2026-09-22 10:31:27 +00:00
Alexander Heldt 9e47aa53ff Stop the food trend's figures contradicting each other
The caption read like "down about 329 g a week — roughly 460 g a day then,
320 g a day now". Subtract the two amounts and you get 140 g, not 329. The
arithmetic behind it was self-consistent, but the sentence was not, and a
caption a reader can disprove by subtracting its own numbers is wrong whatever
the code was doing.

The rate was slope x 7, while the line only covers the complete days in the
window. Today is never fitted, being unfinished, so a 7-day window leaves at
most five days and any marked day takes another — in this case about three.
The rate was therefore stretched well past the days it was measured from, and
the two endpoints, which were not, could never agree with it.

It quotes the change between the two ends now, which is the one figure a reader
can check: "down about 140 g — from roughly 460 g a day to 320 g." The change
is derived from the rounded ends rather than from the slope, so the subtraction
works exactly rather than to within the rounding.

The weekly rate goes rather than being repaired. It cannot reconcile on a short
window, and it only ever meant anything where the fit spanned a week or more —
which is not something to leave as a trap for whichever window the reader
happens to have picked.

The check that let this through asserted the sentence contained certain
phrases, not that its numbers agreed with each other. There is now one that
parses all three figures back out and asserts the move is exactly the
difference of the ends, across each window length; it fails against the old
wording, which is the only evidence worth having that it would have caught this.
2026-09-21 21:01:27 +00:00
Alexander Heldt 7451650b6f Give the food trend's figures, not only its rate
"Up about 40 g a week" is a rate with nothing to anchor it: it says the line
slopes without saying where it sits. The sentence now names both ends of the
fit — "roughly 280 g a day then, 400 g a day now" — which is the reading anyone
actually wants from a growth chart.

When the fit is not trustworthy it quotes the average instead, and says the
day-to-day variation is larger than any trend. That difference is the point.
The average is a measurement and survives the noise; the ends of the line are
the line's own output, and quoting them on a fit nobody should read would dress
a guess up as a reading. Everything rounds to 10 g for the same reason — "287 g
a day" would be false precision from four noisy points.

The wording moves into a pure foodTrendSentence() so those rules can be
checked, which is worth doing precisely because they are judgement rather than
arithmetic: the checks now pin that a clear climb gives both figures, a flat run
gives the average and no endpoints, a see-saw gives neither, and nothing is ever
quoted to the gram.
2026-09-21 20:43:50 +00:00
Alexander Heldt 668f1f039e Draw a trend line through the food bars
The daily grams bars bounce around enough to hide a steady climb, so they
cannot answer the question you actually have about a growing puppy: is he
eating more than he was? A least-squares fit through them can.

Two kinds of day stay out of the fit. Today is half-eaten, and including it
would pull the line down every morning and let it drift back up as meals go in
— a line that tracks the clock rather than the dog. A day marked "not counted"
has a hatch rather than a figure, and fitting a zero there would invent a dip.
The line is drawn only across the days it was fitted on, so it never implies it
knows about the ones it skipped.

The caption is the part that needed the care. A straight line through seven
noisy points will always have a slope, and announcing it as a fact is the same
mistake the walking goal made. So a direction is named only when the fitted
climb is larger than the scatter of the days around it, and only when it clears
5 g a week and a twentieth of a typical day; otherwise it says the variation is
larger than any trend, which over a short window is usually the truth.

It names its window too — "over the last 14 days" — because the 7/14/30 picker
already drove this (weeklyData builds the array the fit runs on) but nothing on
screen said so, and the line moves too little between windows to show it. When
there are fewer than four complete days it now says why there is no line rather
than leaving bars with nothing through them.

Meals can be logged without an amount, so the note counts them: a day can read
low because he ate little or because nobody typed the number, and the chart
should not let those look the same.

The checks cover the refusals rather than the arithmetic — a see-saw is not
reported as a trend, a slope under the scatter is not either, three days will
not fit, a marked day does not shift the line, and each window length reaches
the fit intact.
2026-09-21 20:16:10 +00:00