@zaip-ai/cli 1.3.0 → 1.3.2

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 (258) hide show
  1. package/README.md +3 -2
  2. package/dist/services/agent-adapters.js +1 -1
  3. package/dist/services/agent-adapters.js.map +1 -1
  4. package/dist/services/agent-adapters.test.js +2 -1
  5. package/dist/services/agent-adapters.test.js.map +1 -1
  6. package/dist/services/codex-pre-tool-hook-template.d.ts.map +1 -1
  7. package/dist/services/codex-pre-tool-hook-template.js +89 -7
  8. package/dist/services/codex-pre-tool-hook-template.js.map +1 -1
  9. package/dist/services/codex-rules.d.ts +1 -1
  10. package/dist/services/codex-rules.d.ts.map +1 -1
  11. package/dist/services/codex-rules.js +7 -0
  12. package/dist/services/codex-rules.js.map +1 -1
  13. package/dist/services/install-agents-template.d.ts.map +1 -1
  14. package/dist/services/install-agents-template.js +1 -0
  15. package/dist/services/install-agents-template.js.map +1 -1
  16. package/dist/services/runtime-template.test.js +31 -7
  17. package/dist/services/runtime-template.test.js.map +1 -1
  18. package/dist/services/template-manifest.d.ts.map +1 -1
  19. package/dist/services/template-manifest.js +7 -3
  20. package/dist/services/template-manifest.js.map +1 -1
  21. package/package.json +1 -1
  22. package/templates/.agents/config/agents/catalog.yaml +7 -7
  23. package/templates/.agents/config/clients/codex/agents/zaip-agent-backend.toml +2 -2
  24. package/templates/.agents/config/clients/codex/agents/zaip-agent-designer.toml +2 -2
  25. package/templates/.agents/config/clients/codex/agents/zaip-agent-frontend.toml +8 -8
  26. package/templates/.agents/dashboard/.zaip-managed.json +373 -205
  27. package/templates/.agents/dashboard/apps/frontend/dist/assets/{CostCalculationGuidePage-DxR_pgSV.js → CostCalculationGuidePage-ovxurkIk.js} +1 -1
  28. package/templates/.agents/dashboard/apps/frontend/dist/assets/{DashboardHelpDialog-4jZLTWE_.js → DashboardHelpDialog-BXy6nti1.js} +268 -86
  29. package/templates/.agents/dashboard/apps/frontend/dist/assets/{PricingSettingsDialog-CKcsqb-r.js → PricingSettingsDialog-vSvvOHQp.js} +1 -1
  30. package/templates/.agents/dashboard/apps/frontend/dist/assets/{ProjectSettingsPage-DmmJD2Th.js → ProjectSettingsPage-zH758vfs.js} +1 -1
  31. package/templates/.agents/dashboard/apps/frontend/dist/assets/TraceabilityWorkspace-CTH2wDs5.js +1 -0
  32. package/templates/.agents/dashboard/apps/frontend/dist/assets/{TraceabilityWorkspace-CaKwQP43.css → TraceabilityWorkspace-DvvavDLY.css} +1 -1
  33. package/templates/.agents/dashboard/apps/frontend/dist/assets/cost-monitor-DKwP1stV.js +68 -0
  34. package/templates/.agents/dashboard/apps/frontend/dist/assets/cost-monitor-PoJA2PI1.css +1 -0
  35. package/templates/.agents/dashboard/apps/frontend/dist/assets/{icons-vendor-C0fj2uU9.js → icons-vendor-B_lDDHn1.js} +1 -1
  36. package/templates/.agents/dashboard/apps/frontend/dist/assets/index-B31GMG5O.css +1 -0
  37. package/templates/.agents/dashboard/apps/frontend/dist/assets/index-CuMcoUni.js +2 -0
  38. package/templates/.agents/dashboard/apps/frontend/dist/assets/{react-vendor-BEsJ-FYH.js → react-vendor-B3TJrD4O.js} +1 -1
  39. package/templates/.agents/dashboard/apps/frontend/dist/assets/{traceability-B6y6Gw12.js → traceability-7f1j5wx3.js} +1 -1
  40. package/templates/.agents/dashboard/apps/frontend/dist/index.html +7 -7
  41. package/templates/.agents/dashboard/apps/frontend/playwright.dom.config.ts +32 -0
  42. package/templates/.agents/dashboard/apps/frontend/src/App.tsx +132 -245
  43. package/templates/.agents/dashboard/apps/frontend/src/AppOverlays.tsx +0 -21
  44. package/templates/.agents/dashboard/apps/frontend/src/DashboardAlternateContent.tsx +9 -0
  45. package/templates/.agents/dashboard/apps/frontend/src/TraceabilityWorkspace.dom.e2e.ts +56 -0
  46. package/templates/.agents/dashboard/apps/frontend/src/TraceabilityWorkspace.test.ts +18 -1
  47. package/templates/.agents/dashboard/apps/frontend/src/TraceabilityWorkspace.tsx +75 -8
  48. package/templates/.agents/dashboard/apps/frontend/src/dashboard-api.test.ts +25 -1
  49. package/templates/.agents/dashboard/apps/frontend/src/dashboard-api.ts +7 -9
  50. package/templates/.agents/dashboard/apps/frontend/src/operations-period-render.test.ts +4 -4
  51. package/templates/.agents/dashboard/apps/frontend/src/overview-status-page.test.ts +1 -1
  52. package/templates/.agents/dashboard/apps/frontend/src/snapshot-banner-semantics.test.ts +7 -0
  53. package/templates/.agents/dashboard/apps/frontend/src/snapshot-coherence.test.ts +20 -8
  54. package/templates/.agents/dashboard/apps/frontend/src/sqlite-page-export.test.ts +32 -0
  55. package/templates/.agents/dashboard/apps/frontend/src/sqlite-page-export.ts +21 -0
  56. package/templates/.agents/dashboard/apps/frontend/src/styles.css +19 -0
  57. package/templates/.agents/dashboard/apps/frontend/src/traceability-task-pagination.test.ts +32 -0
  58. package/templates/.agents/dashboard/apps/frontend/src/traceability-task-pagination.ts +36 -0
  59. package/templates/.agents/dashboard/apps/frontend/src/traceability-workspace.css +6 -2
  60. package/templates/.agents/dashboard/apps/frontend/src/use-app-cost-loaders.test.ts +48 -0
  61. package/templates/.agents/dashboard/apps/frontend/src/use-app-cost-loaders.ts +31 -27
  62. package/templates/.agents/dashboard/apps/frontend/src/use-app-trace-documents.ts +7 -1
  63. package/templates/.agents/dashboard/apps/frontend/src/use-app-work-list-page.test.ts +163 -0
  64. package/templates/.agents/dashboard/apps/frontend/src/use-app-work-list-page.ts +136 -0
  65. package/templates/.agents/dashboard/apps/frontend/src/use-operations-detail-export.ts +76 -0
  66. package/templates/.agents/dashboard/apps/frontend/src/work-list-load-dedup.test.ts +23 -0
  67. package/templates/.agents/dashboard/apps/frontend/src/work-list-load-dedup.ts +27 -0
  68. package/templates/.agents/dashboard/apps/frontend/src/work-list-pagination.test.ts +38 -14
  69. package/templates/.agents/dashboard/modules/cost-monitor/src/react/CostDeveloperAttributionPanel.tsx +60 -0
  70. package/templates/.agents/dashboard/modules/cost-monitor/src/react/CostDeveloperAttributionTable.tsx +191 -0
  71. package/templates/.agents/dashboard/modules/cost-monitor/src/react/CostFilters.loading.test.ts +14 -0
  72. package/templates/.agents/dashboard/modules/cost-monitor/src/react/CostFilters.run-page.test.ts +18 -0
  73. package/templates/.agents/dashboard/modules/cost-monitor/src/react/CostFilters.tsx +18 -3
  74. package/templates/.agents/dashboard/modules/cost-monitor/src/react/CostMonitorContent.tsx +8 -2
  75. package/templates/.agents/dashboard/modules/cost-monitor/src/react/CostMonitorLoadErrors.tsx +35 -0
  76. package/templates/.agents/dashboard/modules/cost-monitor/src/react/CostMonitorPage.test.ts +47 -1
  77. package/templates/.agents/dashboard/modules/cost-monitor/src/react/CostMonitorPage.tsx +50 -58
  78. package/templates/.agents/dashboard/modules/cost-monitor/src/react/CostTableLoading.test.ts +132 -0
  79. package/templates/.agents/dashboard/modules/cost-monitor/src/react/CostTableLoading.tsx +70 -0
  80. package/templates/.agents/dashboard/modules/cost-monitor/src/react/CostTables.test.ts +128 -138
  81. package/templates/.agents/dashboard/modules/cost-monitor/src/react/CostTables.tsx +17 -67
  82. package/templates/.agents/dashboard/modules/cost-monitor/src/react/cost-monitor-copy.ts +2 -0
  83. package/templates/.agents/dashboard/modules/cost-monitor/src/react/cost-monitor-views.ts +3 -1
  84. package/templates/.agents/dashboard/modules/cost-monitor/src/react/cost-page-range.ts +35 -0
  85. package/templates/.agents/dashboard/modules/cost-monitor/src/react/use-compact-cost-page.ts +16 -4
  86. package/templates/.agents/dashboard/modules/cost-monitor/src/react/use-cost-group-page.test.ts +37 -2
  87. package/templates/.agents/dashboard/modules/cost-monitor/src/react/use-cost-group-page.ts +44 -8
  88. package/templates/.agents/dashboard/modules/cost-monitor/src/react/use-cost-run-page.ts +6 -1
  89. package/templates/.agents/dashboard/modules/operations/src/contracts.ts +20 -3
  90. package/templates/.agents/dashboard/modules/operations/src/react/DeveloperAttributionPanel.test.ts +54 -0
  91. package/templates/.agents/dashboard/modules/operations/src/react/DeveloperAttributionPanel.tsx +247 -0
  92. package/templates/.agents/dashboard/modules/operations/src/react/OperationsMainContent.tsx +39 -13
  93. package/templates/.agents/dashboard/modules/operations/src/react/OperationsOverview.helpers.tsx +107 -2
  94. package/templates/.agents/dashboard/modules/operations/src/react/OperationsOverview.macro-metrics.test.ts +55 -0
  95. package/templates/.agents/dashboard/modules/operations/src/react/OperationsOverview.test.ts +20 -28
  96. package/templates/.agents/dashboard/modules/operations/src/react/OperationsOverview.tsx +162 -165
  97. package/templates/.agents/dashboard/modules/operations/src/react/OperationsOverviewTopPanel.tsx +127 -0
  98. package/templates/.agents/dashboard/modules/operations/src/react/OperationsPage.tsx +13 -46
  99. package/templates/.agents/dashboard/modules/operations/src/react/OperationsPageOverview.tsx +3 -0
  100. package/templates/.agents/dashboard/modules/operations/src/react/PagedWorkList.test.tsx +43 -0
  101. package/templates/.agents/dashboard/modules/operations/src/react/PagedWorkList.tsx +181 -0
  102. package/templates/.agents/dashboard/modules/operations/src/react/UnifiedWorkList.test.ts +4 -2
  103. package/templates/.agents/dashboard/modules/operations/src/react/UnifiedWorkList.tsx +13 -0
  104. package/templates/.agents/dashboard/modules/operations/src/react/developer-identities.test.ts +18 -0
  105. package/templates/.agents/dashboard/modules/operations/src/react/developer-identities.ts +30 -0
  106. package/templates/.agents/dashboard/modules/operations/src/react/execution-metrics.ts +1 -0
  107. package/templates/.agents/dashboard/modules/operations/src/react/export/ExcelExportAction.test.ts +8 -0
  108. package/templates/.agents/dashboard/modules/operations/src/react/export/ExcelExportAction.tsx +3 -0
  109. package/templates/.agents/dashboard/modules/operations/src/react/export/export-layout.test.ts +14 -5
  110. package/templates/.agents/dashboard/modules/operations/src/react/index.ts +1 -0
  111. package/templates/.agents/dashboard/modules/operations/src/react/operations-coverage-toast.test.ts +4 -3
  112. package/templates/.agents/dashboard/modules/operations/src/react/operations-date-filters.css +1 -1
  113. package/templates/.agents/dashboard/modules/operations/src/react/operations-overview-period-filter.test.ts +21 -11
  114. package/templates/.agents/dashboard/modules/operations/src/react/operations-overview.css +4 -0
  115. package/templates/.agents/dashboard/modules/operations/src/react/operations-page-contract.ts +8 -1
  116. package/templates/.agents/dashboard/modules/operations/src/react/operations-view-order.test.ts +1 -0
  117. package/templates/.agents/dashboard/modules/operations/src/react/period-summary.ts +1 -0
  118. package/templates/.agents/dashboard/modules/operations/src/server/metric-coverage.ts +1 -0
  119. package/templates/.agents/dashboard/packages/local-runtime/dist/server.cjs +527 -335
  120. package/templates/.agents/dashboard/packages/local-runtime/dist/sqlite-read-model.cjs +501 -309
  121. package/templates/.agents/dashboard/packages/local-runtime/gitignore.template +2 -0
  122. package/templates/.agents/dashboard/packages/local-runtime/scripts/rebuild-sqlite-read-model.mjs +3 -1
  123. package/templates/.agents/dashboard/packages/local-runtime/scripts/test-sqlite-read-model.mjs +3 -3
  124. package/templates/.agents/dashboard/packages/local-runtime/scripts/validate-sqlite-read-model.mjs +25 -15
  125. package/templates/.agents/dashboard/packages/local-runtime/server.mjs +527 -335
  126. package/templates/.agents/dashboard/packages/local-runtime/src/server-board.test.ts +34 -0
  127. package/templates/.agents/dashboard/packages/local-runtime/src/server-board.ts +15 -8
  128. package/templates/.agents/dashboard/packages/local-runtime/src/server-classification-page.test.ts +87 -0
  129. package/templates/.agents/dashboard/packages/local-runtime/src/server-period-macro-metric.test.ts +44 -0
  130. package/templates/.agents/dashboard/packages/local-runtime/src/server-period-macro-metric.ts +125 -0
  131. package/templates/.agents/dashboard/packages/local-runtime/src/server-reconciliation-refresh.test.ts +66 -0
  132. package/templates/.agents/dashboard/packages/local-runtime/src/server-request-authority.test.ts +61 -0
  133. package/templates/.agents/dashboard/packages/local-runtime/src/server-request-authority.ts +18 -0
  134. package/templates/.agents/dashboard/packages/local-runtime/src/server-run-projections.test.ts +42 -0
  135. package/templates/.agents/dashboard/packages/local-runtime/src/server-snapshot-refresh.ts +97 -0
  136. package/templates/.agents/dashboard/packages/local-runtime/src/server-snapshot.test.ts +3 -1
  137. package/templates/.agents/dashboard/packages/local-runtime/src/server-sqlite-task-trace.ts +91 -0
  138. package/templates/.agents/dashboard/packages/local-runtime/src/server.test-fixtures.ts +36 -0
  139. package/templates/.agents/dashboard/packages/local-runtime/src/server.test.ts +98 -123
  140. package/templates/.agents/dashboard/packages/local-runtime/src/server.ts +111 -135
  141. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-artifact-counts.ts +28 -0
  142. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-cost-agent-page.test.ts +63 -0
  143. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-cost-agent-page.ts +109 -0
  144. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-cost-aggregates.ts +497 -0
  145. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-cost-fast-page-parity.test-support.ts +65 -0
  146. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-cost-group-page.test.ts +176 -0
  147. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-cost-group-page.ts +119 -0
  148. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-cost-group-projection.ts +41 -0
  149. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-cost-invocation-page.test.ts +16 -0
  150. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-cost-invocation-page.ts +29 -0
  151. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-first-fold-cache.ts +355 -0
  152. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-first-fold-persistence.ts +121 -0
  153. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-first-fold-timing.ts +27 -0
  154. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-operations-projection.ts +15 -0
  155. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-operations-work-list.test.ts +293 -0
  156. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-operations-work-list.ts +371 -0
  157. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-read-model-cache.test-support.ts +23 -0
  158. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-read-model-first-fold-types.ts +89 -0
  159. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-read-model-multi-provider-fixture.ts +227 -0
  160. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-read-model-multi-provider.test.ts +691 -0
  161. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-read-model-source-fingerprint.ts +81 -0
  162. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-read-model.test.ts +440 -863
  163. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-read-model.ts +1120 -1086
  164. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-snapshot-cache.ts +129 -0
  165. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-snapshot-install.test.ts +26 -0
  166. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-snapshot-install.ts +28 -0
  167. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-snapshot-seed.test.ts +116 -0
  168. package/templates/.agents/dashboard/packages/local-runtime/start-dashboard-launch-page.mjs +146 -0
  169. package/templates/.agents/dashboard/packages/local-runtime/start-dashboard-launch-page.test.mjs +165 -0
  170. package/templates/.agents/dashboard/packages/local-runtime/start-dashboard-refresh.mjs +140 -0
  171. package/templates/.agents/dashboard/packages/local-runtime/start-dashboard.mjs +188 -299
  172. package/templates/.agents/dashboard/packages/local-runtime/start-dashboard.test.mjs +51 -70
  173. package/templates/.agents/dashboard/packages/windows-launcher/Program.cs +5 -2
  174. package/templates/.agents/dashboard/tests/e2e/playwright.config.ts +4 -1
  175. package/templates/.agents/dashboard/tests/e2e/specs/cost-monitor-loading.spec.ts +142 -0
  176. package/templates/.agents/dashboard/tests/e2e/specs/cost-monitor-pagination.spec.ts +232 -0
  177. package/templates/.agents/dashboard/tests/e2e/specs/dashboard-performance-matrix.spec.ts +743 -0
  178. package/templates/.agents/dashboard/tests/e2e/specs/dashboard.spec.ts +183 -210
  179. package/templates/.agents/dashboard/zaip-dashboard-windows.exe +0 -0
  180. package/templates/.agents/requirements/autonomous-assurance.md +6 -6
  181. package/templates/.agents/requirements/comparative-audit.md +1 -1
  182. package/templates/.agents/requirements/context-engine.md +1 -1
  183. package/templates/.agents/requirements/cost-observability.md +1 -1
  184. package/templates/.agents/requirements/dashboard-launch.md +1 -1
  185. package/templates/.agents/requirements/data-export.md +1 -1
  186. package/templates/.agents/requirements/design-system.md +1 -1
  187. package/templates/.agents/requirements/execution-engine.md +3 -3
  188. package/templates/.agents/requirements/kanban-dashboard.md +10 -2
  189. package/templates/.agents/requirements/model-orchestration.md +1 -1
  190. package/templates/.agents/requirements/public-distribution.md +1 -1
  191. package/templates/.agents/requirements/software-architecture.md +1 -1
  192. package/templates/.agents/requirements/strategic-improvements.md +1 -1
  193. package/templates/.agents/requirements/work-lifecycle.md +4 -4
  194. package/templates/.agents/requirements/zaip-strategic-95.md +1 -1
  195. package/templates/.agents/rules/dashboard-ui-consistency.md +14 -0
  196. package/templates/.agents/rules/frontend-lite.md +4 -4
  197. package/templates/.agents/rules/run-lifecycle-closure.md +4 -4
  198. package/templates/.agents/runtime/bin/zaip.mjs +1137 -1037
  199. package/templates/.agents/runtime/checksums.sha256 +2 -2
  200. package/templates/.agents/runtime/provenance.json +2 -2
  201. package/templates/.agents/skills/criar-pasta-obsidian/scripts/generate-obsidian-vault.test.mjs +20 -16
  202. package/templates/.agents/skills/zaip-skill-agent-catalog/SKILL.md +22 -0
  203. package/templates/.agents/skills/zaip-skill-agent-catalog/agents/openai.yaml +4 -0
  204. package/templates/.agents/skills/zaip-skill-author-skills/SKILL.md +3 -2
  205. package/templates/.agents/skills/zaip-skill-author-skills/scripts/audit-skill-catalog.mjs +49 -0
  206. package/templates/.agents/skills/zaip-skill-author-skills/scripts/author-skill-scripts.test.mjs +27 -0
  207. package/templates/.agents/skills/zaip-skill-author-skills/scripts/run-skill-tests.mjs +37 -0
  208. package/templates/.agents/skills/zaip-skill-backend/SKILL.md +1 -0
  209. package/templates/.agents/skills/zaip-skill-change/SKILL.md +1 -1
  210. package/templates/.agents/skills/zaip-skill-change/references/observable-evidence.md +83 -0
  211. package/templates/.agents/skills/zaip-skill-dashboard-diagnostics/SKILL.md +22 -0
  212. package/templates/.agents/skills/zaip-skill-dashboard-diagnostics/agents/openai.yaml +4 -0
  213. package/templates/.agents/skills/zaip-skill-execution/SKILL.md +1 -1
  214. package/templates/.agents/skills/zaip-skill-frontend/SKILL.md +4 -3
  215. package/templates/.agents/skills/zaip-skill-frontend/agents/openai.yaml +2 -2
  216. package/templates/.agents/skills/zaip-skill-github-deploy/SKILL.md +10 -10
  217. package/templates/.agents/skills/zaip-skill-lifecycle-closure/SKILL.md +27 -0
  218. package/templates/.agents/skills/zaip-skill-lifecycle-closure/agents/openai.yaml +4 -0
  219. package/templates/.agents/skills/zaip-skill-prd/SKILL.md +4 -0
  220. package/templates/.agents/skills/zaip-skill-reconcile-telemetry/scripts/reconcile-local-telemetry.mjs +72 -22
  221. package/templates/.agents/skills/zaip-skill-reconcile-telemetry/scripts/reconcile-telemetry-explicit-recovery.mjs +223 -0
  222. package/templates/.agents/skills/zaip-skill-reconcile-telemetry/scripts/reconcile-telemetry-lib.mjs +42 -1
  223. package/templates/.agents/skills/zaip-skill-reconcile-telemetry/scripts/reconcile-telemetry-lib.test.mjs +101 -0
  224. package/templates/.agents/skills/zaip-skill-reconcile-telemetry/scripts/reconcile-telemetry-remeasurement.test.mjs +53 -0
  225. package/templates/.agents/skills/zaip-skill-release-qualification/SKILL.md +20 -0
  226. package/templates/.agents/skills/zaip-skill-release-qualification/agents/openai.yaml +4 -0
  227. package/templates/.agents/skills/zaip-skill-review/SKILL.md +5 -0
  228. package/templates/.agents/skills/zaip-skill-rules/SKILL.md +1 -1
  229. package/templates/.agents/skills/zaip-skill-spec/SKILL.md +4 -0
  230. package/templates/.agents/skills/zaip-skill-sqlite-read-model/SKILL.md +22 -90
  231. package/templates/.agents/zaip-aios/INDEX.md +57 -32
  232. package/templates/.agents/zaip-aios/README.md +22 -12
  233. package/templates/.agents/zaip-aios/guides/02-requirements.md +5 -0
  234. package/templates/.agents/zaip-aios/guides/06-execucao.md +13 -4
  235. package/templates/.agents/zaip-aios/guides/07-revisao-e-validacao.md +7 -0
  236. package/templates/.agents/zaip-aios/guides/10-seguranca.md +23 -0
  237. package/templates/.agents/zaip-aios/guides/11-observabilidade-e-custos.md +6 -6
  238. package/templates/.agents/zaip-aios/guides/13-boas-praticas.md +3 -0
  239. package/templates/.agents/zaip-aios/guides/16-lifecycle-e-reconciliacao.md +12 -3
  240. package/templates/.agents/zaip-aios/guides/17-como-o-custo-e-calculado.md +5 -5
  241. package/templates/.agents/zaip-aios/guides/19-assurance-e-autonomia.md +23 -0
  242. package/templates/.agents/zaip-aios/guides/20-instalacao-detalhada.md +44 -8
  243. package/templates/.agents/zaip-aios/guides/21-telemetria-monitor-custos.md +8 -14
  244. package/templates/.agents/zaip-aios/guides/22-fluxos-e-roteamento.md +5 -0
  245. package/templates/.agents/zaip-aios/guides/23-orquestracao-de-modelos.md +6 -6
  246. package/templates/.agents/zaip-aios/guides/24-calculo-de-custos-openai.md +8 -8
  247. package/templates/.agents/zaip-aios/guides/25-fontes-de-precos-openai.md +3 -2
  248. package/templates/.agents/zaip-aios/guides/26-qualidade-e-release.md +92 -10
  249. package/templates/.agents/zaip-aios/guides/27-telemetria-git-native.md +8 -3
  250. package/templates/.agents/zaip-aios/guides/28-lifecycle-completo.md +5 -0
  251. package/templates/template-manifest.json +555 -170
  252. package/templates/.agents/dashboard/apps/frontend/dist/assets/TraceabilityWorkspace-BwtitWa_.js +0 -1
  253. package/templates/.agents/dashboard/apps/frontend/dist/assets/cost-monitor-BZ5WdaMN.css +0 -1
  254. package/templates/.agents/dashboard/apps/frontend/dist/assets/cost-monitor-N5Csr0RP.js +0 -68
  255. package/templates/.agents/dashboard/apps/frontend/dist/assets/index-C4aIB1PV.js +0 -2
  256. package/templates/.agents/dashboard/apps/frontend/dist/assets/index-DK_enB5Z.css +0 -1
  257. package/templates/.agents/dashboard/apps/frontend/src/use-idle-cost-prefetch.ts +0 -45
  258. package/templates/.agents/dashboard/packages/local-runtime/dist/server.cjs.metafile.json +0 -7333
@@ -11,26 +11,78 @@ import {
11
11
  type PersistedTokenUsage,
12
12
  type PricingCatalog,
13
13
  pricingCatalogSchema,
14
+ resolveHistoricalDeveloperAttribution,
14
15
  } from "@zaip-ai/costs";
16
+ import type { CostSummarySource } from "@zaip-ai/dashboard-cost-monitor/server";
17
+ import { allocateSharedCreationValue } from "@zaip-ai/lifecycle-manifests";
15
18
  import {
16
19
  foldRunV2Records,
17
20
  parseRunV2JsonLines,
18
21
  type RunV2Fold,
19
22
  type UsageRecordedV2,
20
23
  } from "../../../../../packages/decisions/src/index.js";
24
+ import { artifactStructuralRowsSql, developerArtifactCountsCte } from "./sqlite-artifact-counts.js";
25
+ import {
26
+ costDimensionFactDateFilter,
27
+ isDateOnlyCostPage,
28
+ readDateOnlyCostDimensionPage,
29
+ } from "./sqlite-cost-agent-page.js";
30
+ import { buildCostDimensionAggregates } from "./sqlite-cost-aggregates.js";
31
+ import { type CostGroupKind, readCostGroupPage } from "./sqlite-cost-group-page.js";
32
+ import { materializeCostGroupDailyProjection } from "./sqlite-cost-group-projection.js";
33
+ import { filterCostInvocationProjection } from "./sqlite-cost-invocation-page.js";
34
+ import {
35
+ readFirstFoldAllTime,
36
+ readFirstFoldBoardAllTime,
37
+ readFirstFoldMonthlyQuick,
38
+ readFirstFoldTopTasksAllTime,
39
+ readFirstFoldViewport,
40
+ } from "./sqlite-first-fold-cache.js";
41
+ import {
42
+ hasRequiredFirstFoldPayloads,
43
+ persistFirstFoldPayloads,
44
+ } from "./sqlite-first-fold-persistence.js";
45
+ import { measureFirstFold } from "./sqlite-first-fold-timing.js";
46
+ import { materializeOperationsWorkProjection } from "./sqlite-operations-projection.js";
47
+ import {
48
+ attachOperationsWorkListDevelopers,
49
+ operationsWorkListPayloadPage,
50
+ operationsWorkListSqlPage,
51
+ operationsWorkListSqlRows,
52
+ operationsWorkRowsInRange,
53
+ } from "./sqlite-operations-work-list.js";
54
+ import {
55
+ installLegacyViewAliases,
56
+ materializeSnapshot,
57
+ mostRecentCompatibleSnapshot,
58
+ pruneSnapshotCache,
59
+ readSnapshotFileCount,
60
+ } from "./sqlite-snapshot-cache.js";
61
+
62
+ export {
63
+ type SqliteReadModelSourceCandidate,
64
+ sourceFingerprint,
65
+ } from "./sqlite-read-model-source-fingerprint.js";
66
+
67
+ import { sourceFingerprint } from "./sqlite-read-model-source-fingerprint.js";
21
68
 
22
69
  const dbRelativePath = ".local/dashboard-sqlite/read-model.sqlite";
23
- const textExtensions = new Set([".json", ".jsonl", ".md", ".yaml", ".yml", ".txt"]);
24
70
  // Schema 58 aligns Operations invocation/day facts with the same factual
25
71
  // source day used by tokens, Cost Monitor and attributed Tempo. Seed reuse is
26
72
  // safe only when both source and this schema version match.
27
- const readModelSchemaVersion = "60";
73
+ // Allocation rows are materialized after attributed-time reconciliation; a
74
+ // prior seed contains the pre-reconciliation payload and is incompatible.
75
+ const readModelSchemaVersion = "70";
28
76
  const requiredPersistedTables = [
29
77
  "zaip_aios_activities",
30
78
  "zaip_dim_llm_model",
31
79
  "zaip_dim_provider",
32
80
  "zaip_dim_zaip_agent",
33
81
  "cost_invocation_dimension_facts",
82
+ "cost_group_daily_projection",
83
+ "cost_group_entity_projection",
84
+ "developer_cost_projection",
85
+ "operations_work_projection",
34
86
  ] as const;
35
87
  const requiredIndexes = [
36
88
  "zaip_activities_agent_run",
@@ -48,6 +100,9 @@ const requiredIndexes = [
48
100
  "cost_dimension_facts_day_model",
49
101
  "cost_dimension_facts_day_provider",
50
102
  "cost_dimension_facts_invocation",
103
+ "cost_group_daily_projection_lookup",
104
+ "cost_group_entity_projection_lookup",
105
+ "developer_cost_projection_lookup",
51
106
  ] as const;
52
107
  const requiredViews = [
53
108
  "zaip_vw_read_model_metadata",
@@ -59,27 +114,6 @@ const requiredViews = [
59
114
  ] as const;
60
115
  const MAX_RECONCILED_RUN_INTERVAL_MS = 2 * 3_600_000;
61
116
 
62
- type CostDimensionAggregateRow = {
63
- key: string;
64
- label: string;
65
- invocation_count: number;
66
- task_count: number;
67
- total_duration: number | null;
68
- source_event_count: number | null;
69
- total_tokens: number | null;
70
- total_cost_usd: number | null;
71
- total_cost_brl: number | null;
72
- };
73
-
74
- function observedDimensionValue(value: unknown): string | null {
75
- if (typeof value !== "string") return null;
76
- const normalized = value.trim();
77
- return normalized.length > 0 &&
78
- !["unavailable", "unknown", "mixed"].includes(normalized.toLowerCase())
79
- ? normalized
80
- : null;
81
- }
82
-
83
117
  function saoPauloCurrentMonth(): { from: string; to: string } {
84
118
  const parts = new Intl.DateTimeFormat("en-CA", {
85
119
  timeZone: "America/Sao_Paulo",
@@ -92,196 +126,7 @@ function saoPauloCurrentMonth(): { from: string; to: string } {
92
126
  return { from: `${value.year}-${value.month}-01`, to: day };
93
127
  }
94
128
 
95
- /** Small, seed-time-only aggregates for the unfiltered Models/Providers tables. */
96
- function buildCostDimensionAggregates(
97
- database: DatabaseSync,
98
- ): Record<"model" | "provider", CostDimensionAggregateRow[]> {
99
- const dimensions = ["model", "provider"] as const;
100
- const allowedProviders = new Set(
101
- (
102
- database.prepare("SELECT provider FROM zaip_dim_provider").all() as Array<{
103
- provider: string;
104
- }>
105
- ).map((row) => row.provider),
106
- );
107
- const activities = database
108
- .prepare(`SELECT activity_row_id, agent_invocation_id, task_id, duration_ms,
109
- source_event_count, provider, model, total_tokens, cost_usd, cost_brl
110
- FROM zaip_aios_activities WHERE payload_type IS NULL`)
111
- .all() as Array<Record<string, unknown>>;
112
- const segments = database
113
- .prepare(`SELECT agent_invocation_id, provider, model, total_tokens, cost_usd, cost_brl
114
- FROM invocation_usage_segments`)
115
- .all() as Array<Record<string, unknown>>;
116
- const activityByInvocation = new Map(
117
- activities
118
- .filter((row) => typeof row.agent_invocation_id === "string")
119
- .map((row) => [String(row.agent_invocation_id), row] as const),
120
- );
121
- const rowsByDimension = new Map<"model" | "provider", Map<string, CostDimensionAggregateRow>>(
122
- dimensions.map((dimension) => [dimension, new Map()]),
123
- );
124
- const ensure = (dimension: "model" | "provider", key: string) => {
125
- const rows = rowsByDimension.get(dimension)!;
126
- const current = rows.get(key);
127
- if (current !== undefined) return current;
128
- const created: CostDimensionAggregateRow = {
129
- key,
130
- label: key,
131
- invocation_count: 0,
132
- task_count: 0,
133
- total_duration: null,
134
- source_event_count: null,
135
- total_tokens: null,
136
- total_cost_usd: null,
137
- total_cost_brl: null,
138
- };
139
- rows.set(key, created);
140
- return created;
141
- };
142
- const add = (
143
- target: CostDimensionAggregateRow,
144
- field: "total_tokens" | "total_cost_usd" | "total_cost_brl",
145
- value: unknown,
146
- ) => {
147
- if (typeof value === "number") target[field] = (target[field] ?? 0) + value;
148
- };
149
- const segmentsByInvocation = new Map<string, Array<Record<string, unknown>>>();
150
- for (const segment of segments) {
151
- const id = String(segment.agent_invocation_id ?? "");
152
- if (id.length === 0) continue;
153
- const invocationSegments = segmentsByInvocation.get(id);
154
- if (invocationSegments === undefined) segmentsByInvocation.set(id, [segment]);
155
- else invocationSegments.push(segment);
156
- }
157
- for (const [id, invocationSegments] of segmentsByInvocation) {
158
- const activity = activityByInvocation.get(id);
159
- if (activity === undefined) continue;
160
- for (const dimension of dimensions) {
161
- const financialSegments = invocationSegments.filter(
162
- (segment) =>
163
- observedDimensionValue(segment[dimension]) !== null &&
164
- observedDimensionValue(segment.provider) !== null &&
165
- allowedProviders.has(observedDimensionValue(segment.provider)!) &&
166
- (typeof segment.total_tokens === "number" ||
167
- typeof segment.cost_usd === "number" ||
168
- typeof segment.cost_brl === "number"),
169
- );
170
- const keys = [
171
- ...new Set(financialSegments.map((row) => observedDimensionValue(row[dimension])!)),
172
- ];
173
- if (keys.length === 0) {
174
- // Keep auditable, dimensionless invocation facts visible so Models
175
- // and Providers reconcile to the Cost KPI instead of silently
176
- // dropping them.
177
- const target = ensure(dimension, "unavailable");
178
- target.invocation_count += 1;
179
- if (typeof activity.duration_ms === "number")
180
- target.total_duration = (target.total_duration ?? 0) + activity.duration_ms;
181
- if (typeof activity.source_event_count === "number")
182
- target.source_event_count =
183
- (target.source_event_count ?? 0) + activity.source_event_count;
184
- add(target, "total_tokens", activity.total_tokens);
185
- add(target, "total_cost_usd", activity.cost_usd);
186
- add(target, "total_cost_brl", activity.cost_brl);
187
- }
188
- for (const segment of financialSegments) {
189
- const target = ensure(dimension, observedDimensionValue(segment[dimension])!);
190
- add(target, "total_tokens", segment.total_tokens);
191
- add(target, "total_cost_usd", segment.cost_usd);
192
- add(target, "total_cost_brl", segment.cost_brl);
193
- }
194
- // An invocation's elapsed time is attributable only when its observed
195
- // financial segments agree on one dimension. Do not fabricate a
196
- // mixed bucket or split wall-clock time across models/providers.
197
- if (keys.length === 1) {
198
- const target = ensure(dimension, keys[0]!);
199
- target.invocation_count += 1;
200
- if (typeof activity.duration_ms === "number")
201
- target.total_duration = (target.total_duration ?? 0) + activity.duration_ms;
202
- if (typeof activity.source_event_count === "number")
203
- target.source_event_count =
204
- (target.source_event_count ?? 0) + activity.source_event_count;
205
- }
206
- }
207
- }
208
- const taskKeys = new Map<"model" | "provider", Map<string, Set<string>>>(
209
- dimensions.map((dimension) => [dimension, new Map()]),
210
- );
211
- for (const [id, invocationSegments] of segmentsByInvocation) {
212
- const taskId = activityByInvocation.get(id)?.task_id;
213
- if (typeof taskId !== "string") continue;
214
- for (const dimension of dimensions) {
215
- const byTask = taskKeys.get(dimension)!;
216
- const keys = byTask.get(taskId) ?? new Set<string>();
217
- for (const segment of invocationSegments) {
218
- const key = observedDimensionValue(segment[dimension]);
219
- const provider = observedDimensionValue(segment.provider);
220
- const hasFinancialFact =
221
- typeof segment.total_tokens === "number" ||
222
- typeof segment.cost_usd === "number" ||
223
- typeof segment.cost_brl === "number";
224
- if (key !== null && provider !== null && allowedProviders.has(provider) && hasFinancialFact)
225
- keys.add(key);
226
- }
227
- byTask.set(taskId, keys);
228
- }
229
- }
230
- for (const dimension of dimensions)
231
- for (const keys of taskKeys.get(dimension)!.values())
232
- for (const key of keys) ensure(dimension, key).task_count += 1;
233
- for (const activity of activities) {
234
- if (typeof activity.agent_invocation_id === "string") continue;
235
- const hasFinancialFact =
236
- typeof activity.total_tokens === "number" ||
237
- typeof activity.cost_usd === "number" ||
238
- typeof activity.cost_brl === "number";
239
- if (!hasFinancialFact) continue;
240
- const provider = observedDimensionValue(activity.provider);
241
- if (provider === null || !allowedProviders.has(provider)) continue;
242
- for (const dimension of dimensions) {
243
- const key = observedDimensionValue(activity[dimension]) ?? "unavailable";
244
- const target = ensure(dimension, key);
245
- target.invocation_count += 1;
246
- if (typeof activity.task_id === "string") target.task_count += 1;
247
- if (typeof activity.duration_ms === "number")
248
- target.total_duration = (target.total_duration ?? 0) + activity.duration_ms;
249
- if (typeof activity.source_event_count === "number")
250
- target.source_event_count = (target.source_event_count ?? 0) + activity.source_event_count;
251
- add(target, "total_tokens", activity.total_tokens);
252
- add(target, "total_cost_usd", activity.cost_usd);
253
- add(target, "total_cost_brl", activity.cost_brl);
254
- }
255
- }
256
- return Object.fromEntries(
257
- dimensions.map((dimension) => [
258
- dimension,
259
- [...rowsByDimension.get(dimension)!.values()].sort(
260
- (left, right) =>
261
- (right.total_cost_brl ?? -1) - (left.total_cost_brl ?? -1) ||
262
- left.key.localeCompare(right.key),
263
- ),
264
- ]),
265
- ) as Record<"model" | "provider", CostDimensionAggregateRow[]>;
266
- }
267
-
268
129
  /** Only canonical inputs consumed by the fact or its direct dimensions. */
269
- function isDashboardCanonicalSource(source: string): boolean {
270
- if (
271
- source === ".agents/config/project.yaml" ||
272
- source === ".agents/config/framework.lock.yaml" ||
273
- source === ".agents/config/agents/catalog.yaml" ||
274
- source === ".agents/config/clients.yaml"
275
- )
276
- return true;
277
- if (source.startsWith(".agents/config/pricing/")) return true;
278
- if (source.startsWith(".agents/records/runs/") && source.endsWith(".jsonl")) return true;
279
- if (/^\.agents\/records\/(tasks|specs|prds)\/.*\.md$/.test(source)) return true;
280
- if (/^\.agents\/records\/executions\/.*\/execution\.md$/.test(source)) return true;
281
- return /^\.agents\/records\/changes\/.*\/(change\.md|decisions\/.*\.md|artifacts\/(evidence|validations)\/.*\.md)$/.test(
282
- source,
283
- );
284
- }
285
130
 
286
131
  type AgentProfile = {
287
132
  catalogId: string;
@@ -296,15 +141,21 @@ type CanonicalEntity = {
296
141
  status: string | null;
297
142
  stage: string | null;
298
143
  risk: string | null;
144
+ specId: string | null;
145
+ changeId: string | null;
299
146
  occurredAt: string | null;
147
+ creationRunId: string | null;
148
+ completedRunId: string | null;
300
149
  };
301
150
  type CanonicalArtifact = {
302
151
  kind: "evidence" | "validation";
303
152
  id: string;
304
153
  occurredAt: string;
305
154
  occurredDaySp: string;
155
+ taskId: string | null;
156
+ changeId: string | null;
157
+ runId: string | null;
306
158
  };
307
-
308
159
  type CanonicalExecution = {
309
160
  id: string;
310
161
  status: string | null;
@@ -315,7 +166,6 @@ type CanonicalExecution = {
315
166
  finishedAt: string | null;
316
167
  sourcePath: string;
317
168
  };
318
-
319
169
  type CanonicalChange = {
320
170
  id: string;
321
171
  title: string;
@@ -332,7 +182,6 @@ type CanonicalChange = {
332
182
  createdAt: string;
333
183
  sourcePath: string;
334
184
  };
335
-
336
185
  type CanonicalDecision = {
337
186
  id: string;
338
187
  title: string;
@@ -342,7 +191,6 @@ type CanonicalDecision = {
342
191
  createdAt: string;
343
192
  sourcePath: string;
344
193
  };
345
-
346
194
  function frontMatterRecord(content: string): Record<string, unknown> | null {
347
195
  const source = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/.exec(content)?.[1];
348
196
  if (source === undefined) return null;
@@ -496,7 +344,11 @@ function readCanonicalEntities(
496
344
  status: scalar("status"),
497
345
  stage: scalar("stage"),
498
346
  risk: scalar("risk"),
347
+ specId: scalar("spec_id"),
348
+ changeId: scalar("change_id"),
499
349
  occurredAt: scalar("created_at") ?? scalar("started_at") ?? scalar("recorded_at"),
350
+ creationRunId: scalar("creation_run_id"),
351
+ completedRunId: scalar("completed_run_id"),
500
352
  });
501
353
  }
502
354
  return entities;
@@ -1030,34 +882,21 @@ function costMonitorFilterDay(value: string | null | undefined): string | null {
1030
882
  return /^\d{4}-\d{2}-\d{2}$/.test(value) ? value : saoPauloDay(value);
1031
883
  }
1032
884
 
1033
- function saoPauloLocalMidnight(day: string): number {
1034
- const [year, month, date] = day.split("-").map(Number);
1035
- const utcGuess = Date.UTC(year!, month! - 1, date!);
1036
- const offset =
1037
- new Intl.DateTimeFormat("en-US", {
1038
- timeZone: "America/Sao_Paulo",
1039
- timeZoneName: "longOffset",
1040
- })
1041
- .formatToParts(new Date(utcGuess))
1042
- .find((part) => part.type === "timeZoneName")?.value ?? "GMT";
1043
- const match = /^GMT([+-])(\d{2}):(\d{2})$/.exec(offset);
1044
- if (match === null) return utcGuess;
1045
- const offsetMs =
1046
- (Number(match[2]) * 60 + Number(match[3])) * 60_000 * (match[1] === "+" ? 1 : -1);
1047
- return utcGuess - offsetMs;
1048
- }
1049
-
1050
- function nextSaoPauloDay(day: string): string {
1051
- const [year, month, date] = day.split("-").map(Number);
1052
- const next = new Date(Date.UTC(year!, month! - 1, date! + 1));
1053
- return next.toISOString().slice(0, 10);
885
+ function sumKnownDeveloperMetric(
886
+ rows: readonly Record<string, unknown>[],
887
+ field: string,
888
+ ): number | null {
889
+ const values = rows
890
+ .map((row) => row[field])
891
+ .filter((value): value is number => typeof value === "number" && Number.isFinite(value));
892
+ return values.length === 0 ? null : values.reduce((sum, value) => sum + value, 0);
1054
893
  }
1055
894
 
1056
895
  /** Extracts the small artifact projection while the canonical file is already being read. */
1057
896
  function readCanonicalArtifact(source: string, content: string): CanonicalArtifact | null {
1058
897
  const kind = source.includes("/artifacts/evidence/")
1059
898
  ? "evidence"
1060
- : source.includes("/artifacts/validations/")
899
+ : source.includes("/artifacts/validations/") || source.includes("/artifacts/tests/")
1061
900
  ? "validation"
1062
901
  : null;
1063
902
  if (kind === null) return null;
@@ -1076,6 +915,9 @@ function readCanonicalArtifact(source: string, content: string): CanonicalArtifa
1076
915
  id: scalar("id") ?? source.slice(source.lastIndexOf("/") + 1, -3),
1077
916
  occurredAt,
1078
917
  occurredDaySp: saoPauloDay(occurredAt),
918
+ taskId: scalar("task_id"),
919
+ changeId: scalar("change_id"),
920
+ runId: scalar("run_id"),
1079
921
  };
1080
922
  }
1081
923
 
@@ -1083,6 +925,7 @@ export function sqliteReadModelPath(workspaceRoot: string): string {
1083
925
  return resolve(process.env.ZAIP_SQLITE_DB_PATH ?? resolve(workspaceRoot, dbRelativePath));
1084
926
  }
1085
927
 
928
+ /** Atomically installs a worker-built immutable generation after its schema is validated. */
1086
929
  /**
1087
930
  * Refuses stale or incomplete SQLite generations before they are served. This
1088
931
  * is intentionally independent of source fingerprints: a matching cache key
@@ -1116,7 +959,7 @@ export function validateSqliteReadModelSchema(path: string): {
1116
959
  tables.some((table, index) => table !== expectedTables[index])
1117
960
  )
1118
961
  throw new Error(
1119
- `SQLite schema ${readModelSchemaVersion} requires exactly five persisted tables; found ${tables.join(", ") || "none"}`,
962
+ `SQLite schema ${readModelSchemaVersion} requires the complete persisted table contract; found ${tables.join(", ") || "none"}`,
1120
963
  );
1121
964
  const names = new Set(objects.map((object) => object.name));
1122
965
  const missingViews = requiredViews.filter((view) => !names.has(view));
@@ -1172,7 +1015,12 @@ export async function rebuildSqliteReadModel(
1172
1015
  };
1173
1016
  const { fingerprint } = sourceSnapshot;
1174
1017
  const fingerprintMs = Math.round(performance.now() - fingerprintStartedAt);
1175
- const existingFiles = readSnapshotFileCount(path, fingerprint);
1018
+ const existingFiles = readSnapshotFileCount(
1019
+ path,
1020
+ fingerprint,
1021
+ readModelSchemaVersion,
1022
+ hasRequiredFirstFoldPayloads,
1023
+ );
1176
1024
  if (existingFiles !== null) {
1177
1025
  validateSqliteReadModelSchema(path);
1178
1026
  return {
@@ -1190,9 +1038,19 @@ export async function rebuildSqliteReadModel(
1190
1038
  // generation path. A normal fresh-port handoff can therefore be warm.
1191
1039
  const cachePath = join(
1192
1040
  process.env.ZAIP_SQLITE_CACHE_DIR ?? join(dirname(path), "cache"),
1193
- `v${readModelSchemaVersion}-${createHash("sha256").update(fingerprint).digest("hex")}.sqlite`,
1041
+ // The cache payload is part of the immutable generation contract. Keep
1042
+ // a separate namespace when that payload changes, so a schema-compatible
1043
+ // older seed cannot bypass the new all-time precomputation.
1044
+ // Keep the immutable name short enough that SQLite can create its
1045
+ // temporary journal beside deep Windows fixture/workspace paths.
1046
+ `v${readModelSchemaVersion}-ff9-${createHash("sha256").update(fingerprint).digest("hex")}.sqlite`,
1047
+ );
1048
+ const cachedFiles = readSnapshotFileCount(
1049
+ cachePath,
1050
+ fingerprint,
1051
+ readModelSchemaVersion,
1052
+ hasRequiredFirstFoldPayloads,
1194
1053
  );
1195
- const cachedFiles = readSnapshotFileCount(cachePath, fingerprint);
1196
1054
  if (cachedFiles !== null) {
1197
1055
  const materializeStartedAt = performance.now();
1198
1056
  await materializeSnapshot(cachePath, path);
@@ -1211,7 +1069,11 @@ export async function rebuildSqliteReadModel(
1211
1069
  // The launcher labels this state as updating and starts an isolated refresh;
1212
1070
  // this process still serves only a schema-validated immutable seed.
1213
1071
  if (!options.forceCanonical && process.env.ZAIP_SQLITE_PREFER_LAST_VALID_SEED === "1") {
1214
- const fallback = await mostRecentCompatibleSnapshot(dirname(cachePath));
1072
+ const fallback = await mostRecentCompatibleSnapshot(
1073
+ dirname(cachePath),
1074
+ readModelSchemaVersion,
1075
+ hasRequiredFirstFoldPayloads,
1076
+ );
1215
1077
  if (fallback !== null) {
1216
1078
  const materializeStartedAt = performance.now();
1217
1079
  await materializeSnapshot(fallback.path, path);
@@ -1231,17 +1093,17 @@ export async function rebuildSqliteReadModel(
1231
1093
  `Encontradas ${sourceSnapshot.candidates.length} fontes canônicas.`,
1232
1094
  );
1233
1095
  await mkdir(dirname(cachePath), { recursive: true });
1234
- const temporaryPath = `${cachePath}.${process.pid}.${randomUUID()}.tmp`;
1096
+ const temporaryPath = join(dirname(cachePath), `.snapshot-${randomUUID()}.tmp`);
1235
1097
  const database = new DatabaseSync(temporaryPath);
1236
1098
  let completedFiles = 0;
1237
1099
  try {
1238
1100
  database.exec("PRAGMA journal_mode=DELETE; BEGIN IMMEDIATE;");
1239
1101
  try {
1240
1102
  database.exec(`
1241
- CREATE TEMP TABLE files (path TEXT PRIMARY KEY, content TEXT NOT NULL, mtime_ms INTEGER NOT NULL, size_bytes INTEGER NOT NULL, content_sha256 TEXT NOT NULL, artifact_kind TEXT, artifact_id TEXT, artifact_occurred_at TEXT, artifact_occurred_day_sp TEXT);
1103
+ CREATE TEMP TABLE files (path TEXT PRIMARY KEY, content TEXT NOT NULL, mtime_ms INTEGER NOT NULL, size_bytes INTEGER NOT NULL, content_sha256 TEXT NOT NULL, artifact_kind TEXT, artifact_id TEXT, artifact_occurred_at TEXT, artifact_occurred_day_sp TEXT, artifact_task_id TEXT, artifact_change_id TEXT, artifact_run_id TEXT);
1242
1104
  CREATE TEMP TABLE read_model_source_state (source_path TEXT PRIMARY KEY, source_hash TEXT NOT NULL, mtime_ms INTEGER NOT NULL, size_bytes INTEGER NOT NULL, source_kind TEXT NOT NULL, last_processed_at TEXT NOT NULL);
1243
1105
  CREATE TEMP TABLE read_model_metadata (key TEXT PRIMARY KEY, value TEXT NOT NULL);
1244
- CREATE TEMP TABLE canonical_work_items (entity_kind TEXT NOT NULL, id TEXT NOT NULL, title TEXT, status TEXT, stage TEXT, risk TEXT, occurred_at TEXT NOT NULL, occurred_day_sp TEXT NOT NULL, PRIMARY KEY(entity_kind, id));
1106
+ CREATE TEMP TABLE canonical_work_items (entity_kind TEXT NOT NULL, id TEXT NOT NULL, title TEXT, status TEXT, stage TEXT, risk TEXT, spec_id TEXT, change_id TEXT, completed_run_id TEXT, occurred_at TEXT NOT NULL, occurred_day_sp TEXT NOT NULL, PRIMARY KEY(entity_kind, id));
1245
1107
  CREATE TEMP TABLE canonical_executions (id TEXT PRIMARY KEY, status TEXT, kind TEXT, origin_type TEXT, origin_id TEXT, created_at TEXT NOT NULL, occurred_day_sp TEXT NOT NULL, finished_at TEXT, source_path TEXT NOT NULL);
1246
1108
  CREATE TEMP TABLE canonical_changes (id TEXT PRIMARY KEY, title TEXT NOT NULL, status TEXT NOT NULL, risk TEXT NOT NULL, source_type TEXT NOT NULL, source_id TEXT NOT NULL, source_spec_id TEXT, source_prd_id TEXT, requirement_ids_json TEXT NOT NULL, acceptance_ids_json TEXT NOT NULL, task_ids_json TEXT NOT NULL, result TEXT, created_at TEXT NOT NULL, occurred_day_sp TEXT NOT NULL, source_path TEXT NOT NULL);
1247
1109
  CREATE TEMP TABLE canonical_decisions (id TEXT PRIMARY KEY, title TEXT NOT NULL, origin TEXT NOT NULL, scope_type TEXT NOT NULL, scope_id TEXT NOT NULL, created_at TEXT NOT NULL, occurred_day_sp TEXT NOT NULL, source_path TEXT NOT NULL);
@@ -1270,6 +1132,7 @@ export async function rebuildSqliteReadModel(
1270
1132
  occurred_at TEXT, occurred_day_sp TEXT, execution_kind TEXT, entrypoint TEXT, work_type TEXT, phase TEXT, purpose TEXT, status TEXT, activity_kind TEXT,
1271
1133
  zaip_agent TEXT, zaip_agent_persona TEXT, zaip_agent_job TEXT, zaip_agent_role TEXT, agent_catalog_id TEXT, agent_catalog_hash TEXT,
1272
1134
  client TEXT, provider TEXT, model TEXT, service_tier TEXT, reasoning_effort TEXT,
1135
+ developer_account_id TEXT, developer_login TEXT, developer_attribution_source TEXT,
1273
1136
  source_duration_ms REAL, source_started_at TEXT, source_finished_at TEXT, duration_ms REAL, duration_provenance TEXT, input_tokens REAL, cached_input_tokens REAL, output_tokens REAL, reasoning_tokens REAL, total_tokens REAL, token_source TEXT, usage_event_count INTEGER, llm_call_count INTEGER,
1274
1137
  cost_usd REAL, cost_brl REAL, cost_method TEXT, pricing_model TEXT, pricing_source TEXT, fx_rate REAL, source_event_count INTEGER,
1275
1138
  root_run_duration REAL, sub_run_duration REAL,
@@ -1296,7 +1159,7 @@ export async function rebuildSqliteReadModel(
1296
1159
  PRIMARY KEY(activity_row_id, occurred_day_sp)
1297
1160
  );`);
1298
1161
  const insert = database.prepare(
1299
- "INSERT INTO files(path, content, mtime_ms, size_bytes, content_sha256, artifact_kind, artifact_id, artifact_occurred_at, artifact_occurred_day_sp) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
1162
+ "INSERT INTO files(path, content, mtime_ms, size_bytes, content_sha256, artifact_kind, artifact_id, artifact_occurred_at, artifact_occurred_day_sp, artifact_task_id, artifact_change_id, artifact_run_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
1300
1163
  );
1301
1164
  const insertSourceState = database.prepare(
1302
1165
  "INSERT INTO read_model_source_state(source_path, source_hash, mtime_ms, size_bytes, source_kind, last_processed_at) VALUES (?, ?, ?, ?, ?, ?)",
@@ -1325,7 +1188,7 @@ export async function rebuildSqliteReadModel(
1325
1188
  ? "run"
1326
1189
  : source.includes("/artifacts/evidence/")
1327
1190
  ? "evidence"
1328
- : source.includes("/artifacts/validations/")
1191
+ : source.includes("/artifacts/validations/") || source.includes("/artifacts/tests/")
1329
1192
  ? "validation"
1330
1193
  : source.startsWith(".agents/config/")
1331
1194
  ? "config"
@@ -1341,6 +1204,9 @@ export async function rebuildSqliteReadModel(
1341
1204
  artifact?.id ?? null,
1342
1205
  artifact?.occurredAt ?? null,
1343
1206
  artifact?.occurredDaySp ?? null,
1207
+ artifact?.taskId ?? null,
1208
+ artifact?.changeId ?? null,
1209
+ artifact?.runId ?? null,
1344
1210
  );
1345
1211
  insertSourceState.run(
1346
1212
  source,
@@ -1367,7 +1233,7 @@ export async function rebuildSqliteReadModel(
1367
1233
  .all() as Array<{ path: string; content: string }>,
1368
1234
  );
1369
1235
  const insertCanonicalWorkItem = database.prepare(
1370
- "INSERT INTO canonical_work_items(entity_kind, id, title, status, stage, risk, occurred_at, occurred_day_sp) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
1236
+ "INSERT INTO canonical_work_items(entity_kind, id, title, status, stage, risk, spec_id, change_id, completed_run_id, occurred_at, occurred_day_sp) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
1371
1237
  );
1372
1238
  for (const entity of canonicalEntities.values()) {
1373
1239
  if (
@@ -1383,6 +1249,9 @@ export async function rebuildSqliteReadModel(
1383
1249
  entity.status,
1384
1250
  entity.stage,
1385
1251
  entity.risk,
1252
+ entity.specId,
1253
+ entity.changeId,
1254
+ entity.completedRunId,
1386
1255
  entity.occurredAt,
1387
1256
  saoPauloDay(entity.occurredAt),
1388
1257
  );
@@ -1480,6 +1349,56 @@ export async function rebuildSqliteReadModel(
1480
1349
  "SELECT path, content, content_sha256 FROM files WHERE path LIKE '.agents/records/runs/%.jsonl' ORDER BY path",
1481
1350
  )
1482
1351
  .all() as Array<{ path: string; content: string; content_sha256: string }>;
1352
+ // Historical attribution is an append-only, self-verifying overlay. It
1353
+ // never changes a Run stream and therefore participates only after the
1354
+ // native identity carried by the Run itself.
1355
+ const historicalDeveloperManifests = (
1356
+ database
1357
+ .prepare(
1358
+ "SELECT content FROM files WHERE path LIKE '.agents/records/lifecycle/historical-developer-attribution/%.json'",
1359
+ )
1360
+ .all() as Array<{ content: string }>
1361
+ ).flatMap((row) => {
1362
+ try {
1363
+ return [JSON.parse(row.content)];
1364
+ } catch {
1365
+ return [];
1366
+ }
1367
+ });
1368
+ const resolveDeveloper = (row: { path: string; content: string }, identity: unknown) => {
1369
+ const native = recordValue(identity);
1370
+ if (
1371
+ native !== null &&
1372
+ typeof native.account_id === "string" &&
1373
+ typeof native.login_snapshot === "string"
1374
+ )
1375
+ return {
1376
+ accountId: native.account_id,
1377
+ login: native.login_snapshot,
1378
+ source:
1379
+ typeof native.attribution_source === "string" ? native.attribution_source : "native",
1380
+ };
1381
+ for (const candidate of historicalDeveloperManifests) {
1382
+ try {
1383
+ const resolved = resolveHistoricalDeveloperAttribution({
1384
+ manifest: candidate,
1385
+ run_id: String(JSON.parse(row.content.split(/\r?\n/u)[0] ?? "{}").run_id ?? ""),
1386
+ path: row.path,
1387
+ content: row.content,
1388
+ });
1389
+ if (resolved !== null)
1390
+ return {
1391
+ accountId: resolved.account_id,
1392
+ login: resolved.login_snapshot,
1393
+ source: resolved.attribution_source,
1394
+ };
1395
+ } catch {
1396
+ // A corrupt or non-matching historical manifest is not an
1397
+ // attribution source. The consumer remains fail-closed.
1398
+ }
1399
+ }
1400
+ return null;
1401
+ };
1483
1402
  const folds = new Map(
1484
1403
  runRows.map((row) => {
1485
1404
  const fold = foldRunV2Records(parseRunV2JsonLines(row.content));
@@ -1552,6 +1471,9 @@ export async function rebuildSqliteReadModel(
1552
1471
  "model",
1553
1472
  "service_tier",
1554
1473
  "reasoning_effort",
1474
+ "developer_account_id",
1475
+ "developer_login",
1476
+ "developer_attribution_source",
1555
1477
  "source_duration_ms",
1556
1478
  "source_started_at",
1557
1479
  "source_finished_at",
@@ -1590,10 +1512,11 @@ export async function rebuildSqliteReadModel(
1590
1512
  "INSERT INTO canonical_run_index(run_id, occurred_at, occurred_day_sp, task_id, phase, run_role, duration_ms, source_event_count, zaip_agent, provider, model, status) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
1591
1513
  );
1592
1514
  let unassistedActivityCount = 0;
1593
- let excludedUnknownTokenRunCount = 0;
1515
+ const excludedUnknownTokenRunCount = 0;
1594
1516
  const temporalIntervals: TemporalInterval[] = [];
1595
1517
  for (const { row, fold } of folds.values()) {
1596
1518
  const started = fold.run;
1519
+ const developer = resolveDeveloper(row, started.developer_identity ?? null);
1597
1520
  const usage = fold.effective_usage_records;
1598
1521
  const agentRunId = started.agent_invocation_id ?? null;
1599
1522
  const isAgentRun = agentRunId !== null;
@@ -1640,13 +1563,9 @@ export async function rebuildSqliteReadModel(
1640
1563
  model,
1641
1564
  fold.status,
1642
1565
  );
1643
- // Keep the analytical fact restricted to measurable usage. Canonical
1644
- // Runs remain untouched and can enter after a later reconciliation.
1645
- // Known zero is measurable; only genuine unknown (NULL) is excluded.
1646
- if (totalTokens === null) {
1647
- excludedUnknownTokenRunCount += 1;
1648
- continue;
1649
- }
1566
+ // Preserve an attributed Run with absent telemetry as an analytical
1567
+ // fact with NULL measures. This makes coverage visibly unknown rather
1568
+ // than silently converting it to a known zero or erasing its owner.
1650
1569
  const profile = agentProfiles.get(zaipAgent) ?? agentProfiles.get(started.agent);
1651
1570
  const workItem =
1652
1571
  started.task_id === null
@@ -1735,6 +1654,9 @@ export async function rebuildSqliteReadModel(
1735
1654
  consolidatedValue(usage.map((record) => record.runtime?.reasoning_effort ?? null)) ??
1736
1655
  started.reasoning_effort ??
1737
1656
  null,
1657
+ developer_account_id: developer?.accountId ?? null,
1658
+ developer_login: developer?.login ?? null,
1659
+ developer_attribution_source: developer?.source ?? null,
1738
1660
  source_duration_ms: durations.source,
1739
1661
  source_started_at: durations.sourceStartedAt,
1740
1662
  source_finished_at: durations.sourceFinishedAt,
@@ -1761,7 +1683,12 @@ export async function rebuildSqliteReadModel(
1761
1683
  payload_json: null,
1762
1684
  } satisfies Record<(typeof columns)[number], string | number | null>;
1763
1685
  canonicalInsertActivity.run(...columns.map((column) => activity[column] ?? null));
1764
- if (totalTokens > 0 && durations.effective !== null && durations.effective >= 0) {
1686
+ if (
1687
+ totalTokens !== null &&
1688
+ totalTokens > 0 &&
1689
+ durations.effective !== null &&
1690
+ durations.effective >= 0
1691
+ ) {
1765
1692
  const startMs = Date.parse(durations.sourceStartedAt ?? started.occurred_at);
1766
1693
  const endMs =
1767
1694
  Date.parse(durations.sourceFinishedAt ?? "") || startMs + durations.effective;
@@ -1817,6 +1744,188 @@ export async function rebuildSqliteReadModel(
1817
1744
  }
1818
1745
  for (const fact of allocateAttributedTokenActiveMs(temporalIntervals))
1819
1746
  insertTemporalFact.run(fact.activityRowId, fact.day, fact.value);
1747
+ // A single authoring sub-Run can create several sibling SPECs. Keep the
1748
+ // Run canonical and attach deterministic shares only to the macro read
1749
+ // model. Payload rows stay outside `activities`, preventing global
1750
+ // Run/Cost totals from counting that Run more than once.
1751
+ const creationSpecsByRun = new Map<string, CanonicalEntity[]>();
1752
+ for (const entity of canonicalEntities.values()) {
1753
+ if (entity.kind !== "spec" || entity.creationRunId === null) continue;
1754
+ creationSpecsByRun.set(entity.creationRunId, [
1755
+ ...(creationSpecsByRun.get(entity.creationRunId) ?? []),
1756
+ entity,
1757
+ ]);
1758
+ }
1759
+ const materializeSharedCreationAllocations = () => {
1760
+ for (const [runId, recipients] of creationSpecsByRun) {
1761
+ if (recipients.length === 0) continue;
1762
+ const source = folds.get(runId);
1763
+ if (source === undefined) continue;
1764
+ const { fold, row } = source;
1765
+ const started = fold.run;
1766
+ const usage = fold.effective_usage_records;
1767
+ const canonicalActivity = database
1768
+ .prepare(
1769
+ "SELECT duration_ms, duration_provenance, source_started_at, source_finished_at FROM zaip_aios_activities WHERE run_id=? AND payload_type IS NULL LIMIT 1",
1770
+ )
1771
+ .get(runId) as
1772
+ | {
1773
+ duration_ms: number | null;
1774
+ duration_provenance: string | null;
1775
+ source_started_at: string | null;
1776
+ source_finished_at: string | null;
1777
+ }
1778
+ | undefined;
1779
+ const cost = aggregateCost(usage, fold, pricingCatalog, fxRate);
1780
+ const recipientIds = recipients.map((recipient) => recipient.id);
1781
+ // The allocator owns the only partition. Read the already-materialized
1782
+ // canonical activity fact after attributed-token-active reconciliation;
1783
+ // never use the wider JSONL session span or a pre-divided value.
1784
+ const sharedCreationDuration = canonicalActivity?.duration_ms ?? null;
1785
+ const allocations = {
1786
+ source_duration_ms: allocateSharedCreationValue(sharedCreationDuration, recipientIds),
1787
+ // Shared creation attribution uses only the observed source interval;
1788
+ // never propagate an effective/legacy aggregate that can include
1789
+ // more than the creation segment.
1790
+ duration_ms: allocateSharedCreationValue(sharedCreationDuration, recipientIds),
1791
+ input_tokens: allocateSharedCreationValue(knownSum(usage, "input"), recipientIds),
1792
+ cached_input_tokens: allocateSharedCreationValue(
1793
+ knownSum(usage, "cached_input"),
1794
+ recipientIds,
1795
+ ),
1796
+ output_tokens: allocateSharedCreationValue(knownSum(usage, "output"), recipientIds),
1797
+ reasoning_tokens: allocateSharedCreationValue(
1798
+ knownSum(usage, "reasoning"),
1799
+ recipientIds,
1800
+ ),
1801
+ total_tokens: allocateSharedCreationValue(knownSum(usage, "total"), recipientIds),
1802
+ cost_usd: allocateSharedCreationValue(cost.usd, recipientIds),
1803
+ cost_brl: allocateSharedCreationValue(cost.brl, recipientIds),
1804
+ };
1805
+ for (const recipient of recipients) {
1806
+ const attributed =
1807
+ usage.find((record) => record.attributed_spec_id === recipient.id) ?? null;
1808
+ const attributedDuration =
1809
+ attributed?.source_started_at && attributed?.source_finished_at
1810
+ ? Math.max(
1811
+ 0,
1812
+ Date.parse(attributed.source_finished_at) -
1813
+ Date.parse(attributed.source_started_at),
1814
+ )
1815
+ : null;
1816
+ const activityRowId = `ACTV-${createHash("sha256")
1817
+ .update(`shared-creation:${runId}:${recipient.id}`)
1818
+ .digest("hex")
1819
+ .slice(0, 24)}`;
1820
+ const lastRecord = fold.records.at(-1);
1821
+ const allocation = {
1822
+ activity_row_id: activityRowId,
1823
+ activity_source_kind: "shared_creation_allocation",
1824
+ activity_key: `shared-creation:${runId}:${recipient.id}`,
1825
+ source_path: row.path,
1826
+ source_hash: row.content_sha256,
1827
+ source_occurred_at: started.occurred_at,
1828
+ recorded_at: lastRecord?.occurred_at ?? started.occurred_at,
1829
+ version_activity_id: null,
1830
+ source_last_event_id: lastRecord?.event_id ?? null,
1831
+ source_last_sequence: lastRecord?.sequence ?? null,
1832
+ work_item_kind: "spec",
1833
+ title: recipient.title,
1834
+ work_item_status: recipient.status,
1835
+ work_stage: recipient.stage,
1836
+ risk: recipient.risk,
1837
+ run_role: "shared_creation_allocation",
1838
+ root_run_id: rootRunId(fold),
1839
+ parent_run_id: started.parent_run_id,
1840
+ agent_run_id: null,
1841
+ root_agent_run_id: null,
1842
+ parent_agent_run_id: null,
1843
+ agent_run_role: null,
1844
+ delegation_depth: started.delegation_depth ?? null,
1845
+ agent_invocation_id: null,
1846
+ root_agent_invocation_id: null,
1847
+ parent_agent_invocation_id: null,
1848
+ invocation_kind: null,
1849
+ execution_id: started.execution_id,
1850
+ task_id: null,
1851
+ spec_id: recipient.id,
1852
+ prd_id: null,
1853
+ change_id: null,
1854
+ conversation_id: started.conversation_id,
1855
+ run_id: started.run_id,
1856
+ occurred_at: started.occurred_at,
1857
+ occurred_day_sp: saoPauloDay(started.occurred_at),
1858
+ execution_kind: started.execution_kind ?? null,
1859
+ entrypoint: started.entrypoint,
1860
+ work_type: started.work_type,
1861
+ phase: started.phase,
1862
+ purpose: started.run_purpose ?? null,
1863
+ status: fold.status,
1864
+ activity_kind: "planning",
1865
+ zaip_agent: started.agent_name ?? started.agent,
1866
+ zaip_agent_persona: null,
1867
+ zaip_agent_job: null,
1868
+ zaip_agent_role: null,
1869
+ agent_catalog_id: null,
1870
+ agent_catalog_hash: catalogHash,
1871
+ client: started.client,
1872
+ provider: null,
1873
+ model: null,
1874
+ service_tier: null,
1875
+ reasoning_effort: started.reasoning_effort ?? null,
1876
+ developer_account_id: null,
1877
+ developer_login: null,
1878
+ developer_attribution_source: null,
1879
+ source_duration_ms:
1880
+ attributedDuration ?? allocations.source_duration_ms.get(recipient.id) ?? null,
1881
+ source_started_at:
1882
+ attributed?.source_started_at ?? canonicalActivity?.source_started_at ?? null,
1883
+ source_finished_at:
1884
+ attributed?.source_finished_at ?? canonicalActivity?.source_finished_at ?? null,
1885
+ duration_ms: attributedDuration ?? allocations.duration_ms.get(recipient.id) ?? null,
1886
+ duration_provenance: attributed
1887
+ ? "explicit_spec_turn_recovery"
1888
+ : "shared_creation_allocation",
1889
+ input_tokens:
1890
+ attributed?.tokens.input ?? allocations.input_tokens.get(recipient.id) ?? null,
1891
+ cached_input_tokens:
1892
+ attributed?.tokens.cached_input ??
1893
+ allocations.cached_input_tokens.get(recipient.id) ??
1894
+ null,
1895
+ output_tokens:
1896
+ attributed?.tokens.output ?? allocations.output_tokens.get(recipient.id) ?? null,
1897
+ reasoning_tokens:
1898
+ attributed?.tokens.reasoning ??
1899
+ allocations.reasoning_tokens.get(recipient.id) ??
1900
+ null,
1901
+ total_tokens:
1902
+ attributed?.tokens.total ?? allocations.total_tokens.get(recipient.id) ?? null,
1903
+ token_source: consolidatedValue(usage.map((record) => record.tokens.source)),
1904
+ usage_event_count: usage.length,
1905
+ llm_call_count: null,
1906
+ cost_usd: attributed?.cost.usd ?? allocations.cost_usd.get(recipient.id) ?? null,
1907
+ cost_brl: attributed?.cost.brl ?? allocations.cost_brl.get(recipient.id) ?? null,
1908
+ cost_method: cost.method,
1909
+ pricing_model: cost.pricingModel,
1910
+ pricing_source: cost.pricingSource,
1911
+ fx_rate: cost.fxRate,
1912
+ source_event_count: fold.records.length,
1913
+ root_run_duration: null,
1914
+ sub_run_duration: null,
1915
+ payload_type: "shared_creation_allocation",
1916
+ payload_json: JSON.stringify({
1917
+ provenance: attributed
1918
+ ? "explicit_spec_turn_recovery"
1919
+ : "shared_creation_allocation",
1920
+ base_duration_provenance: canonicalActivity?.duration_provenance ?? null,
1921
+ creation_run_id: runId,
1922
+ spec_id: recipient.id,
1923
+ }),
1924
+ } satisfies Record<(typeof columns)[number], string | number | null>;
1925
+ canonicalInsertActivity.run(...columns.map((column) => allocation[column] ?? null));
1926
+ }
1927
+ }
1928
+ };
1820
1929
  database.exec(`UPDATE zaip_aios_activities
1821
1930
  SET duration_ms = (
1822
1931
  SELECT SUM(f.attributed_token_active_ms)
@@ -1825,7 +1934,9 @@ export async function rebuildSqliteReadModel(
1825
1934
  ),
1826
1935
  duration_provenance = CASE
1827
1936
  WHEN EXISTS (SELECT 1 FROM attributed_token_active_facts f WHERE f.activity_row_id=zaip_aios_activities.activity_row_id)
1828
- THEN 'attributed_token_active' ELSE NULL END;`);
1937
+ THEN 'attributed_token_active' ELSE NULL END
1938
+ WHERE payload_type IS NULL;`);
1939
+ materializeSharedCreationAllocations();
1829
1940
  const canonicalRunCount = folds.size;
1830
1941
  const invocationCount = (
1831
1942
  database
@@ -1906,7 +2017,7 @@ export async function rebuildSqliteReadModel(
1906
2017
  insertConfiguredModel.run(configuredModel.id);
1907
2018
  insertConfiguredProvider.run(configuredModel.provider);
1908
2019
  }
1909
- const costDimensionAggregates = buildCostDimensionAggregates(database);
2020
+ const costDimensionAggregates = buildCostDimensionAggregates(database, agentProfiles);
1910
2021
  // The rebuild scratch tables above are intentionally TEMP only. Persist
1911
2022
  // their technical inputs as typed payload activities so the on-disk
1912
2023
  // model has one fact plus its three declared dimensions, never hidden
@@ -1929,11 +2040,11 @@ export async function rebuildSqliteReadModel(
1929
2040
  );
1930
2041
  persistPayload(
1931
2042
  "file",
1932
- "SELECT path AS row_key, json_object('path', path, 'content', content, 'mtime_ms', mtime_ms, 'size_bytes', size_bytes, 'content_sha256', content_sha256, 'artifact_kind', artifact_kind, 'artifact_id', artifact_id, 'artifact_occurred_at', artifact_occurred_at, 'artifact_occurred_day_sp', artifact_occurred_day_sp) AS payload_json FROM files",
2043
+ "SELECT path AS row_key, json_object('path', path, 'content', content, 'mtime_ms', mtime_ms, 'size_bytes', size_bytes, 'content_sha256', content_sha256, 'artifact_kind', artifact_kind, 'artifact_id', artifact_id, 'artifact_occurred_at', artifact_occurred_at, 'artifact_occurred_day_sp', artifact_occurred_day_sp, 'artifact_task_id', artifact_task_id, 'artifact_change_id', artifact_change_id, 'artifact_run_id', artifact_run_id) AS payload_json FROM files",
1933
2044
  );
1934
2045
  persistPayload(
1935
2046
  "work_item",
1936
- "SELECT entity_kind || ':' || id AS row_key, json_object('entity_kind', entity_kind, 'id', id, 'title', title, 'status', status, 'stage', stage, 'risk', risk, 'occurred_at', occurred_at, 'occurred_day_sp', occurred_day_sp) AS payload_json FROM canonical_work_items",
2047
+ "SELECT entity_kind || ':' || id AS row_key, json_object('entity_kind', entity_kind, 'id', id, 'title', title, 'status', status, 'stage', stage, 'risk', risk, 'spec_id', spec_id, 'change_id', change_id, 'completed_run_id', completed_run_id, 'occurred_at', occurred_at, 'occurred_day_sp', occurred_day_sp) AS payload_json FROM canonical_work_items",
1937
2048
  );
1938
2049
  persistPayload(
1939
2050
  "execution",
@@ -1991,6 +2102,27 @@ export async function rebuildSqliteReadModel(
1991
2102
  );
1992
2103
  }
1993
2104
  }
2105
+ for (const [dimension, projection] of Object.entries(costDimensionAggregates.remote)) {
2106
+ for (const row of projection.rows) {
2107
+ insertCostDimensionAggregate.run(
2108
+ `TECH-${randomUUID().toLowerCase()}`,
2109
+ `cost_dimension_aggregate_v1:${dimension}:${row.key}`,
2110
+ `sqlite://cost_dimension_aggregate_v1/${dimension}`,
2111
+ JSON.stringify({ schema_version: 1, dimension, row_kind: "row", ...row }),
2112
+ );
2113
+ }
2114
+ insertCostDimensionAggregate.run(
2115
+ `TECH-${randomUUID().toLowerCase()}`,
2116
+ `cost_dimension_aggregate_v1:${dimension}:__totals__`,
2117
+ `sqlite://cost_dimension_aggregate_v1/${dimension}`,
2118
+ JSON.stringify({
2119
+ schema_version: 1,
2120
+ dimension,
2121
+ row_kind: "totals",
2122
+ ...projection.totals,
2123
+ }),
2124
+ );
2125
+ }
1994
2126
  const viewSql = `
1995
2127
  CREATE INDEX zaip_activities_agent_run ON zaip_aios_activities(activity_source_kind, agent_run_id, occurred_day_sp, occurred_at DESC);
1996
2128
  CREATE INDEX zaip_activities_relationships ON zaip_aios_activities(task_id, spec_id, prd_id, execution_id, conversation_id);
@@ -1998,7 +2130,7 @@ export async function rebuildSqliteReadModel(
1998
2130
  CREATE INDEX zaip_activities_spec_day ON zaip_aios_activities(payload_type, spec_id, occurred_day_sp) WHERE spec_id IS NOT NULL;
1999
2131
  CREATE INDEX zaip_activities_prd_day ON zaip_aios_activities(payload_type, prd_id, occurred_day_sp) WHERE prd_id IS NOT NULL;
2000
2132
  CREATE INDEX zaip_activities_agent ON zaip_aios_activities(zaip_agent, zaip_agent_persona, zaip_agent_job, provider, model);
2001
- CREATE UNIQUE INDEX zaip_activities_run_id ON zaip_aios_activities(run_id);
2133
+ CREATE UNIQUE INDEX zaip_activities_run_id ON zaip_aios_activities(run_id) WHERE payload_type IS NULL;
2002
2134
  CREATE INDEX zaip_activities_run_hierarchy ON zaip_aios_activities(root_run_id, parent_run_id, run_role);
2003
2135
  CREATE INDEX zaip_activities_period ON zaip_aios_activities(occurred_day_sp, occurred_at DESC);
2004
2136
  -- Dimension residuals first restrict the visible period, then group
@@ -2008,17 +2140,17 @@ export async function rebuildSqliteReadModel(
2008
2140
  WHERE agent_invocation_id IS NOT NULL;
2009
2141
  CREATE INDEX zaip_activities_status ON zaip_aios_activities(activity_source_kind, status, phase);
2010
2142
  CREATE INDEX zaip_activities_payload ON zaip_aios_activities(payload_type, activity_key);
2011
- CREATE VIEW files AS SELECT json_extract(payload_json, '$.path') AS path, json_extract(payload_json, '$.content') AS content, json_extract(payload_json, '$.mtime_ms') AS mtime_ms, json_extract(payload_json, '$.size_bytes') AS size_bytes, json_extract(payload_json, '$.content_sha256') AS content_sha256, json_extract(payload_json, '$.artifact_kind') AS artifact_kind, json_extract(payload_json, '$.artifact_id') AS artifact_id, json_extract(payload_json, '$.artifact_occurred_at') AS artifact_occurred_at, json_extract(payload_json, '$.artifact_occurred_day_sp') AS artifact_occurred_day_sp FROM zaip_aios_activities WHERE payload_type='file';
2143
+ CREATE VIEW files AS SELECT json_extract(payload_json, '$.path') AS path, json_extract(payload_json, '$.content') AS content, json_extract(payload_json, '$.mtime_ms') AS mtime_ms, json_extract(payload_json, '$.size_bytes') AS size_bytes, json_extract(payload_json, '$.content_sha256') AS content_sha256, json_extract(payload_json, '$.artifact_kind') AS artifact_kind, json_extract(payload_json, '$.artifact_id') AS artifact_id, json_extract(payload_json, '$.artifact_occurred_at') AS artifact_occurred_at, json_extract(payload_json, '$.artifact_occurred_day_sp') AS artifact_occurred_day_sp, json_extract(payload_json, '$.artifact_task_id') AS artifact_task_id, json_extract(payload_json, '$.artifact_change_id') AS artifact_change_id, json_extract(payload_json, '$.artifact_run_id') AS artifact_run_id FROM zaip_aios_activities WHERE payload_type='file';
2012
2144
  CREATE VIEW read_model_source_state AS SELECT json_extract(payload_json, '$.source_path') AS source_path, json_extract(payload_json, '$.source_hash') AS source_hash, json_extract(payload_json, '$.mtime_ms') AS mtime_ms, json_extract(payload_json, '$.size_bytes') AS size_bytes, json_extract(payload_json, '$.source_kind') AS source_kind, json_extract(payload_json, '$.last_processed_at') AS last_processed_at FROM zaip_aios_activities WHERE payload_type='source_state';
2013
2145
  CREATE VIEW read_model_metadata AS SELECT json_extract(payload_json, '$.key') AS key, json_extract(payload_json, '$.value') AS value FROM zaip_aios_activities WHERE payload_type='metadata';
2014
- CREATE VIEW canonical_work_items AS SELECT json_extract(payload_json, '$.entity_kind') AS entity_kind, json_extract(payload_json, '$.id') AS id, json_extract(payload_json, '$.title') AS title, json_extract(payload_json, '$.status') AS status, json_extract(payload_json, '$.stage') AS stage, json_extract(payload_json, '$.risk') AS risk, json_extract(payload_json, '$.occurred_at') AS occurred_at, json_extract(payload_json, '$.occurred_day_sp') AS occurred_day_sp FROM zaip_aios_activities WHERE payload_type='work_item';
2146
+ CREATE VIEW canonical_work_items AS SELECT json_extract(payload_json, '$.entity_kind') AS entity_kind, json_extract(payload_json, '$.id') AS id, json_extract(payload_json, '$.title') AS title, json_extract(payload_json, '$.status') AS status, json_extract(payload_json, '$.stage') AS stage, json_extract(payload_json, '$.risk') AS risk, json_extract(payload_json, '$.spec_id') AS spec_id, json_extract(payload_json, '$.change_id') AS change_id, json_extract(payload_json, '$.completed_run_id') AS completed_run_id, json_extract(payload_json, '$.occurred_at') AS occurred_at, json_extract(payload_json, '$.occurred_day_sp') AS occurred_day_sp FROM zaip_aios_activities WHERE payload_type='work_item';
2015
2147
  CREATE VIEW canonical_executions AS SELECT json_extract(payload_json, '$.id') AS id, json_extract(payload_json, '$.status') AS status, json_extract(payload_json, '$.kind') AS kind, json_extract(payload_json, '$.origin_type') AS origin_type, json_extract(payload_json, '$.origin_id') AS origin_id, json_extract(payload_json, '$.created_at') AS created_at, json_extract(payload_json, '$.occurred_day_sp') AS occurred_day_sp, json_extract(payload_json, '$.finished_at') AS finished_at, json_extract(payload_json, '$.source_path') AS source_path FROM zaip_aios_activities WHERE payload_type='execution';
2016
2148
  CREATE VIEW canonical_changes AS SELECT json_extract(payload_json, '$.id') AS id, json_extract(payload_json, '$.title') AS title, json_extract(payload_json, '$.status') AS status, json_extract(payload_json, '$.risk') AS risk, json_extract(payload_json, '$.source_type') AS source_type, json_extract(payload_json, '$.source_id') AS source_id, json_extract(payload_json, '$.source_spec_id') AS source_spec_id, json_extract(payload_json, '$.source_prd_id') AS source_prd_id, json_extract(payload_json, '$.requirement_ids_json') AS requirement_ids_json, json_extract(payload_json, '$.acceptance_ids_json') AS acceptance_ids_json, json_extract(payload_json, '$.task_ids_json') AS task_ids_json, json_extract(payload_json, '$.result') AS result, json_extract(payload_json, '$.created_at') AS created_at, json_extract(payload_json, '$.occurred_day_sp') AS occurred_day_sp, json_extract(payload_json, '$.source_path') AS source_path FROM zaip_aios_activities WHERE payload_type='change';
2017
2149
  CREATE VIEW canonical_decisions AS SELECT json_extract(payload_json, '$.id') AS id, json_extract(payload_json, '$.title') AS title, json_extract(payload_json, '$.origin') AS origin, json_extract(payload_json, '$.scope_type') AS scope_type, json_extract(payload_json, '$.scope_id') AS scope_id, json_extract(payload_json, '$.created_at') AS created_at, json_extract(payload_json, '$.occurred_day_sp') AS occurred_day_sp, json_extract(payload_json, '$.source_path') AS source_path FROM zaip_aios_activities WHERE payload_type='decision';
2018
2150
  CREATE VIEW canonical_run_index AS SELECT json_extract(payload_json, '$.run_id') AS run_id, json_extract(payload_json, '$.occurred_at') AS occurred_at, json_extract(payload_json, '$.occurred_day_sp') AS occurred_day_sp, json_extract(payload_json, '$.task_id') AS task_id, json_extract(payload_json, '$.phase') AS phase, json_extract(payload_json, '$.run_role') AS run_role, json_extract(payload_json, '$.duration_ms') AS duration_ms, json_extract(payload_json, '$.source_event_count') AS source_event_count, json_extract(payload_json, '$.zaip_agent') AS zaip_agent, json_extract(payload_json, '$.provider') AS provider, json_extract(payload_json, '$.model') AS model, json_extract(payload_json, '$.status') AS status FROM zaip_aios_activities WHERE payload_type='run_index';
2019
2151
  CREATE VIEW invocation_usage_segments AS SELECT json_extract(payload_json, '$.agent_invocation_id') AS agent_invocation_id, json_extract(payload_json, '$.provider') AS provider, json_extract(payload_json, '$.model') AS model, json_extract(payload_json, '$.service_tier') AS service_tier, json_extract(payload_json, '$.reasoning_effort') AS reasoning_effort, json_extract(payload_json, '$.input_tokens') AS input_tokens, json_extract(payload_json, '$.cached_input_tokens') AS cached_input_tokens, json_extract(payload_json, '$.uncached_input_tokens') AS uncached_input_tokens, json_extract(payload_json, '$.output_tokens') AS output_tokens, json_extract(payload_json, '$.reasoning_tokens') AS reasoning_tokens, json_extract(payload_json, '$.visible_output_tokens') AS visible_output_tokens, json_extract(payload_json, '$.total_tokens') AS total_tokens, json_extract(payload_json, '$.input_rate_usd_per_million') AS input_rate_usd_per_million, json_extract(payload_json, '$.cached_input_rate_usd_per_million') AS cached_input_rate_usd_per_million, json_extract(payload_json, '$.output_rate_usd_per_million') AS output_rate_usd_per_million, json_extract(payload_json, '$.reasoning_rate_usd_per_million') AS reasoning_rate_usd_per_million, json_extract(payload_json, '$.context_input_multiplier') AS context_input_multiplier, json_extract(payload_json, '$.context_output_multiplier') AS context_output_multiplier, json_extract(payload_json, '$.speed_cost_multiplier') AS speed_cost_multiplier, json_extract(payload_json, '$.pricing_scenario') AS pricing_scenario, json_extract(payload_json, '$.standard_cost_usd') AS standard_cost_usd, json_extract(payload_json, '$.speed_uplift_usd') AS speed_uplift_usd, json_extract(payload_json, '$.input_cost_usd') AS input_cost_usd, json_extract(payload_json, '$.cached_input_cost_usd') AS cached_input_cost_usd, json_extract(payload_json, '$.output_cost_usd') AS output_cost_usd, json_extract(payload_json, '$.reasoning_cost_usd') AS reasoning_cost_usd, json_extract(payload_json, '$.calculated_cost_usd') AS calculated_cost_usd, json_extract(payload_json, '$.recorded_cost_usd') AS recorded_cost_usd, json_extract(payload_json, '$.cost_variance_usd') AS cost_variance_usd, json_extract(payload_json, '$.cost_usd') AS cost_usd, json_extract(payload_json, '$.cost_brl') AS cost_brl, json_extract(payload_json, '$.cost_method') AS cost_method, json_extract(payload_json, '$.pricing_model') AS pricing_model, json_extract(payload_json, '$.pricing_source') AS pricing_source, json_extract(payload_json, '$.fx_rate') AS fx_rate FROM zaip_aios_activities WHERE payload_type='usage_segment';
2020
2152
  CREATE VIEW attributed_token_active_facts AS SELECT json_extract(payload_json, '$.activity_row_id') AS activity_row_id, json_extract(payload_json, '$.occurred_day_sp') AS occurred_day_sp, json_extract(payload_json, '$.attributed_token_active_ms') AS attributed_token_active_ms FROM zaip_aios_activities WHERE payload_type='duration_allocation';
2021
- CREATE VIEW canonical_artifacts AS SELECT artifact_kind, artifact_id, artifact_occurred_at AS occurred_at, artifact_occurred_day_sp AS occurred_day_sp, path AS source_path FROM files WHERE artifact_kind IS NOT NULL AND artifact_occurred_at IS NOT NULL;
2153
+ CREATE VIEW canonical_artifacts AS SELECT artifact_kind, artifact_id, artifact_occurred_at AS occurred_at, artifact_occurred_day_sp AS occurred_day_sp, artifact_task_id AS task_id, artifact_change_id AS change_id, artifact_run_id AS run_id, path AS source_path FROM files WHERE artifact_kind IS NOT NULL AND artifact_occurred_at IS NOT NULL;
2022
2154
  CREATE VIEW activities AS
2023
2155
  SELECT a.*, f.attributed_token_active_ms
2024
2156
  FROM zaip_aios_activities a
@@ -2027,6 +2159,19 @@ export async function rebuildSqliteReadModel(
2027
2159
  FROM attributed_token_active_facts GROUP BY activity_row_id
2028
2160
  ) f USING(activity_row_id)
2029
2161
  WHERE a.payload_type IS NULL;
2162
+ -- Macro-only allocations are persisted as technical payloads to keep
2163
+ -- the global Run population unique. SPEC summaries opt into this
2164
+ -- projection explicitly; all other activity and cost views do not.
2165
+ CREATE VIEW shared_creation_allocations AS
2166
+ SELECT a.*, NULL AS attributed_token_active_ms
2167
+ FROM zaip_aios_activities a WHERE payload_type='shared_creation_allocation';
2168
+ CREATE VIEW macro_spec_activities AS
2169
+ SELECT a.* FROM activities a WHERE a.spec_id IS NOT NULL
2170
+ AND NOT EXISTS (
2171
+ SELECT 1 FROM shared_creation_allocations allocation
2172
+ WHERE allocation.spec_id=a.spec_id AND allocation.run_id=a.run_id
2173
+ )
2174
+ UNION ALL SELECT * FROM shared_creation_allocations;
2030
2175
  CREATE VIEW attributed_token_active AS
2031
2176
  SELECT f.activity_row_id, f.occurred_day_sp, f.attributed_token_active_ms,
2032
2177
  a.run_id, a.execution_id, a.task_id, a.spec_id, a.prd_id, a.conversation_id,
@@ -2162,7 +2307,10 @@ export async function rebuildSqliteReadModel(
2162
2307
  SUM(sub_run_duration) AS total_sub_run_duration,
2163
2308
  SUM(total_tokens) AS total_tokens,
2164
2309
  SUM(cost_usd) AS total_cost_usd,
2165
- SUM(cost_brl) AS total_cost_brl
2310
+ SUM(cost_brl) AS total_cost_brl,
2311
+ COUNT(total_tokens) AS token_known_runs,
2312
+ COUNT(cost_usd) AS usd_known_runs,
2313
+ COUNT(cost_brl) AS brl_known_runs
2166
2314
  FROM activities
2167
2315
  GROUP BY date(COALESCE(source_started_at, source_occurred_at, occurred_at), '-3 hours')
2168
2316
  ), cost_monitor_financial_daily AS (
@@ -2273,7 +2421,10 @@ export async function rebuildSqliteReadModel(
2273
2421
  o.total_duration AS total_duration,
2274
2422
  f.total_tokens,
2275
2423
  f.total_cost_usd,
2276
- f.total_cost_brl
2424
+ f.total_cost_brl,
2425
+ i.token_known_runs,
2426
+ i.usd_known_runs,
2427
+ i.brl_known_runs
2277
2428
  FROM days d
2278
2429
  LEFT JOIN work_item_daily w USING(day)
2279
2430
  LEFT JOIN artifact_daily a USING(day)
@@ -2302,7 +2453,7 @@ export async function rebuildSqliteReadModel(
2302
2453
  -- population rather than Operations' broader Run/activity totals.
2303
2454
  CREATE VIEW cost_monitor_overview AS SELECT period_kind, period_key, cost_monitor_invocation_count AS total_run_count, cost_monitor_invocation_count AS invocation_count, llm_call_count, source_event_count, cost_monitor_total_duration, total_tokens, total_cost_usd, total_cost_brl FROM overview;
2304
2455
  CREATE VIEW task_summary AS SELECT t.id, t.title, t.status, t.risk, t.occurred_at, t.occurred_day_sp, COUNT(i.run_id) AS invocation_count, SUM(i.llm_call_count) AS llm_call_count, SUM(i.duration_ms) AS total_duration, SUM(i.total_tokens) AS total_tokens, SUM(i.cost_usd) AS total_cost_usd, SUM(i.cost_brl) AS total_cost_brl FROM tasks t LEFT JOIN activities i ON i.task_id = t.id GROUP BY t.id, t.title, t.status, t.risk, t.occurred_at, t.occurred_day_sp;
2305
- CREATE VIEW spec_summary AS SELECT s.id, s.title, s.status, s.risk, s.occurred_at, s.occurred_day_sp, COUNT(i.run_id) AS invocation_count, SUM(i.llm_call_count) AS llm_call_count, SUM(i.duration_ms) AS total_duration, SUM(i.total_tokens) AS total_tokens, SUM(i.cost_usd) AS total_cost_usd, SUM(i.cost_brl) AS total_cost_brl FROM specs s LEFT JOIN activities i ON i.spec_id = s.id GROUP BY s.id, s.title, s.status, s.risk, s.occurred_at, s.occurred_day_sp;
2456
+ CREATE VIEW spec_summary AS SELECT s.id, s.title, s.status, s.risk, s.occurred_at, s.occurred_day_sp, COUNT(i.run_id) AS invocation_count, SUM(i.llm_call_count) AS llm_call_count, SUM(i.duration_ms) AS total_duration, SUM(i.total_tokens) AS total_tokens, SUM(i.cost_usd) AS total_cost_usd, SUM(i.cost_brl) AS total_cost_brl FROM specs s LEFT JOIN macro_spec_activities i ON i.spec_id = s.id GROUP BY s.id, s.title, s.status, s.risk, s.occurred_at, s.occurred_day_sp;
2306
2457
  CREATE VIEW prd_summary AS SELECT p.id, p.title, p.status, p.risk, p.occurred_at, p.occurred_day_sp, COUNT(i.run_id) AS invocation_count, SUM(i.llm_call_count) AS llm_call_count, SUM(i.duration_ms) AS total_duration, SUM(i.total_tokens) AS total_tokens, SUM(i.cost_usd) AS total_cost_usd, SUM(i.cost_brl) AS total_cost_brl FROM prds p LEFT JOIN activities i ON i.prd_id = p.id GROUP BY p.id, p.title, p.status, p.risk, p.occurred_at, p.occurred_day_sp;
2307
2458
  CREATE VIEW cost_monitor_group_daily AS SELECT * FROM cost_monitor_group_daily_cache;
2308
2459
  CREATE VIEW vw_overview AS SELECT * FROM overview;
@@ -2426,7 +2577,7 @@ export async function rebuildSqliteReadModel(
2426
2577
  CREATE VIEW vw_cost_models AS WITH usage AS (SELECT model, agent_invocation_id, SUM(total_tokens) AS total_tokens, SUM(cost_usd) AS cost_usd, SUM(cost_brl) AS cost_brl FROM invocation_usage GROUP BY model, agent_invocation_id), calls AS (SELECT i.agent_invocation_id, CASE WHEN (SELECT COUNT(DISTINCT m.model) FROM invocation_usage m WHERE m.agent_invocation_id = i.agent_invocation_id AND m.model IS NOT NULL) = 1 THEN i.llm_call_count END AS llm_call_count FROM invocations i) SELECT u.model, SUM(c.llm_call_count) AS llm_call_count, SUM(u.total_tokens) AS total_tokens, SUM(u.cost_usd) AS cost_usd, SUM(u.cost_brl) AS cost_brl, COUNT(*) AS invocation_count FROM usage u JOIN calls c USING(agent_invocation_id) GROUP BY u.model;
2427
2578
  CREATE VIEW vw_cost_providers AS WITH usage AS (SELECT provider, agent_invocation_id, SUM(total_tokens) AS total_tokens, SUM(cost_usd) AS cost_usd, SUM(cost_brl) AS cost_brl FROM invocation_usage GROUP BY provider, agent_invocation_id), calls AS (SELECT i.agent_invocation_id, CASE WHEN (SELECT COUNT(DISTINCT p.provider) FROM invocation_usage p WHERE p.agent_invocation_id = i.agent_invocation_id AND p.provider IS NOT NULL) = 1 THEN i.llm_call_count END AS llm_call_count FROM invocations i) SELECT u.provider, SUM(c.llm_call_count) AS llm_call_count, SUM(u.total_tokens) AS total_tokens, SUM(u.cost_usd) AS cost_usd, SUM(u.cost_brl) AS cost_brl, COUNT(*) AS invocation_count FROM usage u JOIN calls c USING(agent_invocation_id) GROUP BY u.provider;
2428
2579
  CREATE VIEW vw_cost_tasks AS SELECT i.task_id, COALESCE(MAX(t.title), i.task_id) AS title, MAX(t.status) AS status, MAX(t.risk) AS risk, MIN(i.occurred_at) AS occurred_at, MIN(i.occurred_day_sp) AS occurred_day_sp, SUM(i.llm_call_count) AS llm_call_count, SUM(i.duration_ms) AS total_duration, SUM(i.total_tokens) AS total_tokens, SUM(i.cost_usd) AS total_cost_usd, SUM(i.cost_brl) AS total_cost_brl, COUNT(*) AS invocation_count FROM invocations i LEFT JOIN tasks t ON t.id = i.task_id WHERE i.task_id IS NOT NULL GROUP BY i.task_id;
2429
- CREATE VIEW vw_cost_specs AS SELECT s.id AS spec_id, s.title, s.status, s.risk, COALESCE(MIN(i.occurred_at), s.occurred_at) AS occurred_at, COALESCE(MIN(i.occurred_day_sp), s.occurred_day_sp) AS occurred_day_sp, SUM(i.llm_call_count) AS llm_call_count, SUM(i.duration_ms) AS total_duration, SUM(i.total_tokens) AS total_tokens, SUM(i.cost_usd) AS total_cost_usd, SUM(i.cost_brl) AS total_cost_brl, COUNT(i.agent_invocation_id) AS invocation_count FROM specs s LEFT JOIN invocations i ON i.spec_id = s.id GROUP BY s.id, s.title, s.status, s.risk, s.occurred_at, s.occurred_day_sp;
2580
+ CREATE VIEW vw_cost_specs AS SELECT s.id AS spec_id, s.title, s.status, s.risk, COALESCE(MIN(i.occurred_at), s.occurred_at) AS occurred_at, COALESCE(MIN(i.occurred_day_sp), s.occurred_day_sp) AS occurred_day_sp, SUM(i.llm_call_count) AS llm_call_count, SUM(i.duration_ms) AS total_duration, SUM(i.total_tokens) AS total_tokens, SUM(i.cost_usd) AS total_cost_usd, SUM(i.cost_brl) AS total_cost_brl, COUNT(i.run_id) AS invocation_count FROM specs s LEFT JOIN macro_spec_activities i ON i.spec_id = s.id GROUP BY s.id, s.title, s.status, s.risk, s.occurred_at, s.occurred_day_sp;
2430
2581
  CREATE VIEW vw_cost_prds AS SELECT p.id AS prd_id, p.title, p.status, p.risk, COALESCE(MIN(i.occurred_at), p.occurred_at) AS occurred_at, COALESCE(MIN(i.occurred_day_sp), p.occurred_day_sp) AS occurred_day_sp, SUM(i.llm_call_count) AS llm_call_count, SUM(i.duration_ms) AS total_duration, SUM(i.total_tokens) AS total_tokens, SUM(i.cost_usd) AS total_cost_usd, SUM(i.cost_brl) AS total_cost_brl, COUNT(i.agent_invocation_id) AS invocation_count FROM prds p LEFT JOIN invocations i ON i.prd_id = p.id GROUP BY p.id, p.title, p.status, p.risk, p.occurred_at, p.occurred_day_sp;
2431
2582
  CREATE VIEW vw_cost_conversations AS SELECT i.conversation_id, i.conversation_id AS title, MAX(c.status) AS status, NULL AS risk, MIN(i.occurred_at) AS occurred_at, MIN(i.occurred_day_sp) AS occurred_day_sp, SUM(i.llm_call_count) AS llm_call_count, SUM(i.duration_ms) AS total_duration, SUM(i.total_tokens) AS total_tokens, SUM(i.cost_usd) AS total_cost_usd, SUM(i.cost_brl) AS total_cost_brl, COUNT(*) AS invocation_count FROM invocations i LEFT JOIN conversations c ON c.id = i.conversation_id WHERE i.conversation_id IS NOT NULL GROUP BY i.conversation_id;
2432
2583
  CREATE VIEW vw_operations_work_list AS SELECT * FROM vw_entities;
@@ -2449,7 +2600,7 @@ export async function rebuildSqliteReadModel(
2449
2600
  GROUP BY spec_id, prd_id
2450
2601
  ), entity_activity AS (
2451
2602
  SELECT 'task' AS entity_kind, task_id AS entity_id, * FROM activities WHERE task_id IS NOT NULL
2452
- UNION ALL SELECT 'spec', spec_id, * FROM activities WHERE spec_id IS NOT NULL
2603
+ UNION ALL SELECT 'spec', spec_id, * FROM macro_spec_activities
2453
2604
  UNION ALL SELECT 'prd', prd_id, * FROM activities WHERE prd_id IS NOT NULL
2454
2605
  UNION ALL
2455
2606
  SELECT 'task', l.task_id, a.*
@@ -2688,7 +2839,7 @@ export async function rebuildSqliteReadModel(
2688
2839
  SUM(total_tokens) AS total_tokens,
2689
2840
  SUM(cost_usd) AS total_cost_usd,
2690
2841
  SUM(cost_brl) AS total_cost_brl
2691
- FROM activities WHERE spec_id IS NOT NULL GROUP BY spec_id
2842
+ FROM macro_spec_activities GROUP BY spec_id
2692
2843
  )
2693
2844
  SELECT COUNT(*) AS entity_count,
2694
2845
  SUM(usage.total_run_count) AS total_run_count,
@@ -2733,7 +2884,7 @@ export async function rebuildSqliteReadModel(
2733
2884
  SUM(total_tokens) AS total_tokens,
2734
2885
  SUM(cost_usd) AS total_cost_usd,
2735
2886
  SUM(cost_brl) AS total_cost_brl
2736
- FROM activities WHERE spec_id IS NOT NULL GROUP BY spec_id
2887
+ FROM macro_spec_activities GROUP BY spec_id
2737
2888
  ), ranked AS (
2738
2889
  SELECT s.id AS entity_id, s.title, s.status, usage.total_run_count, usage.llm_call_count,
2739
2890
  usage.total_duration_ms, usage.total_tokens, usage.total_cost_usd, usage.total_cost_brl,
@@ -2864,6 +3015,67 @@ export async function rebuildSqliteReadModel(
2864
3015
  },
2865
3016
  );
2866
3017
  database.exec(deduplicatedViewSql);
3018
+ // One row per stable GitHub account. Costs are summed from the single
3019
+ // canonical Run fact, while delivery is joined exclusively through the
3020
+ // Task's completed_run_id (never inferred from a branch or latest Run).
3021
+ database.exec(`
3022
+ CREATE TABLE developer_cost_projection AS
3023
+ WITH completed AS (
3024
+ SELECT a.developer_account_id, COUNT(DISTINCT w.id) AS completed_task_count
3025
+ FROM zaip_vw_canonical_work_items w
3026
+ JOIN zaip_vw_activities a ON a.run_id=w.completed_run_id
3027
+ WHERE w.entity_kind='task' AND w.status='completed'
3028
+ AND a.developer_account_id IS NOT NULL
3029
+ GROUP BY a.developer_account_id
3030
+ )
3031
+ SELECT a.developer_account_id AS account_id,
3032
+ MAX(a.developer_login) AS login_snapshot,
3033
+ CASE WHEN COUNT(DISTINCT a.developer_attribution_source)=1 THEN MAX(a.developer_attribution_source) ELSE 'mixed' END AS attribution_source,
3034
+ COUNT(*) AS run_count,
3035
+ SUM(a.source_event_count) AS source_event_count,
3036
+ SUM(a.total_tokens) AS total_tokens,
3037
+ SUM(a.cost_usd) AS total_cost_usd,
3038
+ SUM(a.cost_brl) AS total_cost_brl,
3039
+ COUNT(a.total_tokens) AS token_covered_runs,
3040
+ COUNT(a.cost_usd) AS usd_covered_runs,
3041
+ COUNT(a.cost_brl) AS brl_covered_runs,
3042
+ COALESCE(completed.completed_task_count, 0) AS completed_task_count
3043
+ FROM zaip_vw_activities a LEFT JOIN completed USING(developer_account_id)
3044
+ WHERE a.developer_account_id IS NOT NULL
3045
+ GROUP BY a.developer_account_id
3046
+ ORDER BY total_cost_brl DESC, account_id;
3047
+ CREATE INDEX developer_cost_projection_lookup ON developer_cost_projection(account_id);
3048
+ `);
3049
+ materializeCostGroupDailyProjection(database);
3050
+ materializeOperationsWorkProjection(database);
3051
+ // Persist the complete unfiltered invocation projection while the seed
3052
+ // still owns the compact canonical facts. Runtime pages can then slice
3053
+ // 10/100 rows without expanding the pricing view for every request.
3054
+ const costAgentInvocationProjection = database
3055
+ .prepare(
3056
+ `SELECT * FROM zaip_vw_cost_monitor_agent_invocations
3057
+ ORDER BY occurred_at DESC, agent_invocation_id DESC`,
3058
+ )
3059
+ .all() as Array<Record<string, unknown>>;
3060
+ const insertCostAgentInvocationProjection = database.prepare(
3061
+ `INSERT INTO zaip_aios_activities(
3062
+ activity_row_id, activity_source_kind, activity_key, source_path, source_hash,
3063
+ recorded_at, occurred_day_sp, run_role, root_run_id, payload_type, payload_json
3064
+ ) VALUES (?, 'technical_payload', ?, 'sqlite://cost_agent_invocation_projection_v1', '',
3065
+ datetime('now'), ?, 'technical', 'technical', 'cost_agent_invocation_projection_v1', ?)`,
3066
+ );
3067
+ database.exec("BEGIN;");
3068
+ for (const row of costAgentInvocationProjection) {
3069
+ const invocationId = String(row.agent_invocation_id ?? "");
3070
+ if (invocationId.length === 0) continue;
3071
+ insertCostAgentInvocationProjection.run(
3072
+ `TECH-${randomUUID().toLowerCase()}`,
3073
+ `cost_agent_invocation_projection_v1:${invocationId}`,
3074
+ typeof row.occurred_day_sp === "string" ? row.occurred_day_sp : null,
3075
+ JSON.stringify(row),
3076
+ );
3077
+ }
3078
+ database.exec("COMMIT;");
2867
3079
  reportLaunchProgress(
2868
3080
  "projection_finalize",
2869
3081
  80,
@@ -2877,62 +3089,92 @@ export async function rebuildSqliteReadModel(
2877
3089
  } finally {
2878
3090
  database.close();
2879
3091
  }
2880
- try {
2881
- await rename(temporaryPath, cachePath);
2882
- } catch (error: unknown) {
2883
- // Another launcher may have built the same immutable cache entry while
2884
- // this process was reading. It is safe only if its fingerprint agrees.
2885
- if (readSnapshotFileCount(cachePath, fingerprint) === null) {
2886
- await rm(temporaryPath, { force: true });
2887
- throw error;
2888
- }
2889
- await rm(temporaryPath, { force: true });
2890
- }
2891
3092
  const materializeStartedAt = performance.now();
2892
- await materializeSnapshot(cachePath, path);
2893
- // Compute the exact canonical first-fold population once while rebuilding.
2894
- // Store it as a typed technical payload (not a sixth table) in both the
2895
- // immutable seed and this private generation, so first paint is O(1).
3093
+ const timed = <T>(phase: string, action: () => T) =>
3094
+ measureFirstFold(
3095
+ process.env.ZAIP_DASHBOARD_LAUNCH_PROGRESS === "1",
3096
+ reportLaunchProgress,
3097
+ phase,
3098
+ action,
3099
+ );
3100
+ // The shared cache remains private until every expensive first-fold payload
3101
+ // has been committed. A concurrent launcher may freely use `path`, its own
3102
+ // generation, but must never copy a half-published cache seed.
3103
+ await materializeSnapshot(temporaryPath, path);
2896
3104
  const firstFoldFilesystem = new SqliteReadModelFileSystem(workspaceRoot);
2897
- const firstFoldViewport = firstFoldFilesystem.firstFoldViewportAllTime();
3105
+ const firstFoldViewport = timed("first_fold_viewport", () =>
3106
+ firstFoldFilesystem.firstFoldViewportAllTime(),
3107
+ );
3108
+ const firstFoldAllTime = timed("first_fold_all_time", () =>
3109
+ firstFoldFilesystem.firstFoldAllTime(),
3110
+ );
3111
+ const firstFoldAllTimeBoard = timed("first_fold_board", () =>
3112
+ firstFoldFilesystem.firstFoldBoardAllTime(),
3113
+ );
3114
+ const costMonitorSummaryAllTime = timed("first_fold_cost_summary", () =>
3115
+ firstFoldFilesystem.costMonitorSummaryAggregate({ from: null, to: null }),
3116
+ );
2898
3117
  const quickMonth = saoPauloCurrentMonth();
3118
+ const costMonitorSummaryCurrentMonth = timed("first_fold_cost_summary_month", () =>
3119
+ firstFoldFilesystem.costMonitorSummaryAggregate(quickMonth),
3120
+ );
2899
3121
  const firstFoldMonthlyQuick: SqliteFirstFoldMonthlyQuick = {
2900
3122
  from_day: quickMonth.from,
2901
3123
  to_day: quickMonth.to,
2902
- source_latest_day: firstFoldFilesystem.firstFoldRelevantLatestDay(),
2903
- aggregate: firstFoldFilesystem.firstFoldAggregate(quickMonth),
3124
+ source_latest_day: timed("first_fold_latest_day", () =>
3125
+ firstFoldFilesystem.firstFoldRelevantLatestDay(),
3126
+ ),
3127
+ aggregate: timed("first_fold_month_aggregate", () =>
3128
+ firstFoldFilesystem.firstFoldAggregate(quickMonth),
3129
+ ),
2904
3130
  pages: {
2905
- task: firstFoldFilesystem.summaryPage("task", quickMonth, 0, 10_000).rows,
2906
- spec: firstFoldFilesystem.summaryPage("spec", quickMonth, 0, 10_000).rows,
2907
- prd: firstFoldFilesystem.summaryPage("prd", quickMonth, 0, 10_000).rows,
3131
+ task: operationsWorkRowsInRange(
3132
+ firstFoldAllTimeBoard.pages.task,
3133
+ quickMonth.from,
3134
+ quickMonth.to,
3135
+ ),
3136
+ spec: operationsWorkRowsInRange(
3137
+ firstFoldAllTimeBoard.pages.spec,
3138
+ quickMonth.from,
3139
+ quickMonth.to,
3140
+ ),
3141
+ prd: operationsWorkRowsInRange(
3142
+ firstFoldAllTimeBoard.pages.prd,
3143
+ quickMonth.from,
3144
+ quickMonth.to,
3145
+ ),
2908
3146
  },
2909
- top_tasks: firstFoldFilesystem.topTasks(quickMonth),
3147
+ top_tasks: timed("first_fold_month_top_tasks", () => firstFoldFilesystem.topTasks(quickMonth)),
2910
3148
  };
2911
3149
  firstFoldFilesystem.close();
2912
- const firstFoldPayload = JSON.stringify(firstFoldViewport);
2913
- const firstFoldMonthlyQuickPayload = JSON.stringify(firstFoldMonthlyQuick);
2914
- for (const target of [cachePath, path]) {
2915
- const targetDatabase = new DatabaseSync(target);
2916
- try {
2917
- targetDatabase
2918
- .prepare(`INSERT INTO zaip_aios_activities(
2919
- activity_row_id, activity_source_kind, activity_key, source_path, source_hash,
2920
- recorded_at, run_role, root_run_id, payload_type, payload_json
2921
- ) VALUES (?, 'technical_payload', 'first_fold_viewport_cache:all_time',
2922
- 'sqlite://first_fold_viewport_cache/all_time', '', datetime('now'), 'technical', 'technical',
2923
- 'first_fold_viewport_cache', ?)`)
2924
- .run(`TECH-${randomUUID().toLowerCase()}`, firstFoldPayload);
2925
- targetDatabase
2926
- .prepare(`INSERT INTO zaip_aios_activities(
2927
- activity_row_id, activity_source_kind, activity_key, source_path, source_hash,
2928
- recorded_at, run_role, root_run_id, payload_type, payload_json
2929
- ) VALUES (?, 'technical_payload', 'first_fold_monthly_quick_cache:default',
2930
- 'sqlite://first_fold_monthly_quick_cache/default', '', datetime('now'), 'technical', 'technical',
2931
- 'first_fold_monthly_quick_cache', ?)`)
2932
- .run(`TECH-${randomUUID().toLowerCase()}`, firstFoldMonthlyQuickPayload);
2933
- } finally {
2934
- targetDatabase.close();
3150
+ persistFirstFoldPayloads([temporaryPath, path], {
3151
+ first_fold_viewport_cache: JSON.stringify(firstFoldViewport),
3152
+ first_fold_all_time_cache: JSON.stringify(firstFoldAllTime),
3153
+ first_fold_all_time_board_cache: JSON.stringify(firstFoldAllTimeBoard),
3154
+ cost_monitor_summary_cache: JSON.stringify(costMonitorSummaryAllTime),
3155
+ cost_monitor_summary_month_cache: JSON.stringify({
3156
+ ...quickMonth,
3157
+ aggregate: costMonitorSummaryCurrentMonth,
3158
+ }),
3159
+ first_fold_monthly_quick_cache: JSON.stringify(firstFoldMonthlyQuick),
3160
+ });
3161
+ try {
3162
+ await rename(temporaryPath, cachePath);
3163
+ } catch (error: unknown) {
3164
+ // A concurrent builder wins only when it published a complete seed.
3165
+ if (
3166
+ readSnapshotFileCount(
3167
+ cachePath,
3168
+ fingerprint,
3169
+ readModelSchemaVersion,
3170
+ hasRequiredFirstFoldPayloads,
3171
+ ) === null
3172
+ ) {
3173
+ await rm(temporaryPath, { force: true });
3174
+ throw error;
2935
3175
  }
3176
+ await rm(temporaryPath, { force: true });
3177
+ await materializeSnapshot(cachePath, path);
2936
3178
  }
2937
3179
  reportLaunchProgress("snapshot_ready", 86, "A cópia privada do banco local está pronta.");
2938
3180
  validateSqliteReadModelSchema(path);
@@ -2946,174 +3188,6 @@ export async function rebuildSqliteReadModel(
2946
3188
  };
2947
3189
  }
2948
3190
 
2949
- async function pruneSnapshotCache(directory: string, activePath: string): Promise<void> {
2950
- const entries = await readdir(directory, { withFileTypes: true }).catch(() => []);
2951
- const stale = await Promise.all(
2952
- entries
2953
- .filter(
2954
- (entry) =>
2955
- entry.isFile() &&
2956
- entry.name.endsWith(".sqlite") &&
2957
- resolve(directory, entry.name) !== activePath,
2958
- )
2959
- .map(async (entry) => ({
2960
- path: resolve(directory, entry.name),
2961
- mtime: (await stat(resolve(directory, entry.name))).mtimeMs,
2962
- })),
2963
- );
2964
- // Keep the most recent prior seed for a concurrent launcher; never remove tmp
2965
- // files, which may be an in-progress materialization by another process.
2966
- for (const entry of stale.sort((a, b) => b.mtime - a.mtime).slice(1))
2967
- await rm(entry.path, { force: true }).catch(() => {});
2968
- // Interrupted SQLite builds leave only cache-local temporary files. They
2969
- // cannot be valid immutable seeds and otherwise inflate the launch folder.
2970
- for (const entry of entries) {
2971
- if (!entry.isFile() || !entry.name.includes(".sqlite.") || !entry.name.includes(".tmp"))
2972
- continue;
2973
- const candidate = resolve(directory, entry.name);
2974
- const age = Date.now() - (await stat(candidate)).mtimeMs;
2975
- if (age > 60_000) await rm(candidate, { force: true }).catch(() => {});
2976
- }
2977
- }
2978
-
2979
- function readSnapshotFileCount(path: string, fingerprint: string): number | null {
2980
- try {
2981
- const database = new DatabaseSync(path, { readOnly: true });
2982
- const previous = (
2983
- database
2984
- .prepare("SELECT value FROM zaip_vw_read_model_metadata WHERE key = ?")
2985
- .get("source_fingerprint") as { value?: string } | undefined
2986
- )?.value;
2987
- const schema = (
2988
- database
2989
- .prepare("SELECT value FROM zaip_vw_read_model_metadata WHERE key = ?")
2990
- .get("schema_version") as { value?: string } | undefined
2991
- )?.value;
2992
- const files = Number(
2993
- (
2994
- database
2995
- .prepare("SELECT value FROM zaip_vw_read_model_metadata WHERE key = ?")
2996
- .get("source_file_count") as { value?: string } | undefined
2997
- )?.value ?? 0,
2998
- );
2999
- database.close();
3000
- return previous === fingerprint && schema === readModelSchemaVersion ? files : null;
3001
- } catch {
3002
- return null;
3003
- }
3004
- }
3005
-
3006
- async function mostRecentCompatibleSnapshot(
3007
- directory: string,
3008
- ): Promise<{ path: string; files: number } | null> {
3009
- const entries = await readdir(directory, { withFileTypes: true }).catch(() => []);
3010
- const candidates = await Promise.all(
3011
- entries
3012
- .filter((entry) => entry.isFile() && entry.name.endsWith(".sqlite"))
3013
- .map(async (entry) => ({
3014
- path: join(directory, entry.name),
3015
- mtime: (await stat(join(directory, entry.name))).mtimeMs,
3016
- })),
3017
- );
3018
- for (const candidate of candidates.sort((left, right) => right.mtime - left.mtime)) {
3019
- const files = readCompatibleSnapshotFileCount(candidate.path);
3020
- if (files !== null) return { path: candidate.path, files };
3021
- }
3022
- return null;
3023
- }
3024
-
3025
- function readCompatibleSnapshotFileCount(path: string): number | null {
3026
- try {
3027
- const database = new DatabaseSync(path, { readOnly: true });
3028
- const schema = (
3029
- database
3030
- .prepare("SELECT value FROM zaip_vw_read_model_metadata WHERE key = ?")
3031
- .get("schema_version") as { value?: string } | undefined
3032
- )?.value;
3033
- const files = Number(
3034
- (
3035
- database
3036
- .prepare("SELECT value FROM zaip_vw_read_model_metadata WHERE key = ?")
3037
- .get("source_file_count") as { value?: string } | undefined
3038
- )?.value ?? 0,
3039
- );
3040
- database.close();
3041
- return schema === readModelSchemaVersion ? files : null;
3042
- } catch {
3043
- return null;
3044
- }
3045
- }
3046
-
3047
- /**
3048
- * The persisted schema exposes only zaip_vw_* names. The local runtime keeps
3049
- * concise historical names as connection-local TEMP aliases while callers are
3050
- * migrated; they are never written to the database file.
3051
- */
3052
- function installLegacyViewAliases(database: DatabaseSync): void {
3053
- const views = database
3054
- .prepare("SELECT name FROM sqlite_master WHERE type = 'view' AND name LIKE 'zaip_vw_%'")
3055
- .all() as Array<{ name: string }>;
3056
- for (const { name } of views) {
3057
- const alias = name.slice("zaip_vw_".length);
3058
- const quoted = (identifier: string) => `"${identifier.replaceAll('"', '""')}"`;
3059
- database.exec(`CREATE TEMP VIEW ${quoted(alias)} AS SELECT * FROM main.${quoted(name)}`);
3060
- }
3061
- }
3062
-
3063
- async function materializeSnapshot(source: string, destination: string): Promise<void> {
3064
- await mkdir(dirname(destination), { recursive: true });
3065
- const temporaryPath = `${destination}.${process.pid}.${randomUUID()}.tmp`;
3066
- await copyFile(source, temporaryPath);
3067
- try {
3068
- await rename(temporaryPath, destination);
3069
- } catch (error) {
3070
- await rm(temporaryPath, { force: true });
3071
- throw error;
3072
- }
3073
- }
3074
-
3075
- export type SqliteReadModelSourceCandidate = {
3076
- path: string;
3077
- source: string;
3078
- mtimeMs: number;
3079
- size: number;
3080
- };
3081
-
3082
- export async function sourceFingerprint(workspaceRoot: string): Promise<{
3083
- fingerprint: string;
3084
- candidates: SqliteReadModelSourceCandidate[];
3085
- }> {
3086
- const candidates = (
3087
- await Promise.all(
3088
- [".agents/records", ".agents/config"].map((root) => listFiles(resolve(workspaceRoot, root))),
3089
- )
3090
- )
3091
- .flat()
3092
- .filter(
3093
- (path) =>
3094
- textExtensions.has(path.slice(path.lastIndexOf(".")).toLowerCase()) &&
3095
- isDashboardCanonicalSource(relative(workspaceRoot, path).replaceAll("\\", "/")),
3096
- )
3097
- .sort();
3098
- const candidatesWithMetadata = await Promise.all(
3099
- candidates.map(async (candidate) => {
3100
- const metadata = await stat(candidate);
3101
- return {
3102
- path: candidate,
3103
- source: relative(workspaceRoot, candidate).replaceAll("\\", "/"),
3104
- mtimeMs: Math.trunc(metadata.mtimeMs),
3105
- size: metadata.size,
3106
- };
3107
- }),
3108
- );
3109
- return {
3110
- fingerprint: candidatesWithMetadata
3111
- .map((candidate) => `${candidate.source}:${candidate.mtimeMs}:${candidate.size}`)
3112
- .join("|"),
3113
- candidates: candidatesWithMetadata,
3114
- };
3115
- }
3116
-
3117
3191
  export type SqliteFirstFoldAggregate = {
3118
3192
  days: number;
3119
3193
  total_run_count: number;
@@ -3124,75 +3198,17 @@ export type SqliteFirstFoldAggregate = {
3124
3198
  cost_brl: number | null;
3125
3199
  execution_day_count: number;
3126
3200
  };
3201
+ export type {
3202
+ SqliteFirstFoldAllTime,
3203
+ SqliteFirstFoldAllTimeBoard,
3204
+ SqliteFirstFoldMonthlyQuick,
3205
+ } from "./sqlite-read-model-first-fold-types.js";
3127
3206
 
3128
- export type SqliteFirstFoldAllTime = {
3129
- aggregate: SqliteFirstFoldAggregate & {
3130
- start_day: string | null;
3131
- end_day: string | null;
3132
- task_count: number;
3133
- completed_task_count: number;
3134
- spec_count: number;
3135
- completed_spec_count: number;
3136
- prd_count: number;
3137
- completed_prd_count: number;
3138
- total_cost_usd: number | null;
3139
- latest_task_created_at: string | null;
3140
- };
3141
- task_status_counts: Record<string, number>;
3142
- task_risk_counts: Record<string, number>;
3143
- task_metrics: {
3144
- average_duration_ms_per_task: number | null;
3145
- average_tokens_per_task: number | null;
3146
- average_cost_usd_per_task: number | null;
3147
- average_cost_brl_per_task: number | null;
3148
- latest_task_created_at: string | null;
3149
- };
3150
- top_tasks: Array<{
3151
- rank: number;
3152
- task_id: string;
3153
- title: string;
3154
- status: string | null;
3155
- risk: string | null;
3156
- total_run_count: number;
3157
- llm_call_count: number | null;
3158
- total_duration_ms: number;
3159
- total_tokens: number | null;
3160
- total_cost_usd: number | null;
3161
- total_cost_brl: number | null;
3162
- duration_share: number;
3163
- }>;
3164
- macro_metrics: Record<
3165
- "specs" | "prds",
3166
- {
3167
- status_counts: Record<string, number>;
3168
- classification_counts: Record<string, number>;
3169
- entity_count: number;
3170
- total_run_count: number;
3171
- total_duration_ms: number | null;
3172
- total_tokens: number | null;
3173
- total_cost_usd: number | null;
3174
- total_cost_brl: number | null;
3175
- average_duration_ms: number | null;
3176
- average_tokens: number | null;
3177
- average_cost_usd: number | null;
3178
- average_cost_brl: number | null;
3179
- latest_created_at: string | null;
3180
- top_entities: Array<{
3181
- rank: number;
3182
- entity_id: string;
3183
- title: string;
3184
- status: string | null;
3185
- total_run_count: number;
3186
- llm_call_count: number | null;
3187
- total_duration_ms: number | null;
3188
- total_tokens: number | null;
3189
- total_cost_usd: number | null;
3190
- total_cost_brl: number | null;
3191
- duration_share: number;
3192
- }>;
3193
- }
3194
- >;
3195
- };
3207
+ import type {
3208
+ SqliteFirstFoldAllTime,
3209
+ SqliteFirstFoldAllTimeBoard,
3210
+ SqliteFirstFoldMonthlyQuick,
3211
+ } from "./sqlite-read-model-first-fold-types.js";
3196
3212
 
3197
3213
  /** The only all-time facts needed before the Operations first fold is painted. */
3198
3214
  export type SqliteFirstFoldViewport = Pick<
@@ -3306,51 +3322,24 @@ export type SqliteSummaryPage = {
3306
3322
  * immutable seed is built, rather than competing with the browser after the
3307
3323
  * launcher reports ready.
3308
3324
  */
3309
- export type SqliteFirstFoldMonthlyQuick = {
3310
- from_day: string;
3311
- to_day: string;
3312
- source_latest_day: string | null;
3313
- aggregate: SqliteFirstFoldAggregate;
3314
- pages: Record<SqliteSummaryKind, Array<Record<string, unknown>>>;
3315
- top_tasks: Array<{
3316
- task_id: string;
3317
- total_tokens: number | null;
3318
- cost_brl: number | null;
3319
- run_count: number;
3320
- duration_ms: number;
3321
- }>;
3322
- };
3323
-
3324
- async function listFiles(directory: string): Promise<string[]> {
3325
- const entries = await readdir(directory, { withFileTypes: true }).catch(() => []);
3326
- const paths = await Promise.all(
3327
- entries.map(async (entry) => {
3328
- const candidate = join(directory, entry.name);
3329
- return entry.isDirectory() ? listFiles(candidate) : entry.isFile() ? [candidate] : [];
3330
- }),
3331
- );
3332
- return paths.flat();
3333
- }
3334
-
3335
3325
  /** Read-only filesystem backed by the regenerated SQLite snapshot. */
3336
3326
  export class SqliteReadModelFileSystem extends WorkspaceFileSystem {
3337
3327
  private readonly executionDurationCache = new Map<string, Map<string, number>>();
3338
3328
  private dailyAggregateRowsCache: Array<Record<string, number | string | null>> | null = null;
3339
3329
  private firstFoldAllTimeCache: SqliteFirstFoldAllTime | null = null;
3330
+ private firstFoldAllTimeBoardCache: SqliteFirstFoldAllTimeBoard | null = null;
3340
3331
  private firstFoldTopTasksAllTimeCache: SqliteFirstFoldAllTime["top_tasks"] | null = null;
3341
3332
  private firstFoldViewportCache: SqliteFirstFoldViewport | null = null;
3342
3333
  private firstFoldMonthlyQuickCache: SqliteFirstFoldMonthlyQuick | null | undefined = undefined;
3343
3334
  private firstFoldViewportTimings: Record<string, number> = {};
3344
3335
  private metadataValues: Map<string, string> | null = null;
3345
3336
  private readonly unfilteredViewPageCache = new Map<string, Array<Record<string, unknown>>>();
3337
+ private readonly technicalPayloadRowsCache = new Map<string, Array<Record<string, unknown>>>();
3346
3338
  private readonly viewPageResultCache = new Map<string, Record<string, unknown>>();
3347
- // `operations_work_list` is a derived union of three aggregate views. The
3348
- // first page used to recompute that union (and its duration allocation) for
3349
- // both COUNT and SELECT, then repeat it for every offset. Keep the immutable
3350
- // generation-local result in the read-only runtime instead. The public SQL
3351
- // view and all filtered paths remain the compatibility seam.
3352
- private operationsWorkListPageCache: Array<Record<string, unknown>> | null = null;
3353
-
3339
+ // Summary cards and temporal Cost tables consume the same aggregate. Keep
3340
+ // exact filter results inside the immutable generation so a tab switch does
3341
+ // not repeat its multi-second SQL scan.
3342
+ private readonly costMonitorSummaryCache = new Map<string, CostSummarySource>();
3354
3343
  private dailyAggregateRows(): Array<Record<string, number | string | null>> {
3355
3344
  if (this.dailyAggregateRowsCache === null) {
3356
3345
  this.dailyAggregateRowsCache = this.database
@@ -3360,13 +3349,6 @@ export class SqliteReadModelFileSystem extends WorkspaceFileSystem {
3360
3349
  return this.dailyAggregateRowsCache;
3361
3350
  }
3362
3351
 
3363
- private operationsWorkListRows(): Array<Record<string, unknown>> {
3364
- this.operationsWorkListPageCache ??= this.database
3365
- .prepare("SELECT * FROM operations_work_list ORDER BY occurred_at DESC, id DESC")
3366
- .all() as Array<Record<string, unknown>>;
3367
- return this.operationsWorkListPageCache;
3368
- }
3369
-
3370
3352
  private unfilteredViewRows(view: SqliteViewName): Array<Record<string, unknown>> {
3371
3353
  const cached = this.unfilteredViewPageCache.get(view);
3372
3354
  if (cached !== undefined) return cached;
@@ -3377,6 +3359,27 @@ export class SqliteReadModelFileSystem extends WorkspaceFileSystem {
3377
3359
  this.unfilteredViewPageCache.set(view, rows);
3378
3360
  return rows;
3379
3361
  }
3362
+
3363
+ private technicalPayloadRows(payloadType: string): Array<Record<string, unknown>> {
3364
+ const cached = this.technicalPayloadRowsCache.get(payloadType);
3365
+ if (cached !== undefined) return cached;
3366
+ const rows = (
3367
+ this.database
3368
+ .prepare("SELECT payload_json FROM zaip_aios_activities WHERE payload_type=?")
3369
+ .all(payloadType) as Array<{ payload_json: string }>
3370
+ ).flatMap(({ payload_json }) => {
3371
+ try {
3372
+ const parsed = JSON.parse(payload_json) as unknown;
3373
+ return parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)
3374
+ ? [parsed as Record<string, unknown>]
3375
+ : [];
3376
+ } catch {
3377
+ return [];
3378
+ }
3379
+ });
3380
+ this.technicalPayloadRowsCache.set(payloadType, rows);
3381
+ return rows;
3382
+ }
3380
3383
  private database: DatabaseSync;
3381
3384
 
3382
3385
  constructor(root: string) {
@@ -3451,6 +3454,7 @@ export class SqliteReadModelFileSystem extends WorkspaceFileSystem {
3451
3454
  this.executionDurationCache.clear();
3452
3455
  this.dailyAggregateRowsCache = null;
3453
3456
  this.firstFoldAllTimeCache = null;
3457
+ this.firstFoldAllTimeBoardCache = null;
3454
3458
  this.firstFoldTopTasksAllTimeCache = null;
3455
3459
  this.firstFoldViewportCache = null;
3456
3460
  this.firstFoldMonthlyQuickCache = undefined;
@@ -3459,6 +3463,9 @@ export class SqliteReadModelFileSystem extends WorkspaceFileSystem {
3459
3463
  // Public pages belong to the opened immutable SQLite generation. Never
3460
3464
  // carry them across refresh/rebuild, even when this filesystem instance is
3461
3465
  // reused by the launcher.
3466
+ this.unfilteredViewPageCache.clear();
3467
+ this.technicalPayloadRowsCache.clear();
3468
+ this.costMonitorSummaryCache.clear();
3462
3469
  this.viewPageResultCache.clear();
3463
3470
  }
3464
3471
 
@@ -3647,8 +3654,11 @@ export class SqliteReadModelFileSystem extends WorkspaceFileSystem {
3647
3654
  days.map((day) => {
3648
3655
  const row = rowsByDay.get(day) ?? {};
3649
3656
  const totalRuns = Number(row.total_run_count ?? 0);
3650
- const knownUsageRuns = Number(row.activity_count ?? 0);
3651
- const unknownUsageRuns = Math.max(0, totalRuns - knownUsageRuns);
3657
+ const knownTokenRuns = Number(row.token_known_runs ?? 0);
3658
+ const knownCostRuns = Math.min(
3659
+ Number(row.usd_known_runs ?? 0),
3660
+ Number(row.brl_known_runs ?? 0),
3661
+ );
3652
3662
  return [
3653
3663
  day,
3654
3664
  {
@@ -3661,11 +3671,11 @@ export class SqliteReadModelFileSystem extends WorkspaceFileSystem {
3661
3671
  usage: {
3662
3672
  total_tokens: {
3663
3673
  known_sum: Number(row.total_tokens ?? 0),
3664
- unknown: unknownUsageRuns,
3674
+ unknown: Math.max(0, totalRuns - knownTokenRuns),
3665
3675
  },
3666
3676
  cost: {
3667
3677
  known_sum: Number(row.total_cost_brl ?? 0),
3668
- unknown: unknownUsageRuns,
3678
+ unknown: Math.max(0, totalRuns - knownCostRuns),
3669
3679
  },
3670
3680
  },
3671
3681
  duration: {
@@ -3715,72 +3725,9 @@ export class SqliteReadModelFileSystem extends WorkspaceFileSystem {
3715
3725
  from: string | null;
3716
3726
  to: string | null;
3717
3727
  }): SqliteFirstFoldMonthlyQuick | null {
3718
- if (this.firstFoldMonthlyQuickCache === undefined) {
3719
- const persisted = this.database
3720
- .prepare(
3721
- "SELECT payload_json FROM zaip_aios_activities WHERE payload_type='first_fold_monthly_quick_cache' ORDER BY recorded_at DESC LIMIT 1",
3722
- )
3723
- .get() as { payload_json?: string } | undefined;
3724
- try {
3725
- const candidate =
3726
- typeof persisted?.payload_json === "string"
3727
- ? (JSON.parse(persisted.payload_json) as Partial<SqliteFirstFoldMonthlyQuick>)
3728
- : null;
3729
- this.firstFoldMonthlyQuickCache =
3730
- candidate !== null &&
3731
- typeof candidate.from_day === "string" &&
3732
- typeof candidate.to_day === "string" &&
3733
- candidate.aggregate !== undefined &&
3734
- candidate.pages !== undefined &&
3735
- Array.isArray(candidate.top_tasks)
3736
- ? (candidate as SqliteFirstFoldMonthlyQuick)
3737
- : null;
3738
- } catch {
3739
- this.firstFoldMonthlyQuickCache = null;
3740
- }
3741
- }
3742
- const cached = this.firstFoldMonthlyQuickCache;
3743
- if (cached === null) return null;
3744
- const from = filter.from?.slice(0, 10) ?? null;
3745
- const to = filter.to?.slice(0, 10) ?? null;
3746
- const exactRange = from === cached.from_day && to === cached.to_day;
3747
- const sameMonthLaterDay =
3748
- from === cached.from_day &&
3749
- to !== null &&
3750
- to > cached.to_day &&
3751
- to.slice(0, 7) === cached.from_day.slice(0, 7);
3752
- const noUncoveredFacts =
3753
- cached.source_latest_day === null || cached.source_latest_day <= cached.to_day;
3754
- // The payload itself is exact for its own range even if the immutable
3755
- // generation also contains future-dated facts. Only an extension beyond
3756
- // its end day needs the no-uncovered-facts guard.
3757
- if (exactRange || (sameMonthLaterDay && noUncoveredFacts)) return cached;
3758
-
3759
- const requestedMonthIsLater =
3760
- from !== null &&
3761
- to !== null &&
3762
- from.slice(0, 7) >
3763
- (cached.source_latest_day === null
3764
- ? cached.from_day.slice(0, 7)
3765
- : cached.source_latest_day.slice(0, 7));
3766
- if (!requestedMonthIsLater) return null;
3767
- return {
3768
- from_day: from,
3769
- to_day: to,
3770
- source_latest_day: cached.source_latest_day,
3771
- aggregate: {
3772
- days: 0,
3773
- total_run_count: 0,
3774
- agent_invocation_count: 0,
3775
- llm_call_count: null,
3776
- duration_ms: null,
3777
- total_tokens: null,
3778
- cost_brl: null,
3779
- execution_day_count: 0,
3780
- },
3781
- pages: { task: [], spec: [], prd: [] },
3782
- top_tasks: [],
3783
- };
3728
+ const read = readFirstFoldMonthlyQuick(this.database, this.firstFoldMonthlyQuickCache, filter);
3729
+ this.firstFoldMonthlyQuickCache = read.cache;
3730
+ return read.result;
3784
3731
  }
3785
3732
 
3786
3733
  firstFoldRelevantLatestDay(): string | null {
@@ -3796,261 +3743,43 @@ export class SqliteReadModelFileSystem extends WorkspaceFileSystem {
3796
3743
  return typeof row.day === "string" ? row.day : null;
3797
3744
  }
3798
3745
 
3799
- /**
3800
- * Compact all-time first fold. A runtime reads an immutable private SQLite
3801
- * snapshot, so readiness can warm this object and first paint can reuse the
3802
- * same coherent source values without rescanning entity rows.
3803
- */
3804
3746
  firstFoldAllTime(): SqliteFirstFoldAllTime {
3805
3747
  if (this.firstFoldAllTimeCache !== null) return this.firstFoldAllTimeCache;
3806
- const row = this.database
3807
- .prepare("SELECT * FROM zaip_vw_first_fold_overview_all_time")
3808
- .get() as Record<string, unknown>;
3809
- const statusRows = this.database
3810
- .prepare("SELECT status, task_count FROM zaip_vw_first_fold_task_status_all_time")
3811
- .all() as Array<{ status: string; task_count: number }>;
3812
- const riskRows = this.database
3813
- .prepare("SELECT risk, task_count FROM zaip_vw_first_fold_task_risk_all_time")
3814
- .all() as Array<{ risk: string; task_count: number }>;
3815
- const metrics = this.database
3816
- .prepare("SELECT * FROM zaip_vw_first_fold_task_metrics_all_time")
3817
- .get() as Record<string, unknown>;
3818
- const topTasks = this.database
3819
- .prepare("SELECT * FROM zaip_vw_first_fold_top_tasks_all_time ORDER BY rank")
3820
- .all() as SqliteFirstFoldAllTime["top_tasks"];
3821
- const number = (value: unknown, fallback = 0) => (typeof value === "number" ? value : fallback);
3822
- const nullableNumber = (value: unknown) => (typeof value === "number" ? value : null);
3823
- const readMacroMetrics = (kind: "spec" | "prd") => {
3824
- const statusRows = this.database
3825
- .prepare(`SELECT stage, entity_count FROM zaip_vw_first_fold_${kind}_status_all_time`)
3826
- .all() as Array<{ stage: string; entity_count: number }>;
3827
- const classificationRows = this.database
3828
- .prepare(
3829
- `SELECT classification, entity_count FROM zaip_vw_first_fold_${kind}_classification_all_time`,
3830
- )
3831
- .all() as Array<{ classification: string; entity_count: number }>;
3832
- const macro = this.database
3833
- .prepare(`SELECT * FROM zaip_vw_first_fold_${kind}_metrics_all_time`)
3834
- .get() as Record<string, unknown>;
3835
- const topEntities = this.database
3836
- .prepare(`SELECT * FROM zaip_vw_first_fold_top_${kind}s_all_time ORDER BY rank`)
3837
- .all() as SqliteFirstFoldAllTime["macro_metrics"]["specs"]["top_entities"];
3748
+ this.firstFoldAllTimeCache = readFirstFoldAllTime(
3749
+ this.database,
3750
+ () => this.dailyAggregateRows().length,
3751
+ this.firstFoldViewportAllTime(),
3752
+ );
3753
+ return this.firstFoldAllTimeCache;
3754
+ }
3755
+
3756
+ firstFoldBoardAllTime(): SqliteFirstFoldAllTimeBoard {
3757
+ if (this.firstFoldAllTimeBoardCache !== null) return this.firstFoldAllTimeBoardCache;
3758
+ this.firstFoldAllTimeBoardCache = readFirstFoldBoardAllTime(this.database, () => {
3759
+ const rows = operationsWorkListSqlRows(this.database);
3838
3760
  return {
3839
- status_counts: Object.fromEntries(
3840
- statusRows.map((status) => [status.stage, Number(status.entity_count)]),
3841
- ),
3842
- classification_counts: Object.fromEntries(
3843
- classificationRows.map((row) => [row.classification, Number(row.entity_count)]),
3844
- ),
3845
- entity_count: number(macro.entity_count),
3846
- total_run_count: number(macro.total_run_count),
3847
- total_duration_ms: nullableNumber(macro.total_duration_ms),
3848
- total_tokens: nullableNumber(macro.total_tokens),
3849
- total_cost_usd: nullableNumber(macro.total_cost_usd),
3850
- total_cost_brl: nullableNumber(macro.total_cost_brl),
3851
- average_duration_ms: nullableNumber(macro.average_duration_ms),
3852
- average_tokens: nullableNumber(macro.average_tokens),
3853
- average_cost_usd: nullableNumber(macro.average_cost_usd),
3854
- average_cost_brl: nullableNumber(macro.average_cost_brl),
3855
- latest_created_at:
3856
- typeof macro.latest_created_at === "string" ? macro.latest_created_at : null,
3857
- top_entities: topEntities,
3761
+ pages: {
3762
+ task: rows.filter((row) => row.kind === "task" || row.kind === "standalone-task"),
3763
+ spec: rows.filter((row) => row.kind === "spec"),
3764
+ prd: rows.filter((row) => row.kind === "prd"),
3765
+ },
3858
3766
  };
3859
- };
3860
- this.firstFoldAllTimeCache = {
3861
- aggregate: {
3862
- days:
3863
- typeof row.start_day === "string" && typeof row.end_day === "string"
3864
- ? this.dailyAggregateRows().length
3865
- : 0,
3866
- start_day: typeof row.start_day === "string" ? row.start_day : null,
3867
- end_day: typeof row.end_day === "string" ? row.end_day : null,
3868
- task_count: number(row.task_count),
3869
- completed_task_count: number(row.completed_task_count),
3870
- spec_count: number(row.spec_count),
3871
- completed_spec_count: number(row.completed_spec_count),
3872
- prd_count: number(row.prd_count),
3873
- completed_prd_count: number(row.completed_prd_count),
3874
- total_run_count: number(row.total_run_count),
3875
- agent_invocation_count: number(row.agent_invocation_count),
3876
- llm_call_count: nullableNumber(row.llm_call_count),
3877
- duration_ms: nullableNumber(row.total_duration_ms),
3878
- total_tokens: nullableNumber(row.total_tokens),
3879
- total_cost_usd: nullableNumber(row.total_cost_usd),
3880
- cost_brl: nullableNumber(row.total_cost_brl),
3881
- execution_day_count: number(row.execution_day_count),
3882
- latest_task_created_at:
3883
- typeof row.latest_task_created_at === "string" ? row.latest_task_created_at : null,
3884
- },
3885
- task_status_counts: Object.fromEntries(
3886
- statusRows.map((status) => [status.status, Number(status.task_count)]),
3887
- ),
3888
- task_risk_counts: Object.fromEntries(
3889
- riskRows.map((risk) => [risk.risk, Number(risk.task_count)]),
3890
- ),
3891
- task_metrics: {
3892
- average_duration_ms_per_task: nullableNumber(metrics.average_duration_ms_per_task),
3893
- average_tokens_per_task: nullableNumber(metrics.average_tokens_per_task),
3894
- average_cost_usd_per_task: nullableNumber(metrics.average_cost_usd_per_task),
3895
- average_cost_brl_per_task: nullableNumber(metrics.average_cost_brl_per_task),
3896
- latest_task_created_at:
3897
- typeof metrics.latest_task_created_at === "string"
3898
- ? metrics.latest_task_created_at
3899
- : null,
3900
- },
3901
- top_tasks: topTasks,
3902
- macro_metrics: {
3903
- specs: readMacroMetrics("spec"),
3904
- prds: readMacroMetrics("prd"),
3905
- },
3906
- };
3907
- return this.firstFoldAllTimeCache;
3767
+ });
3768
+ return this.firstFoldAllTimeBoardCache;
3908
3769
  }
3909
3770
 
3910
- /**
3911
- * Narrow all-time ranking for the Operations quick projection. This avoids
3912
- * loading macro metrics and extended aggregates solely to render Top 5.
3913
- */
3914
3771
  firstFoldTopTasksAllTime(): SqliteFirstFoldAllTime["top_tasks"] {
3915
3772
  if (this.firstFoldTopTasksAllTimeCache !== null) return this.firstFoldTopTasksAllTimeCache;
3916
- this.firstFoldTopTasksAllTimeCache = this.database
3917
- .prepare("SELECT * FROM zaip_vw_first_fold_top_tasks_all_time ORDER BY rank")
3918
- .all() as SqliteFirstFoldAllTime["top_tasks"];
3773
+ this.firstFoldTopTasksAllTimeCache = readFirstFoldTopTasksAllTime(this.database);
3919
3774
  return this.firstFoldTopTasksAllTimeCache;
3920
3775
  }
3921
3776
 
3922
- /**
3923
- * First-paint subset: six KPI facts plus the status and risk charts. Keep
3924
- * rankings, task averages and macro views out of readiness and scope=quick.
3925
- */
3926
3777
  firstFoldViewportAllTime(): SqliteFirstFoldViewport {
3927
3778
  if (this.firstFoldViewportCache !== null) return this.firstFoldViewportCache;
3928
- const persisted = this.database
3929
- .prepare(
3930
- "SELECT payload_json FROM zaip_aios_activities WHERE payload_type='first_fold_viewport_cache' ORDER BY recorded_at DESC LIMIT 1",
3931
- )
3932
- .get() as { payload_json?: string } | undefined;
3933
- if (typeof persisted?.payload_json === "string") {
3934
- this.firstFoldViewportCache = JSON.parse(persisted.payload_json) as SqliteFirstFoldViewport;
3935
- return this.firstFoldViewportCache;
3936
- }
3937
- // This is deliberately not the rich overview view: `task_summary` joins
3938
- // every activity and is appropriate for rankings, but status/risk charts
3939
- // need only the canonical work-item dimensions. Keep the three viewport
3940
- // SELECTs independently attributable and free of below-fold joins.
3941
- const profile = process.env.ZAIP_DASHBOARD_PROFILE_FIRST_FOLD === "1";
3942
- const timed = <T>(select: string, read: () => T): T => {
3943
- const startedAt = performance.now();
3944
- const value = read();
3945
- if (profile)
3946
- this.firstFoldViewportTimings[select] =
3947
- Math.round((performance.now() - startedAt) * 10) / 10;
3948
- if (profile)
3949
- console.info(
3950
- JSON.stringify({
3951
- event: "dashboard_first_fold_select",
3952
- select,
3953
- elapsed_ms: Math.round((performance.now() - startedAt) * 10) / 10,
3954
- }),
3955
- );
3956
- return value;
3957
- };
3958
- const row = timed(
3959
- "viewport_overview",
3960
- () =>
3961
- this.database
3962
- .prepare(`WITH totals AS (
3963
- SELECT SUM(CASE WHEN activity_source_kind='agent_run' THEN 1 ELSE 0 END) AS agent_invocation_count,
3964
- SUM(llm_call_count) AS llm_call_count,
3965
- SUM(total_tokens) AS total_tokens, SUM(cost_usd) AS total_cost_usd,
3966
- SUM(cost_brl) AS total_cost_brl FROM activities
3967
- ), run_totals AS (
3968
- SELECT COUNT(*) AS total_run_count FROM canonical_run_index
3969
- ), duration_totals AS (
3970
- SELECT ROUND(SUM(attributed_token_active_ms)) AS total_duration_ms,
3971
- COUNT(DISTINCT occurred_day_sp) AS execution_day_count
3972
- FROM attributed_token_active_facts
3973
- ), bounds AS (
3974
- SELECT MIN(day) AS start_day, MAX(day) AS end_day, COUNT(*) AS day_count FROM (
3975
- SELECT occurred_day_sp AS day FROM activities
3976
- UNION SELECT occurred_day_sp AS day FROM canonical_work_items
3977
- UNION SELECT occurred_day_sp AS day FROM canonical_artifacts
3978
- )
3979
- ), canonical_items AS MATERIALIZED (
3980
- SELECT json_extract(payload_json, '$.entity_kind') AS entity_kind,
3981
- json_extract(payload_json, '$.id') AS id,
3982
- json_extract(payload_json, '$.status') AS status,
3983
- json_extract(payload_json, '$.risk') AS risk,
3984
- json_extract(payload_json, '$.occurred_at') AS occurred_at
3985
- FROM zaip_aios_activities WHERE payload_type='work_item'
3986
- ), items AS MATERIALIZED (
3987
- SELECT entity_kind, id, status, risk, occurred_at FROM canonical_items
3988
- UNION ALL SELECT 'task', a.task_id, MAX(a.work_item_status), MAX(a.risk), MIN(a.occurred_at)
3989
- FROM zaip_aios_activities a WHERE a.payload_type IS NULL AND a.task_id IS NOT NULL
3990
- AND NOT EXISTS (SELECT 1 FROM canonical_items c WHERE c.entity_kind='task' AND c.id=a.task_id)
3991
- GROUP BY task_id
3992
- UNION ALL SELECT 'spec', a.spec_id, MAX(a.work_item_status), MAX(a.risk), MIN(a.occurred_at)
3993
- FROM zaip_aios_activities a WHERE a.payload_type IS NULL AND a.spec_id IS NOT NULL
3994
- AND NOT EXISTS (SELECT 1 FROM canonical_items c WHERE c.entity_kind='spec' AND c.id=a.spec_id)
3995
- GROUP BY spec_id
3996
- UNION ALL SELECT 'prd', a.prd_id, MAX(a.work_item_status), MAX(a.risk), MIN(a.occurred_at)
3997
- FROM zaip_aios_activities a WHERE a.payload_type IS NULL AND a.prd_id IS NOT NULL
3998
- AND NOT EXISTS (SELECT 1 FROM canonical_items c WHERE c.entity_kind='prd' AND c.id=a.prd_id)
3999
- GROUP BY prd_id
4000
- ), item_totals AS (
4001
- SELECT
4002
- SUM(entity_kind='task') AS task_count,
4003
- SUM(entity_kind='task' AND status='completed') AS completed_task_count,
4004
- MAX(CASE WHEN entity_kind='task' THEN occurred_at END) AS latest_task_created_at,
4005
- SUM(entity_kind='spec') AS spec_count,
4006
- SUM(entity_kind='spec' AND status IN ('completed','delivered')) AS completed_spec_count,
4007
- SUM(entity_kind='prd') AS prd_count,
4008
- SUM(entity_kind='prd' AND status IN ('completed','delivered')) AS completed_prd_count
4009
- FROM items
4010
- ), task_status AS (
4011
- SELECT json_group_object(status, task_count) AS task_status_json FROM (
4012
- SELECT COALESCE(status, 'draft') AS status, COUNT(*) AS task_count
4013
- FROM items WHERE entity_kind='task' GROUP BY COALESCE(status, 'draft')
4014
- )
4015
- ), task_risk AS (
4016
- SELECT json_group_object(risk, task_count) AS task_risk_json FROM (
4017
- SELECT COALESCE(risk, 'standard') AS risk, COUNT(*) AS task_count
4018
- FROM items WHERE entity_kind='task' GROUP BY COALESCE(risk, 'standard')
4019
- )
4020
- ) SELECT totals.*, run_totals.*, duration_totals.*, bounds.*, item_totals.*, task_status.*, task_risk.*
4021
- FROM totals CROSS JOIN run_totals CROSS JOIN duration_totals CROSS JOIN bounds
4022
- CROSS JOIN item_totals CROSS JOIN task_status CROSS JOIN task_risk`)
4023
- .get() as Record<string, unknown>,
3779
+ this.firstFoldViewportCache = readFirstFoldViewport(
3780
+ this.database,
3781
+ this.firstFoldViewportTimings,
4024
3782
  );
4025
- const number = (value: unknown, fallback = 0) => (typeof value === "number" ? value : fallback);
4026
- const nullableNumber = (value: unknown) => (typeof value === "number" ? value : null);
4027
- this.firstFoldViewportCache = {
4028
- aggregate: {
4029
- days: number(row.day_count),
4030
- start_day: typeof row.start_day === "string" ? row.start_day : null,
4031
- end_day: typeof row.end_day === "string" ? row.end_day : null,
4032
- task_count: number(row.task_count),
4033
- completed_task_count: number(row.completed_task_count),
4034
- spec_count: number(row.spec_count),
4035
- completed_spec_count: number(row.completed_spec_count),
4036
- prd_count: number(row.prd_count),
4037
- completed_prd_count: number(row.completed_prd_count),
4038
- total_run_count: number(row.total_run_count),
4039
- agent_invocation_count: number(row.agent_invocation_count),
4040
- llm_call_count: nullableNumber(row.llm_call_count),
4041
- duration_ms: nullableNumber(row.total_duration_ms),
4042
- total_tokens: nullableNumber(row.total_tokens),
4043
- total_cost_usd: nullableNumber(row.total_cost_usd),
4044
- cost_brl: nullableNumber(row.total_cost_brl),
4045
- execution_day_count: number(row.execution_day_count),
4046
- latest_task_created_at:
4047
- typeof row.latest_task_created_at === "string" ? row.latest_task_created_at : null,
4048
- },
4049
- task_status_counts:
4050
- typeof row.task_status_json === "string" ? JSON.parse(row.task_status_json) : {},
4051
- task_risk_counts:
4052
- typeof row.task_risk_json === "string" ? JSON.parse(row.task_risk_json) : {},
4053
- };
4054
3783
  return this.firstFoldViewportCache;
4055
3784
  }
4056
3785
 
@@ -4514,12 +4243,281 @@ export class SqliteReadModelFileSystem extends WorkspaceFileSystem {
4514
4243
  * per selected day plus conversation/planning/execution and total rows; no
4515
4244
  * activity-shaped record is returned to the server or serialized to HTTP.
4516
4245
  */
4246
+ developerCostAggregate(filter: {
4247
+ from: string | null;
4248
+ to: string | null;
4249
+ accountIds?: readonly string[];
4250
+ }) {
4251
+ const from = costMonitorFilterDay(filter.from);
4252
+ const to = costMonitorFilterDay(filter.to);
4253
+ const clauses = [
4254
+ "a.developer_account_id IS NOT NULL",
4255
+ "(? IS NULL OR a.occurred_day_sp >= ?)",
4256
+ "(? IS NULL OR a.occurred_day_sp <= ?)",
4257
+ ];
4258
+ const values: Array<string | null> = [from, from, to, to];
4259
+ if (filter.accountIds !== undefined && filter.accountIds.length > 0) {
4260
+ clauses.push(`a.developer_account_id IN (${filter.accountIds.map(() => "?").join(",")})`);
4261
+ values.push(...filter.accountIds);
4262
+ }
4263
+ const where = clauses.join(" AND ");
4264
+ const rows = this.database
4265
+ .prepare(`
4266
+ WITH selected AS (SELECT * FROM zaip_vw_activities a WHERE ${where}), completed AS (
4267
+ SELECT a.developer_account_id, COUNT(DISTINCT w.id) AS completed_task_count
4268
+ FROM zaip_vw_canonical_work_items w JOIN selected a ON a.run_id=w.completed_run_id
4269
+ WHERE w.entity_kind='task' AND w.status='completed' GROUP BY a.developer_account_id
4270
+ ) ${developerArtifactCountsCte()}
4271
+ SELECT a.developer_account_id AS account_id, MAX(a.developer_login) AS login_snapshot,
4272
+ CASE WHEN COUNT(DISTINCT a.developer_attribution_source)=1 THEN MAX(a.developer_attribution_source) ELSE 'mixed' END AS attribution_source,
4273
+ COUNT(DISTINCT a.prd_id) AS prd_count, COUNT(DISTINCT a.spec_id) AS spec_count,
4274
+ COUNT(DISTINCT a.task_id) AS task_count, COUNT(*) AS run_count,
4275
+ COALESCE(artifacts.test_count, 0) AS test_count,
4276
+ COALESCE(artifacts.evidence_count, 0) AS evidence_count,
4277
+ SUM(a.attributed_token_active_ms) AS total_duration_ms,
4278
+ COUNT(a.attributed_token_active_ms) AS duration_covered_runs,
4279
+ SUM(a.source_event_count) AS source_event_count,
4280
+ SUM(a.total_tokens) AS total_tokens, SUM(a.cost_usd) AS total_cost_usd, SUM(a.cost_brl) AS total_cost_brl,
4281
+ COUNT(a.total_tokens) AS token_covered_runs, COUNT(a.cost_usd) AS usd_covered_runs, COUNT(a.cost_brl) AS brl_covered_runs,
4282
+ COALESCE(completed.completed_task_count, 0) AS completed_task_count
4283
+ FROM selected a
4284
+ LEFT JOIN completed USING(developer_account_id)
4285
+ LEFT JOIN artifacts USING(developer_account_id)
4286
+ GROUP BY a.developer_account_id ORDER BY total_cost_brl DESC, account_id`)
4287
+ .all(...values, from, from, to, to) as Array<Record<string, unknown>>;
4288
+ // A delivery is factual only when the canonical Task points at the exact
4289
+ // completed Run. Contributors are the distinct attributed GitHub accounts
4290
+ // on that same Run; no timestamp, commit, or deployment inference enters.
4291
+ const delivered = this.database
4292
+ .prepare(`
4293
+ WITH selected AS (SELECT * FROM zaip_vw_activities a WHERE ${where})
4294
+ SELECT selected.developer_account_id AS delivered_by_account_id, w.id, w.title, w.status, w.completed_run_id,
4295
+ GROUP_CONCAT(DISTINCT contributor.developer_account_id || char(31) || COALESCE(contributor.developer_login, '')) AS contributor_pairs
4296
+ FROM zaip_vw_canonical_work_items w
4297
+ JOIN selected ON selected.run_id=w.completed_run_id
4298
+ LEFT JOIN zaip_vw_activities contributor ON contributor.task_id=w.id
4299
+ AND contributor.developer_account_id IS NOT NULL
4300
+ WHERE w.entity_kind='task' AND w.status='completed'
4301
+ GROUP BY selected.developer_account_id, w.id, w.title, w.status, w.completed_run_id
4302
+ ORDER BY w.id`)
4303
+ .all(...values) as Array<Record<string, unknown>>;
4304
+ const deliveriesByAccount = new Map<string, Array<Record<string, unknown>>>();
4305
+ for (const task of delivered) {
4306
+ const accountId = String(task.delivered_by_account_id);
4307
+ const list = deliveriesByAccount.get(accountId) ?? [];
4308
+ list.push({
4309
+ id: task.id,
4310
+ title: task.title,
4311
+ status: task.status,
4312
+ completed_run_id: task.completed_run_id,
4313
+ delivered_by: { account_id: accountId },
4314
+ contributors: String(task.contributor_pairs ?? "")
4315
+ .split(",")
4316
+ .filter(Boolean)
4317
+ .map((pair) => {
4318
+ const [account_id, login_snapshot] = pair.split(String.fromCharCode(31));
4319
+ return { account_id, login_snapshot: login_snapshot || null };
4320
+ }),
4321
+ });
4322
+ deliveriesByAccount.set(accountId, list);
4323
+ }
4324
+ const qualificationForSource: Record<
4325
+ string,
4326
+ "verified" | "cached_fallback" | "human_assigned" | "mixed"
4327
+ > = {
4328
+ verified_local_account: "verified",
4329
+ cached_fallback: "cached_fallback",
4330
+ human_assigned: "human_assigned",
4331
+ mixed: "mixed",
4332
+ };
4333
+ for (const row of rows) {
4334
+ row.completed_tasks = deliveriesByAccount.get(String(row.account_id)) ?? [];
4335
+ row.qualification = qualificationForSource[String(row.attribution_source)] ?? "unqualified";
4336
+ }
4337
+ return {
4338
+ data_source: "sqlite_read_model" as const,
4339
+ filters: { from, to, account_ids: filter.accountIds ?? [] },
4340
+ rows,
4341
+ totals: {
4342
+ run_count: rows.reduce((sum, row) => sum + Number(row.run_count ?? 0), 0),
4343
+ completed_task_count: rows.reduce(
4344
+ (sum, row) => sum + Number(row.completed_task_count ?? 0),
4345
+ 0,
4346
+ ),
4347
+ total_cost_usd: sumKnownDeveloperMetric(rows, "total_cost_usd"),
4348
+ total_cost_brl: sumKnownDeveloperMetric(rows, "total_cost_brl"),
4349
+ },
4350
+ };
4351
+ }
4352
+
4353
+ /**
4354
+ * Operations-only developer projection. It deliberately keeps the Cost
4355
+ * Monitor aggregate unchanged: status, kind and risk are derived solely
4356
+ * from canonical Task records linked to the developer's observed Task ids.
4357
+ */
4358
+ operationsDeveloperAggregate(filter: {
4359
+ from: string | null;
4360
+ to: string | null;
4361
+ accountIds?: readonly string[];
4362
+ }) {
4363
+ const from = costMonitorFilterDay(filter.from);
4364
+ const to = costMonitorFilterDay(filter.to);
4365
+ const clauses = [
4366
+ "a.developer_account_id IS NOT NULL",
4367
+ "(? IS NULL OR a.occurred_day_sp >= ?)",
4368
+ "(? IS NULL OR a.occurred_day_sp <= ?)",
4369
+ ];
4370
+ const values: Array<string | null> = [from, from, to, to];
4371
+ if (filter.accountIds !== undefined && filter.accountIds.length > 0) {
4372
+ clauses.push(`a.developer_account_id IN (${filter.accountIds.map(() => "?").join(",")})`);
4373
+ values.push(...filter.accountIds);
4374
+ }
4375
+ const where = clauses.join(" AND ");
4376
+ const rows = this.database
4377
+ .prepare(`
4378
+ WITH selected AS (SELECT * FROM zaip_vw_activities a WHERE ${where}),
4379
+ developer_tasks AS (
4380
+ SELECT DISTINCT developer_account_id AS account_id, task_id
4381
+ FROM selected WHERE task_id IS NOT NULL
4382
+ ), canonical_task_facts AS (
4383
+ SELECT developer_tasks.account_id, work.status, work.entity_kind, work.risk
4384
+ FROM developer_tasks JOIN zaip_vw_canonical_work_items work
4385
+ ON work.entity_kind='task' AND work.id=developer_tasks.task_id
4386
+ ), status_counts AS (
4387
+ SELECT account_id, json_group_object(status, task_count) AS status_counts_json
4388
+ FROM (SELECT account_id, status, COUNT(*) AS task_count FROM canonical_task_facts
4389
+ WHERE status IS NOT NULL GROUP BY account_id, status) GROUP BY account_id
4390
+ ), type_counts AS (
4391
+ SELECT account_id, json_group_object(entity_kind, task_count) AS type_counts_json
4392
+ FROM (SELECT account_id, entity_kind, COUNT(*) AS task_count FROM canonical_task_facts
4393
+ WHERE entity_kind IS NOT NULL GROUP BY account_id, entity_kind) GROUP BY account_id
4394
+ ), risk_counts AS (
4395
+ SELECT account_id, json_group_object(risk, task_count) AS risk_counts_json
4396
+ FROM (SELECT account_id, risk, COUNT(*) AS task_count FROM canonical_task_facts
4397
+ WHERE risk IS NOT NULL GROUP BY account_id, risk) GROUP BY account_id
4398
+ ), completed AS (
4399
+ SELECT account_id, COUNT(*) AS completed_task_count
4400
+ FROM canonical_task_facts WHERE status='completed'
4401
+ GROUP BY account_id
4402
+ )
4403
+ -- task_count is the distinct, observed Task-id population attributed
4404
+ -- to this developer in the selected period. Canonical records do not
4405
+ -- retain a creator identity, so this projection never infers one.
4406
+ SELECT selected.developer_account_id AS account_id, MAX(selected.developer_login) AS login_snapshot,
4407
+ COUNT(DISTINCT selected.task_id) AS task_count,
4408
+ COALESCE(completed.completed_task_count, 0) AS completed_task_count,
4409
+ SUM(selected.attributed_token_active_ms) AS total_duration_ms,
4410
+ SUM(selected.total_tokens) AS total_tokens,
4411
+ SUM(selected.cost_brl) AS total_cost_brl,
4412
+ status_counts.status_counts_json, type_counts.type_counts_json, risk_counts.risk_counts_json
4413
+ FROM selected
4414
+ LEFT JOIN completed ON completed.account_id=selected.developer_account_id
4415
+ LEFT JOIN status_counts ON status_counts.account_id=selected.developer_account_id
4416
+ LEFT JOIN type_counts ON type_counts.account_id=selected.developer_account_id
4417
+ LEFT JOIN risk_counts ON risk_counts.account_id=selected.developer_account_id
4418
+ GROUP BY selected.developer_account_id
4419
+ ORDER BY total_cost_brl DESC, account_id`)
4420
+ .all(...values) as Array<Record<string, unknown>>;
4421
+ const distribution = (value: unknown): Record<string, number> | null => {
4422
+ if (typeof value !== "string") return null;
4423
+ try {
4424
+ const parsed = JSON.parse(value) as Record<string, unknown>;
4425
+ return Object.fromEntries(
4426
+ Object.entries(parsed).flatMap(([key, count]) =>
4427
+ typeof count === "number" && Number.isFinite(count) ? [[key, count]] : [],
4428
+ ),
4429
+ );
4430
+ } catch {
4431
+ return null;
4432
+ }
4433
+ };
4434
+ for (const row of rows) {
4435
+ row.status_counts = distribution(row.status_counts_json);
4436
+ row.type_counts = distribution(row.type_counts_json);
4437
+ row.risk_counts = distribution(row.risk_counts_json);
4438
+ delete row.status_counts_json;
4439
+ delete row.type_counts_json;
4440
+ delete row.risk_counts_json;
4441
+ }
4442
+ return {
4443
+ data_source: "sqlite_read_model" as const,
4444
+ filters: { from, to, account_ids: filter.accountIds ?? [] },
4445
+ rows,
4446
+ };
4447
+ }
4448
+
4517
4449
  costMonitorSummaryAggregate(
4518
4450
  filter: Parameters<SqliteReadModelFileSystem["activityCostRows"]>[0],
4519
- ) {
4451
+ ): CostSummarySource {
4520
4452
  const { from, to, providers = [], models = [] } = filter;
4521
4453
  const sourceFrom = costMonitorFilterDay(from);
4522
4454
  const sourceTo = costMonitorFilterDay(to);
4455
+ const cacheKey = JSON.stringify({ ...filter, from: sourceFrom, to: sourceTo });
4456
+ const generationCached = this.costMonitorSummaryCache.get(cacheKey);
4457
+ if (generationCached !== undefined) return structuredClone(generationCached);
4458
+ const remember = (value: CostSummarySource) => {
4459
+ if (this.costMonitorSummaryCache.size >= 64) {
4460
+ const oldest = this.costMonitorSummaryCache.keys().next().value as string | undefined;
4461
+ if (oldest !== undefined) this.costMonitorSummaryCache.delete(oldest);
4462
+ }
4463
+ this.costMonitorSummaryCache.set(cacheKey, structuredClone(value));
4464
+ return structuredClone(value);
4465
+ };
4466
+ const unfilteredAllTime =
4467
+ sourceFrom === null &&
4468
+ sourceTo === null &&
4469
+ [
4470
+ providers,
4471
+ models,
4472
+ filter.agents,
4473
+ filter.phases,
4474
+ filter.statuses,
4475
+ filter.tasks,
4476
+ filter.specs,
4477
+ filter.prds,
4478
+ filter.executions,
4479
+ filter.changes,
4480
+ ].every((values) => values === undefined || values.length === 0);
4481
+ if (unfilteredAllTime) {
4482
+ const cached = this.database
4483
+ .prepare(
4484
+ "SELECT payload_json FROM zaip_aios_activities WHERE payload_type='cost_monitor_summary_cache' ORDER BY recorded_at DESC LIMIT 1",
4485
+ )
4486
+ .get() as { payload_json?: string } | undefined;
4487
+ if (typeof cached?.payload_json === "string")
4488
+ return remember(JSON.parse(cached.payload_json) as CostSummarySource);
4489
+ }
4490
+ if (
4491
+ sourceFrom !== null &&
4492
+ sourceTo !== null &&
4493
+ [
4494
+ providers,
4495
+ models,
4496
+ filter.agents,
4497
+ filter.phases,
4498
+ filter.statuses,
4499
+ filter.tasks,
4500
+ filter.specs,
4501
+ filter.prds,
4502
+ filter.executions,
4503
+ filter.changes,
4504
+ ].every((values) => values === undefined || values.length === 0)
4505
+ ) {
4506
+ const cached = this.database
4507
+ .prepare(
4508
+ "SELECT payload_json FROM zaip_aios_activities WHERE payload_type='cost_monitor_summary_month_cache' ORDER BY recorded_at DESC LIMIT 1",
4509
+ )
4510
+ .get() as { payload_json?: string } | undefined;
4511
+ if (typeof cached?.payload_json === "string") {
4512
+ const payload = JSON.parse(cached.payload_json) as {
4513
+ from: string;
4514
+ to: string;
4515
+ aggregate: CostSummarySource;
4516
+ };
4517
+ if (payload.from === sourceFrom && payload.to === sourceTo)
4518
+ return remember(payload.aggregate);
4519
+ }
4520
+ }
4523
4521
  const sourceDay =
4524
4522
  "date(COALESCE(a.source_started_at, a.source_occurred_at, a.occurred_at), '-3 hours')";
4525
4523
  const clauses = [
@@ -4691,16 +4689,7 @@ export class SqliteReadModelFileSystem extends WorkspaceFileSystem {
4691
4689
  AND (? IS NULL OR day >= ?)
4692
4690
  AND (? IS NULL OR day <= ?)
4693
4691
  GROUP BY day
4694
- UNION ALL
4695
- SELECT occurred_day_sp,
4696
- 0, 0, 0,
4697
- SUM(CASE WHEN artifact_kind='validation' THEN 1 ELSE 0 END),
4698
- SUM(CASE WHEN artifact_kind='evidence' THEN 1 ELSE 0 END)
4699
- FROM canonical_artifacts
4700
- WHERE occurred_day_sp IS NOT NULL
4701
- AND (? IS NULL OR occurred_day_sp >= ?)
4702
- AND (? IS NULL OR occurred_day_sp <= ?)
4703
- GROUP BY occurred_day_sp
4692
+ ${artifactStructuralRowsSql()}
4704
4693
  )
4705
4694
  SELECT day,
4706
4695
  SUM(prd_count) AS prd_count,
@@ -4912,7 +4901,7 @@ export class SqliteReadModelFileSystem extends WorkspaceFileSystem {
4912
4901
  coverage: { events: 0, tokens: 0, usd: 0, brl: 0 },
4913
4902
  };
4914
4903
  });
4915
- return {
4904
+ return remember({
4916
4905
  periods: { daily: periods },
4917
4906
  aggregate_usage: {
4918
4907
  conversation,
@@ -4926,7 +4915,7 @@ export class SqliteReadModelFileSystem extends WorkspaceFileSystem {
4926
4915
  model_groups,
4927
4916
  cost_monitor_daily_duration: durations,
4928
4917
  daily_rows: compactDailyRows,
4929
- };
4918
+ });
4930
4919
  }
4931
4920
 
4932
4921
  /** Keyset cursor or LIMIT/OFFSET pagination for materialized work summaries. */
@@ -5005,6 +4994,21 @@ export class SqliteReadModelFileSystem extends WorkspaceFileSystem {
5005
4994
  };
5006
4995
  }
5007
4996
 
4997
+ /** Canonical page for the Board: keeps telemetry-only historical rows out. */
4998
+ canonicalWorkPage(
4999
+ kind: SqliteSummaryKind,
5000
+ filter: { from: string | null; to: string | null },
5001
+ offset: number,
5002
+ limit: number,
5003
+ ) {
5004
+ return this.viewPage("operations_work_list", {
5005
+ ...filter,
5006
+ kind: kind === "task" ? "all-tasks" : kind,
5007
+ offset,
5008
+ limit,
5009
+ });
5010
+ }
5011
+
5008
5012
  overviewPage(period: "day" | "month" | "year", offset: number, limit: number) {
5009
5013
  const total = Number(
5010
5014
  (
@@ -5037,11 +5041,6 @@ export class SqliteReadModelFileSystem extends WorkspaceFileSystem {
5037
5041
  };
5038
5042
  }
5039
5043
 
5040
- /**
5041
- * Generation-local cache of the complete public page response. It caches
5042
- * post-enrichment rows and totals, never raw facts, so the daily Operations
5043
- * duration and Cost Monitor pre-pagination subtotals retain their contracts.
5044
- */
5045
5044
  viewPage(
5046
5045
  view: SqliteViewName,
5047
5046
  input: {
@@ -5057,13 +5056,11 @@ export class SqliteReadModelFileSystem extends WorkspaceFileSystem {
5057
5056
  origin?: string;
5058
5057
  workType?: string;
5059
5058
  priority?: string;
5059
+ kind?: "all" | "all-tasks" | "task" | "standalone-task" | "prd" | "spec";
5060
5060
  offset?: number;
5061
5061
  limit?: number;
5062
5062
  },
5063
5063
  ): ReturnType<SqliteReadModelFileSystem["uncachedViewPage"]> {
5064
- // The key includes every filter and pagination input. The cache is scoped
5065
- // to the immutable opened SQLite generation and reopen() clears it, so a
5066
- // launcher refresh cannot expose a page from the previous snapshot.
5067
5064
  const key = `${view}:${JSON.stringify(input)}`;
5068
5065
  const cached = this.viewPageResultCache.get(key);
5069
5066
  if (cached !== undefined)
@@ -5093,10 +5090,151 @@ export class SqliteReadModelFileSystem extends WorkspaceFileSystem {
5093
5090
  origin?: string;
5094
5091
  workType?: string;
5095
5092
  priority?: string;
5093
+ kind?: "all" | "all-tasks" | "task" | "standalone-task" | "prd" | "spec";
5096
5094
  offset?: number;
5097
5095
  limit?: number;
5098
5096
  },
5099
5097
  ) {
5098
+ const isUnfilteredAllTimeCostPage =
5099
+ !input.from &&
5100
+ !input.to &&
5101
+ !input.query &&
5102
+ !input.status &&
5103
+ !input.risk &&
5104
+ !input.provider &&
5105
+ !input.model &&
5106
+ !input.agent &&
5107
+ !input.phase &&
5108
+ !input.origin &&
5109
+ !input.workType &&
5110
+ !input.priority;
5111
+ const isDateOnlyCostInvocationPage =
5112
+ view === "cost_monitor_agent_invocations" &&
5113
+ !input.query &&
5114
+ !input.status &&
5115
+ !input.risk &&
5116
+ !input.provider &&
5117
+ !input.model &&
5118
+ !input.agent &&
5119
+ !input.phase &&
5120
+ !input.origin &&
5121
+ !input.workType &&
5122
+ !input.priority;
5123
+ if (isDateOnlyCostInvocationPage) {
5124
+ const projection = filterCostInvocationProjection(
5125
+ this.technicalPayloadRows("cost_agent_invocation_projection_v1"),
5126
+ input,
5127
+ );
5128
+ if (projection.length > 0) {
5129
+ const limit = Math.min(200, Math.max(1, input.limit ?? 100));
5130
+ const offset = Math.max(0, input.offset ?? 0);
5131
+ const rows = projection.slice(offset, offset + limit);
5132
+ return {
5133
+ data_source: "sqlite_read_model" as const,
5134
+ view,
5135
+ offset,
5136
+ limit,
5137
+ total_count: projection.length,
5138
+ has_more: offset + rows.length < projection.length,
5139
+ rows,
5140
+ };
5141
+ }
5142
+ }
5143
+ const remoteAggregateDimensions: Partial<Record<SqliteViewName, string>> = {
5144
+ cost_monitor_zaip_agents: "agent",
5145
+ cost_monitor_executions: "execution",
5146
+ cost_monitor_tasks: "task",
5147
+ cost_monitor_conversations: "conversation",
5148
+ };
5149
+ const remoteAggregateDimension = remoteAggregateDimensions[view];
5150
+ if (isUnfilteredAllTimeCostPage && remoteAggregateDimension !== undefined) {
5151
+ const projection = this.technicalPayloadRows("cost_dimension_aggregate_v1").filter(
5152
+ (row) => row.dimension === remoteAggregateDimension,
5153
+ );
5154
+ const rows = projection
5155
+ .filter((row) => row.row_kind === "row")
5156
+ .map((row) =>
5157
+ view === "cost_monitor_zaip_agents"
5158
+ ? {
5159
+ key: row.key,
5160
+ label: row.label,
5161
+ persona: row.persona,
5162
+ job: row.job,
5163
+ latest_started_at: row.latest_started_at,
5164
+ invocation_count: row.invocation_count,
5165
+ task_count: row.task_count,
5166
+ total_duration: row.total_duration,
5167
+ duration_unknown: row.duration_unknown,
5168
+ duration_known: row.duration_known,
5169
+ source_event_count: row.source_event_count,
5170
+ total_tokens: row.total_tokens,
5171
+ total_cost_usd: row.total_cost_usd,
5172
+ total_cost_brl: row.total_cost_brl,
5173
+ }
5174
+ : {
5175
+ id: row.id,
5176
+ key: row.key,
5177
+ title: row.title,
5178
+ status: row.status,
5179
+ risk: row.risk,
5180
+ first_day: row.first_day,
5181
+ last_day: row.last_day,
5182
+ occurred_at: row.occurred_at,
5183
+ invocation_count: row.invocation_count,
5184
+ llm_call_count: row.llm_call_count,
5185
+ total_duration: row.total_duration,
5186
+ total_input_tokens: row.total_input_tokens,
5187
+ total_cached_input_tokens: row.total_cached_input_tokens,
5188
+ total_output_tokens: row.total_output_tokens,
5189
+ total_reasoning_tokens: row.total_reasoning_tokens,
5190
+ total_tokens: row.total_tokens,
5191
+ total_cost_usd: row.total_cost_usd,
5192
+ total_cost_brl: row.total_cost_brl,
5193
+ },
5194
+ )
5195
+ .sort(
5196
+ (left, right) =>
5197
+ Number(right.total_cost_brl ?? -1) - Number(left.total_cost_brl ?? -1) ||
5198
+ String(left.key ?? "").localeCompare(String(right.key ?? "")),
5199
+ ) as Array<Record<string, unknown>>;
5200
+ const totals = projection.find((row) => row.row_kind === "totals");
5201
+ if (rows.length > 0 && totals !== undefined) {
5202
+ const limit = Math.min(200, Math.max(1, input.limit ?? 50));
5203
+ const offset = Math.max(0, input.offset ?? 0);
5204
+ const page = rows.slice(offset, offset + limit);
5205
+ const publicTotals =
5206
+ view !== "cost_monitor_zaip_agents"
5207
+ ? {
5208
+ entity_count: totals.entity_count,
5209
+ invocation_count: totals.invocation_count,
5210
+ total_duration: totals.total_duration,
5211
+ total_tokens: totals.total_tokens,
5212
+ total_cost_usd: totals.total_cost_usd,
5213
+ total_cost_brl: totals.total_cost_brl,
5214
+ }
5215
+ : {
5216
+ invocation_count: totals.invocation_count,
5217
+ task_count: totals.task_count,
5218
+ total_duration: totals.total_duration,
5219
+ duration_unknown: totals.duration_unknown,
5220
+ duration_known: totals.duration_known,
5221
+ source_event_count: totals.source_event_count,
5222
+ total_tokens: totals.total_tokens,
5223
+ total_cost_usd: totals.total_cost_usd,
5224
+ total_cost_brl: totals.total_cost_brl,
5225
+ };
5226
+ return {
5227
+ data_source: "sqlite_read_model" as const,
5228
+ view,
5229
+ offset,
5230
+ limit,
5231
+ total_count: rows.length,
5232
+ has_more: offset + page.length < rows.length,
5233
+ totals: publicTotals,
5234
+ rows: page,
5235
+ };
5236
+ }
5237
+ }
5100
5238
  const cacheableUnfilteredViews: readonly SqliteViewName[] = [
5101
5239
  "operations_work_kanban",
5102
5240
  "operations_executions",
@@ -5132,31 +5270,18 @@ export class SqliteReadModelFileSystem extends WorkspaceFileSystem {
5132
5270
  rows: page,
5133
5271
  };
5134
5272
  }
5135
- // This is the common unfiltered pagination flow used by Operations. Cache
5136
- // the complete, deterministic result once per private SQLite generation so
5137
- // a 974-row list cannot re-run its aggregate SQL for every browser page.
5138
- // Filtered requests deliberately retain the database path below because
5139
- // SQLite owns their LIKE/filter semantics.
5140
- if (
5141
- view === "operations_work_list" &&
5142
- !input.from &&
5143
- !input.to &&
5144
- !input.query &&
5145
- !input.status &&
5146
- !input.risk
5147
- ) {
5148
- const rows = this.operationsWorkListRows();
5149
- const limit = Math.min(200, Math.max(1, input.limit ?? 50));
5150
- const offset = Math.max(0, input.offset ?? 0);
5151
- const page = rows.slice(offset, offset + limit);
5273
+ if (view === "operations_work_list") {
5274
+ const payload =
5275
+ input.from || input.to
5276
+ ? this.firstFoldMonthlyQuick({ from: input.from ?? null, to: input.to ?? null })
5277
+ : this.technicalPayloadRows("first_fold_all_time_board_cache")[0];
5278
+ const cached = operationsWorkListPayloadPage(input, payload);
5279
+ const result = cached ?? operationsWorkListSqlPage(this.database, input);
5152
5280
  return {
5153
5281
  data_source: "sqlite_read_model" as const,
5154
5282
  view,
5155
- offset,
5156
- limit,
5157
- total_count: rows.length,
5158
- has_more: offset + page.length < rows.length,
5159
- rows: page,
5283
+ ...result,
5284
+ rows: attachOperationsWorkListDevelopers(this.database, result.rows, input),
5160
5285
  };
5161
5286
  }
5162
5287
  if (view === "cost_monitor_agent_invocations") {
@@ -5210,13 +5335,15 @@ export class SqliteReadModelFileSystem extends WorkspaceFileSystem {
5210
5335
  const total_count = Number(
5211
5336
  (
5212
5337
  this.database
5213
- .prepare(`SELECT COUNT(*) AS count FROM cost_agent_invocations i WHERE ${where}`)
5338
+ .prepare(
5339
+ `SELECT COUNT(*) AS count FROM cost_monitor_agent_invocations i WHERE ${where}`,
5340
+ )
5214
5341
  .get(...values) as { count: number }
5215
5342
  ).count,
5216
5343
  );
5217
5344
  const rows = this.database
5218
5345
  .prepare(
5219
- `SELECT i.* FROM cost_agent_invocations i WHERE ${where} ORDER BY i.occurred_at DESC, i.agent_invocation_id DESC LIMIT ? OFFSET ?`,
5346
+ `SELECT i.* FROM cost_monitor_agent_invocations i WHERE ${where} ORDER BY i.occurred_at DESC, i.agent_invocation_id DESC LIMIT ? OFFSET ?`,
5220
5347
  )
5221
5348
  .all(...values, limit, offset) as Array<Record<string, unknown>>;
5222
5349
  return {
@@ -5240,6 +5367,35 @@ export class SqliteReadModelFileSystem extends WorkspaceFileSystem {
5240
5367
  cost_monitor_providers: "provider",
5241
5368
  };
5242
5369
  const filteredCostGroup = filteredCostGroups[view];
5370
+ if (
5371
+ (filteredCostGroup === "agent" ||
5372
+ filteredCostGroup === "model" ||
5373
+ filteredCostGroup === "provider") &&
5374
+ isDateOnlyCostPage(input)
5375
+ ) {
5376
+ const from = costMonitorFilterDay(input.from) ?? "0000-01-01";
5377
+ const to = costMonitorFilterDay(input.to) ?? "9999-12-31";
5378
+ const limit = Math.min(200, Math.max(1, input.limit ?? 50));
5379
+ const offset = Math.max(0, input.offset ?? 0);
5380
+ const fast = readDateOnlyCostDimensionPage(
5381
+ this.database,
5382
+ filteredCostGroup,
5383
+ from,
5384
+ to,
5385
+ limit,
5386
+ offset,
5387
+ );
5388
+ return {
5389
+ data_source: "sqlite_read_model" as const,
5390
+ view,
5391
+ offset,
5392
+ limit,
5393
+ total_count: fast.total_count,
5394
+ has_more: offset + fast.rows.length < fast.total_count,
5395
+ totals: fast.totals,
5396
+ rows: fast.rows,
5397
+ };
5398
+ }
5243
5399
  const isUnfilteredCostDimension =
5244
5400
  (filteredCostGroup === "model" || filteredCostGroup === "provider") &&
5245
5401
  !input.from &&
@@ -5438,20 +5594,23 @@ export class SqliteReadModelFileSystem extends WorkspaceFileSystem {
5438
5594
  activityValues.push(query, query, query);
5439
5595
  }
5440
5596
  const where = clauses.join(" AND ");
5441
- const invocationClauses = [
5442
- "i.zaip_agent IS NOT NULL",
5443
- "(i.total_tokens IS NOT NULL OR i.cost_brl IS NOT NULL OR i.cost_usd IS NOT NULL)",
5444
- ];
5597
+ const invocationClauses = ["i.zaip_agent IS NOT NULL"];
5445
5598
  const invocationValues: Array<string | number> = [];
5446
5599
  const invocationSourceDay =
5447
5600
  "date(COALESCE(i.source_started_at, i.source_occurred_at, i.occurred_at), '-3 hours')";
5448
5601
  if (input.from) {
5449
5602
  invocationClauses.push(`${invocationSourceDay} >= ?`);
5450
5603
  invocationValues.push(input.from.slice(0, 10));
5604
+ // Keep the source-day predicate for historical correctness, while
5605
+ // using the materialized day key to seek the immutable fact first.
5606
+ invocationClauses.push("i.occurred_day_sp >= ?");
5607
+ invocationValues.push(input.from.slice(0, 10));
5451
5608
  }
5452
5609
  if (input.to) {
5453
5610
  invocationClauses.push(`${invocationSourceDay} <= ?`);
5454
5611
  invocationValues.push(input.to.slice(0, 10));
5612
+ invocationClauses.push("i.occurred_day_sp <= ?");
5613
+ invocationValues.push(input.to.slice(0, 10));
5455
5614
  }
5456
5615
  if (input.agent) {
5457
5616
  invocationClauses.push("i.zaip_agent = ?");
@@ -5551,6 +5710,7 @@ export class SqliteReadModelFileSystem extends WorkspaceFileSystem {
5551
5710
  if (filteredCostGroup === "model") {
5552
5711
  const clauses = ["1=1"];
5553
5712
  const values: Array<string | number> = [];
5713
+ const dimensionDateFilter = costDimensionFactDateFilter(input);
5554
5714
  if (input.from) {
5555
5715
  clauses.push(
5556
5716
  "date(COALESCE(i.source_started_at, i.source_occurred_at, i.occurred_at), '-3 hours') >= ?",
@@ -5677,7 +5837,7 @@ export class SqliteReadModelFileSystem extends WorkspaceFileSystem {
5677
5837
  SUM(output_tokens) AS output_tokens, SUM(reasoning_tokens) AS reasoning_tokens,
5678
5838
  SUM(total_tokens) AS total_tokens, SUM(cost_usd) AS cost_usd, SUM(cost_brl) AS cost_brl
5679
5839
  FROM cost_invocation_dimension_facts
5680
- WHERE total_tokens IS NOT NULL OR cost_usd IS NOT NULL OR cost_brl IS NOT NULL
5840
+ WHERE (total_tokens IS NOT NULL OR cost_usd IS NOT NULL OR cost_brl IS NOT NULL)${dimensionDateFilter}
5681
5841
  GROUP BY agent_invocation_id
5682
5842
  ), segments AS (
5683
5843
  SELECT agent_invocation_id,
@@ -5687,7 +5847,7 @@ export class SqliteReadModelFileSystem extends WorkspaceFileSystem {
5687
5847
  SUM(output_tokens) AS output_tokens, SUM(reasoning_tokens) AS reasoning_tokens,
5688
5848
  SUM(total_tokens) AS total_tokens, SUM(cost_usd) AS cost_usd, SUM(cost_brl) AS cost_brl
5689
5849
  FROM cost_invocation_dimension_facts
5690
- WHERE total_tokens IS NOT NULL OR cost_usd IS NOT NULL OR cost_brl IS NOT NULL
5850
+ WHERE (total_tokens IS NOT NULL OR cost_usd IS NOT NULL OR cost_brl IS NOT NULL)${dimensionDateFilter}
5691
5851
  GROUP BY agent_invocation_id, model_key, provider
5692
5852
  UNION ALL
5693
5853
  -- Preserve every factual residual from the consolidated invocation:
@@ -5800,6 +5960,7 @@ export class SqliteReadModelFileSystem extends WorkspaceFileSystem {
5800
5960
  if (filteredCostGroup === "provider") {
5801
5961
  const clauses = ["1=1"];
5802
5962
  const values: Array<string | number> = [];
5963
+ const dimensionDateFilter = costDimensionFactDateFilter(input);
5803
5964
  if (input.from) {
5804
5965
  clauses.push(
5805
5966
  "date(COALESCE(i.source_started_at, i.source_occurred_at, i.occurred_at), '-3 hours') >= ?",
@@ -5863,7 +6024,7 @@ export class SqliteReadModelFileSystem extends WorkspaceFileSystem {
5863
6024
  SUM(output_tokens) AS output_tokens, SUM(reasoning_tokens) AS reasoning_tokens,
5864
6025
  SUM(total_tokens) AS total_tokens, SUM(cost_usd) AS cost_usd, SUM(cost_brl) AS cost_brl
5865
6026
  FROM cost_invocation_dimension_facts
5866
- WHERE total_tokens IS NOT NULL OR cost_usd IS NOT NULL OR cost_brl IS NOT NULL
6027
+ WHERE (total_tokens IS NOT NULL OR cost_usd IS NOT NULL OR cost_brl IS NOT NULL)${dimensionDateFilter}
5867
6028
  GROUP BY agent_invocation_id
5868
6029
  ), segments AS (
5869
6030
  SELECT agent_invocation_id,
@@ -5873,7 +6034,7 @@ export class SqliteReadModelFileSystem extends WorkspaceFileSystem {
5873
6034
  SUM(output_tokens) AS output_tokens, SUM(reasoning_tokens) AS reasoning_tokens,
5874
6035
  SUM(total_tokens) AS total_tokens, SUM(cost_usd) AS cost_usd, SUM(cost_brl) AS cost_brl
5875
6036
  FROM cost_invocation_dimension_facts
5876
- WHERE total_tokens IS NOT NULL OR cost_usd IS NOT NULL OR cost_brl IS NOT NULL
6037
+ WHERE (total_tokens IS NOT NULL OR cost_usd IS NOT NULL OR cost_brl IS NOT NULL)${dimensionDateFilter}
5877
6038
  GROUP BY agent_invocation_id, provider_key, model_key
5878
6039
  UNION ALL
5879
6040
  -- See the equivalent Models branch: attribute only the segment part;
@@ -5973,10 +6134,6 @@ export class SqliteReadModelFileSystem extends WorkspaceFileSystem {
5973
6134
  if (filteredCostGroup !== undefined && temporalCostGroup && !hasDimensionalCostFilter) {
5974
6135
  const from = costMonitorFilterDay(input.from);
5975
6136
  const to = costMonitorFilterDay(input.to);
5976
- // Cost temporal tables reuse the same aggregate-only projection as the
5977
- // summary cards. This intentionally bypasses daily_aggregate_cache,
5978
- // whose recursive all-dashboard view used to expand the complete fact
5979
- // graph before returning a handful of visible columns.
5980
6137
  const aggregate = this.costMonitorSummaryAggregate({ from, to });
5981
6138
  const financialRows = aggregate.daily_rows as Array<Record<string, unknown>>;
5982
6139
  const financialByDay = new Map(financialRows.map((row) => [String(row.key), row]));
@@ -6629,130 +6786,7 @@ export class SqliteReadModelFileSystem extends WorkspaceFileSystem {
6629
6786
  rows,
6630
6787
  };
6631
6788
  }
6632
- const entityMetadata = {
6633
- task: {
6634
- table: "tasks",
6635
- title: "entity.title",
6636
- status: "entity.status",
6637
- risk: "entity.risk",
6638
- },
6639
- spec: {
6640
- table: "specs",
6641
- title: "entity.title",
6642
- status: "entity.status",
6643
- risk: "entity.risk",
6644
- },
6645
- prd: { table: "prds", title: "entity.title", status: "entity.status", risk: "entity.risk" },
6646
- execution: {
6647
- table: "executions",
6648
- title: "grouped.group_value",
6649
- status: "entity.status",
6650
- risk: "NULL",
6651
- },
6652
- conversation: {
6653
- table: "conversations",
6654
- title: "grouped.group_value",
6655
- status: "entity.status",
6656
- risk: "NULL",
6657
- },
6658
- } as const;
6659
- const metadata = entityMetadata[groupKind as keyof typeof entityMetadata];
6660
- const clauses = ["group_kind = ?", "group_value IS NOT NULL"];
6661
- const values: Array<string | number> = [groupKind];
6662
- if (input.from) {
6663
- clauses.push("day >= ?");
6664
- values.push(input.from.slice(0, 10));
6665
- }
6666
- if (input.to) {
6667
- clauses.push("day <= ?");
6668
- values.push(input.to.slice(0, 10));
6669
- }
6670
- if (input.query) {
6671
- clauses.push("group_value LIKE ?");
6672
- values.push(`%${input.query}%`);
6673
- }
6674
- const groupFilter =
6675
- groupKind === "provider"
6676
- ? input.provider
6677
- : groupKind === "model"
6678
- ? input.model
6679
- : groupKind === "zaip_agent"
6680
- ? input.agent
6681
- : undefined;
6682
- if (groupFilter) {
6683
- clauses.push("group_value = ?");
6684
- values.push(groupFilter);
6685
- }
6686
- const where = clauses.join(" AND ");
6687
- const total_count = Number(
6688
- (
6689
- this.database
6690
- .prepare(
6691
- `SELECT COUNT(DISTINCT group_value) AS count FROM cost_monitor_group_daily_cache WHERE ${where}`,
6692
- )
6693
- .get(...values) as { count: number }
6694
- ).count,
6695
- );
6696
- // This aggregate deliberately precedes LIMIT/OFFSET: a compact page is
6697
- // only a window into the selected entity population, never its subtotal.
6698
- // SUM preserves a factual 0 and leaves a wholly unavailable metric NULL.
6699
- const totals = this.database
6700
- .prepare(
6701
- `SELECT COUNT(DISTINCT group_value) AS entity_count,
6702
- COALESCE(SUM(invocation_count), 0) AS invocation_count,
6703
- ROUND(SUM(total_duration)) AS total_duration,
6704
- SUM(total_tokens) AS total_tokens,
6705
- SUM(total_cost_usd) AS total_cost_usd,
6706
- SUM(total_cost_brl) AS total_cost_brl
6707
- FROM cost_monitor_group_daily_cache
6708
- WHERE ${where}`,
6709
- )
6710
- .get(...values) as Record<string, unknown>;
6711
- const limit = Math.min(200, Math.max(1, input.limit ?? 50));
6712
- const offset = Math.max(0, input.offset ?? 0);
6713
- const rows = this.database
6714
- .prepare(
6715
- `WITH grouped AS (
6716
- SELECT group_value, MIN(day) AS first_day, MAX(day) AS last_day,
6717
- SUM(invocation_count) AS invocation_count,
6718
- SUM(llm_call_count) AS llm_call_count,
6719
- SUM(total_duration) AS total_duration,
6720
- SUM(total_input_tokens) AS total_input_tokens,
6721
- SUM(total_cached_input_tokens) AS total_cached_input_tokens,
6722
- SUM(total_output_tokens) AS total_output_tokens,
6723
- SUM(total_reasoning_tokens) AS total_reasoning_tokens,
6724
- SUM(total_tokens) AS total_tokens,
6725
- SUM(total_cost_usd) AS total_cost_usd,
6726
- SUM(total_cost_brl) AS total_cost_brl
6727
- FROM cost_monitor_group_daily_cache
6728
- WHERE ${where}
6729
- GROUP BY group_value
6730
- )
6731
- SELECT grouped.group_value AS id, grouped.group_value AS key,
6732
- COALESCE(${metadata.title}, grouped.group_value) AS title,
6733
- ${metadata.status} AS status, ${metadata.risk} AS risk,
6734
- grouped.first_day, grouped.last_day,
6735
- grouped.last_day || 'T12:00:00.000Z' AS occurred_at,
6736
- grouped.invocation_count, grouped.llm_call_count, grouped.total_duration,
6737
- grouped.total_input_tokens, grouped.total_cached_input_tokens,
6738
- grouped.total_output_tokens, grouped.total_reasoning_tokens,
6739
- grouped.total_tokens, grouped.total_cost_usd, grouped.total_cost_brl
6740
- FROM grouped
6741
- LEFT JOIN ${metadata.table} entity ON entity.id = grouped.group_value
6742
- ORDER BY grouped.total_cost_brl DESC, grouped.group_value
6743
- LIMIT ? OFFSET ?`,
6744
- )
6745
- .all(...values, limit, offset) as Array<Record<string, unknown>>;
6746
- return {
6747
- data_source: "sqlite_read_model" as const,
6748
- view,
6749
- offset,
6750
- limit,
6751
- total_count,
6752
- has_more: offset + rows.length < total_count,
6753
- totals,
6754
- rows,
6755
- };
6789
+ return readCostGroupPage(this.database, view, groupKind as CostGroupKind, input);
6756
6790
  }
6757
6791
  const meta = sqliteViewMetadata[view];
6758
6792
  const clauses: string[] = [];