Contracts and risk
averageScore divides a candidate's total marks by the number of attempts, and requires that attempts is more than zero. A require that could be false is an uncatchable panic in production - exactly what the report's Contracts section counts. Read scoring.ek9. To play it in the IDE: Help > Lessons > Quality and reports…, or ek9 -ide -lesson contracts-and-risk.
Start from
The code the lesson starts from: copy it into a file of your own and follow the steps.
domain.ek9
#!ek9 <?- Driving Test Centre - a candidate's age. REQUIREMENT: a candidate is old enough to book a driving test from age 17 (inclusive) up to 70 (inclusive) - the Centre does not test anyone outside that range at all. -?> defines module quality.centre.domain defines constant MINIMUM_AGE <- 17 defines type CandidateAge as Integer constrain as >= 17 and <= 70 defines function // REQUIREMENT: eligible for a test from MINIMUM_AGE (inclusive) - the Centre's own minimum. isMinimumAge() -> age as CandidateAge <- rtn as Boolean: age > MINIMUM_AGE //EOF
vehicles.ek9
#!ek9 <?- Driving Test Centre - vehicle categories, in their own file (same module as domain.ek9, which a module may span) so the Generate Edge-Case Tests… lesson can scope its whole-file run to domain.ek9 alone - just CandidateAge and isMinimumAge - without these four trivial classes. -?> defines module quality.centre.domain defines trait Vehicle category() as pure <- rtn as String? defines class Car with trait of Vehicle override category() as pure <- rtn as String: "car" default operator ? Motorcycle with trait of Vehicle override category() as pure <- rtn as String: "motorcycle" default operator ? Lorry with trait of Vehicle override category() as pure <- rtn as String: "lorry" default operator ? QuadBike with trait of Vehicle override category() as pure <- rtn as String: "quad-bike" default operator ? //EOF
scoring.ek9
#!ek9 <?- Driving Test Centre - theory test scoring, in its own file (same module as domain.ek9) so the Generate Edge-Case Tests… lesson can scope its whole-file run to domain.ek9 alone, without this function's require/division in the same scope. -?> defines module quality.centre.domain defines function // REQUIREMENT: a candidate's average theory test score across every attempt made so far - // attempts is always at least one (a candidate who has never sat the test has no average yet). averageScore() -> totalMarks as Float attempts as Float <- rtn as Float? require attempts > 0.0 rtn: totalMarks / attempts //EOF
service.ek9
#!ek9 <?- Driving Test Centre - the booking service: a fee schedule, injected into an application, used by a program. Two modules (quality.centre.service depends on quality.centre.domain) for the quality report's Module Dependencies page; AbstractFeeSchedule/StandardFeeSchedule for its Type Hierarchy and DI Wiring pages. -?> defines module quality.centre.service references quality.centre.domain::CandidateAge defines constant JUNIOR_AGE <- 25 SENIOR_AGE <- 60 defines class BookingService schedule as AbstractFeeSchedule! quote() -> age as CandidateAge category as String <- rtn as Money: schedule.feeFor(age, category) default operator ? defines component AbstractFeeSchedule as abstract feeFor() as abstract -> age as CandidateAge category as String <- rtn as Money? default operator ? // REQUIREMENT: the test fee depends on the candidate's age band and the vehicle category - // three age bands across four categories, the Centre's full published fee schedule. StandardFeeSchedule is AbstractFeeSchedule override feeFor() -> age as CandidateAge category as String <- rtn as Money: 0.00#GBP isCar <- category == "car" isMotorcycle <- category == "motorcycle" isLorry <- category == "lorry" isQuadBike <- category == "quad-bike" isJunior <- age <= JUNIOR_AGE isSenior <- age <= SENIOR_AGE if isCar and isJunior rtn: 34.00#GBP else if isCar and isSenior rtn: 40.00#GBP else if isCar rtn: 38.00#GBP else if isMotorcycle and isJunior rtn: 28.00#GBP else if isMotorcycle and isSenior rtn: 32.00#GBP else if isMotorcycle rtn: 29.00#GBP else if isLorry and isJunior rtn: 60.00#GBP else if isLorry and isSenior rtn: 65.00#GBP else if isLorry rtn: 62.00#GBP else if isQuadBike and isJunior rtn: 24.00#GBP else if isQuadBike and isSenior rtn: 27.00#GBP else if isQuadBike rtn: 26.00#GBP default operator ? defines application CentreApp register StandardFeeSchedule() as AbstractFeeSchedule defines program QuoteFee() with application of CentreApp stdout <- Stdout() service <- BookingService() candidate <- CandidateAge(30) fee <- service.quote(candidate, "car") stdout.println(`Fee: ${fee}`) //EOF
dev/tests.ek9
#!ek9 <?- Driving Test Centre - a FEW hand-written tests, deliberately leaving real gaps: isMinimumAge is only tried at one interior age (never the boundary, where the off-by-one lives), averageScore is only tried with ordinary marks (never the require/division edge), and only Car's category() is tried at all - Motorcycle, Lorry, QuadBike and the whole fee schedule are untested. Coverage, input variety and the budget bars all show what is missing. -?> defines module quality.centre.tests references quality.centre.domain::averageScore quality.centre.domain::CandidateAge quality.centre.domain::Car quality.centre.domain::isMinimumAge defines program @Test AdultIsEligible() candidate <- CandidateAge(30) assert isMinimumAge(candidate) @Test CarCategoryIsCar() car <- Car() assert car.category() == "car" @Test AverageOfTwoAttempts() assert averageScore(150.0, 2.0) == 75.0 //EOF
Step 1: Run the quality report
Run > Quality Report (Shift+Cmd+R) writes the report this lesson reads. Choose Quality Report.
Step 2: Risk: hazard times exposure
On index.html, Risk profile: 18 constructs, each scored hazard x exposure (HEX). Hazard is how much a change here can break, from its complexity and coupling. Exposure is how little the tests would notice. The product has two levers, always: make the code less dangerous, or make the tests notice more. The inner ring says how much was measured exactly (6), approximated (2) or has only coverage to go on (10).
Worst measured construct: Lorry.category, score 8, with a link to vehicles.ek9:26. Its hazard is 8% and its exposure 100% - nothing tests it - but it is one line of code, so it is LOW. Worst approximate construct: StandardFeeSchedule.feeFor, 43, MODERATE - hazard 43% (27 paths) and exposure 100%. Its module page shows it as H43 E100 beside the method; isMinimumAge, one test in two, shows H13 E50. "No construct scores HIGH" is a fact about this project, not a promise.
Step 3: Contracts: discharged or unproven
Contracts: 2/4 discharged by construction, 2 unproven. An obligation is a point that could fail for some input - a division by zero, a require that could be false, an unguarded container unwrap, a constrained type built out of range. Discharged means the compiler has already proved it safe for every input. Unproven means nothing has yet - a candidate, not a known failure.
The Contract Verification section names the worst construct: quality.centre.domain::averageScore, scoring.ek9:13, 2 of 2 obligations unproven - its require and its division. On the quality.centre.domain module page it carries C:0/2, amber. An unproven require is exactly where to point the solver.
Step 4: Ask the solver
Choose Code > Generate > Edge-Case Tests (whole file)…
Tick Verify-directed - one @Test per counterexample. It runs the bounded-model solver on every function in scoring.ek9 instead of spraying edge values, so it needs no count or seed.
Choose Generate.
Verified to bound - no counterexamples: the solver found no input within its bound that breaks averageScore, so it wrote no test. Had one existed - a zero that slips past a require, say - it would have been written as a directed @Test, ready to run. Choose Close.
The result
That is risk and contracts. Risk is hazard times exposure: how much a change can break, times how little the tests would notice - so lower either, by simplifying the code or by testing it harder; a low score on untested code means little can go wrong, not that anything is guarded. Contracts are the points that could fail for some input: the compiler discharges those it can prove on every build, and the solver, asked, answers the rest - a concrete failing input to turn into a test, or "verified to bound". Unproven is a question, never a failure. AI assistants ask the same solver with ek9_verify_method, and read both numbers from ek9_run_quality_checks.
Where this lesson comes from
- Answers -
ek9 -qand the IDE's Answers tab: 304. - For AI assistants - the MCP tool
ek9_run_quality_checks,ek9_verify_method.
In the EK9 reference: Compile-Time Code Quality Enforcement







