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,150 @@
1
+ from __future__ import annotations
2
+
3
+ from rich import box
4
+ from rich.panel import Panel
5
+
6
+ from reader_workbench.errors import ReaderError
7
+ from reader_workbench.workbench.commands import reader_command
8
+
9
+ from . import shared
10
+ from ._lazy import load as _load
11
+ from .helpers import load_job_models
12
+ from .shared import emit_json, normalize_output_format, table
13
+
14
+
15
+ def render_records(
16
+ *,
17
+ job_path,
18
+ all_revisions: bool,
19
+ format: str,
20
+ limit: int | None = None,
21
+ continuation: str | None = None,
22
+ ) -> None:
23
+ _, decl = load_job_models(job_path)
24
+ outputs_dir = decl.experiment_semantics.layout.outputs_dir
25
+ runtime = _load("reader_workbench.runtime").builtin_runtime()
26
+ store = runtime.record_store(
27
+ outputs_dir,
28
+ plots_subdir=decl.experiment_semantics.layout.plots_subdir,
29
+ exports_subdir=decl.experiment_semantics.layout.exports_subdir,
30
+ create=False,
31
+ )
32
+ if not store.catalog_exists():
33
+ raise ReaderError(
34
+ f"No outputs/manifests/records.json found. Run '{reader_command('run', job_path)}' first to produce records."
35
+ )
36
+ current_record_ids = frozenset()
37
+ if not all_revisions:
38
+ workbench = _load("reader_workbench.workbench.graph").resolve_workbench(decl)
39
+ current_record_ids = _load("reader_workbench.workbench.inspection.runtime").workbench_record_ids(
40
+ workbench, runtime=runtime
41
+ )
42
+
43
+ fmt = normalize_output_format(format)
44
+ limit, continuation = shared.normalize_paging_options(limit, continuation)
45
+ shared.require_json_paging(format=fmt, limit=limit, continuation=continuation)
46
+ if fmt == "json":
47
+ payload = _load("reader_workbench.workbench.inspection.results").record_catalog_payload(
48
+ experiment=_load("reader_workbench.workbench.inspection.experiments").experiment_identity_payload(
49
+ job_path=job_path, decl=decl
50
+ ),
51
+ store=store,
52
+ outputs_dir=outputs_dir,
53
+ runtime=runtime,
54
+ include_history=all_revisions,
55
+ current_config_digest=decl.config_digest,
56
+ declared_record_ids=current_record_ids,
57
+ )
58
+ page_selection = {
59
+ "config": str(job_path),
60
+ "config_digest": decl.config_digest,
61
+ "include_history": all_revisions,
62
+ "provenance_epoch_id": payload["catalog"]["provenance_epoch_id"],
63
+ "record_revisions": [
64
+ {
65
+ "record_id": item["record_id"],
66
+ "revision_digest": item["revision_digest"],
67
+ "revision_count": item.get("revision_count"),
68
+ }
69
+ for item in payload["records"]
70
+ ],
71
+ }
72
+ page = shared.page_json_collection(
73
+ payload["records"],
74
+ key=lambda item: str(item["record_id"]),
75
+ surface="records",
76
+ selection=page_selection,
77
+ limit=limit,
78
+ continuation=continuation,
79
+ )
80
+ payload["records"] = list(page.items)
81
+ emit_json(payload, truncated=page.truncated, continuation=page.continuation)
82
+ return
83
+
84
+ snapshot = store.catalog_snapshot(
85
+ current_config_digest=decl.config_digest if not all_revisions else None,
86
+ current_record_ids=(current_record_ids or None) if not all_revisions else None,
87
+ )
88
+ latest_records = snapshot.latest_records
89
+ if all_revisions:
90
+ if not latest_records:
91
+ shared.console.print(
92
+ Panel.fit(
93
+ (
94
+ "No record history listed in outputs/manifests/records.json. "
95
+ f"Run '{reader_command('run', job_path)}' first."
96
+ ),
97
+ border_style="warn",
98
+ box=box.ROUNDED,
99
+ )
100
+ )
101
+ return
102
+ revision_counts = snapshot.revision_counts
103
+ listing = table("Records • history")
104
+ listing.add_column("Record")
105
+ listing.add_column("Kind", style="accent")
106
+ listing.add_column("Producer")
107
+ listing.add_column("Description")
108
+ listing.add_column("Revisions", justify="right")
109
+ for record in latest_records:
110
+ listing.add_row(
111
+ record.record_id,
112
+ record.kind,
113
+ f"{record.producer.kind}:{record.producer.id}",
114
+ _load("reader_workbench.workbench.inspection.results").record_description(record, runtime=runtime),
115
+ str(revision_counts[record.record_id]),
116
+ )
117
+ shared.console.print(Panel(listing, border_style="accent", box=box.ROUNDED))
118
+ return
119
+
120
+ if not latest_records:
121
+ shared.console.print(
122
+ Panel.fit(
123
+ (
124
+ "No records listed in outputs/manifests/records.json. "
125
+ f"Run '{reader_command('run', job_path)}' first."
126
+ ),
127
+ border_style="warn",
128
+ box=box.ROUNDED,
129
+ )
130
+ )
131
+ return
132
+ listing = table("Records • latest")
133
+ listing.add_column("Record")
134
+ listing.add_column("Kind", style="accent")
135
+ listing.add_column("Producer")
136
+ listing.add_column("Description")
137
+ listing.add_column("Details", style="path")
138
+ for record in latest_records:
139
+ detail = _load("reader_workbench.workbench.inspection.results").record_detail_text(
140
+ record,
141
+ base=decl.experiment.root,
142
+ )
143
+ listing.add_row(
144
+ record.record_id,
145
+ record.kind,
146
+ f"{record.producer.kind}:{record.producer.id}",
147
+ _load("reader_workbench.workbench.inspection.results").record_description(record, runtime=runtime),
148
+ detail,
149
+ )
150
+ shared.console.print(Panel(listing, border_style="accent", box=box.ROUNDED))
@@ -0,0 +1,443 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Callable
4
+ from pathlib import Path
5
+
6
+ import typer
7
+ from rich import box
8
+ from rich.panel import Panel
9
+
10
+ from reader_workbench.workbench.commands import reader_command
11
+
12
+ from . import shared
13
+ from ._lazy import load as _load
14
+ from .helpers import bind_decl_protocol, format_job_arg, load_job_models
15
+ from .shared import emit_json, normalize_output_format, table
16
+
17
+
18
+ def spec_overrides():
19
+ return _load("reader_workbench.workbench.spec_overrides")
20
+
21
+
22
+ def validate_list_mode_flags(
23
+ *,
24
+ list_only: bool,
25
+ dry_run: bool,
26
+ inputs: list[str] | None,
27
+ sets: list[str] | None,
28
+ ) -> None:
29
+ if not list_only:
30
+ return
31
+ if dry_run:
32
+ raise typer.BadParameter("--dry-run cannot be combined with --list")
33
+ if inputs:
34
+ raise typer.BadParameter("--input cannot be combined with --list")
35
+ if sets:
36
+ raise typer.BadParameter("--set cannot be combined with --list")
37
+
38
+
39
+ def apply_surface_overrides(
40
+ selected,
41
+ *,
42
+ inputs: list[str] | None,
43
+ sets: list[str] | None,
44
+ experiment_root: Path,
45
+ resources,
46
+ ):
47
+ overrides = spec_overrides()
48
+ input_overrides = overrides.parse_input_overrides(inputs or [], root=experiment_root, resources=resources)
49
+ set_overrides = overrides.parse_set_overrides(sets or [])
50
+ selected = overrides.apply_step_overrides(
51
+ selected,
52
+ input_overrides=input_overrides,
53
+ set_overrides=set_overrides,
54
+ root=experiment_root,
55
+ resources=resources,
56
+ )
57
+ return overrides, selected
58
+
59
+
60
+ def _raise_dependency_preflight_errors(*, selected, runtime, bound_protocol, exp_root: Path, label: str) -> None:
61
+ errors: list[str] = []
62
+ for step in selected:
63
+ plugin_cls = runtime.plugins.resolve(step.plugin)
64
+ cfg = plugin_cls.ConfigModel.model_validate(
65
+ bound_protocol.effective_plugin_config(plugin_id=step.plugin, step_with=(step.with_ or {}))
66
+ )
67
+ for issue in plugin_cls.preflight_readiness(exp_dir=exp_root, cfg=cfg, reads=(step.reads or {})):
68
+ if issue.kind == "dependency":
69
+ errors.append(f"{label}:{step.id} • {issue.message}")
70
+ if errors:
71
+ raise typer.BadParameter("\n".join(errors))
72
+
73
+
74
+ def _validate_dry_run_selection(*, decl, selected, runtime, label: str) -> None:
75
+ validation = _load("reader_workbench.workbench.engine.validation")
76
+ validation.validation_summary(
77
+ decl,
78
+ check_files=False,
79
+ exp_root=decl.experiment.root,
80
+ runtime=runtime,
81
+ plot_specs_override=(selected if label == "plot" else []),
82
+ export_specs_override=(selected if label == "export" else []),
83
+ )
84
+
85
+
86
+ def validate_plot_job_for_execution(
87
+ job_path: Path,
88
+ *,
89
+ only: list[str] | None,
90
+ exclude: list[str] | None,
91
+ dry_run: bool,
92
+ inputs: list[str] | None,
93
+ sets: list[str] | None,
94
+ ensure_active_lifecycle_fn: Callable,
95
+ require_dataframe_records_fn: Callable,
96
+ ) -> None:
97
+ runtime = _load("reader_workbench.runtime").builtin_runtime()
98
+ _, decl = load_job_models(job_path, runtime=runtime)
99
+ if not dry_run:
100
+ ensure_active_lifecycle_fn(decl, job_path, command_name="plot")
101
+ workbench = _load("reader_workbench.workbench.graph").resolve_workbench(decl)
102
+ plot_specs = list(workbench.plots)
103
+ if not plot_specs:
104
+ raise typer.BadParameter("No plots configured in this experiment. Add plots to the config.")
105
+ selected = spec_overrides().select_surface_specs(
106
+ plot_specs, only=only or [], exclude=exclude or [], kind="plot spec"
107
+ )
108
+ if not selected:
109
+ raise typer.BadParameter("No plots selected. Adjust --only/--exclude or use --list to inspect valid ids.")
110
+ _, selected = apply_surface_overrides(
111
+ selected,
112
+ inputs=inputs,
113
+ sets=sets,
114
+ experiment_root=decl.experiment.root,
115
+ resources=decl.experiment_semantics.resources,
116
+ )
117
+ if dry_run:
118
+ _validate_dry_run_selection(decl=decl, selected=selected, runtime=runtime, label="plot")
119
+ _raise_dependency_preflight_errors(
120
+ selected=selected,
121
+ runtime=runtime,
122
+ bound_protocol=bind_decl_protocol(decl=decl, runtime=runtime),
123
+ exp_root=decl.experiment.root,
124
+ label="plot",
125
+ )
126
+ if not dry_run:
127
+ require_dataframe_records_fn(decl, job_path, runtime=runtime)
128
+
129
+
130
+ def render_surface_specs_table(
131
+ *,
132
+ title_text: str,
133
+ selected,
134
+ runtime,
135
+ record_producers,
136
+ summaries: dict[str, str],
137
+ ) -> None:
138
+ inspection_runtime = _load("reader_workbench.workbench.inspection.runtime")
139
+ listing = table(title_text)
140
+ listing.add_column("#", justify="right", style="muted")
141
+ listing.add_column("id", style="accent", overflow="fold")
142
+ listing.add_column("summary", overflow="fold")
143
+ listing.add_column("from", overflow="fold")
144
+ listing.add_column("plugin", overflow="fold")
145
+ for index, spec in enumerate(selected, 1):
146
+ spec_payload = inspection_runtime.spec_step_payload(
147
+ spec, summary=summaries.get(spec.id, "—"), runtime=runtime, record_producers=record_producers
148
+ )
149
+ from_refs = ", ".join(inspection_runtime.render_read_binding(item) for item in spec_payload["reads"]) or "—"
150
+ listing.add_row(str(index), spec.id, summaries.get(spec.id, "—"), from_refs, spec.plugin)
151
+ shared.console.print(
152
+ Panel(listing, border_style="accent", box=box.ROUNDED, subtitle=f"[muted]{len(selected)} total[/muted]")
153
+ )
154
+
155
+
156
+ def surface_next_steps(
157
+ *,
158
+ job_hint: str | None,
159
+ output_dir: Path,
160
+ include_plot: bool,
161
+ include_export: bool,
162
+ ) -> None:
163
+ def _cmd(base: str, tail: str = "") -> str:
164
+ return reader_command(base, job_hint, tail)
165
+
166
+ lines = [f"Files saved in [path]{output_dir}[/path]", "", "Next steps:"]
167
+ if include_plot:
168
+ lines.append(f" {_cmd('plot')}")
169
+ if include_export:
170
+ lines.append(f" {_cmd('export')}")
171
+ lines.append(f" {_cmd('notebook')}")
172
+ shared.console.print(Panel.fit("\n".join(lines), border_style="green", box=box.ROUNDED))
173
+
174
+
175
+ def run_plot_job(
176
+ job_path: Path,
177
+ *,
178
+ job_hint: str | None,
179
+ only: list[str] | None,
180
+ exclude: list[str] | None,
181
+ list_only: bool,
182
+ format: str,
183
+ dry_run: bool,
184
+ log_level: str,
185
+ inputs: list[str] | None,
186
+ sets: list[str] | None,
187
+ ensure_active_lifecycle_fn: Callable,
188
+ require_dataframe_records_fn: Callable,
189
+ ) -> None:
190
+ _, decl = load_job_models(job_path)
191
+ if not list_only and not dry_run:
192
+ ensure_active_lifecycle_fn(decl, job_path, command_name="plot")
193
+ runtime = _load("reader_workbench.runtime").builtin_runtime()
194
+ workbench = _load("reader_workbench.workbench.graph").resolve_workbench(decl)
195
+ bound_protocol = bind_decl_protocol(decl=decl, runtime=runtime)
196
+ inspection_catalogs = _load("reader_workbench.workbench.inspection.catalogs")
197
+ inspection_runtime = _load("reader_workbench.workbench.inspection.runtime")
198
+ fmt = normalize_output_format(format)
199
+ if not list_only:
200
+ if fmt == "json" and not dry_run:
201
+ raise typer.BadParameter("--format json is only supported with --list or --dry-run")
202
+ if not dry_run:
203
+ require_dataframe_records_fn(decl, job_path, runtime=runtime)
204
+ plot_specs = list(workbench.plots)
205
+ record_producers = inspection_runtime.record_producer_map(workbench.plugin_steps(), runtime=runtime)
206
+ if not plot_specs:
207
+ if list_only:
208
+ if fmt == "json":
209
+ emit_json(
210
+ inspection_catalogs.workbench_surface_specs_payload(
211
+ job_path=job_path,
212
+ decl=decl,
213
+ runtime=runtime,
214
+ bound_protocol=bound_protocol,
215
+ selected=[],
216
+ kind="plot",
217
+ only=only or [],
218
+ exclude=exclude or [],
219
+ )
220
+ )
221
+ return
222
+ shared.console.print(
223
+ Panel.fit("No plots configured in this experiment.", border_style="warn", box=box.ROUNDED)
224
+ )
225
+ return
226
+ raise typer.BadParameter("No plots configured in this experiment. Add plots to the config.")
227
+ selected = spec_overrides().select_surface_specs(
228
+ plot_specs, only=only or [], exclude=exclude or [], kind="plot spec"
229
+ )
230
+ if list_only:
231
+ if fmt == "json":
232
+ emit_json(
233
+ inspection_catalogs.workbench_surface_specs_payload(
234
+ job_path=job_path,
235
+ decl=decl,
236
+ runtime=runtime,
237
+ bound_protocol=bound_protocol,
238
+ selected=selected,
239
+ kind="plot",
240
+ only=only or [],
241
+ exclude=exclude or [],
242
+ )
243
+ )
244
+ return
245
+ render_surface_specs_table(
246
+ title_text="Plots",
247
+ selected=selected,
248
+ runtime=runtime,
249
+ record_producers=record_producers,
250
+ summaries=inspection_runtime.plot_output_summaries(bound_protocol),
251
+ )
252
+ return
253
+ if not selected:
254
+ raise typer.BadParameter("No plots selected. Adjust --only/--exclude or use --list to inspect valid ids.")
255
+ experiment_root = decl.experiment.root
256
+ resources = decl.experiment_semantics.resources
257
+ _, selected = apply_surface_overrides(
258
+ selected,
259
+ inputs=inputs,
260
+ sets=sets,
261
+ experiment_root=experiment_root,
262
+ resources=resources,
263
+ )
264
+ if dry_run:
265
+ _load("reader_workbench.workbench.engine").normalize_log_level(log_level)
266
+ _validate_dry_run_selection(decl=decl, selected=selected, runtime=runtime, label="plot")
267
+ _raise_dependency_preflight_errors(
268
+ selected=selected,
269
+ runtime=runtime,
270
+ bound_protocol=bound_protocol,
271
+ exp_root=experiment_root,
272
+ label="plot",
273
+ )
274
+ if fmt == "json":
275
+ payload = inspection_catalogs.workbench_surface_specs_payload(
276
+ job_path=job_path,
277
+ decl=decl,
278
+ runtime=runtime,
279
+ bound_protocol=bound_protocol,
280
+ selected=selected,
281
+ kind="plot",
282
+ only=only or [],
283
+ exclude=exclude or [],
284
+ )
285
+ payload["dry_run"] = True
286
+ emit_json(payload)
287
+ return
288
+ _load("reader_workbench.workbench.engine").run_spec(
289
+ decl,
290
+ dry_run=dry_run,
291
+ log_level=log_level,
292
+ console=shared.console,
293
+ include_pipeline=False,
294
+ include_plots=True,
295
+ include_exports=False,
296
+ plot_specs=selected,
297
+ runtime=runtime,
298
+ )
299
+ if not dry_run:
300
+ outputs_dir = decl.experiment_semantics.layout.outputs_dir
301
+ plots_cfg = decl.experiment_semantics.layout.plots_subdir
302
+ plots_dir = outputs_dir if plots_cfg in ("", ".", "./") else outputs_dir / str(plots_cfg)
303
+ surface_next_steps(
304
+ job_hint=format_job_arg(job_hint),
305
+ output_dir=plots_dir,
306
+ include_plot=False,
307
+ include_export=bool(workbench.exports),
308
+ )
309
+
310
+
311
+ def run_export_job(
312
+ job_path: Path,
313
+ *,
314
+ job_hint: str | None,
315
+ only: list[str] | None,
316
+ exclude: list[str] | None,
317
+ list_only: bool,
318
+ format: str,
319
+ dry_run: bool,
320
+ log_level: str,
321
+ inputs: list[str] | None,
322
+ sets: list[str] | None,
323
+ ensure_active_lifecycle_fn: Callable,
324
+ require_dataframe_records_fn: Callable,
325
+ ) -> None:
326
+ _, decl = load_job_models(job_path)
327
+ if not list_only and not dry_run:
328
+ ensure_active_lifecycle_fn(decl, job_path, command_name="export")
329
+ fmt = normalize_output_format(format)
330
+ workbench = _load("reader_workbench.workbench.graph").resolve_workbench(decl)
331
+ runtime = _load("reader_workbench.runtime").builtin_runtime()
332
+ inspection_catalogs = _load("reader_workbench.workbench.inspection.catalogs")
333
+ inspection_runtime = _load("reader_workbench.workbench.inspection.runtime")
334
+ record_producers = inspection_runtime.record_producer_map(workbench.plugin_steps(), runtime=runtime)
335
+ bound_protocol = bind_decl_protocol(decl=decl, runtime=runtime)
336
+ export_specs = list(workbench.exports)
337
+ if not export_specs:
338
+ if list_only:
339
+ if fmt == "json":
340
+ emit_json(
341
+ inspection_catalogs.workbench_surface_specs_payload(
342
+ job_path=job_path,
343
+ decl=decl,
344
+ runtime=runtime,
345
+ bound_protocol=bound_protocol,
346
+ selected=[],
347
+ kind="export",
348
+ only=only or [],
349
+ exclude=exclude or [],
350
+ )
351
+ )
352
+ return
353
+ shared.console.print(
354
+ Panel.fit("No exports configured in this experiment.", border_style="warn", box=box.ROUNDED)
355
+ )
356
+ return
357
+ raise typer.BadParameter("No exports configured in this experiment. Add exports to the config.")
358
+ selected = spec_overrides().select_surface_specs(
359
+ export_specs, only=only or [], exclude=exclude or [], kind="export spec"
360
+ )
361
+ if list_only:
362
+ if fmt == "json":
363
+ emit_json(
364
+ inspection_catalogs.workbench_surface_specs_payload(
365
+ job_path=job_path,
366
+ decl=decl,
367
+ runtime=runtime,
368
+ bound_protocol=bound_protocol,
369
+ selected=selected,
370
+ kind="export",
371
+ only=only or [],
372
+ exclude=exclude or [],
373
+ )
374
+ )
375
+ return
376
+ render_surface_specs_table(
377
+ title_text="Exports",
378
+ selected=selected,
379
+ runtime=runtime,
380
+ record_producers=record_producers,
381
+ summaries=inspection_runtime.export_output_summaries(bound_protocol),
382
+ )
383
+ return
384
+ if not selected:
385
+ raise typer.BadParameter("No exports selected. Adjust --only/--exclude or use --list to inspect valid ids.")
386
+ if fmt == "json" and not dry_run:
387
+ raise typer.BadParameter("--format json is only supported with --list or --dry-run")
388
+ if not dry_run:
389
+ require_dataframe_records_fn(decl, job_path, runtime=runtime)
390
+ experiment_root = decl.experiment.root
391
+ resources = decl.experiment_semantics.resources
392
+ _, selected = apply_surface_overrides(
393
+ selected,
394
+ inputs=inputs,
395
+ sets=sets,
396
+ experiment_root=experiment_root,
397
+ resources=resources,
398
+ )
399
+ if dry_run:
400
+ _load("reader_workbench.workbench.engine").normalize_log_level(log_level)
401
+ _validate_dry_run_selection(decl=decl, selected=selected, runtime=runtime, label="export")
402
+ _raise_dependency_preflight_errors(
403
+ selected=selected,
404
+ runtime=runtime,
405
+ bound_protocol=bound_protocol,
406
+ exp_root=experiment_root,
407
+ label="export",
408
+ )
409
+ if fmt == "json":
410
+ payload = inspection_catalogs.workbench_surface_specs_payload(
411
+ job_path=job_path,
412
+ decl=decl,
413
+ runtime=runtime,
414
+ bound_protocol=bound_protocol,
415
+ selected=selected,
416
+ kind="export",
417
+ only=only or [],
418
+ exclude=exclude or [],
419
+ )
420
+ payload["dry_run"] = True
421
+ emit_json(payload)
422
+ return
423
+ _load("reader_workbench.workbench.engine").run_spec(
424
+ decl,
425
+ dry_run=dry_run,
426
+ log_level=log_level,
427
+ console=shared.console,
428
+ include_pipeline=False,
429
+ include_plots=False,
430
+ include_exports=True,
431
+ export_specs=selected,
432
+ runtime=runtime,
433
+ )
434
+ if not dry_run:
435
+ outputs_dir = decl.experiment_semantics.layout.outputs_dir
436
+ exports_cfg = decl.experiment_semantics.layout.exports_subdir
437
+ exports_dir = outputs_dir if exports_cfg in ("", ".", "./") else outputs_dir / str(exports_cfg)
438
+ surface_next_steps(
439
+ job_hint=format_job_arg(job_hint),
440
+ output_dir=exports_dir,
441
+ include_plot=bool(workbench.plots),
442
+ include_export=False,
443
+ )
@@ -0,0 +1,95 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from pathlib import Path
5
+
6
+ import typer
7
+
8
+ from . import shared
9
+ from ._lazy import load as _load
10
+ from .shared import app, emit_json, emit_json_error, normalize_output_format
11
+
12
+ audit_app = typer.Typer(
13
+ add_completion=False,
14
+ no_args_is_help=True,
15
+ help="Pressure-test experiment workbenches without mutating their outputs.",
16
+ )
17
+ EXPERIMENTS_ROOT_OPTION = typer.Option(Path("experiments"), "--root", metavar="DIR", help="Experiments root.")
18
+ EXPERIMENT_YEARS_OPTION = typer.Option(
19
+ None,
20
+ "--years",
21
+ metavar="YYYY",
22
+ help="Year directory to include (repeatable; defaults to every numeric year).",
23
+ )
24
+ AUDIT_REPORT_PATH_OPTION = typer.Option(
25
+ None,
26
+ "--report-path",
27
+ metavar="FILE",
28
+ help="Optional path for the raw JSON audit report.",
29
+ )
30
+
31
+
32
+ @audit_app.command(
33
+ "experiments", help="Run active experiments from isolated temporary copies and verify their outputs."
34
+ )
35
+ def experiments(
36
+ root: Path = EXPERIMENTS_ROOT_OPTION,
37
+ years: list[str] | None = EXPERIMENT_YEARS_OPTION,
38
+ include_non_active: bool = typer.Option(
39
+ False,
40
+ "--include-non-active",
41
+ help="Run draft and other non-active experiments instead of reporting them as skipped.",
42
+ ),
43
+ fail_fast: bool = typer.Option(False, "--fail-fast", help="Stop after the first failed experiment."),
44
+ report_path: Path | None = AUDIT_REPORT_PATH_OPTION,
45
+ format: str = typer.Option(
46
+ "table",
47
+ "--format",
48
+ metavar="FMT",
49
+ help="Output format: table | json (default: table).",
50
+ ),
51
+ ) -> None:
52
+ fmt = normalize_output_format(format)
53
+ audit_module = _load("reader_workbench.workbench.audit.experiments")
54
+ try:
55
+ payload = audit_module.audit_experiments(
56
+ root,
57
+ years=None if years is None else tuple(years),
58
+ include_non_active=include_non_active,
59
+ fail_fast=fail_fast,
60
+ )
61
+ except ValueError as exc:
62
+ raise typer.BadParameter(str(exc), param_hint="--root/--years") from exc
63
+
64
+ if report_path is not None:
65
+ report_path.parent.mkdir(parents=True, exist_ok=True)
66
+ report_path.write_text(json.dumps(payload, indent=2), encoding="utf-8")
67
+
68
+ failed = int(payload["summary"]["failed"])
69
+ if fmt == "json" and failed:
70
+ first_failed = next(item for item in payload["results"] if item["status"] == "failed")
71
+ detail = str(first_failed["detail"] or "no failure detail")
72
+ bounded_detail = detail if len(detail) <= 400 else f"{detail[:399]}…"
73
+ emit_json_error(
74
+ code="experiment_audit_failed",
75
+ field="experiments",
76
+ reason=(
77
+ f"{failed} experiment audit(s) failed. First: {first_failed['name']} "
78
+ f"during {first_failed['phase']}: {bounded_detail}"
79
+ ),
80
+ remediation=(
81
+ "Inspect the named experiment, correct its configuration, validation, execution, or verification failure, "
82
+ "then rerun the audit. Use --report-path when the complete machine report is required."
83
+ ),
84
+ )
85
+ elif fmt == "json":
86
+ emit_json(payload)
87
+ else:
88
+ results = [audit_module.AuditResult(**item) for item in payload["results"]]
89
+ shared.console.print(audit_module.render_text(results))
90
+
91
+ if failed:
92
+ raise typer.Exit(code=1)
93
+
94
+
95
+ app.add_typer(audit_app, name="audit")