bobframes 0.2.6__tar.gz → 0.2.8__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 (409) hide show
  1. {bobframes-0.2.6 → bobframes-0.2.8}/CHANGELOG.md +71 -1
  2. {bobframes-0.2.6 → bobframes-0.2.8}/PKG-INFO +17 -1
  3. {bobframes-0.2.6 → bobframes-0.2.8}/README.md +16 -0
  4. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/_default_config.toml +5 -1
  5. bobframes-0.2.8/bobframes/_version.py +1 -0
  6. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/aggregates.py +66 -0
  7. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/cli.py +22 -13
  8. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/config.py +10 -0
  9. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/package.py +27 -0
  10. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/reports/assets/components.js +13 -1
  11. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/reports/dashboard.py +28 -20
  12. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/reports/discovery.py +9 -0
  13. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/reports/draws_by_class.py +16 -7
  14. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/reports/instancing_opportunities.py +4 -3
  15. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/reports/orchestrator.py +11 -0
  16. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/reports/overdraw.py +3 -1
  17. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/reports/pass_gpu.py +16 -6
  18. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/reports/preview.py +4 -4
  19. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/reports/shader_hotlist.py +1 -0
  20. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/reports/summary.py +13 -8
  21. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/reports/trend_table.py +84 -32
  22. bobframes-0.2.8/bobframes/serve.py +36 -0
  23. bobframes-0.2.8/bobframes/tests/_ui_util.py +57 -0
  24. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden/_reports/draws_by_class.html +9 -2
  25. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden/_reports/index.html +10 -3
  26. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden/_reports/instancing_opportunities.html +9 -2
  27. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden/_reports/overdraw.html +8 -1
  28. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden/_reports/pass_gpu.html +11 -4
  29. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden/_reports/run/2026-05-27_r110565/draws_by_class.html +11 -5
  30. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden/_reports/run/2026-05-27_r110565/index.html +10 -3
  31. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden/_reports/run/2026-05-27_r110565/instancing_opportunities.html +9 -3
  32. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden/_reports/run/2026-05-27_r110565/overdraw.html +10 -4
  33. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden/_reports/run/2026-05-27_r110565/pass_gpu.html +9 -3
  34. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden/_reports/run/2026-05-27_r110565/shader_hotlist.html +9 -3
  35. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden/_reports/run/2026-05-27_r110565/summary.html +10 -4
  36. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden/_reports/shader_hotlist.html +8 -1
  37. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden/_reports/summary.html +10 -3
  38. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden/_reports/trend_table.html +24 -17
  39. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden_package/redacted/_reports/draws_by_class.html +9 -2
  40. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden_package/redacted/_reports/index.html +10 -3
  41. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden_package/redacted/_reports/instancing_opportunities.html +9 -2
  42. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden_package/redacted/_reports/overdraw.html +8 -1
  43. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden_package/redacted/_reports/pass_gpu.html +11 -4
  44. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden_package/redacted/_reports/run/2026-05-27_r110565/draws_by_class.html +11 -5
  45. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden_package/redacted/_reports/run/2026-05-27_r110565/index.html +10 -3
  46. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden_package/redacted/_reports/run/2026-05-27_r110565/instancing_opportunities.html +9 -3
  47. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden_package/redacted/_reports/run/2026-05-27_r110565/overdraw.html +10 -4
  48. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden_package/redacted/_reports/run/2026-05-27_r110565/pass_gpu.html +9 -3
  49. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden_package/redacted/_reports/run/2026-05-27_r110565/shader_hotlist.html +9 -3
  50. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden_package/redacted/_reports/run/2026-05-27_r110565/summary.html +10 -4
  51. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden_package/redacted/_reports/shader_hotlist.html +8 -1
  52. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden_package/redacted/_reports/summary.html +10 -3
  53. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden_package/redacted/_reports/trend_table.html +24 -17
  54. {bobframes-0.2.6/bobframes/tests/data/golden_package/shared_redacted → bobframes-0.2.8/bobframes/tests/data/golden_package/shared}/_assets/report.js +8 -1
  55. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared/_reports/draws_by_class.html +1 -1
  56. bobframes-0.2.8/bobframes/tests/data/golden_package/shared/_reports/index.html +44 -0
  57. bobframes-0.2.8/bobframes/tests/data/golden_package/shared/_reports/instancing_opportunities.html +46 -0
  58. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared/_reports/pass_gpu.html +3 -3
  59. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared/_reports/run/2026-05-27_r110565/draws_by_class.html +3 -4
  60. bobframes-0.2.8/bobframes/tests/data/golden_package/shared/_reports/run/2026-05-27_r110565/index.html +45 -0
  61. bobframes-0.2.8/bobframes/tests/data/golden_package/shared/_reports/run/2026-05-27_r110565/instancing_opportunities.html +43 -0
  62. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared/_reports/run/2026-05-27_r110565/overdraw.html +2 -3
  63. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared/_reports/run/2026-05-27_r110565/pass_gpu.html +1 -2
  64. bobframes-0.2.8/bobframes/tests/data/golden_package/shared/_reports/run/2026-05-27_r110565/shader_hotlist.html +43 -0
  65. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared/_reports/run/2026-05-27_r110565/summary.html +2 -3
  66. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared/_reports/summary.html +2 -2
  67. bobframes-0.2.8/bobframes/tests/data/golden_package/shared/_reports/trend_table.html +57 -0
  68. {bobframes-0.2.6/bobframes/tests/data/golden_package/shared → bobframes-0.2.8/bobframes/tests/data/golden_package/shared_redacted}/_assets/report.js +8 -1
  69. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared_redacted/_reports/draws_by_class.html +1 -1
  70. bobframes-0.2.8/bobframes/tests/data/golden_package/shared_redacted/_reports/index.html +44 -0
  71. bobframes-0.2.8/bobframes/tests/data/golden_package/shared_redacted/_reports/instancing_opportunities.html +46 -0
  72. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared_redacted/_reports/pass_gpu.html +3 -3
  73. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared_redacted/_reports/run/2026-05-27_r110565/draws_by_class.html +3 -4
  74. bobframes-0.2.8/bobframes/tests/data/golden_package/shared_redacted/_reports/run/2026-05-27_r110565/index.html +45 -0
  75. bobframes-0.2.8/bobframes/tests/data/golden_package/shared_redacted/_reports/run/2026-05-27_r110565/instancing_opportunities.html +43 -0
  76. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared_redacted/_reports/run/2026-05-27_r110565/overdraw.html +2 -3
  77. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared_redacted/_reports/run/2026-05-27_r110565/pass_gpu.html +1 -2
  78. bobframes-0.2.8/bobframes/tests/data/golden_package/shared_redacted/_reports/run/2026-05-27_r110565/shader_hotlist.html +43 -0
  79. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared_redacted/_reports/run/2026-05-27_r110565/summary.html +2 -3
  80. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared_redacted/_reports/summary.html +2 -2
  81. bobframes-0.2.8/bobframes/tests/data/golden_package/shared_redacted/_reports/trend_table.html +57 -0
  82. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden_preview/_chrome_preview.html +9 -2
  83. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/test_aggregates.py +69 -2
  84. bobframes-0.2.8/bobframes/tests/test_browser_shots.py +90 -0
  85. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/test_config.py +6 -1
  86. bobframes-0.2.8/bobframes/tests/test_draws_by_class_kpis.py +34 -0
  87. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/test_package.py +31 -5
  88. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/test_report_polish.py +3 -2
  89. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/test_report_structure.py +41 -10
  90. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/test_run_model.py +32 -1
  91. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/test_summary.py +6 -6
  92. bobframes-0.2.8/bobframes/tests/test_trend_regression_basis.py +103 -0
  93. bobframes-0.2.8/bobframes/tests/test_ui_ab_theme.py +162 -0
  94. bobframes-0.2.8/bobframes/tests/test_ui_browser.py +78 -0
  95. bobframes-0.2.8/bobframes/tests/test_ui_jobs.py +82 -0
  96. bobframes-0.2.8/bobframes/tests/test_ui_js_parses.py +34 -0
  97. bobframes-0.2.8/bobframes/tests/test_ui_progress.py +64 -0
  98. bobframes-0.2.8/bobframes/tests/test_ui_security.py +44 -0
  99. bobframes-0.2.8/bobframes/tests/test_ui_share.py +135 -0
  100. bobframes-0.2.8/bobframes/tests/test_ui_smoke.py +45 -0
  101. bobframes-0.2.8/bobframes/tests/test_ui_state.py +33 -0
  102. bobframes-0.2.8/bobframes/ui/__init__.py +17 -0
  103. bobframes-0.2.8/bobframes/ui/assets/panel.css +50 -0
  104. bobframes-0.2.8/bobframes/ui/assets/panel.js +151 -0
  105. bobframes-0.2.8/bobframes/ui/jobs.py +119 -0
  106. bobframes-0.2.8/bobframes/ui/progress.py +64 -0
  107. bobframes-0.2.8/bobframes/ui/server.py +513 -0
  108. bobframes-0.2.6/bobframes/_version.py +0 -1
  109. bobframes-0.2.6/bobframes/tests/data/golden_package/shared/_reports/index.html +0 -44
  110. bobframes-0.2.6/bobframes/tests/data/golden_package/shared/_reports/instancing_opportunities.html +0 -46
  111. bobframes-0.2.6/bobframes/tests/data/golden_package/shared/_reports/run/2026-05-27_r110565/index.html +0 -45
  112. bobframes-0.2.6/bobframes/tests/data/golden_package/shared/_reports/run/2026-05-27_r110565/instancing_opportunities.html +0 -44
  113. bobframes-0.2.6/bobframes/tests/data/golden_package/shared/_reports/run/2026-05-27_r110565/shader_hotlist.html +0 -44
  114. bobframes-0.2.6/bobframes/tests/data/golden_package/shared/_reports/trend_table.html +0 -57
  115. bobframes-0.2.6/bobframes/tests/data/golden_package/shared_redacted/_reports/index.html +0 -44
  116. bobframes-0.2.6/bobframes/tests/data/golden_package/shared_redacted/_reports/instancing_opportunities.html +0 -46
  117. bobframes-0.2.6/bobframes/tests/data/golden_package/shared_redacted/_reports/run/2026-05-27_r110565/index.html +0 -45
  118. bobframes-0.2.6/bobframes/tests/data/golden_package/shared_redacted/_reports/run/2026-05-27_r110565/instancing_opportunities.html +0 -44
  119. bobframes-0.2.6/bobframes/tests/data/golden_package/shared_redacted/_reports/run/2026-05-27_r110565/shader_hotlist.html +0 -44
  120. bobframes-0.2.6/bobframes/tests/data/golden_package/shared_redacted/_reports/trend_table.html +0 -57
  121. bobframes-0.2.6/bobframes/tests/test_browser_shots.py +0 -49
  122. {bobframes-0.2.6 → bobframes-0.2.8}/.gitignore +0 -0
  123. {bobframes-0.2.6 → bobframes-0.2.8}/LICENSE +0 -0
  124. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/__init__.py +0 -0
  125. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/catalog.py +0 -0
  126. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/derive_post_merge.py +0 -0
  127. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/derives/__init__.py +0 -0
  128. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/derives/classifier.py +0 -0
  129. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/derives/draw_classifier.toml +0 -0
  130. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/derives/pass_class_breakdown.py +0 -0
  131. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/derives/presets/custom-template.toml +0 -0
  132. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/derives/presets/godot.toml +0 -0
  133. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/derives/presets/unity.toml +0 -0
  134. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/derives/texture_usage.py +0 -0
  135. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/discovery.py +0 -0
  136. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/errors.py +0 -0
  137. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/global_entities.py +0 -0
  138. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/health.py +0 -0
  139. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/html/__init__.py +0 -0
  140. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/html/template.py +0 -0
  141. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/lint.py +0 -0
  142. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/lint_banlist.toml +0 -0
  143. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/manifest.py +0 -0
  144. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/parquetize.py +0 -0
  145. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/parsers/__init__.py +0 -0
  146. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/parsers/derive_program_transitions.py +0 -0
  147. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/parsers/parse_init_state.py +0 -0
  148. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/paths.py +0 -0
  149. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/probes/__init__.py +0 -0
  150. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/probes/whatif.py +0 -0
  151. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/qrd_harness.py +0 -0
  152. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/query_examples.py +0 -0
  153. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/rdcmd.py +0 -0
  154. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/replay/__init__.py +0 -0
  155. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/replay/replay_main.py +0 -0
  156. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/reports/__init__.py +0 -0
  157. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/reports/_tokens.py +0 -0
  158. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/reports/ab.py +0 -0
  159. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/reports/assets/Inter-OFL.txt +0 -0
  160. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/reports/assets/README.md +0 -0
  161. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/reports/assets/chrome.css +0 -0
  162. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/reports/assets/components.css +0 -0
  163. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/reports/assets/container.css +0 -0
  164. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/reports/assets/design_tokens.css +0 -0
  165. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/reports/assets/icon_sprite.html +0 -0
  166. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/reports/assets/inter-subset.woff2 +0 -0
  167. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/reports/assets/link_kind.css +0 -0
  168. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/reports/assets/per_drop.css +0 -0
  169. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/reports/assets/print.css +0 -0
  170. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/reports/assets/rdc_table.css +0 -0
  171. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/reports/assets/rdc_table.js +0 -0
  172. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/reports/assets/sticky.css +0 -0
  173. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/reports/base.py +0 -0
  174. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/reports/cache.py +0 -0
  175. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/reports/charts.py +0 -0
  176. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/reports/chrome.py +0 -0
  177. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/reports/cli.py +0 -0
  178. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/reports/delta.py +0 -0
  179. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/reports/design_tokens.toml +0 -0
  180. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/reports/formatters.py +0 -0
  181. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/resource_labels.py +0 -0
  182. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/run.py +0 -0
  183. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/schemas.py +0 -0
  184. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/stable_keys.py +0 -0
  185. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/__init__.py +0 -0
  186. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/_render_util.py +0 -0
  187. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden/_pagedata/catalog.js +0 -0
  188. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden/_reports/drill/District 01/2026-05-28_r110600/_pagedata/buffers.js +0 -0
  189. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden/_reports/drill/District 01/2026-05-28_r110600/_pagedata/clears.js +0 -0
  190. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden/_reports/drill/District 01/2026-05-28_r110600/_pagedata/counters_per_event.js +0 -0
  191. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden/_reports/drill/District 01/2026-05-28_r110600/_pagedata/descriptor_access.js +0 -0
  192. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden/_reports/drill/District 01/2026-05-28_r110600/_pagedata/dispatches.js +0 -0
  193. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden/_reports/drill/District 01/2026-05-28_r110600/_pagedata/draw_bindings.js +0 -0
  194. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden/_reports/drill/District 01/2026-05-28_r110600/_pagedata/draws.js +0 -0
  195. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden/_reports/drill/District 01/2026-05-28_r110600/_pagedata/events.js +0 -0
  196. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden/_reports/drill/District 01/2026-05-28_r110600/_pagedata/fbos.js +0 -0
  197. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden/_reports/drill/District 01/2026-05-28_r110600/_pagedata/frame_totals.js +0 -0
  198. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden/_reports/drill/District 01/2026-05-28_r110600/_pagedata/ibo_samples.js +0 -0
  199. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden/_reports/drill/District 01/2026-05-28_r110600/_pagedata/passes.js +0 -0
  200. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden/_reports/drill/District 01/2026-05-28_r110600/_pagedata/pixel_history.js +0 -0
  201. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden/_reports/drill/District 01/2026-05-28_r110600/_pagedata/post_vs_samples.js +0 -0
  202. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden/_reports/drill/District 01/2026-05-28_r110600/_pagedata/program_transitions.js +0 -0
  203. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden/_reports/drill/District 01/2026-05-28_r110600/_pagedata/programs.js +0 -0
  204. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden/_reports/drill/District 01/2026-05-28_r110600/_pagedata/render_targets.js +0 -0
  205. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden/_reports/drill/District 01/2026-05-28_r110600/_pagedata/resource_creation.js +0 -0
  206. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden/_reports/drill/District 01/2026-05-28_r110600/_pagedata/rt_event_timeline.js +0 -0
  207. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden/_reports/drill/District 01/2026-05-28_r110600/_pagedata/samplers.js +0 -0
  208. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden/_reports/drill/District 01/2026-05-28_r110600/_pagedata/shaders.js +0 -0
  209. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden/_reports/drill/District 01/2026-05-28_r110600/_pagedata/state_change_events.js +0 -0
  210. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden/_reports/drill/District 01/2026-05-28_r110600/_pagedata/texture_samples.js +0 -0
  211. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden/_reports/drill/District 01/2026-05-28_r110600/_pagedata/textures.js +0 -0
  212. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden/_reports/drill/District 01/2026-05-28_r110600/_pagedata/vbo_samples.js +0 -0
  213. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden/_reports/drill/District 01/2026-05-28_r110600/_pagedata/vertex_inputs.js +0 -0
  214. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden/_reports/drill/District 01/2026-05-28_r110600/index.html +0 -0
  215. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden/index.html +0 -0
  216. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden_package/redacted/README.txt +0 -0
  217. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden_package/redacted/_pagedata/catalog.js +0 -0
  218. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden_package/redacted/_reports/drill/District 01/2026-05-28_r110600/_pagedata/buffers.js +0 -0
  219. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden_package/redacted/_reports/drill/District 01/2026-05-28_r110600/_pagedata/clears.js +0 -0
  220. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden_package/redacted/_reports/drill/District 01/2026-05-28_r110600/_pagedata/counters_per_event.js +0 -0
  221. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden_package/redacted/_reports/drill/District 01/2026-05-28_r110600/_pagedata/descriptor_access.js +0 -0
  222. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden_package/redacted/_reports/drill/District 01/2026-05-28_r110600/_pagedata/dispatches.js +0 -0
  223. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden_package/redacted/_reports/drill/District 01/2026-05-28_r110600/_pagedata/draw_bindings.js +0 -0
  224. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden_package/redacted/_reports/drill/District 01/2026-05-28_r110600/_pagedata/draws.js +0 -0
  225. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden_package/redacted/_reports/drill/District 01/2026-05-28_r110600/_pagedata/events.js +0 -0
  226. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden_package/redacted/_reports/drill/District 01/2026-05-28_r110600/_pagedata/fbos.js +0 -0
  227. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden_package/redacted/_reports/drill/District 01/2026-05-28_r110600/_pagedata/frame_totals.js +0 -0
  228. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden_package/redacted/_reports/drill/District 01/2026-05-28_r110600/_pagedata/ibo_samples.js +0 -0
  229. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden_package/redacted/_reports/drill/District 01/2026-05-28_r110600/_pagedata/passes.js +0 -0
  230. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden_package/redacted/_reports/drill/District 01/2026-05-28_r110600/_pagedata/pixel_history.js +0 -0
  231. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden_package/redacted/_reports/drill/District 01/2026-05-28_r110600/_pagedata/post_vs_samples.js +0 -0
  232. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden_package/redacted/_reports/drill/District 01/2026-05-28_r110600/_pagedata/program_transitions.js +0 -0
  233. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden_package/redacted/_reports/drill/District 01/2026-05-28_r110600/_pagedata/programs.js +0 -0
  234. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden_package/redacted/_reports/drill/District 01/2026-05-28_r110600/_pagedata/render_targets.js +0 -0
  235. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden_package/redacted/_reports/drill/District 01/2026-05-28_r110600/_pagedata/resource_creation.js +0 -0
  236. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden_package/redacted/_reports/drill/District 01/2026-05-28_r110600/_pagedata/rt_event_timeline.js +0 -0
  237. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden_package/redacted/_reports/drill/District 01/2026-05-28_r110600/_pagedata/samplers.js +0 -0
  238. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden_package/redacted/_reports/drill/District 01/2026-05-28_r110600/_pagedata/shaders.js +0 -0
  239. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden_package/redacted/_reports/drill/District 01/2026-05-28_r110600/_pagedata/state_change_events.js +0 -0
  240. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden_package/redacted/_reports/drill/District 01/2026-05-28_r110600/_pagedata/texture_samples.js +0 -0
  241. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden_package/redacted/_reports/drill/District 01/2026-05-28_r110600/_pagedata/textures.js +0 -0
  242. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden_package/redacted/_reports/drill/District 01/2026-05-28_r110600/_pagedata/vbo_samples.js +0 -0
  243. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden_package/redacted/_reports/drill/District 01/2026-05-28_r110600/_pagedata/vertex_inputs.js +0 -0
  244. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden_package/redacted/_reports/drill/District 01/2026-05-28_r110600/index.html +0 -0
  245. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden_package/redacted/index.html +0 -0
  246. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared/README.txt +0 -0
  247. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared/_assets/catalog.css +0 -0
  248. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared/_assets/catalog.js +0 -0
  249. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared/_assets/report.css +0 -0
  250. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared/_pagedata/catalog.js +0 -0
  251. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared/_reports/drill/District 01/2026-05-28_r110600/_pagedata/buffers.js +0 -0
  252. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared/_reports/drill/District 01/2026-05-28_r110600/_pagedata/clears.js +0 -0
  253. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared/_reports/drill/District 01/2026-05-28_r110600/_pagedata/counters_per_event.js +0 -0
  254. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared/_reports/drill/District 01/2026-05-28_r110600/_pagedata/descriptor_access.js +0 -0
  255. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared/_reports/drill/District 01/2026-05-28_r110600/_pagedata/dispatches.js +0 -0
  256. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared/_reports/drill/District 01/2026-05-28_r110600/_pagedata/draw_bindings.js +0 -0
  257. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared/_reports/drill/District 01/2026-05-28_r110600/_pagedata/draws.js +0 -0
  258. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared/_reports/drill/District 01/2026-05-28_r110600/_pagedata/events.js +0 -0
  259. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared/_reports/drill/District 01/2026-05-28_r110600/_pagedata/fbos.js +0 -0
  260. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared/_reports/drill/District 01/2026-05-28_r110600/_pagedata/frame_totals.js +0 -0
  261. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared/_reports/drill/District 01/2026-05-28_r110600/_pagedata/ibo_samples.js +0 -0
  262. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared/_reports/drill/District 01/2026-05-28_r110600/_pagedata/passes.js +0 -0
  263. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared/_reports/drill/District 01/2026-05-28_r110600/_pagedata/pixel_history.js +0 -0
  264. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared/_reports/drill/District 01/2026-05-28_r110600/_pagedata/post_vs_samples.js +0 -0
  265. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared/_reports/drill/District 01/2026-05-28_r110600/_pagedata/program_transitions.js +0 -0
  266. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared/_reports/drill/District 01/2026-05-28_r110600/_pagedata/programs.js +0 -0
  267. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared/_reports/drill/District 01/2026-05-28_r110600/_pagedata/render_targets.js +0 -0
  268. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared/_reports/drill/District 01/2026-05-28_r110600/_pagedata/resource_creation.js +0 -0
  269. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared/_reports/drill/District 01/2026-05-28_r110600/_pagedata/rt_event_timeline.js +0 -0
  270. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared/_reports/drill/District 01/2026-05-28_r110600/_pagedata/samplers.js +0 -0
  271. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared/_reports/drill/District 01/2026-05-28_r110600/_pagedata/shaders.js +0 -0
  272. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared/_reports/drill/District 01/2026-05-28_r110600/_pagedata/state_change_events.js +0 -0
  273. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared/_reports/drill/District 01/2026-05-28_r110600/_pagedata/texture_samples.js +0 -0
  274. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared/_reports/drill/District 01/2026-05-28_r110600/_pagedata/textures.js +0 -0
  275. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared/_reports/drill/District 01/2026-05-28_r110600/_pagedata/vbo_samples.js +0 -0
  276. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared/_reports/drill/District 01/2026-05-28_r110600/_pagedata/vertex_inputs.js +0 -0
  277. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared/_reports/drill/District 01/2026-05-28_r110600/index.html +0 -0
  278. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared/_reports/overdraw.html +0 -0
  279. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared/_reports/shader_hotlist.html +0 -0
  280. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared/index.html +0 -0
  281. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared_redacted/README.txt +0 -0
  282. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared_redacted/_assets/catalog.css +0 -0
  283. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared_redacted/_assets/catalog.js +0 -0
  284. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared_redacted/_assets/report.css +0 -0
  285. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared_redacted/_pagedata/catalog.js +0 -0
  286. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared_redacted/_reports/drill/District 01/2026-05-28_r110600/_pagedata/buffers.js +0 -0
  287. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared_redacted/_reports/drill/District 01/2026-05-28_r110600/_pagedata/clears.js +0 -0
  288. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared_redacted/_reports/drill/District 01/2026-05-28_r110600/_pagedata/counters_per_event.js +0 -0
  289. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared_redacted/_reports/drill/District 01/2026-05-28_r110600/_pagedata/descriptor_access.js +0 -0
  290. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared_redacted/_reports/drill/District 01/2026-05-28_r110600/_pagedata/dispatches.js +0 -0
  291. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared_redacted/_reports/drill/District 01/2026-05-28_r110600/_pagedata/draw_bindings.js +0 -0
  292. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared_redacted/_reports/drill/District 01/2026-05-28_r110600/_pagedata/draws.js +0 -0
  293. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared_redacted/_reports/drill/District 01/2026-05-28_r110600/_pagedata/events.js +0 -0
  294. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared_redacted/_reports/drill/District 01/2026-05-28_r110600/_pagedata/fbos.js +0 -0
  295. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared_redacted/_reports/drill/District 01/2026-05-28_r110600/_pagedata/frame_totals.js +0 -0
  296. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared_redacted/_reports/drill/District 01/2026-05-28_r110600/_pagedata/ibo_samples.js +0 -0
  297. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared_redacted/_reports/drill/District 01/2026-05-28_r110600/_pagedata/passes.js +0 -0
  298. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared_redacted/_reports/drill/District 01/2026-05-28_r110600/_pagedata/pixel_history.js +0 -0
  299. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared_redacted/_reports/drill/District 01/2026-05-28_r110600/_pagedata/post_vs_samples.js +0 -0
  300. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared_redacted/_reports/drill/District 01/2026-05-28_r110600/_pagedata/program_transitions.js +0 -0
  301. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared_redacted/_reports/drill/District 01/2026-05-28_r110600/_pagedata/programs.js +0 -0
  302. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared_redacted/_reports/drill/District 01/2026-05-28_r110600/_pagedata/render_targets.js +0 -0
  303. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared_redacted/_reports/drill/District 01/2026-05-28_r110600/_pagedata/resource_creation.js +0 -0
  304. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared_redacted/_reports/drill/District 01/2026-05-28_r110600/_pagedata/rt_event_timeline.js +0 -0
  305. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared_redacted/_reports/drill/District 01/2026-05-28_r110600/_pagedata/samplers.js +0 -0
  306. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared_redacted/_reports/drill/District 01/2026-05-28_r110600/_pagedata/shaders.js +0 -0
  307. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared_redacted/_reports/drill/District 01/2026-05-28_r110600/_pagedata/state_change_events.js +0 -0
  308. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared_redacted/_reports/drill/District 01/2026-05-28_r110600/_pagedata/texture_samples.js +0 -0
  309. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared_redacted/_reports/drill/District 01/2026-05-28_r110600/_pagedata/textures.js +0 -0
  310. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared_redacted/_reports/drill/District 01/2026-05-28_r110600/_pagedata/vbo_samples.js +0 -0
  311. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared_redacted/_reports/drill/District 01/2026-05-28_r110600/_pagedata/vertex_inputs.js +0 -0
  312. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared_redacted/_reports/drill/District 01/2026-05-28_r110600/index.html +0 -0
  313. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared_redacted/_reports/overdraw.html +0 -0
  314. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared_redacted/_reports/shader_hotlist.html +0 -0
  315. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden_package/shared_redacted/index.html +0 -0
  316. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/golden_parquet/digests.json +0 -0
  317. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/_manifest.json +0 -0
  318. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/buffers.parquet +0 -0
  319. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/clears.parquet +0 -0
  320. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/counters_per_event.parquet +0 -0
  321. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/descriptor_access.parquet +0 -0
  322. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/dispatches.parquet +0 -0
  323. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/draw_bindings.parquet +0 -0
  324. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/draws.parquet +0 -0
  325. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/events.parquet +0 -0
  326. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/fbos.parquet +0 -0
  327. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/frame_totals.parquet +0 -0
  328. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/ibo_samples.parquet +0 -0
  329. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/indirect_args.parquet +0 -0
  330. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/passes.parquet +0 -0
  331. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/pixel_history.parquet +0 -0
  332. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/post_vs_samples.parquet +0 -0
  333. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/program_transitions.parquet +0 -0
  334. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/programs.parquet +0 -0
  335. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/render_targets.parquet +0 -0
  336. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/resource_creation.parquet +0 -0
  337. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/rt_event_timeline.parquet +0 -0
  338. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/samplers.parquet +0 -0
  339. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/shaders.parquet +0 -0
  340. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/state_change_events.parquet +0 -0
  341. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/texture_samples.parquet +0 -0
  342. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/textures.parquet +0 -0
  343. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/vbo_samples.parquet +0 -0
  344. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/synthetic/_data/District 01/2026-05-27_r110565/vertex_inputs.parquet +0 -0
  345. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/_manifest.json +0 -0
  346. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/buffers.parquet +0 -0
  347. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/clears.parquet +0 -0
  348. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/counters_per_event.parquet +0 -0
  349. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/descriptor_access.parquet +0 -0
  350. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/dispatches.parquet +0 -0
  351. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/draw_bindings.parquet +0 -0
  352. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/draws.parquet +0 -0
  353. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/events.parquet +0 -0
  354. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/fbos.parquet +0 -0
  355. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/frame_totals.parquet +0 -0
  356. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/ibo_samples.parquet +0 -0
  357. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/indirect_args.parquet +0 -0
  358. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/passes.parquet +0 -0
  359. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/pixel_history.parquet +0 -0
  360. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/post_vs_samples.parquet +0 -0
  361. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/program_transitions.parquet +0 -0
  362. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/programs.parquet +0 -0
  363. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/render_targets.parquet +0 -0
  364. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/resource_creation.parquet +0 -0
  365. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/rt_event_timeline.parquet +0 -0
  366. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/samplers.parquet +0 -0
  367. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/shaders.parquet +0 -0
  368. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/state_change_events.parquet +0 -0
  369. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/texture_samples.parquet +0 -0
  370. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/textures.parquet +0 -0
  371. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/vbo_samples.parquet +0 -0
  372. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/data/synthetic/_data/District 01/2026-05-28_r110600/vertex_inputs.parquet +0 -0
  373. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/make_golden.py +0 -0
  374. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/make_package_golden.py +0 -0
  375. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/make_parquet_golden.py +0 -0
  376. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/make_preview_golden.py +0 -0
  377. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/make_synthetic.py +0 -0
  378. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/smoke.py +0 -0
  379. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/test_assets.py +0 -0
  380. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/test_cache.py +0 -0
  381. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/test_catalog_backup_dedup.py +0 -0
  382. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/test_charts.py +0 -0
  383. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/test_classifier.py +0 -0
  384. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/test_components.py +0 -0
  385. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/test_contrast.py +0 -0
  386. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/test_delta.py +0 -0
  387. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/test_design_tokens.py +0 -0
  388. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/test_determinism.py +0 -0
  389. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/test_discovery.py +0 -0
  390. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/test_element_builder.py +0 -0
  391. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/test_fonts.py +0 -0
  392. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/test_hardening.py +0 -0
  393. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/test_head_assets.py +0 -0
  394. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/test_health.py +0 -0
  395. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/test_js_coupled_classes.py +0 -0
  396. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/test_manifest_guard.py +0 -0
  397. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/test_multicapture_normalize.py +0 -0
  398. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/test_parity.py +0 -0
  399. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/test_parquet_parity.py +0 -0
  400. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/test_parquetize.py +0 -0
  401. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/test_perf.py +0 -0
  402. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/test_replay_drift.py +0 -0
  403. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/test_schemas.py +0 -0
  404. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/test_schemas_unit.py +0 -0
  405. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/test_stable_keys.py +0 -0
  406. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/test_table_component.py +0 -0
  407. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/test_theme.py +0 -0
  408. {bobframes-0.2.6 → bobframes-0.2.8}/bobframes/tests/test_token_guard.py +0 -0
  409. {bobframes-0.2.6 → bobframes-0.2.8}/pyproject.toml +0 -0
@@ -7,6 +7,72 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.2.8] - 2026-06-24
11
+
12
+ A guided local-web control panel (`bobframes ui`) for QA and product teammates who are not comfortable
13
+ in a terminal. Zero new runtime dependency (stdlib `http.server` only) and no report-output change --
14
+ the panel drives the existing verbs and emits no report HTML, so the rendered HTML and parquet digests
15
+ stay byte-identical on the same captures. No schema change (still schema 3). `_version` 0.2.7 -> 0.2.8.
16
+
17
+ ### Added
18
+ - `bobframes ui`: a zero-dependency local-web control panel that drives the whole pipeline from a
19
+ browser (ADR-47). It detects the RenderDoc tools and discovers capture drops, runs ingest /
20
+ re-generate / package / A-B as subprocesses with live per-capture progress streamed over Server-Sent
21
+ Events, opens or serves the rendered report, applies accent theming to a re-render, and can scaffold a
22
+ convention-correct capture folder. Server-rendered HTML + vanilla JS -- no framework, no build step,
23
+ no client router (the hard governance line in ADR-47). Binds `127.0.0.1` only and gates every
24
+ `/api/*` call with a per-session token. Install with `pipx install bobframes`, then run `bobframes ui`.
25
+
26
+ ### Changed
27
+ - The static preview server body moved from `cli._cmd_serve` into a reusable `serve.make_server` /
28
+ `serve.serve_forever` so the panel's background serve and the `bobframes serve` verb share it; the
29
+ `serve` verb's behavior is unchanged.
30
+ - The control panel's JS and CSS are now served as static files (`bobframes/ui/assets/panel.{js,css}`,
31
+ at `GET /panel.js` / `GET /panel.css`) instead of being embedded in a Python string -- so the client
32
+ is a real file that `node --check`/lint can validate (no build step; still no framework or router).
33
+
34
+ ### Tests / CI
35
+ - Added an automated guard that the panel JS parses and runs: a `node --check` gate (a pytest test +
36
+ an unconditional CI step) and an opt-in `browser`-marked headless smoke that drives the live panel in
37
+ Chrome and asserts it populates -- closing the gap that let a broken `<script>` ship undetected.
38
+
39
+ ## [0.2.7] - 2026-06-24
40
+
41
+ An aggregation-consistency pass plus four report/sharing correctness fixes. Every KPI now names its
42
+ estimator and rate KPIs read on one per-frame basis, so the same metric agrees across the summary,
43
+ dashboard, and trend. No schema change (still schema 3); parquet digests stay byte-identical on the
44
+ same captures. `_version` 0.2.6 -> 0.2.7.
45
+
46
+ ### Changed
47
+ - One canonical aggregation policy (ADR-46): every rendered KPI label names its estimator precisely --
48
+ `Pooled mean`, `Mean (per area)`, `Median`, `Total` -- and never a bare "avg". Rate KPIs (GPU, draws)
49
+ read per-frame on a single basis, so the same area's GPU now reads identically across the summary,
50
+ dashboard, and trend (previously e.g. `0.0356`/frame on the summary vs a raw `0.178` total on the
51
+ dashboard with no bridge). A naming gate (`test_no_vague_estimator_labels`) fails the build on any
52
+ vague estimator word. (D-13..D-16, Q-10..Q-13)
53
+ - Regression detection unified on the per-frame basis with config-driven thresholds: `trend_table`
54
+ reads every KPI per frame, so a regression flag is capture-count-independent and agrees with the
55
+ build-health verdict (a 7-vs-5-capture run no longer reports a false +40%); the per-KPI thresholds
56
+ (`draws`/`vbo`/`ibo`/`program_switches`) move from code literals to the `[report]` config, with
57
+ defaults that reproduce the prior values. (H-41)
58
+ - `draws_by_class` ratios use `statistics.median` (was the upper-middle `sorted[n//2]`, an off-by-one
59
+ on even N).
60
+ - `aggregates.frame_counts` is the single owner of every per-(drop, area) frame count; when the
61
+ GPU/draws denominator and the entity-rate denominator legitimately differ (a capture that replayed
62
+ but exported partial entity rows), the divergence is logged as a WARNING rather than silently
63
+ normalized away.
64
+
65
+ ### Fixed
66
+ - Run-selector dropdown did nothing on every report: the component JS runs in `<head>`, so the custom
67
+ element upgraded before its child `<select>` had parsed; initialization now defers to
68
+ `DOMContentLoaded`. (R-20)
69
+ - Exported standalone one-pager carried dead tree-relative navigation -- a run dropdown pointing at
70
+ siblings not in the bundle, a breadcrumb, the dashboard link, and a "viewing an older run" banner.
71
+ The detached summary now strips them; the in-tree summary keeps its working navigation. (R-21)
72
+ - Older-run report pages showed cross-drop graphs and tables spanning all runs, including ones newer
73
+ than the page's own run. Cross-drop renderers now scope to run history (up to and including the
74
+ current run) while the run picker keeps the full list. (R-22)
75
+
10
76
  ## [0.2.6] - 2026-06-06
11
77
 
12
78
  A build-health one-pager, a shareable `package` bundle, a server-side component system, and a full visual
@@ -123,5 +189,9 @@ First standalone release. v1 is Windows-only (the replay stage drives `qrenderdo
123
189
  `python -m _analysis.*` entry points no longer work; switch to the `bobframes` commands (see the
124
190
  migration table in the README). This is a hard rename with no compatibility shim.
125
191
 
126
- [Unreleased]: https://github.com/altpsyche/bobframes/compare/v0.1.0...HEAD
192
+ [Unreleased]: https://github.com/altpsyche/bobframes/compare/v0.2.8...HEAD
193
+ [0.2.8]: https://github.com/altpsyche/bobframes/compare/v0.2.7...v0.2.8
194
+ [0.2.7]: https://github.com/altpsyche/bobframes/compare/v0.2.6...v0.2.7
195
+ [0.2.6]: https://github.com/altpsyche/bobframes/compare/v0.2.0...v0.2.6
196
+ [0.2.0]: https://github.com/altpsyche/bobframes/compare/v0.1.0...v0.2.0
127
197
  [0.1.0]: https://github.com/altpsyche/bobframes/releases/tag/v0.1.0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bobframes
3
- Version: 0.2.6
3
+ Version: 0.2.8
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
@@ -55,6 +55,21 @@ bobframes check
55
55
  `bobframes check` prints the resolved paths for `renderdoccmd` and `qrenderdoc` and exits non-zero if
56
56
  either is missing, so you can confirm the toolchain before a long ingest.
57
57
 
58
+ ## Guided mode (recommended for QA / product)
59
+
60
+ Not comfortable in a terminal? `bobframes ui` opens a local-web control panel in your browser that
61
+ drives the whole pipeline point-and-click: detect the RenderDoc tools, pick a capture folder, ingest
62
+ with live per-capture progress, then open / serve / package the report or compare two runs.
63
+
64
+ ```
65
+ pipx install bobframes
66
+ bobframes ui # opens http://127.0.0.1:8765 in your browser
67
+ ```
68
+
69
+ The panel binds `127.0.0.1` only and carries a one-time session token in the opened URL, so only your
70
+ own browser can drive it. It is stdlib-only -- one `pipx install` gives every feature, no extra setup --
71
+ and it emits no report files of its own; it just runs the same verbs as the CLI.
72
+
58
73
  ## Quickstart
59
74
 
60
75
  ```
@@ -78,6 +93,7 @@ time to rebuild the HTML from existing Parquet without re-replaying captures.
78
93
  | `lint <file>...` | Check HTML or markdown against the banlist. |
79
94
  | `check` | Print resolved tool paths; non-zero when a tool is missing. |
80
95
  | `serve [root] [--port 8000] [--bind 127.0.0.1]` | Static preview via the stdlib HTTP server. |
96
+ | `ui [root] [--port 8765] [--bind 127.0.0.1] [--no-open]` | Guided local-web control panel (ADR-47): ingest / render / package / A-B / open / serve / scaffold from a browser, with live progress. Localhost-bound + per-session token; emits no report output of its own. |
81
97
  | `package [root] [--inline] [--light] [--redact] [--redact-paths {strip,fail}] [--out PATH] [--run KEY] [--no-summary-file] [--stage]` | Bundle a rendered tree into a shareable `<project>-<rundate>-report.zip` + a standalone `<project>-<rundate>-summary.html`, both written OUTSIDE `<root>` (non-mutating). `--redact` scrubs device/host provenance + absolute paths for external sharing. |
82
98
  | `preview [root] [--accent OKLCH] [--accent-data OKLCH]` | Render the chrome gallery to `_reports/_chrome_preview.html`; no capture data needed. `--accent` previews a theme override before you commit it. |
83
99
  | `export-tokens [--format toml\|json\|css] [--theme-template]` | Print the design tokens to stdout in the chosen format. `--theme-template` emits a paste-ready `[theme]` block for `.bobframes.toml`. |
@@ -24,6 +24,21 @@ bobframes check
24
24
  `bobframes check` prints the resolved paths for `renderdoccmd` and `qrenderdoc` and exits non-zero if
25
25
  either is missing, so you can confirm the toolchain before a long ingest.
26
26
 
27
+ ## Guided mode (recommended for QA / product)
28
+
29
+ Not comfortable in a terminal? `bobframes ui` opens a local-web control panel in your browser that
30
+ drives the whole pipeline point-and-click: detect the RenderDoc tools, pick a capture folder, ingest
31
+ with live per-capture progress, then open / serve / package the report or compare two runs.
32
+
33
+ ```
34
+ pipx install bobframes
35
+ bobframes ui # opens http://127.0.0.1:8765 in your browser
36
+ ```
37
+
38
+ The panel binds `127.0.0.1` only and carries a one-time session token in the opened URL, so only your
39
+ own browser can drive it. It is stdlib-only -- one `pipx install` gives every feature, no extra setup --
40
+ and it emits no report files of its own; it just runs the same verbs as the CLI.
41
+
27
42
  ## Quickstart
28
43
 
29
44
  ```
@@ -47,6 +62,7 @@ time to rebuild the HTML from existing Parquet without re-replaying captures.
47
62
  | `lint <file>...` | Check HTML or markdown against the banlist. |
48
63
  | `check` | Print resolved tool paths; non-zero when a tool is missing. |
49
64
  | `serve [root] [--port 8000] [--bind 127.0.0.1]` | Static preview via the stdlib HTTP server. |
65
+ | `ui [root] [--port 8765] [--bind 127.0.0.1] [--no-open]` | Guided local-web control panel (ADR-47): ingest / render / package / A-B / open / serve / scaffold from a browser, with live progress. Localhost-bound + per-session token; emits no report output of its own. |
50
66
  | `package [root] [--inline] [--light] [--redact] [--redact-paths {strip,fail}] [--out PATH] [--run KEY] [--no-summary-file] [--stage]` | Bundle a rendered tree into a shareable `<project>-<rundate>-report.zip` + a standalone `<project>-<rundate>-summary.html`, both written OUTSIDE `<root>` (non-mutating). `--redact` scrubs device/host provenance + absolute paths for external sharing. |
51
67
  | `preview [root] [--accent OKLCH] [--accent-data OKLCH]` | Render the chrome gallery to `_reports/_chrome_preview.html`; no capture data needed. `--accent` previews a theme override before you commit it. |
52
68
  | `export-tokens [--format toml\|json\|css] [--theme-template]` | Print the design tokens to stdout in the chosen format. `--theme-template` emits a paste-ready `[theme]` block for `.bobframes.toml`. |
@@ -57,5 +57,9 @@ shader_complexity_high = 60.0 # fragment shaders at/above this complexity a
57
57
  overdraw_reject_warn_pct = 40.0 # RT sample-rejection % -> warn callout
58
58
  overdraw_reject_alarm_pct = 70.0 # RT sample-rejection % -> alarm callout
59
59
  instancing_repeat_min = 4 # min mesh repeat PER FRAME to flag an instancing candidate (c16v: repeat + shader cost are per-frame rates, normalized by the drop's captured-frame count)
60
- gpu_regression_pct = 10.0 # latest-vs-previous GPU increase % -> regression callout
60
+ gpu_regression_pct = 10.0 # latest-vs-previous GPU/frame increase % -> regression (per frame, capture-count-independent)
61
+ draws_regression_pct = 10.0 # H-41 — per-KPI trend regression thresholds (were baked in trend_table.KPIS literals)
62
+ vbo_regression_pct = 15.0 # " vertex-buffer bytes / frame
63
+ ibo_regression_pct = 15.0 # " index-buffer bytes / frame
64
+ program_switches_regression_pct = 20.0 # " program switches / frame
61
65
  max_prerendered_runs = 10 # c16f — cap on pre-rendered older-run pages (run selector)
@@ -0,0 +1 @@
1
+ __version__ = "0.2.8"
@@ -163,3 +163,69 @@ def shader_aggregates(root: str, drops: list, *, stage: str | None = None) -> Sh
163
163
  sa.cplx[k] = max(sa.cplx.get(k, 0.0), c_val)
164
164
  sa.stype[k] = stype
165
165
  return sa
166
+
167
+
168
+ # --- frame counts (D-15: the single owner of every per-(drop, area) frame count) ----------------
169
+
170
+ def _frame_total_captures(root: str, drops: list) -> dict:
171
+ """``{(drop_key, area): {captures}}`` from ``frame_totals.parquet`` -- the authoritative
172
+ captured-frame set (the distinct ``capture`` values present in the per-frame GPU/draws table that
173
+ ``dashboard._run_totals`` / ``_top_areas_gpu`` divide by). Read live per drop_dir (frame_totals has
174
+ no per-drop cache, mirroring the dashboard readers). If a frame_totals has no ``capture`` column,
175
+ each row is treated as one distinct frame (num_rows) so the count never under-reports."""
176
+ out: dict = defaultdict(set)
177
+ for d in drops:
178
+ for r in d.rows:
179
+ p = os.path.join(r.drop_dir, 'frame_totals.parquet')
180
+ if not os.path.exists(p):
181
+ continue
182
+ try:
183
+ names = set(papq.read_schema(p).names)
184
+ if 'capture' in names:
185
+ caps = papq.read_table(p, columns=['capture']).column('capture').to_pylist()
186
+ else:
187
+ caps = [f'__row_{i}' for i in range(papq.read_metadata(p).num_rows)]
188
+ except Exception:
189
+ continue
190
+ out[(drop_key(r.drop_date, r.drop_label), r.area)].update(caps)
191
+ return out
192
+
193
+
194
+ def frame_counts(root: str, drops: list) -> dict:
195
+ """The ONE owner of every per-(drop, area) frame count (D-15 / D-A4). Returns
196
+ ``{(drop_key, area): {'frames', 'draw_frames', 'shader_frames'}}`` where ``frames`` = distinct
197
+ captures in frame_totals (the GPU/draws-per-frame denominator) and ``draw_frames``/``shader_frames``
198
+ = distinct captures in the draws/shaders entity data (the c16v entity-rate denominators).
199
+
200
+ The three legitimately DIFFER when a capture replayed "ok" but exported no entity rows (the c16v
201
+ as-built, ADR-23): per-frame GPU divides by the frames that produced GPU data, while per-frame mesh/
202
+ shader rates divide by the frames that produced that entity -- both correct for their metric, on
203
+ different populations. ``frame_count_divergences`` surfaces the difference so the different-N
204
+ normalization is visible across reports rather than silent. Raw counts (no >=1 guard) so divergence
205
+ is detectable; the per-frame no-op guard stays in ``DrawAgg.frames`` / ``ShaderAgg.frames``.
206
+ """
207
+ da = draw_aggregates(root, drops)
208
+ sa = shader_aggregates(root, drops)
209
+ ft = _frame_total_captures(root, drops)
210
+ keys = set(ft) | set(da.captures) | set(sa.captures)
211
+ return {
212
+ k: {
213
+ 'frames': len(ft.get(k, ())),
214
+ 'draw_frames': len(da.captures.get(k, ())),
215
+ 'shader_frames': len(sa.captures.get(k, ())),
216
+ }
217
+ for k in keys
218
+ }
219
+
220
+
221
+ def frame_count_divergences(root: str, drops: list) -> list:
222
+ """``[(drop_key, area, frames, draw_frames, shader_frames), ...]`` for every (drop, area) where an
223
+ entity count that EXISTS (>0) differs from the frame_totals frame count -- i.e. a capture produced
224
+ GPU/draw data but not the matching entity rows (or vice versa). Sorted for deterministic logging.
225
+ Empty when every layer agrees (the uniform-capture case). Drives the render-time warning (D-15)."""
226
+ out = []
227
+ for (dk, area), c in sorted(frame_counts(root, drops).items()):
228
+ ftn, dfr, sfr = c['frames'], c['draw_frames'], c['shader_frames']
229
+ if (dfr and dfr != ftn) or (sfr and sfr != ftn):
230
+ out.append((dk, area, ftn, dfr, sfr))
231
+ return out
@@ -251,20 +251,19 @@ def _cmd_check(args: argparse.Namespace) -> int:
251
251
 
252
252
 
253
253
  def _cmd_serve(args: argparse.Namespace) -> int:
254
- import functools
255
- import http.server
256
- import socketserver
254
+ # The static preview server body lives in serve.serve_forever so the `ui` panel can reuse the
255
+ # same builder (serve.make_server) for its background click-to-serve (v028_3).
256
+ from . import serve
257
+ return serve.serve_forever(os.path.abspath(args.root), bind=args.bind, port=args.port)
257
258
 
258
- root = os.path.abspath(args.root)
259
- handler = functools.partial(http.server.SimpleHTTPRequestHandler, directory=root)
260
- log = logging.getLogger('bobframes')
261
- try:
262
- with socketserver.TCPServer((args.bind, args.port), handler) as httpd:
263
- log.info(f'serving {root} at http://{args.bind}:{args.port} (Ctrl+C to stop)')
264
- httpd.serve_forever()
265
- except KeyboardInterrupt:
266
- return 4
267
- return 0
259
+
260
+ def _cmd_ui(args: argparse.Namespace) -> int:
261
+ # ADR-47: a zero-dependency local-web control panel that DRIVES the verbs for non-terminal users.
262
+ # A frontend SURFACE above the verb taxonomy -- it emits no report HTML (golden gate untouched) and
263
+ # pulls no dep into core. Lazy-imported so a core install never loads the ui package.
264
+ from .ui import server
265
+ return server.serve(os.path.abspath(args.root), bind=args.bind, port=args.port,
266
+ open_browser=not args.no_open)
268
267
 
269
268
 
270
269
  def _cmd_package(args: argparse.Namespace) -> int:
@@ -380,6 +379,16 @@ def _build_parser() -> argparse.ArgumentParser:
380
379
  sp.add_argument('--bind', default='127.0.0.1')
381
380
  sp.set_defaults(func=_cmd_serve)
382
381
 
382
+ sp = sub.add_parser('ui', parents=[common],
383
+ help='guided local-web control panel (ingest/generate/package in a browser)')
384
+ sp.add_argument('root', nargs='?', default='.')
385
+ sp.add_argument('--port', type=int, default=8765)
386
+ sp.add_argument('--bind', default='127.0.0.1',
387
+ help='bind address; localhost only by design (ADR-47)')
388
+ sp.add_argument('--no-open', action='store_true',
389
+ help="don't auto-open the browser")
390
+ sp.set_defaults(func=_cmd_ui)
391
+
383
392
  sp = sub.add_parser('package', parents=[common],
384
393
  help='bundle a rendered tree into a shareable zip + standalone summary')
385
394
  sp.add_argument('root', nargs='?', default='.')
@@ -131,6 +131,12 @@ class ReportCfg: # c16 — report-presentation thresholds (
131
131
  overdraw_reject_alarm_pct: float = 70.0
132
132
  instancing_repeat_min: int = 4 # c16v — min mesh repeat PER FRAME (repeat + shader cost are per-frame rates)
133
133
  gpu_regression_pct: float = 10.0
134
+ # H-41 (v027_1) — per-KPI trend regression thresholds; were baked in `trend_table.KPIS` literals.
135
+ # Trend-only (health's verdict has only a gpu trajectory rule). Defaults reproduce the old literals.
136
+ draws_regression_pct: float = 10.0
137
+ vbo_regression_pct: float = 15.0
138
+ ibo_regression_pct: float = 15.0
139
+ program_switches_regression_pct: float = 20.0
134
140
  max_prerendered_runs: int = 10 # c16f — cap on pre-rendered older-run pages (per-run UX)
135
141
 
136
142
 
@@ -327,6 +333,10 @@ def _build_config(root: str | None) -> Config:
327
333
  overdraw_reject_alarm_pct=rpt.get('overdraw_reject_alarm_pct', 70.0),
328
334
  instancing_repeat_min=rpt.get('instancing_repeat_min', 4),
329
335
  gpu_regression_pct=rpt.get('gpu_regression_pct', 10.0),
336
+ draws_regression_pct=rpt.get('draws_regression_pct', 10.0),
337
+ vbo_regression_pct=rpt.get('vbo_regression_pct', 15.0),
338
+ ibo_regression_pct=rpt.get('ibo_regression_pct', 15.0),
339
+ program_switches_regression_pct=rpt.get('program_switches_regression_pct', 20.0),
330
340
  max_prerendered_runs=rpt.get('max_prerendered_runs', 10),
331
341
  ),
332
342
  theme=ThemeCfg(tokens=tuple(theme_tokens.items())) if theme_tokens else None,
@@ -217,6 +217,9 @@ def build(root: str, *, out: str | None = None, light: bool = False,
217
217
  shutil.rmtree(staging, ignore_errors=True)
218
218
 
219
219
  if summary_file:
220
+ # The standalone one-pager is a DETACHED single file -- strip the tree-relative navigation that
221
+ # cannot resolve beside a lone HTML (R-21), so it no longer carries a dead run dropdown.
222
+ summary_bytes = _detach_summary(summary_bytes)
220
223
  with open(summary_path, 'wb') as f:
221
224
  f.write(summary_bytes)
222
225
  else:
@@ -503,6 +506,30 @@ def _redact_text_files(entries: list[tuple[str, str]], *, mode: str) -> int:
503
506
  return n
504
507
 
505
508
 
509
+ # Tree-relative navigation that only resolves inside the rendered tree -- dead in a detached one-pager.
510
+ # Each is a cleanly-delimited, non-nesting element generated by the chrome builders.
511
+ _DETACH_RES = (
512
+ # the run selector: its options point to run/<key>/summary.html siblings not bundled with a lone file
513
+ re.compile(rb'<rdc-ab-picker><label for="rdc-run-select">.*?</rdc-ab-picker>', re.S),
514
+ re.compile(rb'<nav class="crumb">.*?</nav>', re.S), # breadcrumb (root catalog / dashboard)
515
+ re.compile(rb'<a class="sb-link"[^>]*>.*?</a>', re.S), # summary-bar "dashboard" link
516
+ # the "viewing an older run / go to newest" nav banner (older-run one-pagers); its link points to a
517
+ # sibling page absent beside a lone file. Anchored on its title so the health alarm-banner is kept.
518
+ re.compile(rb'<rdc-alarm-banner\b[^>]*>(?:(?!</rdc-alarm-banner>).)*?viewing an older run'
519
+ rb'(?:(?!</rdc-alarm-banner>).)*?</rdc-alarm-banner>', re.S),
520
+ )
521
+
522
+
523
+ def _detach_summary(html: bytes) -> bytes:
524
+ """Make the standalone one-pager genuinely self-contained (R-21): strip the tree-relative navigation
525
+ that cannot work in a LONE file -- the run selector (a silently-dead dropdown), the breadcrumb, and
526
+ the summary-bar dashboard link. The page's own content (verdict, KPIs, movement, by-area) is
527
+ self-contained and untouched; the in-tree `_reports/summary.html` keeps its working nav."""
528
+ for rx in _DETACH_RES:
529
+ html = rx.sub(b'', html)
530
+ return html
531
+
532
+
506
533
  def _redacted_summary_bytes(staging: str, summary_rel: str, *, newest: bool, target,
507
534
  build_ts: str, shared: bool, redact_paths: str) -> bytes:
508
535
  """The standalone one-pager for a redacted bundle: self-contained (INLINE) + redacted.
@@ -6,7 +6,19 @@
6
6
  connectedCallback(){
7
7
  if (this._rdcUp) return;
8
8
  this._rdcUp = true;
9
- try { this.init(); } catch(e) { console.error('rdc init error', this.tagName, e); }
9
+ const run = () => {
10
+ try { this.init(); } catch(e) { console.error('rdc init error', this.tagName, e); }
11
+ };
12
+ // The component JS rides in <head> (adjacent to the CSS), so a custom element upgrades as its
13
+ // START tag is parsed -- BEFORE its light-DOM children (e.g. the run <select>) exist. Inits that
14
+ // querySelector a child would see nothing and bail. Defer to DOMContentLoaded while the document
15
+ // is still parsing so children are present; run immediately for elements created after load.
16
+ // (Mirrors the _wireRowDrill deferral below.)
17
+ if (document.readyState === 'loading'){
18
+ document.addEventListener('DOMContentLoaded', run, { once: true });
19
+ } else {
20
+ run();
21
+ }
10
22
  }
11
23
  init(){}
12
24
  }
@@ -301,11 +301,11 @@ def _global_kpis(drops: list) -> list:
301
301
  avg_gpu_frame = (total_gpu / n_frames) if n_frames else 0.0
302
302
  avg_draws_frame = (total_draws / n_frames) if n_frames else 0.0
303
303
  return [
304
- {'label': 'total gpu (s)', 'value': base.fmt_float(total_gpu, 3)},
305
- {'label': 'avg gpu / frame (s)', 'value': base.fmt_float(avg_gpu_frame, 4)},
306
- {'label': 'total draws', 'value': base.fmt_int(total_draws)},
307
- {'label': 'avg draws / frame', 'value': base.fmt_int(round(avg_draws_frame))},
308
- {'label': 'areas', 'value': base.fmt_int(len(areas))},
304
+ {'label': 'pooled mean gpu / frame (s)', 'value': base.fmt_float(avg_gpu_frame, 4)},
305
+ {'label': 'total gpu (s) over captures', 'value': base.fmt_float(total_gpu, 3)},
306
+ {'label': 'pooled mean draws / frame', 'value': base.fmt_int(round(avg_draws_frame))},
307
+ {'label': 'total draws over captures', 'value': base.fmt_int(total_draws)},
308
+ {'label': 'areas', 'value': base.fmt_int(len(areas))},
309
309
  ]
310
310
 
311
311
 
@@ -351,6 +351,9 @@ def build(root: str, *, drops: list | None = None, ab=None,
351
351
  rc = base.run_context(drops, run_label=run_label, run_date=run_date)
352
352
  cur = rc.current
353
353
  cur_drops = [cur] if cur else []
354
+ # v027_4: per-area captured-frame counts (the single owner, D-15) so the mini cards can read
355
+ # per-frame and stay consistent with summary + the trend card (no bare cross-capture totals).
356
+ area_frames = {a: c['frames'] for (_dk, a), c in _agg.frame_counts(root, cur_drops).items()}
354
357
 
355
358
  # c16f: a per-run dashboard lives at _reports/run/<key>/index.html; its sibling reports (instancing
356
359
  # etc.) are pre-rendered there too, so their links stay bare. trend_table is NOT per-run (it is the
@@ -398,22 +401,23 @@ def build(root: str, *, drops: list | None = None, ab=None,
398
401
  *[base.el('a', {'href': href, 'data-link-kind': 'primary'}, lbl)
399
402
  for href, lbl in _NAV]))
400
403
 
401
- # Card: trend table - GPU time per area (mini bars matching the trend flagship).
404
+ # Card: trend table - GPU PER FRAME per area (D-16: leads with the per-frame mean so the same area
405
+ # reads the SAME number here and on summary; the raw run total rides alongside as the labeled bridge).
402
406
  top_a = top_a[:3]
403
407
  chart_tt = base.figure(base.bar_chart(
404
- [(a, g) for a, g, *_ in top_a], value_fmt=lambda v: f'{v:.3f}', width=280,
405
- title='gpu (s) per area', desc='top areas by GPU seconds', chart_id='dash-tt'))
408
+ [(t[0], t[4]) for t in top_a], value_fmt=lambda v: f'{v:.4f}', width=280,
409
+ title='gpu / frame per area', desc='top areas by GPU per captured frame', chart_id='dash-tt'))
406
410
  body_tt = _card_table(
407
411
  top_a,
408
412
  [
409
413
  ('area', lambda r: r[0], False),
410
- ('gpu (s)', lambda r: base.fmt_float(r[1], 3), True),
411
- ('draws', lambda r: base.fmt_int(r[2]), True),
412
- ('avg draws / frame', lambda r: base.fmt_int(round(r[3])), True),
414
+ ('mean gpu / frame', lambda r: base.fmt_float(r[4], 4), True),
415
+ ('total gpu (s)', lambda r: base.fmt_float(r[1], 3), True),
416
+ ('mean draws / frame', lambda r: base.fmt_int(round(r[3])), True),
413
417
  ],
414
- caption='per-area GPU + draw load (avg draws per captured frame)')
415
- sub_tt = ('GPU time per area in the current run.'
416
- + (f' worst: {top_a[0][0]} {base.fmt_float(top_a[0][1], 3)}s' if top_a else ''))
418
+ caption='per-area GPU per captured frame + the run total (mean = area total / area frames)')
419
+ sub_tt = ('GPU per frame per area in the current run.'
420
+ + (f' worst: {top_a[0][0]} {base.fmt_float(top_a[0][4], 4)}s / frame' if top_a else ''))
417
421
  cards.append(_card(tt_href, 'trend table', sub_tt, chart_tt, body_tt))
418
422
 
419
423
  # Card: instancing - repeat per mesh (mini bars).
@@ -426,7 +430,7 @@ def build(root: str, *, drops: list | None = None, ab=None,
426
430
  [
427
431
  ('mesh', lambda r: r[0], False),
428
432
  ('repeat', lambda r: base.fmt_int(r[1]), True),
429
- ('indices typ', lambda r: base.fmt_int(r[2]), True),
433
+ ('median verts', lambda r: base.fmt_int(r[2]), True),
430
434
  ],
431
435
  caption='most-repeated meshes')
432
436
  sub_im = ('Repeated meshes worth instancing or batching.'
@@ -438,7 +442,7 @@ def build(root: str, *, drops: list | None = None, ab=None,
438
442
  top_p = _top_passes(cur_drops)
439
443
  chart_pg = base.figure(base.bar_chart(
440
444
  [(pl, g) for _, pl, g in top_p], value_fmt=lambda v: f'{v:.3f}', width=280,
441
- title='gpu (s) per pass', desc='heaviest passes by GPU seconds', chart_id='dash-pg'))
445
+ title='total gpu (s) per pass', desc='heaviest passes by total GPU seconds', chart_id='dash-pg'))
442
446
  body_pg = _card_table(
443
447
  top_p,
444
448
  [
@@ -447,9 +451,10 @@ def build(root: str, *, drops: list | None = None, ab=None,
447
451
  # so hover could never reveal it). The CSS clip now truncates the display + the td title=
448
452
  # reveals the full value on hover - consistent with every other mini text column.
449
453
  ('marker', lambda r: base.scrub_chrome_text(r[1]), False),
450
- ('gpu (s)', lambda r: base.fmt_float(r[2], 3), True),
454
+ # per-pass GPU is a cross-capture total (the pass report's native basis); labeled as such.
455
+ ('total gpu (s)', lambda r: base.fmt_float(r[2], 3), True),
451
456
  ],
452
- caption='heaviest passes by GPU time')
457
+ caption='heaviest passes by total GPU time')
453
458
  sub_pg = ('Heaviest GPU passes.'
454
459
  + (f' top: {top_p[0][1]} {base.fmt_float(top_p[0][2], 3)}s in {top_p[0][0]}'
455
460
  if top_p else ''))
@@ -503,10 +508,13 @@ def build(root: str, *, drops: list | None = None, ab=None,
503
508
  pa_rows,
504
509
  [
505
510
  ('area', lambda r: r[0], False),
506
- ('draws', lambda r: base.fmt_int(r[1]['n_draws']), True),
511
+ # per-frame to match summary's By-area + the trend card (D-16 consistency), not a bare total.
512
+ ('mean draws / frame',
513
+ lambda r: base.fmt_int(round(base.per_frame(r[1]['n_draws'], area_frames.get(r[0], 0)))),
514
+ True),
507
515
  ('dominant', lambda r: r[1]['dominant_class'], False),
508
516
  ],
509
- caption='top areas by draw count')
517
+ caption='top areas by mean draws per captured frame')
510
518
  dom = class_totals.most_common(1)[0] if class_totals else None
511
519
  sub_dc = ('Draw mix by class.'
512
520
  + (f' dominant: {dom[0]} {base.fmt_pct(100.0 * dom[1] / grand)}'
@@ -182,6 +182,15 @@ class RunContext:
182
182
  def is_newest(self) -> bool:
183
183
  return bool(self.current) and self.index == self.n_runs - 1
184
184
 
185
+ @property
186
+ def history(self) -> list:
187
+ """Drops up to AND INCLUDING the current run (chronological) -- the data scope for cross-drop
188
+ CHARTS/TABLES on a per-run page, so an OLDER run's page never shows data for runs that came
189
+ AFTER it (ADR-35; R-22). The run picker still lists every run (from `drops`); only the rendered
190
+ data is scoped. Equals `drops` on the newest page (index == n_runs - 1)."""
191
+ i = self.index
192
+ return list(self.drops) if i < 0 else self.drops[:i + 1]
193
+
185
194
  @property
186
195
  def run_label(self) -> str:
187
196
  """current.key (e.g. '2026-06-01_r110788'); '' when there is no current run."""
@@ -7,12 +7,14 @@ Segments colored by draw_class. Below: raw-count table.
7
7
  from __future__ import annotations
8
8
 
9
9
  import os
10
+ import statistics
10
11
  import sys
11
12
  from collections import Counter, defaultdict
12
13
 
13
14
  import pyarrow.parquet as papq
14
15
 
15
16
  from . import base
17
+ from .. import aggregates as _agg
16
18
 
17
19
 
18
20
  def _gather_from_drops(drops: list) -> tuple[dict, list, list, int]:
@@ -79,8 +81,11 @@ def _build_table(counts: dict, drop_keys: list) -> str:
79
81
  caption='raw draw counts per class, per area and drop')
80
82
 
81
83
 
82
- def _compute_kpis(counts: dict, areas: list) -> list:
83
- """Hero KPIs: total draws, n areas, median prepass/opaque ratio, dominant class."""
84
+ def _compute_kpis(counts: dict, areas: list, total_frames: int) -> list:
85
+ """Hero KPIs (current run): mean draws / frame + the labeled raw total, n areas, the MEDIAN
86
+ prepass/opaque ratio across areas, dominant class. Q-12: lead with the per-frame mean (the
87
+ capture-count-independent number) and label the total. Q-11: `statistics.median` (not the
88
+ upper-middle `sorted[n//2]`), and the label names the estimator + population."""
84
89
  total = sum(sum(cc.values()) for cc in counts.values())
85
90
  ratios = []
86
91
  class_totals: Counter = Counter()
@@ -90,14 +95,15 @@ def _compute_kpis(counts: dict, areas: list) -> list:
90
95
  op = cc.get('opaque', 0)
91
96
  if op:
92
97
  ratios.append(cc.get('prepass', 0) / op)
93
- median_ratio = (sorted(ratios)[len(ratios) // 2]
94
- if ratios else 0.0)
98
+ median_ratio = statistics.median(ratios) if ratios else 0.0
95
99
  dominant_cls = (class_totals.most_common(1)[0][0]
96
100
  if class_totals else '')
101
+ mean_draws = base.per_frame(total, total_frames) # per frame (no-op when frames<=1)
97
102
  return [
98
- {'label': 'total draws', 'value': base.fmt_int(total)},
103
+ {'label': 'mean draws / frame', 'value': base.fmt_int(round(mean_draws))},
104
+ {'label': 'total draws over captures', 'value': base.fmt_int(total)},
99
105
  {'label': 'areas', 'value': base.fmt_int(len(areas))},
100
- {'label': 'prepass/opaque (med)', 'value': base.fmt_float(median_ratio, 2)},
106
+ {'label': 'median prepass / opaque (across areas)', 'value': base.fmt_float(median_ratio, 2)},
101
107
  {'label': 'dominant class', 'value': dominant_cls or '-'},
102
108
  ]
103
109
 
@@ -112,9 +118,12 @@ def build(root: str, *, drops: list | None = None, ab=None,
112
118
  # table below keeps every run as the breakdown / comparison view.
113
119
  rc = base.run_context(drops, run_label=run_label, run_date=run_date)
114
120
  cur = rc.current
121
+ drops = rc.history # R-22: cross-drop data scoped to <= current (picker still lists all via rc)
115
122
  counts, areas, drop_keys, total_captures = _gather_from_drops(drops)
116
123
  cur_counts = {k: cc for k, cc in counts.items() if cur and k[1] == cur.key}
117
124
  cur_areas = cur.areas if cur else []
125
+ # Q-12: per-frame mean draws needs the current run's captured-frame count (the single owner, D-15).
126
+ cur_frames = sum(v['frames'] for v in (_agg.frame_counts(root, [cur]) if cur else {}).values())
118
127
 
119
128
  parts = []
120
129
 
@@ -183,7 +192,7 @@ def build(root: str, *, drops: list | None = None, ab=None,
183
192
  drops=len(drops), captures=total_captures, build_ts=build_ts or base.now_iso(),
184
193
  crumb_depth=base.crumb_depth(ab, run=rc), ab=ab, root=root, report_key='draws_by_class',
185
194
  sink=sink, theme=theme,
186
- kpis=_compute_kpis(cur_counts, cur_areas), run=rc,
195
+ kpis=_compute_kpis(cur_counts, cur_areas, cur_frames), run=rc,
187
196
  device=base.provenance_strip(*base.newest_drop_provenance(root, [cur] if cur else []), redact=redact))])
188
197
 
189
198
 
@@ -71,6 +71,7 @@ def build(root: str, *, drops: list | None = None, ab=None,
71
71
  # Run model (ADR-35): live candidates are meshes drawn in the CURRENT run; prior runs supply the
72
72
  # per-drop comparison columns + the resolved-since section, never inflate the live list.
73
73
  rc = base.run_context(drops, run_label=run_label, run_date=run_date)
74
+ drops = rc.history # R-22: cross-drop trend scoped to <= current (picker still lists all via rc)
74
75
  cur = rc.current
75
76
  bl = rc.baseline
76
77
  ck = cur.key if cur else None
@@ -220,7 +221,7 @@ def build(root: str, *, drops: list | None = None, ab=None,
220
221
  if chart_items:
221
222
  mbody.append(base.figure(
222
223
  base.bar_chart(chart_items, title='estimated wasted indices',
223
- desc='(max repeat - 1) x typical indices, per mesh',
224
+ desc='(max repeat - 1) x median indices, per mesh',
224
225
  chart_id='inst-wasted'),
225
226
  'estimated wasted indices (top meshes)'))
226
227
 
@@ -237,9 +238,9 @@ def build(root: str, *, drops: list | None = None, ab=None,
237
238
  cols += [
238
239
  base.Column('areas', 'areas', clip='default'),
239
240
  base.Column('dompass', 'dominant pass', clip='narrow'),
240
- base.Column('ityp', 'indices typical', numeric=True),
241
+ base.Column('ityp', 'median indices', numeric=True),
241
242
  base.Column('wasted', 'wasted indices', numeric=True,
242
- title='(max repeat - 1) x typical indices: index data re-submitted across repeats',
243
+ title='(max repeat - 1) x median indices: index data re-submitted across repeats',
243
244
  render=lambda value, row: base.heatmap_cell(value, 0, row['_maxw'],
244
245
  text=base.fmt_int(value))),
245
246
  ]
@@ -4,6 +4,7 @@ from __future__ import annotations
4
4
 
5
5
  import time
6
6
 
7
+ from .. import aggregates as _agg
7
8
  from .. import lint
8
9
  from ..config import get_config
9
10
  from ..html import template
@@ -43,6 +44,16 @@ def render_all_reports(root: str, log, *,
43
44
  cache_out = reports_base.build_per_drop_cache(root)
44
45
  log(f' built per-drop cache: {cache_out} ({time.monotonic()-t0:.1f}s)')
45
46
 
47
+ # D-15 (D-A4): one owner of every per-(drop, area) frame count. Per-frame GPU/draws divide by the
48
+ # frame_totals frame count; per-frame mesh/shader rates divide by the entity-capture count (c16v).
49
+ # They legitimately differ when a capture replayed "ok" but exported no entity rows -- surface that
50
+ # so cross-report per-frame normalization uses different N visibly, not silently (ADR-23, warn not
51
+ # assert: the divergence is a real data condition, present even in the synthetic fixture).
52
+ for dk, area, ftn, dfr, sfr in _agg.frame_count_divergences(root, reports_base.discover_drops(root)):
53
+ log(f' WARNING: frame-count divergence {area} {dk}: frame_totals={ftn} draws={dfr} '
54
+ f'shaders={sfr} -- per-frame GPU/draws and per-frame entity rates use different '
55
+ f'denominators (a capture replayed but exported partial entity data)')
56
+
46
57
  for mod in all_reports():
47
58
  try:
48
59
  rep = mod.build(root, sink=sink, build_ts=build_ts, redact=redact, theme=theme)