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,367 @@
1
+ from __future__ import annotations
2
+
3
+ from rich import box
4
+ from rich.panel import Panel
5
+ from rich.table import Table
6
+ from rich.text import Text
7
+
8
+ from reader_workbench.workbench.graph import OutputRef, input_ref_display, output_ref_display
9
+
10
+ from .common import flatten_binding_rows
11
+ from .results import record_payload_detail_text
12
+ from .runtime import render_read_binding
13
+ from .semantics import semantic_program_table
14
+
15
+
16
+ def experiment_inspect_renderables(
17
+ *,
18
+ payload: dict[str, object],
19
+ semantic_program,
20
+ ) -> list[Panel]:
21
+ experiment = dict(payload.get("experiment") or {})
22
+ authoring = dict(payload.get("authoring") or {})
23
+ implementation = dict(payload.get("implementation") or {})
24
+ compiled = dict(implementation.get("compiled") or {})
25
+ inputs = dict(implementation.get("inputs") or {})
26
+ generated = dict(implementation.get("generated") or {})
27
+ readiness = dict(implementation.get("readiness") or {})
28
+ generated_counts = dict(generated.get("counts") or {})
29
+ generated_examples = dict(generated.get("examples") or {})
30
+ input_counts = dict(inputs.get("counts") or {})
31
+ input_files = [str(item) for item in (inputs.get("files") or [])]
32
+ resources = [dict(item) for item in (inputs.get("resources") or []) if isinstance(item, dict)]
33
+ records_payload = [dict(item) for item in (generated.get("records") or []) if isinstance(item, dict)]
34
+ authoring_rows: list[tuple[str, str, str]] = []
35
+ for section in ("inputs", "analysis", "outputs"):
36
+ values = authoring.get(section)
37
+ if isinstance(values, dict):
38
+ for path, value in flatten_binding_rows(values):
39
+ authoring_rows.append((section, path, value))
40
+
41
+ renderables: list[Panel] = []
42
+
43
+ overview = Table(box=box.ROUNDED, expand=True, show_header=False)
44
+ overview.add_column("Field", style="accent", no_wrap=True)
45
+ overview.add_column("Value")
46
+ overview.add_row("Experiment", str(experiment.get("id") or "—"))
47
+ if str(experiment.get("lifecycle") or "active") != "active":
48
+ overview.add_row("Lifecycle", str(experiment.get("lifecycle") or "—"))
49
+ overview.add_row("Protocol", str(experiment.get("protocol") or "—"))
50
+ overview.add_row("Config", str(experiment.get("config") or "—"))
51
+ overview.add_row("Root", str(experiment.get("root") or "—"))
52
+ overview.add_row("Inputs", f"{int(input_counts.get('files') or len(input_files))} file(s) under inputs/")
53
+ overview.add_row(
54
+ "Generated",
55
+ (
56
+ f"{int(generated_counts.get('records') or 0)} records, "
57
+ f"{int(generated_counts.get('plots') or 0)} plots, "
58
+ f"{int(generated_counts.get('exports') or 0)} exports, "
59
+ f"{int(generated_counts.get('notebooks') or 0)} notebooks"
60
+ ),
61
+ )
62
+ overview.add_row("Plot profile", str(experiment.get("plot_profile") or "—"))
63
+ renderables.append(Panel(overview, title="Experiment overview", border_style="accent", box=box.ROUNDED))
64
+
65
+ if readiness:
66
+ readiness_table = _table("Readiness")
67
+ readiness_table.add_column("field", style="accent", width=18)
68
+ readiness_table.add_column("value", overflow="fold")
69
+ preflight = dict(readiness.get("preflight") or {})
70
+ capabilities = dict(readiness.get("capabilities") or {})
71
+ next_steps = [dict(item) for item in (readiness.get("next_steps") or []) if isinstance(item, dict)]
72
+ readiness_table.add_row("State", str(readiness.get("summary") or readiness.get("state") or "—"))
73
+ readiness_table.add_row(
74
+ "Preflight",
75
+ (
76
+ f"{preflight.get('status', '—')} • "
77
+ f"files={preflight.get('files', '—')} • "
78
+ f"deps={preflight.get('dependencies', '—')}"
79
+ ),
80
+ )
81
+ readiness_table.add_row(
82
+ "Capabilities",
83
+ ", ".join(f"{key}={'yes' if bool(value) else 'no'}" for key, value in capabilities.items()) or "—",
84
+ )
85
+ errors = [str(item) for item in (readiness.get("errors") or [])]
86
+ readiness_table.add_row("Issues", errors[0] if errors else "—")
87
+ if len(errors) > 1:
88
+ readiness_table.add_row("More issues", f"{len(errors) - 1} more")
89
+ if next_steps:
90
+ readiness_table.add_row(
91
+ "Next",
92
+ "\n".join(f"{item.get('command')} — {item.get('description')}" for item in next_steps[:4]),
93
+ )
94
+ renderables.append(Panel(readiness_table, border_style="accent", box=box.ROUNDED))
95
+
96
+ authoring_table = _table("Config values")
97
+ authoring_table.add_column("section", style="accent", width=10)
98
+ authoring_table.add_column("path", overflow="fold")
99
+ authoring_table.add_column("value", overflow="fold")
100
+ if authoring_rows:
101
+ for section, path, value in authoring_rows:
102
+ authoring_table.add_row(section, path, value)
103
+ else:
104
+ authoring_table.add_row("—", "—", "No explicit bindings; protocol defaults only.")
105
+ renderables.append(Panel(authoring_table, border_style="accent", box=box.ROUNDED))
106
+
107
+ renderables.append(
108
+ Panel(
109
+ semantic_program_table(semantic_program, include_execution=False),
110
+ border_style="accent",
111
+ box=box.ROUNDED,
112
+ )
113
+ )
114
+ renderables.append(
115
+ Panel(
116
+ semantic_program_table(
117
+ semantic_program,
118
+ title="Compiled Semantic Execution",
119
+ ),
120
+ border_style="accent",
121
+ box=box.ROUNDED,
122
+ )
123
+ )
124
+
125
+ filesystem = _table("Inputs + resources")
126
+ filesystem.add_column("kind", style="accent", width=10)
127
+ filesystem.add_column("entry")
128
+ filesystem.add_column("details")
129
+ if input_files:
130
+ for relpath in input_files:
131
+ filesystem.add_row("input", relpath, "detected under inputs/")
132
+ remaining_inputs = int(input_counts.get("files") or len(input_files)) - len(input_files)
133
+ if remaining_inputs > 0:
134
+ filesystem.add_row("input", "…", f"{remaining_inputs} more file(s)")
135
+ else:
136
+ filesystem.add_row("input", "—", "No visible files under inputs/")
137
+ if resources:
138
+ for resource in resources:
139
+ details = resource.get("path")
140
+ if resource.get("kind") == "record":
141
+ details = f"{resource.get('experiment') or '—'}:{resource.get('record') or '—'}"
142
+ filesystem.add_row("resource", str(resource.get("id") or "—"), str(details or "—"))
143
+ renderables.append(Panel(filesystem, border_style="accent", box=box.ROUNDED))
144
+
145
+ generated_table = _table("Generated outputs")
146
+ generated_table.add_column("kind", style="accent", width=10)
147
+ generated_table.add_column("count", justify="right", width=7)
148
+ generated_table.add_column("examples", overflow="fold")
149
+ generated_table.add_row(
150
+ "records", str(generated_counts.get("records") or 0), str(generated_examples.get("records") or "—")
151
+ )
152
+ generated_table.add_row(
153
+ "plots", str(generated_counts.get("plots") or 0), str(generated_examples.get("plots") or "—")
154
+ )
155
+ generated_table.add_row(
156
+ "exports", str(generated_counts.get("exports") or 0), str(generated_examples.get("exports") or "—")
157
+ )
158
+ generated_table.add_row(
159
+ "notebooks",
160
+ str(generated_counts.get("notebooks") or 0),
161
+ str(generated_examples.get("notebooks") or "—"),
162
+ )
163
+ renderables.append(Panel(generated_table, border_style="accent", box=box.ROUNDED))
164
+
165
+ records_table = _table("Records")
166
+ records_table.add_column("record", style="accent", overflow="fold")
167
+ records_table.add_column("kind", width=18)
168
+ records_table.add_column("producer", overflow="fold")
169
+ records_table.add_column("detail", overflow="fold")
170
+ if records_payload:
171
+ for record in records_payload:
172
+ records_table.add_row(
173
+ str(record.get("record_id") or "—"),
174
+ str(record.get("kind") or "—"),
175
+ str(record.get("producer_label") or "—"),
176
+ record_payload_detail_text(record),
177
+ )
178
+ else:
179
+ records_table.add_row("—", "—", "—", "No records found under outputs/manifests/records.json.")
180
+ renderables.append(Panel(records_table, border_style="accent", box=box.ROUNDED))
181
+
182
+ pipeline_rows = [dict(item) for item in (compiled.get("pipeline") or []) if isinstance(item, dict)]
183
+ pipeline_table = _table("Pipeline chain")
184
+ pipeline_table.add_column("#", justify="right", style="muted")
185
+ pipeline_table.add_column("stage", style="accent")
186
+ pipeline_table.add_column("id", overflow="fold")
187
+ pipeline_table.add_column("plugin", overflow="fold")
188
+ pipeline_table.add_column("from", overflow="fold")
189
+ pipeline_table.add_column("writes", overflow="fold")
190
+ for idx, step in enumerate(pipeline_rows, 1):
191
+ from_refs = ", ".join(render_read_binding(item) for item in (step.get("reads") or [])) or "—"
192
+ writes = (
193
+ ", ".join(
194
+ (
195
+ f"{item['label']} -> {item['display']}"
196
+ if isinstance(item, dict) and item.get("kind") == "dataframe"
197
+ else str(item.get("label") if isinstance(item, dict) else item)
198
+ )
199
+ for item in (step.get("writes") or [])
200
+ )
201
+ or "—"
202
+ )
203
+ pipeline_table.add_row(
204
+ str(idx),
205
+ str(step.get("stage") or "—"),
206
+ str(step.get("id") or "—"),
207
+ str(step.get("plugin") or "—"),
208
+ from_refs,
209
+ writes,
210
+ )
211
+ renderables.append(
212
+ Panel(
213
+ pipeline_table,
214
+ border_style="accent",
215
+ box=box.ROUNDED,
216
+ subtitle=f"[muted]{len(pipeline_rows)} step(s)[/muted]",
217
+ )
218
+ )
219
+
220
+ renderables.append(_surface_specs_panel(title="Plot outputs", rows=(compiled.get("plots") or [])))
221
+ renderables.append(_surface_specs_panel(title="Exports", rows=(compiled.get("exports") or [])))
222
+
223
+ return renderables
224
+
225
+
226
+ def workflow_explain_renderables(
227
+ *,
228
+ bound_protocol,
229
+ decl,
230
+ pipeline_steps,
231
+ plot_specs,
232
+ export_specs,
233
+ registry,
234
+ ) -> list[Panel]:
235
+ renderables: list[Panel] = []
236
+ summary = Table(box=box.ROUNDED, expand=True, show_header=False)
237
+ summary.add_column("Field", style="accent", no_wrap=True)
238
+ summary.add_column("Value")
239
+ summary.add_row("Protocol", bound_protocol.id)
240
+ summary.add_row("Input sections", ", ".join(sorted(bound_protocol.inputs)) if bound_protocol.inputs else "—")
241
+ summary.add_row("Analysis knobs", ", ".join(sorted(bound_protocol.analysis)) if bound_protocol.analysis else "—")
242
+ summary.add_row("Pipeline flow", " -> ".join(step.id for step in pipeline_steps) if pipeline_steps else "—")
243
+ summary.add_row("Plots", ", ".join(step.id for step in plot_specs) if plot_specs else "—")
244
+ summary.add_row("Exports", ", ".join(step.id for step in export_specs) if export_specs else "—")
245
+ resources = tuple(decl.experiment_semantics.resources.by_id.keys())
246
+ if resources:
247
+ summary.add_row("Resources", ", ".join(resources))
248
+ renderables.append(Panel(summary, border_style="cyan", box=box.ROUNDED, title="Plan summary"))
249
+
250
+ semantic_program = decl.experiment_semantics.protocol_program
251
+ renderables.append(
252
+ Panel(
253
+ semantic_program_table(semantic_program, include_execution=False),
254
+ border_style="cyan",
255
+ box=box.ROUNDED,
256
+ )
257
+ )
258
+ renderables.append(
259
+ Panel(
260
+ semantic_program_table(semantic_program, title="Compiled Semantic Execution"),
261
+ border_style="cyan",
262
+ box=box.ROUNDED,
263
+ )
264
+ )
265
+
266
+ if pipeline_steps:
267
+ renderables.append(
268
+ Panel(
269
+ _workflow_plan_table(pipeline_steps, registry, title="Pipeline"),
270
+ border_style="cyan",
271
+ box=box.ROUNDED,
272
+ subtitle=Text(f"{len(pipeline_steps)} steps", style="dim"),
273
+ )
274
+ )
275
+ if plot_specs:
276
+ renderables.append(
277
+ Panel(
278
+ _workflow_plan_table(plot_specs, registry, title="Plots"),
279
+ border_style="cyan",
280
+ box=box.ROUNDED,
281
+ subtitle=Text(f"{len(plot_specs)} specs", style="dim"),
282
+ )
283
+ )
284
+ if export_specs:
285
+ renderables.append(
286
+ Panel(
287
+ _workflow_plan_table(export_specs, registry, title="Exports"),
288
+ border_style="cyan",
289
+ box=box.ROUNDED,
290
+ subtitle=Text(f"{len(export_specs)} specs", style="dim"),
291
+ )
292
+ )
293
+ return renderables
294
+
295
+
296
+ def _workflow_plan_table(steps, registry, *, title: str) -> Table:
297
+ table = _table(title)
298
+ table.add_column("#", justify="right", style="muted")
299
+ table.add_column("Step ID", style="accent")
300
+ table.add_column("Plugin")
301
+ table.add_column("Type")
302
+ table.add_column("Inputs")
303
+ table.add_column("Outputs")
304
+ for index, step in enumerate(steps, 1):
305
+ descriptor = registry.resolve_descriptor(step.plugin)
306
+ plugin_cls = descriptor.cls
307
+ input_lines: list[str] = []
308
+ for name, port in plugin_cls.input_ports().items():
309
+ suffix = ", optional" if port.optional else ""
310
+ bound_ref = (step.reads or {}).get(name)
311
+ if bound_ref is not None:
312
+ input_lines.append(f"{name} <- {input_ref_display(bound_ref)} ({port.render()}{suffix})")
313
+ else:
314
+ input_lines.append(f"{name} ({port.render()}{suffix})")
315
+
316
+ output_lines: list[str] = []
317
+ for out_name, port in plugin_cls.output_ports().items():
318
+ if port.kind == "dataframe":
319
+ label_ref = (step.writes or {}).get(out_name, OutputRef(record_id=f"{step.id}/{out_name}"))
320
+ output_lines.append(f"{output_ref_display(label_ref)} ({port.render()})")
321
+ continue
322
+ if title.lower().startswith("plot"):
323
+ output_lines.append(f"{out_name} ({port.render()} → outputs/plots/)")
324
+ continue
325
+ if title.lower().startswith("export"):
326
+ output_lines.append(f"{out_name} ({port.render()} → outputs/exports/)")
327
+ continue
328
+ output_lines.append(f"{out_name} ({port.render()})")
329
+
330
+ table.add_row(
331
+ str(index),
332
+ step.id,
333
+ step.plugin,
334
+ f"{descriptor.domain}/{descriptor.family}",
335
+ "\n".join(input_lines) if input_lines else "—",
336
+ "\n".join(output_lines) if output_lines else "—",
337
+ )
338
+ return table
339
+
340
+
341
+ def _surface_specs_panel(*, title: str, rows) -> Panel:
342
+ table = _table(title)
343
+ table.add_column("#", justify="right", style="muted")
344
+ table.add_column("id", style="accent", overflow="fold")
345
+ table.add_column("summary", overflow="fold")
346
+ table.add_column("from", overflow="fold")
347
+ payload_rows = [dict(item) for item in rows if isinstance(item, dict)]
348
+ if payload_rows:
349
+ for idx, item in enumerate(payload_rows, 1):
350
+ from_refs = ", ".join(render_read_binding(read) for read in (item.get("reads") or [])) or "—"
351
+ table.add_row(str(idx), str(item.get("id") or "—"), str(item.get("summary") or "—"), from_refs)
352
+ else:
353
+ empty = "No plot outputs selected." if title == "Plot outputs" else "No exports selected."
354
+ table.add_row("—", "—", empty, "—")
355
+ return Panel(table, border_style="accent", box=box.ROUNDED)
356
+
357
+
358
+ def _table(title: str) -> Table:
359
+ return Table(
360
+ title=title,
361
+ title_justify="left",
362
+ title_style="bold cyan",
363
+ header_style="bold",
364
+ box=box.ROUNDED,
365
+ expand=True,
366
+ show_lines=False,
367
+ )
@@ -0,0 +1,166 @@
1
+ from __future__ import annotations
2
+
3
+ from collections import Counter
4
+ from copy import deepcopy
5
+ from pathlib import Path
6
+
7
+ from reader_workbench.errors import RegistryError
8
+ from reader_workbench.workbench.records import (
9
+ DataFrameArtifactRecord,
10
+ FileBundleRecord,
11
+ record_revision_digest,
12
+ record_to_dict,
13
+ )
14
+
15
+ from .common import format_relative_path
16
+
17
+
18
+ def _file_paths_detail_text(paths: list[str]) -> str:
19
+ if not paths:
20
+ return "—"
21
+ if len(paths) == 1:
22
+ return paths[0]
23
+ parents = {str(Path(path).parent) for path in paths}
24
+ if len(parents) == 1:
25
+ return f"{len(paths)} files • {next(iter(parents))}"
26
+ return f"{len(paths)} files • {len(parents)} directories • first: {paths[0]}"
27
+
28
+
29
+ def record_detail_text(record, *, base: Path) -> str:
30
+ if isinstance(record, DataFrameArtifactRecord):
31
+ return f"{record.contract_id} • {format_relative_path(record.path, base=base)}"
32
+ return _file_paths_detail_text([format_relative_path(path, base=base) for path in record.files])
33
+
34
+
35
+ def record_payload_detail_text(record: dict[str, object]) -> str:
36
+ if record.get("kind") == "dataframe_artifact":
37
+ contract_id = str(record.get("contract_id") or "")
38
+ path = str(record.get("path") or "")
39
+ return " • ".join(value for value in (contract_id, path) if value) or "—"
40
+ files = record.get("files")
41
+ if not isinstance(files, list):
42
+ return "—"
43
+ return _file_paths_detail_text([str(path) for path in files if path])
44
+
45
+
46
+ def record_payload(
47
+ record,
48
+ *,
49
+ outputs_dir: Path,
50
+ runtime=None,
51
+ revision: int,
52
+ revision_count: int | None = None,
53
+ ) -> dict[str, object]:
54
+ payload = record_to_dict(record, outputs_dir=outputs_dir)
55
+ payload["revision"] = revision
56
+ payload["revision_digest"] = record_revision_digest(record, outputs_dir=outputs_dir)
57
+ payload["producer_label"] = f"{record.producer.kind}:{record.producer.id}"
58
+ if isinstance(record, FileBundleRecord) or runtime is not None:
59
+ payload["description"] = record_description(record, runtime=runtime)
60
+ if revision_count is not None:
61
+ payload["revision_count"] = revision_count
62
+ return payload
63
+
64
+
65
+ def record_description(record, *, runtime) -> str:
66
+ if isinstance(record, FileBundleRecord):
67
+ return record.description or "Description unavailable in this record."
68
+ plugin_id = record.producer.plugin
69
+ if not plugin_id:
70
+ return "Description unavailable because the record has no plugin id."
71
+ try:
72
+ return runtime.plugins.resolve_descriptor(plugin_id).summary
73
+ except RegistryError:
74
+ return f"Description unavailable because plugin {plugin_id!r} is not registered."
75
+
76
+
77
+ def record_entries_payload(
78
+ *,
79
+ store,
80
+ outputs_dir: Path,
81
+ runtime=None,
82
+ include_history: bool = False,
83
+ ) -> list[dict[str, object]]:
84
+ snapshot = store.catalog_snapshot()
85
+ latest_records = snapshot.latest_records
86
+ if not include_history:
87
+ return [
88
+ record_payload(
89
+ record,
90
+ outputs_dir=outputs_dir,
91
+ runtime=runtime,
92
+ revision=snapshot.revision_counts[record.record_id],
93
+ )
94
+ for record in latest_records
95
+ ]
96
+ revision_counts = snapshot.revision_counts
97
+ return [
98
+ record_payload(
99
+ record,
100
+ outputs_dir=outputs_dir,
101
+ runtime=runtime,
102
+ revision=revision_counts[record.record_id],
103
+ revision_count=revision_counts[record.record_id],
104
+ )
105
+ for record in latest_records
106
+ ]
107
+
108
+
109
+ def record_summary_payload(
110
+ *,
111
+ latest_records,
112
+ revision_counts: dict[str, int] | None = None,
113
+ ) -> dict[str, object]:
114
+ kind_counts = Counter(record.kind for record in latest_records)
115
+ producer_counts = Counter(f"{record.producer.kind}:{record.producer.id}" for record in latest_records)
116
+ return {
117
+ "records": len(latest_records),
118
+ "history": {
119
+ "included": revision_counts is not None,
120
+ "revisions": sum(revision_counts.values()) if revision_counts is not None else None,
121
+ },
122
+ "by_kind": dict(sorted(kind_counts.items())),
123
+ "by_producer": dict(sorted(producer_counts.items())),
124
+ }
125
+
126
+
127
+ def record_catalog_payload(
128
+ *,
129
+ experiment: dict[str, object],
130
+ store,
131
+ outputs_dir: Path,
132
+ runtime=None,
133
+ include_history: bool = False,
134
+ current_config_digest: str | None = None,
135
+ declared_record_ids: frozenset[str] = frozenset(),
136
+ ) -> dict[str, object]:
137
+ snapshot = store.catalog_snapshot(
138
+ current_config_digest=current_config_digest if not include_history else None,
139
+ current_record_ids=(declared_record_ids or None) if not include_history else None,
140
+ )
141
+ latest_records = snapshot.latest_records
142
+ revision_counts = snapshot.revision_counts if include_history else None
143
+ return {
144
+ "experiment": deepcopy(experiment),
145
+ "catalog": {
146
+ "path": str(store.records_path),
147
+ "outputs_root": str(outputs_dir),
148
+ "schema_version": snapshot.schema_version,
149
+ "provenance_epoch_id": snapshot.provenance_epoch_id,
150
+ "active_invocation_ledger": str(snapshot.active_invocation_ledger),
151
+ },
152
+ "selection": {
153
+ "include_history": include_history,
154
+ },
155
+ "summary": record_summary_payload(latest_records=latest_records, revision_counts=revision_counts),
156
+ "records": [
157
+ record_payload(
158
+ record,
159
+ outputs_dir=outputs_dir,
160
+ runtime=runtime,
161
+ revision=snapshot.revision_counts[record.record_id],
162
+ revision_count=(revision_counts or {}).get(record.record_id),
163
+ )
164
+ for record in latest_records
165
+ ],
166
+ }