diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..9e314a0 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,35 @@ + + +## What changes + + + +## Spec and Tickets + + + +## Checks + +- **Mock-data check** (Controlled server): +- **Real-data check** (Workstation): +- **Quality goals checked**: + + + + +## Definition of Done + +- [ ] Tests pass +- [ ] `code-review` findings are handled (fixed, or answered in this PR) +- [ ] Mock-data check done +- [ ] Real-data check done, or marked not needed above +- [ ] No secrets or Real data committed +- [ ] Glossary, ADRs and Vision document updated if this Change affects them +- [ ] Relevant Quality goals checked +- [ ] I can explain this Change in my own words diff --git a/CLAUDE.md b/CLAUDE.md index c8a66b3..ccff4ba 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -18,3 +18,10 @@ sudo controlled-dev-git push # first push of a new branch, sets the u Run these from inside this project workspace. `git commit`, `git switch`, `git status`, `git log`, and other purely local commands are unaffected and need no special handling. + +## Failure work stays out of `main` until the release + +Pull requests for the Changes in `docs/failure_roadmap.md` (C1–C5) stay open until the +release: keep each one up to date with `main` and test it on the dev environment. Every +merge into `main` deploys straight to production, so merge them only when the user starts +the release (roadmap decision D5). diff --git a/CONTEXT.md b/CONTEXT.md new file mode 100644 index 0000000..f44d7be --- /dev/null +++ b/CONTEXT.md @@ -0,0 +1,192 @@ +# QMRA Web Application + +A web tool for quantitative microbial risk assessment of water: a user describes a source water, a treatment train and an exposure, and the tool estimates the yearly infection risk and health burden for three reference pathogens. Bounded to steady-state scenarios; process design is out of scope, and treatment failure is next phase. + +## Language + +### Assessment + +**Assessment**: +A scenario together with the results of its risk calculation. Scenarios are compared by comparing assessments. +_Avoid_: risk assessment (in prose), calculation, run, project + +**Scenario**: +The inputs of a risk calculation: inflow concentrations, a treatment train and an exposure. The failure frequency and failure duration of each treatment step belong to the scenario, so scenarios can differ only in their failures. Each scenario is calculated in two cases, best-case and worst-case. +_Avoid_: case (reserved for best-case and worst-case), configuration + +**Saved assessment**: +An assessment stored under a registered user's account so it can be listed, reopened, edited, deleted or exported. + +**Guest assessment**: +An assessment run without an account. Its results are shown once and nothing is stored. +_Avoid_: free trial, anonymous assessment + +**Assessment configurator**: +The form-based interface for entering or selecting assessment inputs. +_Avoid_: wizard, editor + +**Export package**: +A ZIP containing the assessment's input tables, result table, HTML report and plots. +_Avoid_: download, report (the report is one file inside the package) + +### Inputs + +**Source water**: +A named type of water before treatment, such as raw sewage or protected surface water. A source water is described by one inflow concentration per reference pathogen. +_Avoid_: source, water type, feed water + +**Site-specific source water**: +A source water in an assessment whose inflow concentrations were entered or changed by hand, even a single value, or entered without selecting any source water. It is not a bundled source water any more, even if it was derived from one, and is not called by the bundled name. +_Avoid_: custom source water, modified source water, calling it by the unprefixed bundled name + +**Inflow concentration**: +The concentration of one reference pathogen in the source water entering the treatment train, in organisms per litre, given as a minimum and a maximum. An assessment has one inflow concentration per selected pathogen. +_Avoid_: inflow (alone), concentration (alone), concentration range, pathogen input, source concentration + +**Outflow concentration**: +The concentration of one reference pathogen leaving the treatment train: the inflow concentration reduced by the train's LRV for the pathogen's group. It is calculated, never entered, and differs between best-case and worst-case. It determines the dose ingested per exposure event. +_Avoid_: outflow (alone), treated concentration, effluent concentration, dose + +**Exposure**: +The assumed human contact with the treated water, given as events per year and ingested volume per event in litres. +_Avoid_: exposure scenario, ingestion, use + +**Treatment step**: +Any step between source water and exposure that changes the pathogen concentration, with a minimum and maximum log-removal value per pathogen group. It can be a treatment technology or process, a non-technical measure such as a hygiene practice, or recontamination. +_Avoid_: treatment (alone, when a step is meant), barrier, process, technology + +**Recontamination**: +A treatment step with a negative LRV, representing pathogens entering the water after treatment, for example in storage or distribution. +_Avoid_: negative treatment, contamination step + +**Treatment train**: +The ordered sequence of treatment steps in a scenario. Its removal is the sum of the steps' LRVs per pathogen group. +_Avoid_: treatment chain, process chain, treatment list + +**Log-removal value (LRV)**: +A logarithmic measure of pathogen reduction by a treatment step. Each step carries a minimum and a maximum LRV for bacteria, viruses and protozoa. +_Avoid_: log reduction, removal efficiency, log credit + +**Reference pathogen**: +One of the three pathogens the model calculates risk for: Rotavirus, Campylobacter jejuni and Cryptosporidium parvum. Each stands for its pathogen group. +_Avoid_: organism, indicator, microbe + +**Pathogen group**: +One of bacteria, viruses or protozoa. LRVs are defined per group; each reference pathogen belongs to exactly one group. +_Avoid_: pathogen class, pathogen type + +### Data origin + +**Bundled default data**: +Source waters, inflow concentrations, treatment steps, exposures, pathogens and references shipped with the application. +_Avoid_: defaults, static data, system data, library + +**Personal definition**: +A reusable source water, exposure or treatment step created by a registered user and offered alongside the bundled defaults. A personal treatment step can carry its own failure frequency and failure duration. +_Avoid_: custom entry, user data, own data + +**Site-specific input**: +A value typed in for one assessment to represent local evidence. The application does not check that it is representative. +_Avoid_: custom value, manual value, override + +**Reference**: +A literature citation attached to a bundled inflow concentration, exposure or LRV to show where the value came from. +_Avoid_: source (collides with source water), citation, literature + +**Provenance**: +The full record of where every input, parameter and interpretation in an assessment came from. Only partly captured today through references. + +### Calculation + +**Dose-response model**: +The relationship between the ingested dose of a reference pathogen and the probability of infection per event. Each reference pathogen uses one fitted model, exponential or beta-Poisson. +_Avoid_: infection model, dose model + +**Monte Carlo simulation**: +Repeated random sampling of inflow concentrations and exposure events to estimate a distribution of yearly risk instead of one number. +_Avoid_: stochastic run, sampling + +**Best-case**: +The case of a scenario that gives the lower infection-risk estimate. For normal operation it uses every treatment step's maximum LRV. On failure days it applies the mixed-water assumption. +_Avoid_: maximum-LRV case, minimum risk scenario, optimistic case + +**Worst-case**: +The case of a scenario that gives the higher infection-risk estimate. For normal operation it uses every treatment step's minimum LRV. On failure days it assumes all consumed water was treated during the failure event. +_Avoid_: minimum-LRV case, maximum risk scenario, conservative case + +**Annual probability of infection**: +The estimated probability that a person is infected at least once in one year of exposure. +_Avoid_: infection risk (alone), yearly risk + +**DALYs per person per year**: +The estimated health burden in disability-adjusted life years per person per year, derived from the annual probability of infection through the pathogen's illness-to-infection ratio and burden per case. +_Avoid_: DALY, burden, health impact + +**Risk measure**: +One of the two outcomes the application reports per reference pathogen: annual probability of infection or DALYs per person per year. +_Avoid_: metric, indicator, risk type + +**Risk distribution**: +The set of sampled yearly outcomes for one reference pathogen in best-case or worst-case, summarised by minimum, quartiles, median and maximum. +_Avoid_: result set, spread + +**Health-based reference level**: +A comparison value shown next to results: one infection per ten thousand persons per year and one millionth DALY per person per year. It is not a legal limit. +_Avoid_: legal limit, tolerable risk level, guideline value, threshold + +**Reference-level exceedance**: +The verdict for one reference pathogen and one risk measure: whether the mean result exceeds the health-based reference level in both best-case and worst-case, only in worst-case, or in neither. +_Avoid_: risk category, risk flag, pass/fail + +**Uncertainty**: +Variation or lack of knowledge in an assessment. Represented today only through inflow concentration ranges, random sampling and the spread between best-case and worst-case. + +### Boundaries + +**Steady-state scenario**: +A scenario whose inputs are assumed to be the same every year: source water, exposure, treatment performance and, once modelled, failure frequency and failure duration. Results are given per year, never per day, so a scenario with treatment failures is still steady-state. This is the only kind the application models. + +**Normal operation**: +Every treatment step performing within its configured minimum and maximum LRVs. The only state the application models today. +_Avoid_: regular operation, steady operation + +**Treatment failure**: +A treatment step losing all its positive LRVs for a period, so that it removes nothing while it lasts. LRVs of zero or below are not affected: a step with regrowth for one pathogen group keeps that negative LRV during a failure. Any treatment step with at least one positive LRV can fail, including non-technical measures; recontamination, having no positive LRV, cannot. A step fails only if it has a failure frequency above zero. Partial loss of removal is not modelled and not planned. Not modelled today; a next-phase concept. +_Avoid_: damage, incident, outage, malfunction, downtime (alone) + +**Failure event**: +One occurrence of a treatment failure in one treatment step. A step has at most one failure event per day. +_Avoid_: damage event, incident + +**Failure day**: +A day on which at least one treatment step has a failure event. Exposure events on a failure day are calculated with the reduced LRV. +_Avoid_: damage day, incident day + +**Failure frequency**: +The expected number of failure days per year for one treatment step. It is zero unless the user enters it. Which days fail is drawn at random, so a simulated year can have more or fewer failure days than expected. +_Avoid_: failure rate, failure probability + +**Failure duration**: +The average length of one failure event of one treatment step, in minutes within a day. It matters only under the mixed-water assumption. +_Avoid_: downtime, outage time + +**Mixed-water assumption**: +The best-case assumption that water consumed on a failure day is a mixture of water treated during the failure event and water treated in normal operation, in proportion to the failure duration. Worst-case instead assumes the consumed water was treated entirely during the failure event, regardless of its duration. +_Avoid_: mixed concentration, dilution, storage effect + +**Combined failure**: +Failure events in two or more treatment steps on the same day. Worst-case loses all their LRVs for the whole day. Under the mixed-water assumption the failure events are arranged within the day so that the mixed water has the lowest pathogen concentration. This means they do not overlap if their durations fit into one day, and otherwise overlap only by the minutes that exceed a day, shared by the steps whose joint loss matters least. + +**Next phase**: +Capabilities identified for future development and not present in the application today. +_Avoid_: roadmap, backlog, planned + +**Reproducible assessment snapshot**: +A saved assessment that records the model version, data release, configuration and simulation settings needed to reproduce its result exactly. Not stored today. + +**Historical assessment**: +A saved assessment created under an earlier model or data version. Its treatment is an open question. + +**Assessment comparison**: +Viewing the results of several saved assessments side by side. Today this is one plot of infection-risk results per reference pathogen; comparing inputs or DALYs is next phase. +_Avoid_: benchmark, diff diff --git a/docs/QMRA_Failure_Approach.docx b/docs/QMRA_Failure_Approach.docx new file mode 100644 index 0000000..534eb86 Binary files /dev/null and b/docs/QMRA_Failure_Approach.docx differ diff --git a/docs/QMRA_Failure_Approach.md b/docs/QMRA_Failure_Approach.md new file mode 100644 index 0000000..8dd2dec --- /dev/null +++ b/docs/QMRA_Failure_Approach.md @@ -0,0 +1,135 @@ +# Integration of Failure Events into the QMRA Framework + +## Current Calculation of the infection risk + +All equations in this chapter are taken from Hambsch et al., 2019, pp. 61. + +The annual risk of infection is described as the product of the daily risks of infection + +| Eq. 1 | ${P}_{inf-year}=1-\prod_{i=1}^{365} \left( 1-{p}_{inf-day,i} \right)$ | +|---|---| +| with | ${P}_{inf-year}$: Annual risk of infection
${p}_{inf-day,i}$: Daily risk of infection on day i | + +The daily risk of infection, again, depends on + +1. the inflow concentration +2. the log-removal value (LRV) of the entire treatment train +3. the specific probability of infection per pathogen: r +4. the volume ingested: V + +| Eq. 2 | ${p}_{inf-day}=1-{e}^{-\frac{r{c}_{in}V}{{10}^{LRV}}}$ | +|---|---| +| with | ${p}_{inf-day}$: Daily risk of infection
$r$: Specific probability of infection [per pathogen]
${c}_{in}$: Inflow concentration [Pathogen per Liter]
$V$: Ingested volume [Liter]
$LRV$: Log-removal of the entire treatment train | + +Accordingly, the risk of infection is 0 if either the specific probability of infection per pathogen, the inflow concentration, or the volume ingested is equal to 0. + +Furthermore, the risk approaches 0 as the log-removal of the treatment train increases. + +In the QMRA tool, the inflow concentration for each exposure event is drawn from a distribution as part of a Monte Carlo simulation. The calculation is performed in two cases: best-case (maximum LRV per treatment step: LRV_max) and worst-case (minimum LRV per treatment step: LRV_min). + +The distribution of the infection risk is therefore based on the distribution of the inflow concentration and is calculated for each case using separate LRVs. + +## Calculation of LRVs with failure events + +The distinction between best-case and worst-case remains the same on failure days. + +The specific probability of infection, the inflow concentration, and the volume ingested do not change as a result of the failure. The LRV, however, is directly affected by the failure of a treatment step. Depending on the case, a distinction should also be made between whether the treated water was consumed exclusively during the failure event (worst-case) or whether it was a mixture of water from normal operation and the failure event (best-case, the mixed-water assumption)—as is likely often the case when water is stored somewhere. + +If a treatment step in the treatment train fails, the total LRV is reduced by the treatment-specific LRV. This is calculated for both cases. + +| Eq. 3 / Eq. 4 | $LRV_{failure,min}=LRV_{min}-LRV_{treatment,min}$
$LRV_{failure,max}=LRV_{max}-LRV_{treatment,max}$ | +|---|---| +| with | $LRV_{failure,min}\ \text{and}\ LRV_{failure,max}$: Minimum and maximum log-removal of the entire treatment train during the failure event
$LRV_{treatment,min}\ \text{and}\ LRV_{treatment,max}$: Minimum and maximum log-removal of the failing treatment step | + +In worst-case, $LRV_{failure,min}$ is used directly to calculate the daily risk of infection on a failure day. In best-case, the mixed-water assumption gives an average LRV using the following equation: + +| Eq. 5 | $LRV_{failure,mix}=-\log \left( \frac{{x}_{fail}}{{10}^{LRV_{failure,max}}}+\frac{1-{x}_{fail}}{{10}^{LRV_{max}}} \right)$ | +|---|---| +| with | $LRV_{failure,mix}$: Average log-removal of the entire treatment train with failure event
${x}_{fail}$: Time portion of the failure event calculated as the duration of the failure in minutes divided by 1,440 min | + +For best-case, the failure duration is therefore required as an additional input variable. + +The daily infection risk is calculated using Equation 2, in the same way as for the calculation without failure. When calculating the annual infection risk, a distinction must then be made between days with and without failure. + +| Eq. 6 | ${P}_{inf-year}=1-{\left( 1-P_{inf-day,normal} \right)}^{365-n}{\left( 1-{P}_{inf-day,failure} \right)}^{n}$ | +|---|---| +| with | ${P}_{inf-year}$: Annual risk of infection
$P_{inf-day,normal}$: Daily risk of infection without failure
${P}_{inf-day,failure}$: Daily risk of infection with failure
$n$: Failure frequency in failure days per year | + +The day of the year on which the failure occurs or time of the failure within a given day is not relevant for the calculation. + +**Required information in the QMRA tool for integrating failure events, per treatment step:** + +- **Failure duration: average duration of one failure event in minutes (integers between 1 and 1440) Default 30 min** +- **Failure frequency: failure days per year (float between 0 and 365) Default is 0** + +## Next Step: Combined failures across multiple treatment steps + +A failure frequency is given for each treatment step. From this information, a sequence of length 365 consisting of zeros and ones can be randomly selected, where zeros represent days without a failure event and ones represent failure days. The draw is performed using a binomial distribution, in which the probability corresponds to the number of days per year (e.g., 1.37% for 5 days per year means that for each day, a 1 is drawn with a probability of 1.37%). This results in years in which there may be more or fewer failure events than assumed. The random draws become part of the Monte Carlo simulation and can thus be accounted for along with the varying inflow concentrations. + +This process is carried out for each treatment step, so it is possible that several treatment steps could fail at the same time, although this is rather unlikely given the expected low frequency of such occurrences. + +The following table shows an example of one week in a year. + +| | Failure (1: yes, 0: no) | | | LRV | +|---|---|---|---|---| +| Day | Tech1 | Tech2 | Tech3 | | +| 1 | 0 | 1 | 1 | Failure, Tech2-Tech3 | +| 2 | 0 | 0 | 0 | Normal | +| 3 | 0 | 0 | 0 | Normal | +| 4 | 1 | 0 | 0 | Failure, Tech1 | +| 5 | 0 | 0 | 0 | Normal | +| 6 | 0 | 0 | 0 | Normal | +| 7 | 0 | 0 | 1 | Failure, Tech3 | + +For each combination of failing treatment steps, an LRV must then be calculated for best-case and worst-case, and Equation 6 must be extended accordingly. + +## Decisions (2026-09-23) + +Agreed in a domain-modeling session. Terms follow `CONTEXT.md`; the calculation approach +is recorded in `docs/adr/0001-failure-days-drawn-per-exposure-event.md`. + +1. **Entire loss only.** A failing treatment step loses every positive LRV (above 0), for + all pathogen groups at once. LRVs of 0 or below are not affected by the failure. + Partial loss of removal is not modelled and not planned. +2. **Which steps can fail.** Any treatment step with at least one positive LRV, including + non-technical measures such as hygiene practices. Recontamination steps (no positive + LRV) cannot fail. (Refined 2026-09-24, roadmap decision D4.) A step fails only if its failure frequency is above zero. +3. **Inputs per treatment step.** Each step has one failure frequency (failure days per + year, 0–365, default 0) and one failure duration (minutes, 1–1440, default 30). They + are the same in best-case and worst-case. They are user input only; there is no + bundled failure data. They belong to the scenario, and a personal treatment step can + store them for reuse. +4. **Worst-case ignores the failure duration.** On a failure day, worst-case assumes all + consumed water was treated during the failure event, so a 1-minute and a 24-hour + failure give the same result. Only best-case uses the duration (Eq. 5, the + mixed-water assumption). +5. **Draws per exposure event, one calculation path.** The application builds yearly risk + from exposure events, not from 365 days. Each exposure event falls on a failure day of + each treatment step independently, with probability failure frequency / 365, inside + the existing Monte Carlo simulation. The section-3 approach is implemented directly; + Eq. 6 is not implemented as a separate path, not even for a single failing step. See + ADR-0001. +6. **Combined failures in best-case.** Eq. 5 is extended so that failure events of + different steps on the same day overlap as little as possible: not at all if their + durations fit into 1,440 minutes, otherwise only by the minutes beyond a full day. + Example (best-case LRV 10 = UV 4 + UF 4 + other steps 2): UV down 60 min and UF down + 120 min give 4.2 % of the day at LRV 6, 8.3 % at LRV 6 and 87.5 % at LRV 10, a mixed + LRV of about 6.9. UV and UF each down 900 min overlap by 360 min: 25 % at LRV 2 and + 2 × 37.5 % at LRV 6, a mixed LRV of about 2.6. Worst-case loses all failing steps' + LRVs for the whole day. With three or more failing steps, the unavoidable overlap is + placed on the steps whose joint loss gives the lowest mixed concentration; in general, + best-case arranges the failure events of a day so that the mixed water has the lowest + concentration, which always implies minimal overlap (decided 2026-09-24). +7. **Results.** Only the result including failures is shown, and the reference-level + exceedance is taken on it. The effect of failures is seen by comparing scenarios with + different failure inputs in the assessment comparison view. That view compares annual + probability of infection only. Until the extended assessment comparison (under "Waits + until later" in `docs/vision.md`) adds DALYs, the DALY effect of failures is read from each + assessment's own result page. A failure can exceed the DALY reference level without + exceeding the infection one, so this matters. +8. **Still steady-state.** Failure frequency and duration are the same every year and + results are yearly, so a scenario with failures remains a steady-state scenario. + +## Literatur + +Hambsch, Beate, Lipp, Pia, Ho, Johannes, 2019. Modelhafte Prüfung und Konzeptentwicklung zur quantitativen Risikobewertung der mikrobiellen Rohwasserbelastung für die Trinkwasseraufbereitung (Abschlussbericht), DVGW-Förderkennzeichen W 201808. Technologiezentrum Wasser, Karlsruhe. diff --git a/docs/adr/0001-failure-days-drawn-per-exposure-event.md b/docs/adr/0001-failure-days-drawn-per-exposure-event.md new file mode 100644 index 0000000..6694a74 --- /dev/null +++ b/docs/adr/0001-failure-days-drawn-per-exposure-event.md @@ -0,0 +1,63 @@ +# ADR-0001: Failure days are drawn per exposure event inside the Monte Carlo simulation + +- **Status:** Accepted +- **Date:** 2026-09-23 +- **Deciders:** QMRA project team (domain-modeling session on the failure approach) +- **Related:** `docs/QMRA_Failure_Approach.md` (Decisions section), `CONTEXT.md` (Treatment failure, Failure day, Failure frequency) +- **TL;DR:** When treatment failures are added, each exposure event is randomly assigned to a failure day or a normal day inside the existing simulation, instead of counting 365 days per year as the source method does. +- **Quality goals:** Scientific correctness — one calculation path for single and combined failures, consistent with how the existing model builds yearly risk, so benchmark scenarios check one method; Input validity — failure frequency stays in the unit operators know (days per year). + +## Context + +The failure approach (`docs/QMRA_Failure_Approach.md`, after Hambsch et al., 2019) builds +yearly infection risk from 365 daily risks. With one failing treatment step it uses a +closed formula over exactly *n* failure days (Eq. 6); for several failing steps it draws a +random 365-day sequence of failure days per step. + +The application does not work in days. It builds yearly risk from exposure events: it +samples the dose of each event and combines as many events as the exposure has per year. +For drinking water (365 events per year) days and events coincide; for an exposure such as +irrigation with 20 events per year they do not, and "5 failure days per year" has no +direct meaning for the calculation. + +## Decision + +Failure frequency is entered in failure days per year per treatment step. Inside the +Monte Carlo simulation, each exposure event falls on a failure day of each step +independently, with probability failure frequency / 365. Events on a failure day use the +LRV with the failing steps removed (worst-case) or the mixed-water LRV (best-case). There +is no separate deterministic calculation path based on Eq. 6, including for the case of a +single failing step. + +## Options considered + +| Option | Pros | Cons | +|--------|------|------| +| A (chosen): per-event draw with probability n/365 | Works for every exposure; one path for single and combined failures; fits the existing event-based simulation | Departs from the cited method; the number of failure events varies between simulated years | +| B: one exposure event per day, 365-day sequence as in the source | Matches the source method literally | Forces exposures to at most 365 events per year and changes how exposures are defined | +| C: deterministic share, events × n/365 failure events per year | Simple, no extra randomness | Fractional events for small exposures; a second path next to the combined-failure draw | +| D: Eq. 6 for one failing step, random draw only for combined failures | Closest to the source for the simple case | Two calculation paths that give different results for comparable inputs | + +## Consequences + +- Results with failures will not equal a hand calculation with Eq. 6. They agree on + average, but the number of failure events varies from simulated year to year, which + widens the risk distribution. This is intended: it reflects that failures are random. +- For exposures with few events per year and low failure frequencies, many simulated years + contain no failure event at all. The yearly risk distribution will show this as a long + upper tail rather than a shifted median. +- Two exposure events on the same calendar day are not forced to share the same failure + state. This is accepted, since the model has no notion of calendar days. +- Worst-case ignores the failure duration: a one-minute failure and a full-day failure + give the same worst-case result for that exposure event. The reasoning is that all water + consumed in one exposure event was treated during the downtime, which holds as long as + the volume treated during the downtime is much larger than the water demand of one + event. That is expected to be true in most cases. Only best-case, through the + mixed-water assumption, uses the duration. +- The effect of failures is evaluated by comparing assessments with different failure + inputs in the assessment comparison view, which today compares annual probability of + infection only. Until the extended saved-assessment comparison adds DALYs, the DALY + effect of failures has to be read from each assessment's own result page. The failure + feature therefore depends on that next-phase work for a full side-by-side evaluation. +- Anyone comparing the application against Hambsch et al. (2019) needs this ADR to explain + the difference. diff --git a/docs/architecture_baseline.md b/docs/architecture_baseline.md new file mode 100644 index 0000000..37195f8 --- /dev/null +++ b/docs/architecture_baseline.md @@ -0,0 +1,191 @@ +# QMRA application architecture baseline + +## Scope + +This is a concise description of the current architecture of [`KWB-R/qmra-webapp`](https://github.com/KWB-R/qmra-webapp), based exclusively on the repository's current `main` branch. It describes the system as implemented today; it is not a target architecture or a proposal for change. + +## 1. System shape + +QMRA is a Django web application implemented in Python. It serves server-rendered HTML pages from Django templates and executes risk calculations synchronously during web requests. The WSGI entry point is `qmra.wsgi:application`. + +The main runtime components are: + +- **Django project (`qmra/`)**: settings, top-level URL routing, public pages, health/readiness endpoints, authentication integration, and WSGI configuration. +- **Risk-assessment application (`qmra/risk_assessment/`)**: assessment models, scientific default-data access, forms, views, calculation logic, charts, exports, templates, and admin integration. +- **User application (`qmra/user/`)**: the custom user model, registration, login/logout, password changes, and account deletion. +- **Django templates and static data**: server-rendered configurator/result pages plus JSON files containing the checked-in scientific defaults. +- **Django admin**: administration of users and default scientific entities. +- **Single web deployment**: the repository's Docker Compose configuration defines one `web` service running Gunicorn. + +There is no separate public scientific API, calculation service, background worker, queue, cache, or external scientific engine in the inspected repository. The JSON listing endpoints for personal sources, exposures, and treatments are application endpoints, not a separate API layer. + +## 2. Runtime and deployment boundary + +The container is based on Python 3.12, installs the Django application and its Python dependencies, and runs Gunicorn on port 8080. Docker Compose runs static-file collection and database migrations before starting Gunicorn. + +The Compose configuration mounts: + +- a SQLite database directory for the application database; and +- a directory used for collected static files. + +The deployment passes `DOMAIN_NAME` into the container. The repository does not define an external database, object store, message broker, or cache. Operational settings such as production secret management, TLS, backups, and retention are not defined in the repository. + +Django Prometheus middleware and URL endpoints expose metrics. `django-structlog` is used for request logging. `/health` and `/ready` force a database connection and return `Ok` when the check succeeds. + +### Build and deployment process + +Deployment is done by the GitHub pipeline in `.github/workflows/`. It builds a Docker image, copies it to the server by SSH and installs it with Helm on microk8s (`infra/helm/qmra/`, one values file per environment). Every push to `main` deploys to dev and then to production, with no approval step and whatever files changed. Every push to a pull-request branch deploys to dev. The addresses, checks and full pipeline description are in [`environments.md`](environments.md). + +**Where this differs from section 2 above** + +- Section 2 describes Docker Compose. The pipeline deploys with microk8s and Helm. Compose is only used by the older scripts in `infra/deployment_scripts/` (for example `04_deploy_latest.sh` for `dev.qmra.org`). No workflow calls them. +- Section 2 says TLS is not defined in the repository. The Helm chart defines a Let's Encrypt issuer and a TLS ingress. +- The tests run on Python 3.11, and the Docker image uses Python 3.12.5. + +## 3. Main application flow + +The principal request path is: + +```mermaid +flowchart LR + Browser --> URLs[Django URL routing] + URLs --> Views[Risk-assessment views] + Views --> Forms[Django forms and formsets] + Views --> Data[Default data and user data] + Views --> Calc[risk.py calculation functions] + Calc --> Results[Result objects / saved result rows] + Results --> Templates[Server-rendered result template] + Results --> Plots[Plotly figures] + Results --> Export[CSV, HTML, PNG ZIP export] +``` + +The configurator collects one assessment definition, inflows, and an ordered treatment train. The views validate the submitted forms, resolve pathogen and default-data definitions, invoke the calculation functions, and pass result statistics to Django templates. Plotly figures are embedded as HTML for normal web pages. Export requests render the figures to PNG using Kaleido and package them with CSV and HTML files. + +## 4. Persistence and data ownership + +The application defines two SQLite databases and routes models between them with `qmra.risk_assessment.dbrouter.DBRouter`. + +| Store | Current purpose | Main contents | +|---|---|---| +| `default` SQLite database | Application and user-owned data | Django users, saved assessments, assessment inflows, assessment treatments, saved result rows, and personal reusable definitions | +| `qmra` SQLite database | KWB-managed default scientific entities | Django models whose class name contains `QMRA`, routed by the database router | +| Checked-in JSON files under `qmra/static/data/` | Runtime default-data source for static entity wrappers and bootstrap/export data | Pathogens, sources, exposures, treatments, pathogen-group parameters, references, and related default values | + +The default scientific model module (`qmra_models.py`) contains both Django models for default entities and `StaticEntity` wrappers. The wrappers read the JSON files dynamically for form choices and calculation lookup rather than reading the `qmra` database directly. The module documents the JSON files as the bootstrap/export representation for the default database. Admin operations on QMRA default models trigger default-data export and static-file collection. + +This creates an important current boundary: default scientific data exists both as administrable database models and as checked-in JSON consumed by application code. The repository does not make the long-term source-of-truth or synchronization lifecycle explicit. + +### Application assessment model + +The user/application database models are: + +- **`RiskAssessment`**: UUID, owner, timestamps, name, description, source and exposure names, events per year, volume per event, and related inflows, treatments, and results. +- **`Inflow`**: UUID, assessment, pathogen name, and minimum/maximum concentration. Form values are presented in `N/L`. +- **`Treatment`**: UUID, assessment, name, ordered `train_index`, and minimum/maximum LRVs for bacteria, viruses, and protozoa. It also detects configured maximum LRVs above 6 for warning display. +- **`RiskAssessmentResult`**: assessment, pathogen, the reference-level exceedance for infection and for DALYs, and min/max/q1/q3/median statistics for both best-case and worst-case. + +Assessment children and results are deleted with their assessment. Saved results are recalculated when required by the view layer. + +Personal reusable definitions are stored separately in `UserExposure`, `UserSource`, and `UserTreatment`. Personal source values are currently modeled for the same three pathogens used by the assessment forms. + +## 5. Calculation boundary + +The calculation implementation is in `qmra/risk_assessment/risk.py` and is called directly by Django views. + +The current calculation flow is: + +1. Resolve a pathogen model and pathogen group. +2. Sum treatment LRVs by pathogen group. `None` LRV values are treated as zero. +3. Calculate one case using the summed maximum LRV and one using the summed minimum LRV. +4. Sample inflow concentrations in log space. +5. Subtract the selected constant LRV and convert concentration to dose using event volume. +6. Apply the pathogen dose-response distribution. +7. Sample event probabilities over simulated years and events. +8. Calculate annual risk as `1 - product(1 - event_probability)`. +9. Convert infection probability to DALYs using the pathogen's infection-to-illness factor and DALYs-per-case value. +10. Compute min, max, first quartile, third quartile, and median statistics and assign categories against the current reference levels of `1e-4` infection probability and `1e-6` DALYs. + +`get_annual_risk()` creates a NumPy random generator with seed `42` and defaults to 10,000 events and 1,000 years. The supported pathogen distributions in the default model are exponential and beta-Poisson. + +The calculation does not currently model treatment failure, downtime, failure sequences, changing operating conditions, or a separate combined-failure mechanism. There is no persisted calculation-engine version or simulation-settings record in the inspected application model. + +Tests cover the calculation (including regression values), forms and the bundled default data; the default-data tests expect eight source-water, 22 treatment and eight exposure definitions. Coverage is uneven: the plot test is empty and the API test file has no substantive tests. + +## 6. Main user workflows + +### Anonymous assessment + +1. An unauthenticated visitor opens the assessment configurator. +2. Django renders the risk-assessment form, three inflow forms for the current pathogens, and a treatment formset. +3. The visitor submits the forms to the result flow. +4. The forms validate events/year, volume/event, concentrations, concentration ranges, LRV ranges, and treatment-form limits. +5. The view calls `assess_risk(..., save=False)`. +6. The result page renders risk statistics and two Plotly charts without persisting the assessment. + +The treatment formset supports up to 30 treatment steps and supports deletion. The anonymous configurator has no save controls. + +### Registered-user assessment + +1. Django session authentication identifies the user. +2. The authenticated root flow leads to the saved-assessment list. +3. Creating or editing an assessment saves the assessment, inflows, and ordered treatments inside the creation transaction. +4. Duplicate assessment names are handled by appending ` (2)`. +5. Existing results are removed before recalculation; `assess_and_save_results` calculates and stores fresh `RiskAssessmentResult` rows. +6. Saved assessments are listed newest first and can be opened for editing or deleted. +7. A result view recalculates and saves results when an assessment has no stored results. + +The user application uses Django's session authentication and the custom user model is an empty subclass of `AbstractUser`. No application-specific roles for engineers, operators, regulators, or researchers are implemented. + +### Personal reusable definitions + +Authenticated users can create personal exposure, source, and treatment definitions. Listing endpoints return the current user's definitions as JSON; anonymous list requests return an empty object. These definitions are selected by the form/UI layer and remain user-owned data rather than becoming shared defaults. + +### Export + +A saved assessment can be exported as a ZIP archive containing: + +- `exposure-assessment/inflows.csv`; +- `exposure-assessment/treatments.csv`; +- an assessment result CSV; +- an HTML report; +- `results-plots/infection-probability.png`; and +- `results-plots/dalys-pppy.png`. + +Pandas assembles the tabular exports, Plotly creates the figures, and Kaleido renders figures to PNG bytes. No Excel export is implemented in the inspected repository. + +## 7. Presentation and integration boundaries + +- **Presentation**: Django server-side templates, Crispy Forms, and Bootstrap 4. The configurator uses template partials and JavaScript for dynamic form behavior and guided-tour content. +- **Application logic**: Django views coordinate validation, persistence, calculation calls, result rendering, and exports. +- **Scientific calculation**: NumPy-based functions in `risk.py`; no independent service boundary exists. +- **Default scientific data**: JSON/static entities and QMRA-routed Django models, with the dual-representation boundary described above. +- **Persistence**: Django ORM over two SQLite databases selected by a database router. +- **Authentication**: Django users and sessions. +- **Observability**: Prometheus middleware/endpoints and structlog-based request logging. +- **Deployment**: Docker Compose and Gunicorn in a single web service. + +The assessment-list template implements a client-side comparison view: selected saved assessments' serialized results are rendered in a Plotly modal comparing annual infection probability per reference pathogen. There is no server-side comparison endpoint or model; the view reuses the stored per-assessment results. + +## 8. Open questions for possible ADR treatment + +These are unresolved or insufficiently explicit architectural points in the repository. They are recorded as questions, not decisions or recommendations. + +- What is the intended source of truth and lifecycle for default scientific data: the `qmra` database, the JSON files, or synchronization between both? +- Is routing models by whether the class name contains `QMRA` intended to remain the long-term database boundary? +- Should calculation continue to be called directly from Django views, or is a stable internal calculation-engine boundary intended? +- How should model/data versions and simulation settings be represented for saved results and exports? +- How should historical assessments behave after scientific default data or calculation logic changes? +- Is the client-side infection-risk comparison plot intended to grow into a fuller comparison workflow (inputs, DALYs, persistence), and if so, does it need a server-side boundary? +- Several inspected view paths retrieve `RiskAssessment` by ID without an explicit `user=request.user` filter. Is ownership scoping guaranteed elsewhere, or does this boundary require verification? +- Which production security and operational settings are required for deployment? The repository currently includes development-oriented fallbacks such as `DEBUG=True`, a fallback secret key, and a dummy email backend; TLS, backup, and retention policies are not defined here. + +## Evidence in the repository + +- [README.md](https://raw.githubusercontent.com/KWB-R/qmra-webapp/main/README.md) — application and deployment overview. +- [requirements.txt](https://raw.githubusercontent.com/KWB-R/qmra-webapp/main/requirements.txt) — Python dependencies and integrations. +- [Dockerfile](https://raw.githubusercontent.com/KWB-R/qmra-webapp/main/Dockerfile) and [docker-compose.yml](https://raw.githubusercontent.com/KWB-R/qmra-webapp/main/docker-compose.yml) — container and runtime boundary. +- [qmra/settings.py](https://raw.githubusercontent.com/KWB-R/qmra-webapp/main/qmra/settings.py), [qmra/urls.py](https://raw.githubusercontent.com/KWB-R/qmra-webapp/main/qmra/urls.py), and [qmra/wsgi.py](https://raw.githubusercontent.com/KWB-R/qmra-webapp/main/qmra/wsgi.py) — project configuration, routing, and entry point. +- [risk_assessment/models.py](https://raw.githubusercontent.com/KWB-R/qmra-webapp/main/qmra/risk_assessment/models.py), [qmra_models.py](https://raw.githubusercontent.com/KWB-R/qmra-webapp/main/qmra/risk_assessment/qmra_models.py), [user_models.py](https://raw.githubusercontent.com/KWB-R/qmra-webapp/main/qmra/risk_assessment/user_models.py), and [dbrouter.py](https://raw.githubusercontent.com/KWB-R/qmra-webapp/main/qmra/risk_assessment/dbrouter.py) — persistence and database routing. +- [risk.py](https://raw.githubusercontent.com/KWB-R/qmra-webapp/main/qmra/risk_assessment/risk.py), [views.py](https://raw.githubusercontent.com/KWB-R/qmra-webapp/main/qmra/risk_assessment/views.py), and [forms.py](https://raw.githubusercontent.com/KWB-R/qmra-webapp/main/qmra/risk_assessment/forms.py) — calculation, orchestration, validation, and workflows. +- [exports.py](https://raw.githubusercontent.com/KWB-R/qmra-webapp/main/qmra/risk_assessment/exports.py) and [plots.py](https://raw.githubusercontent.com/KWB-R/qmra-webapp/main/qmra/risk_assessment/plots.py) — export and visualization boundaries. +- [default data](https://github.com/KWB-R/qmra-webapp/tree/main/qmra/static/data) and [templates](https://github.com/KWB-R/qmra-webapp/tree/main/qmra/risk_assessment/templates) — checked-in scientific data and server-rendered UI. diff --git a/docs/domain-model-open-questions.md b/docs/domain-model-open-questions.md new file mode 100644 index 0000000..8cd2b9a --- /dev/null +++ b/docs/domain-model-open-questions.md @@ -0,0 +1,60 @@ +# Domain model: open questions + +Status: all items resolved or deferred (2026-09-23). Raised on 2026-09-22 while deriving `CONTEXT.md` from the original +`docs/glossary.md` and cross-checking both against the code. `CONTEXT.md` has since +replaced that glossary, which was removed; "the glossary" below refers to `CONTEXT.md`. Resolve each item, update `CONTEXT.md`, then +remove it from this list. + +## Follow-ups from resolved items + +- **Inflow / outflow concentration (resolved 2026-09-23).** The glossary now uses + *Inflow concentration* and *Outflow concentration*. Decided target for the code, to be + done in a later code change (this branch stays docs-only): + - identifiers `inflow` / `outflow` become `inflow_concentration` / + `outflow_concentration`; + - the export file `inflows.csv` becomes `inflow_concentration.csv`. + + Configurator wording ("pathogen concentrations") is left as it is. + +- **Site-specific source water (resolved 2026-09-23).** When a user enters or edits any + inflow concentration of a bundled source water by hand, the assessment's source water + becomes a site-specific source water. Naming rule for the code: it is named after the + bundled source water with the prefix "site-specific" (e.g. selecting "groundwater" and + editing the Rotavirus concentration gives "site-specific groundwater"). Inflow + concentrations entered without selecting any source water are named plain + "site-specific source water". Personal definitions already carry the user's own name + and keep it when edited. The code does not do this yet: it keeps the plain source water + name next to the edited values, so saved assessments and exports show "groundwater" for + data that is not the bundled groundwater. To be changed in a later code change. + +## Deferred: UI wording that conflicts with the glossary + +Decided 2026-09-23: leave the UI as it is for now. The glossary keeps its terms and +lists the UI words under *Avoid*. Where users see them today: + +- **"Tolerable risk level"** (glossary: *Health-based reference level*): result summary + text (`assessment-result.html`, five times, attributed to the WHO), FAQ (`faqs.html`), + result plots (`plots.py`, `views_v0.py`) and the saved-assessments plot + (`risk-assessment-list.html`). +- **"Exposure scenario"** (glossary: *Exposure*): FAQ (`faqs.html`, five times), home and + configurator guided tours, form help text (`views_v0.py`). +- **"Maximum / minimum risk scenario"** (glossary: *Worst-case* / *Best-case*): explanation text on the saved-assessments page (`risk-assessment-list.html`). + +## Deferred: LRV above 6 + +Decided 2026-09-23: leave it for now; the origin of the rule is unclear. What the code +does: each treatment step's maximum LRV per pathogen group is checked against 6 +(`models.py`, `above_max_lrv`). Above 6, the result page warns that "the WHO standards +do not allow LRVs above 6 in order to promote redundancy and robustness in treatment +trains" (`assessment-result.html`). Minimum LRVs and the train total are not checked, +and the calculation still uses the value above 6. Open: confirm the WHO source, decide +whether it is a domain rule worth naming (e.g. *single-step LRV limit*), and whether it +should warn or cap. + +## ADR candidates + +- **Written:** `docs/adr/0001-failure-days-drawn-per-exposure-event.md` (2026-09-23), + the decision to draw failure days per exposure event inside the Monte Carlo simulation + instead of following the source method's 365-day sequence. +- **Not needed:** "An assessment holds exactly one scenario" is neither hard to reverse + nor surprising, so it does not need an ADR. diff --git a/docs/environments.md b/docs/environments.md new file mode 100644 index 0000000..e446744 --- /dev/null +++ b/docs/environments.md @@ -0,0 +1,112 @@ +# Environments: dev and production + +Status: draft, 2026-09-24. This page says where the QMRA application runs, how to reach it, how to check that it works, and how a change gets there. Facts marked "checked" were verified on 24 September 2026 from the repository, the GitHub workflow runs, and requests to the public addresses. Anything that lives outside the repository is listed as "to fill in" and is not guessed. + +## At a glance + +| | dev | production | +|---|---|---| +| Address | https://dev.qmra.org | https://qmra.org | +| Purpose | try a change before users see it; also receives every pull-request branch | the service users use | +| Server | its own server (checked: the two addresses resolve to different servers) | its own server | +| Debug mode | on | off | +| Replicas | 1 | 1 | +| TLS certificate | Let's Encrypt, renewed by cert-manager. Checked: valid 15 Aug to 13 Nov 2026 | Let's Encrypt, renewed by cert-manager. Checked: valid 22 Aug to 20 Nov 2026 | +| Monitoring address | https://dev2.qmra.org (redirects to a login page) | https://monitoring.qmra.org (redirects to a login page) | +| Deployed by | GitHub pipeline, on every push to `main` and every push to a pull-request branch | GitHub pipeline, on every push to `main` | + +## Addresses and endpoints + +Checked on 24 September 2026. The same paths exist on both addresses. + +| Path | What it is | Login needed | +|---|---|---| +| `/` | the assessment configurator; guests can run an assessment without saving | no | +| `/login`, `/register` | sign in, create an account | no | +| `/admin/login/` | Django admin for users and the default scientific data | admin account | +| `/health` | returns `Ok` when the application can reach its database | no | +| `/ready` | returns `Ok` when the application is ready for traffic | no | +| `/metrics` | Prometheus metrics of the application | no. **Checked: readable by anyone on dev and on production.** See "Open points" | + +## How to check that an environment works + +Five minutes, in this order. + +1. Health. Both must print `Ok`: + ```bash + curl -sS https://dev.qmra.org/health; echo + curl -sS https://dev.qmra.org/ready; echo + ``` + Use `https://qmra.org` for production. +2. Guest assessment. Open the address, keep the bundled defaults, run the assessment, and check that the result page shows both risk measures for the three reference pathogens. +3. Account. On dev, register a test account at `/register`, save an assessment, reopen it, and download its export package (a ZIP). Do not create test accounts or test data on production. +4. Certificate. The browser shows a valid certificate, and the dates above have not passed. +5. After a deploy, check that the change you expected is visible. + +## How a change gets there + +The pipeline is `.github/workflows/ci.yaml`. It has no path filter, so a change to documentation only runs the same jobs as a change to code. + +| What you do | Jobs that run | Where it ends up | Time | +|---|---|---|---| +| Push to a branch that has an open pull request into `main` | test, build-dev, deploy-dev | dev | about 4 minutes | +| Merge a pull request into `main` (a push to `main`) | test, build-dev, build-prod, deploy-dev, deploy-prod | dev, then production | about 5 minutes | + +- **No approval.** The GitHub environments `dev` and `prod` have no protection rules (checked): no required reviewer and no wait time. On the last push to `main`, the production deploy started 5 seconds after the dev deploy finished. +- **Documentation-only changes deploy too.** The last push to `main` (22 September) changed only `.github/pull_request_template.md` and still deployed to production. +- **Dev follows the last branch pushed.** The deploy step runs `git checkout` of the pushed branch on the dev server, so dev shows whichever pull request pushed last. Tell the team before you test a branch there. +- **Migrations run on every deploy.** Before the new web container starts, the Helm chart runs the database migrations for both SQLite databases. Nothing reverses them. +- **What a deploy does.** GitHub builds the Docker image `qmra:`, copies it to the server, imports it into microk8s and runs `helm upgrade --install` with `infra/helm/qmra/.values.yaml`. + +## How to reach the servers + +**The web application** is public at the addresses above. + +**The server shell.** The pipeline reaches each server by SSH. The values are GitHub environment secrets, which are not in the repository and can be read or changed only by a repository admin: + +| Secret | Holds | +|---|---| +| `DEPLOY_HOST` | server address | +| `DEPLOY_USER` | SSH user | +| `DEPLOY_SERVER_SSH_KEY` | the pipeline's private key | +| `DEPLOY_PATH` | folder with the checked-out repository | +| `APP_SECRET_KEY` | the Django secret key, set at deploy time | + +To fill in. None of this can be found from the repository: + +| Item | dev | production | +|---|---|---| +| Who runs the server and grants access | | | +| How a person gets an SSH account or key | | | +| Server address for people (the pipeline's `DEPLOY_HOST`) | | | +| Deploy folder | | | +| Where the databases are backed up, and how often | | | + +**Commands on the server.** These follow from the deploy script (`microk8s`, namespace `qmra`). They were not run for this page: + +```bash +microk8s kubectl -n qmra get pods +microk8s kubectl -n qmra logs deploy/qmra --tail=100 +microk8s helm -n qmra history qmra +``` + +The databases are on the server at `/var/lib/qmra/qmra.db` and `/var/lib/qmra/default_qmra_data.db`; collected static files are at `/var/cache/qmra/static`. + +## Skipping the pipeline + +Put `[skip ci]` in a commit message and GitHub does not start the pipeline for that push. This is GitHub's documented behaviour. `[ci skip]`, `[no ci]`, `[skip actions]` and `[actions skip]` work the same way, and so does a final `skip-checks: true` line. + +- **On a pull-request branch:** use it in the last commit of the push. Nothing deploys to dev. The latest commit then has no test result. If a required check ever blocks the merge, push one more commit without the text. +- **When merging into `main`:** write it in the description box of the merge dialog. Use "Create a merge commit" or "Squash and merge", because "Rebase and merge" creates no commit whose message you can edit. Nothing deploys to dev or production. +- **Do not use it** when a change should really be tested and deployed. Skipped changes are deployed together with the next push that is not skipped. + +Status: checked on a pull-request branch on 24 September 2026 (commit `400279d`): no run and no check started. Not yet checked on a merge into `main`; check the Actions tab right after the first time you do it. + +## Open points + +- **Nothing stops a production deploy.** Any merge into `main` goes to production, finished or not. The failure roadmap works around this by merging nothing into `main` until the release (decision D5 in `docs/failure_roadmap.md`). +- **The metrics address is public** on both environments. It exposes request counts and timings. Decide whether it should be limited to the monitoring system. +- **No rollback is defined.** `helm` keeps revisions, but no workflow or page uses them, and migrations are not reversed. +- **One replica per environment.** A deploy probably interrupts the service for a short time. This was not measured. +- **Same release name and namespace on both servers** (`qmra`). This is safe only because the two environments run on different servers, which the "to fill in" table should confirm. +- **Python versions differ.** The tests run on Python 3.11, and the image uses Python 3.12.5. diff --git a/docs/failure_roadmap.md b/docs/failure_roadmap.md new file mode 100644 index 0000000..c6a25f7 --- /dev/null +++ b/docs/failure_roadmap.md @@ -0,0 +1,141 @@ +# Failure roadmap + +Status: draft, 2026-09-24. Overview of the Changes that deliver the MVP in `docs/vision.md`: +treatment failure, including combined failures. Sources of truth: `docs/vision.md`, +`CONTEXT.md`, `docs/adr/`. Each Large change gets a Spec (`/to-spec`); Small changes are +built directly. Changes are refined into tickets later. All Changes start from `main`. + +## Changes + +### C1 Failure inputs — Large + +Spec issue: #___ (draft: `docs/specs/c1-failure-inputs.md`) + +Purpose: users can enter a failure frequency and a failure duration for each treatment +step, and they are stored with the scenario. + +Scope: failure frequency (failure days per year, 0–365, default 0) and failure duration +(minutes, 1–1440, default 30) on each treatment step of an assessment and on personal +treatment steps; configurator fields with units and validation; only treatment steps with +at least one positive LRV accept failure inputs (D4); values of a personal treatment step are +carried into the assessment; saving, reopening and editing keep them. Existing saved +assessments get failure frequency 0, so their results are unchanged. + +Not included: any effect on the calculation (C2), export and report (C3), explanations +beyond field labels and units (C4). + +### C2 Failure calculation — Large + +Spec issue: #___ (draft: `docs/specs/c2-failure-calculation.md`) + +Purpose: results include treatment failures as decided in ADR-0001. + +Scope: failure days drawn per exposure event and per treatment step inside the Monte Carlo +simulation; worst-case loses the full LRV of every failing step; best-case applies the +mixed-water assumption; a failure removes only positive LRVs, LRVs of 0 or below stay +(D4); combined failures overlap as little as possible; the result page +and the reference-level exceedance show the result including failures. With every failure +frequency at 0, results equal today's. Tested with checks that need no external expected +values: failure frequency 0 reproduces today's results exactly, worst-case risk is never +below best-case, a higher failure frequency never lowers the risk, LRVs of 0 or below are +unaffected, and combined failures overlap minimally in best-case. + +Not included: the comparison with an independent calculation (C5), showing normal +operation next to the result with failures (decided against), DALYs in the assessment +comparison (later). + +### C3 Failure inputs in export and report — Small + +The export package's treatment table and the HTML report show each treatment step's failure +frequency and failure duration, so a reader can see which failures a result is based on. +Left out: renaming export files (`inflows.csv` → `inflow_concentration.csv` waits until +later), origin and reference columns for inputs, changes to the assessment comparison. + +### C4 Explain treatment failure — Small + +The FAQ, the configurator guided tour and the Sphinx documentation (`docs/source/`) explain +failure frequency and failure duration, failure days, why worst-case ignores the failure +duration, the mixed-water assumption and combined failures. Left out: field labels and +units (C1), rewording the deferred UI terms such as "tolerable risk level", and +explanations of unrelated parts of the model. + +### C5 Failure benchmark — Small + +A small set of benchmark scenarios with failures is stored in the repository: a single +failing step, a combined failure, failure durations adding up to more than a day, an +exposure with few events per year, and a step with a negative LRV in one pathogen group. +A domain expert calculates their expected results with an independent Monte Carlo script +(for example in R) that implements the documented method (`CONTEXT.md`, the decisions in +`docs/QMRA_Failure_Approach.md`, ADR-0001) without using the application's code, and +regression tests compare the application's results with them within the agreed tolerance +(D1, D2). The tests then run on every later change. Left out: benchmarks for parts of the +model unrelated to failures, and the reproducibility records named in the vision's quality +goals. + +## Dependencies + +- C2 depends on C1 (it reads the stored failure inputs). +- C3 depends on C1. +- C4 depends on C2 (it explains the calculation's behaviour) and on C1. +- C5 depends on C2 (it checks the implemented calculation). +- Release depends on C1–C5: users must never see failure inputs without effect, results + they cannot trace, or results not checked against an independent calculation. Because + every merge into `main` deploys to production without approval (`docs/environments.md`), + merging any of C1–C5 into `main` before the release is not allowed (D5). Each Change stays an open pull + request, built on the pull request it depends on, and is tested on the dev environment. + +## Parallel work + +- The Specs of C1 and C2 can be written in parallel; both rest on `CONTEXT.md` and ADR-0001. +- Once C1 is done, C2 and C3 can be built in parallel, both on top of C1's pull request. +- C4 can be drafted while C2 is being built and finished once C2 is done. +- The benchmark scenarios of C5 and their expected values (D2) can be prepared while C2 is + being built; only the comparison tests need C2 done. +- C4 and C5 can be built in parallel. + +## Sequence + +1. `/to-spec` C1 and C2 (in parallel). +2. `/implement` C1. +3. `/implement` C2; build C3 in parallel. +4. Build C4 and C5 once C2 is done. +5. Release: once C1–C5 are done and tested together on the dev environment and D3 is + settled, merge all five pull requests into `main` together. That deploys them to + production. + +"Done" means the pull request is reviewed and tested on the dev environment, but not merged. +The dev environment shows only the pull request pushed last, so testing there is announced +to the team first. Each open pull request is kept up to date with `main` to limit +conflicts at the release. + +## Unresolved decisions + +| # | Decision | Owner | Depends on | Blocks | +|---|---|---|---|---| +| D1 | Tolerance for matching benchmark scenarios. With an independent Monte Carlo script (D2), differences are only sampling noise or real errors | Wolfgang, Malte | — | C5 | +| D2 | Who calculates the expected values; ideally not the author of C2 | Wolfgang, Malte | — | C5 | +| D3 | Which one or two domain experts sign off the C5 benchmark comparison | Wolfgang, Malte | — | Release | + +## Resolved decisions + +- **D4 (2026-09-24).** A treatment step accepts failure inputs if at least one of its LRVs + is positive. A failure removes only the positive LRVs; LRVs of 0 or below, such as + regrowth in one pathogen group, are not affected. +- **Implement before benchmarking (2026-09-24).** C2 is built and tested with internal + checks; the comparison with an independent calculation moved to C5. The release still + waits for C5. +- **Benchmark method (2026-09-24).** Expected values come from an independent Monte Carlo + script that implements the documented method, not from a spreadsheet of the original + Hambsch equations, so that differences are noise or errors rather than method + differences. +- **Acceptance before release (2026-09-24).** Domain experts accept the results by + reviewing the C5 benchmark comparison and confirming it in writing, for example in the + C5 pull request. The explanations in C4 are not part of this sign-off. Who signs off is + still open (D3). +- **D5: no merge into `main` before the release (2026-09-24).** Merging any of C1–C5 into + `main` before the release is not allowed. They stay open pull requests, each + built on the one it depends on and tested on the dev environment, and are merged into + `main` together at the release. Rejected: a setting that hides failures in production + (extra code in C1), and a required approval for production deploys (would block urgent + fixes on `main`). Accepted cost: long-lived branches and conflicts to resolve with + `main`. diff --git a/docs/specs/c1-failure-inputs.md b/docs/specs/c1-failure-inputs.md new file mode 100644 index 0000000..c8e50af --- /dev/null +++ b/docs/specs/c1-failure-inputs.md @@ -0,0 +1,230 @@ +# Spec C1: Failure inputs + +Status: draft, 2026-09-24. Change C1 in `docs/failure_roadmap.md` (Large). Spec issue: #___ +Sources: `docs/vision.md` (MVP item 1 and 2), `CONTEXT.md`, ADR-0001, roadmap decisions D4 +and D5. + +## Problem Statement + +A utility operator or engineer who wants to know how failures of their treatment steps +affect the yearly risk has nowhere to say how often a step fails and for how long. The +assessment configurator only knows LRVs, and a personal treatment step only stores a name +and LRVs. Before the calculation can include treatment failures (C2), users need a place to +enter a failure frequency and a failure duration for each treatment step, and the +application needs to keep those values with the scenario. + +## Solution + +Every treatment step in the assessment configurator gets two more fields next to its LRVs: +**failure frequency** (failure days per year) and **failure duration** (minutes). They +start at 0 days per year and 30 minutes, so a step does not fail unless the user says so. +The application checks the values when the assessment is run or saved and explains what is +wrong. A step can only have a failure frequency above 0 if at least one of its LRVs is +positive, since a failure removes only positive LRVs (D4). + +Registered users can also store both values on a personal treatment step. When they add +that step to an assessment, its failure frequency and failure duration are copied in like +its LRVs. Saved assessments keep the values, and reopening or editing an assessment shows +them again. Assessments saved before this change reopen with failure frequency 0 and +failure duration 30, so nothing about them changes. + +In this Change the values do not yet affect the result. The calculation follows in C2, and +nothing reaches production before C1–C5 are released together (D5). + +## User Stories + +1. As a utility operator, I want to enter a failure frequency for each treatment step in my + assessment, so that the assessment reflects how often each step fails at my plant. +2. As a utility operator, I want to enter a failure duration for each treatment step, so + that the assessment reflects how long a typical failure event lasts. +3. As a user, I want the failure frequency to be given in failure days per year, so that I + can use the numbers from my plant's operating records directly. +4. As a user, I want the failure duration to be given in minutes, so that short failure + events can be expressed without fractions. +5. As a user, I want the unit of each failure field shown next to it, so that I do not + enter hours instead of minutes or events instead of days. +6. As a user, I want every treatment step to start with failure frequency 0, so that an + assessment without failure information behaves as before. +7. As a user, I want every treatment step to start with failure duration 30 minutes, so + that I only change the duration when I know a better value. +8. As a user, I want to enter a fractional failure frequency such as 0.5 days per year, so + that I can describe a step that fails once every two years. +9. As a user, I want a failure frequency below 0 or above 365 to be rejected with a clear + message, so that I notice a typing mistake. +10. As a user, I want a failure duration below 1 or above 1440 minutes, or a non-whole + number of minutes, to be rejected with a clear message, so that I notice a typing + mistake or a wrong unit. +11. As a user, I want to be told when I give a failure frequency above 0 to a step that has + no positive LRV, so that I understand that such a step cannot fail. +12. As a user modelling recontamination with a negative LRV, I want that step to keep + failure frequency 0, so that recontamination is never treated as a failing step. +13. As a user, I want a step with a positive LRV for one pathogen group and a zero or + negative LRV for another to accept failure inputs, so that steps like chlorination + (protozoa minimum 0) or a storage tank with regrowth can fail. +14. As a user, I want the failure fields to appear in the same card as the step's LRVs, so + that I see all values of a step together. +15. As a user, I want the failure values of each step to stay with that step when I + reorder or remove other steps, so that values do not move to the wrong step. +16. As a user adding the same bundled step twice, I want to give each copy its own failure + values, so that two parallel units can have different failure records. +17. As a guest, I want to enter failure inputs and run an assessment, so that I can try the + feature without an account. +18. As a guest, I want invalid failure inputs to be rejected in the same way as for + registered users, so that the rules do not depend on having an account. +19. As a registered user, I want the failure values of each step saved with my assessment, + so that I do not have to enter them again. +20. As a registered user, I want to see the saved failure values when I reopen an + assessment, so that I can check what the assessment is based on. +21. As a registered user, I want to change failure values when I edit an assessment and + have the new values saved, so that I can correct or update them. +22. As a registered user, I want assessments I saved before this change to reopen with + failure frequency 0 and failure duration 30, so that their results do not change. +23. As a registered user, I want to store a failure frequency and failure duration on a + personal treatment step, so that my plant's failure record is reused in every + assessment that uses the step. +24. As a registered user, I want the same checks on a personal treatment step as in the + configurator, so that a personal step never carries values an assessment would reject. +25. As a registered user, I want a personal treatment step's failure values copied into the + assessment when I add the step, so that I do not type them again. +26. As a registered user, I want to change the copied failure values in one assessment + without changing the personal treatment step, so that I can try other values in one + scenario. +27. As a registered user, I want personal treatment steps I created before this change to + offer failure frequency 0 and failure duration 30, so that they keep working. +28. As a registered user, I want my personal treatment steps and their failure values to be + visible only to me, so that my plant's failure record stays private. +29. As a user comparing scenarios, I want two saved assessments to differ only in their + failure inputs, so that I can later compare the effect of different failure + frequencies. +30. As a user, I want bundled treatment steps to come without failure values, so that I am + not given literature failure data the application does not have. +31. As a developer of C2, I want the failure frequency and failure duration of every step in + an assessment to be available where the calculation reads the treatment train, so that + C2 only has to change the calculation. +32. As a developer of C3, I want the stored failure values to be readable from a saved + assessment, so that C3 can add them to the export package and report. + +## Implementation Decisions + +- **Two new attributes on two kinds of treatment step.** The treatment step of an + assessment and the personal treatment step each get *failure frequency* (a decimal + number of failure days per year) and *failure duration* (a whole number of minutes). + Both are required, with defaults 0 and 30. A schema migration adds them to both, and + existing rows receive the defaults. +- **Bundled treatment steps are unchanged.** They carry no failure data (failure inputs are + user input only). A bundled step added to an assessment starts at the defaults. +- **One validation rule, used by both forms.** The treatment step form of the configurator + and the personal treatment step form apply the same rule, defined once: + - failure frequency from 0 to 365 inclusive, decimals allowed; + - failure duration a whole number from 1 to 1440 inclusive; + - failure frequency above 0 only if at least one of the step's six LRVs (minimum and + maximum for bacteria, viruses and protozoa) is above 0; an empty LRV counts as 0 (D4). + The error is attached to the failure frequency field. + The duration range is checked even when the frequency is 0, so stored values are always + valid. The existing minimum ≤ maximum LRV check, which both forms currently duplicate, + may move into the same shared rule. +- **Configurator.** Each treatment step card shows the two fields below its LRV rows, + labelled with their units ("Failure frequency (days per year)", "Failure duration + (minutes)"). The fields are always shown; the D4 rule is enforced by validation, since the + user can change LRVs after entering a frequency. Guests see and use the same fields. +- **Saving and reopening.** The treatment step formset saves the two values with each step, + and reopening an assessment fills them in again. Each value belongs to its step, not to a + position in the train. +- **Personal treatment steps.** The personal treatment step form gets the two fields. The + list of personal treatment steps that the configurator loads includes both values, and + the configurator copies them into a newly added step together with its LRVs. The copy is + independent: editing it in an assessment leaves the personal treatment step unchanged. +- **Guest path.** The guest result submits the same treatment step formset. Failure values + are validated there too and are not stored. +- **No effect on results.** The calculation, the result page and the export package are not + changed in C1. The values are stored so that C2 and C3 can use them. +- **Delivery.** C1 stays an open pull request and is tested on the dev environment; it is + not merged into `main` before the release (D5). C2 and C3 build on it. + +## Testing Decisions + +- **What a good test is.** A test drives the application from the outside the way a browser + does and checks what a user or a later request can observe: the response, the saved + assessment, the reopened form, the list of personal treatment steps. It does not check + how validation is organised internally. +- **One seam: HTTP through the Django test client.** Tests post the configurator form (save + and guest result) and the personal treatment step form, then read the saved assessment, + the reopened configurator or the personal treatment step list. This covers form + validation, saving, reopening and ownership without a separate form-level seam. +- **Behaviour covered at that seam:** + - saving an assessment with failure inputs and reopening it returns the same values; + editing and saving again stores the new values; + - out-of-range frequency or duration, a non-whole duration, and a frequency above 0 on a + step without a positive LRV are rejected with a message, on the saved configurator, on + the guest result and on the personal treatment step form; + - a step whose only positive LRV is one maximum (for example protozoa 0–2) accepts a + frequency above 0; a step with only negative or empty LRVs does not; + - a guest result with valid failure inputs is accepted; + - a personal treatment step saved with failure inputs appears with them in its owner's + list; another user's list does not contain it; + - an assessment and a personal treatment step created without failure values (as before + the migration) come back with frequency 0 and duration 30; + - the result of an assessment is identical with and without failure inputs (C1 does not + change the calculation). +- **Prior art.** The form tests in the risk-assessment test module (min ≤ max, negative LRVs + allowed) show the validation cases to extend; the calculation regression test shows how + fixed results are compared. There are no HTTP-level tests yet; C1 adds the first ones, + using Django's test client and a logged-in test user. +- **Manual check on the dev environment.** Copying a personal treatment step's failure + values into the configurator happens in the browser and is not covered by the HTTP seam. + Before C1 is done, a tester on the dev environment creates a personal treatment step with + failure inputs, adds it to an assessment, sees the copied values, changes them, saves, + reopens, and confirms the personal treatment step itself is unchanged. +- The existing test suite passes unchanged. + +## Acceptance criteria + +1. Each treatment step in the configurator, for guests and registered users, shows failure + frequency and failure duration with their units, starting at 0 and 30. +2. The validation rules above reject invalid values with a message on the configurator, + the guest result and the personal treatment step form, and accept every valid + combination, including the D4 cases. +3. Failure values survive save, reopen and edit of an assessment, per step. +4. Personal treatment steps store failure values, the configurator copies them into an + added step, and the copy can be changed independently (manual check on the dev + environment). +5. Assessments and personal treatment steps that existed before C1 read back failure + frequency 0 and failure duration 30. +6. Results and export packages are the same as before C1 for every assessment. +7. All HTTP-level tests listed above pass, and the existing suite passes unchanged. + +Quality goals from `docs/vision.md`, one criterion each: + +| Quality goal | Criterion in C1 | +|---|---| +| Input validity | Every failure input shows its unit, and a value outside 0–365 days per year or 1–1440 whole minutes, or a frequency above 0 on a step without a positive LRV, is rejected with a message (criteria 1–2). | +| Reproducibility | Reopening a saved assessment returns exactly the stored failure values, and assessments from before C1 keep their results, since they read back frequency 0 (criteria 3, 5, 6). | +| Export stability | The export package of any assessment is unchanged by C1 (criterion 6); adding failure values to it is C3's job. | +| Privacy of registered-user data | A personal treatment step and its failure values appear only in its owner's list; a test requests the list as another user and finds nothing of it (criterion 7). | + +Not relevant to C1: *Scientific correctness* (the calculation is untouched; C2 and C5) and +*Traceability* (showing failure inputs in the export package and report is C3). + +## Out of Scope + +- Any effect of failure inputs on the calculation, the result page or the reference-level + exceedance (C2). +- Failure inputs in the export package and the HTML report (C3). +- Explanations of failure frequency, failure duration and the two cases beyond field + labels and units: FAQ, guided tour, Sphinx documentation (C4). +- Benchmark scenarios (C5). +- Failure data for bundled treatment steps. +- Partial loss of removal (not planned). +- Hiding the failure fields in production; D5 keeps C1 out of `main` instead. +- A browser test setup; the one browser-only behaviour is checked manually. +- The naming follow-ups in `docs/domain-model-open-questions.md` and the deferred UI + wording. + +## Further Notes + +- Until C2 is done, failure inputs have no effect on results. That is acceptable only + because of D5: C1 stays out of `main` and production until the release. +- While exploring: saving a personal treatment step checks the name for uniqueness against + the user's personal *exposures*, not their personal treatment steps. That is an existing + bug outside this Change; it is noted here so it is not mistaken for C1 behaviour. +- The open roadmap decisions D1–D3 do not affect C1. diff --git a/docs/specs/c2-failure-calculation.md b/docs/specs/c2-failure-calculation.md new file mode 100644 index 0000000..093203e --- /dev/null +++ b/docs/specs/c2-failure-calculation.md @@ -0,0 +1,231 @@ +# Spec C2: Failure calculation + +Status: draft, 2026-09-24. Change C2 in `docs/failure_roadmap.md` (Large). Spec issue: #___ +Sources: `docs/vision.md` (MVP item 3 and 4), `CONTEXT.md`, ADR-0001, the decisions in +`docs/QMRA_Failure_Approach.md`, roadmap decisions D4 and D5. Builds on C1 +(`docs/specs/c1-failure-inputs.md`). + +## Problem Statement + +After C1, users can enter a failure frequency and a failure duration for each treatment +step, but the result ignores them: an assessment with a UV unit failing 10 days per year +gives the same annual probability of infection as one where it never fails. A utility +operator therefore still cannot see how failures of their treatment steps affect the +yearly risk, whether the reference level is still met, or how two failure scenarios +compare. + +## Solution + +The risk calculation takes treatment failures into account as decided in ADR-0001. In the +Monte Carlo simulation, every simulated exposure event falls on a failure day of each +treatment step with probability failure frequency / 365. On a failure day: + +- **worst-case** calculates the event with the failing steps' positive LRVs removed for the + whole day, whatever the failure duration; +- **best-case** applies the mixed-water assumption: the consumed water is a mixture of water + treated during the failure events and water treated in normal operation, in proportion to + the failure durations, with the failure events arranged within the day so that the mixed + water has the lowest pathogen concentration. + +LRVs of 0 or below are never affected (D4). The result page, the reference-level +exceedance, the saved results and the result table in the export package all show the +result including failures, with no change to their layout. An assessment in which every +failure frequency is 0 gives exactly the same results as before C2. + +## User Stories + +1. As a utility operator, I want the result to include the failures I entered, so that I + see the yearly risk my plant actually runs. +2. As a utility operator, I want the reference-level exceedance to be judged on the result + including failures, so that I learn whether failures push a pathogen over the reference + level. +3. As a utility operator, I want both risk measures (annual probability of infection and + DALYs per person per year) to include failures, so that I can judge the health burden as + well as the infection risk. +4. As a user, I want worst-case to assume that all water consumed on a failure day was + treated during the failure, so that worst-case stays the cautious estimate. +5. As a user, I want best-case to assume mixed water on a failure day, weighted by the + failure duration, so that best-case reflects storage that dilutes failure water. +6. As a user, I want a longer failure duration to raise the best-case risk and leave the + worst-case risk unchanged, so that the two cases behave as documented. +7. As a user, I want a higher failure frequency never to lower the risk, so that the results + make sense when I try different values. +8. As a user, I want worst-case risk never to be below best-case risk, so that the two cases + keep their meaning with failures. +9. As a user, I want the number of failure days to vary between simulated years, so that + the risk distribution shows years with more or fewer failures, as real years do. +10. As a user with an exposure of few events per year, I want a low failure frequency to + show up as an upper tail of the risk distribution, so that I see the rare bad years. +11. As a user with several failing steps, I want failures of different steps on the same + day to be combined, so that combined failures are not missed. +12. As a user, I want a combined failure in best-case to assume that failure events do not + overlap when they fit into one day, so that best-case stays the favourable assumption. +13. As a user, I want combined failure events longer than a day in total to overlap only as + much as necessary, on the steps whose joint loss matters least, so that best-case stays + favourable without a jump at 24 hours. +14. As a user, I want a failure to remove only the positive LRVs of a step, so that a step + with regrowth (a negative LRV) in one pathogen group does not improve during a failure. +15. As a user, I want a pathogen group whose LRV at a step is 0 or empty to be unaffected by + that step's failure, so that failures only matter where the step removes pathogens. +16. As a user, I want every failure frequency at 0 to give exactly the results I got before, + so that existing assessments and comparisons keep their numbers. +17. As a registered user, I want saved assessments to store the result including failures, + so that the saved-assessments page and the assessment comparison show it. +18. As a user comparing two saved assessments that differ only in their failure inputs, I + want the comparison to show different infection risks, so that I see what the failures + cost. +19. As a registered user, I want the export package's result table to contain the result + including failures, so that my report matches the result page. +20. As a guest, I want the unsaved result to include failures in the same way, so that the + result does not depend on having an account. +21. As a user, I want the same assessment to give the same result every time it is + calculated, so that I can reproduce and discuss a result. +22. As a reviewer, I want the calculation to follow ADR-0001 and the documented decisions, + so that the result can be checked against an independent calculation (C5). +23. As a user, I want the result page to look as before, so that I do not need to learn a + new layout. +24. As a user, I want the calculation with failures to finish in about the same time as + before, so that running an assessment stays quick. +25. As a developer of C5, I want the calculation to be deterministic for a given scenario, + so that benchmark comparisons are stable. +26. As a developer of C4, I want the calculation's rules to be stated in the Spec in plain + terms, so that the explanations for users can be written from it. + +## Implementation Decisions + +- **Only the calculation module changes.** Its public entry point keeps its interface: it + receives an assessment, its inflow concentrations and its treatment steps, and returns one + result per reference pathogen. It now reads each treatment step's failure frequency and + failure duration (stored by C1). The result record, the result page, saving and the export + are not changed; they show whatever the calculation returns. +- **What a failure removes (D4).** For each treatment step and pathogen group, worst-case + can lose the step's minimum LRV and best-case its maximum LRV, each only if it is above 0. + LRVs of 0, below 0 or empty stay in place during a failure. +- **Failure days per exposure event (ADR-0001).** For every simulated exposure event of every + simulated year, and for every treatment step with failure frequency above 0, the step + fails that day if a uniform random number is below failure frequency / 365. The same + random numbers are used for both cases and all reference pathogens. As a consequence, + raising a failure frequency only ever adds failure days, and worst-case and best-case see + the same failure days. +- **Worst-case on a failure day.** The event's LRV is the worst-case LRV of the train minus + the removable minimum LRVs of all steps failing that day. The failure duration is not + used. +- **Best-case on a failure day (mixed-water assumption).** The day is split into time + segments by which failing steps are down. The event's LRV is −log₁₀ of the time-weighted + average of 10^(−LRV of each segment), where a segment's LRV is the best-case LRV of the + train minus the removable maximum LRVs of the steps down in it. The failure events of the + day are arranged within 1,440 minutes so that this average concentration is lowest. This + gives no overlap when the durations fit into one day and otherwise the minimal overlap, + placed on the steps whose joint loss matters least (`CONTEXT.md`, *Combined failure*). For + a single failing step it equals Eq. 5 of the failure approach. With the few failing steps + per day that occur in practice, the arrangement can be found exactly, for example by + choosing the time share of each combination of failing steps. +- **Concentration sampling unchanged.** The sampled inflow concentrations and the choice of + which sample each simulated event uses stay exactly as today, including the fixed seed. + The failure draws come from their own fixed-seed random generator. When all failure + frequencies are 0, no failure draws are made and the results are identical to today's. +- **Dose and yearly risk.** Each event's dose follows from its sampled concentration, its + LRV (normal, or reduced as above) and the volume per event, and the yearly risk combines + the events of a simulated year as today. The summary statistics and the reference-level + exceedance (on the mean) are computed as today, now from the result including failures. +- **Saved results.** Saving an assessment recalculates and stores its results as today. + Results stored before C2 stay valid: every assessment in production has failure frequency + 0 at the release, so its stored results equal what C2 calculates. +- **Speed.** When failures are present, the dose-response is evaluated per simulated event + instead of per concentration sample. The work per failure day depends only on which steps + fail, so repeated combinations can be reused. The calculation time for a drinking-water + scenario (365 events per year) with failures is measured on the dev environment and + recorded in the pull request; the vision sets no response-time target yet. +- **Unchanged warnings.** The warning for a maximum LRV above 6 is not changed. +- **Delivery.** C2 is built on C1's pull request, stays open and is tested on the dev + environment; it is not merged into `main` before the release (D5). + +## Testing Decisions + +- **What a good test is.** A test gives the calculation a complete scenario (inflow + concentrations, treatment steps with failure inputs, exposure) and checks the returned + results: mean, median, quartiles, minimum and maximum per reference pathogen, for both + cases and both risk measures, and the reference-level exceedance. It does not inspect + intermediate arrays or random draws. +- **One seam: the calculation's public entry point**, called the way the existing + calculation tests call it. The result page, saving and export only pass results through, + and C1's HTTP tests already show that failure inputs arrive at the calculation. C2 adds no + HTTP tests. +- **Behaviour covered at that seam** (internal checks; the independent benchmark is C5): + - **No failures:** with every failure frequency at 0, all results equal today's exactly, + including the existing regression test's numbers. + - **Always failing, worst-case:** a step with failure frequency 365 gives the same + worst-case results as the scenario without that step's positive minimum LRVs. + - **Always failing, full day, best-case:** failure frequency 365 and duration 1,440 give + the same best-case results as the scenario without that step's positive maximum LRVs. + - **Always failing, part of the day, best-case:** failure frequency 365 and duration 60 + give the same best-case results as the scenario with the train's best-case LRV replaced + by the constant mixed LRV from Eq. 5, computed by hand in the test. + - **Combined failures:** two steps always failing with 900 + 900 minutes equal the + hand-computed constant LRV with 360 minutes of overlap; three steps always failing with + durations above a day in total equal the hand-computed lowest-concentration arrangement. + - **Monotonicity:** raising a failure frequency never lowers any mean; raising a failure + duration never lowers a best-case mean and leaves worst-case unchanged. + - **Case order:** worst-case mean ≥ best-case mean for every pathogen and risk measure, + with and without failures. + - **D4:** a step with a negative LRV for one pathogen group and a positive LRV for another + leaves the first group's results unchanged when it fails; a group with LRV 0 is + unaffected. + - **Reproducibility:** calculating the same scenario twice gives identical results. +- **Prior art.** The existing calculation tests: a scenario built from bundled data and + passed to the calculation, and the regression test comparing fixed numbers. +- **Manual check on the dev environment.** Enter a scenario with failures in the + configurator, run it as a guest and as a registered user, and confirm the result page, + the saved-assessments page and the export's result table show the changed results. Record + the calculation time. +- The existing test suite passes unchanged. + +## Acceptance criteria + +1. The calculation follows the rules above: failure days per exposure event, worst-case + full loss, best-case mixed-water assumption with the lowest-concentration arrangement, + and only positive LRVs lost. +2. With every failure frequency at 0, all results equal today's exactly. +3. All tests listed above pass, and the existing suite passes unchanged. +4. The result page, the reference-level exceedance, saved results, the assessment comparison + and the export's result table show the result including failures, with unchanged + layout (manual check on the dev environment). +5. The same scenario gives identical results on every calculation. +6. The calculation time with failures is measured and recorded in the pull request. + +Quality goals from `docs/vision.md`, one criterion each: + +| Quality goal | Criterion in C2 | +|---|---| +| Scientific correctness | The internal checks pass, including the closed-form cases (always failing steps compared with hand-computed LRVs) and the frequency-0 regression (criteria 2–3). Agreement with an independent calculation is C5. | +| Reproducibility | A scenario gives identical results on every calculation because the failure draws use their own fixed seed, and assessments without failures keep their exact results (criteria 2, 5). | +| Export stability | The export package keeps its files and columns; only the result values of assessments with failures change (criterion 4). | + +Not relevant to C2: *Input validity* (C1), *Traceability* (C3), *Privacy of registered-user +data* (no change to access). + +## Out of Scope + +- Failure inputs, their validation and storage (C1). +- Failure inputs in the export package and the HTML report (C3). +- Explanations for users in the FAQ, guided tour and documentation (C4). +- Comparison with an independent calculation and the benchmark scenarios (C5); the + tolerance (D1) is not needed here. +- Showing normal operation next to the result with failures (decided against). +- DALYs or inputs in the assessment comparison (later). +- Partial loss of removal (not planned). +- Recording the model version and simulation settings with results (reproducible assessment + snapshots, later). +- A response-time target (open question in the vision). + +## Further Notes + +- The rule for arranging three or more failure events of one day in best-case was decided + on 2026-09-24 while writing this Spec and added to `CONTEXT.md` (*Combined failure*) and + to decision 6 in `docs/QMRA_Failure_Approach.md`. Overlapping two failure events never + lowers the mixed concentration, so the lowest-concentration arrangement always has + minimal overlap and agrees with the rule for two steps. +- No contradiction with ADR-0001 was found. ADR-0001 notes that two exposure events on the + same calendar day need not share a failure state; this Spec keeps that. +- Results with failures will differ from a hand calculation with Eq. 6 of the failure + approach, as ADR-0001 explains; they agree on average. diff --git a/docs/templates/vision.md b/docs/templates/vision.md new file mode 100644 index 0000000..50cee08 --- /dev/null +++ b/docs/templates/vision.md @@ -0,0 +1,59 @@ + + +# Vision + +## Purpose + + + +## Users + + + +## Main workflows + + + +## Scope + + + +## Out of scope + + + +## MVP + + + +## Success criteria + + + +## Quality goals + + + +### + +- **Goal**: +- **Example situation**: +- **How we check it**: + +## Open questions + + diff --git a/docs/tickets/01-onboard-malte-to-ai-development-framework.md b/docs/tickets/01-onboard-malte-to-ai-development-framework.md new file mode 100644 index 0000000..6d6cccc --- /dev/null +++ b/docs/tickets/01-onboard-malte-to-ai-development-framework.md @@ -0,0 +1,27 @@ +# 01: Onboard Malte to the AI development framework + +**What to build:** Malte can work on the QMRA web app the way our AI development framework describes. In practice this means three things. He can log in to our shared development server (a Linux machine hosted at Hetzner, which we call the "Controlled server"). He can open the QMRA project folder there. And he can start Claude Code, the AI coding assistant, with his own login. At the end he also knows where to read how our way of working starts. + +We follow the framework's onboarding page step by step. His personal setup file in the framework already exists. + +**Blocked by:** None (can start immediately) + +**Status:** done + +Already done on the server (checked on 23 September 2026): + +- [x] His setup file exists in the framework. It lists the QMRA project and the full set of Claude skills (small add-ons that teach Claude our method). +- [x] His user account on the Controlled server exists. He logs in with an SSH key, a digital key pair used instead of a password. +- [x] He is in the project group for the QMRA web app. This group gives him access to the shared Git key and to the `controlled-dev-git` command used to pull and push code. +- [x] He has admin rights on the GitHub repository KWB-R/qmra-webapp. + +Done on 24 September 2026: + +- [x] Check that the QMRA project was copied into his home folder on the server, and that its CLAUDE.md file contains the note about `controlled-dev-git`. +- [x] Check that he can log in from his own computer with his server key, and that `sudo controlled-dev-git pull` works inside the project folder. +- [x] Sit with him for his first start of Claude Code. He chooses the subscription login. Claude shows a web address. We send it to Nico, who approves it and sends back a short code. Malte pastes that code into Claude. Plan this for a time when Nico is available, because our shared Claude subscription needs his approval. +- [x] Malte confirms the "Trust this folder" question himself the first time Claude opens the project. +- [x] Check that his Claude skills are installed and match his setup file. Always install them through the `manage-user.sh` script, not through the install script on its own. +- [x] Write the audit record: who did it, for whom, which project, what was done, when, and the result. +- [x] Walk him through the method overview and the "Project start" phase. Point him to the open pull request #17, which holds the current Vision and Glossary. +- [x] Run the environment check script `05-validate-environment.sh` afterwards to confirm that the account is set up correctly. diff --git a/docs/tickets/02-establish-product-vision-and-glossary.md b/docs/tickets/02-establish-product-vision-and-glossary.md new file mode 100644 index 0000000..dfcd810 --- /dev/null +++ b/docs/tickets/02-establish-product-vision-and-glossary.md @@ -0,0 +1,24 @@ +# 02: Establish the current product vision and glossary + +**What to build:** A short document, `docs/vision.md`, that describes the QMRA web app as it is today. It explains why the tool exists, who uses it, the main things users do with it, and which functions are available now. It describes the current product, and in a separate MVP section the next useful version (treatment failure). The purpose is to give the AI agents a reliable description of the existing product. We do not need to answer every open question at this stage. Open questions, contradictions and unfinished behaviour are listed in a short section at the end of the document. + +Next to the vision, a glossary: a short list of the key terms of the QMRA app with one clear meaning each, so that the vision and all future work use the same words. + +**Blocked by:** None (can start immediately) + +**Status:** done + +- [x] Malte wrote the first version of the vision. +- [x] Nico aligned the vision with the current code in the GitHub repository KWB-R/qmra-webapp. +- [x] Open questions and contradictions are listed at the end of the vision instead of being resolved now. +- [x] Nico created the glossary. +- [x] The glossary was moved into the file `CONTEXT.md` in the root folder of the repository. This is the place where our framework expects the project vocabulary. Each concept has one agreed term and a list of words to avoid. +- [x] The vision and the glossary were compared with the code. Where they disagreed, the points were written down in `docs/domain-model-open-questions.md` and settled on 23 September 2026. The code changes that follow from them are listed there as later work. +- [x] A review comment on the vision was addressed: the app already has a plot that compares the infection risk of several saved assessments, and the vision now describes it as an existing function. +- [x] The vision was restructured to follow the template `docs/templates/vision.md`: Purpose, Users, Main workflows, Scope, Out of scope, MVP, Success criteria, Quality goals and Open questions with owners. Technical details moved to `docs/architecture_baseline.md`. +- [x] Everything is published in pull request #17 (branch `docs-vision-architecture-glossary`). + +Follow-up work, not part of this ticket: + +- Merge pull request #17. +- Later code changes recorded in `docs/domain-model-open-questions.md`: rename "inflow" to "inflow concentration" in the code and the export file, and mark a bundled source water as "site-specific" once a user edits its values. diff --git a/docs/tickets/03-vision-rewrite-and-failure-roadmap.md b/docs/tickets/03-vision-rewrite-and-failure-roadmap.md new file mode 100644 index 0000000..c21a653 --- /dev/null +++ b/docs/tickets/03-vision-rewrite-and-failure-roadmap.md @@ -0,0 +1,29 @@ +# 03: Rewrite the vision to the template and draft the failure roadmap + +**What to build:** The vision document `docs/vision.md` follows the framework's vision template: Purpose, Users, Main workflows, Scope, Out of scope, MVP, Success criteria, Quality goals and Open questions with owners. The MVP is the next useful version of the app: treatment failure, including combined failures. Next to it, a roadmap `docs/failure_roadmap.md` lists the Changes that deliver this MVP, their size (Large with a Spec, or Small and built directly), their order, what can run in parallel, and the decisions that are still open and who owns them. + +**Blocked by:** None (can start immediately) + +**Status:** in-progress + +Done on 24 September 2026 (branch `docs-vision-architecture-glossary`, pull request #17): + +- [x] Vision restructured to the template. Technical details moved to `docs/architecture_baseline.md`. +- [x] Six quality goals written as goal, example situation and a check someone can run: scientific correctness, traceability, reproducibility, input validity, export stability, privacy of registered-user data. +- [x] MVP defined as treatment failure in four functionalities, following `docs/QMRA_Failure_Approach.md` and ADR-0001. +- [x] Open questions listed with owners, technical ones marked for Architecture. +- [x] Vision and architecture baseline use the glossary terms (best-case, worst-case, reference-level exceedance). +- [x] Failure roadmap drafted with four Changes (C1 failure inputs, C2 failure calculation, C3 export and report, C4 explanations), dependencies, sequence and four unresolved decisions. +- [x] Roadmap decisions revisited: C2 is built first and tested with internal checks; the comparison with an independent calculation became a fifth Change, C5 failure benchmark (Small), required before release. D4 settled: steps with at least one positive LRV accept failure inputs, and a failure removes only positive LRVs. Expected benchmark values come from an independent Monte Carlo script; domain experts sign off the C5 benchmark comparison before release. The vision's success criterion now covers results only. +- [x] ADR-0001 records why worst-case ignores the failure duration and that failure comparison covers infection risk only. It cites the vision's quality goals by their new names. +- [x] The vision template is copied into `docs/templates/`. + +Still to do: + +- [ ] Make the MVP list and the roadmap map one to one, as the template asks: today MVP items 1 and 2 both fall into C1, item 4 is split between C2 and C3, and C4 and C5 have no MVP item. +- [ ] Decide whether the roadmap file keeps the name `failure_roadmap.md` or becomes `docs/roadmap.md`, the name the framework's Roadmap stage expects. +- [ ] Fix the site-specific workflow in the vision: only registered users have personal definitions, guests do not. +- [ ] Replace "QMRA project team" in the ADR's Deciders line with names or roles. +- [ ] Create the Spec issues for C1 and C2 and fill in their numbers in the roadmap. +- [ ] Settle the three remaining open decisions in the roadmap: D1 benchmark tolerance, D2 who writes the independent benchmark script (ideally not the C2 author), D3 which one or two domain experts sign off the benchmark comparison. Owners: Wolfgang and Malte. None of them blocks the Specs of C1 and C2. +- [ ] Get pull request #17 reviewed and merged. diff --git a/docs/vision.md b/docs/vision.md new file mode 100644 index 0000000..84a6b3c --- /dev/null +++ b/docs/vision.md @@ -0,0 +1,146 @@ +# QMRA Vision + +## Purpose + +Quantitative microbial risk assessment is well established, but applying it to a real water system means assembling pathogen data, exposure assumptions, treatment removal, dose-response and health parameters, which most water professionals cannot do from scratch. QMRA is a web application that does this for them: a user describes a source water, a treatment train and an exposure, and gets the annual probability of infection and DALYs per person per year for three reference pathogens, compared with health-based reference levels. It supports risk assessment, scenario exploration, planning, teaching and communication in drinking water and water reuse. + +## Users + +- **Water-process and environmental engineers** need to compare treatment trains and see how treatment assumptions change the risk. +- **Utility operators and water-safety professionals** need to assess their own plant with local values instead of literature defaults. +- **Public-health specialists and regulators** need to review the inputs, assumptions and the comparison with reference levels without running the model themselves. +- **QMRA researchers and domain experts** need to inspect inputs, distributions and references, and check results against their own calculations. +- **Students and newcomers to QMRA** need a guided path with defaults and explanations to learn how a risk assessment works. + +All of them use the same functions. The only difference in access is between guests, who cannot save, and registered users. + +## Main workflows + +### Quick screening + +1. A guest or registered user opens the assessment configurator. +2. They select a bundled source water, bundled treatment steps and a bundled exposure. +3. The application runs the Monte Carlo simulation and shows, per reference pathogen, the risk distributions of both risk measures in best-case and worst-case, with the reference-level exceedance. For a guest nothing is stored. + +### Site-specific assessment + +1. A guest or registered user starts from bundled data or their personal definitions. +2. They replace values with local ones: inflow concentrations (which makes the source water a site-specific source water), LRVs of treatment steps, and exposure events per year and volume per event. Recontamination is entered as a treatment step with a negative LRV. +3. The results are the same as in quick screening. The application does not check whether the entered values represent the site. + +### Save, reopen and export + +1. A registered user saves an assessment with a name and description. +2. They can list, reopen, edit and delete saved assessments. +3. For a saved assessment they download an export package: a ZIP with input tables, a result table, an HTML report and plots. + +### Personal definitions + +1. A registered user creates a reusable source water, exposure or treatment step. +2. The configurator offers it next to the bundled default data in every later assessment. + +### Compare scenarios + +1. A registered user selects several saved assessments on the saved-assessments page. +2. The assessment comparison shows one plot: the annual probability of infection per reference pathogen for each selected assessment, against the health-based reference level. DALYs and inputs are not compared. + +## Scope + +- Microbial risk in drinking water and water reuse, for three reference pathogens: Rotavirus, Campylobacter jejuni and Cryptosporidium parvum. +- Steady-state scenarios: inflow concentrations given as minimum and maximum, a treatment train whose LRVs are summed per pathogen group, and an exposure given as events per year and volume per event. +- A Monte Carlo simulation in two cases, best-case (maximum LRVs) and worst-case (minimum LRVs), reporting risk distributions with minimum, quartiles, median and maximum. +- Two risk measures, each compared with its health-based reference level: 1 × 10⁻⁴ infections per person per year and 1 × 10⁻⁶ DALYs per person per year. +- Bundled default data with references, personal definitions and site-specific inputs. +- Guest use without saving; saving, comparison and export for registered users. The tool is free to use. +- A warning when a treatment step's maximum LRV is above 6. The calculation still runs. + +Known limitations of the current product: + +- Uncertainty is represented only through inflow concentration ranges, random sampling and the spread between best-case and worst-case, not through a complete uncertainty model. +- Provenance is incomplete: bundled data carries references, but personal definitions and site-specific inputs do not. +- Saved assessments and export packages do not record the model version, data release or simulation settings, so a result cannot be reproduced exactly later. +- The assessment comparison covers annual probability of infection only. + +## Out of scope + +- Treatment process design and sizing, the order of treatment steps, and interactions between steps beyond the sum of their LRVs. +- Chemical and physical hazards. +- Conditions that change within a year, such as varying inflow concentrations or operating conditions. Results are yearly, never daily. +- Partial loss of removal in a treatment step. Treatment failure, once modelled, means the entire loss of a step's LRV. +- Proof of legal compliance. The health-based reference levels are comparison values, not legal limits, and results must not be the only basis for treatment design, regulatory or public-health decisions. + +## MVP + +The current application is the base. The next useful version adds **treatment failure**, including combined failures, following `docs/QMRA_Failure_Approach.md` and ADR-0001. It serves utility operators and engineers who want to know how failures of their treatment steps affect the yearly risk. + +Functionalities, each one Change: + +1. **Failure inputs per treatment step.** In the assessment configurator, each treatment step with at least one positive LRV gets a failure frequency (failure days per year, 0–365, default 0) and a failure duration (minutes, 1–1440, default 30). The inputs belong to the scenario. +2. **Failure inputs on personal treatment steps.** A personal treatment step can store its failure frequency and failure duration for reuse. +3. **Calculation with failure days.** Inside the Monte Carlo simulation, each exposure event falls on a failure day of each step with probability failure frequency / 365. Worst-case loses the full LRV of every failing step; best-case applies the mixed-water assumption, with combined failures overlapping as little as possible. +4. **Results and export including failures.** The result page, the reference-level exceedance and the export package show the result including failures, and the export records the failure inputs of each step. + +Waits until later: + +- Extended assessment comparison with DALYs and inputs. Until then, the DALY effect of different failure inputs is read from each assessment's own result page. +- Reproducible assessment snapshots. +- Handling of historical assessments after model or data changes. +- Extended interfaces: a calculation-engine boundary, public API, batch, notebook or command-line use. +- The naming follow-ups recorded in `docs/domain-model-open-questions.md`. + +## Success criteria + +- A water utility can assess the effect of its own failure frequencies on the yearly risk, for both risk measures, without help from the development team. +- Results for the bundled benchmark scenarios match the reference calculation within an agreed tolerance. +- Domain experts at KWB accept the results as defensible for use in projects. + +## Quality goals + +### Scientific correctness + +- **Goal**: calculated results match an independent reference calculation. +- **Example situation**: a domain expert recalculates the worst-case Cryptosporidium risk of a bundled scenario in a spreadsheet and gets a different median. +- **How we check it**: version-controlled benchmark scenarios with expected results from an independent calculation; regression tests run on every change and fail outside the agreed tolerance. + +### Traceability + +- **Goal**: for every input of an assessment, a reader can see whether it is bundled default data, a personal definition or a site-specific input, and for bundled data which reference it comes from. +- **Example situation**: a reviewer at a health authority asks where the Rotavirus concentration of a "site-specific groundwater" came from. +- **How we check it**: the export package lists the origin of each input and the reference of each bundled value; a test checks that every exported input has an origin and every bundled value a reference. + +### Reproducibility + +- **Goal**: a saved assessment gives the same result every time, and can be recalculated later under the same model and data. +- **Example situation**: a year after a report, a utility reopens an assessment after a model update and gets different numbers without knowing why. +- **How we check it**: each saved assessment and export package records the model version, data release and simulation settings; a test recalculates stored benchmark assessments and compares them with their recorded results. + +### Input validity + +- **Goal**: every input shows its unit and is checked against a plausible range; invalid values are rejected with a clear message. +- **Example situation**: a user enters 2000 as volume per event, thinking in millilitres. +- **How we check it**: form tests per input field for the unit label, the lower and upper bounds (for example failure frequency 0–365, failure duration 1–1440) and minimum ≤ maximum. + +### Export stability + +- **Goal**: the content and layout of the export package change only on purpose. +- **Example situation**: a utility's script that reads the result table breaks after an update renamed a column. +- **How we check it**: a test compares the export package of a benchmark assessment with a stored reference export; changing it requires updating the reference and noting it in the release notes. + +### Privacy of registered-user data + +- **Goal**: a registered user's saved assessments, export packages and personal definitions are accessible only to that user. +- **Example situation**: a logged-in user changes the assessment ID in the URL to one belonging to another user. +- **How we check it**: tests request every assessment, export and personal-definition URL as a different user and expect a refusal. + +## Open questions + +- Should model and data releases get explicit identifiers and change logs, attached to every saved assessment and export package? (Wolfgang, Malte; for Architecture) +- What must be stored so that a saved assessment can be reproduced from the same model, data, configuration and simulation settings? (Wolfgang, Malte; for Architecture) +- Should the calculation engine be separated behind a stable internal boundary? (Wolfgang, Malte; for Architecture) +- Is there a need for public API access, batch execution, notebook or command-line use, or integration with other digital-water platforms? (Wolfgang, Malte) +- Which project data is confidential or commercially sensitive even when it is not personal data, and what controls do saved assessments need? (Wolfgang, Malte) +- How should historical assessments behave when the model, data or assessment structure changes: stay frozen, be migrated, or be recalculated under a chosen version? (Wolfgang, Malte) +- Beyond DALYs, which comparisons of saved assessments are needed: inputs, best-case and worst-case side by side, saved comparisons? (Wolfgang, Malte) +- What tolerance counts as a match for the benchmark scenarios, and which domain experts accept the results? (Wolfgang, Malte) +- Where does the rule "maximum LRV above 6" come from, and should it warn or cap? See `docs/domain-model-open-questions.md`. (Wolfgang, Malte) +- Which response-time and reliability targets apply, and which deployment controls, backups, retention rules and incident-response processes are required for production use? (Wolfgang, Malte; for Architecture)