data-designer 0.9.2__tar.gz → 0.9.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (148) hide show
  1. {data_designer-0.9.2 → data_designer-0.9.3}/PKG-INFO +3 -3
  2. {data_designer-0.9.2 → data_designer-0.9.3}/src/data_designer/integrations/huggingface/client.py +8 -8
  3. {data_designer-0.9.2 → data_designer-0.9.3}/src/data_designer/interface/__init__.py +22 -0
  4. {data_designer-0.9.2 → data_designer-0.9.3}/src/data_designer/interface/composite_workflow.py +41 -4
  5. {data_designer-0.9.2 → data_designer-0.9.3}/src/data_designer/interface/data_designer.py +6 -3
  6. data_designer-0.9.3/src/data_designer/interface/workflow_metadata.py +97 -0
  7. data_designer-0.9.3/tests/docs/test_chart_qa_recipe.py +136 -0
  8. {data_designer-0.9.2 → data_designer-0.9.3}/tests/interface/test_composite_workflow.py +107 -1
  9. {data_designer-0.9.2 → data_designer-0.9.3}/tests/interface/test_data_designer.py +115 -0
  10. data_designer-0.9.3/tests/interface/test_workflow_metadata.py +191 -0
  11. {data_designer-0.9.2 → data_designer-0.9.3}/.gitignore +0 -0
  12. {data_designer-0.9.2 → data_designer-0.9.3}/README.md +0 -0
  13. {data_designer-0.9.2 → data_designer-0.9.3}/dev-tools/hatch_build.py +0 -0
  14. {data_designer-0.9.2 → data_designer-0.9.3}/pyproject.toml +0 -0
  15. {data_designer-0.9.2 → data_designer-0.9.3}/src/data_designer/cli/README.md +0 -0
  16. {data_designer-0.9.2 → data_designer-0.9.3}/src/data_designer/cli/__init__.py +0 -0
  17. {data_designer-0.9.2 → data_designer-0.9.3}/src/data_designer/cli/agent_command_defs.py +0 -0
  18. {data_designer-0.9.2 → data_designer-0.9.3}/src/data_designer/cli/commands/__init__.py +0 -0
  19. {data_designer-0.9.2 → data_designer-0.9.3}/src/data_designer/cli/commands/agent.py +0 -0
  20. {data_designer-0.9.2 → data_designer-0.9.3}/src/data_designer/cli/commands/check_models.py +0 -0
  21. {data_designer-0.9.2 → data_designer-0.9.3}/src/data_designer/cli/commands/create.py +0 -0
  22. {data_designer-0.9.2 → data_designer-0.9.3}/src/data_designer/cli/commands/download.py +0 -0
  23. {data_designer-0.9.2 → data_designer-0.9.3}/src/data_designer/cli/commands/list.py +0 -0
  24. {data_designer-0.9.2 → data_designer-0.9.3}/src/data_designer/cli/commands/mcp.py +0 -0
  25. {data_designer-0.9.2 → data_designer-0.9.3}/src/data_designer/cli/commands/models.py +0 -0
  26. {data_designer-0.9.2 → data_designer-0.9.3}/src/data_designer/cli/commands/plugin.py +0 -0
  27. {data_designer-0.9.2 → data_designer-0.9.3}/src/data_designer/cli/commands/preview.py +0 -0
  28. {data_designer-0.9.2 → data_designer-0.9.3}/src/data_designer/cli/commands/providers.py +0 -0
  29. {data_designer-0.9.2 → data_designer-0.9.3}/src/data_designer/cli/commands/reset.py +0 -0
  30. {data_designer-0.9.2 → data_designer-0.9.3}/src/data_designer/cli/commands/tools.py +0 -0
  31. {data_designer-0.9.2 → data_designer-0.9.3}/src/data_designer/cli/commands/validate.py +0 -0
  32. {data_designer-0.9.2 → data_designer-0.9.3}/src/data_designer/cli/controllers/__init__.py +0 -0
  33. {data_designer-0.9.2 → data_designer-0.9.3}/src/data_designer/cli/controllers/download_controller.py +0 -0
  34. {data_designer-0.9.2 → data_designer-0.9.3}/src/data_designer/cli/controllers/generation_controller.py +0 -0
  35. {data_designer-0.9.2 → data_designer-0.9.3}/src/data_designer/cli/controllers/mcp_provider_controller.py +0 -0
  36. {data_designer-0.9.2 → data_designer-0.9.3}/src/data_designer/cli/controllers/model_controller.py +0 -0
  37. {data_designer-0.9.2 → data_designer-0.9.3}/src/data_designer/cli/controllers/plugin_catalog_controller.py +0 -0
  38. {data_designer-0.9.2 → data_designer-0.9.3}/src/data_designer/cli/controllers/provider_controller.py +0 -0
  39. {data_designer-0.9.2 → data_designer-0.9.3}/src/data_designer/cli/controllers/tool_controller.py +0 -0
  40. {data_designer-0.9.2 → data_designer-0.9.3}/src/data_designer/cli/forms/__init__.py +0 -0
  41. {data_designer-0.9.2 → data_designer-0.9.3}/src/data_designer/cli/forms/builder.py +0 -0
  42. {data_designer-0.9.2 → data_designer-0.9.3}/src/data_designer/cli/forms/field.py +0 -0
  43. {data_designer-0.9.2 → data_designer-0.9.3}/src/data_designer/cli/forms/form.py +0 -0
  44. {data_designer-0.9.2 → data_designer-0.9.3}/src/data_designer/cli/forms/mcp_provider_builder.py +0 -0
  45. {data_designer-0.9.2 → data_designer-0.9.3}/src/data_designer/cli/forms/model_builder.py +0 -0
  46. {data_designer-0.9.2 → data_designer-0.9.3}/src/data_designer/cli/forms/provider_builder.py +0 -0
  47. {data_designer-0.9.2 → data_designer-0.9.3}/src/data_designer/cli/forms/tool_builder.py +0 -0
  48. {data_designer-0.9.2 → data_designer-0.9.3}/src/data_designer/cli/lazy_group.py +0 -0
  49. {data_designer-0.9.2 → data_designer-0.9.3}/src/data_designer/cli/main.py +0 -0
  50. {data_designer-0.9.2 → data_designer-0.9.3}/src/data_designer/cli/plugin_catalog.py +0 -0
  51. {data_designer-0.9.2 → data_designer-0.9.3}/src/data_designer/cli/repositories/__init__.py +0 -0
  52. {data_designer-0.9.2 → data_designer-0.9.3}/src/data_designer/cli/repositories/base.py +0 -0
  53. {data_designer-0.9.2 → data_designer-0.9.3}/src/data_designer/cli/repositories/mcp_provider_repository.py +0 -0
  54. {data_designer-0.9.2 → data_designer-0.9.3}/src/data_designer/cli/repositories/model_repository.py +0 -0
  55. {data_designer-0.9.2 → data_designer-0.9.3}/src/data_designer/cli/repositories/persona_repository.py +0 -0
  56. {data_designer-0.9.2 → data_designer-0.9.3}/src/data_designer/cli/repositories/plugin_catalog_repository.py +0 -0
  57. {data_designer-0.9.2 → data_designer-0.9.3}/src/data_designer/cli/repositories/provider_repository.py +0 -0
  58. {data_designer-0.9.2 → data_designer-0.9.3}/src/data_designer/cli/repositories/tool_repository.py +0 -0
  59. {data_designer-0.9.2 → data_designer-0.9.3}/src/data_designer/cli/runtime.py +0 -0
  60. {data_designer-0.9.2 → data_designer-0.9.3}/src/data_designer/cli/services/__init__.py +0 -0
  61. {data_designer-0.9.2 → data_designer-0.9.3}/src/data_designer/cli/services/download_service.py +0 -0
  62. {data_designer-0.9.2 → data_designer-0.9.3}/src/data_designer/cli/services/mcp_provider_service.py +0 -0
  63. {data_designer-0.9.2 → data_designer-0.9.3}/src/data_designer/cli/services/model_service.py +0 -0
  64. {data_designer-0.9.2 → data_designer-0.9.3}/src/data_designer/cli/services/plugin_catalog_service.py +0 -0
  65. {data_designer-0.9.2 → data_designer-0.9.3}/src/data_designer/cli/services/plugin_install_service.py +0 -0
  66. {data_designer-0.9.2 → data_designer-0.9.3}/src/data_designer/cli/services/provider_service.py +0 -0
  67. {data_designer-0.9.2 → data_designer-0.9.3}/src/data_designer/cli/services/tool_service.py +0 -0
  68. {data_designer-0.9.2 → data_designer-0.9.3}/src/data_designer/cli/ui.py +0 -0
  69. {data_designer-0.9.2 → data_designer-0.9.3}/src/data_designer/cli/utils/__init__.py +0 -0
  70. {data_designer-0.9.2 → data_designer-0.9.3}/src/data_designer/cli/utils/agent_introspection.py +0 -0
  71. {data_designer-0.9.2 → data_designer-0.9.3}/src/data_designer/cli/utils/agent_text_formatter.py +0 -0
  72. {data_designer-0.9.2 → data_designer-0.9.3}/src/data_designer/cli/utils/config_loader.py +0 -0
  73. {data_designer-0.9.2 → data_designer-0.9.3}/src/data_designer/cli/utils/sample_records_pager.py +0 -0
  74. {data_designer-0.9.2 → data_designer-0.9.3}/src/data_designer/cli/version_notice.py +0 -0
  75. {data_designer-0.9.2 → data_designer-0.9.3}/src/data_designer/integrations/huggingface/__init__.py +0 -0
  76. {data_designer-0.9.2 → data_designer-0.9.3}/src/data_designer/integrations/huggingface/dataset_card.py +0 -0
  77. {data_designer-0.9.2 → data_designer-0.9.3}/src/data_designer/integrations/huggingface/dataset_card_template.md +0 -0
  78. {data_designer-0.9.2 → data_designer-0.9.3}/src/data_designer/integrations/opentelemetry.py +0 -0
  79. {data_designer-0.9.2 → data_designer-0.9.3}/src/data_designer/interface/errors.py +0 -0
  80. {data_designer-0.9.2 → data_designer-0.9.3}/src/data_designer/interface/results.py +0 -0
  81. {data_designer-0.9.2 → data_designer-0.9.3}/tests/cli/commands/test_agent_command.py +0 -0
  82. {data_designer-0.9.2 → data_designer-0.9.3}/tests/cli/commands/test_check_models_command.py +0 -0
  83. {data_designer-0.9.2 → data_designer-0.9.3}/tests/cli/commands/test_create_command.py +0 -0
  84. {data_designer-0.9.2 → data_designer-0.9.3}/tests/cli/commands/test_download_command.py +0 -0
  85. {data_designer-0.9.2 → data_designer-0.9.3}/tests/cli/commands/test_list_command.py +0 -0
  86. {data_designer-0.9.2 → data_designer-0.9.3}/tests/cli/commands/test_models_command.py +0 -0
  87. {data_designer-0.9.2 → data_designer-0.9.3}/tests/cli/commands/test_plugin_command.py +0 -0
  88. {data_designer-0.9.2 → data_designer-0.9.3}/tests/cli/commands/test_preview_command.py +0 -0
  89. {data_designer-0.9.2 → data_designer-0.9.3}/tests/cli/commands/test_providers_command.py +0 -0
  90. {data_designer-0.9.2 → data_designer-0.9.3}/tests/cli/commands/test_reset_command.py +0 -0
  91. {data_designer-0.9.2 → data_designer-0.9.3}/tests/cli/commands/test_validate_command.py +0 -0
  92. {data_designer-0.9.2 → data_designer-0.9.3}/tests/cli/conftest.py +0 -0
  93. {data_designer-0.9.2 → data_designer-0.9.3}/tests/cli/controllers/test_download_controller.py +0 -0
  94. {data_designer-0.9.2 → data_designer-0.9.3}/tests/cli/controllers/test_generation_controller.py +0 -0
  95. {data_designer-0.9.2 → data_designer-0.9.3}/tests/cli/controllers/test_mcp_provider_controller.py +0 -0
  96. {data_designer-0.9.2 → data_designer-0.9.3}/tests/cli/controllers/test_model_controller.py +0 -0
  97. {data_designer-0.9.2 → data_designer-0.9.3}/tests/cli/controllers/test_plugin_catalog_controller.py +0 -0
  98. {data_designer-0.9.2 → data_designer-0.9.3}/tests/cli/controllers/test_provider_controller.py +0 -0
  99. {data_designer-0.9.2 → data_designer-0.9.3}/tests/cli/controllers/test_tool_controller.py +0 -0
  100. {data_designer-0.9.2 → data_designer-0.9.3}/tests/cli/fixtures/upstream-catalogs/catalog-invalid-install.json +0 -0
  101. {data_designer-0.9.2 → data_designer-0.9.3}/tests/cli/fixtures/upstream-catalogs/catalog-unsupported-version.json +0 -0
  102. {data_designer-0.9.2 → data_designer-0.9.3}/tests/cli/fixtures/upstream-catalogs/catalog-valid.json +0 -0
  103. {data_designer-0.9.2 → data_designer-0.9.3}/tests/cli/forms/test_field.py +0 -0
  104. {data_designer-0.9.2 → data_designer-0.9.3}/tests/cli/forms/test_form.py +0 -0
  105. {data_designer-0.9.2 → data_designer-0.9.3}/tests/cli/forms/test_mcp_provider_builder.py +0 -0
  106. {data_designer-0.9.2 → data_designer-0.9.3}/tests/cli/forms/test_model_builder.py +0 -0
  107. {data_designer-0.9.2 → data_designer-0.9.3}/tests/cli/forms/test_provider_builder.py +0 -0
  108. {data_designer-0.9.2 → data_designer-0.9.3}/tests/cli/forms/test_tool_builder.py +0 -0
  109. {data_designer-0.9.2 → data_designer-0.9.3}/tests/cli/repositories/test_mcp_provider_repository.py +0 -0
  110. {data_designer-0.9.2 → data_designer-0.9.3}/tests/cli/repositories/test_model_repository.py +0 -0
  111. {data_designer-0.9.2 → data_designer-0.9.3}/tests/cli/repositories/test_persona_repository.py +0 -0
  112. {data_designer-0.9.2 → data_designer-0.9.3}/tests/cli/repositories/test_plugin_catalog_repository.py +0 -0
  113. {data_designer-0.9.2 → data_designer-0.9.3}/tests/cli/repositories/test_provider_repository.py +0 -0
  114. {data_designer-0.9.2 → data_designer-0.9.3}/tests/cli/repositories/test_tool_repository.py +0 -0
  115. {data_designer-0.9.2 → data_designer-0.9.3}/tests/cli/services/test_download_service.py +0 -0
  116. {data_designer-0.9.2 → data_designer-0.9.3}/tests/cli/services/test_mcp_provider_service.py +0 -0
  117. {data_designer-0.9.2 → data_designer-0.9.3}/tests/cli/services/test_model_service.py +0 -0
  118. {data_designer-0.9.2 → data_designer-0.9.3}/tests/cli/services/test_plugin_catalog_service.py +0 -0
  119. {data_designer-0.9.2 → data_designer-0.9.3}/tests/cli/services/test_plugin_install_service.py +0 -0
  120. {data_designer-0.9.2 → data_designer-0.9.3}/tests/cli/services/test_provider_service.py +0 -0
  121. {data_designer-0.9.2 → data_designer-0.9.3}/tests/cli/services/test_tool_service.py +0 -0
  122. {data_designer-0.9.2 → data_designer-0.9.3}/tests/cli/test_cli_utils.py +0 -0
  123. {data_designer-0.9.2 → data_designer-0.9.3}/tests/cli/test_main.py +0 -0
  124. {data_designer-0.9.2 → data_designer-0.9.3}/tests/cli/test_plugin_catalog.py +0 -0
  125. {data_designer-0.9.2 → data_designer-0.9.3}/tests/cli/test_runtime.py +0 -0
  126. {data_designer-0.9.2 → data_designer-0.9.3}/tests/cli/test_version_notice.py +0 -0
  127. {data_designer-0.9.2 → data_designer-0.9.3}/tests/cli/utils/test_agent_introspection.py +0 -0
  128. {data_designer-0.9.2 → data_designer-0.9.3}/tests/cli/utils/test_agent_introspection_integration.py +0 -0
  129. {data_designer-0.9.2 → data_designer-0.9.3}/tests/cli/utils/test_agent_text_formatter.py +0 -0
  130. {data_designer-0.9.2 → data_designer-0.9.3}/tests/cli/utils/test_config_loader.py +0 -0
  131. {data_designer-0.9.2 → data_designer-0.9.3}/tests/cli/utils/test_sample_records_pager.py +0 -0
  132. {data_designer-0.9.2 → data_designer-0.9.3}/tests/conftest.py +0 -0
  133. {data_designer-0.9.2 → data_designer-0.9.3}/tests/docs/test_build_notebooks_cached.py +0 -0
  134. {data_designer-0.9.2 → data_designer-0.9.3}/tests/docs/test_docs_workflows.py +0 -0
  135. {data_designer-0.9.2 → data_designer-0.9.3}/tests/docs/test_document_review_gate_recipe.py +0 -0
  136. {data_designer-0.9.2 → data_designer-0.9.3}/tests/docs/test_fern_notebook_snapshot.py +0 -0
  137. {data_designer-0.9.2 → data_designer-0.9.3}/tests/docs/test_fern_published_branch.py +0 -0
  138. {data_designer-0.9.2 → data_designer-0.9.3}/tests/docs/test_image_generation_recipes.py +0 -0
  139. {data_designer-0.9.2 → data_designer-0.9.3}/tests/integrations/huggingface/__init__.py +0 -0
  140. {data_designer-0.9.2 → data_designer-0.9.3}/tests/integrations/huggingface/test_client.py +0 -0
  141. {data_designer-0.9.2 → data_designer-0.9.3}/tests/integrations/huggingface/test_dataset_card.py +0 -0
  142. {data_designer-0.9.2 → data_designer-0.9.3}/tests/integrations/test_opentelemetry.py +0 -0
  143. {data_designer-0.9.2 → data_designer-0.9.3}/tests/interface/test_acreate.py +0 -0
  144. {data_designer-0.9.2 → data_designer-0.9.3}/tests/interface/test_results.py +0 -0
  145. {data_designer-0.9.2 → data_designer-0.9.3}/tests/test_agentic_ci_attempts.py +0 -0
  146. {data_designer-0.9.2 → data_designer-0.9.3}/tests/test_dependency_audit.py +0 -0
  147. {data_designer-0.9.2 → data_designer-0.9.3}/tests/test_import_perf.py +0 -0
  148. {data_designer-0.9.2 → data_designer-0.9.3}/tests/test_lazy_imports.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: data-designer
3
- Version: 0.9.2
3
+ Version: 0.9.3
4
4
  Summary: General framework for synthetic data generation
5
5
  License-Expression: Apache-2.0
6
6
  Classifier: Development Status :: 4 - Beta
@@ -15,8 +15,8 @@ Classifier: Programming Language :: Python :: 3.14
15
15
  Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
16
16
  Classifier: Topic :: Software Development
17
17
  Requires-Python: >=3.10
18
- Requires-Dist: data-designer-config==0.9.2
19
- Requires-Dist: data-designer-engine==0.9.2
18
+ Requires-Dist: data-designer-config==0.9.3
19
+ Requires-Dist: data-designer-engine==0.9.3
20
20
  Requires-Dist: huggingface-hub<2,>=1.0.1
21
21
  Requires-Dist: opentelemetry-api<1.44,>=1.43
22
22
  Requires-Dist: opentelemetry-exporter-prometheus<0.65,>=0.64b0
@@ -119,7 +119,7 @@ class HuggingFaceHubClient:
119
119
  URL to the uploaded dataset
120
120
 
121
121
  Raises:
122
- HuggingFaceUploadError: If validation fails or upload encounters errors
122
+ HuggingFaceHubClientUploadError: If validation fails or upload encounters errors
123
123
  """
124
124
  logger.info(f"🤗 Uploading dataset to Hugging Face Hub: {repo_id}")
125
125
 
@@ -162,7 +162,7 @@ class HuggingFaceHubClient:
162
162
  private: Whether to create private repo
163
163
 
164
164
  Raises:
165
- HuggingFaceUploadError: If repository creation fails
165
+ HuggingFaceHubClientUploadError: If repository creation fails
166
166
  """
167
167
  logger.info(f"{LOG_INDENT}{RandomEmoji.working()} Checking if repository exists...")
168
168
  try:
@@ -203,7 +203,7 @@ class HuggingFaceHubClient:
203
203
  parquet_folder: Path to folder containing parquet files
204
204
 
205
205
  Raises:
206
- HuggingFaceUploadError: If upload fails
206
+ HuggingFaceHubClientUploadError: If upload fails
207
207
  """
208
208
  logger.info(f"{LOG_INDENT}{RandomEmoji.loading()} Uploading main dataset files...")
209
209
  try:
@@ -225,7 +225,7 @@ class HuggingFaceHubClient:
225
225
  images_folder: Path to images folder
226
226
 
227
227
  Raises:
228
- HuggingFaceUploadError: If upload fails
228
+ HuggingFaceHubClientUploadError: If upload fails
229
229
  """
230
230
  if not images_folder.exists():
231
231
  return
@@ -255,7 +255,7 @@ class HuggingFaceHubClient:
255
255
  processors_folder: Path to folder containing processor output directories
256
256
 
257
257
  Raises:
258
- HuggingFaceUploadError: If upload fails
258
+ HuggingFaceHubClientUploadError: If upload fails
259
259
  """
260
260
  if not processors_folder.exists():
261
261
  return
@@ -290,7 +290,7 @@ class HuggingFaceHubClient:
290
290
  builder_config_path: Path to builder_config.json file
291
291
 
292
292
  Raises:
293
- HuggingFaceUploadError: If upload fails
293
+ HuggingFaceHubClientUploadError: If upload fails
294
294
  """
295
295
  logger.info(f"{LOG_INDENT}{RandomEmoji.loading()} Uploading configuration files...")
296
296
 
@@ -345,7 +345,7 @@ class HuggingFaceHubClient:
345
345
  tags: Additional custom tags for the dataset
346
346
 
347
347
  Raises:
348
- HuggingFaceUploadError: If dataset card generation or upload fails
348
+ HuggingFaceHubClientUploadError: If dataset card generation or upload fails
349
349
  """
350
350
  try:
351
351
  with open(metadata_path) as f:
@@ -455,7 +455,7 @@ class HuggingFaceHubClient:
455
455
  base_dataset_path: Path to dataset directory
456
456
 
457
457
  Raises:
458
- HuggingFaceUploadError: If directory structure is invalid
458
+ HuggingFaceHubClientUploadError: If directory structure is invalid
459
459
  """
460
460
  if not base_dataset_path.exists():
461
461
  raise HuggingFaceHubClientUploadError(f"Dataset path does not exist: {base_dataset_path}")
@@ -22,19 +22,41 @@ if TYPE_CHECKING:
22
22
  DataDesignerWorkflowError,
23
23
  )
24
24
  from data_designer.interface.results import DatasetCreationResults # noqa: F401
25
+ from data_designer.interface.workflow_metadata import ( # noqa: F401
26
+ CompletedWorkflowStageMetadata,
27
+ FailedWorkflowStageMetadata,
28
+ RunningWorkflowStageMetadata,
29
+ SkippedWorkflowStageMetadata,
30
+ WorkflowMetadata,
31
+ WorkflowStageMetadata,
32
+ validate_workflow_stage_metadata,
33
+ )
25
34
 
26
35
  _LAZY_IMPORTS: dict[str, tuple[str, str]] = {
27
36
  "CompositeWorkflow": ("data_designer.interface.composite_workflow", "CompositeWorkflow"),
28
37
  "CompositeWorkflowResults": ("data_designer.interface.composite_workflow", "CompositeWorkflowResults"),
38
+ "CompletedWorkflowStageMetadata": (
39
+ "data_designer.interface.workflow_metadata",
40
+ "CompletedWorkflowStageMetadata",
41
+ ),
29
42
  "DataDesigner": ("data_designer.interface.data_designer", "DataDesigner"),
30
43
  "DataDesignerEarlyShutdownError": ("data_designer.interface.errors", "DataDesignerEarlyShutdownError"),
31
44
  "DataDesignerGenerationError": ("data_designer.interface.errors", "DataDesignerGenerationError"),
32
45
  "DataDesignerProfilingError": ("data_designer.interface.errors", "DataDesignerProfilingError"),
33
46
  "DataDesignerWorkflowError": ("data_designer.interface.errors", "DataDesignerWorkflowError"),
34
47
  "DatasetCreationResults": ("data_designer.interface.results", "DatasetCreationResults"),
48
+ "FailedWorkflowStageMetadata": ("data_designer.interface.workflow_metadata", "FailedWorkflowStageMetadata"),
35
49
  "ResumeMode": ("data_designer.config.run_config", "ResumeMode"),
50
+ "RunningWorkflowStageMetadata": ("data_designer.interface.workflow_metadata", "RunningWorkflowStageMetadata"),
36
51
  "SkippedStageResult": ("data_designer.interface.composite_workflow", "SkippedStageResult"),
37
52
  "SkippedStageStatus": ("data_designer.interface.composite_workflow", "SkippedStageStatus"),
53
+ "SkippedWorkflowStageMetadata": ("data_designer.interface.workflow_metadata", "SkippedWorkflowStageMetadata"),
54
+ "WorkflowMetadata": ("data_designer.interface.workflow_metadata", "WorkflowMetadata"),
55
+ "WorkflowStageMetadata": ("data_designer.interface.workflow_metadata", "WorkflowStageMetadata"),
56
+ "validate_workflow_stage_metadata": (
57
+ "data_designer.interface.workflow_metadata",
58
+ "validate_workflow_stage_metadata",
59
+ ),
38
60
  }
39
61
 
40
62
  __all__ = list(_LAZY_IMPORTS.keys())
@@ -40,6 +40,7 @@ from data_designer.interface.results import (
40
40
  _export_jsonl,
41
41
  _export_parquet,
42
42
  )
43
+ from data_designer.interface.workflow_metadata import WorkflowMetadata, validate_workflow_stage_metadata
43
44
 
44
45
  if TYPE_CHECKING:
45
46
  import pandas as pd
@@ -219,8 +220,19 @@ class CompositeWorkflow:
219
220
  _validate_dir_name(name, "stage name")
220
221
  if any(stage.name == name for stage in self._stages):
221
222
  raise DataDesignerWorkflowError(f"Stage name {name!r} is already used in workflow {self.name!r}.")
222
- if num_records is not None and num_records < 1:
223
- raise DataDesignerWorkflowError("Stage num_records must be at least 1.")
223
+ if num_records is not None:
224
+ if not isinstance(num_records, int) or isinstance(num_records, bool):
225
+ raise DataDesignerWorkflowError("Stage num_records must be an integer.")
226
+ if num_records < 1:
227
+ raise DataDesignerWorkflowError("Stage num_records must be at least 1.")
228
+ if on_success_version is not None and not isinstance(on_success_version, str):
229
+ raise DataDesignerWorkflowError("Stage on_success_version must be a string.")
230
+ if not isinstance(allow_empty, bool):
231
+ raise DataDesignerWorkflowError("Stage allow_empty must be a boolean.")
232
+ if not isinstance(sampling_strategy, SamplingStrategy):
233
+ raise DataDesignerWorkflowError("Stage sampling_strategy must be a SamplingStrategy.")
234
+ if selection_strategy is not None and not isinstance(selection_strategy, (IndexRange, PartitionBlock)):
235
+ raise DataDesignerWorkflowError("Stage selection_strategy must be an IndexRange or PartitionBlock.")
224
236
  _validate_stage_output(output)
225
237
  output_processors = output_processors or []
226
238
  _validate_distinct_output_processors(config_builder, output_processors)
@@ -276,6 +288,7 @@ class CompositeWorkflow:
276
288
  workflow_path.mkdir(parents=True, exist_ok=True)
277
289
  prior_metadata = _read_prior_workflow_metadata(workflow_path, self.name, resume)
278
290
  metadata: dict[str, Any] = {
291
+ **(prior_metadata or {}),
279
292
  "name": self.name,
280
293
  "library_version": get_library_version(),
281
294
  "stages": [],
@@ -388,6 +401,10 @@ class CompositeWorkflow:
388
401
  f"Cannot resume workflow {self.name!r}: stage {stage.name!r} is not reusable."
389
402
  )
390
403
 
404
+ if stage_resume == ResumeMode.ALWAYS and prior_stage_metadata is not None:
405
+ prior_stage = validate_workflow_stage_metadata(prior_stage_metadata)
406
+ stage_metadata.update(prior_stage.model_extra or {})
407
+
391
408
  if stage_resume == ResumeMode.NEVER and stage_path.exists():
392
409
  shutil.rmtree(stage_path)
393
410
 
@@ -598,6 +615,22 @@ def _read_prior_workflow_metadata(
598
615
  raise DataDesignerWorkflowError(
599
616
  f"Cannot resume workflow {workflow_name!r}: workflow metadata has invalid shape."
600
617
  )
618
+ try:
619
+ validated_metadata = WorkflowMetadata.model_validate(metadata)
620
+ except ValidationError as exc:
621
+ if resume != ResumeMode.ALWAYS:
622
+ error = exc.errors(include_url=False, include_input=False)[0]
623
+ location = ".".join(str(part) for part in error["loc"])
624
+ logger.warning(
625
+ "Workflow metadata for %r has invalid field %s (%s); starting fresh.",
626
+ workflow_name,
627
+ location,
628
+ error["msg"],
629
+ )
630
+ return None
631
+ raise DataDesignerWorkflowError(
632
+ f"Cannot resume workflow {workflow_name!r}: workflow metadata has invalid shape."
633
+ ) from exc
601
634
  if metadata.get("name") != workflow_name:
602
635
  if resume != ResumeMode.ALWAYS:
603
636
  logger.warning("Workflow metadata for %r has a different name; starting fresh.", workflow_name)
@@ -605,7 +638,7 @@ def _read_prior_workflow_metadata(
605
638
  raise DataDesignerWorkflowError(
606
639
  f"Cannot resume workflow {workflow_name!r}: workflow metadata name does not match."
607
640
  )
608
- return metadata
641
+ return validated_metadata.model_dump(mode="json", exclude_unset=True)
609
642
 
610
643
 
611
644
  def _get_prior_stage_metadata(
@@ -870,9 +903,13 @@ def _parquet_files(path: Path) -> list[Path]:
870
903
  def _write_workflow_metadata(workflow_path: Path, metadata: dict[str, Any]) -> None:
871
904
  path = workflow_path / WORKFLOW_METADATA_FILENAME
872
905
  tmp_path = path.with_name(f"{path.name}.tmp.{os.getpid()}.{uuid.uuid4().hex}")
906
+ try:
907
+ validated_metadata = WorkflowMetadata.model_validate(metadata)
908
+ except ValidationError as exc:
909
+ raise DataDesignerWorkflowError("Workflow metadata has invalid shape.") from exc
873
910
  try:
874
911
  with tmp_path.open("w", encoding="utf-8") as f:
875
- json.dump(metadata, f, indent=2, sort_keys=True)
912
+ json.dump(validated_metadata.model_dump(mode="json", exclude_unset=True), f, indent=2, sort_keys=True)
876
913
  f.flush()
877
914
  os.fsync(f.fileno())
878
915
  os.replace(tmp_path, path)
@@ -354,11 +354,10 @@ class DataDesigner(DataDesignerInterface[DatasetCreationResults]):
354
354
  )
355
355
 
356
356
  try:
357
- profiler_column_configs = config_builder.get_column_configs() or builder.data_designer_config.columns
358
357
  profiler = self._create_dataset_profiler(
359
358
  config_builder,
360
359
  resource_provider,
361
- column_configs=profiler_column_configs,
360
+ column_configs=builder.data_designer_config.columns,
362
361
  )
363
362
  analysis = profiler.profile_dataset(num_records, dataset_for_profiler)
364
363
  except Exception as e:
@@ -457,7 +456,11 @@ class DataDesigner(DataDesignerInterface[DatasetCreationResults]):
457
456
  dataset_for_profiler = processed_dataset
458
457
 
459
458
  try:
460
- profiler = self._create_dataset_profiler(config_builder, resource_provider)
459
+ profiler = self._create_dataset_profiler(
460
+ config_builder,
461
+ resource_provider,
462
+ column_configs=builder.data_designer_config.columns,
463
+ )
461
464
  analysis = profiler.profile_dataset(num_records, dataset_for_profiler)
462
465
  except Exception as e:
463
466
  raise DataDesignerProfilingError(f"🛑 Error profiling preview dataset: {e}") from e
@@ -0,0 +1,97 @@
1
+ # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ from __future__ import annotations
5
+
6
+ from typing import Annotated, Any, Literal, TypeAlias
7
+
8
+ from pydantic import BaseModel, ConfigDict, Field, TypeAdapter
9
+
10
+
11
+ class _WorkflowStageMetadataBase(BaseModel):
12
+ """Common metadata for a composite workflow stage."""
13
+
14
+ model_config = ConfigDict(extra="allow")
15
+
16
+ status: Literal["running", "failed", "completed", "completed_empty", "skipped_empty_upstream"]
17
+ index: int
18
+ name: str
19
+ stage_dir: str
20
+ depends_on: list[str]
21
+ allow_empty: bool
22
+ on_success_version: str | None
23
+ output_processors: list[dict[str, Any]]
24
+ output: str
25
+ sampling_strategy: str
26
+ selection_strategy: dict[str, Any] | None
27
+
28
+
29
+ class _StartedWorkflowStageMetadata(_WorkflowStageMetadataBase):
30
+ fingerprint: str
31
+ num_records_requested: int
32
+ seeded_from_stage: str | None
33
+ seed_path: str | None
34
+ config: dict[str, Any]
35
+
36
+
37
+ class RunningWorkflowStageMetadata(_StartedWorkflowStageMetadata):
38
+ """Metadata for a running workflow stage."""
39
+
40
+ status: Literal["running"]
41
+
42
+
43
+ class FailedWorkflowStageMetadata(_StartedWorkflowStageMetadata):
44
+ """Metadata for a failed workflow stage."""
45
+
46
+ status: Literal["failed"]
47
+ duration_sec: float | None = None
48
+
49
+
50
+ class CompletedWorkflowStageMetadata(_StartedWorkflowStageMetadata):
51
+ """Metadata for a completed workflow stage."""
52
+
53
+ status: Literal["completed", "completed_empty"]
54
+ num_records_actual: int
55
+ output_records: int
56
+ output_seed_path: str
57
+ callback_output_path: str | None
58
+ stage_output_override_path: str | None = None
59
+ output_processor_output_path: str | None
60
+ duration_sec: float
61
+
62
+
63
+ class SkippedWorkflowStageMetadata(_WorkflowStageMetadataBase):
64
+ """Metadata for a stage skipped after an empty upstream stage."""
65
+
66
+ status: Literal["skipped_empty_upstream"]
67
+ upstream_stage: str
68
+
69
+
70
+ WorkflowStageMetadata: TypeAlias = Annotated[
71
+ RunningWorkflowStageMetadata
72
+ | FailedWorkflowStageMetadata
73
+ | CompletedWorkflowStageMetadata
74
+ | SkippedWorkflowStageMetadata,
75
+ Field(discriminator="status"),
76
+ ]
77
+
78
+ _WORKFLOW_STAGE_METADATA_ADAPTER = TypeAdapter(WorkflowStageMetadata)
79
+
80
+
81
+ def validate_workflow_stage_metadata(value: object) -> WorkflowStageMetadata:
82
+ """Validate the stage union and return its concrete status model.
83
+
84
+ Use the concrete status models for runtime type checks.
85
+ """
86
+
87
+ return _WORKFLOW_STAGE_METADATA_ADAPTER.validate_python(value)
88
+
89
+
90
+ class WorkflowMetadata(BaseModel):
91
+ """Metadata persisted for a composite workflow run."""
92
+
93
+ model_config = ConfigDict(extra="allow")
94
+
95
+ name: str
96
+ library_version: str
97
+ stages: list[WorkflowStageMetadata]
@@ -0,0 +1,136 @@
1
+ # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ from __future__ import annotations
5
+
6
+ import importlib.util
7
+ from pathlib import Path
8
+ from types import ModuleType
9
+ from unittest.mock import MagicMock
10
+
11
+ import pytest
12
+
13
+ REPO_ROOT = Path(__file__).resolve().parents[4]
14
+ RECIPE_PATH = REPO_ROOT / "docs" / "assets" / "recipes" / "chart_qa" / "chart_qa.py"
15
+
16
+
17
+ def _load_recipe() -> ModuleType:
18
+ spec = importlib.util.spec_from_file_location("chart_qa_recipe", RECIPE_PATH)
19
+ assert spec is not None
20
+ assert spec.loader is not None
21
+ module = importlib.util.module_from_spec(spec)
22
+ spec.loader.exec_module(module)
23
+ return module
24
+
25
+
26
+ @pytest.fixture(scope="module")
27
+ def recipe() -> ModuleType:
28
+ return _load_recipe()
29
+
30
+
31
+ @pytest.fixture
32
+ def seed_path(tmp_path: Path) -> Path:
33
+ path = tmp_path / "chart_seed.jsonl"
34
+ path.write_text('{"image_url": "https://example.com/chart.png"}\n', encoding="utf-8")
35
+ return path
36
+
37
+
38
+ @pytest.mark.parametrize("prompt_version", ["level1", "level2"])
39
+ def test_chart_prompt_versions_match_pipeline_config(recipe: ModuleType, seed_path: Path, prompt_version: str) -> None:
40
+ config = recipe.build_config(seed_path=str(seed_path), prompt_version=prompt_version).build()
41
+ columns = {column.name: column for column in config.columns}
42
+ models = {model.alias: model for model in config.model_configs}
43
+
44
+ assert list(columns) == ["question_type", "question", "answer", "quality_check"]
45
+ assert columns["question_type"].params.values == list(recipe.QUESTION_TYPES)
46
+ assert columns["question_type"].params.weights == list(recipe.QUESTION_TYPE_WEIGHTS)
47
+ assert columns["question"].model_alias == "chart-vlm-question"
48
+ assert columns["answer"].model_alias == "chart-vlm"
49
+ assert columns["quality_check"].model_alias == "chart-vlm-quality"
50
+ assert columns["answer"].extract_reasoning_content is True
51
+ assert columns["question"].multi_modal_context[0].column_name == "image_url"
52
+ assert columns["question"].multi_modal_context[0].data_type == "url"
53
+ assert columns["question"].multi_modal_context[0].image_format is None
54
+ assert columns["answer"].multi_modal_context == columns["question"].multi_modal_context
55
+ assert columns["quality_check"].multi_modal_context == columns["question"].multi_modal_context
56
+ assert "{{ answer__reasoning_content }}" in columns["quality_check"].prompt
57
+ assert config.seed_config.sampling_strategy == recipe.dd.SamplingStrategy.ORDERED
58
+ assert list(models) == ["chart-vlm", "chart-vlm-question", "chart-vlm-quality"]
59
+
60
+ for model in models.values():
61
+ inference = model.inference_parameters
62
+ assert model.model == recipe.DEFAULT_VLM_MODEL
63
+ assert model.provider == recipe.VLLM_PROVIDER_NAME
64
+ assert model.skip_health_check is False
65
+ assert inference.max_tokens == 50000
66
+ assert inference.timeout == 1200
67
+ assert inference.temperature == 0.6
68
+ assert inference.top_p == 0.95
69
+ assert inference.max_parallel_requests == 20
70
+ assert inference.extra_body == recipe.PRODUCTION_SAMPLING
71
+
72
+
73
+ @pytest.mark.parametrize(
74
+ ("prompt_version", "raw_prompts"),
75
+ [
76
+ (
77
+ "level1",
78
+ ("LEVEL1_QUESTION_PROMPT", "LEVEL1_ANSWER_PROMPT", "LEVEL1_QUALITY_PROMPT"),
79
+ ),
80
+ (
81
+ "level2",
82
+ (
83
+ "LEVEL2_QUESTION_PROMPT",
84
+ "LEVEL2_ANSWER_PROMPT",
85
+ "LEVEL2_QUALITY_PROMPT",
86
+ ),
87
+ ),
88
+ ],
89
+ )
90
+ def test_prompt_pack_matches_runtime_whitespace_handling(
91
+ recipe: ModuleType, prompt_version: str, raw_prompts: tuple[str, str, str]
92
+ ) -> None:
93
+ prompt_pack = recipe.get_prompt_pack(prompt_version)
94
+
95
+ assert tuple(prompt_pack) == tuple(getattr(recipe, name).rstrip() for name in raw_prompts)
96
+ assert all(not prompt.endswith(("\n", "\r", " ", "\t")) for prompt in prompt_pack)
97
+
98
+ if prompt_version == "level2":
99
+ assert "DIFFICULTY FLOOR (required)" in prompt_pack.question
100
+ assert "NO-ATTEMPT HANDLING" in prompt_pack.answer
101
+
102
+
103
+ def test_unknown_prompt_version_is_rejected(recipe: ModuleType) -> None:
104
+ with pytest.raises(ValueError, match="Unknown prompt version"):
105
+ recipe.get_prompt_pack("level3")
106
+
107
+
108
+ def test_create_dataset_uses_production_provider_settings(
109
+ recipe: ModuleType, seed_path: Path, monkeypatch: pytest.MonkeyPatch
110
+ ) -> None:
111
+ config_builder = recipe.build_config(seed_path=str(seed_path))
112
+ dataset_results = MagicMock()
113
+ data_designer = MagicMock()
114
+ data_designer.create.return_value = dataset_results
115
+ data_designer_class = MagicMock(return_value=data_designer)
116
+ monkeypatch.setattr(recipe, "DataDesigner", data_designer_class)
117
+
118
+ result = recipe.create_dataset(
119
+ config_builder,
120
+ num_records=7,
121
+ vllm_endpoint="http://localhost:8000/v1",
122
+ prompt_version="level1",
123
+ artifact_path="artifacts",
124
+ )
125
+
126
+ assert result is dataset_results
127
+ provider = data_designer_class.call_args.kwargs["model_providers"][0]
128
+ assert provider.name == "vllm-vl"
129
+ assert provider.endpoint == "http://localhost:8000/v1"
130
+ assert provider.provider_type == "openai"
131
+ assert provider.api_key is None
132
+ data_designer.create.assert_called_once_with(
133
+ config_builder,
134
+ num_records=7,
135
+ dataset_name="chart_qa_level1",
136
+ )
@@ -6,6 +6,7 @@ from __future__ import annotations
6
6
  import json
7
7
  import shutil
8
8
  from pathlib import Path
9
+ from typing import Any
9
10
  from unittest.mock import MagicMock
10
11
 
11
12
  import pytest
@@ -22,6 +23,7 @@ from data_designer.config.seed_source import LocalFileSeedSource
22
23
  from data_designer.config.seed_source_dataframe import DataFrameSeedSource
23
24
  from data_designer.engine.secret_resolver import PlaintextResolver
24
25
  from data_designer.engine.storage.artifact_storage import ArtifactStorage, BatchStage, ResumeMode
26
+ from data_designer.interface import WorkflowMetadata
25
27
  from data_designer.interface.composite_workflow import SkippedStageResult, SkippedStageStatus
26
28
  from data_designer.interface.data_designer import DataDesigner
27
29
  from data_designer.interface.errors import DataDesignerWorkflowError
@@ -194,6 +196,9 @@ def test_composite_workflow_runs_linear_stages_with_disk_handoff(
194
196
  assert data_designer.artifact_path == stub_artifact_path
195
197
 
196
198
  metadata = _load_workflow_metadata(stub_artifact_path, "linear-chain")
199
+ metadata_path = stub_artifact_path / "linear-chain" / "workflow-metadata.json"
200
+ validated_metadata = WorkflowMetadata.model_validate_json(metadata_path.read_text())
201
+ assert validated_metadata.model_dump(mode="json", exclude_unset=True) == metadata
197
202
  assert [stage["status"] for stage in metadata["stages"]] == ["completed", "completed"]
198
203
  assert metadata["stages"][1]["seeded_from_stage"] == "base"
199
204
  assert metadata["stages"][1]["depends_on"] == ["base"]
@@ -364,6 +369,33 @@ def test_composite_workflow_rejects_invalid_stage_outputs(
364
369
  workflow.add_stage("base", _category_builder(stub_model_configs), output=output)
365
370
 
366
371
 
372
+ @pytest.mark.parametrize(
373
+ ("kwargs", "match"),
374
+ [
375
+ ({"num_records": "3"}, "num_records must be an integer"),
376
+ ({"num_records": True}, "num_records must be an integer"),
377
+ ({"num_records": 0}, "num_records must be at least 1"),
378
+ ({"on_success_version": 1}, "on_success_version must be a string"),
379
+ ({"allow_empty": "yes"}, "allow_empty must be a boolean"),
380
+ ({"sampling_strategy": "ordered"}, "sampling_strategy must be a SamplingStrategy"),
381
+ ({"selection_strategy": {}}, "selection_strategy must be an IndexRange or PartitionBlock"),
382
+ ],
383
+ )
384
+ def test_composite_workflow_rejects_invalid_stage_metadata_before_artifacts(
385
+ stub_artifact_path: Path,
386
+ stub_model_providers: list[ModelProvider],
387
+ stub_model_configs: list[ModelConfig],
388
+ kwargs: dict[str, Any],
389
+ match: str,
390
+ ) -> None:
391
+ workflow = _data_designer(stub_artifact_path, stub_model_providers).compose_workflow(name="invalid-version")
392
+
393
+ with pytest.raises(DataDesignerWorkflowError, match=match):
394
+ workflow.add_stage("base", _category_builder(stub_model_configs), **kwargs)
395
+
396
+ assert not stub_artifact_path.exists()
397
+
398
+
367
399
  def test_composite_workflow_rejects_unknown_processor_stage_output(
368
400
  stub_artifact_path: Path,
369
401
  stub_model_providers: list[ModelProvider],
@@ -568,7 +600,7 @@ def test_composite_workflow_stage_output_override_path_must_exist(
568
600
  assert create_mock.call_count == 0
569
601
 
570
602
 
571
- def test_composite_workflow_resume_if_possible_skips_completed_stages(
603
+ def test_composite_workflow_resume_if_possible_skips_legacy_completed_stages(
572
604
  stub_artifact_path: Path,
573
605
  stub_model_providers: list[ModelProvider],
574
606
  stub_model_configs: list[ModelConfig],
@@ -580,6 +612,13 @@ def test_composite_workflow_resume_if_possible_skips_completed_stages(
580
612
  workflow.add_stage("base", _category_builder(stub_model_configs), num_records=3)
581
613
  workflow.add_stage("copy", _copy_builder(stub_model_configs))
582
614
  workflow.run()
615
+ metadata_path = stub_artifact_path / "resume-skip" / "workflow-metadata.json"
616
+ metadata = json.loads(metadata_path.read_text(encoding="utf-8"))
617
+ metadata["workflow_extension"] = True
618
+ for stage in metadata["stages"]:
619
+ stage.pop("stage_output_override_path")
620
+ stage["stage_extension"] = {"value": stage["index"]}
621
+ metadata_path.write_text(json.dumps(metadata), encoding="utf-8")
583
622
  create_mock.reset_mock()
584
623
 
585
624
  resumed = data_designer.compose_workflow(name="resume-skip")
@@ -590,6 +629,10 @@ def test_composite_workflow_resume_if_possible_skips_completed_stages(
590
629
  assert create_mock.call_count == 0
591
630
  assert results.count_records() == 3
592
631
  assert results.load_dataset()["category"].tolist() == ["alpha", "alpha", "alpha"]
632
+ resumed_metadata = json.loads(metadata_path.read_text(encoding="utf-8"))
633
+ assert resumed_metadata["workflow_extension"] is True
634
+ assert [stage["stage_extension"] for stage in resumed_metadata["stages"]] == [{"value": 0}, {"value": 1}]
635
+ assert all("stage_output_override_path" not in stage for stage in resumed_metadata["stages"])
593
636
 
594
637
 
595
638
  def test_composite_workflow_resume_if_possible_skips_stage_with_output_processors(
@@ -825,6 +868,30 @@ def test_composite_workflow_resume_if_possible_invalid_metadata_shape_starts_fre
825
868
  assert [call.kwargs["dataset_name"] for call in create_mock.call_args_list] == ["stage-0-base", "stage-1-copy"]
826
869
 
827
870
 
871
+ def test_composite_workflow_resume_if_possible_invalid_stage_metadata_starts_fresh(
872
+ stub_artifact_path: Path,
873
+ stub_model_providers: list[ModelProvider],
874
+ stub_model_configs: list[ModelConfig],
875
+ stub_dataset_profiler_results,
876
+ ) -> None:
877
+ data_designer = _data_designer(stub_artifact_path, stub_model_providers)
878
+ create_mock = _patch_create(data_designer, stub_dataset_profiler_results)
879
+ workflow = data_designer.compose_workflow(name="resume-invalid-stage")
880
+ workflow.add_stage("base", _category_builder(stub_model_configs), num_records=2)
881
+ workflow.run()
882
+ metadata_path = stub_artifact_path / "resume-invalid-stage" / "workflow-metadata.json"
883
+ metadata = json.loads(metadata_path.read_text(encoding="utf-8"))
884
+ metadata["stages"][0]["status"] = "unknown"
885
+ metadata_path.write_text(json.dumps(metadata), encoding="utf-8")
886
+ create_mock.reset_mock()
887
+
888
+ resumed = data_designer.compose_workflow(name="resume-invalid-stage")
889
+ resumed.add_stage("base", _category_builder(stub_model_configs), num_records=2)
890
+ resumed.run(resume=ResumeMode.IF_POSSIBLE)
891
+
892
+ assert [call.kwargs["dataset_name"] for call in create_mock.call_args_list] == ["stage-0-base"]
893
+
894
+
828
895
  @pytest.mark.parametrize("status", ["running", "failed"])
829
896
  def test_composite_workflow_resume_if_possible_delegates_matching_resumable_stage(
830
897
  stub_artifact_path: Path,
@@ -842,8 +909,20 @@ def test_composite_workflow_resume_if_possible_delegates_matching_resumable_stag
842
909
  metadata_path = stub_artifact_path / "resume-partial" / "workflow-metadata.json"
843
910
  metadata = json.loads(metadata_path.read_text(encoding="utf-8"))
844
911
  _mark_stage_resumable(metadata, 0, status)
912
+ # duration_sec is schema-owned by failed metadata, but is an extension on running metadata.
913
+ if status == "failed":
914
+ metadata["stages"][0]["duration_sec"] = 123.0
915
+ metadata["stages"][0]["stage_extension"] = {"value": status}
845
916
  metadata_path.write_text(json.dumps(metadata), encoding="utf-8")
846
917
  create_mock.reset_mock()
918
+ create_side_effect = create_mock.side_effect
919
+ metadata_at_create: list[dict[str, Any]] = []
920
+
921
+ def capture_create(*args: Any, **kwargs: Any) -> DatasetCreationResults:
922
+ metadata_at_create.append(json.loads(metadata_path.read_text(encoding="utf-8")))
923
+ return create_side_effect(*args, **kwargs)
924
+
925
+ create_mock.side_effect = capture_create
847
926
 
848
927
  resumed = data_designer.compose_workflow(name="resume-partial")
849
928
  resumed.add_stage("base", _category_builder(stub_model_configs), num_records=2)
@@ -852,6 +931,10 @@ def test_composite_workflow_resume_if_possible_delegates_matching_resumable_stag
852
931
 
853
932
  assert [call.kwargs["dataset_name"] for call in create_mock.call_args_list] == ["stage-0-base", "stage-1-copy"]
854
933
  assert [call.kwargs["resume"] for call in create_mock.call_args_list] == [ResumeMode.ALWAYS, ResumeMode.NEVER]
934
+ assert metadata_at_create[0]["stages"][0]["stage_extension"] == {"value": status}
935
+ assert "duration_sec" not in metadata_at_create[0]["stages"][0]
936
+ resumed_metadata = json.loads(metadata_path.read_text(encoding="utf-8"))
937
+ assert resumed_metadata["stages"][0]["stage_extension"] == {"value": status}
855
938
 
856
939
 
857
940
  def test_composite_workflow_resume_always_reruns_descendants_after_partial_stage(
@@ -936,6 +1019,29 @@ def test_composite_workflow_resume_always_rejects_invalid_metadata_shape(
936
1019
  resumed.run(resume=ResumeMode.ALWAYS)
937
1020
 
938
1021
 
1022
+ def test_composite_workflow_resume_always_rejects_invalid_stage_metadata(
1023
+ stub_artifact_path: Path,
1024
+ stub_model_providers: list[ModelProvider],
1025
+ stub_model_configs: list[ModelConfig],
1026
+ stub_dataset_profiler_results,
1027
+ ) -> None:
1028
+ data_designer = _data_designer(stub_artifact_path, stub_model_providers)
1029
+ _patch_create(data_designer, stub_dataset_profiler_results)
1030
+ workflow = data_designer.compose_workflow(name="resume-invalid-stage-always")
1031
+ workflow.add_stage("base", _category_builder(stub_model_configs), num_records=2)
1032
+ workflow.run()
1033
+ metadata_path = stub_artifact_path / "resume-invalid-stage-always" / "workflow-metadata.json"
1034
+ metadata = json.loads(metadata_path.read_text(encoding="utf-8"))
1035
+ metadata["stages"][0]["status"] = "unknown"
1036
+ metadata_path.write_text(json.dumps(metadata), encoding="utf-8")
1037
+
1038
+ resumed = data_designer.compose_workflow(name="resume-invalid-stage-always")
1039
+ resumed.add_stage("base", _category_builder(stub_model_configs), num_records=2)
1040
+
1041
+ with pytest.raises(DataDesignerWorkflowError, match="workflow metadata has invalid shape"):
1042
+ resumed.run(resume=ResumeMode.ALWAYS)
1043
+
1044
+
939
1045
  def test_composite_workflow_resume_always_rejects_changed_stage(
940
1046
  stub_artifact_path: Path,
941
1047
  stub_model_providers: list[ModelProvider],