bobframes 0.1.0__tar.gz → 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (218) hide show
  1. {bobframes-0.1.0 → bobframes-0.2.0}/CHANGELOG.md +32 -0
  2. {bobframes-0.1.0 → bobframes-0.2.0}/PKG-INFO +26 -2
  3. {bobframes-0.1.0 → bobframes-0.2.0}/README.md +24 -1
  4. bobframes-0.2.0/bobframes/_default_config.toml +61 -0
  5. bobframes-0.2.0/bobframes/_version.py +1 -0
  6. {bobframes-0.1.0 → bobframes-0.2.0}/bobframes/catalog.py +8 -15
  7. {bobframes-0.1.0 → bobframes-0.2.0}/bobframes/cli.py +109 -13
  8. bobframes-0.2.0/bobframes/config.py +462 -0
  9. {bobframes-0.1.0 → bobframes-0.2.0}/bobframes/derive_post_merge.py +22 -44
  10. bobframes-0.2.0/bobframes/derives/classifier.py +142 -0
  11. bobframes-0.2.0/bobframes/derives/draw_classifier.toml +78 -0
  12. {bobframes-0.1.0 → bobframes-0.2.0}/bobframes/derives/pass_class_breakdown.py +3 -1
  13. bobframes-0.2.0/bobframes/derives/presets/custom-template.toml +52 -0
  14. bobframes-0.2.0/bobframes/derives/presets/godot.toml +60 -0
  15. bobframes-0.2.0/bobframes/derives/presets/unity.toml +61 -0
  16. {bobframes-0.1.0 → bobframes-0.2.0}/bobframes/discovery.py +18 -2
  17. bobframes-0.2.0/bobframes/errors.py +65 -0
  18. {bobframes-0.1.0 → bobframes-0.2.0}/bobframes/global_entities.py +17 -10
  19. {bobframes-0.1.0 → bobframes-0.2.0}/bobframes/html/template.py +207 -487
  20. {bobframes-0.1.0 → bobframes-0.2.0}/bobframes/lint.py +8 -20
  21. bobframes-0.2.0/bobframes/lint_banlist.toml +70 -0
  22. {bobframes-0.1.0 → bobframes-0.2.0}/bobframes/manifest.py +25 -4
  23. {bobframes-0.1.0 → bobframes-0.2.0}/bobframes/parquetize.py +107 -64
  24. {bobframes-0.1.0 → bobframes-0.2.0}/bobframes/parsers/parse_init_state.py +13 -3
  25. {bobframes-0.1.0 → bobframes-0.2.0}/bobframes/paths.py +43 -9
  26. {bobframes-0.1.0 → bobframes-0.2.0}/bobframes/qrd_harness.py +3 -13
  27. {bobframes-0.1.0 → bobframes-0.2.0}/bobframes/rdcmd.py +4 -14
  28. {bobframes-0.1.0 → bobframes-0.2.0}/bobframes/replay/replay_main.py +21 -41
  29. bobframes-0.2.0/bobframes/reports/__init__.py +40 -0
  30. bobframes-0.2.0/bobframes/reports/_tokens.py +69 -0
  31. {bobframes-0.1.0 → bobframes-0.2.0}/bobframes/reports/ab.py +10 -18
  32. bobframes-0.2.0/bobframes/reports/assets/Inter-OFL.txt +92 -0
  33. bobframes-0.2.0/bobframes/reports/assets/README.md +32 -0
  34. bobframes-0.2.0/bobframes/reports/assets/inter-subset.woff2 +0 -0
  35. {bobframes-0.1.0 → bobframes-0.2.0}/bobframes/reports/base.py +49 -8
  36. {bobframes-0.1.0 → bobframes-0.2.0}/bobframes/reports/cache.py +91 -5
  37. bobframes-0.2.0/bobframes/reports/charts.py +549 -0
  38. bobframes-0.2.0/bobframes/reports/chrome.py +2395 -0
  39. {bobframes-0.1.0 → bobframes-0.2.0}/bobframes/reports/cli.py +32 -13
  40. bobframes-0.2.0/bobframes/reports/dashboard.py +519 -0
  41. {bobframes-0.1.0 → bobframes-0.2.0}/bobframes/reports/delta.py +17 -5
  42. bobframes-0.2.0/bobframes/reports/design_tokens.toml +127 -0
  43. bobframes-0.2.0/bobframes/reports/discovery.py +231 -0
  44. bobframes-0.2.0/bobframes/reports/draws_by_class.py +202 -0
  45. {bobframes-0.1.0 → bobframes-0.2.0}/bobframes/reports/formatters.py +38 -15
  46. bobframes-0.2.0/bobframes/reports/instancing_opportunities.py +355 -0
  47. bobframes-0.2.0/bobframes/reports/orchestrator.py +79 -0
  48. {bobframes-0.1.0 → bobframes-0.2.0}/bobframes/reports/overdraw.py +136 -70
  49. {bobframes-0.1.0 → bobframes-0.2.0}/bobframes/reports/pass_gpu.py +95 -42
  50. bobframes-0.2.0/bobframes/reports/preview.py +154 -0
  51. bobframes-0.2.0/bobframes/reports/shader_hotlist.py +377 -0
  52. {bobframes-0.1.0 → bobframes-0.2.0}/bobframes/reports/trend_table.py +101 -53
  53. {bobframes-0.1.0 → bobframes-0.2.0}/bobframes/run.py +125 -50
  54. {bobframes-0.1.0 → bobframes-0.2.0}/bobframes/schemas.py +64 -33
  55. bobframes-0.2.0/bobframes/tests/_render_util.py +202 -0
  56. bobframes-0.2.0/bobframes/tests/data/golden/_pagedata/catalog.js +1 -0
  57. bobframes-0.2.0/bobframes/tests/data/golden/_reports/draws_by_class.html +844 -0
  58. bobframes-0.2.0/bobframes/tests/data/golden/_reports/drill/District 01/2026-05-28_r110600/_pagedata/buffers.js +1 -0
  59. bobframes-0.2.0/bobframes/tests/data/golden/_reports/drill/District 01/2026-05-28_r110600/_pagedata/clears.js +1 -0
  60. bobframes-0.2.0/bobframes/tests/data/golden/_reports/drill/District 01/2026-05-28_r110600/_pagedata/counters_per_event.js +1 -0
  61. bobframes-0.2.0/bobframes/tests/data/golden/_reports/drill/District 01/2026-05-28_r110600/_pagedata/descriptor_access.js +1 -0
  62. bobframes-0.2.0/bobframes/tests/data/golden/_reports/drill/District 01/2026-05-28_r110600/_pagedata/dispatches.js +1 -0
  63. bobframes-0.2.0/bobframes/tests/data/golden/_reports/drill/District 01/2026-05-28_r110600/_pagedata/draw_bindings.js +1 -0
  64. bobframes-0.2.0/bobframes/tests/data/golden/_reports/drill/District 01/2026-05-28_r110600/_pagedata/draws.js +1 -0
  65. bobframes-0.2.0/bobframes/tests/data/golden/_reports/drill/District 01/2026-05-28_r110600/_pagedata/events.js +1 -0
  66. bobframes-0.2.0/bobframes/tests/data/golden/_reports/drill/District 01/2026-05-28_r110600/_pagedata/fbos.js +1 -0
  67. bobframes-0.2.0/bobframes/tests/data/golden/_reports/drill/District 01/2026-05-28_r110600/_pagedata/frame_totals.js +1 -0
  68. bobframes-0.2.0/bobframes/tests/data/golden/_reports/drill/District 01/2026-05-28_r110600/_pagedata/ibo_samples.js +1 -0
  69. bobframes-0.2.0/bobframes/tests/data/golden/_reports/drill/District 01/2026-05-28_r110600/_pagedata/passes.js +1 -0
  70. bobframes-0.2.0/bobframes/tests/data/golden/_reports/drill/District 01/2026-05-28_r110600/_pagedata/pixel_history.js +1 -0
  71. bobframes-0.2.0/bobframes/tests/data/golden/_reports/drill/District 01/2026-05-28_r110600/_pagedata/post_vs_samples.js +1 -0
  72. bobframes-0.2.0/bobframes/tests/data/golden/_reports/drill/District 01/2026-05-28_r110600/_pagedata/program_transitions.js +1 -0
  73. bobframes-0.2.0/bobframes/tests/data/golden/_reports/drill/District 01/2026-05-28_r110600/_pagedata/programs.js +1 -0
  74. bobframes-0.2.0/bobframes/tests/data/golden/_reports/drill/District 01/2026-05-28_r110600/_pagedata/render_targets.js +1 -0
  75. bobframes-0.2.0/bobframes/tests/data/golden/_reports/drill/District 01/2026-05-28_r110600/_pagedata/resource_creation.js +1 -0
  76. bobframes-0.2.0/bobframes/tests/data/golden/_reports/drill/District 01/2026-05-28_r110600/_pagedata/rt_event_timeline.js +1 -0
  77. bobframes-0.2.0/bobframes/tests/data/golden/_reports/drill/District 01/2026-05-28_r110600/_pagedata/samplers.js +1 -0
  78. bobframes-0.2.0/bobframes/tests/data/golden/_reports/drill/District 01/2026-05-28_r110600/_pagedata/shaders.js +1 -0
  79. bobframes-0.2.0/bobframes/tests/data/golden/_reports/drill/District 01/2026-05-28_r110600/_pagedata/state_change_events.js +1 -0
  80. bobframes-0.2.0/bobframes/tests/data/golden/_reports/drill/District 01/2026-05-28_r110600/_pagedata/texture_samples.js +1 -0
  81. bobframes-0.2.0/bobframes/tests/data/golden/_reports/drill/District 01/2026-05-28_r110600/_pagedata/textures.js +1 -0
  82. bobframes-0.2.0/bobframes/tests/data/golden/_reports/drill/District 01/2026-05-28_r110600/_pagedata/vbo_samples.js +1 -0
  83. bobframes-0.2.0/bobframes/tests/data/golden/_reports/drill/District 01/2026-05-28_r110600/_pagedata/vertex_inputs.js +1 -0
  84. bobframes-0.2.0/bobframes/tests/data/golden/_reports/drill/District 01/2026-05-28_r110600/index.html +2064 -0
  85. bobframes-0.2.0/bobframes/tests/data/golden/_reports/index.html +787 -0
  86. bobframes-0.2.0/bobframes/tests/data/golden/_reports/instancing_opportunities.html +789 -0
  87. bobframes-0.2.0/bobframes/tests/data/golden/_reports/overdraw.html +798 -0
  88. bobframes-0.2.0/bobframes/tests/data/golden/_reports/pass_gpu.html +799 -0
  89. bobframes-0.2.0/bobframes/tests/data/golden/_reports/run/2026-05-27_r110565/draws_by_class.html +843 -0
  90. bobframes-0.2.0/bobframes/tests/data/golden/_reports/run/2026-05-27_r110565/index.html +788 -0
  91. bobframes-0.2.0/bobframes/tests/data/golden/_reports/run/2026-05-27_r110565/instancing_opportunities.html +787 -0
  92. bobframes-0.2.0/bobframes/tests/data/golden/_reports/run/2026-05-27_r110565/overdraw.html +797 -0
  93. bobframes-0.2.0/bobframes/tests/data/golden/_reports/run/2026-05-27_r110565/pass_gpu.html +796 -0
  94. bobframes-0.2.0/bobframes/tests/data/golden/_reports/run/2026-05-27_r110565/shader_hotlist.html +787 -0
  95. bobframes-0.2.0/bobframes/tests/data/golden/_reports/shader_hotlist.html +789 -0
  96. bobframes-0.2.0/bobframes/tests/data/golden/_reports/trend_table.html +920 -0
  97. bobframes-0.2.0/bobframes/tests/data/golden/index.html +1681 -0
  98. bobframes-0.2.0/bobframes/tests/data/golden_parquet/digests.json +4966 -0
  99. bobframes-0.2.0/bobframes/tests/data/golden_preview/_chrome_preview.html +804 -0
  100. {bobframes-0.1.0 → bobframes-0.2.0}/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/_manifest.json +11 -0
  101. {bobframes-0.1.0 → bobframes-0.2.0}/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/_manifest.json +11 -0
  102. bobframes-0.2.0/bobframes/tests/make_golden.py +44 -0
  103. bobframes-0.2.0/bobframes/tests/make_parquet_golden.py +37 -0
  104. bobframes-0.2.0/bobframes/tests/make_preview_golden.py +23 -0
  105. {bobframes-0.1.0 → bobframes-0.2.0}/bobframes/tests/make_synthetic.py +20 -3
  106. {bobframes-0.1.0 → bobframes-0.2.0}/bobframes/tests/smoke.py +4 -4
  107. bobframes-0.2.0/bobframes/tests/test_cache.py +68 -0
  108. bobframes-0.2.0/bobframes/tests/test_charts.py +147 -0
  109. bobframes-0.2.0/bobframes/tests/test_classifier.py +189 -0
  110. bobframes-0.2.0/bobframes/tests/test_config.py +293 -0
  111. bobframes-0.2.0/bobframes/tests/test_delta.py +33 -0
  112. bobframes-0.2.0/bobframes/tests/test_design_tokens.py +265 -0
  113. {bobframes-0.1.0 → bobframes-0.2.0}/bobframes/tests/test_discovery.py +2 -1
  114. bobframes-0.2.0/bobframes/tests/test_fonts.py +56 -0
  115. bobframes-0.2.0/bobframes/tests/test_hardening.py +278 -0
  116. bobframes-0.2.0/bobframes/tests/test_manifest_guard.py +68 -0
  117. {bobframes-0.1.0 → bobframes-0.2.0}/bobframes/tests/test_parity.py +12 -0
  118. bobframes-0.2.0/bobframes/tests/test_parquet_parity.py +36 -0
  119. bobframes-0.2.0/bobframes/tests/test_parquetize.py +107 -0
  120. bobframes-0.2.0/bobframes/tests/test_report_polish.py +102 -0
  121. bobframes-0.2.0/bobframes/tests/test_report_structure.py +521 -0
  122. bobframes-0.2.0/bobframes/tests/test_run_model.py +241 -0
  123. {bobframes-0.1.0 → bobframes-0.2.0}/bobframes/tests/test_schemas_unit.py +7 -5
  124. {bobframes-0.1.0 → bobframes-0.2.0}/pyproject.toml +3 -0
  125. bobframes-0.1.0/bobframes/_version.py +0 -1
  126. bobframes-0.1.0/bobframes/reports/_dashboard.py +0 -425
  127. bobframes-0.1.0/bobframes/reports/chrome.py +0 -1306
  128. bobframes-0.1.0/bobframes/reports/discovery.py +0 -118
  129. bobframes-0.1.0/bobframes/reports/draws_by_class.py +0 -165
  130. bobframes-0.1.0/bobframes/reports/instancing_opportunities.py +0 -276
  131. bobframes-0.1.0/bobframes/reports/orchestrator.py +0 -59
  132. bobframes-0.1.0/bobframes/reports/shader_hotlist.py +0 -240
  133. bobframes-0.1.0/bobframes/tests/__init__.py +0 -0
  134. bobframes-0.1.0/bobframes/tests/_render_util.py +0 -84
  135. bobframes-0.1.0/bobframes/tests/data/golden/_reports/draws_by_class.html +0 -323
  136. bobframes-0.1.0/bobframes/tests/data/golden/_reports/drill/District 01/2026-05-28_r110600/index.html +0 -1560
  137. bobframes-0.1.0/bobframes/tests/data/golden/_reports/index.html +0 -264
  138. bobframes-0.1.0/bobframes/tests/data/golden/_reports/instancing_opportunities.html +0 -266
  139. bobframes-0.1.0/bobframes/tests/data/golden/_reports/overdraw.html +0 -275
  140. bobframes-0.1.0/bobframes/tests/data/golden/_reports/pass_gpu.html +0 -277
  141. bobframes-0.1.0/bobframes/tests/data/golden/_reports/shader_hotlist.html +0 -265
  142. bobframes-0.1.0/bobframes/tests/data/golden/_reports/trend_table.html +0 -390
  143. bobframes-0.1.0/bobframes/tests/data/golden/index.html +0 -1175
  144. bobframes-0.1.0/bobframes/tests/test_hardening.py +0 -142
  145. {bobframes-0.1.0 → bobframes-0.2.0}/.gitignore +0 -0
  146. {bobframes-0.1.0 → bobframes-0.2.0}/LICENSE +0 -0
  147. {bobframes-0.1.0 → bobframes-0.2.0}/bobframes/__init__.py +0 -0
  148. {bobframes-0.1.0 → bobframes-0.2.0}/bobframes/derives/__init__.py +0 -0
  149. {bobframes-0.1.0 → bobframes-0.2.0}/bobframes/derives/texture_usage.py +0 -0
  150. {bobframes-0.1.0 → bobframes-0.2.0}/bobframes/html/__init__.py +0 -0
  151. {bobframes-0.1.0 → bobframes-0.2.0}/bobframes/parsers/__init__.py +0 -0
  152. {bobframes-0.1.0 → bobframes-0.2.0}/bobframes/parsers/derive_program_transitions.py +0 -0
  153. {bobframes-0.1.0 → bobframes-0.2.0}/bobframes/probes/__init__.py +0 -0
  154. {bobframes-0.1.0 → bobframes-0.2.0}/bobframes/probes/whatif.py +0 -0
  155. {bobframes-0.1.0 → bobframes-0.2.0}/bobframes/query_examples.py +0 -0
  156. {bobframes-0.1.0 → bobframes-0.2.0}/bobframes/replay/__init__.py +0 -0
  157. {bobframes-0.1.0 → bobframes-0.2.0}/bobframes/resource_labels.py +0 -0
  158. {bobframes-0.1.0 → bobframes-0.2.0}/bobframes/stable_keys.py +0 -0
  159. {bobframes-0.1.0/bobframes/reports → bobframes-0.2.0/bobframes/tests}/__init__.py +0 -0
  160. {bobframes-0.1.0 → bobframes-0.2.0}/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/buffers.parquet +0 -0
  161. {bobframes-0.1.0 → bobframes-0.2.0}/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/clears.parquet +0 -0
  162. {bobframes-0.1.0 → bobframes-0.2.0}/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/counters_per_event.parquet +0 -0
  163. {bobframes-0.1.0 → bobframes-0.2.0}/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/descriptor_access.parquet +0 -0
  164. {bobframes-0.1.0 → bobframes-0.2.0}/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/dispatches.parquet +0 -0
  165. {bobframes-0.1.0 → bobframes-0.2.0}/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/draw_bindings.parquet +0 -0
  166. {bobframes-0.1.0 → bobframes-0.2.0}/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/draws.parquet +0 -0
  167. {bobframes-0.1.0 → bobframes-0.2.0}/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/events.parquet +0 -0
  168. {bobframes-0.1.0 → bobframes-0.2.0}/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/fbos.parquet +0 -0
  169. {bobframes-0.1.0 → bobframes-0.2.0}/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/frame_totals.parquet +0 -0
  170. {bobframes-0.1.0 → bobframes-0.2.0}/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/ibo_samples.parquet +0 -0
  171. {bobframes-0.1.0 → bobframes-0.2.0}/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/indirect_args.parquet +0 -0
  172. {bobframes-0.1.0 → bobframes-0.2.0}/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/passes.parquet +0 -0
  173. {bobframes-0.1.0 → bobframes-0.2.0}/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/pixel_history.parquet +0 -0
  174. {bobframes-0.1.0 → bobframes-0.2.0}/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/post_vs_samples.parquet +0 -0
  175. {bobframes-0.1.0 → bobframes-0.2.0}/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/program_transitions.parquet +0 -0
  176. {bobframes-0.1.0 → bobframes-0.2.0}/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/programs.parquet +0 -0
  177. {bobframes-0.1.0 → bobframes-0.2.0}/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/render_targets.parquet +0 -0
  178. {bobframes-0.1.0 → bobframes-0.2.0}/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/resource_creation.parquet +0 -0
  179. {bobframes-0.1.0 → bobframes-0.2.0}/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/rt_event_timeline.parquet +0 -0
  180. {bobframes-0.1.0 → bobframes-0.2.0}/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/samplers.parquet +0 -0
  181. {bobframes-0.1.0 → bobframes-0.2.0}/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/shaders.parquet +0 -0
  182. {bobframes-0.1.0 → bobframes-0.2.0}/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/state_change_events.parquet +0 -0
  183. {bobframes-0.1.0 → bobframes-0.2.0}/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/texture_samples.parquet +0 -0
  184. {bobframes-0.1.0 → bobframes-0.2.0}/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/textures.parquet +0 -0
  185. {bobframes-0.1.0 → bobframes-0.2.0}/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/vbo_samples.parquet +0 -0
  186. {bobframes-0.1.0 → bobframes-0.2.0}/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/vertex_inputs.parquet +0 -0
  187. {bobframes-0.1.0 → bobframes-0.2.0}/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/buffers.parquet +0 -0
  188. {bobframes-0.1.0 → bobframes-0.2.0}/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/clears.parquet +0 -0
  189. {bobframes-0.1.0 → bobframes-0.2.0}/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/counters_per_event.parquet +0 -0
  190. {bobframes-0.1.0 → bobframes-0.2.0}/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/descriptor_access.parquet +0 -0
  191. {bobframes-0.1.0 → bobframes-0.2.0}/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/dispatches.parquet +0 -0
  192. {bobframes-0.1.0 → bobframes-0.2.0}/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/draw_bindings.parquet +0 -0
  193. {bobframes-0.1.0 → bobframes-0.2.0}/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/draws.parquet +0 -0
  194. {bobframes-0.1.0 → bobframes-0.2.0}/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/events.parquet +0 -0
  195. {bobframes-0.1.0 → bobframes-0.2.0}/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/fbos.parquet +0 -0
  196. {bobframes-0.1.0 → bobframes-0.2.0}/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/frame_totals.parquet +0 -0
  197. {bobframes-0.1.0 → bobframes-0.2.0}/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/ibo_samples.parquet +0 -0
  198. {bobframes-0.1.0 → bobframes-0.2.0}/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/indirect_args.parquet +0 -0
  199. {bobframes-0.1.0 → bobframes-0.2.0}/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/passes.parquet +0 -0
  200. {bobframes-0.1.0 → bobframes-0.2.0}/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/pixel_history.parquet +0 -0
  201. {bobframes-0.1.0 → bobframes-0.2.0}/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/post_vs_samples.parquet +0 -0
  202. {bobframes-0.1.0 → bobframes-0.2.0}/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/program_transitions.parquet +0 -0
  203. {bobframes-0.1.0 → bobframes-0.2.0}/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/programs.parquet +0 -0
  204. {bobframes-0.1.0 → bobframes-0.2.0}/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/render_targets.parquet +0 -0
  205. {bobframes-0.1.0 → bobframes-0.2.0}/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/resource_creation.parquet +0 -0
  206. {bobframes-0.1.0 → bobframes-0.2.0}/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/rt_event_timeline.parquet +0 -0
  207. {bobframes-0.1.0 → bobframes-0.2.0}/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/samplers.parquet +0 -0
  208. {bobframes-0.1.0 → bobframes-0.2.0}/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/shaders.parquet +0 -0
  209. {bobframes-0.1.0 → bobframes-0.2.0}/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/state_change_events.parquet +0 -0
  210. {bobframes-0.1.0 → bobframes-0.2.0}/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/texture_samples.parquet +0 -0
  211. {bobframes-0.1.0 → bobframes-0.2.0}/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/textures.parquet +0 -0
  212. {bobframes-0.1.0 → bobframes-0.2.0}/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/vbo_samples.parquet +0 -0
  213. {bobframes-0.1.0 → bobframes-0.2.0}/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/vertex_inputs.parquet +0 -0
  214. {bobframes-0.1.0 → bobframes-0.2.0}/bobframes/tests/test_determinism.py +0 -0
  215. {bobframes-0.1.0 → bobframes-0.2.0}/bobframes/tests/test_perf.py +0 -0
  216. {bobframes-0.1.0 → bobframes-0.2.0}/bobframes/tests/test_replay_drift.py +0 -0
  217. {bobframes-0.1.0 → bobframes-0.2.0}/bobframes/tests/test_schemas.py +0 -0
  218. {bobframes-0.1.0 → bobframes-0.2.0}/bobframes/tests/test_stable_keys.py +0 -0
@@ -7,6 +7,38 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.2.0] - 2026-06-03
11
+
12
+ De-hardcoding + a full report overhaul. No schema change (still schema 3); extraction output is stable
13
+ where the pipeline is unchanged, so parquet digests are byte-identical to 0.1.0 on the same captures.
14
+
15
+ ### Added
16
+ - Inline-SVG chart toolkit (`reports/charts.py`): a flagship chart per report (pass-GPU treemap,
17
+ draws-by-class donut, shader complexity-vs-cost scatter, overdraw reject-rate bars, instancing
18
+ wasted-index bars, per-KPI trend lines), deterministic and dependency-free (no `random`/`Date`).
19
+ - Report design language: hero KPI strip, callouts with config-driven severity, provenance/device
20
+ strip, section cards, sticky section headers, copy buttons on long IDs, vendored Inter subset font
21
+ (offline, base64-inlined), and an auto-tint heatmap on ranked numeric columns.
22
+ - Run model: per-run truth (each report names "run N of M"), a run picker, A/B compare, and a trend
23
+ table across runs; older runs are pre-rendered up to a configurable limit.
24
+ - One unified `rdc-table` engine (progressive enhancement, two modes): server-baked `static` for reports
25
+ (JS-off / print / Ctrl-F / golden all hold) and windowed `virtual` for the catalog + per-drop drill.
26
+ Shared natural-numeric sort, type-split, heatmap, column groups, search/filter, controllable cell
27
+ truncation with hover-reveal, and full sort/filter a11y (aria-sort + keyboard-operable headers + a
28
+ labelled filter input) across both modes.
29
+ - TOML config (`.bobframes.toml`, `[report]`/`[pipeline]` sections), `design_tokens.toml` token system,
30
+ and a state-capable draw classifier driven by table columns.
31
+ - Reports cache with a SHA256 sidecar (corrupt/missing/mismatch falls back to a live scan, never
32
+ silent-empty); manifest schema-version guard (`ingest --force` hint on mismatch).
33
+
34
+ ### Changed
35
+ - Catalog + drill readability: roomier rows, type-split mono/sans, and the heavy per-table row data moved
36
+ out of the HTML into `_pagedata/*.js` companions (the ~21 MB time-to-interactive fix) loaded via a
37
+ file://-safe `<script defer src>`.
38
+ - External tool resolution + clearer pipeline error messages and exit codes; environment variables
39
+ renamed `RDC_*` -> `BOBFRAMES_*` (one-release legacy fallback with a deprecation warning).
40
+ - Project paths, the report registry, and the drill-size budget are configurable rather than hardcoded.
41
+
10
42
  ## [0.1.0] - 2026-05-31
11
43
 
12
44
  First standalone release. v1 is Windows-only (the replay stage drives `qrenderdoc`).
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bobframes
3
- Version: 0.1.0
3
+ Version: 0.2.0
4
4
  Summary: RenderDoc capture pipeline: ingest, analyze, render.
5
5
  Project-URL: Homepage, https://github.com/altpsyche/bobframes
6
6
  Project-URL: Issues, https://github.com/altpsyche/bobframes/issues
@@ -21,6 +21,7 @@ Classifier: Topic :: Multimedia :: Graphics
21
21
  Classifier: Topic :: Software Development :: Debuggers
22
22
  Requires-Python: <3.15,>=3.10
23
23
  Requires-Dist: pyarrow<22,>=17
24
+ Requires-Dist: tomli>=2.0; python_version < '3.11'
24
25
  Provides-Extra: dev
25
26
  Requires-Dist: build; extra == 'dev'
26
27
  Requires-Dist: hatchling; extra == 'dev'
@@ -65,19 +66,42 @@ time to rebuild the HTML from existing Parquet without re-replaying captures.
65
66
  | Command | Purpose |
66
67
  |---|---|
67
68
  | `ingest [root] [--area X] [--label Y] [--capture N] [--force] [--pixel-grid 4] [--render-only]` | Full pipeline: export, parse, replay, parquetize, derive, manifest, commit, catalog, render. |
68
- | `render [root] [--area X] [--label Y]` | Rebuild HTML and catalog from existing Parquet. |
69
+ | `render [root] [--area X] [--label Y] [--watch]` | Rebuild HTML and catalog from existing Parquet. `--watch` re-renders on token or chrome edits (alpha). |
69
70
  | `ab [root] --baseline-label X --compare-label Y` | All reports for one drop pair under `_reports/ab/<pair>/`. |
70
71
  | `report [root] <name>` | Build one named report (draws-by-class, trend, instancing, pass-gpu, shader, overdraw, dashboard). |
71
72
  | `catalog [root]` | Rebuild `_data/_catalog.parquet` only. |
72
73
  | `lint <file>...` | Check HTML or markdown against the banlist. |
73
74
  | `check` | Print resolved tool paths; non-zero when a tool is missing. |
74
75
  | `serve [root] [--port 8000] [--bind 127.0.0.1]` | Static preview via the stdlib HTTP server. |
76
+ | `preview [root]` | Render the chrome gallery to `_reports/_chrome_preview.html`; no capture data needed. |
77
+ | `export-tokens [--format toml\|json\|css]` | Print the design tokens to stdout in the chosen format. |
75
78
  | `smoke [--data DIR]` | End-to-end check; render-only against the bundled fixture when `--data` is omitted. |
76
79
  | `version` | Print `bobframes`, schema, and pyarrow versions. |
77
80
 
78
81
  `<root>` is positional and defaults to `.`. Flags are long-form only. Exit codes: `0` success,
79
82
  `1` pipeline or build failure, `2` usage error, `3` external tool missing, `4` interrupted.
80
83
 
84
+ ## Customizing reports
85
+
86
+ The report palette lives in `bobframes/reports/design_tokens.toml` (colors, spacing, type, motion,
87
+ and base layout sizes). A designer can edit values there without touching Python. The CSS variable
88
+ name is fixed by its key: `surface_0` becomes `--surface-0`, `accent_primary` becomes
89
+ `--accent-primary`.
90
+
91
+ Workflow:
92
+
93
+ ```
94
+ bobframes preview # writes _reports/_chrome_preview.html (every primitive, no data)
95
+ # edit a value in design_tokens.toml
96
+ bobframes preview # under a second; reload the page in a browser
97
+ bobframes render C:\captures # apply the change to real reports from existing Parquet
98
+ ```
99
+
100
+ `bobframes render --watch` re-runs render-only whenever the token file or a chrome module changes
101
+ (alpha; a 500ms poll, no extra dependency). `bobframes export-tokens --format css` prints the live
102
+ `:root` block; `--format json` prints the nested table; `--format toml` prints the file itself. For
103
+ prototyping, a browser's dev tools can live-edit the same variables before you commit a value.
104
+
81
105
  ## External tools
82
106
 
83
107
  The export stage runs `renderdoccmd convert`; the replay stage runs `qrenderdoc --python`. v1 looks
@@ -35,19 +35,42 @@ time to rebuild the HTML from existing Parquet without re-replaying captures.
35
35
  | Command | Purpose |
36
36
  |---|---|
37
37
  | `ingest [root] [--area X] [--label Y] [--capture N] [--force] [--pixel-grid 4] [--render-only]` | Full pipeline: export, parse, replay, parquetize, derive, manifest, commit, catalog, render. |
38
- | `render [root] [--area X] [--label Y]` | Rebuild HTML and catalog from existing Parquet. |
38
+ | `render [root] [--area X] [--label Y] [--watch]` | Rebuild HTML and catalog from existing Parquet. `--watch` re-renders on token or chrome edits (alpha). |
39
39
  | `ab [root] --baseline-label X --compare-label Y` | All reports for one drop pair under `_reports/ab/<pair>/`. |
40
40
  | `report [root] <name>` | Build one named report (draws-by-class, trend, instancing, pass-gpu, shader, overdraw, dashboard). |
41
41
  | `catalog [root]` | Rebuild `_data/_catalog.parquet` only. |
42
42
  | `lint <file>...` | Check HTML or markdown against the banlist. |
43
43
  | `check` | Print resolved tool paths; non-zero when a tool is missing. |
44
44
  | `serve [root] [--port 8000] [--bind 127.0.0.1]` | Static preview via the stdlib HTTP server. |
45
+ | `preview [root]` | Render the chrome gallery to `_reports/_chrome_preview.html`; no capture data needed. |
46
+ | `export-tokens [--format toml\|json\|css]` | Print the design tokens to stdout in the chosen format. |
45
47
  | `smoke [--data DIR]` | End-to-end check; render-only against the bundled fixture when `--data` is omitted. |
46
48
  | `version` | Print `bobframes`, schema, and pyarrow versions. |
47
49
 
48
50
  `<root>` is positional and defaults to `.`. Flags are long-form only. Exit codes: `0` success,
49
51
  `1` pipeline or build failure, `2` usage error, `3` external tool missing, `4` interrupted.
50
52
 
53
+ ## Customizing reports
54
+
55
+ The report palette lives in `bobframes/reports/design_tokens.toml` (colors, spacing, type, motion,
56
+ and base layout sizes). A designer can edit values there without touching Python. The CSS variable
57
+ name is fixed by its key: `surface_0` becomes `--surface-0`, `accent_primary` becomes
58
+ `--accent-primary`.
59
+
60
+ Workflow:
61
+
62
+ ```
63
+ bobframes preview # writes _reports/_chrome_preview.html (every primitive, no data)
64
+ # edit a value in design_tokens.toml
65
+ bobframes preview # under a second; reload the page in a browser
66
+ bobframes render C:\captures # apply the change to real reports from existing Parquet
67
+ ```
68
+
69
+ `bobframes render --watch` re-runs render-only whenever the token file or a chrome module changes
70
+ (alpha; a 500ms poll, no extra dependency). `bobframes export-tokens --format css` prints the live
71
+ `:root` block; `--format json` prints the nested table; `--format toml` prints the file itself. For
72
+ prototyping, a browser's dev tools can live-edit the same variables before you commit a value.
73
+
51
74
  ## External tools
52
75
 
53
76
  The export stage runs `renderdoccmd convert`; the replay stage runs `qrenderdoc --python`. v1 looks
@@ -0,0 +1,61 @@
1
+ # bobframes bundled default config (c07). Single source of truth for de-hardcoded literals.
2
+ # Values here reproduce today's output byte-identically (ADR-6). A user .bobframes.toml is
3
+ # deep-merged ON TOP of this (override one key without restating the rest — ADR-25).
4
+ #
5
+ # Regex / format-string values MUST be TOML *literal* (single-quoted) strings: a basic
6
+ # (double-quoted) string interprets backslash escapes and breaks on \d. Do NOT "fix" a decode
7
+ # error by double-escaping — that silently changes the pattern (ADR-23 trap).
8
+
9
+ schema_version = 1
10
+
11
+ [tools]
12
+ # renderdoccmd = "..." # unset by default; resolve_tool falls through to PATH / known paths
13
+ # qrenderdoc = "..."
14
+
15
+ [pipeline]
16
+ replay_timeout_s = 600.0 # H-12 — per-capture qrenderdoc replay budget (seconds)
17
+ convert_timeout_s = 120.0 # H-13 — per-file renderdoccmd convert budget (seconds)
18
+ # workers / pixel_grid / keep_stage are §6 keys but NOT wired in c07 (argparse remains the
19
+ # source; workers default is min(4, os.cpu_count()), so wiring it would change behavior).
20
+
21
+ [discovery]
22
+ dated_re = '^(\d{4}-\d{2}-\d{2})(?:_(.*))?$' # H-30 — dated drop-folder pattern
23
+
24
+ [formatting]
25
+ id_short_n = 12 # H-23 — id/hash truncation length
26
+ text_trunc_max = 60 # H-23 — mid/left text truncation length
27
+ chrome_scrub_chars = '[—–…“”‘’→←↑↓×·]' # H-16 — chars scrubbed from rendered chrome
28
+
29
+ [delta]
30
+ fmt = '{:+,.0f}' # H-22 — default signed delta format
31
+ bar_label_min_pct = 8.0 # H-21 — min segment % to draw an inline label
32
+
33
+ [scoring.complexity] # H-17 / Q-3 — shader complexity_score weights
34
+ w_texture_samples = 2.0
35
+ w_branches = 0.5
36
+ w_loops = 2.0
37
+ w_discards = 0.3
38
+ w_dfdx_dfdy = 0.5
39
+ w_mat4 = 0.3
40
+ src_len_divisor = 100.0
41
+ src_len_cap = 50.0
42
+
43
+ [lint]
44
+ # User-appended banned tokens, same shape as lint_banlist.toml [[banned]] entries:
45
+ # [[lint.extra_banned]]
46
+ # pattern = 'foo'
47
+ # label = 'why'
48
+ # flags = ['I']
49
+ extra_banned = []
50
+
51
+ [classifier] # c09 — draw-classification preset (H-1..H-5)
52
+ preset = "ue" # draw_classifier.toml (UE default); "unity"/"godot"/... -> presets/<name>.toml
53
+ # custom_path = "C:/path/to/my_engine.toml" # absolute path to a custom preset (wins over `preset`)
54
+
55
+ [report] # c16 — thresholds that drive report callout severity. Tune per project.
56
+ shader_complexity_high = 60.0 # fragment shaders at/above this complexity are flagged
57
+ overdraw_reject_warn_pct = 40.0 # RT sample-rejection % -> warn callout
58
+ overdraw_reject_alarm_pct = 70.0 # RT sample-rejection % -> alarm callout
59
+ instancing_repeat_min = 4 # min mesh repeat to count as an instancing candidate
60
+ gpu_regression_pct = 10.0 # latest-vs-previous GPU increase % -> regression callout
61
+ max_prerendered_runs = 10 # c16f — cap on pre-rendered older-run pages (run selector)
@@ -0,0 +1 @@
1
+ __version__ = "0.2.0"
@@ -21,22 +21,12 @@ import pyarrow as pa
21
21
  import pyarrow.csv as pacsv
22
22
  import pyarrow.parquet as papq
23
23
 
24
- from . import paths as _paths
24
+ from . import manifest as _manifest, paths as _paths, schemas
25
25
 
26
26
 
27
- _CATALOG_TABLE_KEYS = [
28
- 'draws', 'events', 'shaders', 'textures',
29
- 'render_targets', 'buffers', 'programs',
30
- 'samplers', 'fbos', 'state_change_events',
31
- 'counters_per_event', 'descriptor_access',
32
- 'passes', 'frame_totals',
33
- 'clears', 'dispatches', 'rt_event_timeline',
34
- 'vertex_inputs', 'resource_creation',
35
- 'draw_bindings', 'program_transitions',
36
- 'pixel_history', 'vbo_samples', 'ibo_samples',
37
- 'post_vs_samples', 'texture_samples', 'indirect_args',
38
- 'pass_class_breakdown', 'texture_usage',
39
- ]
27
+ # Derived from the single registry (H-10). schemas.TABLES key order IS the catalog order, so the
28
+ # row_count_* column order baked into the golden root index.html stays byte-identical.
29
+ _CATALOG_TABLE_KEYS = tuple(schemas.TABLES.keys())
40
30
 
41
31
 
42
32
  def _find_manifests(root: str) -> list[tuple[str, str, dict]]:
@@ -55,7 +45,7 @@ def _find_manifests(root: str) -> list[tuple[str, str, dict]]:
55
45
  drop_dir = os.path.join(area_dir, drop_entry)
56
46
  if not os.path.isdir(drop_dir):
57
47
  continue
58
- mf = os.path.join(drop_dir, '_manifest.json')
48
+ mf = os.path.join(drop_dir, _paths.MANIFEST_NAME)
59
49
  if not os.path.exists(mf):
60
50
  continue
61
51
  try:
@@ -113,6 +103,9 @@ def build_catalog(root: str) -> dict:
113
103
  manifests = _find_manifests(root)
114
104
  all_rows: list[dict] = []
115
105
  for data_dir, rel_path, m in manifests:
106
+ # D-7: refuse to (re)build over a drop written under a different SCHEMA_VERSION. This is the
107
+ # shared chokepoint for `render` (which rebuilds the catalog first) and `catalog`.
108
+ _manifest.check_schema_version(m, source=rel_path)
116
109
  all_rows.extend(_capture_rows(data_dir, rel_path, m))
117
110
 
118
111
  cols = [
@@ -17,6 +17,8 @@ import logging
17
17
  import os
18
18
  import sys
19
19
 
20
+ from .errors import BobFramesError
21
+
20
22
  # CLI report name -> reports submodule. `build(root)` on each builds one report.
21
23
  _REPORTS = {
22
24
  'draws-by-class': 'draws_by_class',
@@ -25,7 +27,7 @@ _REPORTS = {
25
27
  'pass-gpu': 'pass_gpu',
26
28
  'shader': 'shader_hotlist',
27
29
  'overdraw': 'overdraw',
28
- 'dashboard': '_dashboard',
30
+ 'dashboard': 'dashboard',
29
31
  }
30
32
 
31
33
 
@@ -68,19 +70,80 @@ def _cmd_ingest(args: argparse.Namespace) -> int:
68
70
  if args.render_only:
69
71
  rargv += ['--render-only']
70
72
  rargv += ['--workers', str(args.workers), '--pixel-grid', str(args.pixel_grid)]
73
+ if args.replay_timeout is not None:
74
+ rargv += ['--replay-timeout', str(args.replay_timeout)]
75
+ if args.convert_timeout is not None:
76
+ rargv += ['--convert-timeout', str(args.convert_timeout)]
71
77
  return run.main(rargv)
72
78
 
73
79
 
74
80
  def _cmd_render(args: argparse.Namespace) -> int:
75
81
  from . import run
76
- rargv = ['--root', os.path.abspath(args.root), '--render-only']
82
+ root = os.path.abspath(args.root)
83
+ rargv = ['--root', root, '--render-only']
77
84
  if args.area:
78
85
  rargv += ['--area', args.area]
79
86
  if args.label:
80
87
  rargv += ['--label', args.label]
88
+ if getattr(args, 'watch', False):
89
+ return _render_watch(rargv)
81
90
  return run.main(rargv)
82
91
 
83
92
 
93
+ def _watch_paths() -> list[str]:
94
+ """Files the render --watch loop polls: the design tokens + the modules that emit chrome."""
95
+ from .reports import chrome as _c, delta as _d, formatters as _fm
96
+ tokens = os.path.join(os.path.dirname(_c.__file__), 'design_tokens.toml')
97
+ return [tokens, _c.__file__, _fm.__file__, _d.__file__]
98
+
99
+
100
+ def _render_watch(rargv: list[str]) -> int:
101
+ """Alpha hot-reload (DESIGNER Track A): 500ms mtime poll on the token/chrome files; re-render in a
102
+ fresh subprocess on change so edited modules/TOML are reloaded. No watchdog dependency."""
103
+ import subprocess
104
+ import time
105
+ log = logging.getLogger('bobframes')
106
+ watched = _watch_paths()
107
+
108
+ def snapshot() -> dict:
109
+ return {p: (os.stat(p).st_mtime if os.path.exists(p) else 0.0) for p in watched}
110
+
111
+ cmd = [sys.executable, '-m', 'bobframes.run', *rargv]
112
+ rc = subprocess.run(cmd).returncode
113
+ log.info(f'watching {len(watched)} files; save an edit to re-render (Ctrl+C to stop)')
114
+ last = snapshot()
115
+ try:
116
+ while True:
117
+ time.sleep(0.5)
118
+ now = snapshot()
119
+ if now != last:
120
+ last = now
121
+ log.info('change detected; re-rendering')
122
+ rc = subprocess.run(cmd).returncode
123
+ except KeyboardInterrupt:
124
+ return 4
125
+ return rc
126
+
127
+
128
+ def _cmd_preview(args: argparse.Namespace) -> int:
129
+ from .reports import preview
130
+ out = preview.build(os.path.abspath(args.root))
131
+ print(f'wrote {out}')
132
+ return 0
133
+
134
+
135
+ def _cmd_export_tokens(args: argparse.Namespace) -> int:
136
+ import json
137
+ from .reports import _tokens, chrome
138
+ if args.format == 'toml':
139
+ sys.stdout.write(_tokens.tokens_toml_text())
140
+ elif args.format == 'json':
141
+ print(json.dumps(_tokens.load_tokens(), indent=2))
142
+ else: # css
143
+ sys.stdout.write(chrome.design_tokens_css())
144
+ return 0
145
+
146
+
84
147
  def _cmd_ab(args: argparse.Namespace) -> int:
85
148
  from .reports import ab
86
149
  argv = [os.path.abspath(args.root),
@@ -130,20 +193,24 @@ def _cmd_check(args: argparse.Namespace) -> int:
130
193
  print('bobframes v1 is Windows-only (qrenderdoc replay requirement). '
131
194
  'Track GH issue for Linux/macOS support.', file=sys.stderr)
132
195
  return 3
196
+ from . import config
133
197
  if args.write_config:
134
- print('--write-config is a v0.2 feature (config layer); not available yet.',
135
- file=sys.stderr)
136
- return 2
137
- from . import qrd_harness, rdcmd
198
+ path, written = config.write_config_stub(os.path.abspath('.'))
199
+ print(f'wrote {path}' if written else f'{path} already exists; left unchanged')
200
+ return 0
201
+ from .errors import EXIT_TOOL_MISSING, ToolNotFound
138
202
  missing = False
139
- for name, finder in (('renderdoccmd', rdcmd.find_renderdoccmd),
140
- ('qrenderdoc', qrd_harness.find_qrenderdoc)):
203
+ for name in ('renderdoccmd', 'qrenderdoc'):
141
204
  try:
142
- print(f'{name}: {finder()}')
143
- except FileNotFoundError:
144
- print(f'{name}: NOT FOUND', file=sys.stderr)
205
+ path, source = config.resolve_tool_verbose(name)
206
+ # `source` is the winning step's description; for env/config/PATH it explains the
207
+ # precedence, but for a known-path hit it IS the path -> don't print it twice.
208
+ suffix = '' if source == path else f' (via {source})'
209
+ print(f'{name}: {path}{suffix}')
210
+ except ToolNotFound as e:
211
+ print(str(e), file=sys.stderr)
145
212
  missing = True
146
- return 3 if missing else 0
213
+ return EXIT_TOOL_MISSING if missing else 0
147
214
 
148
215
 
149
216
  def _cmd_serve(args: argparse.Namespace) -> int:
@@ -191,6 +258,10 @@ def _build_parser() -> argparse.ArgumentParser:
191
258
  sp.add_argument('--render-only', action='store_true')
192
259
  sp.add_argument('--workers', type=int, default=min(4, os.cpu_count() or 4))
193
260
  sp.add_argument('--pixel-grid', type=int, default=4)
261
+ sp.add_argument('--replay-timeout', type=float, default=None,
262
+ help='per-capture qrenderdoc replay budget (s); overrides config')
263
+ sp.add_argument('--convert-timeout', type=float, default=None,
264
+ help='per-file renderdoccmd convert budget (s); overrides config')
194
265
  sp.set_defaults(func=_cmd_ingest)
195
266
 
196
267
  sp = sub.add_parser('render', parents=[common],
@@ -198,6 +269,8 @@ def _build_parser() -> argparse.ArgumentParser:
198
269
  sp.add_argument('root', nargs='?', default='.')
199
270
  sp.add_argument('--area')
200
271
  sp.add_argument('--label')
272
+ sp.add_argument('--watch', action='store_true',
273
+ help='re-render on design_tokens.toml / chrome edits (alpha, 500ms mtime poll)')
201
274
  sp.set_defaults(func=_cmd_render)
202
275
 
203
276
  sp = sub.add_parser('ab', parents=[common], help='all reports for one drop pair')
@@ -222,12 +295,23 @@ def _build_parser() -> argparse.ArgumentParser:
222
295
  sp.set_defaults(func=_cmd_lint)
223
296
 
224
297
  sp = sub.add_parser('check', parents=[common], help='resolve external tool paths')
225
- sp.add_argument('--write-config', action='store_true', help='(v0.2)')
298
+ sp.add_argument('--write-config', action='store_true',
299
+ help='write a starter .bobframes.toml to the current dir (skip if present)')
226
300
  sp.set_defaults(func=_cmd_check)
227
301
 
228
302
  sp = sub.add_parser('version', parents=[common], help='print version, schema, pyarrow')
229
303
  sp.set_defaults(func=_cmd_version)
230
304
 
305
+ sp = sub.add_parser('preview', parents=[common],
306
+ help='render the chrome preview gallery (_reports/_chrome_preview.html; no data)')
307
+ sp.add_argument('root', nargs='?', default='.')
308
+ sp.set_defaults(func=_cmd_preview)
309
+
310
+ sp = sub.add_parser('export-tokens', parents=[common],
311
+ help='print design tokens to stdout as toml|json|css')
312
+ sp.add_argument('--format', choices=['toml', 'json', 'css'], default='toml')
313
+ sp.set_defaults(func=_cmd_export_tokens)
314
+
231
315
  sp = sub.add_parser('serve', parents=[common], help='static preview server')
232
316
  sp.add_argument('root', nargs='?', default='.')
233
317
  sp.add_argument('--port', type=int, default=8000)
@@ -256,10 +340,22 @@ def main(argv: list[str] | None = None) -> int:
256
340
 
257
341
  _configure_logging(getattr(args, 'verbose', False))
258
342
  try:
343
+ # Load the config singleton against the verb's root so <root>/.bobframes.toml is honored
344
+ # (§6) for report/catalog/ab/render/ingest/serve. Verbs without a root (version/check/lint)
345
+ # lazily load bundled defaults (cwd) on first get_config(). ingest/render re-load via
346
+ # run.main (idempotent). ConfigError surfaces as a typed BobFramesError below.
347
+ root = getattr(args, 'root', None)
348
+ if root is not None:
349
+ from . import config
350
+ config.load_config(os.path.abspath(root))
259
351
  return args.func(args)
260
352
  except KeyboardInterrupt:
261
353
  print('interrupted', file=sys.stderr)
262
354
  return 4
355
+ except BobFramesError as e:
356
+ # Typed pipeline/tool errors carry their own exit code (errors.py / ARCHITECTURE §4).
357
+ print(str(e), file=sys.stderr)
358
+ return e.exit_code
263
359
 
264
360
 
265
361
  if __name__ == '__main__':