All notable changes to KitchenMetrics. The application source is private; this file records what changed and, where it matters, why — because most of the entries below are corrections, and a correction without its reason teaches nobody anything.
Dates are the date of the work, not of the Play rollout.
A document registry ships with the app. Thirty entries, one per document the app
leans on, each carrying what it is, where a reader can obtain it, what claim in the app
rests on it — and, separately, two things that are usually conflated. tier says whether
a document is primary (the enacted text, the paper reporting the measurements) or
secondary (a handbook, an encyclopedia entry, a review). status says how far we
actually got: read at first hand, seen only in part, or not obtained at all.
Twenty-three read, three partial, four never obtained.
Listing sources on that second axis made something visible that counting them never would: in three cases the primary source is not obtained while the secondary one that reports it is read.
| primary source | reaches us through | what rests on it |
|---|---|---|
| Choi & Okos (1986) | ASHRAE Refrigeration ch. 9 | the cooking engine’s 38 coefficients |
| Cleland et al. (1987a) | ASHRAE Refrigeration ch. 20 | the freezing engine’s shape factor |
| Hossain, Cleland & Cleland (1992) | Cleland (2011) | the shape factor recommended instead |
Both engines take their core numbers from a handbook that reports them. This is not a defect in the numbers — ASHRAE is a serious body and verifying 38 coefficients against its tabulation is real work. It is a difference in what may be said: “matches the ASHRAE tabulation” and “matches Choi & Okos” are not the same claim, and only the first is ours to make. The registry now says which is which, on every row.
One row of ASHRAE Table 3 does not reproduce, and it has been reported. While checking what the app inherits from that chapter, the “Specific Heat Above Freezing” column was recomputed for 28 foods using the chapter’s own Equation (7) and the constituent coefficients from its Tables 1 and 2. Twenty-seven come back within 1.1 %, which is what rounding to two decimals costs — including the chapter’s own worked example. Chicken does not: published at 1.04 Btu/(lb·°F), and 4.34 kJ/(kg·K) in the 2014 SI edition where liquid water is 4.18, against 0.821 recomputed — a 21 % difference, twenty times the largest disagreement among the rows that do reproduce. Equation (7) is a mass-weighted average of constituents, none of which has a specific heat above water’s, so a food cannot exceed water; within Table 3 itself the most watery item, club soda at 99.90 % water, is printed below the chicken. Reported to ASHRAE on 31 July 2026 with a self-contained script they can run.
The app is not affected. The error is in the tabulated column, not in the Tables 1 and 2 coefficients the engine uses, and this app recomputes properties from composition at run time rather than storing tabulated values — which is the only reason it was visible at all. That is a position, not a merit. The registry entry for that chapter now carries the caveat, because the registry vouches for the document.
Test suite: 36 suites, 12,703 assertions, zero failures, regenerated from the runner.
The freezing panel’s error bar doubled, from ±10 % to ±20 %. It had shown ±10 %, described to the user as “the accuracy the method’s authors claim”. Cleland, D.J. (2011), Freezing: Time Calculations, Encyclopedia of Agricultural, Food and Biological Engineering, 2nd ed., Taylor & Francis, pp. 635–639, DOI 10.1081/E-EAFE2-120045855, read in full, gives four figures on p. 638 that are not interchangeable: maximum differences up to ±30 %, 95 % confidence limits of ±15–20 %, an inherent uncertainty under ±10 % — but only “when it is used within the range of conditions for which it was developed” — and, for practice, estimates that “should be treated as being accurate to within ±20 % at best”, because product characteristics and freezing conditions are seldom known accurately or tightly controlled. The app cannot establish that a user is inside the development range: the heat transfer coefficient is estimated, composition is a database row, and the initial freezing point is often swept rather than measured. It had been quoting the best case as the general one. Widening a band is the cheap direction to be wrong in.
The fat database stopped calling an attribution a citation. All 148 rows carry a
src field, printed on screen under the word “Source:”. Counted: 40 distinct strings,
exactly one carrying a year. The rest name a journal or an organisation — no author, no
volume, no page, no DOI — so no reader can reach a document. The file also contains
evidence against itself: the same publication written two ways, Food Chemistry on 7
rows beside Food Chemistry Journal on 14, and two more pairs like it, which is what
happens when a name is recalled rather than copied from a document in hand. This says
nothing about whether the numbers are right; most are probably fine. It says they are not
checkable, and the label promised they were. They now read “Attributed to:” with a
sentence stating that the app cannot put the document in front of you. The strings are
kept — a lead to check beats a blank.
A stylesheet had been rewriting the SI. text-transform: uppercase on the label rules
turned (g), (kW), (min), (cm), (h) into (G), (KW), (MIN), (CM), (H) on
screen — 37 labels, on the first screen a cook sees. G is not the gram, KW is not a unit,
and in the SI the case of a prefix is exactly what separates milli from mega. Found from
a screenshot rather than from a test, so there is now a test, pinned to the two
declarations that carry units. It was verified to fail against the pre-fix stylesheet
before being trusted.
Also disclosed rather than tidied away: the crystallisation column holds a temperature on 135 rows and a crystal habit on the other 13, under one label; and a note attributing to the CREA 2017 dossier the claim that TPC formation tracks unsaturation was corrected — the source says oxidation does, and TPC also counts hydrolysis products.
Test suite: 36 suites, 12,694 assertions, zero failures (31 July 2026), regenerated from the runner. A new suite checks that the source archive’s manifest agrees with the folder it indexes — its stated total had read “17” for ten days while the table listed 19.
The Italian 25% frying-oil figure is not a law, and now we can prove it in public. Circolare del Ministero della Sanità 11 gennaio 1991 n. 1 has been read in full and archived. Its operative sentence says the Istituto Superiore di Sanità “ritiene … sia opportuno” that polar compounds not exceed 25 g/100 g — an opinion, relayed by the Ministry, adopted “in analogia a quanto previsto dalla legislazione di altri Paesi”. The Circular also describes a draft bill under which the binding limit was to be set by a later ministerial decree it does not contain. Italy moves from disputed to no statutory limit, benchmark 25% — the same shape as the UK and Ireland. The discard point does not move. Every fat in the registry carries 25%, so an Italian kitchen is still told to discard at 25 — from the fat’s own chemistry rather than from a law that turned out not to exist. A demotion in status must never loosen a limit in a kitchen, and a test now asserts exactly that.
Allergen names now follow Reg. (EU) 1169/2011 Annex II, which had never been opened. The fryer panel has cited that regulation for months. Reading it showed the names did not match: “Wheat” is not a category — entry 1 is “cereals containing gluten, namely: wheat, rye, barley, oats, spelt, kamut”, and naming the example instead of the category under-declares for anyone avoiding rye or barley. “Tree Nuts” is US FALCPA wording; one row read “Tree Nuts (FDA)”, a US agency named inside a field presented under EU law. 42 terms normalised. Still open, and running the other way: celery and sulphites are declared by no food in either database, which is not asserted to be correct.
One fat contradicted its own composition. Refined Camelina Oil carried risk: "low"
at 64% polyunsaturates. Of the 49 fats above 55% PUFA, forty are “high” and eight
“medium”; camelina was alone at “low”, with no note claiming an exception. Polyunsaturation
is what makes a fat oxidise. Corrected, and pinned three ways.
The banded oil-degradation rate is coarse but not arbitrary — measured against our own registry, it tracks polyunsaturation with a rank correlation of τ ≈ 0.70, which is the direction the CREA 2017 scientific dossier gives. That is a materially better thing to say than “banded”, and it is what the file now says.
Checked and NOT changed: four Italian trade articles announcing “new EU rules” on acrylamide all describe Reg. (EU) 2017/2158 itself. No maximum levels exist. The claim stands — but it now carries the date it was checked and an admission that it rests on trade press rather than on the consolidated text of Reg. (EU) 2023/915, which remains unopened.
Not read, and listed as such: Codex CXC 67-2009 (HTTP 403 through the FAO proxy), and Polley, Snyder & Kotnour, A Compilation of Thermal Properties of Foods, Food Technology 34(11):76–94 (1980) — the cross-check we want for the Choi–Okos properties, consultable “in the library of the IIR headquarters only”. The debt is no longer vague: it has a citation and a page range, so it can be requested rather than wished for.
Everything in this section is committed but not yet built into a release. Version 1.0 (versionCode 4) was approved and distributed to closed testing on 18 July 2026 and is frozen; the changes below will ship in the next build, versionCode 5.
⚠️ This file has a gap. Work done on 18 and 19 July — the radiative term in the thermal engine, the acrylamide cap, the FDA Food Code cooking temperatures, the per-jurisdiction temperature registry, the Choi–Okos verification against the ASHRAE Handbook, and the demotion of Italy’s 25% TPC figure from statutory to disputed — is recorded in the private repository and in
SOURCES.md, but was never written up here. It is listed as missing rather than backfilled from memory: an entry written days later, from recollection, is exactly the kind of claim the rest of this file exists to avoid.
The food can now be a three-dimensional body. The engine offered slab, cylinder and sphere, and the first two are infinite in the textbook sense: an infinite slab loses heat through one pair of faces, an infinite cylinder only radially. A joint of meat is neither, so modelling it flat over-predicts the cook. Two shapes were added — Block (3D) and Finite Cylinder (3D) — using the classical product solution (Newman’s rule): the centre solution of a finite body is the product of the 1-D solutions whose intersection forms it. For a 10 cm cube in a 180 °C oven to a 72 °C core the difference is 145.0 min as an infinite slab against 75.2 min as a real cube. Dimensions may be left blank and are completed from mass ÷ density. The code had been contradicting itself here: with no dimension typed, the fallback was documented as “treat the volume as a cube” and then solved as an infinite slab.
⚠️ These shapes return SHORTER times, which is the correct direction and also the unsafe one. So the three original geometries are unchanged, the conservative slab remains the default, and the panel states that a shorter time is only right if the food really has that shape — a block is a terrine or a tray bake, not a joint that is thick at one end.
Fo > 0.2; a very long member has Fo ≈ 0, far outside that, and there the
truncated series tends not to θ = 1 as the physics requires but to A₁ — up to 1.27 for a slab.
That surplus multiplied into the product and slowed the whole solve. Left in, the new model
would have been worse than the one it replaced, and wrong in the direction that overcooks.
Fixed with the physical bound θ ≤ 1: a member whose one-term θ comes out ≥ 1 is out of domain,
not slow, so it is dropped and the product re-solved. A long loin now collapses onto exactly the
infinite cylinder.Two counts the app printed were contradicted by its own database. The Thermal panel’s “Documents, read at source” block — the app’s credibility statement — claimed “FDA Food Code 2022 — 258 foods” and “791 foods have no rule behind them”. The database held 315 and 782. Both had drifted as rows were re-sourced. The four figures are now derived from the data and compared by the test suite rather than typed by hand.
The fryer invented a degradation rate for 13 fats. Butter, lard, beef tallow, duck fat, ghee and eight more carry no rate at all, and the code substituted 1.5 %/h — a vegetable-oil figure — then printed a batch increase and an oil life in the same typeface as a sourced number. Both derived figures are now withheld for those fats, with the reason on screen. The discard limit is known for them and is unchanged.
And the rate is banded even where it exists. Found by a tripwire looking for the old fallback: 135 fats share eight round values. Nobody has published a degradation rate for refined papaya seed oil or bear fat. The values are not changed — there is no better source to change them to — but the panel now says the figures are an order of magnitude from a banded input, and the registry header says it too. It had disclosed exactly this defect for the limit field while staying silent about the rate field, which is the one driving the advice.
A green verdict beside a red one. When a fat cannot reach the temperature a food needs, the recommendation is clamped to the fat’s ceiling — usually above 160 °C — so the panel printed “✅ 180 °C appropriate for chips” directly above “🚨 DANGER: this fat cannot safely reach the temperature this food needs”.
A false attribution, withdrawn. The fryer food database’s only comment read “FDA/USDA/ISS/EFSA EXPANDED DATABASE”. None of those bodies publishes frying base times, bulk densities or oil-absorption coefficients. The attribution is gone and the file now states, field by field, that only the core temperature has any external backing.
100 rows removed from that database (332 → 232): plated dishes and foods whose own name states a method that is not frying — Eggs Benedict, Crème Brûlée, Sushi Roll, Cheese Board, Tahini Sauce, Ceviche, Miso Soup, Halibut Steamed, Pork Chops Roasted. Done by hand, because a keyword filter flagged Breaded Vegetables, Water Chestnuts and Pasta (Cooked), all three of which are legitimately fried.
Four ways the costing engines returned a wrong number in silence: a sub-recipe with no
finished yield was costed as if it made 1000 g (a 200 g sauce priced at a fifth of its
cost, in every dish using it); a butchering yield above 100 % was accepted, pricing the
edible portion below the as-purchased price; cooking loss was silently clamped at 90 %;
and monthly fixed costs with no covers-per-month silently became zero. All four were a
|| default or a Math.min deciding on the user’s behalf.
The entry above about the fryer database was not published with this line, and it should have been: an earlier draft of the source file claimed that 44 absorption notes explained oil uptake by the food’s water content, and therefore contradicted the CREA dossier’s finding that microstructure governs uptake. That count came from a keyword search, not from reading the notes, and it was wrong. Read properly: 17 of them say water content and then “significant weight loss during frying”, which is correct — the water does leave and the mass does drop. 21 say “moderate frying behaviour”, vague but claiming nothing about uptake. Three describe tofu. One correctly says a chicken thigh holds more moisture than a breast and so takes longer. Exactly one note tied water content to absorption — mushrooms — and that one is corrected. The other 43 are left alone, because correcting text that is not wrong is how a database gains errors rather than loses them. Recorded rather than quietly edited: a project that publishes its corrections has to publish the corrections to its corrections, or the practice is decoration.
The acrylamide advice was Italian in every language. The nine Annex II Part A measures had their plain-language instruction written in Italian in the source, and it is rendered through a translator that translates from English — so it stayed Italian with the interface set to English. A second error in the same block dropped the regulation’s closing clause “taking into account the food safety requirements” from the instruction a cook reads, turning a qualified rule into an absolute one.
The time–temperature paradigm as a module. Every temperature field in the app is a single number, and a single number cannot express food safety: safety is a pair. D(T) = D_ref · 10^((T_ref−T)/z), the hold for n decimal reductions, and the equivalence t_low = t_high · 10^((T_high−T_low)/z). With z = 6 °C, giving up 6 °C costs ten times the time. It reproduces no authority’s time/temperature table, and refuses outside the model’s domain rather than estimating.
Doses per food in the fryer, portion size × portions, replacing one global quantity that could not describe 4 × 200 g of chips beside 2 × 150 g of squid — and that silently attributed the whole load to whichever food was added first.
Cycles per session, and with them the answer to “the batch you just ran consumes no oil life”: the panel now reports the TPC a whole service adds, where that leaves the oil, and how many cycles remain before the discard limit — floored, because a cycle that would cross the limit is not a cycle you may run.
Two foods, one fryer, two temperatures. Chips capped at 175 °C for acrylamide beside chicken wanting 182 °C cannot both be fried correctly at one setting. The panel says so.
The HACCP guidance box was one paragraph of up to six findings. It is now one row per finding, sorted by severity, worst first.
thermal_geometry 102 → 247 assertions; the original 102 pass unchanged, which is what
establishes that nothing already shipped moved. Seventeen suites, 7031 assertions, green.Germany’s 24% is no longer called a limit. It was the only country figure in the app quoted without a status and without a legal basis, while all twelve registry entries carry both — and the fryer legend called it “the strictest known limit anywhere”. The figure originates from a DGF recommendation (Deutsche Gesellschaft für Fettwissenschaft, 3rd International Symposium on Frying Fats, 2000), used by German Länder inspectors as one assessment criterion among several, not as a statute. The legend now reads “strictest known figure anywhere: Germany, 24% (DGF recommendation)” — true whether or not German law also adopts it. A clarification has been requested from the DGF; this entry will be updated with their answer. This is the same failure shape as the “EFSA/FDA threshold” removed in July 2025: a plausible number attributed to an authority that never set it. It was found in five places outside the fryer code, including the Play listing copy.
“Discard oil when TPC exceeds X%” → “Discard oil at or above X% TPC”. Spain requires the polar content to be “inferior al 25 por 100” (art. 6.3) and Argentina condemns oil at a content “igual o superior al 25 %” (art. 552 bis). 25.0% is therefore already non-compliant, not the last acceptable value. The arithmetic had always treated the threshold as inclusive; only the sentence a cook reads was a hair too generous.
An absence can no longer be marked “official”. The registry’s confidence field — the one
that records whether anyone actually checked a claim — defined official as “checked against
the primary legal text”. The United States sat at official for the claim that no such text
exists. You cannot read a text that does not exist: nothing had been read, because there was
nothing to read. It is now literature, and the basis says what is true — “no federal TPC
limit found“, rather than asserting the FDA has set none.
The taxonomy now states the distinction plainly: a positive claim is established by reading
a text; an absence must be attested by the body itself. There is deliberately no confidence
value for an attested absence yet — if FSANZ or the DGF answer with one, it will be added as a
deliberate act with the reply archived, rather than by stretching official over it again.
confidence was also the only field in the registry with no test: 35 assertions were added,
and mutation-checked by reintroducing the bug to confirm they catch it.
Spain’s registry note was backwards and is corrected. It claimed the law “bans selling food that touched oil at 25% TPC or above — the offence is the dish, not the fryer”. The primary text says the opposite: art. 3 binds whoever uses and handles heated frying oils, art. 6.3 requires the oil in use to be below 25%, and art. 9.2 prohibits commercialising the used oil. The offence is the fryer.
Geometry selector in the Thermal Calculator — slab, cylinder or sphere. A whole chicken is
not an infinite slab. The eigenvalue λ₁, the coefficient A₁ and the characteristic length now
follow the chosen shape (λ tan λ = Bi · λ J₁/J₀ = Bi · 1 − λ cot λ = Bi), with Bessel
functions from Abramowitz & Stegun 9.4.1/9.4.4. All twelve λ₁/A₁ pairs were verified against
Cengel’s published one-term table at Bi = 0.5, 1, 5 and 100 to better than 5·10⁻⁴.
The slab remains the default and is unchanged: at equal size it is the slowest to heat, so it is
the conservative choice.
The fryer now says when the fat itself is the problem. If a fat cannot safely reach the temperature a food needs, the app used to lower the recommendation silently and still print “conform to HACCP guidelines”; the red warning tested a condition the clamp made arithmetically impossible. It now names the cause: this fat cannot safely reach the temperature this food needs — choose a fat with a higher smoke point. (Re-checked afterwards: still 0 hits across all 49,284 oil × food combinations, so nothing changes for users today. The branch is now silent because the data is good, not because no data could ever reach it.)
Legal citations down to the article. Switzerland: ODOV/VLpH art. 6 para. 4 (RS 817.022.17) — previously just “Swiss federal food legislation (FDHA)”, which named the department and not the act. Chile: RSA art. 266 let. c (D.S. 977/96). Spain: art. 6.3 (BOE-A-1989-2265). Each was read in the primary text, not taken from a review paper.
The thermal engine had no tests at all. It is the part of the app that tells a cook when
chicken is safe, and it was the only area without a suite. thermal_geometry now pins the
eigenvalues, the A₁ coefficients and the Bessel functions with 102 assertions against
published tables — Cengel’s one-term approximation (12 λ₁/A₁ pairs at Bi = 0.5/1/5/100 across
all three geometries) and Abramowitz & Stegun 9.4.1/9.4.4 for J₀/J₁. The references are
deliberately external: a suite that pins yesterday’s numbers only proves the code still does
what it did.
It also pins the app’s worst historical bug as what it actually was — A1 = 4/π and λ1 = π/2
are asserted to be the Bi → ∞ limit of the correct solution (a surface reaching oven
temperature instantly), not constants wrong in themselves — and asserts that a real fan oven
(Bi ≈ 0.69) sits nowhere near that limit, which is why the fix mattered.
Plus invariants no table covers: λ₁ monotonic in Bi and never reaching its ceiling, A₁ positive
and bounded, and the ordering slab > cylinder > sphere at equal Bi — which is why the slab
default is the conservative one. If that ever inverts, the default has stopped being safe.
confidence — the field recording whether anyone actually checked a claim — had no
assertions either, and had rotted exactly where you would expect (see the USA entry above).
35 new assertions (138 → 173): every entry carries a known confidence; an official statute
must cite an article, so a basis naming only the act cannot pass as a citation; no absence and
no disputed entry may claim official.
Both suites were mutation-checked, because a green suite can be a vacuous one. Re-introducing
the USA bug produces FAIL no absence claims 'official': US. A 0.25% drift in the sphere’s A₁
produces 5 failures — including the A1 ≤ 2 invariant, which the table check alone would have
missed. One wrong Bessel coefficient produces 11, cascading into the cylinder’s eigenvalue
because it solves J₁/J₀.
Suites now: cost_engine 31 · smoke 64 · i18n 1852 · db_i18n 920 · tpc_limits 173 · vault 15 ·
thermal_geometry 102. Still untested: the thermal engine’s DOM-coupled half (Choi–Okos
properties, results panel).
The costing model is European; the legal registry is not. The app now cites the frying-oil
law of twelve countries to the article — and prints € for all of them. Nine of the twelve
do not use the euro, and the Spanish and English interfaces serve only non-euro countries
besides Spain. Nothing is miscalculated (the arithmetic is currency-agnostic; only the label is
wrong), but it is recorded here rather than left to be found. Same for the 10% VAT default
(Italian, and coincidentally Spanish) and for “menu price incl. VAT”, which has no meaning in the
United States. See SOURCES.md §4.
The design was decided before writing any code, and the rejected option is worth recording:
using the currency symbol as the selector that drives the applicable law was proposed and
turned down, because currency does not identify jurisdiction. The euro alone spans Italy (25%),
Germany (24% — and a DGF recommendation, not an established statute) and Ireland (no statutory
limit at all); the $ sign spans seven countries in this registry whose rules range from 25% to
none to disputed. It would have inferred the specific from the generic — and it would have worked
perfectly in Italy, which is where it would have been tested.
The country will be the selector and the currency will follow from it. Amounts will never be
converted, only relabelled: conversion needs exchange rates → the network → the end of the
zero-permission guarantee.
com.kitchenmetrics.app · versionCode 3 · targetSdk 36 · minSdk 26 · ~2.4 MB AAB
The first public build, submitted for review on 15 July 2026. Two and a half months earlier this was a Python exercise written while learning to program.
The app requests zero Android permissions — not even INTERNET, so the operating system itself
prevents it from sending anything off the device. No account, no ads, no analytics. An earlier USDA
nutrition lookup was removed after concluding it could neither scale (a per-key rate limit shared by
every install) nor keep its API key secret client-side.
Runtime translation including the 148-fat database. Food names are deliberately not machine-translated: the allergen field hangs off the name, so a mistranslation would be a safety bug, not a typo. Translated fat labels keep the English name alongside — “Strutto (Lard)” — for the same reason.