ЁЯПл The SchoolтА║ЁЯПЫя╕П Software ArchitectureтА║ЁЯФН рдзрдбрд╛ 09 тАФ Fitness functions: рдХрдзреАрд╣реА рд╕реБрдЯреНрдЯреА рди рдШреЗрдгрд╛рд░реА рдЗрдорд╛рд░рдд-рдирд┐рд░реАрдХреНрд╖рдХ
ЁЯЦ╝я╕П See the drawing + lab ЁЯПа Course home ЁЯМ┐ Branch on GitHub тЬПя╕П View source
ЁЯЦ╝я╕П рдЖрдХреГрддреА рдЖрдгрд┐ labThe drawing + lab рдкреВрд░реНрдг рдкрд╛рдирд╛рд╡рд░ рдЙрдШрдбрд╛ тЖЧOpen full page тЖЧ

ЁЯФН рдзрдбрд╛ 09 тАФ Fitness functions: рдХрдзреАрд╣реА рд╕реБрдЯреНрдЯреА рди рдШреЗрдгрд╛рд░реА рдЗрдорд╛рд░рдд-рдирд┐рд░реАрдХреНрд╖рдХ

ЁЯУН рддреБрдореНрд╣реА рдЗрдереЗ рдЖрд╣рд╛рдд: 12 рдкреИрдХреА рдзрдбрд╛ 09 ┬╖ рдорд╛рдЧрдЪрд╛: lesson-08-data-ownership ┬╖ рдкреБрдврдЪрд╛: lesson-10-c4


ЁЯУж рдпрд╛ рдмреНрд░рдБрдЪрдордзреНрдпреЗ рдХрд╛рдп рдЖрд╣реЗ

рдзрдбреЗ 01тАУ08, рдЖрдгрд┐ рдЕрдВрдорд▓рдмрдЬрд╛рд╡рдгреА. рдЖрдзреАрдЪреНрдпрд╛ рдзрдбреНрдпрд╛рдВрдордзрд▓рд╛ рдкреНрд░рддреНрдпреЗрдХ рдирд┐рдпрдо рдЖрддрд╛ рдкреНрд░рддреНрдпреЗрдХ push рд╡рд░ рдЪрд╛рд▓рдгрд╛рд░реА architecture test рдмрдирддреЛ тАФ рдПрдХ fitness function. рдирд╡реАрди рдорд╛рд░реНрдЧрд┐рдХрд╛ рдЖрд░рд╛рдЦрдбрд╛ рдореЛрдбрддреЗ рддреЗрд╡реНрд╣рд╛ build fail рд╣реЛрддреЛ, рдЬрд╕рд╛ unit test рдореЛрдбрд▓реНрдпрд╛рд╡рд░ рд╣реЛрддреЛ. fitness() рдЖрдгрд┐ check command arch/analyze.py рдордзреНрдпреЗ, рдЖрдгрд┐ fitness arch/demo.py рдордзреНрдпреЗ.

ЁЯзТ 5 рд╡рд░реНрд╖рд╛рдВрдЪреНрдпрд╛ рдореБрд▓рд╛рд▓рд╛ рд╕рдордЬрд╛рд╡рд▓реНрдпрд╛рд╕рд╛рд░рдЦреЗ

рдХрддрд░рд┐рдирд╛рдиреЗ рдПрдХ рд╕реБрдВрджрд░ рдЗрдорд╛рд░рддреАрдЪрд╛ рдирдХрд╛рд╢рд╛ рдХрд╛рдврд▓рд╛. рдордЧ рдмрд╛рдВрдзрдХрд╛рдо рдХрд░рдгрд╛рд░реЗ рдЖрд▓реЗ. ЁЯС╖тАНтЩАя╕П рддреНрдпрд╛рдВрдЪреНрдпрд╛рдкреИрдХреА рдПрдХ рдШрд╛рдИрдд рд╣реЛрддрд╛ рдЖрдгрд┐ рдмрдВрдж рд░рд╛рд╣рд╛рдпрд▓рд╛ рд╣рд╡реА рд╣реЛрддреА рдЕрд╢рд╛ рднрд┐рдВрддреАрддреВрди рддреНрдпрд╛рдиреЗ рджрд╛рд░ рдлреЛрдбрд▓реЗ. рдХреЛрдгрд╛рдЪреНрдпрд╛рдЪ рд▓рдХреНрд╖рд╛рдд рдЖрд▓реЗ рдирд╛рд╣реА. рд╕рд╣рд╛ рдорд╣рд┐рдиреНрдпрд╛рдВрдиреА рдЕрд╢реА рджрд╣рд╛ рджрд╛рд░реЗ рдЭрд╛рд▓реА, рдЖрдгрд┐ рднрд┐рдВрддреАрд╡рд░рдЪреНрдпрд╛ рдирдХрд╛рд╢рд╛рдЪрд╛ рдЗрдорд╛рд░рддреАрд╢реА рдХрд╛рд╣реАрдЪ рд╕рдВрдмрдВрдз рдЙрд░рд▓рд╛ рдирд╛рд╣реА.

рдореНрд╣рдгреВрди рдРрд╢реНрд╡рд░реНрдпрд╛ рдЗрдорд╛рд░рдд-рдирд┐рд░реАрдХреНрд╖рдХ рдмрдирддреЗ. ЁЯУЛ рддрд┐рдЪреНрдпрд╛рдХрдбреЗ рдПрдХ рдпрд╛рджреА рдЖрд╣реЗ: "рдирд┐рдпрдордкреБрд╕реНрддрд┐рдХреЗрдЪреНрдпрд╛ рдЦреЛрд▓реАрддреВрди рднрд╛рдВрдбрд╛рд░ рдЦреЛрд▓реАрдХрдбреЗ рджрд╛рд░ рдирд╛рд╣реА", "рджреЛрдиреНрд╣реА рджрд┐рд╢рд╛рдВрдирд╛ рдЬрд╛рдгрд╛рд░реЗ рдЬрд┐рдиреЗ рдирд╛рд╣реАрдд", "рд╡рд┐рднрд╛рдЧ рдлрдХреНрдд рдЖрдкрд╛рдкрд▓реНрдпрд╛ рдЦрд┐рдбрдХреНрдпрд╛рдВрдордзреВрдирдЪ рдмреЛрд▓рддрд╛рдд". рдЖрдгрд┐ рд╕рдЧрд│реНрдпрд╛рдд рдЫрд╛рди рдЧреЛрд╖реНрдЯ: рддреА рджрд░рд░реЛрдЬ, рдЖрдкреЛрдЖрдк рддрдкрд╛рд╕рддреЗ, рдХреЛрдгрддреЗрд╣реА рдирд╡реАрди рджрд╛рд░ рдЯрд┐рдХреВ рджреЗрдгреНрдпрд╛рдЖрдзреА.

рдирд╡реАрди shortcut? рдРрд╢реНрд╡рд░реНрдпрд╛рдЪреА рдпрд╛рджреА рд▓рд╛рд▓ рд╣реЛрддреЗ, рдЖрдгрд┐ рдХреЛрдгреА рддреЗ рджреБрд░реБрд╕реНрдд рдХрд░реЗрдкрд░реНрдпрдВрдд тАФ рдХрд┐рдВрд╡рд╛ рд╣реЗрддреБрдкреВрд░реНрд╡рдХ рдЖрд░рд╛рдЦрдбрд╛ рдмрджрд▓реЗрдкрд░реНрдпрдВрдд тАФ рддреЗ рджрд╛рд░ рд╕реНрд╡реАрдХрд╛рд░рд▓реЗ рдЬрд╛рдд рдирд╛рд╣реА.

ЁЯЧ║я╕П рдЖрдХреГрддреА

flowchart LR
    push["ЁЯСйтАНЁЯТ╗ git push"] --> ci["тЪЩя╕П CI"]
    ci --> unit["ЁЯзк unit tests"]
    ci --> fit["ЁЯФН fitness functions<br/>python3 arch/analyze.py check"]
    fit --> r1["тЭМ no import cycles"]
    fit --> r2["тЭМ dependencies point inward"]
    fit --> r3["тЭМ domain imports only the domain"]
    fit --> r4["тЭМ departments apart"]
    fit --> r5["тЬЕ fan-out тЙд 5"]
    fit --> r6["тЬЕ domain instability тЙд 0.5"]
    r1 --> fail["exit code 1: the merge is blocked"]

ЁЯЧ║я╕П рдХрд╛рдврд▓реЗрд▓реА рдЖрдХреГрддреА + рдПрдХ lab: https://school-edh.pages.dev/software-architecture/lesson-diagrams.html#l09

тЭУ рдХрд╛рдп

ЁЯдФ рдХрд╛

рдХрд╛рд░рдг architecture рдПрдХрд╛ рд╡реЗрд│реА рдПрдХ, рдпреЛрдЧреНрдп рд╡рд╛рдЯрдгрд╛рд▒реНрдпрд╛ import рдиреЗ рдЭрд┐рдЬрддреЗ, рдЖрдгрд┐ code review рд▓рд╛ 3 рдУрд│реАрдВрдЪреНрдпрд╛ diff рдордзреНрдпреЗ рд░рдЪрдиреЗрдЪрд╛ рдирд┐рдпрдо рджрд┐рд╕реВ рд╢рдХрдд рдирд╛рд╣реА. рдХреЛрдгреАрдЪ рди рддрдкрд╛рд╕рдгрд╛рд░рд╛ рдЖрд░рд╛рдЦрдбрд╛ рдореНрд╣рдгрдЬреЗ рдлрдХреНрдд рдЗрдЪреНрдЫрд╛. Build fail рдХрд░рдгрд╛рд░рд╛ рдЖрд░рд╛рдЦрдбрд╛ рдореНрд╣рдгрдЬреЗ рдЯрд┐рдХреВрди рд░рд╛рд╣рдгрд╛рд░рд╛ рдирд┐рд░реНрдгрдп тАФ рдЖрдгрд┐ рдирд┐рдпрдордЪ рдЪреБрдХреАрдЪрд╛ рдЕрд╕реЗрд▓, рддрд░ рддреЛ рдмрджрд▓рдгреЗ рд╣рд╛ рд╢рд╛рдВрддрдкрдгреЗ рд╣реЛрдгрд╛рд░рд╛ drift рди рд░рд╛рд╣рддрд╛ рджрд┐рд╕рдгрд╛рд░рд╛, review рдЭрд╛рд▓реЗрд▓рд╛ рдмрджрд▓ рдард░рддреЛ.

ЁЯФз рдХрд╕реЗ (рдпрд╛ repo рдордзреНрдпреЗ)

arch/analyze.py рдордзрд▓реЗ fitness(g, classes, max_fan_out=5, max_core_instability=0.5) рд╕рд╣рд╛ рдирд┐рдпрдо рдЪрд╛рд▓рд╡рддреЗ, рдкреНрд░рддреНрдпреЗрдХ рдЖрдзреАрдЪреНрдпрд╛ рдзрдбреНрдпрд╛рддреВрди рдмрдирд▓реЗрд▓рд╛: cycles() (рдзрдбрд╛ 02/12), ring_violations() рдЖрдгрд┐ core_leaks() (рдзрдбрд╛ 04), context_edges() (рдзрдбрд╛ 05), рдкреНрд░рддреНрдпреЗрдХ рдЦреЛрд▓реАрдЪрд╛ рд╕рд░реНрд╡рд╛рдд рдореЛрдард╛ fan-out (рдзрдбрд╛ 02), рдЖрдгрд┐ domain рдЪреА instability I (рдзрдбрд╛ 02). рдкреНрд░рддреНрдпреЗрдХ рдирд┐рдпрдо (name, passed, detail) рдкрд░рдд рдХрд░рддреЛ. python3 arch/analyze.py check рддреЗ рдЫрд╛рдкрддреЗ рдЖрдгрд┐ рдХреЛрдгрддрд╛рд╣реА fail рдЭрд╛рд▓рд╛ рддрд░ exit 1 рдХрд░рддреЗ тАФ CI step рд▓рд╛ рдиреЗрдордХреЗ рд╣реЗрдЪ рд╣рд╡реЗ рдЕрд╕рддреЗ.

ЁЯзк рдХрд░реВрди рдкрд╛рд╣рд╛

python3 arch/demo.py fitness
python3 arch/analyze.py check; echo "exit code $?"
python3 - <<'EOF'
import sys; sys.path.insert(0, "arch"); from analyze import scan, apply, fitness, FIXES
cb = scan(); fixed = apply(cb.imports, remove=FIXES)
oops = apply(fixed, add=[("domain.fees", "infra.db")])     # a new shortcut, pushed on a Friday
for name, ok, detail in fitness(oops, cb.classes):
    print(f"{'PASS' if ok else 'FAIL'}  {name:<40} {detail}")
EOF

тЬЕ рддрдкрд╛рд╕рд╛ тАФ рддреБрдореНрд╣рд╛рд▓рд╛ рдХрд╛рдп рджрд┐рд╕рд╛рдпрд▓рд╛ рд╣рд╡реЗ

fitness рд╣реЗ рдЫрд╛рдкрддреЗ:

тФАтФА the fitness functions run on every push (python3 arch/analyze.py check)
   FAIL  no import cycles                         1 cycle(s): domain.grades тЖФ infra.db
   FAIL  dependencies point inward                3 outward import(s)
   FAIL  the domain imports only the domain       2 leak(s)
   FAIL  departments do not import each other     2 cross-context import(s)
   PASS  fan-out тЙд 5 per room (main excluded)     max 4 (web.views)
   PASS  domain instability тЙд 0.5                 I = 0.29
   after removing the 5 planted corridors: 6/6 pass ┬╖ CI exit code 0

check command рдЪрд╛ рд╢реЗрд╡рдЯ рдЕрд╕рд╛ рд╣реЛрддреЛ:

4 rule(s) failed
exit code 1

рддреБрдордЪрд╛ snippet рд╣реЗ рдЫрд╛рдкрддреЛ:

PASS  no import cycles                         0 cycle(s)
FAIL  dependencies point inward                1 outward import(s)
FAIL  the domain imports only the domain       1 leak(s)
FAIL  departments do not import each other     1 cross-context import(s)
PASS  fan-out тЙд 5 per room (main excluded)     max 4 (web.views)
PASS  domain instability тЙд 0.5                 I = 0.17

ЁЯПБ рддреБрдореНрд╣реА рдЖрддреНрддрд╛рдЪ рдХрд╛рдп рд╕рд┐рджреНрдз рдХреЗрд▓реЗ

рджрд┐рд▓реЗрд▓реА рдЗрдорд╛рд░рдд 6 рдкреИрдХреА 4 рдирд┐рдпрдорд╛рдВрдд fail рд╣реЛрддреЗ рдЖрдгрд┐ рддрдкрд╛рд╕рдгреА exit 1 рдХрд░рддреЗ тАФ CI job merge рдерд╛рдВрдмрд╡реЗрд▓. рдкреЗрд░рд▓реЗрд▓реНрдпрд╛ 5 рдорд╛рд░реНрдЧрд┐рдХрд╛ рдХрд╛рдврд▓реНрдпрд╛рд╡рд░ рд╕рд░реНрд╡ 6 pass рд╣реЛрддрд╛рдд рдЖрдгрд┐ рддреА exit 0 рдХрд░рддреЗ. рдордЧ рд╢реБрдХреНрд░рд╡рд╛рд░рдЪрд╛ рдПрдХ shortcut тАФ domain.fees рдиреЗ infra.db import рдХрд░рдгреЗ тАФ рддреАрди рдирд┐рдпрдорд╛рдВрдиреА рдПрдХрд╛рдЪ рд╡реЗрд│реА рдкрдХрдбрд▓рд╛ рдЬрд╛рддреЛ: рддреЛ рдмрд╛рд╣реЗрд░рдЪреНрдпрд╛ рджрд┐рд╢реЗрдиреЗ рдЬрд╛рддреЛ, рддреЛ domain рдЧрд│реВ рджреЗрддреЛ, рдЖрдгрд┐ рддреЛ fees рд╡рд┐рднрд╛рдЧрд╛рддреВрди grades рдЪреНрдпрд╛ рдорд╛рд▓рдХреАрдЪреНрдпрд╛ рдЦреЛрд▓реАрдд рдЬрд╛рддреЛ. рдХрд╢рд╛рдиреЗ рддреЛ рдкрдХрдбрд▓рд╛ рдирд╛рд╣реА рддреЗ рдкрд╛рд╣рд╛: domain рдЪреА instability рдлрдХреНрдд 0.00 рд╡рд░реВрди 0.17 рдЭрд╛рд▓реА, рдЕрдЬреВрдирд╣реА 0.5 threshold рдЪреНрдпрд╛ рдЦрд╛рд▓реА. Metric thresholds рдвреЛрдмрд│ рдЕрд╕рддрд╛рдд; рд╕реНрдкрд╖реНрдЯ рдирд┐рдпрдо рдзрд╛рд░рджрд╛рд░ рдЕрд╕рддрд╛рдд. рджреЛрдиреНрд╣реА рд╡рд╛рдкрд░рд╛.

тЪая╕П рдиреЗрд╣рдореАрдЪреНрдпрд╛ рдЪреБрдХрд╛

ЁЯПн рдкреНрд░рддреНрдпрдХреНрд╖ рд╡рд╛рдкрд░рд╛рдд

рдЦрд▒реНрдпрд╛ project рд╡рд░ тАФ checks unit tests рдЪреНрдпрд╛рдЪ pipeline рдордзреНрдпреЗ рдЪрд╛рд▓рд╡рд╛. GitHub Actions:

name: architecture
on: [push, pull_request]
jobs:
  fitness:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: "3.12"
      - run: python3 arch/analyze.py check
      - run: pip install import-linter && lint-imports

рдЗрддрд░ рднрд╛рд╖рд╛рдВрдордзреНрдпреЗ рд╣реАрдЪ рдХрд▓реНрдкрдирд╛:

Exception list рдореБрд│реЗ рддреБрдореНрд╣реА рдирд┐рдпрдо рдЖрдЬрдЪ рдЪрд╛рд▓реВ рдХрд░реВ рд╢рдХрддрд╛ (import-linter):

[importlinter:contract:core]
name = The domain imports no adapters
type = forbidden
source_modules = schoolapp.domain
forbidden_modules = schoolapp.infra
# a known violation, ticket SCH-142 тАФ the list may shrink, never grow
ignore_imports =
    schoolapp.domain.grades -> schoolapp.infra.db

ЁЯПн рдкреНрд░рддреНрдпрдХреНрд╖ рд╡рд╛рдкрд░рд╛рдд рд╣реЗ рдХрд╛ рдорд╣рддреНрддреНрд╡рд╛рдЪреЗ: рддреБрдордЪреЗ рд░рдЪрдиреЗрдЪреЗ рджреЛрди рд╕рд░реНрд╡рд╛рдд рдорд╣рддреНрддреНрд╡рд╛рдЪреЗ рдирд┐рдпрдо рдирд┐рд╡рдбрд╛, рдпрд╛ рдЖрдард╡рдбреНрдпрд╛рдд рддреЗ tests рдореНрд╣рдгреВрди рд▓рд┐рд╣рд╛, рдЖрдгрд┐ рдПрдХ рдЙрд▓реНрд▓рдВрдШрди рдкреЗрд░реВрди рддреЗ fail рд╣реЛрддрд╛рдирд╛ рдкрд╛рд╣рд╛. рдЬреЛ рдирд┐рдпрдо рддреБрдореНрд╣реА рдХрдзреА fail рд╣реЛрддрд╛рдирд╛ рдкрд╛рд╣рд┐рд▓реЗрд▓рд╛ рдирд╛рд╣реА, рддреЛ рдЪрд╛рд▓рддреЛ рдХреА рдирд╛рд╣реА рд╣реЗ рддреБрдореНрд╣рд╛рд▓рд╛ рдорд╛рд╣реАрдд рдирд╕рддреЗ.

тПня╕П рдкреБрдвреЗ

рдирд┐рд░реАрдХреНрд╖рдХ рдЗрдорд╛рд░рдд рддрдкрд╛рд╕рддреЗ. рдЖрддрд╛ рддреА рдХрд╛рдврд╛ тАФ code рдордзреВрдирдЪ, рдореНрд╣рдгрдЬреЗ рдЪрд┐рддреНрд░ рдЦреЛрдЯреЗ рдмреЛрд▓реВ рд╢рдХрдд рдирд╛рд╣реА. C4 model.

git checkout lesson-10-c4

ЁЯФН Lesson 09 тАФ Fitness functions: the building inspector who never takes a day off

ЁЯУН You are here: Lesson 09 of 12 ┬╖ Previous: lesson-08-data-ownership ┬╖ Next: lesson-10-c4


ЁЯУж What's in this branch

Lessons 01тАУ08, plus enforcement. Every rule from the earlier lessons becomes an architecture test that runs on every push тАФ a fitness function. The build fails when a new corridor breaks the plan, the same way it fails when a unit test breaks. fitness() and the check command in arch/analyze.py, and fitness in arch/demo.py.

ЁЯзТ Explain like I'm 5

Katrina drew a beautiful floor plan. Then the builders came. ЁЯС╖тАНтЩАя╕П One of them was in a hurry and knocked a door through a wall that was supposed to stay closed. Nobody noticed. Six months later, there were ten such doors, and the plan on the wall had nothing to do with the building.

So Aishwarya becomes the building inspector. ЁЯУЛ She has a checklist: "no door from the rule book to the store room", "no stairs going both ways", "departments only talk through their windows". And the best part: she checks every single day, automatically, before any new door is allowed to stay.

A new shortcut? Aishwarya's list goes red, and the door is not accepted until someone fixes it тАФ or changes the plan on purpose.

ЁЯЧ║я╕П Diagram

flowchart LR
    push["ЁЯСйтАНЁЯТ╗ git push"] --> ci["тЪЩя╕П CI"]
    ci --> unit["ЁЯзк unit tests"]
    ci --> fit["ЁЯФН fitness functions<br/>python3 arch/analyze.py check"]
    fit --> r1["тЭМ no import cycles"]
    fit --> r2["тЭМ dependencies point inward"]
    fit --> r3["тЭМ domain imports only the domain"]
    fit --> r4["тЭМ departments apart"]
    fit --> r5["тЬЕ fan-out тЙд 5"]
    fit --> r6["тЬЕ domain instability тЙд 0.5"]
    r1 --> fail["exit code 1: the merge is blocked"]

ЁЯЧ║я╕П Drawn version + a lab: https://school-edh.pages.dev/software-architecture/lesson-diagrams.html#l09

тЭУ What

ЁЯдФ Why

Because architecture decays one reasonable-looking import at a time, and code review cannot see a structural rule in a 3-line diff. A plan that nobody checks is a wish. A plan that fails the build is a decision that stays made тАФ and when the rule is wrong, changing it is a visible, reviewed change instead of a quiet drift.

ЁЯФз How (in this repo)

fitness(g, classes, max_fan_out=5, max_core_instability=0.5) in arch/analyze.py runs six rules, each built from an earlier lesson: cycles() (lesson 02/12), ring_violations() and core_leaks() (lesson 04), context_edges() (lesson 05), the largest fan-out per room (lesson 02), and the domain's instability I (lesson 02). Each rule returns (name, passed, detail). python3 arch/analyze.py check prints them and exits 1 if any failed тАФ exactly what a CI step needs.

ЁЯзк Try it

python3 arch/demo.py fitness
python3 arch/analyze.py check; echo "exit code $?"
python3 - <<'EOF'
import sys; sys.path.insert(0, "arch"); from analyze import scan, apply, fitness, FIXES
cb = scan(); fixed = apply(cb.imports, remove=FIXES)
oops = apply(fixed, add=[("domain.fees", "infra.db")])     # a new shortcut, pushed on a Friday
for name, ok, detail in fitness(oops, cb.classes):
    print(f"{'PASS' if ok else 'FAIL'}  {name:<40} {detail}")
EOF

тЬЕ Verify тАФ what you should see

fitness prints:

тФАтФА the fitness functions run on every push (python3 arch/analyze.py check)
   FAIL  no import cycles                         1 cycle(s): domain.grades тЖФ infra.db
   FAIL  dependencies point inward                3 outward import(s)
   FAIL  the domain imports only the domain       2 leak(s)
   FAIL  departments do not import each other     2 cross-context import(s)
   PASS  fan-out тЙд 5 per room (main excluded)     max 4 (web.views)
   PASS  domain instability тЙд 0.5                 I = 0.29
   after removing the 5 planted corridors: 6/6 pass ┬╖ CI exit code 0

The check command ends with:

4 rule(s) failed
exit code 1

Your snippet prints:

PASS  no import cycles                         0 cycle(s)
FAIL  dependencies point inward                1 outward import(s)
FAIL  the domain imports only the domain       1 leak(s)
FAIL  departments do not import each other     1 cross-context import(s)
PASS  fan-out тЙд 5 per room (main excluded)     max 4 (web.views)
PASS  domain instability тЙд 0.5                 I = 0.17

ЁЯПБ What you just proved

The shipped building fails 4 of 6 rules and the check exits 1 тАФ a CI job would block the merge. With the 5 planted corridors removed, all 6 pass and it exits 0. Then one Friday shortcut тАФ domain.fees importing infra.db тАФ is caught by three rules at once: it points outward, it leaks the domain, and it crosses from the fees department into a room owned by grades. Note what did not catch it: the domain's instability only moved from 0.00 to 0.17, still under the 0.5 threshold. Metric thresholds are coarse; explicit rules are sharp. Use both.

тЪая╕П Common mistakes

ЁЯПн In production

On a real project тАФ run the checks in the same pipeline as the unit tests. GitHub Actions:

name: architecture
on: [push, pull_request]
jobs:
  fitness:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: "3.12"
      - run: python3 arch/analyze.py check
      - run: pip install import-linter && lint-imports

The same idea in other languages:

An exception list lets you switch the rule on today (import-linter):

[importlinter:contract:core]
name = The domain imports no adapters
type = forbidden
source_modules = schoolapp.domain
forbidden_modules = schoolapp.infra
# a known violation, ticket SCH-142 тАФ the list may shrink, never grow
ignore_imports =
    schoolapp.domain.grades -> schoolapp.infra.db

ЁЯПн Why this matters in production: pick your two most important structural rules, write them as tests this week, and plant a violation to watch them fail. A rule you have never seen fail is a rule you do not know works.

тПня╕П Next

The inspector checks the building. Now draw it тАФ from the code, so the drawing cannot lie. The C4 model.

git checkout lesson-10-c4
тЖР Previousdata ownershipNext тЖТc4

This page is the lesson's README from the lesson-09-fitness-functions branch, shown here so the whole School stays on one site. Code files open on GitHub at the same branch.