@@ -63,25 +63,25 @@ for f in "${required_files[@]}"; do
6363 pass " found $f "
6464 else
6565 fail " missing required file: $f "
66- (( missing += 1 ))
66+ missing= $ (( missing + 1 ))
6767 fi
6868done
6969if ls " $REPO_ROOT /tests/" * .sql & > /dev/null 2>&1 ; then
7070 pass " found singular test in tests/"
7171else
7272 fail " no .sql file found in tests/ -- add at least one singular test"
73- (( missing += 1 ))
73+ missing= $ (( missing + 1 ))
7474fi
7575if [[ -s " $REPO_ROOT /docs/lineage.png" ]]; then
7676 pass " found docs/lineage.png"
7777else
7878 fail " docs/lineage.png missing -- run dbt docs generate + serve and screenshot the lineage graph"
79- (( missing += 1 ))
79+ missing= $ (( missing + 1 ))
8080fi
8181if [[ " $missing " -eq 0 ]]; then
8282 l1=10
8383fi
84- (( score += l1 ))
84+ score= $ (( score + l1 ))
8585pass " Level 1: required files ($l1 /10 pts)"
8686
8787# ── Level 2 (15 pts): secrets hygiene ───────────────────────────────────────
@@ -90,23 +90,23 @@ gi="$REPO_ROOT/.gitignore"
9090ex=" $REPO_ROOT /profiles.yml.example"
9191
9292if [[ -f " $gi " ]] && grep -qE " ^profiles\.yml$" " $gi " ; then
93- (( l2 += 5 )) ; pass " .gitignore: profiles.yml excluded"
93+ l2= $ (( l2 + 5 )) ; pass " .gitignore: profiles.yml excluded"
9494else
9595 fail " .gitignore must contain 'profiles.yml' on its own line"
9696fi
9797
9898if [[ -f " $ex " ]] && grep -qE " env_var\(" " $ex " ; then
99- (( l2 += 5 )) ; pass " profiles.yml.example: uses env_var() -- no hardcoded password"
99+ l2= $ (( l2 + 5 )) ; pass " profiles.yml.example: uses env_var() -- no hardcoded password"
100100else
101101 fail " profiles.yml.example must use env_var('PG_PASSWORD') instead of a hardcoded password"
102102fi
103103
104104if [[ -f " $REPO_ROOT /profiles.yml" ]]; then
105105 fail " profiles.yml is committed -- BLOCKER: run: git rm --cached profiles.yml"
106106else
107- (( l2 += 5 )) ; pass " profiles.yml not committed (correctly git-ignored)"
107+ l2= $ (( l2 + 5 )) ; pass " profiles.yml not committed (correctly git-ignored)"
108108fi
109- (( score += l2 ))
109+ score= $ (( score + l2 ))
110110pass " Level 2: secrets hygiene ($l2 /15 pts)"
111111
112112# ── Level 3 (20 pts): staging models ────────────────────────────────────────
@@ -115,79 +115,79 @@ st="$REPO_ROOT/models/staging/stg_trips.sql"
115115sz=" $REPO_ROOT /models/staging/stg_zones.sql"
116116
117117if file_is_filled " $st " && sqlgrep " \{\{[[:space:]]*source\(" " $st " ; then
118- (( l3 += 4 )) ; pass " stg_trips.sql: uses {{ source() }} reference"
118+ l3= $ (( l3 + 4 )) ; pass " stg_trips.sql: uses {{ source() }} reference"
119119else
120120 fail " stg_trips.sql: must use {{ source('nyc_taxi', 'raw_trips') }}"
121121fi
122122
123123if file_is_filled " $st " && sqlgrep " pickup_location_id" " $st " && sqlgrep " IS NOT NULL|NOT NULL" " $st " ; then
124- (( l3 += 4 )) ; pass " stg_trips.sql: filters NULL pickup_location_id"
124+ l3= $ (( l3 + 4 )) ; pass " stg_trips.sql: filters NULL pickup_location_id"
125125else
126126 fail " stg_trips.sql: must filter WHERE pickup_location_id IS NOT NULL"
127127fi
128128
129129if file_is_filled " $st " && sqlgrep " fare_amount" " $st " && sqlgrep " >=.*0|> -1" " $st " ; then
130- (( l3 += 4 )) ; pass " stg_trips.sql: filters negative fares (fare_amount >= 0)"
130+ l3= $ (( l3 + 4 )) ; pass " stg_trips.sql: filters negative fares (fare_amount >= 0)"
131131else
132132 fail " stg_trips.sql: must filter WHERE fare_amount >= 0"
133133fi
134134
135135if file_is_filled " $st " && sqlgrep " tip_pct|safe_divide" " $st " ; then
136- (( l3 += 4 )) ; pass " stg_trips.sql: tip_pct column present"
136+ l3= $ (( l3 + 4 )) ; pass " stg_trips.sql: tip_pct column present"
137137else
138138 fail " stg_trips.sql: tip_pct column missing -- add {{ safe_divide('tip_amount', 'fare_amount') }} AS tip_pct"
139139fi
140140
141141if file_is_filled " $sz " && sqlgrep " \{\{[[:space:]]*source\(" " $sz " ; then
142- (( l3 += 4 )) ; pass " stg_zones.sql: uses {{ source() }} and is filled"
142+ l3= $ (( l3 + 4 )) ; pass " stg_zones.sql: uses {{ source() }} and is filled"
143143else
144144 fail " stg_zones.sql: must use {{ source('nyc_taxi', 'raw_zones') }}"
145145fi
146- (( score += l3 ))
146+ score= $ (( score + l3 ))
147147pass " Level 3: staging models ($l3 /20 pts)"
148148
149149# ── Level 4 (20 pts): mart model ─────────────────────────────────────────────
150150l4=0
151151mart=" $REPO_ROOT /models/marts/fct_daily_borough_stats.sql"
152152
153153if file_is_filled " $mart " ; then
154- (( l4 += 2 )) ; pass " fct_daily_borough_stats.sql: file filled"
154+ l4= $ (( l4 + 2 )) ; pass " fct_daily_borough_stats.sql: file filled"
155155
156156 if sqlgrep " \{\{[[:space:]]*ref\('stg_trips'\)" " $mart " && sqlgrep " \{\{[[:space:]]*ref\('stg_zones'\)" " $mart " ; then
157- (( l4 += 4 )) ; pass " mart: ref() to both stg_trips and stg_zones"
157+ l4= $ (( l4 + 4 )) ; pass " mart: ref() to both stg_trips and stg_zones"
158158 else
159159 fail " mart: must reference both {{ ref('stg_trips') }} and {{ ref('stg_zones') }}"
160160 fi
161161
162162 if sqlgrep " (INNER|LEFT)?[[:space:]]*JOIN" " $mart " ; then
163- (( l4 += 4 )) ; pass " mart: JOIN to zones present"
163+ l4= $ (( l4 + 4 )) ; pass " mart: JOIN to zones present"
164164 else
165165 fail " mart: no JOIN -- must join stg_trips to stg_zones on pickup_location_id = location_id"
166166 fi
167167
168168 if sqlgrep " GROUP[[:space:]]+BY" " $mart " ; then
169- (( l4 += 4 )) ; pass " mart: GROUP BY present"
169+ l4= $ (( l4 + 4 )) ; pass " mart: GROUP BY present"
170170 else
171171 fail " mart: no GROUP BY -- must aggregate to (pickup_borough, pickup_date) grain"
172172 fi
173173
174174 cols_ok=0
175175 for col in pickup_borough pickup_date trip_count total_fare avg_tip_pct avg_trip_distance; do
176176 if sqlgrep " $col " " $mart " ; then
177- (( cols_ok += 1 ))
177+ cols_ok= $ (( cols_ok + 1 ))
178178 else
179179 fail " mart: required output column '$col ' not found"
180180 fi
181181 done
182182 if [[ " $cols_ok " -eq 6 ]]; then
183- (( l4 += 6 )) ; pass " mart: all 6 required output columns present"
183+ l4= $ (( l4 + 6 )) ; pass " mart: all 6 required output columns present"
184184 elif [[ " $cols_ok " -ge 4 ]]; then
185- (( l4 += 3 )) ; warn " mart: $cols_ok /6 required columns present"
185+ l4= $ (( l4 + 3 )) ; warn " mart: $cols_ok /6 required columns present"
186186 fi
187187else
188188 fail " fct_daily_borough_stats.sql: still contains TODO stubs"
189189fi
190- (( score += l4 ))
190+ score= $ (( score + l4 ))
191191pass " Level 4: mart model ($l4 /20 pts)"
192192
193193# ── Level 5 (15 pts): tests ─────────────────────────────────────────────────
@@ -196,28 +196,28 @@ mart_yml="$REPO_ROOT/models/marts/_fct_daily_borough_stats.yml"
196196singular_test=$( ls " $REPO_ROOT /tests/" * .sql 2> /dev/null | head -1 || true)
197197
198198if [[ -f " $mart_yml " ]] && grep -qiE " unique_combination_of_columns" " $mart_yml " ; then
199- (( l5 += 5 )) ; pass " mart YAML: dbt_utils.unique_combination_of_columns test present"
199+ l5= $ (( l5 + 5 )) ; pass " mart YAML: dbt_utils.unique_combination_of_columns test present"
200200else
201201 fail " mart YAML: missing dbt_utils.unique_combination_of_columns on (pickup_borough, pickup_date)"
202202fi
203203
204204stg_yml=" $REPO_ROOT /models/staging/_stg_trips.yml"
205205if [[ -f " $stg_yml " ]] && grep -qiE " not_null" " $stg_yml " ; then
206- (( l5 += 5 )) ; pass " staging YAML: not_null tests present"
206+ l5= $ (( l5 + 5 )) ; pass " staging YAML: not_null tests present"
207207else
208208 fail " staging YAML (_stg_trips.yml): not_null tests missing on key columns"
209209fi
210210
211211if [[ -n " $singular_test " ]] && file_is_filled " $singular_test " ; then
212212 if sqlgrep " \{\{[[:space:]]*ref\('fct_daily_borough_stats'\)" " $singular_test " ; then
213- (( l5 += 5 )) ; pass " singular test: filled and references fct_daily_borough_stats"
213+ l5= $ (( l5 + 5 )) ; pass " singular test: filled and references fct_daily_borough_stats"
214214 else
215- (( l5 += 3 )) ; pass " singular test: filled (does not reference fct_daily_borough_stats -- check)"
215+ l5= $ (( l5 + 3 )) ; pass " singular test: filled (does not reference fct_daily_borough_stats -- check)"
216216 fi
217217else
218218 fail " singular test: empty or still a TODO stub"
219219fi
220- (( score += l5 ))
220+ score= $ (( score + l5 ))
221221pass " Level 5: tests ($l5 /15 pts)"
222222
223223# ── Level 6 (10 pts): documentation ─────────────────────────────────────────
@@ -226,7 +226,7 @@ mart_yml="$REPO_ROOT/models/marts/_fct_daily_borough_stats.yml"
226226
227227if [[ -f " $mart_yml " ]] && grep -qiE " grain|one row per" " $mart_yml " ; then
228228 if ! grep -B5 -A5 " grain\|one row per" " $mart_yml " | grep -qiE " TODO" ; then
229- (( l6 += 5 )) ; pass " mart YAML: grain stated in model description"
229+ l6= $ (( l6 + 5 )) ; pass " mart YAML: grain stated in model description"
230230 else
231231 fail " mart YAML: grain mentioned but description still has TODO -- fill it in"
232232 fi
237237if [[ -f " $mart_yml " ]]; then
238238 filled_descs=$( grep " description:" " $mart_yml " | grep -v " TODO" | grep -cvE ' ^\s*description:\s*("")?\s*$' || true)
239239 if [[ " $filled_descs " -ge 4 ]]; then
240- (( l6 += 5 )) ; pass " mart YAML: $filled_descs column descriptions filled"
240+ l6= $ (( l6 + 5 )) ; pass " mart YAML: $filled_descs column descriptions filled"
241241 elif [[ " $filled_descs " -ge 2 ]]; then
242- (( l6 += 3 )) ; warn " mart YAML: only $filled_descs column descriptions filled (target: all 6)"
242+ l6= $ (( l6 + 3 )) ; warn " mart YAML: only $filled_descs column descriptions filled (target: all 6)"
243243 else
244244 fail " mart YAML: column descriptions are empty -- explain meaning and units for each column"
245245 fi
246246fi
247- (( score += l6 ))
247+ score= $ (( score + l6 ))
248248pass " Level 6: documentation ($l6 /10 pts)"
249249
250250# ── Level 7 (10 pts): business answers + AI log ─────────────────────────────
@@ -255,11 +255,11 @@ ai="$REPO_ROOT/AI_ASSIST.md"
255255if file_is_filled " $ans " ; then
256256 sql_blocks=$( grep -c " ^\`\`\` sql" " $ans " 2> /dev/null || echo 0)
257257 if [[ " $sql_blocks " -ge 4 ]]; then
258- (( l7 += 5 )) ; pass " reports/answers.md: 4 SQL blocks present and filled"
258+ l7= $ (( l7 + 5 )) ; pass " reports/answers.md: 4 SQL blocks present and filled"
259259 elif [[ " $sql_blocks " -ge 2 ]]; then
260- (( l7 += 3 )) ; warn " reports/answers.md: $sql_blocks SQL block(s) present (need 4)"
260+ l7= $ (( l7 + 3 )) ; warn " reports/answers.md: $sql_blocks SQL block(s) present (need 4)"
261261 else
262- (( l7 += 2 )) ; warn " reports/answers.md: filled but SQL blocks missing"
262+ l7= $ (( l7 + 2 )) ; warn " reports/answers.md: filled but SQL blocks missing"
263263 fi
264264else
265265 fail " reports/answers.md: empty or still contains TODO stubs"
268268if file_is_filled " $ai " ; then
269269 chars=$( wc -c < " $ai " | tr -d ' ' )
270270 if [[ " $chars " -ge 800 ]]; then
271- (( l7 += 5 )) ; pass " AI_ASSIST.md: filled (${chars} chars)"
271+ l7= $ (( l7 + 5 )) ; pass " AI_ASSIST.md: filled (${chars} chars)"
272272 else
273- (( l7 += 2 )) ; warn " AI_ASSIST.md: present but brief (${chars} chars -- target 800+)"
273+ l7= $ (( l7 + 2 )) ; warn " AI_ASSIST.md: present but brief (${chars} chars -- target 800+)"
274274 fi
275275else
276276 fail " AI_ASSIST.md: empty or still contains TODO stubs"
277277fi
278- (( score += l7 ))
278+ score= $ (( score + l7 ))
279279pass " Level 7: business answers + AI log ($l7 /10 pts)"
280280
281281# ── Final ─────────────────────────────────────────────────────────────────────
0 commit comments