@@ -20,24 +20,24 @@ warn() { _grader_details+=("WARN: $1"); }
2020l1=0
2121for f in Dockerfile " src/pipeline.py" " tests/test_pipeline.py" " AI_ASSIST.md" ; do
2222 if [[ -f " $REPO_ROOT /$f " ]]; then
23- (( l1 += 3 ))
23+ l1= $ (( l1 + 3 ))
2424 else
2525 fail " missing $f "
2626 fi
2727done
2828# ci.yml
2929if ls " $REPO_ROOT /.github/workflows/" * .yml 2> /dev/null | grep -q . ; then
30- (( l1 += 2 ))
30+ l1= $ (( l1 + 2 ))
3131else
3232 fail " missing .github/workflows/*.yml"
3333fi
3434# requirements.txt or pyproject.toml
3535if [[ -f " $REPO_ROOT /requirements.txt" ]] || [[ -f " $REPO_ROOT /pyproject.toml" ]]; then
36- (( l1 += 1 ))
36+ l1= $ (( l1 + 1 ))
3737else
3838 fail " missing requirements.txt or pyproject.toml"
3939fi
40- (( score += l1 ))
40+ score= $ (( score + l1 ))
4141pass " Level 1: required files ($l1 /15 pts)"
4242
4343
4646df=" $REPO_ROOT /Dockerfile"
4747if [[ -f " $df " ]]; then
4848 if grep -qE " ^FROM\s+python:3\.11-slim" " $df " ; then
49- (( l2 += 5 )) ; pass " Dockerfile uses python:3.11-slim base image"
49+ l2= $ (( l2 + 5 )) ; pass " Dockerfile uses python:3.11-slim base image"
5050 else
5151 fail " Dockerfile does not use python:3.11-slim base image"
5252 fi
@@ -55,18 +55,18 @@ if [[ -f "$df" ]]; then
5555 req_line=$( grep -n " COPY.*requirements" " $df " | head -1 | cut -d: -f1 || echo 0)
5656 src_line=$( grep -n " COPY.*src" " $df " | head -1 | cut -d: -f1 || echo 9999)
5757 if [[ " $req_line " -gt 0 && " $src_line " -lt 9999 && " $req_line " -lt " $src_line " ]]; then
58- (( l2 += 7 )) ; pass " Dockerfile copies requirements before source (cache-friendly)"
58+ l2= $ (( l2 + 7 )) ; pass " Dockerfile copies requirements before source (cache-friendly)"
5959 else
6060 fail " Dockerfile does not copy requirements before source code"
6161 fi
6262
6363 if grep -qE " ^CMD" " $df " ; then
64- (( l2 += 3 )) ; pass " Dockerfile has a CMD instruction"
64+ l2= $ (( l2 + 3 )) ; pass " Dockerfile has a CMD instruction"
6565 else
6666 fail " Dockerfile missing CMD instruction"
6767 fi
6868fi
69- (( score += l2 ))
69+ score= $ (( score + l2 ))
7070pass " Level 2: Dockerfile ($l2 /15 pts)"
7171
7272# ── Level 3 (10 pts): unit tests ─────────────────────────────────────────────
@@ -75,67 +75,67 @@ test_file="$REPO_ROOT/tests/test_pipeline.py"
7575if [[ -f " $test_file " ]]; then
7676 test_count=$( grep -cE " ^[[:space:]]*def test_" " $test_file " || true)
7777 if [[ " $test_count " -ge 2 ]]; then
78- (( l3 += 7 )) ; pass " tests/test_pipeline.py has $test_count test functions (≥2 required)"
78+ l3= $ (( l3 + 7 )) ; pass " tests/test_pipeline.py has $test_count test functions (≥2 required)"
7979 else
8080 fail " tests/test_pipeline.py has only $test_count test function(s) — at least 2 required"
8181 fi
8282 if ! grep -q " NotImplementedError" " $test_file " ; then
83- (( l3 += 3 )) ; pass " tests/test_pipeline.py has no NotImplementedError stubs remaining"
83+ l3= $ (( l3 + 3 )) ; pass " tests/test_pipeline.py has no NotImplementedError stubs remaining"
8484 else
8585 fail " tests/test_pipeline.py still contains NotImplementedError stubs"
8686 fi
8787fi
88- (( score += l3 ))
88+ score= $ (( score + l3 ))
8989pass " Level 3: unit tests ($l3 /10 pts)"
9090
9191# ── Level 4 (10 pts): pinned dependencies ───────────────────────────────────
9292l4=0
9393if [[ -f " $REPO_ROOT /requirements.txt" ]]; then
9494 pinned=$( grep -cE " ^[a-zA-Z].*==" " $REPO_ROOT /requirements.txt" || true)
9595 if [[ " $pinned " -ge 1 ]]; then
96- (( l4 += 7 )) ; pass " requirements.txt has $pinned pinned package(s)"
96+ l4= $ (( l4 + 7 )) ; pass " requirements.txt has $pinned pinned package(s)"
9797 else
9898 fail " requirements.txt has no pinned packages (use package==version)"
9999 fi
100100fi
101101if [[ -f " $REPO_ROOT /uv.lock" ]]; then
102- (( l4 += 3 )) ; pass " uv.lock present (full dependency tree pinned)"
102+ l4= $ (( l4 + 3 )) ; pass " uv.lock present (full dependency tree pinned)"
103103elif [[ " $l4 " -ge 7 ]]; then
104- (( l4 += 3 )) ; pass " requirements.txt pins satisfied (no uv.lock needed)"
104+ l4= $ (( l4 + 3 )) ; pass " requirements.txt pins satisfied (no uv.lock needed)"
105105fi
106- (( score += l4 ))
106+ score= $ (( score + l4 ))
107107pass " Level 4: pinned dependencies ($l4 /10 pts)"
108108
109109# ── Level 5 (20 pts): CI workflow ────────────────────────────────────────────
110110l5=0
111111ci_file=$( ls " $REPO_ROOT /.github/workflows/" * .yml 2> /dev/null | head -1 || true)
112112if [[ -n " $ci_file " ]]; then
113- grep -q " pull_request" " $ci_file " && { (( l5 += 4 )) ; pass " ci.yml triggers on pull_request" ; } || fail " ci.yml missing pull_request trigger"
114- grep -qE ' \bmain\b' " $ci_file " && { (( l5 += 4 )) ; pass " ci.yml triggers on push to main" ; } || fail " ci.yml missing push to main trigger"
115- grep -q " ruff check" " $ci_file " && { (( l5 += 3 )) ; pass " ci.yml runs ruff check (lint)" ; } || fail " ci.yml missing ruff check step"
116- grep -q " ruff format" " $ci_file " && { (( l5 += 3 )) ; pass " ci.yml runs ruff format (format check)" ; } || fail " ci.yml missing ruff format step"
117- grep -q " pytest" " $ci_file " && { (( l5 += 3 )) ; pass " ci.yml runs pytest" ; } || fail " ci.yml missing pytest step"
118- grep -q " docker build" " $ci_file " && { (( l5 += 3 )) ; pass " ci.yml runs docker build" ; } || fail " ci.yml missing docker build step"
113+ grep -q " pull_request" " $ci_file " && { l5= $ (( l5 + 4 )) ; pass " ci.yml triggers on pull_request" ; } || fail " ci.yml missing pull_request trigger"
114+ grep -qE ' \bmain\b' " $ci_file " && { l5= $ (( l5 + 4 )) ; pass " ci.yml triggers on push to main" ; } || fail " ci.yml missing push to main trigger"
115+ grep -q " ruff check" " $ci_file " && { l5= $ (( l5 + 3 )) ; pass " ci.yml runs ruff check (lint)" ; } || fail " ci.yml missing ruff check step"
116+ grep -q " ruff format" " $ci_file " && { l5= $ (( l5 + 3 )) ; pass " ci.yml runs ruff format (format check)" ; } || fail " ci.yml missing ruff format step"
117+ grep -q " pytest" " $ci_file " && { l5= $ (( l5 + 3 )) ; pass " ci.yml runs pytest" ; } || fail " ci.yml missing pytest step"
118+ grep -q " docker build" " $ci_file " && { l5= $ (( l5 + 3 )) ; pass " ci.yml runs docker build" ; } || fail " ci.yml missing docker build step"
119119fi
120- (( score += l5 ))
120+ score= $ (( score + l5 ))
121121pass " Level 5: CI workflow ($l5 /20 pts)"
122122
123123# ── Level 6 (15 pts): env-var configuration ──────────────────────────────────
124124l6=0
125125py=" $REPO_ROOT /src/pipeline.py"
126126if [[ -f " $py " ]]; then
127127 if grep -qE " os\.(environ|getenv)|from os import (environ|getenv)" " $py " ; then
128- (( l6 += 10 )) ; pass " pipeline.py reads config from os.environ/os.getenv"
128+ l6= $ (( l6 + 10 )) ; pass " pipeline.py reads config from os.environ/os.getenv"
129129 else
130130 fail " pipeline.py does not read from os.environ or os.getenv"
131131 fi
132132 if ! grep -q " NotImplementedError" " $py " ; then
133- (( l6 += 5 )) ; pass " pipeline.py has no NotImplementedError stubs remaining"
133+ l6= $ (( l6 + 5 )) ; pass " pipeline.py has no NotImplementedError stubs remaining"
134134 else
135135 fail " pipeline.py still contains NotImplementedError"
136136 fi
137137fi
138- (( score += l6 ))
138+ score= $ (( score + l6 ))
139139pass " Level 6: env-var config ($l6 /15 pts)"
140140
141141# ── Level 7 (10 pts): ACR screenshot ────────────────────────────────────────
@@ -144,14 +144,14 @@ screenshot="$REPO_ROOT/assets/acr_push_week5.png"
144144if [[ -f " $screenshot " ]]; then
145145 size=$( wc -c < " $screenshot " )
146146 if [[ " $size " -gt 1024 ]]; then
147- (( l7 += 10 )) ; pass " assets/acr_push_week5.png present and non-trivial (${size} bytes)"
147+ l7= $ (( l7 + 10 )) ; pass " assets/acr_push_week5.png present and non-trivial (${size} bytes)"
148148 else
149149 fail " assets/acr_push_week5.png exists but looks empty (${size} bytes)"
150150 fi
151151else
152152 fail " assets/acr_push_week5.png missing (Task 7 deliverable)"
153153fi
154- (( score += l7 ))
154+ score= $ (( score + l7 ))
155155pass " Level 7: ACR screenshot ($l7 /10 pts)"
156156
157157# ── Level 8 (5 pts): AI_ASSIST.md content ───────────────────────────────────
@@ -165,22 +165,22 @@ if [[ -f "$ai" ]]; then
165165 has_todo=$( grep -c " ^TODO:" " $ai " || true)
166166
167167 if [[ " $has_prompt " -ge 1 && " $has_code " -ge 1 && " $has_changed " -ge 1 ]]; then
168- (( l8 += 3 )) ; pass " AI_ASSIST.md has all three required sections"
168+ l8= $ (( l8 + 3 )) ; pass " AI_ASSIST.md has all three required sections"
169169 else
170170 fail " AI_ASSIST.md missing one or more required sections"
171171 fi
172172 if [[ " $chars " -gt 500 && " $has_todo " -eq 0 ]]; then
173- (( l8 += 2 )) ; pass " AI_ASSIST.md is filled in (${chars} chars, no TODO placeholders)"
173+ l8= $ (( l8 + 2 )) ; pass " AI_ASSIST.md is filled in (${chars} chars, no TODO placeholders)"
174174 else
175175 fail " AI_ASSIST.md still contains TODO placeholders or is too short (${chars} chars)"
176176 fi
177177fi
178- (( score += l8 ))
178+ score= $ (( score + l8 ))
179179pass " Level 8: AI report ($l8 /5 pts)"
180180
181181# ── Code hygiene (warnings from grader_lib) ──────────────────────────────────
182- check_no_print_statements " $REPO_ROOT /src" " src/"
183- check_gitignore_python " $REPO_ROOT /.gitignore"
182+ check_no_print_statements " $REPO_ROOT /src" " src/" || true
183+ check_gitignore_python " $REPO_ROOT /.gitignore" || true
184184
185185# ── Final result ─────────────────────────────────────────────────────────────
186186passing_score=60
0 commit comments