Complexity limits and budget bars

StandardFeeSchedule.feeFor prices a driving test by age band and vehicle category - twelve branches, built to sit near half the compiler's complexity budget. Read service.ek9. To play it in the IDE: Help > Lessons > Quality and reports…, or ek9 -ide -lesson complexity-limits.

The EK9 IDE as the lesson opens

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: Complexity badges in the editor

Choose View > Toggles > Complexity Badges (⌥⌘C).

The EK9 IDE once this is done: Choose View > Toggles > Complexity Badges (⌥⌘C)

A red circle marked C appears beside feeFor's own declaration line - CRITICAL, the editor's tightest severity. Hover it for the exact numbers: cyclomatic, cognitive and nesting depth, the same three the report names.

The EK9 IDE once this is done: A red circle marked C appears beside feeFor's own declaration line - CRITICAL, the editor's tightest severity

Step 2: Run the quality report

Run > Quality Report (Shift+Cmd+R) writes the report this lesson reads next. Choose Quality Report.

The EK9 IDE once this is done: Run > Quality Report (Shift+Cmd+R) writes the report this lesson reads next

Step 3: The budget bars

On index.html, Code Quality Metrics: Complexity shows max 27, avg 2.7, 18 of 45 headroom - the bar itself is amber (warning), because 27 of 45 is 60%, over the report's own 50% line. Cognitive reads max 12 of 35 (34%) - still green on this bar, even though the SAME method read red in the editor a moment ago: the editor's own threshold (20) is far tighter than the compiler's hard limit (35).

The EK9 IDE once this is done: On index.html, Code Quality Metrics

feeFor's own module page names it directly beside the bar - quality.centre.service::StandardFeeSchedule.feeFor, the worst (and only) construct behind both numbers. 45 and 35 are not advisory: E11010 and E11021 refuse a method that crosses them outright, so the budget bar is reporting distance from a line the compiler itself enforces.

The result

That is complexity, read two ways: the editor's badge is an early warning with tight thresholds (15 cyclomatic, 20 cognitive), long before anything is close to failing; the report's bar is the compiler's own hard stop (45, 35) - amber at half, red at 80%. feeFor sits at 60% of the hard limit, with 18 points of headroom left before E11010 would refuse it outright. AI assistants read the same two numbers from ek9_query_complexity, no editor or report needed.

Where this lesson comes from