@@ -224,13 +224,14 @@ def _is_build_ok(version_dir: Path) -> bool:
224224 Truth sources, in priority order:
225225 1. A `.build-ok` marker file → build completed successfully on a previous run.
226226 2. Implicit fallback for legacy builds that predate the marker:
227- - api.json or manifest.json exists, AND
228- - either outputs/ is missing (no notebooks) or every outputs/*.json reports
229- success: true.
230- This keeps healthy historical versions from being rebuilt unnecessarily.
231-
232- A build that produced any failed notebook output is NOT ok — the version is
233- rebuilt next run so the regression gets healed automatically.
227+ - manifest.json or api.json exists,
228+ - if notebooks/ contains any .ipynb files then outputs/ must contain at
229+ least as many *.json files (allowing fewer when some notebooks are
230+ marked non-executable — we accept a small slack), and
231+ - every outputs/*.json reports success: true.
232+
233+ A build with failed outputs OR a notebooks/ dir without matching outputs/ is
234+ NOT ok — it gets retried next run.
234235 """
235236 if not version_dir .exists ():
236237 return False
@@ -243,19 +244,27 @@ def _is_build_ok(version_dir: Path) -> bool:
243244 if not has_core :
244245 return False
245246
247+ notebooks_dir = version_dir / "notebooks"
246248 outputs_dir = version_dir / "outputs"
247- if not outputs_dir .exists ():
248- # No notebooks to execute → nothing could have failed
249- return True
250249
251- for output_file in outputs_dir .glob ("*.json" ):
252- try :
253- with open (output_file , "r" , encoding = "utf-8" ) as f :
254- data = json .load (f )
255- except (json .JSONDecodeError , OSError ):
256- return False
257- if data .get ("success" ) is False :
258- return False
250+ n_notebooks = len (list (notebooks_dir .glob ("*.ipynb" ))) if notebooks_dir .exists () else 0
251+ n_outputs = len (list (outputs_dir .glob ("*.json" ))) if outputs_dir .exists () else 0
252+
253+ # Notebooks present but nothing executed (e.g. cleaned-up outputs) → rebuild.
254+ # We don't require an exact match because some notebooks can legitimately be
255+ # marked non-executable in the manifest and won't produce an output file.
256+ if n_notebooks > 0 and n_outputs == 0 :
257+ return False
258+
259+ if outputs_dir .exists ():
260+ for output_file in outputs_dir .glob ("*.json" ):
261+ try :
262+ with open (output_file , "r" , encoding = "utf-8" ) as f :
263+ data = json .load (f )
264+ except (json .JSONDecodeError , OSError ):
265+ return False
266+ if data .get ("success" ) is False :
267+ return False
259268
260269 return True
261270
0 commit comments