From 8d6a1340d53c9c1e9211b62539acc3fcb7ebebdf Mon Sep 17 00:00:00 2001 From: Harshit Sharma <66710144+harshitethic@users.noreply.github.com> Date: Sat, 19 Sep 2026 21:50:52 +0530 Subject: [PATCH 1/2] fix: publish scratch artifacts to content-addressed storage --- src/hflow/app.py | 57 +++++++++++++++++++++++++++++++++++------------- 1 file changed, 42 insertions(+), 15 deletions(-) diff --git a/src/hflow/app.py b/src/hflow/app.py index 0c4b6d3f..006de1b7 100644 --- a/src/hflow/app.py +++ b/src/hflow/app.py @@ -2803,23 +2803,50 @@ def render_contact_sheets(media_episode: Episode) -> EnrichmentResult: if enrichment_result is None: continue for artifact_name, artifact_path in enrichment_result.artifacts.items(): - resolved_artifact_path = artifact_path.resolve() - try: - artifact_relative_path = resolved_artifact_path.relative_to(run_dir.resolve()) - artifact_key = artifact_relative_path.as_posix() - except ValueError: - step_directory = ( - f"{_sanitize_topic(enrichment_run.enrichment.name)}-" - f"{enrichment_run.enrichment.version}" - ) - artifact_name_digest = hashlib.sha256(artifact_name.encode()).hexdigest()[:8] - artifact_key = ( - f"artifacts/{step_directory}/{_sanitize_topic(artifact_name)}-" - f"{artifact_name_digest}/{artifact_path.name}" - ) try: + resolved_artifact_path = artifact_path.resolve() + try: + resolved_artifact_path.relative_to(scratch_dir.resolve()) + artifact_is_scratch_bound = True + except ValueError: + artifact_is_scratch_bound = False + + if artifact_is_scratch_bound: + step_directory = ( + f"{_sanitize_topic(enrichment_run.enrichment.name)}-" + f"{enrichment_run.enrichment.version}" + ) + artifact_name_digest = hashlib.sha256(artifact_name.encode()).hexdigest()[:8] + with resolved_artifact_path.open("rb") as artifact_stream: + artifact_content_digest = hashlib.file_digest( + artifact_stream, "sha256" + ).hexdigest() + artifact_key = ( + f"artifacts/{step_directory}/{_sanitize_topic(artifact_name)}-" + f"{artifact_name_digest}/{artifact_content_digest}/" + f"{artifact_path.name}" + ) + else: + try: + artifact_relative_path = resolved_artifact_path.relative_to( + run_dir.resolve() + ) + artifact_key = artifact_relative_path.as_posix() + except ValueError: + step_directory = ( + f"{_sanitize_topic(enrichment_run.enrichment.name)}-" + f"{enrichment_run.enrichment.version}" + ) + artifact_name_digest = hashlib.sha256( + artifact_name.encode() + ).hexdigest()[:8] + artifact_key = ( + f"artifacts/{step_directory}/{_sanitize_topic(artifact_name)}-" + f"{artifact_name_digest}/{artifact_path.name}" + ) + enrichment_run.artifact_uris[artifact_name] = run_storage_root.publish( - artifact_path, artifact_key + resolved_artifact_path, artifact_key ) except Exception as error: # A missing or unreadable artifact file is the STEP's From 064f69db34f5cb23b4d58b86ca71b5bbdc54e58f Mon Sep 17 00:00:00 2001 From: Harshit Sharma <66710144+harshitethic@users.noreply.github.com> Date: Sat, 19 Sep 2026 21:50:56 +0530 Subject: [PATCH 2/2] test: require durable camera video artifact URIs --- tests/test_camera_video.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/test_camera_video.py b/tests/test_camera_video.py index b510f34f..76bad678 100644 --- a/tests/test_camera_video.py +++ b/tests/test_camera_video.py @@ -64,8 +64,11 @@ def test_camera_video_publishes_a_playable_mp4_per_camera_with_its_clock( } for topic in camera_topics: published_mp4 = Path(video_run.artifact_uris[video_artifact_name(topic)]) - # Published under the data root, not left in the scratch workdir. + # Published under durable, content-addressed storage, not scratch. assert published_mp4.is_relative_to(data_root) + assert "scratch" not in published_mp4.parts + assert "artifacts" in published_mp4.parts + assert len(published_mp4.parent.name) == 64 assert published_mp4.suffix == ".mp4" labels = video_run.result.labels assert labels[f"{topic}/video_fps"] == pytest.approx(15.0, rel=0.05)