@zaip-ai/cli 1.3.8 → 1.4.1

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 (315) hide show
  1. package/README.md +58 -3
  2. package/dist/commands/init.d.ts +1 -0
  3. package/dist/commands/init.d.ts.map +1 -1
  4. package/dist/commands/init.js +17 -6
  5. package/dist/commands/init.js.map +1 -1
  6. package/dist/index.d.ts +1 -0
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +2 -0
  9. package/dist/index.js.map +1 -1
  10. package/dist/services/adapters.js +4 -4
  11. package/dist/services/adapters.js.map +1 -1
  12. package/dist/services/codex-config-template.d.ts +2 -2
  13. package/dist/services/codex-config-template.d.ts.map +1 -1
  14. package/dist/services/codex-config-template.js +3 -3
  15. package/dist/services/codex-config-template.js.map +1 -1
  16. package/dist/services/codex-rules.d.ts +1 -1
  17. package/dist/services/codex-rules.d.ts.map +1 -1
  18. package/dist/services/codex-rules.js +9 -24
  19. package/dist/services/codex-rules.js.map +1 -1
  20. package/dist/services/delivery-git-preflight.d.ts +23 -0
  21. package/dist/services/delivery-git-preflight.d.ts.map +1 -0
  22. package/dist/services/delivery-git-preflight.js +69 -0
  23. package/dist/services/delivery-git-preflight.js.map +1 -0
  24. package/dist/services/doctor.d.ts.map +1 -1
  25. package/dist/services/doctor.js +13 -1
  26. package/dist/services/doctor.js.map +1 -1
  27. package/dist/services/install-agents-template.d.ts.map +1 -1
  28. package/dist/services/install-agents-template.js +22 -28
  29. package/dist/services/install-agents-template.js.map +1 -1
  30. package/dist/services/install-profile.d.ts +8 -0
  31. package/dist/services/install-profile.d.ts.map +1 -0
  32. package/dist/services/install-profile.js +56 -0
  33. package/dist/services/install-profile.js.map +1 -0
  34. package/dist/services/logger.d.ts.map +1 -1
  35. package/dist/services/logger.js +6 -5
  36. package/dist/services/logger.js.map +1 -1
  37. package/dist/services/stack-git.d.ts +9 -0
  38. package/dist/services/stack-git.d.ts.map +1 -0
  39. package/dist/services/stack-git.js +188 -0
  40. package/dist/services/stack-git.js.map +1 -0
  41. package/dist/services/stack-plan.d.ts +3 -1
  42. package/dist/services/stack-plan.d.ts.map +1 -1
  43. package/dist/services/stack-plan.js +15 -1
  44. package/dist/services/stack-plan.js.map +1 -1
  45. package/dist/services/stack-terraform.d.ts +13 -0
  46. package/dist/services/stack-terraform.d.ts.map +1 -0
  47. package/dist/services/stack-terraform.js +274 -0
  48. package/dist/services/stack-terraform.js.map +1 -0
  49. package/dist/services/template-manifest.d.ts.map +1 -1
  50. package/dist/services/template-manifest.js +14 -1
  51. package/dist/services/template-manifest.js.map +1 -1
  52. package/dist/types.d.ts +1 -0
  53. package/dist/types.d.ts.map +1 -1
  54. package/package.json +1 -1
  55. package/templates/.agents/.env.example +34 -0
  56. package/templates/{README.md → .agents/README.md} +124 -60
  57. package/templates/.agents/config/agents/catalog.yaml +23 -23
  58. package/templates/.agents/config/clients/codex/agents/zaip-agent-analyst.toml +1 -0
  59. package/templates/.agents/config/clients/codex/agents/zaip-agent-auditor.toml +1 -0
  60. package/templates/.agents/config/clients/codex/agents/zaip-agent-backend.toml +5 -4
  61. package/templates/.agents/config/clients/codex/agents/zaip-agent-brand-strategist.toml +1 -0
  62. package/templates/.agents/config/clients/codex/agents/zaip-agent-builder.toml +1 -0
  63. package/templates/.agents/config/clients/codex/agents/zaip-agent-cloud-engineer.toml +10 -7
  64. package/templates/.agents/config/clients/codex/agents/zaip-agent-data-analyst.toml +1 -0
  65. package/templates/.agents/config/clients/codex/agents/zaip-agent-designer.toml +1 -0
  66. package/templates/.agents/config/clients/codex/agents/zaip-agent-finalizer.toml +1 -0
  67. package/templates/.agents/config/clients/codex/agents/zaip-agent-frontend.toml +5 -4
  68. package/templates/.agents/config/clients/codex/agents/zaip-agent-general-operations.toml +1 -0
  69. package/templates/.agents/config/clients/codex/agents/zaip-agent-governance-compliance.toml +1 -0
  70. package/templates/.agents/config/clients/codex/agents/zaip-agent-growth-strategist.toml +1 -0
  71. package/templates/.agents/config/clients/codex/agents/zaip-agent-integrator.toml +1 -0
  72. package/templates/.agents/config/clients/codex/agents/zaip-agent-marketing-analyst.toml +5 -4
  73. package/templates/.agents/config/clients/codex/agents/zaip-agent-orchestrator.toml +1 -0
  74. package/templates/.agents/config/clients/codex/agents/zaip-agent-refactoring-specialist.toml +1 -0
  75. package/templates/.agents/config/clients/codex/agents/zaip-agent-reviewer.toml +1 -0
  76. package/templates/.agents/config/clients/codex/agents/zaip-agent-sales-specialist.toml +1 -0
  77. package/templates/.agents/config/clients/codex/agents/zaip-agent-security-auditor.toml +1 -0
  78. package/templates/.agents/config/clients/codex/agents/zaip-agent-task-executor.toml +1 -0
  79. package/templates/.agents/config/clients/codex/agents/zaip-agent-website-developer.toml +5 -4
  80. package/templates/.agents/config/clients/codex/agents/zaip-agent-writer.toml +1 -0
  81. package/templates/.agents/config/pricing/models.json +25 -1
  82. package/templates/.agents/config/project.yaml +2 -0
  83. package/templates/.agents/dashboard/.zaip-managed.json +200 -88
  84. package/templates/.agents/dashboard/DATA-DICTIONARY.md +1 -1
  85. package/templates/.agents/dashboard/README.md +58 -19
  86. package/templates/.agents/dashboard/apps/frontend/dist/assets/{CostCalculationGuidePage-DwVZOmMV.js → CostCalculationGuidePage-snUCQYTC.js} +1 -1
  87. package/templates/.agents/dashboard/apps/frontend/dist/assets/{DashboardHelpDialog-VcLx9iOO.js → DashboardHelpDialog-oZVlf-l5.js} +72 -8
  88. package/templates/.agents/dashboard/apps/frontend/dist/assets/{PricingSettingsDialog-CyjMXkm8.js → PricingSettingsDialog-BPtVUGgs.js} +1 -1
  89. package/templates/.agents/dashboard/apps/frontend/dist/assets/ProjectSettingsPage-3Awyaovx.js +3 -0
  90. package/templates/.agents/dashboard/apps/frontend/dist/assets/{TraceabilityWorkspace-DFZwIXSC.js → TraceabilityWorkspace-BYjVOHI_.js} +1 -1
  91. package/templates/.agents/dashboard/apps/frontend/dist/assets/cost-monitor-DbaLbU2q.js +68 -0
  92. package/templates/.agents/dashboard/apps/frontend/dist/assets/{icons-vendor-DXBOtWNI.js → icons-vendor-TPUT2IUt.js} +1 -1
  93. package/templates/.agents/dashboard/apps/frontend/dist/assets/index-Cmlx8OYH.css +1 -0
  94. package/templates/.agents/dashboard/apps/frontend/dist/assets/index-ZJFXmK7m.js +2 -0
  95. package/templates/.agents/dashboard/apps/frontend/dist/assets/{react-vendor-DaNXQPyk.js → react-vendor-Ct48XHGL.js} +1 -1
  96. package/templates/.agents/dashboard/apps/frontend/dist/assets/{traceability-I4AKqVDX.js → traceability-BDe4_Ubl.js} +1 -1
  97. package/templates/.agents/dashboard/apps/frontend/dist/index.html +6 -6
  98. package/templates/.agents/dashboard/apps/frontend/src/App.tsx +73 -67
  99. package/templates/.agents/dashboard/apps/frontend/src/AppLoadingBoundary.tsx +35 -7
  100. package/templates/.agents/dashboard/apps/frontend/src/AppWorkBoard.ts +55 -0
  101. package/templates/.agents/dashboard/apps/frontend/src/ProjectStackEditor.tsx +11 -0
  102. package/templates/.agents/dashboard/apps/frontend/src/app-loading-boundary.css +98 -0
  103. package/templates/.agents/dashboard/apps/frontend/src/cost-period-refresh.test.ts +5 -5
  104. package/templates/.agents/dashboard/apps/frontend/src/dashboard-api-reconciliation.ts +33 -0
  105. package/templates/.agents/dashboard/apps/frontend/src/dashboard-api.test.ts +75 -0
  106. package/templates/.agents/dashboard/apps/frontend/src/dashboard-api.ts +7 -8
  107. package/templates/.agents/dashboard/apps/frontend/src/project-settings-model.ts +14 -2
  108. package/templates/.agents/dashboard/apps/frontend/src/project-settings.test.ts +35 -0
  109. package/templates/.agents/dashboard/apps/frontend/src/snapshot-coherence.test.ts +78 -2
  110. package/templates/.agents/dashboard/apps/frontend/src/use-app-cost-loaders.test.ts +28 -0
  111. package/templates/.agents/dashboard/apps/frontend/src/use-app-cost-loaders.ts +8 -5
  112. package/templates/.agents/dashboard/apps/frontend/src/use-app-effects.ts +180 -1
  113. package/templates/.agents/dashboard/modules/cost-monitor/src/contracts.ts +7 -4
  114. package/templates/.agents/dashboard/modules/cost-monitor/src/react/CostMonitorContent.tsx +13 -7
  115. package/templates/.agents/dashboard/modules/cost-monitor/src/react/CostMonitorPage.tsx +14 -0
  116. package/templates/.agents/dashboard/modules/cost-monitor/src/react/CostTableDisplay.ts +211 -0
  117. package/templates/.agents/dashboard/modules/cost-monitor/src/react/CostTables.test.ts +152 -157
  118. package/templates/.agents/dashboard/modules/cost-monitor/src/react/CostTables.tsx +155 -142
  119. package/templates/.agents/dashboard/modules/cost-monitor/src/react/InvocationManifestQuery.test.ts +145 -0
  120. package/templates/.agents/dashboard/modules/cost-monitor/src/react/period-cost-groups.ts +7 -5
  121. package/templates/.agents/dashboard/modules/cost-monitor/src/react/use-compact-cost-page.ts +4 -1
  122. package/templates/.agents/dashboard/modules/cost-monitor/src/react/use-cost-group-page.ts +4 -1
  123. package/templates/.agents/dashboard/modules/cost-monitor/src/react/use-cost-run-page.ts +4 -1
  124. package/templates/.agents/dashboard/modules/cost-monitor/src/server/build-cost-summary-projection.ts +4 -4
  125. package/templates/.agents/dashboard/modules/operations/src/react/export/ExcelExportAction.test.ts +79 -1
  126. package/templates/.agents/dashboard/modules/operations/src/react/export/ExcelExportAction.tsx +105 -11
  127. package/templates/.agents/dashboard/packages/local-runtime/dist/server.cjs +482 -332
  128. package/templates/.agents/dashboard/packages/local-runtime/dist/sqlite-read-model.cjs +430 -285
  129. package/templates/.agents/dashboard/packages/local-runtime/scripts/build-sqlite-runtime.mjs +14 -2
  130. package/templates/.agents/dashboard/packages/local-runtime/scripts/rebuild-sqlite-read-model.mjs +6 -26
  131. package/templates/.agents/dashboard/packages/local-runtime/scripts/validate-sqlite-read-model.mjs +1 -1
  132. package/templates/.agents/dashboard/packages/local-runtime/scripts/verify-persistent-launch.mjs +268 -0
  133. package/templates/.agents/dashboard/packages/local-runtime/server.mjs +482 -332
  134. package/templates/.agents/dashboard/packages/local-runtime/src/project-settings.test.ts +90 -1
  135. package/templates/.agents/dashboard/packages/local-runtime/src/project-settings.ts +112 -22
  136. package/templates/.agents/dashboard/packages/local-runtime/src/server-agent-assets.ts +39 -0
  137. package/templates/.agents/dashboard/packages/local-runtime/src/server-browser-lease.ts +33 -0
  138. package/templates/.agents/dashboard/packages/local-runtime/src/server-reconciliation-refresh.test.ts +148 -11
  139. package/templates/.agents/dashboard/packages/local-runtime/src/server-reconciliation-result.test.ts +68 -0
  140. package/templates/.agents/dashboard/packages/local-runtime/src/server-snapshot-refresh.test.ts +19 -0
  141. package/templates/.agents/dashboard/packages/local-runtime/src/server-snapshot-refresh.ts +39 -43
  142. package/templates/.agents/dashboard/packages/local-runtime/src/server-snapshot-revision.test.ts +42 -0
  143. package/templates/.agents/dashboard/packages/local-runtime/src/server-snapshot-revision.ts +26 -0
  144. package/templates/.agents/dashboard/packages/local-runtime/src/server-snapshot-synchronization.ts +34 -0
  145. package/templates/.agents/dashboard/packages/local-runtime/src/server-startup.ts +30 -0
  146. package/templates/.agents/dashboard/packages/local-runtime/src/server-work-reconciliation.test.ts +89 -0
  147. package/templates/.agents/dashboard/packages/local-runtime/src/server-work-reconciliation.ts +88 -0
  148. package/templates/.agents/dashboard/packages/local-runtime/src/server-workspace.ts +68 -7
  149. package/templates/.agents/dashboard/packages/local-runtime/src/server.test.ts +11 -5
  150. package/templates/.agents/dashboard/packages/local-runtime/src/server.ts +129 -99
  151. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-agent-productivity.test.ts +111 -0
  152. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-agent-productivity.ts +140 -0
  153. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-cost-agent-page.test.ts +127 -4
  154. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-cost-agent-page.ts +145 -5
  155. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-cost-aggregates.ts +156 -23
  156. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-cost-group-projection.ts +18 -9
  157. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-cost-invocation-projection.ts +52 -0
  158. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-first-fold-persistence.ts +35 -13
  159. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-force-canonical.test.ts +56 -0
  160. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-incremental-cache.test.ts +29 -0
  161. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-incremental-cache.ts +136 -0
  162. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-incremental-refresh.test.ts +251 -0
  163. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-operations-projection.ts +22 -7
  164. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-read-model-cache.test-support.ts +1 -1
  165. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-read-model-canonical-parsing.ts +42 -0
  166. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-read-model-canonical.ts +793 -0
  167. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-read-model-multi-provider-duration.test.ts +270 -0
  168. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-read-model-multi-provider-fixture.ts +3 -2
  169. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-read-model-multi-provider.test.ts +18 -4
  170. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-read-model-source-fingerprint.test.ts +87 -0
  171. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-read-model-source-fingerprint.ts +105 -16
  172. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-read-model.test.ts +131 -59
  173. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-read-model.ts +1000 -1115
  174. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-snapshot-cache.ts +13 -92
  175. package/templates/.agents/dashboard/packages/local-runtime/start-dashboard-launch-claims.test.mjs +79 -0
  176. package/templates/.agents/dashboard/packages/local-runtime/start-dashboard-launch-page.mjs +27 -0
  177. package/templates/.agents/dashboard/packages/local-runtime/start-dashboard-lifecycle.mjs +6 -6
  178. package/templates/.agents/dashboard/packages/local-runtime/start-dashboard-prerequisites.mjs +1 -1
  179. package/templates/.agents/dashboard/packages/local-runtime/start-dashboard-refresh.mjs +2 -10
  180. package/templates/.agents/dashboard/packages/local-runtime/start-dashboard-server.mjs +142 -0
  181. package/templates/.agents/dashboard/packages/local-runtime/start-dashboard.mjs +231 -239
  182. package/templates/.agents/dashboard/packages/local-runtime/start-dashboard.test.mjs +18 -29
  183. package/templates/.agents/dashboard/packages/windows-launcher/Program.cs +6 -0
  184. package/templates/.agents/dashboard/zaip-dashboard-windows.exe +0 -0
  185. package/templates/.agents/requirements/autonomous-assurance.md +1 -1
  186. package/templates/.agents/requirements/cloud-hostinger-autonomy.md +47 -0
  187. package/templates/.agents/requirements/comparative-audit.md +1 -1
  188. package/templates/.agents/requirements/context-engine.md +1 -1
  189. package/templates/.agents/requirements/cost-observability.md +1 -1
  190. package/templates/.agents/requirements/dashboard-launch.md +2 -2
  191. package/templates/.agents/requirements/data-export.md +1 -1
  192. package/templates/.agents/requirements/design-system.md +1 -1
  193. package/templates/.agents/requirements/execution-engine.md +1 -1
  194. package/templates/.agents/requirements/kanban-dashboard.md +1 -1
  195. package/templates/.agents/requirements/model-orchestration.md +1 -1
  196. package/templates/.agents/requirements/public-distribution.md +1 -1
  197. package/templates/.agents/requirements/software-architecture.md +1 -1
  198. package/templates/.agents/requirements/strategic-improvements.md +1 -1
  199. package/templates/.agents/requirements/work-lifecycle.md +1 -1
  200. package/templates/.agents/requirements/zaip-strategic-95.md +1 -1
  201. package/templates/.agents/rules/autonomy-decision.md +1 -1
  202. package/templates/.agents/rules/cloud-hostinger-autonomy.md +21 -0
  203. package/templates/.agents/rules/core.md +5 -4
  204. package/templates/.agents/rules/local-sqlite-storage.md +18 -0
  205. package/templates/.agents/rules/meta-mcp.md +21 -0
  206. package/templates/.agents/rules/workflow.md +1 -0
  207. package/templates/.agents/runtime/bin/zaip.mjs +305 -137
  208. package/templates/.agents/runtime/checksums.sha256 +2 -2
  209. package/templates/.agents/runtime/provenance.json +2 -2
  210. package/templates/.agents/skills/zaip-skill-api-to-mcp/SKILL.md +4 -0
  211. package/templates/.agents/skills/zaip-skill-brand-governance/SKILL.md +25 -9
  212. package/templates/.agents/skills/zaip-skill-brand-governance/references/brandbook-framework.md +2 -2
  213. package/templates/.agents/skills/zaip-skill-brand-governance/references/brandbook-standards.md +2 -2
  214. package/templates/.agents/skills/zaip-skill-change/references/observable-evidence.md +15 -0
  215. package/templates/.agents/skills/zaip-skill-dashboard-sqlite/SKILL.md +13 -5
  216. package/templates/.agents/skills/zaip-skill-data-analyst/SKILL.md +10 -5
  217. package/templates/.agents/skills/zaip-skill-execution/SKILL.md +5 -4
  218. package/templates/.agents/skills/zaip-skill-frontend-launcher/SKILL.md +4 -3
  219. package/templates/.agents/skills/zaip-skill-frontend-launcher/references/launcher-contract.md +5 -3
  220. package/templates/.agents/skills/zaip-skill-github-deploy/SKILL.md +5 -5
  221. package/templates/.agents/skills/zaip-skill-github-deploy/scripts/preflight.mjs +78 -13
  222. package/templates/.agents/skills/zaip-skill-github-deploy/scripts/preflight.test.mjs +59 -0
  223. package/templates/.agents/skills/zaip-skill-github-deploy/scripts/yaml-parser.mjs +157 -0
  224. package/templates/.agents/skills/zaip-skill-growth-strategy-revops/SKILL.md +11 -6
  225. package/templates/.agents/skills/zaip-skill-hostinger-cloudflare-terraform/SKILL.md +17 -0
  226. package/templates/.agents/skills/zaip-skill-hostinger-cloudflare-terraform/agents/openai.yaml +7 -0
  227. package/templates/.agents/skills/zaip-skill-hostinger-cloudflare-terraform/references/autonomous-cloud-inputs.example.json +41 -0
  228. package/templates/.agents/skills/zaip-skill-hostinger-cloudflare-terraform/references/autonomous-runner.md +84 -0
  229. package/templates/.agents/skills/zaip-skill-hostinger-cloudflare-terraform/references/operational-contract.md +19 -0
  230. package/templates/.agents/skills/zaip-skill-local-credentials/SKILL.md +43 -0
  231. package/templates/.agents/skills/zaip-skill-local-credentials/agents/openai.yaml +6 -0
  232. package/templates/.agents/skills/zaip-skill-local-credentials/references/dotenv-resolution.md +47 -0
  233. package/templates/.agents/skills/zaip-skill-marketing/SKILL.md +8 -3
  234. package/templates/.agents/skills/zaip-skill-meta-ads/SKILL.md +16 -0
  235. package/templates/.agents/skills/zaip-skill-meta-ads/agents/openai.yaml +4 -0
  236. package/templates/.agents/skills/zaip-skill-meta-ads/references/setup.md +3 -0
  237. package/templates/.agents/skills/zaip-skill-meta-ads-setup/SKILL.md +24 -0
  238. package/templates/.agents/skills/zaip-skill-meta-ads-setup/agents/openai.yaml +4 -0
  239. package/templates/.agents/skills/zaip-skill-meta-ads-setup/references/diagnostics.md +13 -0
  240. package/templates/.agents/skills/zaip-skill-meta-ads-setup/references/official-sources.md +10 -0
  241. package/templates/.agents/skills/zaip-skill-meta-devtools-api-health/SKILL.md +12 -0
  242. package/templates/.agents/skills/zaip-skill-meta-devtools-api-health/agents/openai.yaml +4 -0
  243. package/templates/.agents/skills/zaip-skill-meta-devtools-api-health/references/LICENSE +21 -0
  244. package/templates/.agents/skills/zaip-skill-meta-devtools-api-health/references/upstream.md +77 -0
  245. package/templates/.agents/skills/zaip-skill-meta-devtools-api-integration/SKILL.md +12 -0
  246. package/templates/.agents/skills/zaip-skill-meta-devtools-api-integration/agents/openai.yaml +4 -0
  247. package/templates/.agents/skills/zaip-skill-meta-devtools-api-integration/references/LICENSE +21 -0
  248. package/templates/.agents/skills/zaip-skill-meta-devtools-api-integration/references/upstream.md +126 -0
  249. package/templates/.agents/skills/zaip-skill-meta-devtools-app-health-check/SKILL.md +12 -0
  250. package/templates/.agents/skills/zaip-skill-meta-devtools-app-health-check/agents/openai.yaml +4 -0
  251. package/templates/.agents/skills/zaip-skill-meta-devtools-app-health-check/references/LICENSE +21 -0
  252. package/templates/.agents/skills/zaip-skill-meta-devtools-app-health-check/references/upstream.md +67 -0
  253. package/templates/.agents/skills/zaip-skill-meta-devtools-app-review-prep/SKILL.md +12 -0
  254. package/templates/.agents/skills/zaip-skill-meta-devtools-app-review-prep/agents/openai.yaml +4 -0
  255. package/templates/.agents/skills/zaip-skill-meta-devtools-app-review-prep/references/LICENSE +21 -0
  256. package/templates/.agents/skills/zaip-skill-meta-devtools-app-review-prep/references/upstream.md +69 -0
  257. package/templates/.agents/skills/zaip-skill-meta-devtools-compliance-check/SKILL.md +12 -0
  258. package/templates/.agents/skills/zaip-skill-meta-devtools-compliance-check/agents/openai.yaml +4 -0
  259. package/templates/.agents/skills/zaip-skill-meta-devtools-compliance-check/references/LICENSE +21 -0
  260. package/templates/.agents/skills/zaip-skill-meta-devtools-compliance-check/references/upstream.md +79 -0
  261. package/templates/.agents/skills/zaip-skill-meta-devtools-debug-access-token/SKILL.md +14 -0
  262. package/templates/.agents/skills/zaip-skill-meta-devtools-debug-access-token/agents/openai.yaml +4 -0
  263. package/templates/.agents/skills/zaip-skill-meta-devtools-debug-access-token/references/LICENSE +21 -0
  264. package/templates/.agents/skills/zaip-skill-meta-devtools-debug-access-token/references/upstream.md +144 -0
  265. package/templates/.agents/skills/zaip-skill-meta-devtools-debug-webhooks/SKILL.md +12 -0
  266. package/templates/.agents/skills/zaip-skill-meta-devtools-debug-webhooks/agents/openai.yaml +4 -0
  267. package/templates/.agents/skills/zaip-skill-meta-devtools-debug-webhooks/references/LICENSE +21 -0
  268. package/templates/.agents/skills/zaip-skill-meta-devtools-debug-webhooks/references/upstream.md +83 -0
  269. package/templates/.agents/skills/zaip-skill-meta-devtools-search-docs/SKILL.md +10 -0
  270. package/templates/.agents/skills/zaip-skill-meta-devtools-search-docs/agents/openai.yaml +4 -0
  271. package/templates/.agents/skills/zaip-skill-meta-devtools-search-docs/references/LICENSE +21 -0
  272. package/templates/.agents/skills/zaip-skill-meta-devtools-search-docs/references/setup.md +11 -0
  273. package/templates/.agents/skills/zaip-skill-meta-devtools-search-docs/references/upstream.md +55 -0
  274. package/templates/.agents/skills/zaip-skill-meta-devtools-webhook-setup/SKILL.md +12 -0
  275. package/templates/.agents/skills/zaip-skill-meta-devtools-webhook-setup/agents/openai.yaml +4 -0
  276. package/templates/.agents/skills/zaip-skill-meta-devtools-webhook-setup/references/LICENSE +21 -0
  277. package/templates/.agents/skills/zaip-skill-meta-devtools-webhook-setup/references/upstream.md +56 -0
  278. package/templates/.agents/skills/zaip-skill-sales-methodology/SKILL.md +12 -7
  279. package/templates/.agents/skills/zaip-skill-sqlite-read-model/SKILL.md +11 -4
  280. package/templates/.agents/skills/zaip-skill-tool-setup/SKILL.md +6 -4
  281. package/templates/.agents/skills/zaip-skill-tool-setup/references/codex-config.md +4 -2
  282. package/templates/.agents/skills/zaip-skill-tool-setup/references/secrets.md +1 -0
  283. package/templates/.agents/tools/catalog.yaml +6 -0
  284. package/templates/.agents/tools/cloud-runner-dns.mjs +28 -0
  285. package/templates/.agents/tools/cloud-runner-terraform.mjs +212 -0
  286. package/templates/.agents/tools/cloud-runner.integration.test.mjs +239 -0
  287. package/templates/.agents/tools/cloud-runner.mjs +786 -0
  288. package/templates/.agents/tools/cloud-runner.test.mjs +534 -0
  289. package/templates/.agents/tools/cloud-terraform-plan-policy.mjs +262 -0
  290. package/templates/.agents/tools/mcp/development/meta-devtools/README.md +14 -0
  291. package/templates/.agents/tools/mcp/development/meta-devtools/manifest.yaml +28 -0
  292. package/templates/.agents/tools/mcp/infra/cloudflare/manifest.yaml +2 -3
  293. package/templates/.agents/tools/mcp/infra/hostinger/manifest.yaml +1 -3
  294. package/templates/.agents/tools/mcp/marketing/meta-ads/README.md +15 -13
  295. package/templates/.agents/tools/mcp/marketing/meta-ads/manifest.yaml +15 -10
  296. package/templates/.agents/tools/profiles/development.yaml +3 -0
  297. package/templates/infra/terraform/.terraform.lock.hcl +42 -0
  298. package/templates/infra/terraform/README.md +31 -0
  299. package/templates/infra/terraform/backend.tf.json +13 -0
  300. package/templates/infra/terraform/deployment-inputs.example.yaml +22 -0
  301. package/templates/infra/terraform/fixtures/approved-targets.json +32 -0
  302. package/templates/infra/terraform/fixtures/plan-approved.json +67 -0
  303. package/templates/infra/terraform/main.tf +77 -0
  304. package/templates/infra/terraform/outputs.tf +20 -0
  305. package/templates/infra/terraform/terraform.tfvars.example +26 -0
  306. package/templates/infra/terraform/variables.tf +91 -0
  307. package/templates/infra/terraform/versions.tf +18 -0
  308. package/templates/scripts/validate-cloud-hostinger-contract.mjs +89 -0
  309. package/templates/scripts/validate-cloud-terraform-plan.mjs +1 -0
  310. package/templates/template-manifest.json +599 -167
  311. package/templates/.agents/dashboard/apps/frontend/dist/assets/ProjectSettingsPage-BwnJpl02.js +0 -3
  312. package/templates/.agents/dashboard/apps/frontend/dist/assets/cost-monitor-C09KmlSw.js +0 -68
  313. package/templates/.agents/dashboard/apps/frontend/dist/assets/index-Dyo_wpL4.js +0 -2
  314. package/templates/.agents/dashboard/apps/frontend/dist/assets/index-hPPfVGQv.css +0 -1
  315. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-read-model-stale-seed.ts +0 -50
@@ -1,6 +1,7 @@
1
1
  import { createHash, randomUUID } from "node:crypto";
2
- import { copyFile, mkdir, readdir, readFile, rename, rm, stat } from "node:fs/promises";
3
- import { dirname, join, relative, resolve } from "node:path";
2
+ import { existsSync } from "node:fs";
3
+ import { mkdir, readFile } from "node:fs/promises";
4
+ import { dirname, resolve } from "node:path";
4
5
  import { DatabaseSync } from "node:sqlite";
5
6
  import { parseYaml, WorkspaceFileSystem } from "@zaip-ai/core";
6
7
  import {
@@ -21,17 +22,24 @@ import {
21
22
  type RunV2Fold,
22
23
  type UsageRecordedV2,
23
24
  } from "../../../../../packages/decisions/src/index.js";
25
+ import { attachAgentProductivity } from "./sqlite-agent-productivity.js";
24
26
  import { artifactStructuralRowsSql, developerArtifactCountsCte } from "./sqlite-artifact-counts.js";
25
27
  import {
26
28
  costDimensionFactDateFilter,
29
+ creationProductivityByModel,
27
30
  isDateOnlyCostPage,
28
31
  readDateOnlyCostDimensionPage,
32
+ sumKnownProductivity,
29
33
  } from "./sqlite-cost-agent-page.js";
30
34
  import { buildCostDimensionAggregates } from "./sqlite-cost-aggregates.js";
31
35
  import { rebuildCostInvocationDimensionFactsSql } from "./sqlite-cost-dimension-facts.js";
32
36
  import { type CostGroupKind, readCostGroupPage } from "./sqlite-cost-group-page.js";
33
37
  import { materializeCostGroupDailyProjection } from "./sqlite-cost-group-projection.js";
34
38
  import { filterCostInvocationProjection } from "./sqlite-cost-invocation-page.js";
39
+ import {
40
+ costInvocationProjectionSelect,
41
+ readCostInvocationProjection,
42
+ } from "./sqlite-cost-invocation-projection.js";
35
43
  import {
36
44
  readFirstFoldAllTime,
37
45
  readFirstFoldBoardAllTime,
@@ -41,9 +49,14 @@ import {
41
49
  } from "./sqlite-first-fold-cache.js";
42
50
  import {
43
51
  hasRequiredFirstFoldPayloads,
44
- persistFirstFoldPayloads,
52
+ persistFirstFoldPayloadsInDatabase,
45
53
  } from "./sqlite-first-fold-persistence.js";
46
54
  import { measureFirstFold } from "./sqlite-first-fold-timing.js";
55
+ import {
56
+ affectedTemporalIntervals,
57
+ SqliteParsedSourceCache,
58
+ synchronizePayloadRows,
59
+ } from "./sqlite-incremental-cache.js";
47
60
  import { materializeOperationsWorkProjection } from "./sqlite-operations-projection.js";
48
61
  import {
49
62
  attachOperationsWorkListDevelopers,
@@ -52,12 +65,7 @@ import {
52
65
  operationsWorkListSqlRows,
53
66
  operationsWorkRowsInRange,
54
67
  } from "./sqlite-operations-work-list.js";
55
- import {
56
- installLegacyViewAliases,
57
- materializeSnapshot,
58
- pruneSnapshotCache,
59
- readSnapshotFileCount,
60
- } from "./sqlite-snapshot-cache.js";
68
+ import { installLegacyViewAliases, readSnapshotFileCount } from "./sqlite-snapshot-cache.js";
61
69
 
62
70
  export {
63
71
  type SqliteReadModelSourceCandidate,
@@ -65,13 +73,13 @@ export {
65
73
  } from "./sqlite-read-model-source-fingerprint.js";
66
74
 
67
75
  import { reportSqliteReadModelLaunchProgress } from "./sqlite-read-model-launch-progress.js";
76
+ import type { SqliteReadModelSourceCheckpoint } from "./sqlite-read-model-source-fingerprint.js";
68
77
  import { sourceFingerprint } from "./sqlite-read-model-source-fingerprint.js";
69
- import { materializePreferredStaleSeed } from "./sqlite-read-model-stale-seed.js";
70
78
 
71
- const dbRelativePath = ".local/dashboard-sqlite/read-model.sqlite";
72
- // Schema 71 requires the canonical work-item identity index. Older seeds must
73
- // rebuild rather than silently retaining the unindexed correlated lookups.
74
- const readModelSchemaVersion = "71";
79
+ const dbRelativePath = ".local/sqlite/zaip-aios-dashboard.sqlite";
80
+ // Schema 73 persists exact source checkpoints and parsed records for delta refresh.
81
+ // Older snapshots initialize these inputs once in the same database transaction.
82
+ const readModelSchemaVersion = "73";
75
83
  const requiredPersistedTables = [
76
84
  "zaip_aios_activities",
77
85
  "zaip_dim_llm_model",
@@ -114,818 +122,107 @@ const requiredViews = [
114
122
  ] as const;
115
123
  const MAX_RECONCILED_RUN_INTERVAL_MS = 2 * 3_600_000;
116
124
 
117
- function saoPauloCurrentMonth(): { from: string; to: string } {
118
- const parts = new Intl.DateTimeFormat("en-CA", {
119
- timeZone: "America/Sao_Paulo",
120
- year: "numeric",
121
- month: "2-digit",
122
- day: "2-digit",
123
- }).formatToParts(new Date());
124
- const value = Object.fromEntries(parts.map((part) => [part.type, part.value]));
125
- const day = `${value.year}-${value.month}-${value.day}`;
126
- return { from: `${value.year}-${value.month}-01`, to: day };
127
- }
128
-
129
- /** Only canonical inputs consumed by the fact or its direct dimensions. */
130
-
131
- type AgentProfile = {
132
- catalogId: string;
133
- persona: string | null;
134
- job: string | null;
135
- role: string | null;
136
- };
137
- type CanonicalEntity = {
138
- kind: string;
139
- id: string;
140
- title: string | null;
141
- status: string | null;
142
- stage: string | null;
143
- risk: string | null;
144
- specId: string | null;
145
- changeId: string | null;
146
- occurredAt: string | null;
147
- creationRunId: string | null;
148
- completedRunId: string | null;
149
- };
150
- type CanonicalArtifact = {
151
- kind: "evidence" | "validation";
152
- id: string;
153
- occurredAt: string;
154
- occurredDaySp: string;
155
- taskId: string | null;
156
- changeId: string | null;
157
- runId: string | null;
158
- };
159
- type CanonicalExecution = {
160
- id: string;
161
- status: string | null;
162
- kind: string | null;
163
- originType: string | null;
164
- originId: string | null;
165
- createdAt: string;
166
- finishedAt: string | null;
167
- sourcePath: string;
168
- };
169
- type CanonicalChange = {
170
- id: string;
171
- title: string;
172
- status: string;
173
- risk: string;
174
- sourceType: string;
175
- sourceId: string;
176
- sourceSpecId: string | null;
177
- sourcePrdId: string | null;
178
- requirementIds: string[];
179
- acceptanceIds: string[];
180
- taskIds: string[];
181
- result: string | null;
182
- createdAt: string;
183
- sourcePath: string;
184
- };
185
- type CanonicalDecision = {
186
- id: string;
187
- title: string;
188
- origin: "human" | "agent";
189
- scopeType: "change" | "execution";
190
- scopeId: string;
191
- createdAt: string;
192
- sourcePath: string;
193
- };
194
- function frontMatterRecord(content: string): Record<string, unknown> | null {
195
- const source = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/.exec(content)?.[1];
196
- if (source === undefined) return null;
197
- try {
198
- const parsed = parseYaml(source);
199
- return parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)
200
- ? (parsed as Record<string, unknown>)
201
- : null;
202
- } catch {
203
- return null;
204
- }
205
- }
206
-
207
- function recordValue(value: unknown): Record<string, unknown> | null {
208
- return value !== null && typeof value === "object" && !Array.isArray(value)
209
- ? (value as Record<string, unknown>)
210
- : null;
211
- }
212
-
213
- function stringValue(value: unknown): string | null {
214
- return typeof value === "string" && value.length > 0 ? value : null;
215
- }
125
+ import {
126
+ type ActivityMetric,
127
+ type AgentProfile,
128
+ activityKind,
129
+ aggregateCost,
130
+ allocateAttributedTokenActiveMs,
131
+ type CanonicalArtifact,
132
+ type CanonicalChange,
133
+ type CanonicalDecision,
134
+ type CanonicalEntity,
135
+ type CanonicalExecution,
136
+ canonicalProvider,
137
+ consolidatedValue,
138
+ costMonitorFilterDay,
139
+ factualDuration,
140
+ frontMatterRecord,
141
+ knownSum,
142
+ latestFxRate,
143
+ pricingModelForUsage,
144
+ type ResolvedSourceInterval,
145
+ readAgentProfiles,
146
+ readCanonicalArtifact,
147
+ readCanonicalEntities,
148
+ readCanonicalLifecycleRecords,
149
+ recordValue,
150
+ resolveDurations,
151
+ resolvedSourceInterval,
152
+ saoPauloCurrentMonth,
153
+ saoPauloDay,
154
+ stringValue,
155
+ stringValues,
156
+ sumKnownDeveloperMetric,
157
+ type TemporalInterval,
158
+ usagePricingAudit,
159
+ } from "./sqlite-read-model-canonical.js";
216
160
 
217
- function stringValues(value: unknown): string[] {
218
- return Array.isArray(value)
219
- ? value.filter((item): item is string => typeof item === "string")
220
- : [];
161
+ export { allocateAttributedTokenActiveMs } from "./sqlite-read-model-canonical.js";
162
+ export function sqliteReadModelPath(workspaceRoot: string): string {
163
+ return resolve(process.env.ZAIP_SQLITE_DB_PATH ?? resolve(workspaceRoot, dbRelativePath));
221
164
  }
222
165
 
223
- function readCanonicalLifecycleRecords(rows: Array<{ path: string; content: string }>): {
224
- executions: CanonicalExecution[];
225
- changes: CanonicalChange[];
226
- decisions: CanonicalDecision[];
227
- } {
228
- const executions: CanonicalExecution[] = [];
229
- const changes: CanonicalChange[] = [];
230
- const decisions: CanonicalDecision[] = [];
231
- for (const row of rows) {
232
- const metadata = frontMatterRecord(row.content);
233
- if (metadata === null) continue;
234
- const id = stringValue(metadata.id);
235
- const createdAt = stringValue(metadata.created_at);
236
- if (id === null || createdAt === null || Number.isNaN(Date.parse(createdAt))) continue;
237
- if (/^\.agents\/records\/executions\/.*\/execution\.md$/.test(row.path)) {
238
- const origin = recordValue(metadata.origin);
239
- const originType = stringValue(origin?.type);
240
- const originId =
241
- originType === "interaction"
242
- ? stringValue(origin?.conversation_id)
243
- : originType === "task"
244
- ? stringValue(origin?.task_id)
245
- : originType === "spec"
246
- ? stringValue(origin?.spec_id)
247
- : originType === "prd"
248
- ? stringValue(origin?.prd_id)
249
- : null;
250
- executions.push({
251
- id,
252
- status: stringValue(metadata.status),
253
- kind: stringValue(metadata.kind),
254
- originType,
255
- originId,
256
- createdAt,
257
- finishedAt: stringValue(metadata.finished_at),
258
- sourcePath: row.path,
259
- });
260
- continue;
261
- }
262
- if (/^\.agents\/records\/changes\/.*\/change\.md$/.test(row.path)) {
263
- const source = recordValue(metadata.source);
264
- const sourceType = stringValue(source?.type);
265
- const sourceId =
266
- sourceType === "task"
267
- ? stringValue(source?.task_id)
268
- : sourceType === "spec"
269
- ? stringValue(source?.spec_id)
270
- : sourceType === "prd"
271
- ? stringValue(source?.prd_id)
272
- : null;
273
- if (sourceType === null || sourceId === null) continue;
274
- changes.push({
275
- id,
276
- title: stringValue(metadata.title) ?? id,
277
- status: stringValue(metadata.status) ?? "unclassified",
278
- risk: stringValue(metadata.risk) ?? "unclassified",
279
- sourceType,
280
- sourceId,
281
- // A Change sourced from a PRD can still name its selected SPEC. Keep
282
- // both canonical lineage links: choosing one by source.type loses the
283
- // only evidence that a planned Task belongs to that SPEC.
284
- sourceSpecId: stringValue(source?.spec_id),
285
- sourcePrdId: stringValue(source?.prd_id),
286
- requirementIds: stringValues(metadata.requirement_ids),
287
- acceptanceIds: stringValues(metadata.acceptance_ids),
288
- taskIds: stringValues(metadata.task_ids),
289
- result: stringValue(metadata.result),
290
- createdAt,
291
- sourcePath: row.path,
292
- });
293
- continue;
294
- }
295
- if (/^\.agents\/records\/(changes|executions)\/.*\/decisions\/.*\.md$/.test(row.path)) {
296
- const origin = metadata.origin;
297
- const scopeType = metadata.scope_type;
298
- const scopeId = stringValue(metadata.scope_id);
299
- if (
300
- (origin !== "human" && origin !== "agent") ||
301
- (scopeType !== "change" && scopeType !== "execution") ||
302
- scopeId === null
166
+ function persistedSourceCheckpoint(path: string): Map<string, SqliteReadModelSourceCheckpoint> {
167
+ // The first materialization has no persistent database to checkpoint.
168
+ // Opening SQLite read-only before this guard throws ERR_SQLITE_ERROR.
169
+ if (!existsSync(path)) return new Map();
170
+ let database: DatabaseSync | undefined;
171
+ try {
172
+ database = new DatabaseSync(path, { readOnly: true });
173
+ const rows = database
174
+ .prepare(
175
+ "SELECT source_path, source_hash, mtime_ms, ctime_ms, size_bytes, file_identity FROM zaip_vw_read_model_source_state",
303
176
  )
304
- continue;
305
- decisions.push({
306
- id,
307
- title: stringValue(metadata.title) ?? id,
308
- origin,
309
- scopeType,
310
- scopeId,
311
- createdAt,
312
- sourcePath: row.path,
313
- });
314
- }
315
- }
316
- return { executions, changes, decisions };
317
- }
318
-
319
- /** Reads only scalar front matter from canonical records to enrich linked activities. */
320
- function readCanonicalEntities(
321
- rows: Array<{ path: string; content: string }>,
322
- ): Map<string, CanonicalEntity> {
323
- const entities = new Map<string, CanonicalEntity>();
324
- for (const row of rows) {
325
- const match = /^\.agents\/records\/(tasks|specs|prds|changes|executions|conversations)\//.exec(
326
- row.path,
327
- );
328
- if (match === null) continue;
329
- const frontMatter = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/.exec(row.content)?.[1];
330
- if (frontMatter === undefined) continue;
331
- const scalar = (key: string): string | null => {
332
- const value = new RegExp(`^${key}:\\s*(.+?)\\s*$`, "m").exec(frontMatter)?.[1]?.trim();
333
- if (value === undefined || value === "null") return null;
334
- return value.replace(/^['"]|['"]$/g, "");
335
- };
336
- const id = scalar("id");
337
- if (id === null) continue;
338
- const kind = match[1]?.slice(0, -1);
339
- if (kind === undefined) continue;
340
- entities.set(`${kind}:${id}`, {
341
- kind,
342
- id,
343
- title: scalar("title"),
344
- status: scalar("status"),
345
- stage: scalar("stage"),
346
- risk: scalar("risk"),
347
- specId: scalar("spec_id"),
348
- changeId: scalar("change_id"),
349
- occurredAt: scalar("created_at") ?? scalar("started_at") ?? scalar("recorded_at"),
350
- creationRunId: scalar("creation_run_id"),
351
- completedRunId: scalar("completed_run_id"),
352
- });
353
- }
354
- return entities;
355
- }
356
-
357
- /** Catalog data is snapshotted into the fact so historical labels never change after a catalog edit. */
358
- function readAgentProfiles(
359
- catalog: string | undefined,
360
- clients: string | undefined,
361
- ): Map<string, AgentProfile> {
362
- const profiles = new Map<string, AgentProfile>();
363
- for (const block of (catalog ?? "").split(/^ {2}- id: /m).slice(1)) {
364
- const catalogId = block.slice(0, block.indexOf("\n")).trim();
365
- const agent = /\bcodex_name:\s*([^\s#]+)/.exec(block)?.[1] ?? null;
366
- if (agent === null) continue;
367
- const persona = /persona:\s*\{\s*nome:\s*([^,}\s]+),\s*cargo:\s*([^,}\s]+)\s*\}/.exec(block);
368
- const role = /\brole:\s*([^\s#]+)/.exec(block)?.[1] ?? null;
369
- const profile = {
370
- catalogId,
371
- persona: persona?.[1] ?? null,
372
- job: persona?.[2] ?? null,
373
- role,
374
- };
375
- // Runs before canonicalization used catalog IDs directly, while older
376
- // host records used `zaip-` forms of deprecated IDs. All of these are
377
- // explicit identities from the catalog; no UI fallback is involved.
378
- const identities = new Set([agent, catalogId]);
379
- for (const alias of block.matchAll(
380
- /^ {8}- kind:\s*(?:id|codex_name|codex_task_name)\s*\r?\n {10}value:\s*([^\s#]+)/gm,
381
- )) {
382
- const value = alias[1];
383
- if (value !== undefined) identities.add(value);
384
- }
385
- for (const identity of [...identities]) {
386
- if (identity.startsWith("agent-")) identities.add(`zaip-${identity}`);
387
- }
388
- for (const identity of identities) profiles.set(identity, profile);
389
- }
390
- // `codex` is the host client identity, not a delegable Agent Catalog
391
- // member. Its canonical client profile makes the host classification
392
- // explicit, while keeping it separate from ZAIP subagent routing.
393
- if (/^ {2}- id:\s*codex\s*$/m.test(clients ?? "")) {
394
- profiles.set("codex", {
395
- catalogId: "client:codex",
396
- persona: "codex",
397
- job: "client_host",
398
- role: "host",
399
- });
400
- }
401
- return profiles;
402
- }
403
-
404
- function activityKind(value: {
405
- execution_id: string | null;
406
- task_id: string | null;
407
- spec_id: string | null;
408
- prd_id: string | null;
409
- phase: string | null;
410
- purpose: string | null;
411
- }): "conversation" | "planning" | "development" {
412
- if (value.prd_id !== null && value.execution_id === null) return "planning";
413
- if (value.spec_id !== null && value.execution_id === null) return "planning";
414
- if (value.phase === "planning" || value.purpose === "planning") return "planning";
415
- if (
416
- value.execution_id !== null ||
417
- value.task_id !== null ||
418
- value.phase === "execution" ||
419
- value.purpose === "implementation"
420
- )
421
- return "development";
422
- return "conversation";
423
- }
424
-
425
- type ActivityMetric = number | null;
426
-
427
- function knownSum(
428
- records: readonly UsageRecordedV2[],
429
- key: keyof PersistedTokenUsage,
430
- ): ActivityMetric {
431
- const values = records
432
- .map((record) => record.tokens[key])
433
- .filter((value): value is number => typeof value === "number");
434
- return values.length === 0 ? null : values.reduce((sum, value) => sum + value, 0);
435
- }
436
-
437
- function consolidatedValue(values: readonly (string | null | undefined)[]): string | null {
438
- const known = [
439
- ...new Set(
440
- values.filter(
441
- (value): value is string =>
442
- typeof value === "string" && value.length > 0 && value !== "unavailable",
443
- ),
444
- ),
445
- ];
446
- return known.length === 0 ? null : known.length === 1 ? known[0]! : "mixed";
447
- }
448
-
449
- function canonicalProvider(
450
- value: string | null | undefined,
451
- model: string | null,
452
- client: string,
453
- ): "openai" | "anthropic" | null {
454
- if (value === "openai" || value === "anthropic") return value;
455
- // The canonical sentinel denotes an absent source attribute. It must not be
456
- // re-inferred from the dashboard client as a known provider.
457
- if (value === "unavailable" || model === "unavailable") return null;
458
- return model === null ? null : inferProvider(model, client, "");
459
- }
460
-
461
- function factualDuration(record: UsageRecordedV2): boolean {
462
- return (
463
- typeof record.duration_ms === "number" &&
464
- (record.duration_provenance === "observed" ||
465
- record.duration_provenance === "reconciled" ||
466
- record.duration_provenance === "legacy_inferred")
467
- );
468
- }
469
-
470
- type ResolvedSourceInterval = { start: string; end: string; duration: number };
471
-
472
- /**
473
- * Reconciled usage is the closest canonical observation of when model work
474
- * happened. It wins over Run finalization elapsed time, which may include
475
- * orchestration/capture lag or be recorded much later.
476
- */
477
- function resolvedSourceInterval(
478
- records: readonly UsageRecordedV2[],
479
- ): ResolvedSourceInterval | null {
480
- const intervals = records.flatMap((record) => {
481
- const start = record.source_started_at;
482
- const end = record.source_finished_at;
483
- const startMs = typeof start === "string" ? Date.parse(start) : Number.NaN;
484
- const endMs = typeof end === "string" ? Date.parse(end) : Number.NaN;
485
- return typeof start === "string" &&
486
- typeof end === "string" &&
487
- Number.isFinite(startMs) &&
488
- Number.isFinite(endMs) &&
489
- endMs > startMs &&
490
- endMs - startMs <= MAX_RECONCILED_RUN_INTERVAL_MS
491
- ? [{ start, end, startMs, endMs }]
492
- : [];
493
- });
494
- if (intervals.length === 0) return null;
495
- const start = intervals.reduce((earliest, interval) =>
496
- interval.startMs < earliest.startMs ? interval : earliest,
497
- );
498
- const end = intervals.reduce((latest, interval) =>
499
- interval.endMs > latest.endMs ? interval : latest,
500
- );
501
- return end.endMs - start.startMs <= MAX_RECONCILED_RUN_INTERVAL_MS
502
- ? { start: start.start, end: end.end, duration: end.endMs - start.startMs }
503
- : null;
504
- }
505
-
506
- function resolveDurations(
507
- records: readonly UsageRecordedV2[],
508
- fold: RunV2Fold,
509
- ): {
510
- source: number | null;
511
- effective: number | null;
512
- provenance: string | null;
513
- sourceStartedAt: string | null;
514
- sourceFinishedAt: string | null;
515
- } {
516
- const sourceInterval = resolvedSourceInterval(records);
517
- if (sourceInterval !== null) {
518
- return {
519
- source: sourceInterval.duration,
520
- effective: sourceInterval.duration,
521
- provenance: "reconciled",
522
- sourceStartedAt: sourceInterval.start,
523
- sourceFinishedAt: sourceInterval.end,
524
- };
525
- }
526
- if (
527
- typeof fold.finished?.duration_ms === "number" &&
528
- fold.finished.duration_provenance !== "estimated" &&
529
- fold.finished.duration_provenance !== "unavailable"
530
- ) {
531
- return {
532
- source: fold.finished.duration_ms,
533
- effective: fold.finished.duration_ms,
534
- provenance: fold.finished.duration_provenance ?? "observed",
535
- sourceStartedAt: null,
536
- sourceFinishedAt: null,
537
- };
538
- }
539
- const factual = records.filter(factualDuration);
540
- if (factual.length > 0) {
541
- const source = factual.reduce((sum, record) => sum + (record.duration_ms ?? 0), 0);
542
- const provenance =
543
- consolidatedValue(factual.map((record) => record.duration_provenance)) ?? "observed";
544
- return { source, effective: source, provenance, sourceStartedAt: null, sourceFinishedAt: null };
545
- }
546
- // A canonical Run pair can supply elapsed time when terminal records predate
547
- // the explicit duration field. Historical finalizers can however close a
548
- // Run days later, so only a bounded interval is trusted as reconciliation.
549
- const startedAt = Date.parse(fold.run.occurred_at);
550
- const finishedAt =
551
- typeof fold.finished?.occurred_at === "string"
552
- ? Date.parse(fold.finished.occurred_at)
553
- : Number.NaN;
554
- if (
555
- Number.isFinite(startedAt) &&
556
- Number.isFinite(finishedAt) &&
557
- finishedAt > startedAt &&
558
- finishedAt - startedAt <= MAX_RECONCILED_RUN_INTERVAL_MS
559
- ) {
560
- return {
561
- source: null,
562
- effective: finishedAt - startedAt,
563
- provenance: "reconciled",
564
- sourceStartedAt: null,
565
- sourceFinishedAt: null,
566
- };
567
- }
568
- // Tokens and lifecycle timestamps are not a time source. Absence stays
569
- // absent: consumers must display unavailable rather than manufacture time.
570
- return {
571
- source: null,
572
- effective: null,
573
- provenance: null,
574
- sourceStartedAt: null,
575
- sourceFinishedAt: null,
576
- };
577
- }
578
-
579
- function latestFxRate(rows: Array<{ path: string; content: string }>): FxRate | null {
580
- return (
581
- rows
582
- .flatMap((row) => {
583
- try {
584
- return [fxRateSchema.parse(JSON.parse(row.content))];
585
- } catch {
177
+ .all() as Array<Record<string, unknown>>;
178
+ return new Map(
179
+ rows.flatMap((row) => {
180
+ const source = typeof row.source_path === "string" ? row.source_path : null;
181
+ const contentHash = typeof row.source_hash === "string" ? row.source_hash : null;
182
+ const fileIdentity = typeof row.file_identity === "string" ? row.file_identity : null;
183
+ const mtimeMs =
184
+ typeof row.mtime_ms === "number"
185
+ ? row.mtime_ms
186
+ : typeof row.mtime_ms === "string"
187
+ ? Number(row.mtime_ms)
188
+ : Number.NaN;
189
+ const ctimeMs =
190
+ typeof row.ctime_ms === "number"
191
+ ? row.ctime_ms
192
+ : typeof row.ctime_ms === "string"
193
+ ? Number(row.ctime_ms)
194
+ : Number.NaN;
195
+ if (
196
+ source === null ||
197
+ contentHash === null ||
198
+ fileIdentity === null ||
199
+ !Number.isFinite(mtimeMs) ||
200
+ !Number.isFinite(ctimeMs) ||
201
+ typeof row.size_bytes !== "number"
202
+ )
586
203
  return [];
587
- }
588
- })
589
- .sort((left, right) => right.as_of.localeCompare(left.as_of))[0] ?? null
590
- );
591
- }
592
-
593
- function aggregateCost(
594
- records: readonly UsageRecordedV2[],
595
- fold: RunV2Fold,
596
- catalog: PricingCatalog,
597
- fxRate: FxRate | null,
598
- ) {
599
- let usd = 0;
600
- let brl = 0;
601
- let knownUsd = false;
602
- let knownBrl = false;
603
- let estimated = false;
604
- const pricingModels: string[] = [];
605
- const pricingSources: string[] = [];
606
- for (const record of records) {
607
- if (typeof record.cost.usd === "number") {
608
- usd += record.cost.usd;
609
- knownUsd = true;
610
- }
611
- if (typeof record.cost.brl === "number") {
612
- brl += record.cost.brl;
613
- knownBrl = true;
614
- }
615
- if (typeof record.cost.usd === "number" && typeof record.cost.brl === "number") continue;
616
- if (record.tokens.total === null) continue;
617
- if (record.tokens.total === 0) {
618
- knownUsd = true;
619
- knownBrl = true;
620
- continue;
621
- }
622
- const model = record.model ?? fold.run.model;
623
- if (model === null || model === "unavailable") continue;
624
- const priced = estimateUsageCost({
625
- provider: canonicalProvider(record.provider, model, fold.run.client),
626
- model,
627
- usage: record.tokens,
628
- catalog,
629
- fxRate,
630
- serviceTier: record.runtime?.service_tier ?? null,
631
- });
632
- if (typeof record.cost.usd !== "number" && priced.estimated_usd !== null) {
633
- usd += priced.estimated_usd;
634
- knownUsd = true;
635
- estimated = true;
636
- }
637
- if (typeof record.cost.brl !== "number" && priced.estimated_brl !== null) {
638
- brl += priced.estimated_brl;
639
- knownBrl = true;
640
- estimated = true;
641
- }
642
- if (priced.pricing_model !== null) pricingModels.push(priced.pricing_model);
643
- if (priced.pricing_source !== null) pricingSources.push(priced.pricing_source);
644
- }
645
- const totalTokens = knownSum(records, "total");
646
- return {
647
- usd: knownUsd ? Math.round(usd * 1_000_000) / 1_000_000 : null,
648
- brl: knownBrl ? Math.round(brl * 1_000_000) / 1_000_000 : null,
649
- method:
650
- totalTokens === 0
651
- ? "zero_no_llm_usage"
652
- : estimated
653
- ? "token_pricing_estimate"
654
- : knownUsd || knownBrl
655
- ? "canonical_recorded"
656
- : null,
657
- pricingModel: consolidatedValue(pricingModels),
658
- pricingSource: consolidatedValue(pricingSources),
659
- fxRate: knownBrl ? (fxRate?.rate ?? null) : null,
660
- };
661
- }
662
-
663
- function pricingModelForUsage(catalog: PricingCatalog, model: string | null) {
664
- if (model === null) return null;
665
- const normalizedModel = model.toLowerCase();
666
- const matches = (identity: string) => {
667
- const normalizedIdentity = identity.toLowerCase();
668
- if (normalizedModel === normalizedIdentity) return true;
669
- const suffix = normalizedModel.slice(normalizedIdentity.length);
670
- return (
671
- suffix.startsWith("@") ||
672
- suffix.startsWith(":") ||
673
- suffix.startsWith("/") ||
674
- /^-\d{4}-\d{2}-\d{2}(?:$|[.:@/])/.test(suffix)
675
- );
676
- };
677
- return (
678
- catalog.models.find(
679
- (candidate) => matches(candidate.id) || candidate.aliases.some((alias) => matches(alias)),
680
- ) ?? null
681
- );
682
- }
683
-
684
- /** Pricing columns needed to reproduce the Guide calculation at invocation grain. */
685
- function usagePricingAudit(
686
- record: UsageRecordedV2,
687
- fold: RunV2Fold,
688
- catalog: PricingCatalog,
689
- fxRate: FxRate | null,
690
- ) {
691
- const model = record.model ?? fold.run.model;
692
- const provider = canonicalProvider(record.provider, model, fold.run.client);
693
- const serviceTier = record.runtime?.service_tier ?? null;
694
- const pricingModel = pricingModelForUsage(catalog, model);
695
- const actual =
696
- model === null
697
- ? null
698
- : estimateUsageCost({
699
- provider,
700
- model,
701
- usage: record.tokens,
702
- catalog,
703
- fxRate,
704
- serviceTier,
705
- });
706
- const standard =
707
- model === null
708
- ? null
709
- : estimateUsageCost({
710
- provider,
711
- model,
712
- usage: record.tokens,
713
- catalog,
714
- fxRate,
715
- serviceTier: "default",
716
- });
717
- const longContext =
718
- pricingModel?.long_context_threshold_tokens !== null &&
719
- pricingModel?.long_context_threshold_tokens !== undefined &&
720
- (record.tokens.input ?? 0) > pricingModel.long_context_threshold_tokens;
721
- const priority = serviceTier === "priority" || serviceTier === "fast";
722
- const priorityAvailable = priority && !longContext;
723
- const inputRate = pricingModel
724
- ? priorityAvailable
725
- ? (pricingModel.priority_input_per_million_usd ?? null)
726
- : pricingModel.input_per_million_usd
727
- : null;
728
- const cachedRate = pricingModel
729
- ? priorityAvailable
730
- ? (pricingModel.priority_cached_input_per_million_usd ?? null)
731
- : pricingModel.cached_input_per_million_usd
732
- : null;
733
- const outputRate = pricingModel
734
- ? priorityAvailable
735
- ? (pricingModel.priority_output_per_million_usd ?? null)
736
- : pricingModel.output_per_million_usd
737
- : null;
738
- const reasoningRate = pricingModel
739
- ? priorityAvailable
740
- ? (pricingModel.priority_reasoning_per_million_usd ??
741
- pricingModel.priority_output_per_million_usd ??
742
- null)
743
- : (pricingModel.reasoning_per_million_usd ?? pricingModel.output_per_million_usd)
744
- : null;
745
- // Priority + long-context has no published combined tariff. The Guide uses
746
- // the published Standard long-context scenario in that case, matching the
747
- // canonical historical estimate without inventing a price table.
748
- const priced = actual?.estimated_usd !== null && actual !== null ? actual : standard;
749
- const calculatedUsd = priced?.estimated_usd ?? null;
750
- const standardUsd = standard?.estimated_usd ?? null;
751
- const recordedUsd = record.cost.usd;
752
- const effectiveUsd = recordedUsd ?? calculatedUsd;
753
- const effectiveBrl =
754
- record.cost.brl ??
755
- actual?.estimated_brl ??
756
- (effectiveUsd !== null && fxRate !== null
757
- ? Math.round(effectiveUsd * fxRate.rate * 1_000_000) / 1_000_000
758
- : null);
759
- return {
760
- provider,
761
- model,
762
- serviceTier,
763
- uncachedInputTokens:
764
- record.tokens.input === null
765
- ? null
766
- : Math.max(record.tokens.input - (record.tokens.cached_input ?? 0), 0),
767
- visibleOutputTokens:
768
- record.tokens.output === null
769
- ? null
770
- : Math.max(record.tokens.output - (record.tokens.reasoning ?? 0), 0),
771
- inputRate,
772
- cachedRate,
773
- outputRate,
774
- reasoningRate,
775
- contextInputMultiplier: pricingModel
776
- ? longContext
777
- ? pricingModel.long_context_input_multiplier
778
- : 1
779
- : null,
780
- contextOutputMultiplier: pricingModel
781
- ? longContext
782
- ? pricingModel.long_context_output_multiplier
783
- : 1
784
- : null,
785
- speedCostMultiplier:
786
- calculatedUsd !== null && standardUsd !== null && standardUsd > 0
787
- ? Math.round((calculatedUsd / standardUsd) * 1000) / 1000
788
- : null,
789
- pricingScenario:
790
- priority && longContext
791
- ? "standard_long_context_fallback"
792
- : priority
793
- ? "priority"
794
- : "standard",
795
- standardUsd,
796
- speedUpliftUsd:
797
- calculatedUsd !== null && standardUsd !== null
798
- ? Math.round((calculatedUsd - standardUsd) * 1_000_000) / 1_000_000
799
- : null,
800
- inputUsd: priced?.input_usd ?? null,
801
- cachedInputUsd: priced?.cached_input_usd ?? null,
802
- outputUsd: priced?.output_usd ?? null,
803
- reasoningUsd: priced?.reasoning_usd ?? null,
804
- calculatedUsd,
805
- recordedUsd,
806
- effectiveUsd,
807
- effectiveBrl,
808
- varianceUsd:
809
- recordedUsd !== null && calculatedUsd !== null
810
- ? Math.round((recordedUsd - calculatedUsd) * 1_000_000) / 1_000_000
811
- : null,
812
- pricingModel: priced?.pricing_model ?? null,
813
- pricingSource: priced?.pricing_source ?? null,
814
- fxRate: record.cost.fx_rate ?? fxRate?.rate ?? null,
815
- };
816
- }
817
-
818
- function saoPauloDay(timestamp: string): string {
819
- const values = Object.fromEntries(
820
- new Intl.DateTimeFormat("en", {
821
- timeZone: "America/Sao_Paulo",
822
- year: "numeric",
823
- month: "2-digit",
824
- day: "2-digit",
825
- })
826
- .formatToParts(new Date(timestamp))
827
- .filter((part) => part.type !== "literal")
828
- .map((part) => [part.type, part.value]),
829
- ) as { year: string; month: string; day: string };
830
- return `${values.year}-${values.month}-${values.day}`;
831
- }
832
-
833
- type TemporalInterval = { activityRowId: string; startMs: number; endMs: number };
834
-
835
- /**
836
- * Splits every observed interval at every activity boundary and São Paulo day
837
- * boundary. Each elementary segment is divided equally among the token-active
838
- * activities covering it, making the materialized fact additive by activity,
839
- * day, and every parent dimension.
840
- */
841
- export function allocateAttributedTokenActiveMs(intervals: readonly TemporalInterval[]) {
842
- const boundaries = new Set<number>();
843
- for (const interval of intervals) {
844
- boundaries.add(interval.startMs);
845
- boundaries.add(interval.endMs);
846
- for (let cursor = interval.startMs; cursor < interval.endMs; ) {
847
- const nextDay =
848
- Date.parse(`${saoPauloDay(new Date(cursor).toISOString())}T03:00:00.000Z`) + 86_400_000;
849
- if (nextDay >= interval.endMs) break;
850
- boundaries.add(nextDay);
851
- cursor = nextDay;
852
- }
853
- }
854
- const points = [...boundaries].sort((left, right) => left - right);
855
- const allocated = new Map<string, { activityRowId: string; day: string; value: number }>();
856
- for (let index = 0; index < points.length - 1; index += 1) {
857
- const startMs = points[index]!;
858
- const endMs = points[index + 1]!;
859
- const active = intervals.filter(
860
- (interval) => interval.startMs < endMs && interval.endMs > startMs,
204
+ return [
205
+ [
206
+ source,
207
+ {
208
+ source,
209
+ mtimeMs,
210
+ ctimeMs,
211
+ size: row.size_bytes,
212
+ contentHash,
213
+ fileIdentity,
214
+ } satisfies SqliteReadModelSourceCheckpoint,
215
+ ],
216
+ ];
217
+ }),
861
218
  );
862
- if (active.length === 0 || endMs <= startMs) continue;
863
- const share = (endMs - startMs) / active.length;
864
- const day = saoPauloDay(new Date(startMs).toISOString());
865
- for (const interval of active) {
866
- const key = `${interval.activityRowId}:${day}`;
867
- const previous = allocated.get(key);
868
- allocated.set(key, {
869
- activityRowId: interval.activityRowId,
870
- day,
871
- value: (previous?.value ?? 0) + share,
872
- });
873
- }
219
+ } catch {
220
+ return new Map();
221
+ } finally {
222
+ database?.close();
874
223
  }
875
- return [...allocated.values()];
876
- }
877
-
878
- /** A dashboard API boundary can be a date or an ISO instant. Normalize both
879
- * to the same Sao Paulo calendar key used by Cost Monitor SQL buckets. */
880
- function costMonitorFilterDay(value: string | null | undefined): string | null {
881
- if (value === null || value === undefined) return null;
882
- return /^\d{4}-\d{2}-\d{2}$/.test(value) ? value : saoPauloDay(value);
883
- }
884
-
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);
893
- }
894
-
895
- /** Extracts the small artifact projection while the canonical file is already being read. */
896
- function readCanonicalArtifact(source: string, content: string): CanonicalArtifact | null {
897
- const kind = source.includes("/artifacts/evidence/")
898
- ? "evidence"
899
- : source.includes("/artifacts/validations/") || source.includes("/artifacts/tests/")
900
- ? "validation"
901
- : null;
902
- if (kind === null) return null;
903
- const frontMatter = /^---\r?\n([\s\S]*?)\r?\n---/.exec(content)?.[1] ?? "";
904
- const scalar = (key: string): string | null => {
905
- const value = new RegExp(`^${key}:\\s*['"]?(.+?)['"]?\\s*$`, "m")
906
- .exec(frontMatter)?.[1]
907
- ?.trim();
908
- return value === undefined || value === "null" ? null : value;
909
- };
910
- const occurredAt =
911
- scalar("observed_at") ?? scalar("executed_at") ?? scalar("created_at") ?? scalar("recorded_at");
912
- if (occurredAt === null || Number.isNaN(Date.parse(occurredAt))) return null;
913
- return {
914
- kind,
915
- id: scalar("id") ?? source.slice(source.lastIndexOf("/") + 1, -3),
916
- occurredAt,
917
- occurredDaySp: saoPauloDay(occurredAt),
918
- taskId: scalar("task_id"),
919
- changeId: scalar("change_id"),
920
- runId: scalar("run_id"),
921
- };
922
- }
923
-
924
- export function sqliteReadModelPath(workspaceRoot: string): string {
925
- return resolve(process.env.ZAIP_SQLITE_DB_PATH ?? resolve(workspaceRoot, dbRelativePath));
926
224
  }
927
225
 
928
- /** Atomically installs a worker-built immutable generation after its schema is validated. */
929
226
  /**
930
227
  * Refuses stale or incomplete SQLite generations before they are served. This
931
228
  * is intentionally independent of source fingerprints: a matching cache key
@@ -1006,31 +303,79 @@ function hasReusableSnapshotPayloads(database: DatabaseSync): boolean {
1006
303
  /** Recreates the disposable comparison read model from ZAIP records and configuration. */
1007
304
  export async function rebuildSqliteReadModel(
1008
305
  workspaceRoot: string,
1009
- options: { forceCanonical?: boolean } = {},
306
+ options: { forceCanonical?: boolean; preferExisting?: boolean } = {},
1010
307
  ): Promise<{
1011
308
  files: number;
1012
309
  path: string;
1013
310
  cache: "generation" | "seed" | "stale_seed" | "rebuilt";
1014
311
  fingerprint_ms: number;
1015
312
  materialize_ms: number;
313
+ refresh: {
314
+ reused_sources: number;
315
+ processed_sources: number;
316
+ added_sources: number;
317
+ changed_sources: number;
318
+ removed_sources: number;
319
+ canonical_runs_refolded: number;
320
+ };
1016
321
  }> {
1017
322
  const path = sqliteReadModelPath(workspaceRoot);
323
+ // Bind immediately from the last complete transaction. The serving process
324
+ // checks canonical freshness in its worker before declaring this current.
325
+ if (options.preferExisting && !options.forceCanonical) {
326
+ try {
327
+ validateSqliteReadModelSchema(path);
328
+ const database = new DatabaseSync(path, { readOnly: true });
329
+ try {
330
+ if (hasReusableSnapshotPayloads(database)) {
331
+ const row = database
332
+ .prepare(
333
+ "SELECT value FROM zaip_vw_read_model_metadata WHERE key = 'source_file_count'",
334
+ )
335
+ .get() as { value: string };
336
+ return {
337
+ files: Number(row.value),
338
+ path,
339
+ cache: "stale_seed",
340
+ fingerprint_ms: 0,
341
+ materialize_ms: 0,
342
+ refresh: {
343
+ reused_sources: 0,
344
+ processed_sources: 0,
345
+ added_sources: 0,
346
+ changed_sources: 0,
347
+ removed_sources: 0,
348
+ canonical_runs_refolded: 0,
349
+ },
350
+ };
351
+ }
352
+ } finally {
353
+ database.close();
354
+ }
355
+ } catch {
356
+ // Missing, incompatible, or incomplete models require a canonical build.
357
+ }
358
+ }
1018
359
  const reportLaunchProgress = reportSqliteReadModelLaunchProgress;
1019
- const cacheDirectory = process.env.ZAIP_SQLITE_CACHE_DIR ?? join(dirname(path), "cache");
1020
- const preferredSeed = await materializePreferredStaleSeed({
1021
- destination: path,
1022
- enabled: !options.forceCanonical && process.env.ZAIP_SQLITE_PREFER_LAST_VALID_SEED === "1",
1023
- fingerprintMs: 0,
1024
- hasRequiredPayloads: hasReusableSnapshotPayloads,
1025
- reportProgress: true,
1026
- schemaVersion: readModelSchemaVersion,
1027
- });
1028
- if (preferredSeed !== null) return preferredSeed;
1029
360
  const fingerprintStartedAt = performance.now();
1030
361
  reportLaunchProgress("source_fingerprint_started", 8, "Inventariando fontes canônicas.");
1031
- const sourceSnapshot = await sourceFingerprint(workspaceRoot);
362
+ let sourceSnapshot = await sourceFingerprint(
363
+ workspaceRoot,
364
+ options.forceCanonical
365
+ ? { verifyContent: true }
366
+ : { previous: persistedSourceCheckpoint(path) },
367
+ );
368
+ let refresh = {
369
+ reused_sources: sourceSnapshot.reusedFiles,
370
+ processed_sources: sourceSnapshot.hashedFiles,
371
+ added_sources: 0,
372
+ changed_sources: 0,
373
+ removed_sources: 0,
374
+ canonical_runs_refolded: 0,
375
+ };
1032
376
  reportLaunchProgress("source_fingerprint_ready", 20, "Inventário canônico concluído.");
1033
- const { fingerprint } = sourceSnapshot;
377
+ // Revalidate date-dependent payloads on every São Paulo calendar day.
378
+ let fingerprint = `${sourceSnapshot.fingerprint}\ncurrent_day=${saoPauloDay(new Date().toISOString())}`;
1034
379
  const fingerprintMs = Math.round(performance.now() - fingerprintStartedAt);
1035
380
  const existingFiles = readSnapshotFileCount(
1036
381
  path,
@@ -1038,7 +383,7 @@ export async function rebuildSqliteReadModel(
1038
383
  readModelSchemaVersion,
1039
384
  hasReusableSnapshotPayloads,
1040
385
  );
1041
- if (existingFiles !== null) {
386
+ if (existingFiles !== null && !options.forceCanonical && sourceSnapshot.hashedFiles === 0) {
1042
387
  validateSqliteReadModelSchema(path);
1043
388
  return {
1044
389
  files: existingFiles,
@@ -1046,71 +391,275 @@ export async function rebuildSqliteReadModel(
1046
391
  cache: "generation",
1047
392
  fingerprint_ms: fingerprintMs,
1048
393
  materialize_ms: 0,
394
+ refresh,
1049
395
  };
1050
396
  }
1051
397
 
1052
- // The launcher intentionally gives every live server a private database path
1053
- // (Windows keeps read-only handles open). Cache an immutable snapshot by
1054
- // source fingerprint, then atomically materialize that cache into the
1055
- // generation path. A normal fresh-port handoff can therefore be warm.
1056
- const cachePath = join(
1057
- cacheDirectory,
1058
- // The cache payload is part of the immutable generation contract. Keep
1059
- // a separate namespace when that payload changes, so a schema-compatible
1060
- // older seed cannot bypass the new all-time precomputation.
1061
- // Keep the immutable name short enough that SQLite can create its
1062
- // temporary journal beside deep Windows fixture/workspace paths.
1063
- `v${readModelSchemaVersion}-ff9-${createHash("sha256").update(fingerprint).digest("hex")}.sqlite`,
1064
- );
1065
- const cachedFiles = readSnapshotFileCount(
1066
- cachePath,
1067
- fingerprint,
1068
- readModelSchemaVersion,
1069
- hasReusableSnapshotPayloads,
1070
- );
1071
- if (cachedFiles !== null) {
1072
- const materializeStartedAt = performance.now();
1073
- await materializeSnapshot(cachePath, path);
1074
- validateSqliteReadModelSchema(path);
1075
- return {
1076
- files: cachedFiles,
1077
- path,
1078
- cache: "seed",
1079
- fingerprint_ms: fingerprintMs,
1080
- materialize_ms: Math.round(performance.now() - materializeStartedAt),
1081
- };
1082
- }
1083
-
1084
- // A normal launcher click must not block the browser on a full canonical
1085
- // reconstruction merely because a record changed since the last snapshot.
1086
- // The launcher labels this state as updating and starts an isolated refresh;
1087
- // this process still serves only a schema-validated immutable seed.
1088
- const fallback = await materializePreferredStaleSeed({
1089
- destination: path,
1090
- enabled: !options.forceCanonical && process.env.ZAIP_SQLITE_PREFER_LAST_VALID_SEED === "1",
1091
- fingerprintMs,
1092
- hasRequiredPayloads: hasReusableSnapshotPayloads,
1093
- schemaVersion: readModelSchemaVersion,
1094
- });
1095
- if (fallback !== null) return fallback;
1096
-
1097
398
  reportLaunchProgress(
1098
399
  "source_inventory",
1099
400
  52,
1100
401
  `Encontradas ${sourceSnapshot.candidates.length} fontes canônicas.`,
1101
402
  );
1102
- await mkdir(dirname(cachePath), { recursive: true });
1103
- const temporaryPath = join(dirname(cachePath), `.snapshot-${randomUUID()}.tmp`);
1104
- const database = new DatabaseSync(temporaryPath);
403
+ await mkdir(dirname(path), { recursive: true });
404
+ const database = new DatabaseSync(path);
1105
405
  let completedFiles = 0;
1106
406
  try {
1107
- database.exec("PRAGMA journal_mode=DELETE; BEGIN IMMEDIATE;");
407
+ database.exec("PRAGMA busy_timeout=180000; PRAGMA journal_mode=WAL; BEGIN IMMEDIATE;");
1108
408
  try {
409
+ // Another process may have finished an update while this writer waited.
410
+ // Capture canonical inputs only after acquiring exclusive writer ownership.
411
+ sourceSnapshot = await sourceFingerprint(
412
+ workspaceRoot,
413
+ options.forceCanonical
414
+ ? { verifyContent: true }
415
+ : { previous: persistedSourceCheckpoint(path) },
416
+ );
417
+ refresh = {
418
+ reused_sources: sourceSnapshot.reusedFiles,
419
+ processed_sources: sourceSnapshot.hashedFiles,
420
+ added_sources: 0,
421
+ changed_sources: 0,
422
+ removed_sources: 0,
423
+ canonical_runs_refolded: 0,
424
+ };
425
+ fingerprint = `${sourceSnapshot.fingerprint}\ncurrent_day=${saoPauloDay(new Date().toISOString())}`;
426
+ // Keep the previous canonical bytes available while the transaction
427
+ // decides which sources changed. `files` is a persisted view over
428
+ // typed technical payloads, so a matching checkpoint can be reused
429
+ // without touching the canonical filesystem again. A missing source
430
+ // is deliberately absent from `sourceSnapshot` and is therefore not
431
+ // copied into the next materialization.
432
+ const persistedFiles = new Map(
433
+ (() => {
434
+ try {
435
+ return database
436
+ .prepare("SELECT path, content, content_sha256 FROM zaip_vw_files")
437
+ .all() as Array<{ path: string; content: string; content_sha256: string }>;
438
+ } catch {
439
+ // An older or incomplete schema takes the bootstrap path below.
440
+ return [] as Array<{ path: string; content: string; content_sha256: string }>;
441
+ }
442
+ })().map((row) => [row.path, row] as const),
443
+ );
444
+ const existing = database
445
+ .prepare("SELECT name, type FROM sqlite_master WHERE name NOT LIKE 'sqlite_%'")
446
+ .all() as Array<{ name: string; type: string }>;
447
+ const hasPersistentSchema =
448
+ !options.forceCanonical &&
449
+ requiredPersistedTables.every((name) =>
450
+ existing.some((entry) => entry.type === "table" && entry.name === name),
451
+ ) &&
452
+ requiredIndexes.every((name) =>
453
+ existing.some((entry) => entry.type === "index" && entry.name === name),
454
+ ) &&
455
+ requiredViews.every((name) =>
456
+ existing.some((entry) => entry.type === "view" && entry.name === name),
457
+ ) &&
458
+ (() => {
459
+ try {
460
+ const schemaVersion = database
461
+ .prepare("SELECT value FROM zaip_vw_read_model_metadata WHERE key='schema_version'")
462
+ .get() as { value?: string } | undefined;
463
+ database
464
+ .prepare(
465
+ "SELECT source_path, ctime_ms, file_identity FROM zaip_vw_read_model_source_state LIMIT 1",
466
+ )
467
+ .all();
468
+ return schemaVersion?.value === readModelSchemaVersion;
469
+ } catch {
470
+ return false;
471
+ }
472
+ })();
473
+ const persistedSourceHashes = new Map(
474
+ (() => {
475
+ try {
476
+ return database
477
+ .prepare("SELECT source_path, source_hash FROM zaip_vw_read_model_source_state")
478
+ .all() as Array<{ source_path: string; source_hash: string }>;
479
+ } catch {
480
+ return [] as Array<{ source_path: string; source_hash: string }>;
481
+ }
482
+ })().map((row) => [row.source_path, row.source_hash] as const),
483
+ );
484
+ refresh = {
485
+ ...refresh,
486
+ added_sources: sourceSnapshot.candidates.filter(
487
+ (candidate) => !persistedSourceHashes.has(candidate.source),
488
+ ).length,
489
+ changed_sources: sourceSnapshot.candidates.filter((candidate) => {
490
+ const previous = persistedSourceHashes.get(candidate.source);
491
+ return previous !== undefined && previous !== candidate.contentHash;
492
+ }).length,
493
+ removed_sources: [...persistedSourceHashes.keys()].filter(
494
+ (source) => !sourceSnapshot.candidates.some((candidate) => candidate.source === source),
495
+ ).length,
496
+ };
497
+ const changedCandidates = sourceSnapshot.candidates.filter(
498
+ (candidate) => persistedSourceHashes.get(candidate.source) !== candidate.contentHash,
499
+ );
500
+ const removedSources = [...persistedSourceHashes.keys()].filter(
501
+ (source) => !sourceSnapshot.candidates.some((candidate) => candidate.source === source),
502
+ );
503
+ if (hasPersistentSchema && changedCandidates.length === 0 && removedSources.length === 0) {
504
+ const oldFingerprint = database
505
+ .prepare("SELECT value FROM zaip_vw_read_model_metadata WHERE key='source_fingerprint'")
506
+ .get() as { value: string };
507
+ const checkpoint = persistedSourceCheckpoint(path);
508
+ for (const candidate of sourceSnapshot.candidates) {
509
+ const old = checkpoint.get(candidate.source);
510
+ if (
511
+ old &&
512
+ (old.mtimeMs !== candidate.mtimeMs ||
513
+ old.ctimeMs !== candidate.ctimeMs ||
514
+ old.fileIdentity !== candidate.fileIdentity)
515
+ )
516
+ database
517
+ .prepare(`UPDATE zaip_aios_activities SET payload_json=json_set(payload_json,'$.mtime_ms',?,'$.ctime_ms',?,'$.file_identity',?,'$.size_bytes',?)
518
+ WHERE payload_type='source_state' AND activity_key=?`)
519
+ .run(
520
+ String(candidate.mtimeMs),
521
+ String(candidate.ctimeMs),
522
+ candidate.fileIdentity,
523
+ candidate.size,
524
+ `source_state:${candidate.source}`,
525
+ );
526
+ }
527
+ database
528
+ .prepare(
529
+ "UPDATE zaip_aios_activities SET payload_json=json_set(payload_json,'$.value',?) WHERE payload_type='metadata' AND activity_key='metadata:source_fingerprint'",
530
+ )
531
+ .run(fingerprint);
532
+ if (
533
+ oldFingerprint.value.split("\ncurrent_day=")[1] !== fingerprint.split("\ncurrent_day=")[1]
534
+ ) {
535
+ database.exec(
536
+ "DELETE FROM zaip_aios_activities WHERE payload_type IN ('cost_monitor_summary_month_cache','first_fold_monthly_quick_cache')",
537
+ );
538
+ refreshFirstFoldPayloads(workspaceRoot, database);
539
+ }
540
+ database.exec("COMMIT;");
541
+ return {
542
+ files: sourceSnapshot.candidates.length,
543
+ path,
544
+ cache: "rebuilt",
545
+ fingerprint_ms: fingerprintMs,
546
+ materialize_ms: Math.round(performance.now() - fingerprintStartedAt),
547
+ refresh,
548
+ };
549
+ }
550
+ const candidateBySource = new Map(
551
+ sourceSnapshot.candidates.map((candidate) => [candidate.source, candidate]),
552
+ );
553
+ const changedSources = new Set([
554
+ ...changedCandidates.map((candidate) => candidate.source),
555
+ ...removedSources,
556
+ ]);
557
+ const previousActivities = hasPersistentSchema
558
+ ? (database
559
+ .prepare("SELECT * FROM zaip_aios_activities WHERE payload_type IS NULL")
560
+ .all() as Array<Record<string, any>>)
561
+ : [];
562
+ const previousRunIndex = hasPersistentSchema
563
+ ? (database.prepare("SELECT * FROM zaip_vw_canonical_run_index").all() as Array<
564
+ Record<string, any>
565
+ >)
566
+ : [];
567
+ const previousUsage = hasPersistentSchema
568
+ ? (database.prepare("SELECT * FROM zaip_vw_invocation_usage_segments").all() as Array<
569
+ Record<string, any>
570
+ >)
571
+ : [];
572
+ const previousDurations = hasPersistentSchema
573
+ ? (database.prepare("SELECT * FROM zaip_vw_attributed_token_active_facts").all() as Array<
574
+ Record<string, any>
575
+ >)
576
+ : [];
577
+ const durationByRun = new Map(previousRunIndex.map((row) => [row.run_id, row.duration_ms]));
578
+ const intervalFor = (
579
+ row: Record<string, any>,
580
+ duration: number | null,
581
+ ): TemporalInterval | null => {
582
+ if (!(row.total_tokens > 0) || duration === null || duration < 0) return null;
583
+ const startMs = Date.parse(row.source_started_at ?? row.occurred_at);
584
+ const endMs = Date.parse(row.source_finished_at ?? "") || startMs + duration;
585
+ return Number.isFinite(startMs) && Number.isFinite(endMs) && endMs > startMs
586
+ ? { activityRowId: row.activity_row_id, startMs, endMs }
587
+ : null;
588
+ };
589
+ const oldIntervals = previousActivities.flatMap((row) => {
590
+ const interval = intervalFor(row, durationByRun.get(row.run_id) ?? null);
591
+ return interval ? [interval] : [];
592
+ });
593
+ const configurationChanged = [...changedSources].some(
594
+ (source) =>
595
+ source.startsWith(".agents/config/pricing/") ||
596
+ source === ".agents/config/agents/catalog.yaml" ||
597
+ source === ".agents/config/clients.yaml" ||
598
+ source.includes("/historical-developer-attribution/"),
599
+ );
600
+ const entityChanged = [...changedSources].some((source) =>
601
+ /^\.agents\/records\/(tasks|specs|prds)\//.test(source),
602
+ );
603
+ const changedCreationRunIds = new Set<string>();
604
+ if (hasPersistentSchema)
605
+ for (const source of changedSources) {
606
+ if (!source.startsWith(".agents/records/specs/")) continue;
607
+ const cached = database
608
+ .prepare(
609
+ "SELECT payload_json FROM zaip_aios_activities WHERE payload_type='parsed_source' AND activity_key=?",
610
+ )
611
+ .get(`parsed_source:entities:${source}`) as { payload_json: string } | undefined;
612
+ if (cached)
613
+ for (const [, entity] of JSON.parse(cached.payload_json) as Array<
614
+ [string, CanonicalEntity]
615
+ >)
616
+ if (entity.creationRunId) changedCreationRunIds.add(entity.creationRunId);
617
+ }
618
+ const dirtyRunIds = new Set(
619
+ previousActivities
620
+ .filter((row) => configurationChanged || changedSources.has(row.source_path))
621
+ .map((row) => String(row.run_id)),
622
+ );
623
+ // A parent identity change may affect descendants even when their own bytes are unchanged.
624
+ let expanded = true;
625
+ while (expanded) {
626
+ expanded = false;
627
+ for (const row of previousActivities)
628
+ if (dirtyRunIds.has(row.parent_run_id) && !dirtyRunIds.has(row.run_id)) {
629
+ dirtyRunIds.add(row.run_id);
630
+ expanded = true;
631
+ }
632
+ }
633
+ const quote = (name: string) => `"${name.replaceAll('"', '""')}"`;
634
+ if (!hasPersistentSchema) {
635
+ for (const entry of existing.filter((entry) => entry.type === "view"))
636
+ database.exec(`DROP VIEW IF EXISTS ${quote(entry.name)}`);
637
+ for (const entry of existing.filter((entry) => entry.type === "table"))
638
+ database.exec(`DROP TABLE IF EXISTS ${quote(entry.name)}`);
639
+ } else {
640
+ // A refresh keeps the physical schema and source payload identities.
641
+ // Only derived facts are replaced; stable files/checkpoints survive
642
+ // untouched, while changed and removed sources are deleted below.
643
+ database.exec(`
644
+ DELETE FROM zaip_aios_activities
645
+ WHERE payload_type IS NOT NULL AND payload_type NOT IN ('file', 'source_state', 'parsed_source', 'usage_segment', 'run_index', 'duration_allocation', 'shared_creation_allocation', 'work_item', 'execution', 'change', 'decision', 'cost_dimension_aggregate_v1', 'cost_agent_invocation_projection_v1');
646
+ DELETE FROM zaip_dim_zaip_agent;
647
+ DELETE FROM zaip_dim_llm_model;
648
+ DELETE FROM zaip_dim_provider;
649
+ `);
650
+ try {
651
+ database.exec(
652
+ "DELETE FROM developer_cost_projection; DELETE FROM cost_invocation_dimension_facts;",
653
+ );
654
+ } catch {
655
+ // Older compatible models do not have every optional projection.
656
+ }
657
+ }
1109
658
  database.exec(`
1110
- 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);
1111
- 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);
659
+ CREATE TEMP TABLE files (path TEXT PRIMARY KEY, content TEXT NOT NULL, mtime_ms REAL 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);
660
+ CREATE TEMP TABLE read_model_source_state (source_path TEXT PRIMARY KEY, source_hash TEXT NOT NULL, mtime_ms TEXT NOT NULL, ctime_ms TEXT NOT NULL, size_bytes INTEGER NOT NULL, file_identity TEXT NOT NULL, source_kind TEXT NOT NULL, last_processed_at TEXT NOT NULL);
1112
661
  CREATE TEMP TABLE read_model_metadata (key TEXT PRIMARY KEY, value TEXT NOT NULL);
1113
- 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));
662
+ 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, creation_run_id TEXT, completed_run_id TEXT, occurred_at TEXT NOT NULL, occurred_day_sp TEXT NOT NULL, PRIMARY KEY(entity_kind, id));
1114
663
  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);
1115
664
  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);
1116
665
  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);
@@ -1127,7 +676,7 @@ export async function rebuildSqliteReadModel(
1127
676
  calculated_cost_usd REAL, recorded_cost_usd REAL, cost_variance_usd REAL,
1128
677
  cost_usd REAL, cost_brl REAL, cost_method TEXT, pricing_model TEXT, pricing_source TEXT, fx_rate REAL
1129
678
  );
1130
- CREATE TABLE zaip_aios_activities (
679
+ CREATE TABLE IF NOT EXISTS zaip_aios_activities (
1131
680
  activity_row_id TEXT PRIMARY KEY, activity_source_kind TEXT NOT NULL, activity_key TEXT NOT NULL,
1132
681
  source_path TEXT NOT NULL, source_hash TEXT NOT NULL, source_occurred_at TEXT, recorded_at TEXT NOT NULL, version_activity_id TEXT,
1133
682
  source_last_event_id TEXT, source_last_sequence INTEGER,
@@ -1146,13 +695,13 @@ export async function rebuildSqliteReadModel(
1146
695
  payload_type TEXT, payload_json TEXT
1147
696
  );`);
1148
697
  database.exec(`
1149
- CREATE TABLE zaip_dim_zaip_agent (
698
+ CREATE TABLE IF NOT EXISTS zaip_dim_zaip_agent (
1150
699
  zaip_agent TEXT PRIMARY KEY, catalog_id TEXT, persona TEXT, job TEXT, role TEXT, catalog_hash TEXT
1151
700
  );
1152
- CREATE TABLE zaip_dim_llm_model (
701
+ CREATE TABLE IF NOT EXISTS zaip_dim_llm_model (
1153
702
  model TEXT PRIMARY KEY
1154
703
  );
1155
- CREATE TABLE zaip_dim_provider (
704
+ CREATE TABLE IF NOT EXISTS zaip_dim_provider (
1156
705
  provider TEXT PRIMARY KEY
1157
706
  );
1158
707
  `);
@@ -1165,11 +714,48 @@ export async function rebuildSqliteReadModel(
1165
714
  attributed_token_active_ms REAL NOT NULL,
1166
715
  PRIMARY KEY(activity_row_id, occurred_day_sp)
1167
716
  );`);
717
+ database.exec(
718
+ "CREATE INDEX IF NOT EXISTS zaip_activities_payload ON zaip_aios_activities(payload_type, activity_key)",
719
+ );
720
+ const parsedSources = new SqliteParsedSourceCache(database);
721
+ const sourceHashFor = (source: string) => candidateBySource.get(source)?.contentHash ?? "";
722
+ const insertRows = (table: string, rows: Array<Record<string, any>>) => {
723
+ if (!rows.length) return;
724
+ const columns = Object.keys(rows[0]!);
725
+ const statement = database.prepare(
726
+ `INSERT INTO ${table}(${columns.join(",")}) VALUES (${columns.map(() => "?").join(",")})`,
727
+ );
728
+ for (const row of rows) statement.run(...columns.map((column) => row[column] ?? null));
729
+ };
730
+ insertRows(
731
+ "canonical_run_index",
732
+ previousRunIndex.filter((row) => !dirtyRunIds.has(row.run_id)),
733
+ );
734
+ const dirtyInvocationIds = new Set(
735
+ previousActivities
736
+ .filter((row) => dirtyRunIds.has(row.run_id))
737
+ .map((row) => row.agent_invocation_id),
738
+ );
739
+ insertRows(
740
+ "invocation_usage_segments",
741
+ previousUsage.filter((row) => !dirtyInvocationIds.has(row.agent_invocation_id)),
742
+ );
743
+ insertRows("attributed_token_active_facts", previousDurations);
744
+ for (const runId of dirtyRunIds)
745
+ database
746
+ .prepare("DELETE FROM zaip_aios_activities WHERE payload_type IS NULL AND run_id=?")
747
+ .run(runId);
748
+ for (const source of removedSources)
749
+ database
750
+ .prepare(
751
+ "DELETE FROM zaip_aios_activities WHERE payload_type='parsed_source' AND source_path=?",
752
+ )
753
+ .run(source);
1168
754
  const insert = database.prepare(
1169
755
  "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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
1170
756
  );
1171
757
  const insertSourceState = database.prepare(
1172
- "INSERT INTO read_model_source_state(source_path, source_hash, mtime_ms, size_bytes, source_kind, last_processed_at) VALUES (?, ?, ?, ?, ?, ?)",
758
+ "INSERT INTO read_model_source_state(source_path, source_hash, mtime_ms, ctime_ms, size_bytes, file_identity, source_kind, last_processed_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
1173
759
  );
1174
760
  let files = 0;
1175
761
  const skipped = 0;
@@ -1183,14 +769,25 @@ export async function rebuildSqliteReadModel(
1183
769
  // Parallel reads reduce cold I/O latency, while the following ordered
1184
770
  // loop retains deterministic SQLite inserts and canonical projections.
1185
771
  const contents = await Promise.all(
1186
- batch.map((candidate) => readFile(candidate.path, "utf8")),
772
+ batch.map(async (candidate) => {
773
+ const previous = persistedFiles.get(candidate.source);
774
+ if (previous?.content_sha256 === candidate.contentHash) return previous.content;
775
+ // The fingerprint captured a stable stat/hash pair. Any read
776
+ // failure here must abort the transaction, preserving the prior
777
+ // complete model instead of treating an inaccessible file as a
778
+ // deletion.
779
+ const content = await readFile(candidate.path, "utf8");
780
+ if (createHash("sha256").update(content).digest("hex") !== candidate.contentHash)
781
+ throw new Error(`Canonical source changed during refresh: ${candidate.source}`);
782
+ return content;
783
+ }),
1187
784
  );
1188
785
  for (let index = 0; index < batch.length; index += 1) {
1189
786
  const candidate = batch[index];
1190
787
  const content = contents[index];
1191
788
  if (candidate === undefined || content === undefined) continue;
1192
- const { source, mtimeMs, size } = candidate;
1193
- const sourceHash = createHash("sha256").update(content).digest("hex");
789
+ const { source, mtimeMs, ctimeMs, size, fileIdentity } = candidate;
790
+ const sourceHash = candidate.contentHash;
1194
791
  const sourceKind = source.includes("/runs/")
1195
792
  ? "run"
1196
793
  : source.includes("/artifacts/evidence/")
@@ -1200,7 +797,9 @@ export async function rebuildSqliteReadModel(
1200
797
  : source.startsWith(".agents/config/")
1201
798
  ? "config"
1202
799
  : "entity";
1203
- const artifact = readCanonicalArtifact(source, content);
800
+ const artifact = parsedSources.read("artifact", source, sourceHash, () =>
801
+ readCanonicalArtifact(source, content),
802
+ );
1204
803
  insert.run(
1205
804
  source,
1206
805
  content,
@@ -1218,8 +817,10 @@ export async function rebuildSqliteReadModel(
1218
817
  insertSourceState.run(
1219
818
  source,
1220
819
  sourceHash,
1221
- mtimeMs,
820
+ String(mtimeMs),
821
+ String(ctimeMs),
1222
822
  size,
823
+ fileIdentity,
1223
824
  sourceKind,
1224
825
  new Date().toISOString(),
1225
826
  );
@@ -1234,13 +835,32 @@ export async function rebuildSqliteReadModel(
1234
835
  );
1235
836
  }
1236
837
  reportLaunchProgress("projection_build", 74, "Organizando fatos e projeções do banco local.");
1237
- const canonicalEntities = readCanonicalEntities(
1238
- database
1239
- .prepare("SELECT path, content FROM files WHERE path LIKE '.agents/records/%'")
1240
- .all() as Array<{ path: string; content: string }>,
838
+ const canonicalRows = database
839
+ .prepare(
840
+ "SELECT path, content FROM files WHERE path LIKE '.agents/records/%' AND path NOT LIKE '.agents/records/runs/%'",
841
+ )
842
+ .all() as Array<{ path: string; content: string }>;
843
+ const canonicalEntities = new Map<string, CanonicalEntity>(
844
+ canonicalRows.flatMap((row) =>
845
+ parsedSources.read<Array<[string, CanonicalEntity]>>(
846
+ "entities",
847
+ row.path,
848
+ sourceHashFor(row.path),
849
+ () => [...readCanonicalEntities([row])],
850
+ ),
851
+ ),
1241
852
  );
853
+ for (const row of canonicalRows)
854
+ if (changedSources.has(row.path) && row.path.startsWith(".agents/records/specs/"))
855
+ for (const [, entity] of parsedSources.read<Array<[string, CanonicalEntity]>>(
856
+ "entities",
857
+ row.path,
858
+ sourceHashFor(row.path),
859
+ () => [...readCanonicalEntities([row])],
860
+ ))
861
+ if (entity.creationRunId) changedCreationRunIds.add(entity.creationRunId);
1242
862
  const insertCanonicalWorkItem = database.prepare(
1243
- "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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
863
+ "INSERT INTO canonical_work_items(entity_kind, id, title, status, stage, risk, spec_id, change_id, creation_run_id, completed_run_id, occurred_at, occurred_day_sp) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
1244
864
  );
1245
865
  for (const entity of canonicalEntities.values()) {
1246
866
  if (
@@ -1258,16 +878,25 @@ export async function rebuildSqliteReadModel(
1258
878
  entity.risk,
1259
879
  entity.specId,
1260
880
  entity.changeId,
881
+ entity.creationRunId,
1261
882
  entity.completedRunId,
1262
883
  entity.occurredAt,
1263
884
  saoPauloDay(entity.occurredAt),
1264
885
  );
1265
886
  }
1266
- const canonicalLifecycle = readCanonicalLifecycleRecords(
1267
- database
1268
- .prepare("SELECT path, content FROM files WHERE path LIKE '.agents/records/%'")
1269
- .all() as Array<{ path: string; content: string }>,
1270
- );
887
+ const canonicalLifecycle = {
888
+ executions: [] as CanonicalExecution[],
889
+ changes: [] as CanonicalChange[],
890
+ decisions: [] as CanonicalDecision[],
891
+ };
892
+ for (const row of canonicalRows) {
893
+ const parsed = parsedSources.read("lifecycle", row.path, sourceHashFor(row.path), () =>
894
+ readCanonicalLifecycleRecords([row]),
895
+ );
896
+ canonicalLifecycle.executions.push(...parsed.executions);
897
+ canonicalLifecycle.changes.push(...parsed.changes);
898
+ canonicalLifecycle.decisions.push(...parsed.decisions);
899
+ }
1271
900
  const insertCanonicalExecution = database.prepare(
1272
901
  "INSERT INTO canonical_executions(id, status, kind, origin_type, origin_id, created_at, occurred_day_sp, finished_at, source_path) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
1273
902
  );
@@ -1356,6 +985,7 @@ export async function rebuildSqliteReadModel(
1356
985
  "SELECT path, content, content_sha256 FROM files WHERE path LIKE '.agents/records/runs/%.jsonl' ORDER BY path",
1357
986
  )
1358
987
  .all() as Array<{ path: string; content: string; content_sha256: string }>;
988
+
1359
989
  // Historical attribution is an append-only, self-verifying overlay. It
1360
990
  // never changes a Run stream and therefore participates only after the
1361
991
  // native identity carried by the Run itself.
@@ -1406,18 +1036,51 @@ export async function rebuildSqliteReadModel(
1406
1036
  }
1407
1037
  return null;
1408
1038
  };
1409
- const folds = new Map(
1410
- runRows.map((row) => {
1411
- const fold = foldRunV2Records(parseRunV2JsonLines(row.content));
1412
- return [fold.run.run_id, { row, fold }] as const;
1413
- }),
1039
+ const runRowsById = new Map(
1040
+ runRows.map((row) => [
1041
+ row.path
1042
+ .split("/")
1043
+ .at(-1)!
1044
+ .replace(/\.jsonl$/, ""),
1045
+ row,
1046
+ ]),
1414
1047
  );
1048
+ for (const previous of previousActivities) {
1049
+ const row = runRows.find((row) => row.path === previous.source_path);
1050
+ if (row) runRowsById.set(previous.run_id, row);
1051
+ }
1052
+ const folds = new Map<string, { row: (typeof runRows)[number]; fold: RunV2Fold }>();
1053
+ const readFold = (row: (typeof runRows)[number]) =>
1054
+ parsedSources.read("fold", row.path, row.content_sha256, () => {
1055
+ refresh.canonical_runs_refolded += 1;
1056
+ return foldRunV2Records(parseRunV2JsonLines(row.content));
1057
+ });
1058
+ const getFold = (runId: string) => {
1059
+ const available = folds.get(runId);
1060
+ if (available) return available;
1061
+ const row = runRowsById.get(runId);
1062
+ if (!row) return undefined;
1063
+ return { row, fold: readFold(row) };
1064
+ };
1065
+ for (const row of runRows) {
1066
+ const previous = previousActivities.find((activity) => activity.source_path === row.path);
1067
+ if (
1068
+ hasPersistentSchema &&
1069
+ previous &&
1070
+ !dirtyRunIds.has(previous.run_id) &&
1071
+ !changedSources.has(row.path)
1072
+ )
1073
+ continue;
1074
+ const fold = readFold(row);
1075
+ dirtyRunIds.add(fold.run.run_id);
1076
+ folds.set(fold.run.run_id, { row, fold });
1077
+ }
1415
1078
  const rootRunId = (fold: RunV2Fold): string => {
1416
1079
  let current = fold;
1417
1080
  const seen = new Set<string>();
1418
1081
  while (current.run.parent_run_id !== null && !seen.has(current.run.run_id)) {
1419
1082
  seen.add(current.run.run_id);
1420
- const parent = folds.get(current.run.parent_run_id)?.fold;
1083
+ const parent = getFold(current.run.parent_run_id)?.fold;
1421
1084
  if (parent === undefined) break;
1422
1085
  current = parent;
1423
1086
  }
@@ -1518,9 +1181,17 @@ export async function rebuildSqliteReadModel(
1518
1181
  const insertCanonicalRun = database.prepare(
1519
1182
  "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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
1520
1183
  );
1521
- let unassistedActivityCount = 0;
1184
+ let unassistedActivityCount = previousActivities.filter(
1185
+ (row) => !dirtyRunIds.has(row.run_id) && row.agent_invocation_id === null,
1186
+ ).length;
1522
1187
  const excludedUnknownTokenRunCount = 0;
1523
- const temporalIntervals: TemporalInterval[] = [];
1188
+ const temporalIntervals: TemporalInterval[] = oldIntervals.filter(
1189
+ (interval) =>
1190
+ !previousActivities.some(
1191
+ (row) => row.activity_row_id === interval.activityRowId && dirtyRunIds.has(row.run_id),
1192
+ ),
1193
+ );
1194
+ const zeroDurationFacts: Array<{ activityRowId: string; day: string }> = [];
1524
1195
  for (const { row, fold } of folds.values()) {
1525
1196
  const started = fold.run;
1526
1197
  const developer = resolveDeveloper(row, started.developer_identity ?? null);
@@ -1702,7 +1373,7 @@ export async function rebuildSqliteReadModel(
1702
1373
  if (Number.isFinite(startMs) && Number.isFinite(endMs) && endMs > startMs)
1703
1374
  temporalIntervals.push({ activityRowId, startMs, endMs });
1704
1375
  else if (durations.effective === 0)
1705
- insertTemporalFact.run(activityRowId, saoPauloDay(started.occurred_at), 0);
1376
+ zeroDurationFacts.push({ activityRowId, day: saoPauloDay(started.occurred_at) });
1706
1377
  }
1707
1378
  if (agentRunId !== null) {
1708
1379
  for (const record of usage) {
@@ -1749,8 +1420,74 @@ export async function rebuildSqliteReadModel(
1749
1420
  }
1750
1421
  if (!isAgentRun) unassistedActivityCount += 1;
1751
1422
  }
1752
- for (const fact of allocateAttributedTokenActiveMs(temporalIntervals))
1423
+ const dirtyActivityIds = new Set(
1424
+ previousActivities
1425
+ .filter((row) => dirtyRunIds.has(row.run_id))
1426
+ .map((row) => String(row.activity_row_id)),
1427
+ );
1428
+ for (const row of database
1429
+ .prepare(
1430
+ "SELECT activity_row_id,run_id FROM zaip_aios_activities WHERE payload_type IS NULL",
1431
+ )
1432
+ .all() as Array<{ activity_row_id: string; run_id: string }>)
1433
+ if (dirtyRunIds.has(row.run_id)) dirtyActivityIds.add(row.activity_row_id);
1434
+ const temporalDelta = affectedTemporalIntervals(
1435
+ oldIntervals,
1436
+ temporalIntervals,
1437
+ dirtyActivityIds,
1438
+ );
1439
+ for (const id of temporalDelta.ids)
1440
+ database
1441
+ .prepare("DELETE FROM attributed_token_active_facts WHERE activity_row_id=?")
1442
+ .run(id);
1443
+ for (const fact of allocateAttributedTokenActiveMs(temporalDelta.intervals))
1753
1444
  insertTemporalFact.run(fact.activityRowId, fact.day, fact.value);
1445
+ for (const fact of zeroDurationFacts) insertTemporalFact.run(fact.activityRowId, fact.day, 0);
1446
+ const updateDuration =
1447
+ database.prepare(`UPDATE zaip_aios_activities SET duration_ms=?,duration_provenance=?
1448
+ WHERE activity_row_id=? AND (duration_ms IS NOT ? OR duration_provenance IS NOT ?)`);
1449
+ const temporalRuns = new Set(dirtyRunIds);
1450
+ for (const id of temporalDelta.ids) {
1451
+ const fact = database
1452
+ .prepare(
1453
+ "SELECT SUM(attributed_token_active_ms) value,COUNT(*) count FROM attributed_token_active_facts WHERE activity_row_id=?",
1454
+ )
1455
+ .get(id) as { value: number | null; count: number };
1456
+ const provenance = fact.count ? "attributed_token_active" : null;
1457
+ updateDuration.run(fact.value, provenance, id, fact.value, provenance);
1458
+ const run = database
1459
+ .prepare("SELECT run_id FROM zaip_aios_activities WHERE activity_row_id=?")
1460
+ .get(id) as { run_id: string } | undefined;
1461
+ if (run) temporalRuns.add(run.run_id);
1462
+ }
1463
+ // Entity links and titles are projections of canonical metadata, not new Run usage.
1464
+ if (entityChanged)
1465
+ for (const row of database
1466
+ .prepare(
1467
+ "SELECT activity_row_id,task_id,spec_id,prd_id,change_id,execution_id,conversation_id FROM zaip_aios_activities WHERE payload_type IS NULL",
1468
+ )
1469
+ .all() as Array<Record<string, any>>) {
1470
+ const entity =
1471
+ row.task_id !== null
1472
+ ? canonicalEntities.get(`task:${row.task_id}`)
1473
+ : row.spec_id !== null
1474
+ ? canonicalEntities.get(`spec:${row.spec_id}`)
1475
+ : row.prd_id !== null
1476
+ ? canonicalEntities.get(`prd:${row.prd_id}`)
1477
+ : null;
1478
+ const values = [
1479
+ entity?.kind ?? null,
1480
+ entity?.title ?? null,
1481
+ entity?.status ?? null,
1482
+ entity?.stage ?? null,
1483
+ entity?.risk ?? null,
1484
+ ];
1485
+ database
1486
+ .prepare(
1487
+ `UPDATE zaip_aios_activities SET work_item_kind=?,title=?,work_item_status=?,work_stage=?,risk=? WHERE activity_row_id=? AND (work_item_kind IS NOT ? OR title IS NOT ? OR work_item_status IS NOT ? OR work_stage IS NOT ? OR risk IS NOT ?)`,
1488
+ )
1489
+ .run(...values, row.activity_row_id, ...values);
1490
+ }
1754
1491
  // A single authoring sub-Run can create several sibling SPECs. Keep the
1755
1492
  // Run canonical and attach deterministic shares only to the macro read
1756
1493
  // model. Payload rows stay outside `activities`, preventing global
@@ -1763,10 +1500,23 @@ export async function rebuildSqliteReadModel(
1763
1500
  entity,
1764
1501
  ]);
1765
1502
  }
1503
+ for (const row of database
1504
+ .prepare(
1505
+ "SELECT DISTINCT run_id FROM zaip_aios_activities WHERE payload_type='shared_creation_allocation'",
1506
+ )
1507
+ .all() as Array<{ run_id: string }>)
1508
+ if (changedCreationRunIds.has(row.run_id) || temporalRuns.has(row.run_id))
1509
+ database
1510
+ .prepare(
1511
+ "DELETE FROM zaip_aios_activities WHERE payload_type='shared_creation_allocation' AND run_id=?",
1512
+ )
1513
+ .run(row.run_id);
1766
1514
  const materializeSharedCreationAllocations = () => {
1767
1515
  for (const [runId, recipients] of creationSpecsByRun) {
1768
1516
  if (recipients.length === 0) continue;
1769
- const source = folds.get(runId);
1517
+ if (hasPersistentSchema && !changedCreationRunIds.has(runId) && !temporalRuns.has(runId))
1518
+ continue;
1519
+ const source = getFold(runId);
1770
1520
  if (source === undefined) continue;
1771
1521
  const { fold, row } = source;
1772
1522
  const started = fold.run;
@@ -1933,18 +1683,14 @@ export async function rebuildSqliteReadModel(
1933
1683
  }
1934
1684
  }
1935
1685
  };
1936
- database.exec(`UPDATE zaip_aios_activities
1937
- SET duration_ms = (
1938
- SELECT SUM(f.attributed_token_active_ms)
1939
- FROM attributed_token_active_facts f
1940
- WHERE f.activity_row_id=zaip_aios_activities.activity_row_id
1941
- ),
1942
- duration_provenance = CASE
1943
- WHEN EXISTS (SELECT 1 FROM attributed_token_active_facts f WHERE f.activity_row_id=zaip_aios_activities.activity_row_id)
1944
- THEN 'attributed_token_active' ELSE NULL END
1945
- WHERE payload_type IS NULL;`);
1946
1686
  materializeSharedCreationAllocations();
1947
- const canonicalRunCount = folds.size;
1687
+ const canonicalRunCount = Number(
1688
+ (
1689
+ database
1690
+ .prepare("SELECT COUNT(*) count FROM zaip_aios_activities WHERE payload_type IS NULL")
1691
+ .get() as { count: number }
1692
+ ).count,
1693
+ );
1948
1694
  const invocationCount = (
1949
1695
  database
1950
1696
  .prepare(
@@ -1966,7 +1712,11 @@ export async function rebuildSqliteReadModel(
1966
1712
  .get() as { count: number }
1967
1713
  ).count;
1968
1714
  const activityCount = (
1969
- database.prepare("SELECT COUNT(*) AS count FROM zaip_aios_activities").get() as {
1715
+ database
1716
+ .prepare(
1717
+ "SELECT COUNT(*) AS count FROM zaip_aios_activities WHERE payload_type IS NULL OR payload_type='shared_creation_allocation'",
1718
+ )
1719
+ .get() as {
1970
1720
  count: number;
1971
1721
  }
1972
1722
  ).count;
@@ -2029,29 +1779,28 @@ export async function rebuildSqliteReadModel(
2029
1779
  // their technical inputs as typed payload activities so the on-disk
2030
1780
  // model has one fact plus its three declared dimensions, never hidden
2031
1781
  // canonical/index/cache tables.
2032
- const persistPayload = (payloadType: string, selectSql: string) =>
2033
- database.exec(`INSERT INTO zaip_aios_activities(
2034
- activity_row_id, activity_source_kind, activity_key, source_path, source_hash,
2035
- recorded_at, run_role, root_run_id, payload_type, payload_json
2036
- ) SELECT
2037
- 'TECH-' || lower(hex(randomblob(16))), 'technical_payload', '${payloadType}:' || row_key,
2038
- 'sqlite://${payloadType}', '', datetime('now'), 'technical', 'technical', '${payloadType}', payload_json
2039
- FROM (${selectSql});`);
1782
+ const persistPayload = (
1783
+ payloadType: string,
1784
+ selectSql: string,
1785
+ _preserveExistingSource = false,
1786
+ ) => synchronizePayloadRows(database, payloadType, selectSql);
2040
1787
  persistPayload(
2041
1788
  "metadata",
2042
1789
  "SELECT key AS row_key, json_object('key', key, 'value', value) AS payload_json FROM read_model_metadata",
2043
1790
  );
2044
1791
  persistPayload(
2045
1792
  "source_state",
2046
- "SELECT source_path AS row_key, json_object('source_path', source_path, 'source_hash', source_hash, 'mtime_ms', mtime_ms, 'size_bytes', size_bytes, 'source_kind', source_kind, 'last_processed_at', last_processed_at) AS payload_json FROM read_model_source_state",
1793
+ "SELECT source_path AS row_key, json_object('source_path', source_path, 'source_hash', source_hash, 'mtime_ms', mtime_ms, 'ctime_ms', ctime_ms, 'size_bytes', size_bytes, 'file_identity', file_identity, 'source_kind', source_kind, 'last_processed_at', last_processed_at) AS payload_json FROM read_model_source_state",
1794
+ true,
2047
1795
  );
2048
1796
  persistPayload(
2049
1797
  "file",
2050
1798
  "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",
1799
+ true,
2051
1800
  );
2052
1801
  persistPayload(
2053
1802
  "work_item",
2054
- "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",
1803
+ "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, 'creation_run_id', creation_run_id, 'completed_run_id', completed_run_id, 'occurred_at', occurred_at, 'occurred_day_sp', occurred_day_sp) AS payload_json FROM canonical_work_items",
2055
1804
  );
2056
1805
  persistPayload(
2057
1806
  "execution",
@@ -2071,7 +1820,7 @@ export async function rebuildSqliteReadModel(
2071
1820
  );
2072
1821
  persistPayload(
2073
1822
  "usage_segment",
2074
- "SELECT agent_invocation_id || ':' || rowid AS row_key, json_object('agent_invocation_id', agent_invocation_id, 'provider', provider, 'model', model, 'service_tier', service_tier, 'reasoning_effort', reasoning_effort, 'input_tokens', input_tokens, 'cached_input_tokens', cached_input_tokens, 'uncached_input_tokens', uncached_input_tokens, 'output_tokens', output_tokens, 'reasoning_tokens', reasoning_tokens, 'visible_output_tokens', visible_output_tokens, 'total_tokens', total_tokens, 'input_rate_usd_per_million', input_rate_usd_per_million, 'cached_input_rate_usd_per_million', cached_input_rate_usd_per_million, 'output_rate_usd_per_million', output_rate_usd_per_million, 'reasoning_rate_usd_per_million', reasoning_rate_usd_per_million, 'context_input_multiplier', context_input_multiplier, 'context_output_multiplier', context_output_multiplier, 'speed_cost_multiplier', speed_cost_multiplier, 'pricing_scenario', pricing_scenario, 'standard_cost_usd', standard_cost_usd, 'speed_uplift_usd', speed_uplift_usd, 'input_cost_usd', input_cost_usd, 'cached_input_cost_usd', cached_input_cost_usd, 'output_cost_usd', output_cost_usd, 'reasoning_cost_usd', reasoning_cost_usd, 'calculated_cost_usd', calculated_cost_usd, 'recorded_cost_usd', recorded_cost_usd, 'cost_variance_usd', cost_variance_usd, 'cost_usd', cost_usd, 'cost_brl', cost_brl, 'cost_method', cost_method, 'pricing_model', pricing_model, 'pricing_source', pricing_source, 'fx_rate', fx_rate) AS payload_json FROM invocation_usage_segments",
1823
+ "SELECT agent_invocation_id || ':' || ROW_NUMBER() OVER(PARTITION BY agent_invocation_id ORDER BY rowid) AS row_key, json_object('agent_invocation_id', agent_invocation_id, 'provider', provider, 'model', model, 'service_tier', service_tier, 'reasoning_effort', reasoning_effort, 'input_tokens', input_tokens, 'cached_input_tokens', cached_input_tokens, 'uncached_input_tokens', uncached_input_tokens, 'output_tokens', output_tokens, 'reasoning_tokens', reasoning_tokens, 'visible_output_tokens', visible_output_tokens, 'total_tokens', total_tokens, 'input_rate_usd_per_million', input_rate_usd_per_million, 'cached_input_rate_usd_per_million', cached_input_rate_usd_per_million, 'output_rate_usd_per_million', output_rate_usd_per_million, 'reasoning_rate_usd_per_million', reasoning_rate_usd_per_million, 'context_input_multiplier', context_input_multiplier, 'context_output_multiplier', context_output_multiplier, 'speed_cost_multiplier', speed_cost_multiplier, 'pricing_scenario', pricing_scenario, 'standard_cost_usd', standard_cost_usd, 'speed_uplift_usd', speed_uplift_usd, 'input_cost_usd', input_cost_usd, 'cached_input_cost_usd', cached_input_cost_usd, 'output_cost_usd', output_cost_usd, 'reasoning_cost_usd', reasoning_cost_usd, 'calculated_cost_usd', calculated_cost_usd, 'recorded_cost_usd', recorded_cost_usd, 'cost_variance_usd', cost_variance_usd, 'cost_usd', cost_usd, 'cost_brl', cost_brl, 'cost_method', cost_method, 'pricing_model', pricing_model, 'pricing_source', pricing_source, 'fx_rate', fx_rate) AS payload_json FROM invocation_usage_segments",
2075
1824
  );
2076
1825
  persistPayload(
2077
1826
  "duration_allocation",
@@ -2093,45 +1842,41 @@ export async function rebuildSqliteReadModel(
2093
1842
  profile.role,
2094
1843
  catalogHash,
2095
1844
  );
2096
- const insertCostDimensionAggregate = database.prepare(
2097
- `INSERT INTO zaip_aios_activities(
2098
- activity_row_id, activity_source_kind, activity_key, source_path, source_hash,
2099
- recorded_at, run_role, root_run_id, payload_type, payload_json
2100
- ) VALUES (?, 'technical_payload', ?, ?, '', datetime('now'), 'technical', 'technical', 'cost_dimension_aggregate_v1', ?)`,
2101
- );
2102
- for (const dimension of ["model", "provider"] as const) {
2103
- for (const row of costDimensionAggregates[dimension]) {
2104
- insertCostDimensionAggregate.run(
2105
- `TECH-${randomUUID().toLowerCase()}`,
2106
- `cost_dimension_aggregate_v1:${dimension}:${row.key}`,
2107
- `sqlite://cost_dimension_aggregate_v1/${dimension}`,
2108
- JSON.stringify({ schema_version: 1, dimension, ...row }),
2109
- );
2110
- }
2111
- }
1845
+ const dimensionPayloads: Array<{ row_key: string; payload_json: string }> = [];
1846
+ for (const dimension of ["model", "provider"] as const)
1847
+ for (const row of costDimensionAggregates[dimension])
1848
+ dimensionPayloads.push({
1849
+ row_key: `${dimension}:${row.key}`,
1850
+ payload_json: JSON.stringify({ schema_version: 1, dimension, ...row }),
1851
+ });
2112
1852
  for (const [dimension, projection] of Object.entries(costDimensionAggregates.remote)) {
2113
- for (const row of projection.rows) {
2114
- insertCostDimensionAggregate.run(
2115
- `TECH-${randomUUID().toLowerCase()}`,
2116
- `cost_dimension_aggregate_v1:${dimension}:${row.key}`,
2117
- `sqlite://cost_dimension_aggregate_v1/${dimension}`,
2118
- JSON.stringify({ schema_version: 1, dimension, row_kind: "row", ...row }),
2119
- );
2120
- }
2121
- insertCostDimensionAggregate.run(
2122
- `TECH-${randomUUID().toLowerCase()}`,
2123
- `cost_dimension_aggregate_v1:${dimension}:__totals__`,
2124
- `sqlite://cost_dimension_aggregate_v1/${dimension}`,
2125
- JSON.stringify({
1853
+ for (const row of projection.rows)
1854
+ dimensionPayloads.push({
1855
+ row_key: `${dimension}:${row.key}`,
1856
+ payload_json: JSON.stringify({ schema_version: 1, dimension, row_kind: "row", ...row }),
1857
+ });
1858
+ dimensionPayloads.push({
1859
+ row_key: `${dimension}:__totals__`,
1860
+ payload_json: JSON.stringify({
2126
1861
  schema_version: 1,
2127
1862
  dimension,
2128
1863
  row_kind: "totals",
2129
1864
  ...projection.totals,
2130
1865
  }),
2131
- );
1866
+ });
2132
1867
  }
1868
+ synchronizePayloadRows(database, "cost_dimension_aggregate_v1", dimensionPayloads);
2133
1869
  // Keep canonical TEMP references outside the public-view name rewriter.
2134
- database.exec(rebuildCostInvocationDimensionFactsSql);
1870
+ database.exec(
1871
+ hasPersistentSchema
1872
+ ? rebuildCostInvocationDimensionFactsSql
1873
+ .replace(
1874
+ "CREATE TABLE cost_invocation_dimension_facts AS",
1875
+ "DELETE FROM cost_invocation_dimension_facts; INSERT INTO cost_invocation_dimension_facts",
1876
+ )
1877
+ .replace(/CREATE INDEX cost_dimension_facts_[A-Za-z_]+ ON [^;]+;/g, "")
1878
+ : rebuildCostInvocationDimensionFactsSql,
1879
+ );
2135
1880
  const viewSql = `
2136
1881
  CREATE INDEX zaip_activities_agent_run ON zaip_aios_activities(activity_source_kind, agent_run_id, occurred_day_sp, occurred_at DESC);
2137
1882
  CREATE INDEX zaip_activities_relationships ON zaip_aios_activities(task_id, spec_id, prd_id, execution_id, conversation_id);
@@ -2148,12 +1893,12 @@ export async function rebuildSqliteReadModel(
2148
1893
  CREATE INDEX zaip_activities_period_invocation ON zaip_aios_activities(occurred_day_sp, agent_invocation_id)
2149
1894
  WHERE agent_invocation_id IS NOT NULL;
2150
1895
  CREATE INDEX zaip_activities_status ON zaip_aios_activities(activity_source_kind, status, phase);
2151
- CREATE INDEX zaip_activities_payload ON zaip_aios_activities(payload_type, activity_key);
1896
+ CREATE INDEX IF NOT EXISTS zaip_activities_payload ON zaip_aios_activities(payload_type, activity_key);
2152
1897
  CREATE INDEX zaip_activities_work_item_identity ON zaip_aios_activities(json_extract(payload_json, '$.entity_kind'), json_extract(payload_json, '$.id')) WHERE payload_type='work_item';
2153
1898
  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';
2154
- 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';
1899
+ 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, '$.ctime_ms') AS ctime_ms, json_extract(payload_json, '$.size_bytes') AS size_bytes, json_extract(payload_json, '$.file_identity') AS file_identity, 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';
2155
1900
  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';
2156
- 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';
1901
+ 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, '$.creation_run_id') AS creation_run_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';
2157
1902
  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';
2158
1903
  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';
2159
1904
  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';
@@ -2507,44 +2252,7 @@ export async function rebuildSqliteReadModel(
2507
2252
  SUM(cost_monitor_total_duration)
2508
2253
  FROM daily GROUP BY substr(day, 1, 4);
2509
2254
  CREATE VIEW vw_cost_executions AS SELECT i.execution_id, i.execution_id AS title, e.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 executions e ON e.id = i.execution_id WHERE i.execution_id IS NOT NULL GROUP BY i.execution_id, e.status;
2510
- CREATE VIEW vw_cost_agent_invocations AS
2511
- SELECT i.*,
2512
- COUNT(u.usage_index) AS pricing_segment_count,
2513
- CASE WHEN COUNT(DISTINCT COALESCE(u.provider, '') || '|' || COALESCE(u.model, '') || '|' || COALESCE(u.service_tier, 'default') || '|' || COALESCE(CAST(u.context_input_multiplier AS TEXT), '') || '|' || COALESCE(CAST(u.context_output_multiplier AS TEXT), '')) > 1 THEN 1 ELSE 0 END AS pricing_mixed,
2514
- SUM(u.uncached_input_tokens) AS uncached_input_tokens,
2515
- SUM(u.visible_output_tokens) AS visible_output_tokens,
2516
- CASE COUNT(DISTINCT u.input_rate_usd_per_million) WHEN 1 THEN MIN(u.input_rate_usd_per_million) ELSE NULL END AS input_rate_usd_per_million,
2517
- CASE COUNT(DISTINCT u.cached_input_rate_usd_per_million) WHEN 1 THEN MIN(u.cached_input_rate_usd_per_million) ELSE NULL END AS cached_input_rate_usd_per_million,
2518
- CASE COUNT(DISTINCT u.output_rate_usd_per_million) WHEN 1 THEN MIN(u.output_rate_usd_per_million) ELSE NULL END AS output_rate_usd_per_million,
2519
- CASE COUNT(DISTINCT u.reasoning_rate_usd_per_million) WHEN 1 THEN MIN(u.reasoning_rate_usd_per_million) ELSE NULL END AS reasoning_rate_usd_per_million,
2520
- CASE COUNT(DISTINCT u.context_input_multiplier) WHEN 1 THEN MIN(u.context_input_multiplier) ELSE NULL END AS context_input_multiplier,
2521
- CASE COUNT(DISTINCT u.context_output_multiplier) WHEN 1 THEN MIN(u.context_output_multiplier) ELSE NULL END AS context_output_multiplier,
2522
- CASE COUNT(DISTINCT u.speed_cost_multiplier) WHEN 1 THEN MIN(u.speed_cost_multiplier) ELSE NULL END AS speed_cost_multiplier,
2523
- CASE COUNT(DISTINCT u.pricing_scenario) WHEN 1 THEN MIN(u.pricing_scenario) ELSE 'mixed' END AS pricing_scenario,
2524
- SUM(u.standard_cost_usd) AS standard_cost_usd,
2525
- SUM(u.speed_uplift_usd) AS speed_uplift_usd,
2526
- SUM(u.input_cost_usd) AS input_cost_usd,
2527
- SUM(u.cached_input_cost_usd) AS cached_input_cost_usd,
2528
- SUM(u.output_cost_usd) AS output_cost_usd,
2529
- SUM(u.reasoning_cost_usd) AS reasoning_cost_usd,
2530
- SUM(u.calculated_cost_usd) AS calculated_cost_usd,
2531
- SUM(u.recorded_cost_usd) AS recorded_cost_usd,
2532
- CASE COUNT(DISTINCT CASE WHEN u.recorded_cost_usd IS NOT NULL THEN u.fx_rate END)
2533
- WHEN 1 THEN MIN(CASE WHEN u.recorded_cost_usd IS NOT NULL THEN u.fx_rate END)
2534
- ELSE NULL
2535
- END AS recorded_fx_rate,
2536
- SUM(CASE WHEN u.recorded_cost_usd IS NOT NULL THEN u.cost_brl END) AS recorded_brl,
2537
- (SELECT json_extract(fx.content, '$.rate')
2538
- FROM files fx
2539
- WHERE fx.path LIKE '.agents/config/pricing/fx/%.json'
2540
- ORDER BY CASE WHEN fx.path LIKE '%/current-usd-brl.json' THEN 0 ELSE 1 END,
2541
- json_extract(fx.content, '$.as_of') DESC
2542
- LIMIT 1) AS current_fx_rate,
2543
- SUM(u.cost_variance_usd) AS cost_variance_usd,
2544
- CASE COUNT(DISTINCT u.pricing_model) WHEN 1 THEN MIN(u.pricing_model) ELSE NULL END AS audit_pricing_model,
2545
- CASE COUNT(DISTINCT u.pricing_source) WHEN 1 THEN MIN(u.pricing_source) ELSE NULL END AS audit_pricing_source
2546
- FROM invocations i LEFT JOIN invocation_usage u USING(agent_invocation_id)
2547
- GROUP BY i.agent_invocation_id;
2255
+ CREATE VIEW vw_cost_agent_invocations AS ${costInvocationProjectionSelect};
2548
2256
  CREATE VIEW vw_cost_zaip_agents AS SELECT zaip_agent, SUM(llm_call_count) AS llm_call_count, SUM(total_tokens) AS total_tokens, SUM(cost_usd) AS cost_usd, SUM(cost_brl) AS cost_brl, COUNT(*) AS invocation_count FROM invocations GROUP BY zaip_agent;
2549
2257
  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;
2550
2258
  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;
@@ -2963,7 +2671,7 @@ export async function rebuildSqliteReadModel(
2963
2671
  CREATE VIEW prd_summary_daily AS SELECT * FROM vw_prd_summary_daily;
2964
2672
  CREATE VIEW task_status_daily AS SELECT * FROM vw_task_status_daily;
2965
2673
  CREATE VIEW task_risk_daily AS SELECT * FROM vw_task_risk_daily;
2966
- COMMIT;`;
2674
+ `;
2967
2675
  // Every SQLite view has a single discoverable namespace. References in
2968
2676
  // the view definitions are rewritten from their concise source aliases
2969
2677
  // at build time, so DBeaver and direct SQL never expose unprefixed views.
@@ -2986,11 +2694,11 @@ export async function rebuildSqliteReadModel(
2986
2694
  return statement;
2987
2695
  },
2988
2696
  );
2989
- database.exec(deduplicatedViewSql);
2697
+ if (!hasPersistentSchema) database.exec(deduplicatedViewSql);
2990
2698
  // One row per stable GitHub account. Costs are summed from the single
2991
2699
  // canonical Run fact, while delivery is joined exclusively through the
2992
2700
  // Task's completed_run_id (never inferred from a branch or latest Run).
2993
- database.exec(`
2701
+ const developerProjectionSql = `
2994
2702
  CREATE TABLE developer_cost_projection AS
2995
2703
  WITH completed AS (
2996
2704
  SELECT a.developer_account_id, COUNT(DISTINCT w.id) AS completed_task_count
@@ -3017,51 +2725,91 @@ export async function rebuildSqliteReadModel(
3017
2725
  GROUP BY a.developer_account_id
3018
2726
  ORDER BY total_cost_brl DESC, account_id;
3019
2727
  CREATE INDEX developer_cost_projection_lookup ON developer_cost_projection(account_id);
3020
- `);
2728
+ `;
2729
+ database.exec(
2730
+ hasPersistentSchema
2731
+ ? developerProjectionSql
2732
+ .replace(
2733
+ "CREATE TABLE developer_cost_projection AS",
2734
+ "DELETE FROM developer_cost_projection; INSERT INTO developer_cost_projection",
2735
+ )
2736
+ .replace(
2737
+ "CREATE INDEX developer_cost_projection_lookup ON developer_cost_projection(account_id);",
2738
+ "",
2739
+ )
2740
+ : developerProjectionSql,
2741
+ );
3021
2742
  materializeCostGroupDailyProjection(database);
3022
2743
  materializeOperationsWorkProjection(database);
3023
2744
  // Persist the complete unfiltered invocation projection while the seed
3024
2745
  // still owns the compact canonical facts. Runtime pages can then slice
3025
2746
  // 10/100 rows without expanding the pricing view for every request.
3026
- const costAgentInvocationProjection = database
3027
- .prepare(
3028
- `SELECT * FROM zaip_vw_cost_monitor_agent_invocations
3029
- ORDER BY occurred_at DESC, agent_invocation_id DESC`,
3030
- )
3031
- .all() as Array<Record<string, unknown>>;
3032
- const insertCostAgentInvocationProjection = database.prepare(
3033
- `INSERT INTO zaip_aios_activities(
3034
- activity_row_id, activity_source_kind, activity_key, source_path, source_hash,
3035
- recorded_at, occurred_day_sp, run_role, root_run_id, payload_type, payload_json
3036
- ) VALUES (?, 'technical_payload', ?, 'sqlite://cost_agent_invocation_projection_v1', '',
3037
- datetime('now'), ?, 'technical', 'technical', 'cost_agent_invocation_projection_v1', ?)`,
2747
+ const costAgentInvocationProjection = readCostInvocationProjection(database);
2748
+ synchronizePayloadRows(
2749
+ database,
2750
+ "cost_agent_invocation_projection_v1",
2751
+ costAgentInvocationProjection.flatMap((row) => {
2752
+ const invocationId = String(row.agent_invocation_id ?? "");
2753
+ return invocationId
2754
+ ? [
2755
+ {
2756
+ row_key: invocationId,
2757
+ payload_json: JSON.stringify(row),
2758
+ occurred_day_sp:
2759
+ typeof row.occurred_day_sp === "string" ? row.occurred_day_sp : null,
2760
+ },
2761
+ ]
2762
+ : [];
2763
+ }),
3038
2764
  );
3039
- database.exec("BEGIN;");
3040
- for (const row of costAgentInvocationProjection) {
3041
- const invocationId = String(row.agent_invocation_id ?? "");
3042
- if (invocationId.length === 0) continue;
3043
- insertCostAgentInvocationProjection.run(
3044
- `TECH-${randomUUID().toLowerCase()}`,
3045
- `cost_agent_invocation_projection_v1:${invocationId}`,
3046
- typeof row.occurred_day_sp === "string" ? row.occurred_day_sp : null,
3047
- JSON.stringify(row),
3048
- );
3049
- }
2765
+ // Source staging tables are no longer needed after the projections are
2766
+ // materialized. Release their TEMP names before installing the public
2767
+ // view aliases on this same connection.
2768
+ database.exec(`
2769
+ DROP TABLE IF EXISTS files;
2770
+ DROP TABLE IF EXISTS read_model_source_state;
2771
+ DROP TABLE IF EXISTS read_model_metadata;
2772
+ DROP TABLE IF EXISTS canonical_work_items;
2773
+ DROP TABLE IF EXISTS canonical_executions;
2774
+ DROP TABLE IF EXISTS canonical_changes;
2775
+ DROP TABLE IF EXISTS canonical_decisions;
2776
+ DROP TABLE IF EXISTS canonical_run_index;
2777
+ DROP TABLE IF EXISTS invocation_usage_segments;
2778
+ `);
2779
+ refreshFirstFoldPayloads(workspaceRoot, database);
3050
2780
  database.exec("COMMIT;");
3051
2781
  reportLaunchProgress(
3052
2782
  "projection_finalize",
3053
2783
  80,
3054
- "Finalizando índices e a cópia local do dashboard.",
2784
+ "Finalizando índices do banco persistente do dashboard.",
3055
2785
  );
3056
2786
  completedFiles = files;
3057
2787
  } catch (error) {
3058
2788
  database.exec("ROLLBACK;");
3059
2789
  throw error;
3060
2790
  }
2791
+ // Reclaim the transaction log when no reader pins an older snapshot.
2792
+ // With the default zero busy timeout this never waits on a dashboard read;
2793
+ // a busy checkpoint leaves the valid WAL for the next synchronization.
2794
+ database.exec("PRAGMA busy_timeout=0; PRAGMA wal_checkpoint(TRUNCATE);");
3061
2795
  } finally {
3062
2796
  database.close();
3063
2797
  }
3064
- const materializeStartedAt = performance.now();
2798
+ const materializeStartedAt = fingerprintStartedAt + fingerprintMs;
2799
+ reportLaunchProgress("snapshot_ready", 86, "O banco SQLite persistente está pronto.");
2800
+ validateSqliteReadModelSchema(path);
2801
+ return {
2802
+ files: completedFiles,
2803
+ path,
2804
+ cache: "rebuilt",
2805
+ fingerprint_ms: fingerprintMs,
2806
+ materialize_ms: Math.round(performance.now() - materializeStartedAt),
2807
+ refresh,
2808
+ };
2809
+ }
2810
+
2811
+ function refreshFirstFoldPayloads(workspaceRoot: string, database: DatabaseSync): void {
2812
+ const reportLaunchProgress = reportSqliteReadModelLaunchProgress;
3065
2813
  const timed = <T>(phase: string, action: () => T) =>
3066
2814
  measureFirstFold(
3067
2815
  process.env.ZAIP_DASHBOARD_LAUNCH_PROGRESS === "1",
@@ -3069,11 +2817,10 @@ export async function rebuildSqliteReadModel(
3069
2817
  phase,
3070
2818
  action,
3071
2819
  );
3072
- // The shared cache remains private until every expensive first-fold payload
3073
- // has been committed. A concurrent launcher may freely use `path`, its own
3074
- // generation, but must never copy a half-published cache seed.
3075
- await materializeSnapshot(temporaryPath, path);
3076
- const firstFoldFilesystem = new SqliteReadModelFileSystem(workspaceRoot);
2820
+ // This reader shares the writer connection, therefore it sees the
2821
+ // uncommitted canonical facts and projections. Its payloads join the
2822
+ // same BEGIN IMMEDIATE transaction below.
2823
+ const firstFoldFilesystem = new SqliteReadModelFileSystem(workspaceRoot, database);
3077
2824
  const firstFoldViewport = timed("first_fold_viewport", () =>
3078
2825
  firstFoldFilesystem.firstFoldViewportAllTime(),
3079
2826
  );
@@ -3119,7 +2866,7 @@ export async function rebuildSqliteReadModel(
3119
2866
  top_tasks: timed("first_fold_month_top_tasks", () => firstFoldFilesystem.topTasks(quickMonth)),
3120
2867
  };
3121
2868
  firstFoldFilesystem.close();
3122
- persistFirstFoldPayloads([temporaryPath, path], {
2869
+ persistFirstFoldPayloadsInDatabase(database, {
3123
2870
  first_fold_viewport_cache: JSON.stringify(firstFoldViewport),
3124
2871
  first_fold_all_time_cache: JSON.stringify(firstFoldAllTime),
3125
2872
  first_fold_all_time_board_cache: JSON.stringify(firstFoldAllTimeBoard),
@@ -3130,34 +2877,6 @@ export async function rebuildSqliteReadModel(
3130
2877
  }),
3131
2878
  first_fold_monthly_quick_cache: JSON.stringify(firstFoldMonthlyQuick),
3132
2879
  });
3133
- try {
3134
- await rename(temporaryPath, cachePath);
3135
- } catch (error: unknown) {
3136
- // A concurrent builder wins only when it published a complete seed.
3137
- if (
3138
- readSnapshotFileCount(
3139
- cachePath,
3140
- fingerprint,
3141
- readModelSchemaVersion,
3142
- hasReusableSnapshotPayloads,
3143
- ) === null
3144
- ) {
3145
- await rm(temporaryPath, { force: true });
3146
- throw error;
3147
- }
3148
- await rm(temporaryPath, { force: true });
3149
- await materializeSnapshot(cachePath, path);
3150
- }
3151
- reportLaunchProgress("snapshot_ready", 86, "A cópia privada do banco local está pronta.");
3152
- validateSqliteReadModelSchema(path);
3153
- await pruneSnapshotCache(dirname(cachePath), cachePath);
3154
- return {
3155
- files: completedFiles,
3156
- path,
3157
- cache: "rebuilt",
3158
- fingerprint_ms: fingerprintMs,
3159
- materialize_ms: Math.round(performance.now() - materializeStartedAt),
3160
- };
3161
2880
  }
3162
2881
 
3163
2882
  export type SqliteFirstFoldAggregate = {
@@ -3353,10 +3072,15 @@ export class SqliteReadModelFileSystem extends WorkspaceFileSystem {
3353
3072
  return rows;
3354
3073
  }
3355
3074
  private database: DatabaseSync;
3075
+ private readonly ownsDatabase: boolean;
3356
3076
 
3357
- constructor(root: string) {
3077
+ constructor(root: string, database?: DatabaseSync) {
3358
3078
  super(root);
3359
- this.database = new DatabaseSync(sqliteReadModelPath(root), { readOnly: true });
3079
+ this.ownsDatabase = database === undefined;
3080
+ this.database = database ?? new DatabaseSync(sqliteReadModelPath(root), { readOnly: true });
3081
+ // The aliases are TEMP views on this connection. The rebuilding writer
3082
+ // needs them too because first-fold readers deliberately query through
3083
+ // the same uncommitted transaction.
3360
3084
  installLegacyViewAliases(this.database);
3361
3085
  }
3362
3086
 
@@ -3417,7 +3141,7 @@ export class SqliteReadModelFileSystem extends WorkspaceFileSystem {
3417
3141
  }
3418
3142
 
3419
3143
  close(): void {
3420
- this.database.close();
3144
+ if (this.ownsDatabase) this.database.close();
3421
3145
  }
3422
3146
 
3423
3147
  private reopen(): void {
@@ -5038,6 +4762,7 @@ export class SqliteReadModelFileSystem extends WorkspaceFileSystem {
5038
4762
  if (cached !== undefined)
5039
4763
  return structuredClone(cached) as ReturnType<SqliteReadModelFileSystem["uncachedViewPage"]>;
5040
4764
  const result = this.uncachedViewPage(view, input);
4765
+ if (view === "cost_monitor_zaip_agents") attachAgentProductivity(this.database, input, result);
5041
4766
  if (this.viewPageResultCache.size >= 256) {
5042
4767
  const oldest = this.viewPageResultCache.keys().next().value as string | undefined;
5043
4768
  if (oldest !== undefined) this.viewPageResultCache.delete(oldest);
@@ -5340,9 +5065,7 @@ export class SqliteReadModelFileSystem extends WorkspaceFileSystem {
5340
5065
  };
5341
5066
  const filteredCostGroup = filteredCostGroups[view];
5342
5067
  if (
5343
- (filteredCostGroup === "agent" ||
5344
- filteredCostGroup === "model" ||
5345
- filteredCostGroup === "provider") &&
5068
+ (filteredCostGroup === "agent" || filteredCostGroup === "provider") &&
5346
5069
  isDateOnlyCostPage(input)
5347
5070
  ) {
5348
5071
  const from = costMonitorFilterDay(input.from) ?? "0000-01-01";
@@ -5390,6 +5113,11 @@ export class SqliteReadModelFileSystem extends WorkspaceFileSystem {
5390
5113
  json_extract(payload_json, '$.label') AS label,
5391
5114
  json_extract(payload_json, '$.invocation_count') AS invocation_count,
5392
5115
  json_extract(payload_json, '$.task_count') AS task_count,
5116
+ json_extract(payload_json, '$.conversation_count') AS conversation_count,
5117
+ json_extract(payload_json, '$.prd_count') AS prd_count,
5118
+ json_extract(payload_json, '$.spec_count') AS spec_count,
5119
+ json_extract(payload_json, '$.test_count') AS validation_count,
5120
+ json_extract(payload_json, '$.evidence_count') AS evidence_count,
5393
5121
  json_extract(payload_json, '$.total_duration') AS total_duration,
5394
5122
  json_extract(payload_json, '$.source_event_count') AS source_event_count,
5395
5123
  json_extract(payload_json, '$.total_tokens') AS total_tokens,
@@ -5400,18 +5128,77 @@ export class SqliteReadModelFileSystem extends WorkspaceFileSystem {
5400
5128
  AND json_extract(payload_json, '$.dimension')=?
5401
5129
  ORDER BY total_cost_brl DESC, key`)
5402
5130
  .all(dimension) as Array<Record<string, unknown>>;
5131
+ // The Models surface is an explicit comparison of identified models.
5132
+ // Keep unattributed facts in storage and in the other dimensions, but
5133
+ // do not let their sentinel become a displayed model or its footer.
5134
+ const visiblePayloadRows =
5135
+ dimension === "model"
5136
+ ? payloadRows.filter((row) => row.key !== "unavailable" && row.key !== null)
5137
+ : payloadRows;
5403
5138
  const limit = Math.min(200, Math.max(1, input.limit ?? 50));
5404
5139
  const offset = Math.max(0, input.offset ?? 0);
5405
- const rows = payloadRows.slice(offset, offset + limit);
5406
- return {
5407
- data_source: "sqlite_read_model" as const,
5408
- view,
5409
- offset,
5410
- limit,
5411
- total_count: payloadRows.length,
5412
- has_more: offset + rows.length < payloadRows.length,
5413
- rows,
5414
- };
5140
+ const rows = visiblePayloadRows.slice(offset, offset + limit);
5141
+ // Older cached aggregate payloads predate conversation_count. Route
5142
+ // Models through the physical fact query rather than presenting a
5143
+ // misleading zero until the next projection refresh persists it.
5144
+ const modelConversationPayloadComplete =
5145
+ dimension !== "model" ||
5146
+ visiblePayloadRows.every((row) => typeof row.conversation_count === "number");
5147
+ const modelNumericFields = [
5148
+ "invocation_count",
5149
+ "task_count",
5150
+ "conversation_count",
5151
+ "prd_count",
5152
+ "spec_count",
5153
+ "validation_count",
5154
+ "evidence_count",
5155
+ "total_duration",
5156
+ "source_event_count",
5157
+ "total_tokens",
5158
+ "total_cost_usd",
5159
+ "total_cost_brl",
5160
+ ];
5161
+ if (dimension === "model") {
5162
+ for (const row of visiblePayloadRows)
5163
+ for (const field of modelNumericFields)
5164
+ row[field] = typeof row[field] === "number" ? row[field] : 0;
5165
+ }
5166
+ const nullableSum = (field: string) =>
5167
+ visiblePayloadRows.some((row) => typeof row[field] === "number")
5168
+ ? visiblePayloadRows.reduce(
5169
+ (sum, row) => sum + (typeof row[field] === "number" ? Number(row[field]) : 0),
5170
+ 0,
5171
+ )
5172
+ : null;
5173
+ if (modelConversationPayloadComplete)
5174
+ return {
5175
+ data_source: "sqlite_read_model" as const,
5176
+ view,
5177
+ offset,
5178
+ limit,
5179
+ total_count: visiblePayloadRows.length,
5180
+ has_more: offset + rows.length < visiblePayloadRows.length,
5181
+ totals: {
5182
+ invocation_count: visiblePayloadRows.reduce(
5183
+ (sum, row) => sum + Number(row.invocation_count ?? 0),
5184
+ 0,
5185
+ ),
5186
+ task_count: nullableSum("task_count") ?? (dimension === "model" ? 0 : null),
5187
+ conversation_count:
5188
+ nullableSum("conversation_count") ?? (dimension === "model" ? 0 : null),
5189
+ prd_count: nullableSum("prd_count") ?? (dimension === "model" ? 0 : null),
5190
+ spec_count: nullableSum("spec_count") ?? (dimension === "model" ? 0 : null),
5191
+ validation_count: nullableSum("validation_count") ?? (dimension === "model" ? 0 : null),
5192
+ evidence_count: nullableSum("evidence_count") ?? (dimension === "model" ? 0 : null),
5193
+ total_duration: nullableSum("total_duration") ?? (dimension === "model" ? 0 : null),
5194
+ source_event_count:
5195
+ nullableSum("source_event_count") ?? (dimension === "model" ? 0 : null),
5196
+ total_tokens: nullableSum("total_tokens") ?? (dimension === "model" ? 0 : null),
5197
+ total_cost_usd: nullableSum("total_cost_usd") ?? (dimension === "model" ? 0 : null),
5198
+ total_cost_brl: nullableSum("total_cost_brl") ?? (dimension === "model" ? 0 : null),
5199
+ },
5200
+ rows,
5201
+ };
5415
5202
  }
5416
5203
  if (filteredCostGroup === "agent") {
5417
5204
  const clauses = ["i.zaip_agent IS NOT NULL"];
@@ -5841,7 +5628,7 @@ export class SqliteReadModelFileSystem extends WorkspaceFileSystem {
5841
5628
  SELECT agent_invocation_id, COUNT(DISTINCT model_key) AS model_count
5842
5629
  FROM model_segments WHERE model_key <> 'unavailable' GROUP BY agent_invocation_id
5843
5630
  ), filtered_segments AS (
5844
- SELECT s.*, i.task_id, i.duration_ms, i.source_event_count, km.model_count
5631
+ SELECT s.*, i.task_id, i.conversation_id, i.duration_ms, i.source_event_count, km.model_count
5845
5632
  FROM segments s JOIN invocations i USING(agent_invocation_id)
5846
5633
  LEFT JOIN tasks t ON t.id=i.task_id LEFT JOIN known_models km USING(agent_invocation_id)
5847
5634
  WHERE ${where}`;
@@ -5859,7 +5646,7 @@ export class SqliteReadModelFileSystem extends WorkspaceFileSystem {
5859
5646
  SELECT key, SUM(total_tokens) AS total_tokens, SUM(total_cost_usd) AS total_cost_usd, SUM(total_cost_brl) AS total_cost_brl
5860
5647
  FROM financial_components GROUP BY key
5861
5648
  ), population_models AS (
5862
- SELECT agent_invocation_id, model_key, MAX(task_id) AS task_id, MAX(duration_ms) AS duration_ms,
5649
+ SELECT agent_invocation_id, model_key, MAX(task_id) AS task_id, MAX(conversation_id) AS conversation_id, MAX(duration_ms) AS duration_ms,
5863
5650
  MAX(source_event_count) AS source_event_count, MAX(model_count) AS model_count
5864
5651
  FROM filtered_segments GROUP BY agent_invocation_id, model_key
5865
5652
  ), population_ranked AS (
@@ -5872,52 +5659,150 @@ export class SqliteReadModelFileSystem extends WorkspaceFileSystem {
5872
5659
  SUM(source_event_count) AS source_event_count
5873
5660
  FROM population_ranked WHERE model_rank = 1 AND model_count = 1
5874
5661
  GROUP BY model_key
5875
- ), task_models AS (
5662
+ ), agent_task_models AS (
5876
5663
  SELECT task_id, model_key
5877
- FROM population_models WHERE task_id IS NOT NULL
5664
+ FROM population_models WHERE task_id IS NOT NULL AND model_key <> 'unavailable'
5878
5665
  GROUP BY task_id, model_key
5666
+ ), run_conversations AS (
5667
+ SELECT run_id, MAX(conversation_id) AS conversation_id
5668
+ FROM activities
5669
+ WHERE run_id IS NOT NULL AND conversation_id IS NOT NULL
5670
+ GROUP BY run_id
5671
+ ), direct_model_activities AS (
5672
+ SELECT CASE WHEN a.model IS NULL OR trim(a.model) = '' OR lower(a.model) IN ('unavailable', 'unknown', 'mixed') THEN 'unavailable' ELSE a.model END AS model_key,
5673
+ a.task_id, COALESCE(a.conversation_id, run_conversations.conversation_id) AS conversation_id,
5674
+ a.activity_source_kind
5675
+ FROM activities a
5676
+ LEFT JOIN tasks activity_task ON activity_task.id=a.task_id
5677
+ LEFT JOIN run_conversations ON run_conversations.run_id=a.run_id
5678
+ WHERE ${activityClauses.join(" AND ")}
5679
+ AND (a.total_tokens IS NOT NULL OR a.cost_usd IS NOT NULL OR a.cost_brl IS NOT NULL)
5680
+ ), direct_task_models AS (
5681
+ SELECT task_id, model_key FROM direct_model_activities
5682
+ -- A legacy task_id on an unassisted turn is correlation metadata, not
5683
+ -- an agent-run Task participation. That turn still contributes its
5684
+ -- conversation below.
5685
+ WHERE task_id IS NOT NULL AND COALESCE(activity_source_kind, '') <> 'unassisted_turn'
5686
+ AND model_key <> 'unavailable'
5687
+ ), task_models AS (
5688
+ SELECT task_id, model_key FROM agent_task_models
5689
+ UNION SELECT task_id, model_key FROM direct_task_models
5879
5690
  ), task_population AS (
5880
5691
  SELECT model_key AS key,
5881
5692
  COUNT(*) AS task_count
5882
5693
  FROM task_models
5883
5694
  GROUP BY model_key
5695
+ ), direct_conversations AS (
5696
+ SELECT model_key, conversation_id FROM direct_model_activities
5697
+ WHERE conversation_id IS NOT NULL
5698
+ AND (activity_source_kind='unassisted_turn' OR task_id IS NULL)
5699
+ ), conversation_models AS (
5700
+ SELECT conversation_id, model_key FROM population_models
5701
+ WHERE conversation_id IS NOT NULL AND task_id IS NULL
5702
+ UNION
5703
+ SELECT conversation_id, model_key FROM direct_conversations
5704
+ ), conversation_population AS (
5705
+ SELECT model_key AS key, COUNT(*) AS conversation_count
5706
+ FROM conversation_models GROUP BY model_key
5884
5707
  ), keys AS (
5885
- SELECT key FROM financial UNION SELECT key FROM population UNION SELECT key FROM task_population
5708
+ SELECT key FROM financial UNION SELECT key FROM population UNION SELECT key FROM task_population UNION SELECT key FROM conversation_population
5709
+ ), displayed_keys AS (
5710
+ SELECT key FROM keys WHERE key <> 'unavailable'
5886
5711
  )`;
5887
5712
  const limit = Math.min(200, Math.max(1, input.limit ?? 50));
5888
5713
  const offset = Math.max(0, input.offset ?? 0);
5889
5714
  const total_count = Number(
5890
5715
  (
5891
5716
  this.database
5892
- .prepare(`${ctes} ${facts} SELECT COUNT(*) AS count FROM keys`)
5893
- .get(...values, ...activityValues) as { count: number }
5717
+ .prepare(`${ctes} ${facts} SELECT COUNT(*) AS count FROM displayed_keys`)
5718
+ .get(...values, ...activityValues, ...activityValues) as { count: number }
5894
5719
  ).count,
5895
5720
  );
5896
- // Dimension membership is intentionally non-additive: one Task can use
5897
- // several concrete models. Expose the distinct population separately so
5898
- // a footer never reports the inflated sum of row memberships as total.
5721
+ // Model membership is intentionally additive in this view: a Task is
5722
+ // counted once for every model it used, so the footer sums the rows.
5899
5723
  const totals = this.database
5900
5724
  .prepare(`${ctes} ${facts} SELECT
5901
- (SELECT COUNT(*) FROM population_ranked WHERE model_rank = 1) AS invocation_count,
5902
- (SELECT COUNT(DISTINCT task_id) FROM population_models) AS task_count,
5903
- (SELECT SUM(duration_ms) FROM population_ranked WHERE model_rank = 1) AS total_duration,
5904
- (SELECT SUM(CASE WHEN duration_ms IS NULL THEN 1 ELSE 0 END) FROM population_ranked WHERE model_rank = 1) AS duration_unknown,
5905
- (SELECT SUM(CASE WHEN duration_ms IS NULL THEN 0 ELSE duration_ms END) FROM population_ranked WHERE model_rank = 1) AS duration_known,
5906
- (SELECT SUM(source_event_count) FROM population_ranked WHERE model_rank = 1) AS source_event_count,
5907
- SUM(input_tokens) AS total_input_tokens,
5908
- SUM(cached_input_tokens) AS total_cached_input_tokens,
5909
- SUM(output_tokens) AS total_output_tokens,
5910
- SUM(reasoning_tokens) AS total_reasoning_tokens,
5911
- SUM(total_tokens) AS total_tokens,
5912
- SUM(cost_usd) AS total_cost_usd,
5913
- SUM(cost_brl) AS total_cost_brl
5914
- FROM filtered_segments`)
5915
- .get(...values, ...activityValues) as Record<string, unknown>;
5725
+ SUM(COALESCE(population.invocation_count, 0)) AS invocation_count,
5726
+ SUM(COALESCE(task_population.task_count, 0)) AS task_count,
5727
+ SUM(COALESCE(conversation_population.conversation_count, 0)) AS conversation_count,
5728
+ SUM(COALESCE(population.total_duration, 0)) AS total_duration,
5729
+ SUM(CASE WHEN population.invocation_count > 0 AND population.total_duration IS NULL THEN population.invocation_count ELSE 0 END) AS duration_unknown,
5730
+ SUM(COALESCE(population.total_duration, 0)) AS duration_known,
5731
+ SUM(COALESCE(population.source_event_count, 0)) AS source_event_count,
5732
+ SUM(COALESCE(financial.total_tokens, 0)) AS total_tokens,
5733
+ SUM(COALESCE(financial.total_cost_usd, 0)) AS total_cost_usd,
5734
+ SUM(COALESCE(financial.total_cost_brl, 0)) AS total_cost_brl
5735
+ FROM displayed_keys
5736
+ LEFT JOIN population USING(key)
5737
+ LEFT JOIN task_population USING(key)
5738
+ LEFT JOIN conversation_population USING(key)
5739
+ LEFT JOIN financial USING(key)`)
5740
+ .get(...values, ...activityValues, ...activityValues) as Record<string, unknown>;
5916
5741
  const rows = this.database
5917
5742
  .prepare(
5918
- `${ctes} ${facts} SELECT keys.key, keys.key AS label, COALESCE(population.invocation_count, 0) AS invocation_count, COALESCE(task_population.task_count, 0) AS task_count, population.total_duration, population.source_event_count, financial.total_tokens, financial.total_cost_usd, financial.total_cost_brl FROM keys LEFT JOIN population USING(key) LEFT JOIN task_population USING(key) LEFT JOIN financial USING(key) ORDER BY financial.total_cost_brl DESC, keys.key LIMIT ? OFFSET ?`,
5743
+ `${ctes} ${facts} SELECT displayed_keys.key, displayed_keys.key AS label, COALESCE(population.invocation_count, 0) AS invocation_count, COALESCE(task_population.task_count, 0) AS task_count, COALESCE(conversation_population.conversation_count, 0) AS conversation_count, population.total_duration, population.source_event_count, financial.total_tokens, financial.total_cost_usd, financial.total_cost_brl FROM displayed_keys LEFT JOIN population USING(key) LEFT JOIN task_population USING(key) LEFT JOIN conversation_population USING(key) LEFT JOIN financial USING(key) ORDER BY financial.total_cost_brl DESC, displayed_keys.key LIMIT ? OFFSET ?`,
5919
5744
  )
5920
- .all(...values, ...activityValues, limit, offset) as Array<Record<string, unknown>>;
5745
+ .all(...values, ...activityValues, ...activityValues, limit, offset) as Array<
5746
+ Record<string, unknown>
5747
+ >;
5748
+ const productivityRows = creationProductivityByModel(
5749
+ this.database,
5750
+ input.from?.slice(0, 10) ?? "0000-01-01",
5751
+ input.to?.slice(0, 10) ?? "9999-12-31",
5752
+ {
5753
+ ...(input.model ? { model: input.model } : {}),
5754
+ ...(input.provider ? { provider: input.provider } : {}),
5755
+ },
5756
+ );
5757
+ const productivity = new Map(productivityRows.map((row) => [String(row.key), row]));
5758
+ for (const row of rows) {
5759
+ const extra = productivity.get(String(row.key));
5760
+ Object.assign(row, {
5761
+ prd_count: extra?.prd_count ?? 0,
5762
+ spec_count: extra?.spec_count ?? 0,
5763
+ validation_count: extra?.validation_count ?? 0,
5764
+ evidence_count: extra?.evidence_count ?? 0,
5765
+ });
5766
+ }
5767
+ Object.assign(totals, {
5768
+ task_count: Number(totals.task_count ?? 0),
5769
+ conversation_count: Number(totals.conversation_count ?? 0),
5770
+ prd_count: sumKnownProductivity(productivityRows, "prd_count") ?? 0,
5771
+ spec_count: sumKnownProductivity(productivityRows, "spec_count") ?? 0,
5772
+ validation_count: sumKnownProductivity(productivityRows, "validation_count") ?? 0,
5773
+ evidence_count: sumKnownProductivity(productivityRows, "evidence_count") ?? 0,
5774
+ });
5775
+ for (const row of rows)
5776
+ for (const field of [
5777
+ "invocation_count",
5778
+ "task_count",
5779
+ "conversation_count",
5780
+ "prd_count",
5781
+ "spec_count",
5782
+ "validation_count",
5783
+ "evidence_count",
5784
+ "total_duration",
5785
+ "source_event_count",
5786
+ "total_tokens",
5787
+ "total_cost_usd",
5788
+ "total_cost_brl",
5789
+ ])
5790
+ row[field] = typeof row[field] === "number" ? row[field] : 0;
5791
+ for (const field of [
5792
+ "invocation_count",
5793
+ "task_count",
5794
+ "conversation_count",
5795
+ "prd_count",
5796
+ "spec_count",
5797
+ "validation_count",
5798
+ "evidence_count",
5799
+ "total_duration",
5800
+ "source_event_count",
5801
+ "total_tokens",
5802
+ "total_cost_usd",
5803
+ "total_cost_brl",
5804
+ ])
5805
+ totals[field] = typeof totals[field] === "number" ? totals[field] : 0;
5921
5806
  return {
5922
5807
  data_source: "sqlite_read_model" as const,
5923
5808
  view,