health.qrisk-inputs-validate Unreviewed
Check a QRISK3 cardiovascular risk input set against the calculator's published ranges; does not compute risk.
1.0.1 · published 2026-10-03 by charlie · Anterra
Pinned by 29 tests, run in TypeScript, Python and Rust.
Unreviewed. Not for clinical use; regulatory review pending. This capability’s implementations agree in every language and pass its published test vectors, which were worked out from the official sources cited. But no qualified primary-care clinician has yet checked those vectors, or confirmed that the capability covers the cases it claims. Treat it as a draft. Do not use it for real people, money or decisions without your own expert review. Once a qualified reviewer signs off, this notice is replaced with their name, qualification and the date. Each new version needs fresh sign-off.
Not professional advice. This capability calculates health figures from published rules. It is a software component for developers, not medical advice. Rules change and every rate here has an effective date. Check that the dates cover your case. Verify results against the official sources listed in its README, and have a primary-care clinician review how you use it, before anyone relies on the output. Provided “as is” under its licence, without warranty.
Not a medical device. It is not intended to diagnose, treat or support clinical decisions about any individual. Anyone building it into clinical software is responsible for that software’s regulatory status, and must validate it under their own clinical governance.
What it does
Status: needs review and sign-off by a qualified clinician before it is published. Not a medical device; for decision support only; always follow local clinical guidelines.
Checks that a set of inputs is fit to be given to the QRISK3 cardiovascular risk calculator. **It does NOT compute risk.** QRISK3 itself is a licensed algorithm (ClinRisk / Endeavour Predict); use an accredited implementation for the score. This capability catches the data problems before the call: missing required values, values outside the ranges the calculator accepts, a height without a weight, and people QRISK3 is not valid for.
For example
validate_qrisk_inputs(age 64, sex male, ethnicity indian, postcode SW1A 1AA, smoking ex-smoker, diabetes type-2, family history cvd true, chronic kidney disease false, atrial fibrillation false, blood …)→ valid true, errors a complete, valid input setvalidate_qrisk_inputs(age 40, sex female, ethnicity white-or-not-stated, postcode —, smoking non-smoker, diabetes none, family history cvd false, chronic kidney disease false, atrial fibrillation false…)→ valid true, errors the minimum: required fields only, every optional one nullvalidate_qrisk_inputs(age 25, sex female, ethnicity white-or-not-stated, postcode —, smoking non-smoker, diabetes none, family history cvd false, chronic kidney disease false, atrial fibrillation false…)→ valid true, errors age 25 is the youngest accepted
The function
The same function in TypeScript, Python and Rust, pinned by the same tests. Pick your language; the choice follows you around the registry.
pub fn validate_qrisk_inputs(inputs: &QriskInputs) -> QriskInputsValidation
| inputs | QriskInputs | the values a QRISK3 calculation would be given; null where not recorded |
| returns | QriskInputsValidation | every problem found, in field order; valid when there are none |
The types it declares, generated into your project
/// The QRISK3 input set, as raw recorded values to be checked.
#[derive(Debug, Clone, PartialEq)]
pub struct QriskInputs {
/// years; required, 25 to 84
pub age: Option<i64>,
/// female or male; required
pub sex: Option<String>,
/// white-or-not-stated, indian, pakistani, bangladeshi, chinese, other-asian, black-caribbean, black-african or other; required
pub ethnicity: Option<String>,
/// UK postcode; optional
pub postcode: Option<String>,
/// non-smoker, ex-smoker, light (under 10 a day), moderate (10 to 19) or heavy (20 or more); required
pub smoking: Option<String>,
/// none, type-1 or type-2; required
pub diabetes: Option<String>,
/// angina or heart attack in a 1st degree relative
pub family_history_cvd: bool,
/// CKD stage 3, 4 or 5
pub chronic_kidney_disease: bool,
pub atrial_fibrillation: bool,
/// on treatment for high blood pressure
pub blood_pressure_treatment: bool,
pub migraine: bool,
pub rheumatoid_arthritis: bool,
/// systemic lupus erythematosus (SLE)
pub systemic_lupus: bool,
/// schizophrenia, bipolar disorder or moderate/severe depression
pub severe_mental_illness: bool,
/// on atypical antipsychotic medication
pub atypical_antipsychotic: bool,
/// on regular steroid tablets
pub corticosteroids: bool,
/// a diagnosis of or treatment for erectile dysfunction
pub erectile_dysfunction: bool,
/// total cholesterol / HDL; optional, 1.0 to 11.0
pub cholesterol_hdl_ratio: Option<f64>,
/// mmHg; optional, 70 to 210
pub systolic_bp: Option<i64>,
/// standard deviation of at least two recent systolic readings; optional, 0.0 to 40.0
pub systolic_bp_sd: Option<f64>,
/// whole centimetres; optional with weight, 140 to 210
pub height_cm: Option<i64>,
/// whole kilograms; optional with height, 40 to 180
pub weight_kg: Option<i64>,
/// already diagnosed with coronary heart disease, stroke or TIA
pub existing_cvd: bool,
/// already taking a statin
pub on_statins: bool,
}
/// One problem with one field.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct QriskInputError {
/// the QriskInputs field name
pub field: String,
/// required, out-of-range, unknown-value, incomplete-pair, bad-postcode or not-applicable
pub reason: String,
/// plain English, in the calculator's own wording where it has one
pub message: String,
}
/// Whether the set can be given to QRISK3, and if not, why.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct QriskInputsValidation {
pub valid: bool,
/// empty when valid
pub errors: Vec<QriskInputError>,
}
Your code names it in one line, in the file that uses it
fune!(health.qrisk-inputs-validate@^1); // then call validate_qrisk_inputs(…)
Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.
use super::funejson::Value; ← the fune runtime: the JSON value the test vectors use; fune build keeps it only where a signature takes one
use super::validation_uk_postcode::validate_uk_postcode; ← from validation.uk-postcode ^2.0.0 · built alongside by fune
const SEXES: [&str; 2] = ["female", "male"];
const ETHNICITIES: [&str; 9] = [
"white-or-not-stated",
"indian",
"pakistani",
"bangladeshi",
"chinese",
"other-asian",
"black-caribbean",
"black-african",
"other",
];
const SMOKING: [&str; 5] = ["non-smoker", "ex-smoker", "light", "moderate", "heavy"];
const DIABETES: [&str; 3] = ["none", "type-1", "type-2"];
fn error(field: &str, reason: &str, message: &str) -> QriskInputError {
QriskInputError {
field: field.to_string(),
reason: reason.to_string(),
message: message.to_string(),
}
}
fn choice(errors: &mut Vec<QriskInputError>, field: &str, value: &Option<String>, allowed: &[&str], label: &str) {
let message = format!("{}: enter one of {}", label, allowed.join(", "));
match value {
None => errors.push(error(field, "required", &message)),
Some(v) if !allowed.contains(&v.as_str()) => errors.push(error(field, "unknown-value", &message)),
_ => {}
}
}
/// Check a QRISK3 input set against the calculator's published input
/// definitions (qrisk.org form and its validation). It reports every problem,
/// in field order, and never computes a risk.
pub fn validate_qrisk_inputs(inputs: &QriskInputs) -> QriskInputsValidation {
let mut errors: Vec<QriskInputError> = Vec::new();
let age_message = "Age: enter an integer between 25 and 84";
match inputs.age {
None => errors.push(error("age", "required", age_message)),
Some(a) if !(25..=84).contains(&a) => errors.push(error("age", "out-of-range", age_message)),
_ => {}
}
choice(&mut errors, "sex", &inputs.sex, &SEXES, "Sex");
choice(&mut errors, "ethnicity", &inputs.ethnicity, ÐNICITIES, "Ethnicity");
// Blank means unknown, as on the calculator ("leave blank if unknown").
if let Some(p) = &inputs.postcode {
if !p.trim().is_empty() && !validate_uk_postcode(p).valid {
errors.push(error(
"postcode",
"bad-postcode",
"Postcode: the postcode is not recognised. Please re-enter or leave blank.",
));
}
}
choice(&mut errors, "smoking", &inputs.smoking, &SMOKING, "Smoking status");
choice(&mut errors, "diabetes", &inputs.diabetes, &DIABETES, "Diabetes status");
if let Some(r) = inputs.cholesterol_hdl_ratio {
if !(1.0..=11.0).contains(&r) {
errors.push(error(
"cholesterolHdlRatio",
"out-of-range",
"Cholesterol/HDL ratio: either leave blank or enter a number between 1.0 and 11.0.",
));
}
}
if let Some(s) = inputs.systolic_bp {
if !(70..=210).contains(&s) {
errors.push(error(
"systolicBp",
"out-of-range",
"Systolic blood pressure: either leave blank or enter an integer between 70 and 210",
));
}
}
if let Some(sd) = inputs.systolic_bp_sd {
if !(0.0..=40.0).contains(&sd) {
errors.push(error(
"systolicBpSd",
"out-of-range",
"Standard deviation of SBP: either leave blank or enter a value between 0.0 and 40.0",
));
}
}
let pair = "You have only entered one of height and weight: please enter both or leave both blank.";
if inputs.height_cm.is_none() && inputs.weight_kg.is_some() {
errors.push(error("heightCm", "incomplete-pair", pair));
}
if let Some(h) = inputs.height_cm {
if !(140..=210).contains(&h) {
errors.push(error("heightCm", "out-of-range", "Height: enter an integer between 140 and 210"));
}
}
if inputs.weight_kg.is_none() && inputs.height_cm.is_some() {
errors.push(error("weightKg", "incomplete-pair", pair));
}
if let Some(w) = inputs.weight_kg {
if !(40..=180).contains(&w) {
errors.push(error("weightKg", "out-of-range", "Weight: enter an integer between 40 and 180"));
}
}
if inputs.existing_cvd {
errors.push(error(
"existingCvd",
"not-applicable",
"QRISK3 is only valid for people without a diagnosis of coronary heart disease (including angina or heart attack) or stroke/transient ischaemic attack.",
));
}
if inputs.on_statins {
errors.push(error("onStatins", "not-applicable", "QRISK3 is only valid for people who are not on statins."));
}
QriskInputsValidation {
valid: errors.is_empty(),
errors,
}
}
pub fn qrisk_inputs_validation_to_value(result: &QriskInputsValidation) -> Value {
Value::obj(vec![
("valid", Value::Bool(result.valid)),
(
"errors",
Value::Arr(
result
.errors
.iter()
.map(|e| {
Value::obj(vec![
("field", Value::str(&e.field)),
("reason", Value::str(&e.reason)),
("message", Value::str(&e.message)),
])
})
.collect(),
),
),
])
}
/// The adapter refuses a value the typed struct cannot hold, with the wording
/// TypeScript and Python use, instead of letting a conversion change it.
fn whole_or_none(v: &Value, name: &str) -> Option<i64> {
match v {
Value::Null => None,
Value::Int(i) => Some(*i),
Value::Float(f) if f.is_finite() && f.fract() == 0.0 => Some(*f as i64),
_ => panic!("{} must be a whole number or null, received {:?}", name, v),
}
}
fn number_or_none(v: &Value, name: &str) -> Option<f64> {
match v {
Value::Null => None,
Value::Int(_) | Value::Float(_) => Some(v.as_f64()),
_ => panic!("{} must be a number or null, received {:?}", name, v),
}
}
fn text_or_none(v: &Value, name: &str) -> Option<String> {
match v {
Value::Null => None,
Value::Str(s) => Some(s.clone()),
_ => panic!("{} must be text or null, received {:?}", name, v),
}
}
pub fn qrisk_inputs_from_value(v: &Value) -> QriskInputs {
QriskInputs {
age: whole_or_none(v.get("age"), "age"),
sex: text_or_none(v.get("sex"), "sex"),
ethnicity: text_or_none(v.get("ethnicity"), "ethnicity"),
postcode: text_or_none(v.get("postcode"), "postcode"),
smoking: text_or_none(v.get("smoking"), "smoking"),
diabetes: text_or_none(v.get("diabetes"), "diabetes"),
family_history_cvd: v.get("familyHistoryCvd").as_bool(),
chronic_kidney_disease: v.get("chronicKidneyDisease").as_bool(),
atrial_fibrillation: v.get("atrialFibrillation").as_bool(),
blood_pressure_treatment: v.get("bloodPressureTreatment").as_bool(),
migraine: v.get("migraine").as_bool(),
rheumatoid_arthritis: v.get("rheumatoidArthritis").as_bool(),
systemic_lupus: v.get("systemicLupus").as_bool(),
severe_mental_illness: v.get("severeMentalIllness").as_bool(),
atypical_antipsychotic: v.get("atypicalAntipsychotic").as_bool(),
corticosteroids: v.get("corticosteroids").as_bool(),
erectile_dysfunction: v.get("erectileDysfunction").as_bool(),
cholesterol_hdl_ratio: number_or_none(v.get("cholesterolHdlRatio"), "cholesterolHdlRatio"),
systolic_bp: whole_or_none(v.get("systolicBp"), "systolicBp"),
systolic_bp_sd: number_or_none(v.get("systolicBpSd"), "systolicBpSd"),
height_cm: whole_or_none(v.get("heightCm"), "heightCm"),
weight_kg: whole_or_none(v.get("weightKg"), "weightKg"),
existing_cvd: v.get("existingCvd").as_bool(),
on_statins: v.get("onStatins").as_bool(),
}
}
pub fn fune_vector(args: &[Value]) -> Value {
qrisk_inputs_validation_to_value(&validate_qrisk_inputs(&qrisk_inputs_from_value(&args[0])))
}Install
fune build
With that line in your source, in a Rust project (language rust in fune.project), fune build resolves it and its 1 dependency, pins them in fune.lock, downloads only the Rust package of each, and builds the code above into your project’s .fune/build, one readable file per capability with a header linking back here. A crate’s build.rs runs it before every compile. Or pin a range in fune.project and build in one step:
fune add health.qrisk-inputs-validate
The manifest, vectors and README with only the Rust implementation. Install it without the registry with fune add ./health.qrisk-inputs-validate-1.0.1-rust.fune, or fetch it from a terminal with fune pull health.qrisk-inputs-validate@1.0.1:rust.
The whole function, every language, is one file too: health.qrisk-inputs-validate-1.0.1.fune, 63,589 bytes, sha256 60ced6a1a2baa49117e7b29dcfa24a8b99cc406022f04baaf04502da74e7e2f7. It installs into a project of any language.
Customise it in your app
The seams this capability offers. Put a marker directly above a function of your own and fune build wires it into the built code; the package on the registry is not changed, the built file’s header lists it under CUSTOMISED, and fune hooks lists every hook in the project. How hooks work.
before — your function gets the arguments and returns them, changed or not, or throws to refuse the call.
// fune: before health.qrisk-inputs-validate
after — your function gets the result and the arguments, and returns the final result.
// fune: after health.qrisk-inputs-validate
replace — inside this capability’s code only, calls to a dependency go to your function, with the same signature. Other capabilities that use it are unaffected; write in * to replace it everywhere.
// fune: replace validation.uk-postcode in health.qrisk-inputs-validate
step — your function runs at a numbered point inside the function’s body, receives the in-scope values it names as parameters, and may return replacements. List the points with fune show health.qrisk-inputs-validate --steps.
// fune: step health.qrisk-inputs-validate after <n|label>
Tests
A version published now needs at least 8 tests for every function, and one that expects the error for each function that throws; the registry refuses it otherwise. fune verify --all runs each case in TypeScript, Python and Rust, and a project runs them again with fune verify. This page lists the cases; it does not run them. The exact JSON is vectors.json.
| Case | Arguments | Expected | |
|---|---|---|---|
| a complete, valid input set | age 64, sex male, ethnicity indian, postcode SW1A 1AA, smoking ex-smoker, diabetes type-2, family history cvd true, chronic kidney disease false, atrial fibrillation false, blood … | → | valid true, errors |
| the minimum: required fields only, every optional one null | age 40, sex female, ethnicity white-or-not-stated, postcode —, smoking non-smoker, diabetes none, family history cvd false, chronic kidney disease false, atrial fibrillation false… | → | valid true, errors |
| age 25 is the youngest accepted | age 25, sex female, ethnicity white-or-not-stated, postcode —, smoking non-smoker, diabetes none, family history cvd false, chronic kidney disease false, atrial fibrillation false… | → | valid true, errors |
| age 84 is the oldest accepted | age 84, sex female, ethnicity white-or-not-stated, postcode —, smoking non-smoker, diabetes none, family history cvd false, chronic kidney disease false, atrial fibrillation false… | → | valid true, errors |
| age 24 is out of range | age 24, sex female, ethnicity white-or-not-stated, postcode —, smoking non-smoker, diabetes none, family history cvd false, chronic kidney disease false, atrial fibrillation false… | → | valid false, errors ×1 |
| age 85 is out of range | age 85, sex female, ethnicity white-or-not-stated, postcode —, smoking non-smoker, diabetes none, family history cvd false, chronic kidney disease false, atrial fibrillation false… | → | valid false, errors ×1 |
| missing age and sex are both required | age —, sex —, ethnicity white-or-not-stated, postcode —, smoking non-smoker, diabetes none, family history cvd false, chronic kidney disease false, atrial fibrillation false, bloo… | → | valid false, errors ×2 |
| an ethnicity code outside the nine is unknown | age 40, sex female, ethnicity asian, postcode —, smoking non-smoker, diabetes none, family history cvd false, chronic kidney disease false, atrial fibrillation false, blood pressu… | → | valid false, errors ×1 |
| a smoking description rather than its code is unknown | age 40, sex female, ethnicity white-or-not-stated, postcode —, smoking light smoker (less than 10), diabetes none, family history cvd false, chronic kidney disease false, atrial f… | → | valid false, errors ×1 |
| a postcode in lower case without its space is valid | age 40, sex female, ethnicity white-or-not-stated, postcode sw1a1aa, smoking non-smoker, diabetes none, family history cvd false, chronic kidney disease false, atrial fibrillation… | → | valid true, errors |
Show the other 19 tests
| Case | Arguments | Expected | |
|---|---|---|---|
| a blank postcode means unknown, as on the calculator | age 40, sex female, ethnicity white-or-not-stated, postcode , smoking non-smoker, diabetes none, family history cvd false, chronic kidney disease false, atrial fibrillation fals… | → | valid true, errors |
| a postcode that cannot exist is reported | age 40, sex female, ethnicity white-or-not-stated, postcode QQ1 1QQ, smoking non-smoker, diabetes none, family history cvd false, chronic kidney disease false, atrial fibrillation… | → | valid false, errors ×1 |
| cholesterol/HDL ratio 1.0 and SD 40.0 are the inclusive limits | age 40, sex female, ethnicity white-or-not-stated, postcode —, smoking non-smoker, diabetes none, family history cvd false, chronic kidney disease false, atrial fibrillation false… | → | valid true, errors |
| cholesterol/HDL ratio 11.0 and SD 0.0 are accepted | age 40, sex female, ethnicity white-or-not-stated, postcode —, smoking non-smoker, diabetes none, family history cvd false, chronic kidney disease false, atrial fibrillation false… | → | valid true, errors |
| cholesterol/HDL ratio 0.9 is out of range | age 40, sex female, ethnicity white-or-not-stated, postcode —, smoking non-smoker, diabetes none, family history cvd false, chronic kidney disease false, atrial fibrillation false… | → | valid false, errors ×1 |
| cholesterol/HDL ratio 11.1 is out of range | age 40, sex female, ethnicity white-or-not-stated, postcode —, smoking non-smoker, diabetes none, family history cvd false, chronic kidney disease false, atrial fibrillation false… | → | valid false, errors ×1 |
| systolic 210 is the highest accepted | age 40, sex female, ethnicity white-or-not-stated, postcode —, smoking non-smoker, diabetes none, family history cvd false, chronic kidney disease false, atrial fibrillation false… | → | valid true, errors |
| systolic 69 is out of range | age 40, sex female, ethnicity white-or-not-stated, postcode —, smoking non-smoker, diabetes none, family history cvd false, chronic kidney disease false, atrial fibrillation false… | → | valid false, errors ×1 |
| systolic 211 is out of range | age 40, sex female, ethnicity white-or-not-stated, postcode —, smoking non-smoker, diabetes none, family history cvd false, chronic kidney disease false, atrial fibrillation false… | → | valid false, errors ×1 |
| SD of systolic 40.1 is out of range | age 40, sex female, ethnicity white-or-not-stated, postcode —, smoking non-smoker, diabetes none, family history cvd false, chronic kidney disease false, atrial fibrillation false… | → | valid false, errors ×1 |
| height without weight is an incomplete pair | age 40, sex female, ethnicity white-or-not-stated, postcode —, smoking non-smoker, diabetes none, family history cvd false, chronic kidney disease false, atrial fibrillation false… | → | valid false, errors ×1 |
| weight without height is reported against height | age 40, sex female, ethnicity white-or-not-stated, postcode —, smoking non-smoker, diabetes none, family history cvd false, chronic kidney disease false, atrial fibrillation false… | → | valid false, errors ×1 |
| height 139 and weight 181 are both out of range | age 40, sex female, ethnicity white-or-not-stated, postcode —, smoking non-smoker, diabetes none, family history cvd false, chronic kidney disease false, atrial fibrillation false… | → | valid false, errors ×2 |
| height in metres is out of range, not silently converted | age 40, sex female, ethnicity white-or-not-stated, postcode —, smoking non-smoker, diabetes none, family history cvd false, chronic kidney disease false, atrial fibrillation false… | → | valid false, errors ×1 |
| an existing CVD diagnosis makes QRISK3 not applicable | age 64, sex male, ethnicity indian, postcode SW1A 1AA, smoking ex-smoker, diabetes type-2, family history cvd true, chronic kidney disease false, atrial fibrillation false, blood … | → | valid false, errors ×1 |
| taking a statin makes QRISK3 not applicable | age 64, sex male, ethnicity indian, postcode SW1A 1AA, smoking ex-smoker, diabetes type-2, family history cvd true, chronic kidney disease false, atrial fibrillation false, blood … | → | valid false, errors ×1 |
| several problems at once come back in field order | age 90, sex male, ethnicity —, postcode SW1A 1AA, smoking ex-smoker, diabetes type 2, family history cvd true, chronic kidney disease false, atrial fibrillation false, blood press… | → | valid false, errors ×6 |
| a fractional age is a type error, not a clinical one | age 40.5, sex female, ethnicity white-or-not-stated, postcode —, smoking non-smoker, diabetes none, family history cvd false, chronic kidney disease false, atrial fibrillation fal… | → | error: age must be a whole number or null |
| a height with a fraction is refused: the calculator takes whole centimetres | age 40, sex female, ethnicity white-or-not-stated, postcode —, smoking non-smoker, diabetes none, family history cvd false, chronic kidney disease false, atrial fibrillation false… | → | error: heightCm must be a whole number or null |
More from the author
Every problem is reported, not just the first, in the order of the `QriskInputs` fields and then `existingCvd` and `onStatins`, each with the field name, a reason code and a message in the calculator's own wording where it has one. The result is `valid` only when there are none.
## The published input definitions
From the QRISK3 calculator form at https://qrisk.org and its client-side validation script https://qrisk.org/form.v003.js:
| Field | Required | Accepted | |---|---|---| | age | yes | whole years, 25 to 84 | | sex | yes | `female`, `male` | | ethnicity | yes | `white-or-not-stated`, `indian`, `pakistani`, `bangladeshi`, `chinese`, `other-asian`, `black-caribbean`, `black-african`, `other` | | postcode | no ("leave blank if unknown") | a valid UK postcode (checked with validation.uk-postcode) | | smoking | yes | `non-smoker`, `ex-smoker`, `light` (under 10 a day), `moderate` (10 to 19), `heavy` (20 or more) | | diabetes | yes | `none`, `type-1`, `type-2` | | cholesterolHdlRatio | no | 1.0 to 11.0 | | systolicBp | no | whole mmHg, 70 to 210 | | systolicBpSd | no | 0.0 to 40.0 (SD of at least two recent systolic readings) | | heightCm, weightKg | no, but both or neither | whole cm 140 to 210; whole kg 40 to 180 |
The eleven yes/no conditions (family history, CKD 3-5, AF, treated hypertension, migraine, rheumatoid arthritis, SLE, severe mental illness, atypical antipsychotics, regular steroid tablets, erectile dysfunction) are booleans and cannot be invalid.
The calculator states that it "is only valid if you do not already have a diagnosis of coronary heart disease (including angina or heart attack) or stroke/transient ischaemic attack, and not on statins", so `existingCvd` or `onStatins` being true is reported as `not-applicable`.
The codes for the categorical fields are this capability's own lower-case spellings of the calculator's options; the fields are plain strings, not enums, because the point is to check raw recorded values.
## Edge cases and decisions
- **Height and weight are whole numbers** in the published form ("enter an integer"). Round a recorded 172.5 cm before calling; a fractional height is refused with an error (it is a type error, thrown in every language, not a reported clinical problem), as is a fractional age or systolic pressure. A whole number sent as 170.0 is accepted. - **A lone height or weight** is reported against the missing one, with the calculator's "only entered one of height and weight" message. - **A blank postcode** (empty or spaces) is treated as unknown, as on the form. - **Units are not converted.** A height of 2 (metres) is out of range, not 200 cm. A total cholesterol typed into the ratio field cannot be told apart from a ratio if it is between 1 and 11: that is for the caller. - **Erectile dysfunction** is, as far as we know, a term of the male QRISK3 equation only (BMJ 2017; the paper's tables were not re-read for this package); the calculator's form does not reject it for women and neither does this. - The ranges are the calculator's input limits, not clinical normal ranges.
## Sources
- QRISK3 risk calculator, https://qrisk.org, and its form validation https://qrisk.org/form.v003.js (both read September 2026). - Hippisley-Cox J, Coupland C, Brindle P. Development and validation of QRISK3 risk prediction algorithms to estimate future risk of cardiovascular disease: prospective cohort study. BMJ 2017;357:j2099.
## Before you rely on this
**Not professional advice.** This capability calculates health figures from published rules. It is a software component for developers, not medical advice. Rules change and every rate here has an effective date. Check that the dates cover your case. Verify results against the official sources listed above, and have a primary-care clinician review how you use it, before anyone relies on the output. Provided "as is" under its licence, without warranty.
**Not a medical device.** It is not intended to diagnose, treat or support clinical decisions about any individual. Anyone building it into clinical software is responsible for that software's regulatory status, and must validate it under their own clinical governance.
**Not for clinical use; regulatory review pending.** Whether publishing this capability makes it a medical device is under regulatory review. Until that is settled it is a developer library only.
**Unreviewed.** This capability's implementations agree in every language and pass its published test vectors, which were worked out from the official sources cited. But no qualified primary-care clinician has yet checked those vectors, or confirmed that the capability covers the cases it claims. Treat it as a draft. Do not use it for real people, money or decisions without your own expert review. Once a qualified reviewer signs off, this notice is replaced with their name, qualification and the date. Each new version needs fresh sign-off.
## Notices
QRISK® is a registered trademark of ClinRisk Ltd; this capability is not affiliated with or endorsed by ClinRisk.
1.0.1 marks it unreviewed (not for clinical use; regulatory review pending) and adds its attribution notices (NOTICE). The code and the tests are unchanged.
Files
| Path | Bytes |
|---|---|
| NOTICE | 145 |
| README.md | 5,890 |
| impl/python.py | 5,486 |
| impl/rust.rs | 8,084 |
| impl/typescript.ts | 5,095 |
| vectors.json | 27,980 |