reader-workbench 1.0.0__py3-none-any.whl

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 (293) hide show
  1. reader_workbench/__init__.py +22 -0
  2. reader_workbench/__main__.py +4 -0
  3. reader_workbench/_version.py +17 -0
  4. reader_workbench/api/__init__.py +74 -0
  5. reader_workbench/api/_record_reads.py +75 -0
  6. reader_workbench/api/artifacts.py +79 -0
  7. reader_workbench/api/facade.py +538 -0
  8. reader_workbench/api/models.py +285 -0
  9. reader_workbench/api/notebooks.py +63 -0
  10. reader_workbench/contracts/__init__.py +18 -0
  11. reader_workbench/contracts/builtins/__init__.py +36 -0
  12. reader_workbench/contracts/builtins/cytometry.py +140 -0
  13. reader_workbench/contracts/builtins/four_state_event_window.py +214 -0
  14. reader_workbench/contracts/builtins/generic.py +21 -0
  15. reader_workbench/contracts/builtins/logic.py +149 -0
  16. reader_workbench/contracts/builtins/plate_reader.py +47 -0
  17. reader_workbench/contracts/catalog.py +257 -0
  18. reader_workbench/contracts/model.py +109 -0
  19. reader_workbench/domains/__init__.py +1 -0
  20. reader_workbench/domains/cytometry/__init__.py +3 -0
  21. reader_workbench/domains/cytometry/analysis/__init__.py +29 -0
  22. reader_workbench/domains/cytometry/analysis/events.py +182 -0
  23. reader_workbench/domains/cytometry/analysis/gating.py +175 -0
  24. reader_workbench/domains/cytometry/analysis/workflow.py +248 -0
  25. reader_workbench/domains/cytometry/io/__init__.py +3 -0
  26. reader_workbench/domains/cytometry/io/fcs.py +135 -0
  27. reader_workbench/domains/cytometry/plots/__init__.py +5 -0
  28. reader_workbench/domains/cytometry/plots/diagnostic.py +155 -0
  29. reader_workbench/domains/logic/__init__.py +3 -0
  30. reader_workbench/domains/logic/crosstalk/__init__.py +3 -0
  31. reader_workbench/domains/logic/crosstalk/pairs.py +661 -0
  32. reader_workbench/domains/logic/four_state_vector/__init__.py +7 -0
  33. reader_workbench/domains/logic/four_state_vector/builder.py +321 -0
  34. reader_workbench/domains/logic/four_state_vector/collection/__init__.py +20 -0
  35. reader_workbench/domains/logic/four_state_vector/collection/checks.py +49 -0
  36. reader_workbench/domains/logic/four_state_vector/collection/constants.py +29 -0
  37. reader_workbench/domains/logic/four_state_vector/collection/model.py +19 -0
  38. reader_workbench/domains/logic/four_state_vector/collection/render.py +383 -0
  39. reader_workbench/domains/logic/four_state_vector/collection/sources.py +185 -0
  40. reader_workbench/domains/logic/four_state_vector/config.py +214 -0
  41. reader_workbench/domains/logic/four_state_vector/diagnostic.py +361 -0
  42. reader_workbench/domains/logic/four_state_vector/heatmap.py +86 -0
  43. reader_workbench/domains/logic/four_state_vector/math.py +191 -0
  44. reader_workbench/domains/logic/four_state_vector/reference.py +85 -0
  45. reader_workbench/domains/logic/four_state_vector/selection.py +228 -0
  46. reader_workbench/domains/logic/four_state_vector/treatment_semantics.py +51 -0
  47. reader_workbench/domains/logic/four_state_vector/validation.py +19 -0
  48. reader_workbench/domains/logic/logic_symmetry/__init__.py +3 -0
  49. reader_workbench/domains/logic/logic_symmetry/encodings.py +93 -0
  50. reader_workbench/domains/logic/logic_symmetry/extract_corners.py +192 -0
  51. reader_workbench/domains/logic/logic_symmetry/main.py +236 -0
  52. reader_workbench/domains/logic/logic_symmetry/metrics.py +96 -0
  53. reader_workbench/domains/logic/logic_symmetry/overlay.py +129 -0
  54. reader_workbench/domains/logic/logic_symmetry/prep.py +138 -0
  55. reader_workbench/domains/logic/logic_symmetry/render.py +356 -0
  56. reader_workbench/domains/logic/treatment_columns.py +42 -0
  57. reader_workbench/domains/plate_reader/__init__.py +1 -0
  58. reader_workbench/domains/plate_reader/analysis/__init__.py +14 -0
  59. reader_workbench/domains/plate_reader/analysis/fold_change.py +474 -0
  60. reader_workbench/domains/plate_reader/analysis/four_state_event_window/__init__.py +21 -0
  61. reader_workbench/domains/plate_reader/analysis/four_state_event_window/aggregation.py +191 -0
  62. reader_workbench/domains/plate_reader/analysis/four_state_event_window/contract_fields.py +50 -0
  63. reader_workbench/domains/plate_reader/analysis/four_state_event_window/contracts.py +320 -0
  64. reader_workbench/domains/plate_reader/analysis/four_state_event_window/design_dispositions.py +54 -0
  65. reader_workbench/domains/plate_reader/analysis/four_state_event_window/disposition_records.py +140 -0
  66. reader_workbench/domains/plate_reader/analysis/four_state_event_window/event_sensitivity.py +27 -0
  67. reader_workbench/domains/plate_reader/analysis/four_state_event_window/materialize.py +274 -0
  68. reader_workbench/domains/plate_reader/analysis/four_state_event_window/observation_resampling.py +97 -0
  69. reader_workbench/domains/plate_reader/analysis/four_state_event_window/reduction.py +62 -0
  70. reader_workbench/domains/plate_reader/analysis/four_state_event_window/seeds.py +15 -0
  71. reader_workbench/domains/plate_reader/analysis/four_state_event_window/sources.py +308 -0
  72. reader_workbench/domains/plate_reader/analysis/four_state_event_window/well_exclusion_validation.py +94 -0
  73. reader_workbench/domains/plate_reader/analysis/four_state_event_window/well_exclusions.py +54 -0
  74. reader_workbench/domains/plate_reader/analysis/timepoints.py +76 -0
  75. reader_workbench/domains/plate_reader/io/__init__.py +6 -0
  76. reader_workbench/domains/plate_reader/io/sample_map.py +65 -0
  77. reader_workbench/domains/plate_reader/io/synergy_h1/__init__.py +6 -0
  78. reader_workbench/domains/plate_reader/io/synergy_h1/_kinetic.py +141 -0
  79. reader_workbench/domains/plate_reader/io/synergy_h1/_parser.py +295 -0
  80. reader_workbench/domains/plate_reader/io/synergy_h1/_shared.py +195 -0
  81. reader_workbench/domains/plate_reader/io/synergy_h1/_snapshot.py +130 -0
  82. reader_workbench/domains/plate_reader/ordering.py +59 -0
  83. reader_workbench/domains/plate_reader/plots/__init__.py +15 -0
  84. reader_workbench/domains/plate_reader/plots/_data.py +29 -0
  85. reader_workbench/domains/plate_reader/plots/common.py +346 -0
  86. reader_workbench/domains/plate_reader/plots/distributions.py +324 -0
  87. reader_workbench/domains/plate_reader/plots/dual_reporter_triptych.py +525 -0
  88. reader_workbench/domains/plate_reader/plots/dual_reporter_triptych_render.py +195 -0
  89. reader_workbench/domains/plate_reader/plots/four_state_event_window/__init__.py +26 -0
  90. reader_workbench/domains/plate_reader/plots/four_state_event_window/diagnostic.py +295 -0
  91. reader_workbench/domains/plate_reader/plots/four_state_event_window/diagnostic_components.py +149 -0
  92. reader_workbench/domains/plate_reader/plots/four_state_event_window/diagnostic_render.py +308 -0
  93. reader_workbench/domains/plate_reader/plots/four_state_event_window/diagnostic_style.py +51 -0
  94. reader_workbench/domains/plate_reader/plots/four_state_event_window/schema.py +8 -0
  95. reader_workbench/domains/plate_reader/plots/four_state_event_window/summary.py +140 -0
  96. reader_workbench/domains/plate_reader/plots/grouping.py +53 -0
  97. reader_workbench/domains/plate_reader/plots/panels/__init__.py +12 -0
  98. reader_workbench/domains/plate_reader/plots/panels/snapshot.py +161 -0
  99. reader_workbench/domains/plate_reader/plots/panels/snapshot_data.py +91 -0
  100. reader_workbench/domains/plate_reader/plots/panels/time_series.py +296 -0
  101. reader_workbench/domains/plate_reader/plots/single_reporter_diagnostic.py +435 -0
  102. reader_workbench/domains/plate_reader/plots/single_reporter_diagnostic_render.py +300 -0
  103. reader_workbench/domains/plate_reader/plots/snapshot_barplot/__init__.py +315 -0
  104. reader_workbench/domains/plate_reader/plots/snapshot_barplot/planning.py +168 -0
  105. reader_workbench/domains/plate_reader/plots/snapshot_heatmap/__init__.py +205 -0
  106. reader_workbench/domains/plate_reader/plots/snapshot_heatmap/inputs.py +103 -0
  107. reader_workbench/domains/plate_reader/plots/time_series.py +317 -0
  108. reader_workbench/domains/plate_reader/plots/ts_and_snap/__init__.py +479 -0
  109. reader_workbench/domains/plate_reader/plots/ts_and_snap/planning.py +283 -0
  110. reader_workbench/domains/time_series/__init__.py +29 -0
  111. reader_workbench/domains/time_series/aggregation.py +60 -0
  112. reader_workbench/domains/time_series/contracts.py +368 -0
  113. reader_workbench/domains/time_series/reduction.py +395 -0
  114. reader_workbench/errors.py +55 -0
  115. reader_workbench/maintenance/__init__.py +6 -0
  116. reader_workbench/maintenance/docs.py +335 -0
  117. reader_workbench/maintenance/model.py +28 -0
  118. reader_workbench/maintenance/release.py +39 -0
  119. reader_workbench/maintenance/skills.py +124 -0
  120. reader_workbench/plotting/__init__.py +20 -0
  121. reader_workbench/plotting/mpl.py +56 -0
  122. reader_workbench/plotting/sinks.py +69 -0
  123. reader_workbench/plotting/style.py +175 -0
  124. reader_workbench/plotting/utils.py +27 -0
  125. reader_workbench/plugins/__init__.py +1 -0
  126. reader_workbench/plugins/catalog.py +33 -0
  127. reader_workbench/plugins/export/__init__.py +0 -0
  128. reader_workbench/plugins/export/_paths.py +21 -0
  129. reader_workbench/plugins/export/csv.py +41 -0
  130. reader_workbench/plugins/export/xlsx.py +44 -0
  131. reader_workbench/plugins/ingest/__init__.py +0 -0
  132. reader_workbench/plugins/ingest/_discovery.py +58 -0
  133. reader_workbench/plugins/ingest/discovery_policy.py +66 -0
  134. reader_workbench/plugins/ingest/flow_cytometer.py +139 -0
  135. reader_workbench/plugins/ingest/synergy_h1.py +234 -0
  136. reader_workbench/plugins/manifests/__init__.py +1 -0
  137. reader_workbench/plugins/manifests/export.py +29 -0
  138. reader_workbench/plugins/manifests/ingest.py +29 -0
  139. reader_workbench/plugins/manifests/plot.py +161 -0
  140. reader_workbench/plugins/manifests/transform.py +172 -0
  141. reader_workbench/plugins/manifests/validator.py +18 -0
  142. reader_workbench/plugins/plot/__init__.py +0 -0
  143. reader_workbench/plugins/plot/_shared.py +55 -0
  144. reader_workbench/plugins/plot/cytometry_diagnostic.py +54 -0
  145. reader_workbench/plugins/plot/distributions.py +58 -0
  146. reader_workbench/plugins/plot/dual_reporter_triptych.py +224 -0
  147. reader_workbench/plugins/plot/four_state_event_window_diagnostic.py +133 -0
  148. reader_workbench/plugins/plot/four_state_event_window_summary.py +53 -0
  149. reader_workbench/plugins/plot/four_state_vector_collection.py +45 -0
  150. reader_workbench/plugins/plot/four_state_vector_diagnostic.py +105 -0
  151. reader_workbench/plugins/plot/four_state_vector_heatmap.py +63 -0
  152. reader_workbench/plugins/plot/logic_symmetry.py +56 -0
  153. reader_workbench/plugins/plot/single_reporter_diagnostic.py +279 -0
  154. reader_workbench/plugins/plot/snapshot_barplot.py +65 -0
  155. reader_workbench/plugins/plot/snapshot_heatmap.py +104 -0
  156. reader_workbench/plugins/plot/time_series.py +114 -0
  157. reader_workbench/plugins/plot/ts_and_snap.py +210 -0
  158. reader_workbench/plugins/transform/__init__.py +0 -0
  159. reader_workbench/plugins/transform/_four_state_vector.py +204 -0
  160. reader_workbench/plugins/transform/_labeling.py +109 -0
  161. reader_workbench/plugins/transform/alias.py +70 -0
  162. reader_workbench/plugins/transform/assay_labels.py +62 -0
  163. reader_workbench/plugins/transform/blank.py +79 -0
  164. reader_workbench/plugins/transform/crosstalk_pairs.py +180 -0
  165. reader_workbench/plugins/transform/cytometry_gating.py +120 -0
  166. reader_workbench/plugins/transform/fold_change.py +79 -0
  167. reader_workbench/plugins/transform/four_state_event_window.py +93 -0
  168. reader_workbench/plugins/transform/four_state_vector.py +62 -0
  169. reader_workbench/plugins/transform/four_state_vector_collection.py +41 -0
  170. reader_workbench/plugins/transform/logic_symmetry.py +67 -0
  171. reader_workbench/plugins/transform/outlier_filter.py +60 -0
  172. reader_workbench/plugins/transform/overflow.py +197 -0
  173. reader_workbench/plugins/transform/ratio.py +237 -0
  174. reader_workbench/plugins/transform/sample_map.py +170 -0
  175. reader_workbench/plugins/transform/sample_metadata.py +94 -0
  176. reader_workbench/plugins/validator/__init__.py +1 -0
  177. reader_workbench/plugins/validator/to_tidy_plus_map.py +155 -0
  178. reader_workbench/protocols/__init__.py +80 -0
  179. reader_workbench/protocols/_builtins_plate_reader_growth.py +179 -0
  180. reader_workbench/protocols/_builtins_plate_reader_variants.py +274 -0
  181. reader_workbench/protocols/builtins.py +1656 -0
  182. reader_workbench/protocols/compiler.py +22 -0
  183. reader_workbench/protocols/compilers/__init__.py +1 -0
  184. reader_workbench/protocols/compilers/common.py +100 -0
  185. reader_workbench/protocols/compilers/cytometry.py +87 -0
  186. reader_workbench/protocols/compilers/generic.py +14 -0
  187. reader_workbench/protocols/compilers/logic.py +245 -0
  188. reader_workbench/protocols/compilers/plate_reader.py +937 -0
  189. reader_workbench/protocols/compilers/plate_reader_pipeline.py +197 -0
  190. reader_workbench/protocols/model.py +1486 -0
  191. reader_workbench/protocols/semantic_coverage.py +234 -0
  192. reader_workbench/runtime/__init__.py +12 -0
  193. reader_workbench/runtime/builtin.py +23 -0
  194. reader_workbench/runtime/model.py +42 -0
  195. reader_workbench/workbench/__init__.py +60 -0
  196. reader_workbench/workbench/assets/__init__.py +22 -0
  197. reader_workbench/workbench/assets/types.py +118 -0
  198. reader_workbench/workbench/audit/__init__.py +5 -0
  199. reader_workbench/workbench/audit/experiments.py +307 -0
  200. reader_workbench/workbench/audit/staging.py +187 -0
  201. reader_workbench/workbench/cli/__init__.py +51 -0
  202. reader_workbench/workbench/cli/_lazy.py +9 -0
  203. reader_workbench/workbench/cli/_records_view.py +150 -0
  204. reader_workbench/workbench/cli/_surface_execution.py +443 -0
  205. reader_workbench/workbench/cli/audit.py +95 -0
  206. reader_workbench/workbench/cli/automation.py +229 -0
  207. reader_workbench/workbench/cli/demo.py +46 -0
  208. reader_workbench/workbench/cli/dop.py +91 -0
  209. reader_workbench/workbench/cli/experiments.py +635 -0
  210. reader_workbench/workbench/cli/helpers.py +232 -0
  211. reader_workbench/workbench/cli/main.py +59 -0
  212. reader_workbench/workbench/cli/maintenance.py +82 -0
  213. reader_workbench/workbench/cli/notebooks.py +260 -0
  214. reader_workbench/workbench/cli/pagination.py +117 -0
  215. reader_workbench/workbench/cli/protocols.py +336 -0
  216. reader_workbench/workbench/cli/shared.py +309 -0
  217. reader_workbench/workbench/cli/surfaces.py +534 -0
  218. reader_workbench/workbench/cli/verification.py +128 -0
  219. reader_workbench/workbench/commands.py +10 -0
  220. reader_workbench/workbench/config/__init__.py +47 -0
  221. reader_workbench/workbench/config/identity.py +13 -0
  222. reader_workbench/workbench/config/load.py +405 -0
  223. reader_workbench/workbench/config/model.py +274 -0
  224. reader_workbench/workbench/context.py +26 -0
  225. reader_workbench/workbench/decl/__init__.py +31 -0
  226. reader_workbench/workbench/decl/build.py +190 -0
  227. reader_workbench/workbench/decl/model.py +81 -0
  228. reader_workbench/workbench/dop/__init__.py +12 -0
  229. reader_workbench/workbench/dop/builtins.py +261 -0
  230. reader_workbench/workbench/dop/model.py +209 -0
  231. reader_workbench/workbench/engine/__init__.py +42 -0
  232. reader_workbench/workbench/engine/_shared.py +76 -0
  233. reader_workbench/workbench/engine/contracts.py +283 -0
  234. reader_workbench/workbench/engine/execution.py +326 -0
  235. reader_workbench/workbench/engine/file_outputs.py +260 -0
  236. reader_workbench/workbench/engine/inputs.py +161 -0
  237. reader_workbench/workbench/engine/invocations.py +507 -0
  238. reader_workbench/workbench/engine/planning.py +72 -0
  239. reader_workbench/workbench/engine/runtime.py +464 -0
  240. reader_workbench/workbench/engine/setup.py +149 -0
  241. reader_workbench/workbench/engine/validation.py +684 -0
  242. reader_workbench/workbench/experiment/__init__.py +47 -0
  243. reader_workbench/workbench/experiment/model.py +381 -0
  244. reader_workbench/workbench/experiments.py +133 -0
  245. reader_workbench/workbench/graph/__init__.py +47 -0
  246. reader_workbench/workbench/graph/nodes.py +102 -0
  247. reader_workbench/workbench/graph/normalize.py +177 -0
  248. reader_workbench/workbench/graph/refs.py +148 -0
  249. reader_workbench/workbench/input_discovery.py +19 -0
  250. reader_workbench/workbench/inspection/__init__.py +3 -0
  251. reader_workbench/workbench/inspection/catalogs.py +128 -0
  252. reader_workbench/workbench/inspection/common.py +92 -0
  253. reader_workbench/workbench/inspection/dop.py +64 -0
  254. reader_workbench/workbench/inspection/experiments.py +449 -0
  255. reader_workbench/workbench/inspection/inventory.py +68 -0
  256. reader_workbench/workbench/inspection/protocols.py +368 -0
  257. reader_workbench/workbench/inspection/readiness.py +333 -0
  258. reader_workbench/workbench/inspection/reports.py +367 -0
  259. reader_workbench/workbench/inspection/results.py +166 -0
  260. reader_workbench/workbench/inspection/runtime.py +287 -0
  261. reader_workbench/workbench/inspection/semantics.py +192 -0
  262. reader_workbench/workbench/inspection/validation.py +30 -0
  263. reader_workbench/workbench/notebooks/__init__.py +17 -0
  264. reader_workbench/workbench/notebooks/_launch_registry.py +112 -0
  265. reader_workbench/workbench/notebooks/_launch_runtime.py +104 -0
  266. reader_workbench/workbench/notebooks/components/__init__.py +21 -0
  267. reader_workbench/workbench/notebooks/components/deliverables.py +403 -0
  268. reader_workbench/workbench/notebooks/components/overview.py +119 -0
  269. reader_workbench/workbench/notebooks/eda.marimo.py.txt +153 -0
  270. reader_workbench/workbench/notebooks/launch.py +274 -0
  271. reader_workbench/workbench/notebooks/presentation.py +136 -0
  272. reader_workbench/workbench/notebooks/scaffold.py +60 -0
  273. reader_workbench/workbench/ontology.py +78 -0
  274. reader_workbench/workbench/paths.py +44 -0
  275. reader_workbench/workbench/ports/__init__.py +31 -0
  276. reader_workbench/workbench/ports/model.py +168 -0
  277. reader_workbench/workbench/records/__init__.py +44 -0
  278. reader_workbench/workbench/records/epoch.py +329 -0
  279. reader_workbench/workbench/records/evidence.py +247 -0
  280. reader_workbench/workbench/records/identity.py +87 -0
  281. reader_workbench/workbench/records/locking.py +185 -0
  282. reader_workbench/workbench/records/model.py +711 -0
  283. reader_workbench/workbench/records/sources.py +73 -0
  284. reader_workbench/workbench/records/store.py +1022 -0
  285. reader_workbench/workbench/records/verification.py +998 -0
  286. reader_workbench/workbench/registry.py +333 -0
  287. reader_workbench/workbench/spec_overrides.py +215 -0
  288. reader_workbench-1.0.0.dist-info/METADATA +91 -0
  289. reader_workbench-1.0.0.dist-info/RECORD +293 -0
  290. reader_workbench-1.0.0.dist-info/WHEEL +5 -0
  291. reader_workbench-1.0.0.dist-info/entry_points.txt +2 -0
  292. reader_workbench-1.0.0.dist-info/licenses/LICENSE +21 -0
  293. reader_workbench-1.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,307 @@
1
+ from __future__ import annotations
2
+
3
+ import io
4
+ import tempfile
5
+ import time
6
+ from dataclasses import asdict, dataclass
7
+ from pathlib import Path
8
+
9
+ from rich.console import Console
10
+
11
+ from reader_workbench.errors import ReaderError
12
+ from reader_workbench.runtime import builtin_runtime
13
+ from reader_workbench.workbench.decl import load_workbench_decl
14
+ from reader_workbench.workbench.engine import run_spec
15
+ from reader_workbench.workbench.engine.validation import validation_summary
16
+ from reader_workbench.workbench.experiments import discover_experiment_configs
17
+ from reader_workbench.workbench.graph import resolve_workbench
18
+ from reader_workbench.workbench.inspection.runtime import record_producer_map, workbench_record_verification_scope
19
+ from reader_workbench.workbench.records import verify_record_store
20
+
21
+ from .staging import stage_audit_workspace
22
+
23
+
24
+ @dataclass(slots=True)
25
+ class AuditResult:
26
+ config: str
27
+ name: str
28
+ lifecycle: str
29
+ status: str
30
+ phase: str
31
+ seconds: float
32
+ detail: str | None
33
+ expected_plots: int
34
+ expected_exports: int
35
+
36
+
37
+ def discover_year_dirs(root: Path) -> list[str]:
38
+ return sorted(path.name for path in root.iterdir() if path.is_dir() and path.name.isdigit() and len(path.name) == 4)
39
+
40
+
41
+ def discover_configs(root: Path, years: set[str]) -> list[Path]:
42
+ configs = discover_experiment_configs(root, include_scaffolds=False)
43
+ selected: list[Path] = []
44
+ for config in configs:
45
+ try:
46
+ rel = config.relative_to(root)
47
+ except ValueError:
48
+ continue
49
+ if not rel.parts:
50
+ continue
51
+ if years and rel.parts[0] not in years:
52
+ continue
53
+ selected.append(config)
54
+ return sorted(selected)
55
+
56
+
57
+ def verify_outputs(decl, runtime) -> tuple[int, int, str | None]:
58
+ workbench = resolve_workbench(decl)
59
+ verification_scope = workbench_record_verification_scope(workbench, runtime=runtime)
60
+ layout = decl.experiment_semantics.layout
61
+ outputs = layout.outputs_dir
62
+ store = runtime.record_store(
63
+ outputs,
64
+ plots_subdir=layout.plots_subdir,
65
+ exports_subdir=layout.exports_subdir,
66
+ experiment_root=decl.experiment.root,
67
+ create=False,
68
+ )
69
+ latest_ids = {record.record_id for record in store.iter_latest_records()}
70
+ expected_plot_ids = {f"plot:{plot.id}" for plot in workbench.plots}
71
+ expected_export_ids = {f"export:{export.id}" for export in workbench.exports}
72
+ expected_dataframe_ids = set(record_producer_map(workbench.plugin_steps(), runtime=runtime))
73
+
74
+ if (outputs / "manifests" / "records.json").exists() is False:
75
+ return len(expected_plot_ids), len(expected_export_ids), "records.json missing"
76
+ missing_dataframe_ids = sorted(expected_dataframe_ids - latest_ids)
77
+ if missing_dataframe_ids:
78
+ return (
79
+ len(expected_plot_ids),
80
+ len(expected_export_ids),
81
+ f"missing declared dataframe records: {missing_dataframe_ids}",
82
+ )
83
+
84
+ missing_plot_ids = sorted(expected_plot_ids - latest_ids)
85
+ if missing_plot_ids:
86
+ return len(expected_plot_ids), len(expected_export_ids), f"missing plot records: {missing_plot_ids}"
87
+ missing_export_ids = sorted(expected_export_ids - latest_ids)
88
+ if missing_export_ids:
89
+ return len(expected_plot_ids), len(expected_export_ids), f"missing export records: {missing_export_ids}"
90
+
91
+ plots_dir = outputs / layout.plots_subdir
92
+ exports_dir = outputs / layout.exports_subdir
93
+ if expected_plot_ids and not any(path.is_file() for path in plots_dir.rglob("*")):
94
+ return len(expected_plot_ids), len(expected_export_ids), "expected plot files were not created"
95
+ if expected_export_ids and not any(path.is_file() for path in exports_dir.rglob("*")):
96
+ return len(expected_plot_ids), len(expected_export_ids), "expected export files were not created"
97
+
98
+ verification = verify_record_store(
99
+ store,
100
+ experiment_root=decl.experiment.root,
101
+ expected_config_digest=decl.config_digest,
102
+ scope=verification_scope,
103
+ )
104
+ if verification["status"] != "ok":
105
+ issues = list(verification.get("issues") or [])
106
+ for record in verification.get("records") or []:
107
+ issues.extend(record.get("issues") or [])
108
+ first = issues[0] if issues else {"code": "verification.failed", "reason": "unknown verification failure"}
109
+ return (
110
+ len(expected_plot_ids),
111
+ len(expected_export_ids),
112
+ f"reader verify failed: {first['code']} • {first['reason']}",
113
+ )
114
+
115
+ return len(expected_plot_ids), len(expected_export_ids), None
116
+
117
+
118
+ def audit_config(config_path: Path, *, include_non_active: bool, runtime) -> AuditResult:
119
+ start = time.perf_counter()
120
+ try:
121
+ decl = load_workbench_decl(config_path, protocols=runtime.protocols)
122
+ except ReaderError as exc:
123
+ return AuditResult(
124
+ config=str(config_path),
125
+ name=config_path.parent.name,
126
+ lifecycle="unknown",
127
+ status="failed",
128
+ phase="config",
129
+ seconds=time.perf_counter() - start,
130
+ detail=str(exc),
131
+ expected_plots=0,
132
+ expected_exports=0,
133
+ )
134
+ lifecycle = decl.experiment.lifecycle
135
+ rel_config = str(config_path)
136
+ if lifecycle != "active" and not include_non_active:
137
+ return AuditResult(
138
+ config=rel_config,
139
+ name=config_path.parent.name,
140
+ lifecycle=lifecycle,
141
+ status="skipped",
142
+ phase="lifecycle",
143
+ seconds=time.perf_counter() - start,
144
+ detail=f"skipped non-active lifecycle: {lifecycle}",
145
+ expected_plots=0,
146
+ expected_exports=0,
147
+ )
148
+
149
+ source_summary = validation_summary(
150
+ decl,
151
+ check_files=True,
152
+ exp_root=decl.experiment.root,
153
+ runtime=runtime,
154
+ )
155
+ if source_summary["status"] != "ok":
156
+ return AuditResult(
157
+ config=rel_config,
158
+ name=config_path.parent.name,
159
+ lifecycle=lifecycle,
160
+ status="failed",
161
+ phase="validate",
162
+ seconds=time.perf_counter() - start,
163
+ detail="; ".join(source_summary["errors"]) or "validation failed",
164
+ expected_plots=0,
165
+ expected_exports=0,
166
+ )
167
+
168
+ with tempfile.TemporaryDirectory(prefix="reader-local-audit-") as tmpdir:
169
+ try:
170
+ staged_config = stage_audit_workspace(
171
+ config_path=config_path,
172
+ target_root=Path(tmpdir),
173
+ resources=decl.experiment_semantics.resources,
174
+ runtime=runtime,
175
+ )
176
+ staged_decl = load_workbench_decl(staged_config, protocols=runtime.protocols)
177
+ except (ReaderError, OSError) as exc:
178
+ return AuditResult(
179
+ config=rel_config,
180
+ name=config_path.parent.name,
181
+ lifecycle=lifecycle,
182
+ status="failed",
183
+ phase="validate",
184
+ seconds=time.perf_counter() - start,
185
+ detail=str(exc),
186
+ expected_plots=0,
187
+ expected_exports=0,
188
+ )
189
+ summary = validation_summary(
190
+ staged_decl, check_files=True, exp_root=staged_decl.experiment.root, runtime=runtime
191
+ )
192
+ if summary["status"] != "ok":
193
+ return AuditResult(
194
+ config=rel_config,
195
+ name=config_path.parent.name,
196
+ lifecycle=lifecycle,
197
+ status="failed",
198
+ phase="validate",
199
+ seconds=time.perf_counter() - start,
200
+ detail="; ".join(summary["errors"]) or "validation failed",
201
+ expected_plots=0,
202
+ expected_exports=0,
203
+ )
204
+
205
+ try:
206
+ run_spec(
207
+ staged_decl,
208
+ include_pipeline=True,
209
+ include_plots=True,
210
+ include_exports=True,
211
+ runtime=runtime,
212
+ console=Console(file=io.StringIO(), force_terminal=False, color_system=None),
213
+ log_level="ERROR",
214
+ verbose=False,
215
+ )
216
+ except Exception as exc: # pragma: no cover - exercised by live audit runs
217
+ return AuditResult(
218
+ config=rel_config,
219
+ name=config_path.parent.name,
220
+ lifecycle=lifecycle,
221
+ status="failed",
222
+ phase="run",
223
+ seconds=time.perf_counter() - start,
224
+ detail=str(exc),
225
+ expected_plots=0,
226
+ expected_exports=0,
227
+ )
228
+
229
+ expected_plots, expected_exports, verify_error = verify_outputs(staged_decl, runtime)
230
+ if verify_error is not None:
231
+ return AuditResult(
232
+ config=rel_config,
233
+ name=config_path.parent.name,
234
+ lifecycle=lifecycle,
235
+ status="failed",
236
+ phase="verify",
237
+ seconds=time.perf_counter() - start,
238
+ detail=verify_error,
239
+ expected_plots=expected_plots,
240
+ expected_exports=expected_exports,
241
+ )
242
+
243
+ return AuditResult(
244
+ config=rel_config,
245
+ name=config_path.parent.name,
246
+ lifecycle=lifecycle,
247
+ status="passed",
248
+ phase="complete",
249
+ seconds=time.perf_counter() - start,
250
+ detail=None,
251
+ expected_plots=expected_plots,
252
+ expected_exports=expected_exports,
253
+ )
254
+
255
+
256
+ def render_text(results: list[AuditResult]) -> str:
257
+ lines = []
258
+ for result in results:
259
+ detail = f" :: {result.detail}" if result.detail else ""
260
+ lines.append(
261
+ f"[{result.status.upper():7}] {result.name} "
262
+ f"(lifecycle={result.lifecycle}, phase={result.phase}, "
263
+ f"plots={result.expected_plots}, exports={result.expected_exports}, "
264
+ f"seconds={result.seconds:.2f}){detail}"
265
+ )
266
+ counts: dict[str, int] = {}
267
+ for result in results:
268
+ counts[result.status] = counts.get(result.status, 0) + 1
269
+ summary = ", ".join(f"{key}={counts[key]}" for key in sorted(counts))
270
+ lines.append(f"Summary: {summary}")
271
+ return "\n".join(lines)
272
+
273
+
274
+ def audit_experiments(
275
+ root: Path,
276
+ *,
277
+ years: tuple[str, ...] | None = None,
278
+ include_non_active: bool = False,
279
+ fail_fast: bool = False,
280
+ ) -> dict[str, object]:
281
+ root = root.resolve()
282
+ if not root.exists() or not root.is_dir():
283
+ raise ValueError(f"Experiments root not found: {root}")
284
+ selected_years = discover_year_dirs(root) if years is None else [str(year) for year in years]
285
+ if not selected_years:
286
+ raise ValueError(f"No numeric year directories found under {root}. Use --years to select specific directories.")
287
+ runtime = builtin_runtime()
288
+ configs = discover_configs(root, set(selected_years))
289
+
290
+ results: list[AuditResult] = []
291
+ for config_path in configs:
292
+ result = audit_config(config_path, include_non_active=include_non_active, runtime=runtime)
293
+ results.append(result)
294
+ if fail_fast and result.status == "failed":
295
+ break
296
+
297
+ return {
298
+ "root": str(root),
299
+ "years": sorted(selected_years),
300
+ "summary": {
301
+ "experiments": len(results),
302
+ "passed": sum(1 for item in results if item.status == "passed"),
303
+ "failed": sum(1 for item in results if item.status == "failed"),
304
+ "skipped": sum(1 for item in results if item.status == "skipped"),
305
+ },
306
+ "results": [asdict(result) for result in results],
307
+ }
@@ -0,0 +1,187 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import os
5
+ import shutil
6
+ from pathlib import Path
7
+
8
+ from reader_workbench.errors import ConfigError, RecordError
9
+ from reader_workbench.runtime import ReaderRuntime
10
+ from reader_workbench.workbench.experiment import ResourceCatalog
11
+ from reader_workbench.workbench.experiments import find_experiments_root
12
+ from reader_workbench.workbench.graph import SourceRecordRef
13
+ from reader_workbench.workbench.records import (
14
+ record_paths,
15
+ record_to_dict,
16
+ verify_record_artifact_integrity,
17
+ )
18
+ from reader_workbench.workbench.records.store import RECORD_CATALOG_SCHEMA_VERSION
19
+
20
+
21
+ def stage_experiment(source_dir: Path, target_dir: Path) -> Path:
22
+ shutil.copytree(
23
+ source_dir,
24
+ target_dir,
25
+ ignore=shutil.ignore_patterns("outputs", "__pycache__", ".DS_Store"),
26
+ symlinks=True,
27
+ )
28
+ _retarget_internal_symlinks(source_dir=source_dir, target_dir=target_dir)
29
+ return target_dir / "config.yaml"
30
+
31
+
32
+ def stage_audit_workspace(
33
+ *,
34
+ config_path: Path,
35
+ target_root: Path,
36
+ resources: ResourceCatalog,
37
+ runtime: ReaderRuntime,
38
+ ) -> Path:
39
+ """Stage one writable experiment and exact read-only snapshots of its record resources."""
40
+
41
+ record_resources = [resource for resource in resources.by_id.values() if resource.kind == "record"]
42
+ try:
43
+ source_experiments_root = find_experiments_root(config_path.parent)
44
+ except ConfigError:
45
+ if record_resources:
46
+ raise
47
+ source_experiments_root = None
48
+ staged_experiments_root = target_root / "experiments"
49
+ relative_experiment_dir = (
50
+ config_path.parent.resolve().relative_to(source_experiments_root)
51
+ if source_experiments_root is not None
52
+ else Path(config_path.parent.name)
53
+ )
54
+ staged_config = stage_experiment(
55
+ config_path.parent,
56
+ staged_experiments_root / relative_experiment_dir,
57
+ )
58
+
59
+ resources_by_provider: dict[tuple[Path, Path], set[str]] = {}
60
+ for resource in record_resources:
61
+ provider_key = (resource.experiment_root.resolve(), resource.outputs_dir.resolve())
62
+ resources_by_provider.setdefault(provider_key, set()).add(resource.record_id)
63
+ if resources_by_provider and source_experiments_root is None:
64
+ raise ConfigError("Cross-experiment record resources require a canonical experiments/ workspace")
65
+ for (provider_root, provider_outputs), record_ids in sorted(
66
+ resources_by_provider.items(),
67
+ key=lambda item: (str(item[0][0]), str(item[0][1])),
68
+ ):
69
+ _stage_record_provider(
70
+ provider_root=provider_root,
71
+ provider_outputs=provider_outputs,
72
+ record_ids=record_ids,
73
+ source_experiments_root=source_experiments_root,
74
+ staged_experiments_root=staged_experiments_root,
75
+ staged_target_dir=staged_config.parent,
76
+ runtime=runtime,
77
+ )
78
+ return staged_config
79
+
80
+
81
+ def _stage_record_provider(
82
+ *,
83
+ provider_root: Path,
84
+ provider_outputs: Path,
85
+ record_ids: set[str],
86
+ source_experiments_root: Path,
87
+ staged_experiments_root: Path,
88
+ staged_target_dir: Path,
89
+ runtime: ReaderRuntime,
90
+ ) -> None:
91
+ staged_provider_dir = _stage_catalog_config(
92
+ source_root=provider_root,
93
+ source_experiments_root=source_experiments_root,
94
+ staged_experiments_root=staged_experiments_root,
95
+ staged_target_dir=staged_target_dir,
96
+ )
97
+ relative_outputs = provider_outputs.relative_to(provider_root)
98
+ staged_outputs = staged_provider_dir / relative_outputs
99
+ source_store = runtime.record_store(
100
+ provider_outputs,
101
+ experiment_root=provider_root,
102
+ create=False,
103
+ )
104
+ catalog = {
105
+ "schema_version": RECORD_CATALOG_SCHEMA_VERSION,
106
+ "provenance_epoch_id": source_store.provenance_epoch_id(),
107
+ "latest": {},
108
+ "history": {},
109
+ }
110
+ source_records = {record.record_id: record for record in source_store.iter_latest_records()}
111
+ copied_paths: set[Path] = set()
112
+ for record_id in sorted(record_ids):
113
+ record = source_records.get(record_id)
114
+ if record is None:
115
+ raise RecordError(f"Source record {record_id!r} is missing from {provider_root.name!r}")
116
+ verify_record_artifact_integrity(record, outputs_dir=provider_outputs)
117
+ payload = record_to_dict(record, outputs_dir=provider_outputs)
118
+ catalog["latest"][record_id] = payload
119
+ catalog["history"][record_id] = [payload]
120
+ for source_path in record_paths(record):
121
+ relative_path = source_path.resolve(strict=True).relative_to(provider_outputs)
122
+ if relative_path in copied_paths:
123
+ continue
124
+ copied_paths.add(relative_path)
125
+ staged_path = staged_outputs / relative_path
126
+ staged_path.parent.mkdir(parents=True, exist_ok=True)
127
+ shutil.copy2(source_path, staged_path)
128
+ for evidence in record.inputs:
129
+ if not isinstance(evidence.ref, SourceRecordRef):
130
+ continue
131
+ _stage_catalog_config(
132
+ source_root=evidence.ref.experiment_root,
133
+ source_experiments_root=source_experiments_root,
134
+ staged_experiments_root=staged_experiments_root,
135
+ staged_target_dir=staged_target_dir,
136
+ )
137
+
138
+ staged_manifests = staged_outputs / "manifests"
139
+ staged_manifests.mkdir(parents=True, exist_ok=True)
140
+ (staged_manifests / "records.json").write_text(
141
+ json.dumps(catalog, indent=2, sort_keys=True) + "\n",
142
+ encoding="utf-8",
143
+ )
144
+
145
+
146
+ def _stage_catalog_config(
147
+ *,
148
+ source_root: Path,
149
+ source_experiments_root: Path,
150
+ staged_experiments_root: Path,
151
+ staged_target_dir: Path,
152
+ ) -> Path:
153
+ staged_dir = staged_experiments_root / source_root.resolve().relative_to(source_experiments_root)
154
+ if staged_dir == staged_target_dir:
155
+ return staged_dir
156
+ staged_dir.mkdir(parents=True, exist_ok=True)
157
+ staged_config = staged_dir / "config.yaml"
158
+ if not staged_config.exists():
159
+ shutil.copy2(source_root / "config.yaml", staged_config)
160
+ return staged_dir
161
+
162
+
163
+ def _retarget_internal_symlinks(*, source_dir: Path, target_dir: Path) -> None:
164
+ """Map confined source symlinks into the staged tree without following links while copying."""
165
+
166
+ source_root = source_dir.resolve(strict=True)
167
+ target_root = target_dir.resolve(strict=True)
168
+ for current, dirnames, filenames in os.walk(target_root, followlinks=False):
169
+ current_path = Path(current)
170
+ for name in [*dirnames, *filenames]:
171
+ staged_link = current_path / name
172
+ if not staged_link.is_symlink():
173
+ continue
174
+ relative_link = staged_link.relative_to(target_root)
175
+ source_link = source_root / relative_link
176
+ try:
177
+ source_target = source_link.resolve(strict=True)
178
+ relative_target = source_target.relative_to(source_root)
179
+ except (OSError, RuntimeError, ValueError):
180
+ # External, dangling, or cyclic links remain links. Staged
181
+ # validation can then reject them without copytree traversing them.
182
+ continue
183
+ staged_target = target_root / relative_target
184
+ link_target = os.path.relpath(staged_target, start=staged_link.parent)
185
+ target_is_directory = source_target.is_dir()
186
+ staged_link.unlink()
187
+ staged_link.symlink_to(link_target, target_is_directory=target_is_directory)
@@ -0,0 +1,51 @@
1
+ from __future__ import annotations
2
+
3
+ from . import audit as _audit # noqa: F401
4
+ from . import demo as _demo # noqa: F401
5
+ from . import dop as _dop # noqa: F401
6
+ from . import experiments as _experiments # noqa: F401
7
+ from . import maintenance as _maintenance # noqa: F401
8
+ from . import notebooks as notebook_commands
9
+ from . import protocols as _protocols # noqa: F401
10
+ from . import shared
11
+ from . import surfaces as _surfaces # noqa: F401
12
+ from . import verification as _verification # noqa: F401
13
+ from .audit import audit_app
14
+ from .experiments import config, explain, inspect, ls, run, validate
15
+ from .helpers import infer_job_path as _infer_job_path
16
+ from .main import main
17
+ from .maintenance import maintain_app
18
+ from .notebooks import _launch_marimo, notebook
19
+ from .protocols import init, protocols
20
+ from .shared import THEME, app, console, subprocess
21
+ from .surfaces import export, plot, plugins, records, steps
22
+ from .verification import verify
23
+
24
+ __all__ = [
25
+ "THEME",
26
+ "_infer_job_path",
27
+ "_launch_marimo",
28
+ "audit_app",
29
+ "app",
30
+ "config",
31
+ "console",
32
+ "explain",
33
+ "export",
34
+ "init",
35
+ "inspect",
36
+ "ls",
37
+ "maintain_app",
38
+ "main",
39
+ "notebook",
40
+ "notebook_commands",
41
+ "plot",
42
+ "plugins",
43
+ "protocols",
44
+ "records",
45
+ "run",
46
+ "shared",
47
+ "steps",
48
+ "subprocess",
49
+ "validate",
50
+ "verify",
51
+ ]
@@ -0,0 +1,9 @@
1
+ from __future__ import annotations
2
+
3
+ from functools import cache
4
+ from importlib import import_module
5
+
6
+
7
+ @cache
8
+ def load(module_name: str):
9
+ return import_module(module_name)