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,287 @@
1
+ from __future__ import annotations
2
+
3
+ from copy import deepcopy
4
+
5
+ from reader_workbench.runtime import ReaderRuntime
6
+ from reader_workbench.workbench.config import ReaderSpec
7
+ from reader_workbench.workbench.decl import RecordCollectionInputDecl, WorkbenchDecl
8
+ from reader_workbench.workbench.graph import OutputRef, input_ref_display, output_ref_to_dict, resolve_workbench
9
+ from reader_workbench.workbench.records import RecordVerificationScope
10
+
11
+
12
+ def plot_output_summaries(bound_protocol) -> dict[str, str]:
13
+ return {item.id: item.summary for item in bound_protocol.descriptor.figures}
14
+
15
+
16
+ def export_output_summaries(bound_protocol) -> dict[str, str]:
17
+ return {item.id: item.summary for item in bound_protocol.descriptor.artifacts}
18
+
19
+
20
+ def selected_plan_payload(*, spec: ReaderSpec, decl: WorkbenchDecl, runtime: ReaderRuntime) -> dict[str, object]:
21
+ bound_protocol = runtime.bind_protocol(decl.experiment_semantics.protocol)
22
+ workbench = resolve_workbench(decl)
23
+ plot_ids = [spec_decl.id for spec_decl in workbench.plots]
24
+ export_ids = [spec_decl.id for spec_decl in workbench.exports]
25
+ pipeline_ids = [step.id for step in workbench.pipeline]
26
+ return {
27
+ "plot_profile": spec.protocol.outputs.plots.profile or bound_protocol.default_plot_profile or "—",
28
+ "pipeline": {"count": len(pipeline_ids), "ids": pipeline_ids},
29
+ "plots": {"count": len(plot_ids), "ids": plot_ids},
30
+ "exports": {"count": len(export_ids), "ids": export_ids},
31
+ }
32
+
33
+
34
+ def selected_plan_summary(selected: dict[str, object] | None) -> str:
35
+ if not selected:
36
+ return "—"
37
+ pipeline = dict(selected["pipeline"])
38
+ plots = dict(selected["plots"])
39
+ exports = dict(selected["exports"])
40
+ profile = str(selected.get("plot_profile") or "—")
41
+ return f"{profile} • {pipeline['count']} st • {plots['count']} pl • {exports['count']} ex"
42
+
43
+
44
+ def generated_summary(generated: dict[str, int]) -> str:
45
+ return (
46
+ f"{generated['records']} rec • "
47
+ f"{generated['plots']} pl • "
48
+ f"{generated['exports']} ex • "
49
+ f"{generated['notebooks']} nb"
50
+ )
51
+
52
+
53
+ def implementation_plan_payload(
54
+ *,
55
+ bound_protocol,
56
+ decl: WorkbenchDecl,
57
+ pipeline_steps,
58
+ plot_steps,
59
+ export_steps,
60
+ ) -> dict[str, object]:
61
+ return {
62
+ "protocol": bound_protocol.id,
63
+ "input_sections": sorted(bound_protocol.inputs),
64
+ "analysis_knobs": sorted(bound_protocol.analysis),
65
+ "resources": sorted(decl.experiment_semantics.resources.by_id.keys()),
66
+ "pipeline_flow": [step.id for step in pipeline_steps],
67
+ "plots": [step.id for step in plot_steps],
68
+ "exports": [step.id for step in export_steps],
69
+ }
70
+
71
+
72
+ def compiled_workbench_payload(
73
+ *,
74
+ bound_protocol,
75
+ pipeline_steps,
76
+ plot_steps,
77
+ export_steps,
78
+ runtime: ReaderRuntime,
79
+ record_producers,
80
+ ) -> dict[str, object]:
81
+ plot_summaries = plot_output_summaries(bound_protocol)
82
+ export_summaries = export_output_summaries(bound_protocol)
83
+ return {
84
+ "pipeline": [
85
+ pipeline_step_payload(step, runtime=runtime, record_producers=record_producers) for step in pipeline_steps
86
+ ],
87
+ "plots": [
88
+ spec_step_payload(
89
+ step,
90
+ summary=plot_summaries.get(step.id, "—"),
91
+ runtime=runtime,
92
+ record_producers=record_producers,
93
+ )
94
+ for step in plot_steps
95
+ ],
96
+ "exports": [
97
+ spec_step_payload(
98
+ step,
99
+ summary=export_summaries.get(step.id, "—"),
100
+ runtime=runtime,
101
+ record_producers=record_producers,
102
+ )
103
+ for step in export_steps
104
+ ],
105
+ }
106
+
107
+
108
+ def record_producer_map(steps, *, runtime: ReaderRuntime) -> dict[str, dict[str, object]]:
109
+ producers: dict[str, dict[str, object]] = {}
110
+ for step in steps:
111
+ plugin_cls = runtime.plugins.resolve_descriptor(step.plugin).cls
112
+ for output_name, port in plugin_cls.output_ports().items():
113
+ if port.kind != "dataframe":
114
+ continue
115
+ record_ref = (step.writes or {}).get(output_name, OutputRef(record_id=f"{step.id}/{output_name}"))
116
+ producers[record_ref.record_id] = {
117
+ "producer": {"id": step.id, "plugin": step.plugin, "stage": step.plugin.split("/", 1)[0]},
118
+ "output": output_name,
119
+ "contract": port.contract,
120
+ "surface": _output_surface_payload(port),
121
+ }
122
+ return producers
123
+
124
+
125
+ def workbench_record_verification_scope(workbench, *, runtime: ReaderRuntime) -> RecordVerificationScope:
126
+ return RecordVerificationScope(record_ids=workbench_record_ids(workbench, runtime=runtime))
127
+
128
+
129
+ def workbench_record_ids(workbench, *, runtime: ReaderRuntime) -> frozenset[str]:
130
+ """Return record identities owned by the current compiled workbench."""
131
+
132
+ record_ids = set(record_producer_map(workbench.plugin_steps(), runtime=runtime))
133
+ record_ids.update(f"plot:{step.id}" for step in workbench.plots)
134
+ record_ids.update(f"export:{step.id}" for step in workbench.exports)
135
+ return frozenset(record_ids)
136
+
137
+
138
+ def pipeline_step_payload(step, *, runtime: ReaderRuntime, record_producers=None) -> dict[str, object]:
139
+ plugin_cls = runtime.plugins.resolve_descriptor(step.plugin).cls
140
+ return {
141
+ "stage": step.plugin.split("/", 1)[0],
142
+ "id": step.id,
143
+ "plugin": step.plugin,
144
+ "semantics": plugin_semantics_payload(step.plugin, runtime=runtime),
145
+ "reads": serialize_reads(
146
+ step.reads, declared_ports=plugin_cls.input_ports(), record_producers=record_producers
147
+ ),
148
+ "writes": pipeline_writes_payload(step, runtime=runtime),
149
+ }
150
+
151
+
152
+ def spec_step_payload(step, *, summary: str, runtime: ReaderRuntime, record_producers=None) -> dict[str, object]:
153
+ plugin_cls = runtime.plugins.resolve_descriptor(step.plugin).cls
154
+ return {
155
+ "id": step.id,
156
+ "plugin": step.plugin,
157
+ "summary": summary,
158
+ "semantics": plugin_semantics_payload(step.plugin, runtime=runtime),
159
+ "reads": serialize_reads(
160
+ step.reads, declared_ports=plugin_cls.input_ports(), record_producers=record_producers
161
+ ),
162
+ }
163
+
164
+
165
+ def render_read_binding(item: dict[str, object]) -> str:
166
+ rendered = f"{item['label']} <- {item['display']}"
167
+ source = item.get("source")
168
+ if not isinstance(source, dict):
169
+ return rendered
170
+ surface = source.get("surface")
171
+ if not isinstance(surface, dict):
172
+ return rendered
173
+ producer = source.get("producer")
174
+ producer_id = producer.get("id") if isinstance(producer, dict) else None
175
+ if not isinstance(producer_id, str) or not producer_id:
176
+ return rendered
177
+ mode = surface.get("runtime_mode")
178
+ promoted = [str(value) for value in (surface.get("promoted") or []) if str(value).strip()]
179
+ contract = item.get("contract")
180
+ if mode == "promoted" and promoted:
181
+ return f"{rendered} (via {producer_id}; may promote to {', '.join(promoted)})"
182
+ if mode == "passthrough" and isinstance(contract, str) and contract in promoted:
183
+ return f"{rendered} (via {producer_id}; preserves {contract})"
184
+ return rendered
185
+
186
+
187
+ def plugin_semantics_payload(plugin: str, *, runtime: ReaderRuntime) -> dict[str, object]:
188
+ descriptor = runtime.plugins.resolve_descriptor(plugin)
189
+ return {
190
+ "category": descriptor.category,
191
+ "domain": descriptor.domain,
192
+ "family": descriptor.family,
193
+ "summary": descriptor.summary,
194
+ }
195
+
196
+
197
+ def serialize_reads(reads, *, declared_ports=None, record_producers=None) -> list[dict[str, object]]:
198
+ payload: list[dict[str, object]] = []
199
+ for label, ref in (reads or {}).items():
200
+ item: dict[str, object] = {"label": label, "display": _binding_display(ref)}
201
+ record_id = getattr(ref, "record_id", None)
202
+ resource_id = getattr(ref, "resource_id", None)
203
+ path = getattr(ref, "path", None)
204
+ if isinstance(record_id, str) and record_id:
205
+ item["ref"] = {"record": record_id}
206
+ if record_producers and record_id in record_producers:
207
+ item["source"] = deepcopy(record_producers[record_id])
208
+ elif isinstance(resource_id, str) and resource_id:
209
+ item["ref"] = {"resource": resource_id}
210
+ elif isinstance(ref, RecordCollectionInputDecl):
211
+ item["ref"] = {"record_resources": list(ref.resource_ids)}
212
+ elif path is not None:
213
+ item["ref"] = {"file": str(path)}
214
+ else:
215
+ item["ref"] = {"display": _binding_display(ref)}
216
+ declared_port = (declared_ports or {}).get(label)
217
+ if declared_port is not None:
218
+ item["kind"] = declared_port.kind
219
+ item["declared"] = declared_port.render()
220
+ if declared_port.contract is not None:
221
+ item["contract"] = declared_port.contract
222
+ if declared_port.optional:
223
+ item["optional"] = True
224
+ payload.append(item)
225
+ return payload
226
+
227
+
228
+ def pipeline_writes_payload(step, *, runtime: ReaderRuntime) -> list[dict[str, object]]:
229
+ plugin_cls = runtime.plugins.resolve_descriptor(step.plugin).cls
230
+ outputs: list[dict[str, object]] = []
231
+ for output_name, port in plugin_cls.output_ports().items():
232
+ if port.kind == "dataframe":
233
+ record_ref = (step.writes or {}).get(output_name, OutputRef(record_id=f"{step.id}/{output_name}"))
234
+ output: dict[str, object] = {
235
+ "label": output_name,
236
+ "kind": port.kind,
237
+ "declared": port.render(),
238
+ "display": record_ref.record_id,
239
+ "contract": port.contract,
240
+ "ref": output_ref_to_dict(record_ref),
241
+ }
242
+ surface = _output_surface_payload(port)
243
+ if surface is not None:
244
+ output["surface"] = surface
245
+ outputs.append(output)
246
+ continue
247
+ outputs.append(
248
+ {
249
+ "label": output_name,
250
+ "kind": port.kind,
251
+ "declared": port.render(),
252
+ "display": output_name,
253
+ }
254
+ )
255
+ return outputs
256
+
257
+
258
+ def binding_display(ref) -> str:
259
+ return _binding_display(ref)
260
+
261
+
262
+ def _binding_display(ref) -> str:
263
+ if isinstance(ref, RecordCollectionInputDecl):
264
+ return "record_resources(" + ", ".join(ref.resource_ids) + ")"
265
+ record_id = getattr(ref, "record_id", None)
266
+ if isinstance(record_id, str) and record_id:
267
+ return record_id
268
+ resource_id = getattr(ref, "resource_id", None)
269
+ if isinstance(resource_id, str) and resource_id:
270
+ return f"resource({resource_id})"
271
+ path = getattr(ref, "path", None)
272
+ if path is not None:
273
+ return str(path)
274
+ return input_ref_display(ref)
275
+
276
+
277
+ def _output_surface_payload(port) -> dict[str, object] | None:
278
+ surface = getattr(port, "contract_surface", None)
279
+ if surface is None:
280
+ return None
281
+ return {
282
+ "minimum": surface.minimum,
283
+ "runtime_mode": surface.runtime_mode,
284
+ "promoted": list(surface.promoted),
285
+ "note": surface.note,
286
+ "rendered": surface.render(),
287
+ }
@@ -0,0 +1,192 @@
1
+ from __future__ import annotations
2
+
3
+ from rich import box
4
+ from rich.table import Table
5
+
6
+
7
+ def _table(title: str) -> Table:
8
+ return Table(
9
+ title=f"[title]{title}[/title]",
10
+ title_justify="left",
11
+ header_style="bold",
12
+ box=box.ROUNDED,
13
+ expand=True,
14
+ show_lines=False,
15
+ show_edge=True,
16
+ )
17
+
18
+
19
+ def semantic_node_payload(node, *, include_execution: bool = True) -> dict[str, object]:
20
+ payload = {
21
+ "id": node.id,
22
+ "kind": node.kind,
23
+ "summary": node.summary,
24
+ "profiles": list(node.profiles),
25
+ }
26
+ if include_execution:
27
+ payload["execution"] = {
28
+ "status": node.execution.status,
29
+ "step_ids": list(node.execution.step_ids),
30
+ "plugin_ids": list(node.execution.plugin_ids),
31
+ "record_ids": list(node.execution.record_ids),
32
+ "config_paths": list(node.execution.config_paths),
33
+ "note": node.execution.note,
34
+ }
35
+ if node.kind == "control_rule":
36
+ payload["match_on"] = list(node.match_on)
37
+ payload["control_selector"] = node.control_selector
38
+ if node.kind == "window":
39
+ payload["anchor"] = node.anchor
40
+ payload["selector"] = node.selector
41
+ payload["params"] = dict(node.params)
42
+ if node.kind == "metric":
43
+ payload["stage"] = node.stage
44
+ payload["formula"] = node.formula
45
+ payload["depends_on"] = list(node.depends_on)
46
+ payload["value_space"] = node.value_space
47
+ payload["unit"] = node.unit
48
+ payload["comparable_group"] = node.comparable_group
49
+ if node.kind == "ranking":
50
+ payload["primary_metric"] = node.primary_metric
51
+ payload["direction"] = node.direction
52
+ payload["penalties"] = list(node.penalties)
53
+ payload["supporting_metrics"] = list(node.supporting_metrics)
54
+ return payload
55
+
56
+
57
+ def semantic_program_structure_summary(program) -> dict[str, object]:
58
+ summary = {
59
+ "total": 0,
60
+ "by_kind": {
61
+ "control_rule": 0,
62
+ "window": 0,
63
+ "metric": 0,
64
+ "ranking": 0,
65
+ },
66
+ }
67
+
68
+ def _record(node) -> None:
69
+ summary["total"] += 1
70
+ summary["by_kind"][str(node.kind)] += 1
71
+
72
+ for node in program.controls:
73
+ _record(node)
74
+ for node in program.windows:
75
+ _record(node)
76
+ for node in program.metrics:
77
+ _record(node)
78
+ if program.ranking is not None:
79
+ _record(program.ranking)
80
+ return summary
81
+
82
+
83
+ def semantic_program_execution_summary(program) -> dict[str, object]:
84
+ summary = {
85
+ "total": 0,
86
+ "compiled": 0,
87
+ "descriptive_only": 0,
88
+ "by_kind": {
89
+ "control_rule": {"total": 0, "compiled": 0, "descriptive_only": 0},
90
+ "window": {"total": 0, "compiled": 0, "descriptive_only": 0},
91
+ "metric": {"total": 0, "compiled": 0, "descriptive_only": 0},
92
+ "ranking": {"total": 0, "compiled": 0, "descriptive_only": 0},
93
+ },
94
+ }
95
+
96
+ def _record(node) -> None:
97
+ status = str(node.execution.status)
98
+ kind = str(node.kind)
99
+ summary["total"] += 1
100
+ summary[status] += 1
101
+ kind_counts = dict(summary["by_kind"][kind])
102
+ kind_counts["total"] += 1
103
+ kind_counts[status] += 1
104
+ summary["by_kind"][kind] = kind_counts
105
+
106
+ for node in program.controls:
107
+ _record(node)
108
+ for node in program.windows:
109
+ _record(node)
110
+ for node in program.metrics:
111
+ _record(node)
112
+ if program.ranking is not None:
113
+ _record(program.ranking)
114
+ return summary
115
+
116
+
117
+ def semantic_program_payload(program, *, include_execution: bool = True) -> dict[str, object]:
118
+ return {
119
+ "protocol": program.protocol,
120
+ "profiles": [
121
+ {
122
+ "id": profile.id,
123
+ "family": profile.family,
124
+ "summary": profile.summary,
125
+ "primary_metric": profile.primary_metric,
126
+ "primary_readout": profile.primary_readout,
127
+ "tags": list(profile.tags),
128
+ }
129
+ for profile in program.profiles
130
+ ],
131
+ "active_profile": program.active_profile,
132
+ "summary": (
133
+ semantic_program_execution_summary(program)
134
+ if include_execution
135
+ else semantic_program_structure_summary(program)
136
+ ),
137
+ "controls": [semantic_node_payload(node, include_execution=include_execution) for node in program.controls],
138
+ "windows": [semantic_node_payload(node, include_execution=include_execution) for node in program.windows],
139
+ "metrics": [semantic_node_payload(node, include_execution=include_execution) for node in program.metrics],
140
+ "ranking": (
141
+ semantic_node_payload(program.ranking, include_execution=include_execution)
142
+ if program.ranking is not None
143
+ else None
144
+ ),
145
+ }
146
+
147
+
148
+ def semantic_program_table(
149
+ program,
150
+ *,
151
+ title: str = "Semantic Program",
152
+ include_execution: bool = True,
153
+ ) -> Table:
154
+ profile_text = f" • profile: {program.active_profile}" if program.active_profile else ""
155
+ if include_execution:
156
+ coverage = semantic_program_execution_summary(program)
157
+ table = _table(
158
+ f"{title}{profile_text} • {coverage['compiled']}/{coverage['total']} compiled"
159
+ f" • {coverage['descriptive_only']} descriptive"
160
+ )
161
+ table.add_column("kind", style="accent", width=13)
162
+ table.add_column("id", style="accent", overflow="fold")
163
+ table.add_column("status", width=18)
164
+ table.add_column("compiled via", overflow="fold")
165
+ table.add_column("summary", overflow="fold")
166
+
167
+ def _add_node(kind: str, node) -> None:
168
+ compiled_via = ", ".join(node.execution.step_ids) or "—"
169
+ note = node.execution.note
170
+ summary = node.summary if not note else f"{node.summary} ({note})"
171
+ table.add_row(kind, node.id, node.execution.status, compiled_via, summary)
172
+ else:
173
+ coverage = semantic_program_structure_summary(program)
174
+ table = _table(f"{title}{profile_text} • {coverage['total']} node(s)")
175
+ table.add_column("kind", style="accent", width=13)
176
+ table.add_column("id", style="accent", overflow="fold")
177
+ table.add_column("profiles", overflow="fold")
178
+ table.add_column("summary", overflow="fold")
179
+
180
+ def _add_node(kind: str, node) -> None:
181
+ profiles = ", ".join(node.profiles) or "all"
182
+ table.add_row(kind, node.id, profiles, node.summary)
183
+
184
+ for node in program.controls:
185
+ _add_node("control_rule", node)
186
+ for node in program.windows:
187
+ _add_node("window", node)
188
+ for node in program.metrics:
189
+ _add_node("metric", node)
190
+ if program.ranking is not None:
191
+ _add_node("ranking", program.ranking)
192
+ return table
@@ -0,0 +1,30 @@
1
+ from __future__ import annotations
2
+
3
+ from copy import deepcopy
4
+ from typing import Any
5
+
6
+
7
+ def validation_surface_payload(
8
+ *,
9
+ experiment: dict[str, object],
10
+ check_files: bool,
11
+ summary: dict[str, Any],
12
+ ) -> dict[str, object]:
13
+ payload = deepcopy(summary)
14
+ return {
15
+ "experiment": deepcopy(experiment),
16
+ "selection": {
17
+ "check_files": check_files,
18
+ },
19
+ "summary": {
20
+ "status": payload.pop("status"),
21
+ "checks": payload.pop("checks"),
22
+ "counts": payload.pop("counts"),
23
+ },
24
+ "validation": {
25
+ "files": payload.pop("files"),
26
+ "dependencies": payload.pop("dependencies"),
27
+ "errors": payload.pop("errors"),
28
+ "tip": payload.pop("tip"),
29
+ },
30
+ }
@@ -0,0 +1,17 @@
1
+ from __future__ import annotations
2
+
3
+ import importlib
4
+
5
+ _EXPORTS = {
6
+ "scaffold": {"CANONICAL_NOTEBOOK_ID", "write_experiment_notebook"},
7
+ }
8
+
9
+ __all__ = tuple(sorted({name for names in _EXPORTS.values() for name in names}))
10
+
11
+
12
+ def __getattr__(name: str):
13
+ for module_name, names in _EXPORTS.items():
14
+ if name in names:
15
+ module = importlib.import_module(f"reader_workbench.workbench.notebooks.{module_name}")
16
+ return getattr(module, name)
17
+ raise AttributeError(name)
@@ -0,0 +1,112 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from collections.abc import Callable
5
+ from dataclasses import asdict, dataclass
6
+ from pathlib import Path
7
+
8
+
9
+ @dataclass(frozen=True)
10
+ class MarimoSessionRecord:
11
+ pid: int
12
+ port: int
13
+ host: str
14
+ mode: str
15
+ notebook: str
16
+ experiment_root: str
17
+ repo_root: str
18
+ launched_at: float
19
+ notebook_mtime_ns: int | None = None
20
+ notebook_size_bytes: int | None = None
21
+ runtime_fingerprint: str | None = None
22
+
23
+ @classmethod
24
+ def from_dict(cls, payload: dict[str, object]) -> MarimoSessionRecord | None:
25
+ try:
26
+ return cls(
27
+ pid=int(payload["pid"]),
28
+ port=int(payload["port"]),
29
+ host=str(payload["host"]),
30
+ mode=str(payload["mode"]),
31
+ notebook=str(payload["notebook"]),
32
+ experiment_root=str(payload["experiment_root"]),
33
+ repo_root=str(payload["repo_root"]),
34
+ launched_at=float(payload["launched_at"]),
35
+ notebook_mtime_ns=(
36
+ int(payload["notebook_mtime_ns"]) if payload.get("notebook_mtime_ns") is not None else None
37
+ ),
38
+ notebook_size_bytes=(
39
+ int(payload["notebook_size_bytes"]) if payload.get("notebook_size_bytes") is not None else None
40
+ ),
41
+ runtime_fingerprint=(
42
+ str(payload["runtime_fingerprint"]) if payload.get("runtime_fingerprint") is not None else None
43
+ ),
44
+ )
45
+ except (KeyError, TypeError, ValueError):
46
+ return None
47
+
48
+
49
+ def load_registry(registry_path: Path) -> list[MarimoSessionRecord]:
50
+ if not registry_path.exists():
51
+ return []
52
+ try:
53
+ payload = json.loads(registry_path.read_text(encoding="utf-8"))
54
+ except (OSError, json.JSONDecodeError):
55
+ return []
56
+ if not isinstance(payload, list):
57
+ return []
58
+ records: list[MarimoSessionRecord] = []
59
+ for item in payload:
60
+ if isinstance(item, dict):
61
+ record = MarimoSessionRecord.from_dict(item)
62
+ if record is not None:
63
+ records.append(record)
64
+ return records
65
+
66
+
67
+ def write_registry(registry_path: Path, records: list[MarimoSessionRecord]) -> None:
68
+ registry_path.parent.mkdir(parents=True, exist_ok=True)
69
+ payload = [asdict(record) for record in records]
70
+ registry_path.write_text(json.dumps(payload, indent=2, sort_keys=True), encoding="utf-8")
71
+
72
+
73
+ def prune_registry(
74
+ records: list[MarimoSessionRecord],
75
+ *,
76
+ pid_is_live: Callable[[int], bool],
77
+ ) -> list[MarimoSessionRecord]:
78
+ pruned: list[MarimoSessionRecord] = []
79
+ for record in records:
80
+ notebook_path = Path(record.notebook)
81
+ if not notebook_path.exists():
82
+ continue
83
+ if not pid_is_live(record.pid):
84
+ continue
85
+ pruned.append(record)
86
+ return pruned
87
+
88
+
89
+ def session_matches_current_inputs(
90
+ record: MarimoSessionRecord,
91
+ *,
92
+ mode: str,
93
+ resolved_target: Path,
94
+ experiment_root: Path,
95
+ runtime_fingerprint: str,
96
+ notebook_mtime_ns: int,
97
+ notebook_size_bytes: int,
98
+ port_is_open: Callable[[str, int], bool],
99
+ ) -> bool:
100
+ if record.mode != mode:
101
+ return False
102
+ if record.notebook != str(resolved_target):
103
+ return False
104
+ if record.experiment_root != str(experiment_root):
105
+ return False
106
+ if record.notebook_mtime_ns != notebook_mtime_ns:
107
+ return False
108
+ if record.notebook_size_bytes != notebook_size_bytes:
109
+ return False
110
+ if record.runtime_fingerprint != runtime_fingerprint:
111
+ return False
112
+ return port_is_open(record.host, record.port)