@zaip-ai/cli 4.5.0 → 4.7.0

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 (244) hide show
  1. package/README.md +2 -0
  2. package/dist/commands/init.js +1 -1
  3. package/dist/commands/init.js.map +1 -1
  4. package/dist/services/adapters.d.ts +1 -1
  5. package/dist/services/adapters.d.ts.map +1 -1
  6. package/dist/services/adapters.js +6 -6
  7. package/dist/services/adapters.js.map +1 -1
  8. package/dist/services/codex-config-overlay.d.ts.map +1 -1
  9. package/dist/services/codex-config-overlay.js +27 -10
  10. package/dist/services/codex-config-overlay.js.map +1 -1
  11. package/dist/services/codex-native-tool-authority-template.d.ts.map +1 -1
  12. package/dist/services/codex-native-tool-authority-template.js +188 -9
  13. package/dist/services/codex-native-tool-authority-template.js.map +1 -1
  14. package/dist/services/codex-pre-tool-hook-template.d.ts.map +1 -1
  15. package/dist/services/codex-pre-tool-hook-template.js +50 -128
  16. package/dist/services/codex-pre-tool-hook-template.js.map +1 -1
  17. package/dist/services/codex-pre-tool-hook-template.test.js +4 -0
  18. package/dist/services/codex-pre-tool-hook-template.test.js.map +1 -1
  19. package/dist/services/codex-pre-tool-support-template.d.ts.map +1 -1
  20. package/dist/services/codex-pre-tool-support-template.js +1 -4
  21. package/dist/services/codex-pre-tool-support-template.js.map +1 -1
  22. package/dist/services/codex-stale-git-lock-template.d.ts +2 -0
  23. package/dist/services/codex-stale-git-lock-template.d.ts.map +1 -0
  24. package/dist/services/codex-stale-git-lock-template.js +64 -0
  25. package/dist/services/codex-stale-git-lock-template.js.map +1 -0
  26. package/dist/services/global-codex-config-migration.test.js +1 -4
  27. package/dist/services/global-codex-config-migration.test.js.map +1 -1
  28. package/dist/services/global-codex-config.d.ts.map +1 -1
  29. package/dist/services/global-codex-config.js +2 -1
  30. package/dist/services/global-codex-config.js.map +1 -1
  31. package/dist/services/global-codex-config.test.js +3 -1
  32. package/dist/services/global-codex-config.test.js.map +1 -1
  33. package/dist/services/hook-readiness.d.ts.map +1 -1
  34. package/dist/services/hook-readiness.js.map +1 -1
  35. package/dist/services/install-agents-template.js +2 -2
  36. package/dist/services/install-agents-template.js.map +1 -1
  37. package/dist/services/install-template-policy.d.ts.map +1 -1
  38. package/dist/services/install-template-policy.js +2 -0
  39. package/dist/services/install-template-policy.js.map +1 -1
  40. package/dist/services/logger.d.ts +1 -1
  41. package/dist/services/logger.d.ts.map +1 -1
  42. package/dist/services/logger.js +2 -1
  43. package/dist/services/logger.js.map +1 -1
  44. package/dist/services/logger.test.js +77 -0
  45. package/dist/services/logger.test.js.map +1 -1
  46. package/dist/services/runtime-template-integrity.test.d.ts +2 -0
  47. package/dist/services/runtime-template-integrity.test.d.ts.map +1 -0
  48. package/dist/services/runtime-template-integrity.test.js +26 -0
  49. package/dist/services/runtime-template-integrity.test.js.map +1 -0
  50. package/dist/services/runtime-template-test-support.d.ts +3 -0
  51. package/dist/services/runtime-template-test-support.d.ts.map +1 -0
  52. package/dist/services/runtime-template-test-support.js +32 -0
  53. package/dist/services/runtime-template-test-support.js.map +1 -0
  54. package/dist/services/runtime-template.test.js +37 -48
  55. package/dist/services/runtime-template.test.js.map +1 -1
  56. package/package.json +1 -1
  57. package/templates/.agents/README.md +8 -0
  58. package/templates/.agents/config/agents/discovery.json +1 -1
  59. package/templates/.agents/config/deploy-policy.json +5 -0
  60. package/templates/.agents/dashboard/.zaip-managed.json +235 -83
  61. package/templates/.agents/dashboard/README.md +2 -0
  62. package/templates/.agents/dashboard/apps/frontend/dist/assets/{CostCalculationGuidePage-f4ZGJRg3.js → CostCalculationGuidePage-OUjdwEcM.js} +1 -1
  63. package/templates/.agents/dashboard/apps/frontend/dist/assets/{DashboardHelpDialog-CkQ_y8kg.js → DashboardHelpDialog-Cei1ChjE.js} +99 -10
  64. package/templates/.agents/dashboard/apps/frontend/dist/assets/{PricingSettingsDialog-v9zGRTlk.js → PricingSettingsDialog-_woNCrTT.js} +1 -1
  65. package/templates/.agents/dashboard/apps/frontend/dist/assets/{ProjectSettingsPage-doIMhei7.js → ProjectSettingsPage-B2_7MVOP.js} +1 -1
  66. package/templates/.agents/dashboard/apps/frontend/dist/assets/{TraceabilityWorkspace-C_4ftjyB.js → TraceabilityWorkspace-C_jCsSZe.js} +1 -1
  67. package/templates/.agents/dashboard/apps/frontend/dist/assets/cost-monitor-C8zQDBT7.js +68 -0
  68. package/templates/.agents/dashboard/apps/frontend/dist/assets/{icons-vendor-Cv3WBFIF.js → icons-vendor-iMPXot7Y.js} +1 -1
  69. package/templates/.agents/dashboard/apps/frontend/dist/assets/index-DZhppOIk.css +1 -0
  70. package/templates/.agents/dashboard/apps/frontend/dist/assets/index-DiCwz0sr.js +2 -0
  71. package/templates/.agents/dashboard/apps/frontend/dist/assets/{react-vendor-mHgsmxb6.js → react-vendor-721Re3Ss.js} +1 -1
  72. package/templates/.agents/dashboard/apps/frontend/dist/assets/{traceability-B0CPyEVq.js → traceability-BjdTkWS0.js} +1 -1
  73. package/templates/.agents/dashboard/apps/frontend/dist/index.html +6 -6
  74. package/templates/.agents/dashboard/apps/frontend/package.json +1 -1
  75. package/templates/.agents/dashboard/apps/frontend/src/AppOverlays.tsx +6 -34
  76. package/templates/.agents/dashboard/apps/frontend/src/ReconciliationFeedback.dom.test.ts +130 -0
  77. package/templates/.agents/dashboard/apps/frontend/src/ReconciliationFeedback.tsx +226 -0
  78. package/templates/.agents/dashboard/apps/frontend/src/dashboard-api-sqlite-view.ts +45 -0
  79. package/templates/.agents/dashboard/apps/frontend/src/dashboard-api.test.ts +15 -0
  80. package/templates/.agents/dashboard/apps/frontend/src/dashboard-api.ts +17 -46
  81. package/templates/.agents/dashboard/apps/frontend/src/reconciliation-feedback-fixtures.ts +23 -0
  82. package/templates/.agents/dashboard/apps/frontend/src/reconciliation-feedback-model.test.ts +69 -0
  83. package/templates/.agents/dashboard/apps/frontend/src/reconciliation-feedback-model.ts +59 -0
  84. package/templates/.agents/dashboard/apps/frontend/src/reconciliation-feedback.css +131 -0
  85. package/templates/.agents/dashboard/apps/frontend/src/styles.css +0 -32
  86. package/templates/.agents/dashboard/apps/frontend/src/use-dashboard-app-model.tsx +2 -2
  87. package/templates/.agents/dashboard/modules/cost-monitor/package.json +1 -1
  88. package/templates/.agents/dashboard/modules/cost-monitor/src/react/CostMonitorPage.test.ts +2 -2
  89. package/templates/.agents/dashboard/modules/cost-monitor/src/react/CostMonitorPage.tsx +1 -1
  90. package/templates/.agents/dashboard/modules/operations/package.json +1 -1
  91. package/templates/.agents/dashboard/modules/operations/src/react/OperationsOverview.macro-metrics.test.ts +7 -2
  92. package/templates/.agents/dashboard/modules/operations/src/react/OperationsOverview.tsx +6 -6
  93. package/templates/.agents/dashboard/modules/operations/src/react/OperationsPage.tsx +1 -4
  94. package/templates/.agents/dashboard/modules/operations/src/react/UnifiedWorkBoard.navigation.test.ts +236 -0
  95. package/templates/.agents/dashboard/modules/operations/src/react/UnifiedWorkBoard.tsx +1 -24
  96. package/templates/.agents/dashboard/modules/operations/src/react/operations-overview-period-filter.test.ts +3 -2
  97. package/templates/.agents/dashboard/modules/operations/src/react/overview-duration-hint.ts +14 -0
  98. package/templates/.agents/dashboard/modules/traceability/package.json +1 -1
  99. package/templates/.agents/dashboard/packages/branding/package.json +1 -1
  100. package/templates/.agents/dashboard/packages/contracts/package.json +1 -1
  101. package/templates/.agents/dashboard/packages/data-export/package.json +1 -1
  102. package/templates/.agents/dashboard/packages/local-runtime/DIAGNOSTICS.md +42 -0
  103. package/templates/.agents/dashboard/packages/local-runtime/dashboard-diagnostics.d.mts +10 -0
  104. package/templates/.agents/dashboard/packages/local-runtime/dashboard-diagnostics.mjs +101 -0
  105. package/templates/.agents/dashboard/packages/local-runtime/dashboard-diagnostics.test.mjs +58 -0
  106. package/templates/.agents/dashboard/packages/local-runtime/dist/server.cjs +318 -236
  107. package/templates/.agents/dashboard/packages/local-runtime/dist/sqlite-read-model.cjs +285 -205
  108. package/templates/.agents/dashboard/packages/local-runtime/package.json +1 -1
  109. package/templates/.agents/dashboard/packages/local-runtime/scripts/test-sqlite-dashboard-startup.mjs +233 -0
  110. package/templates/.agents/dashboard/packages/local-runtime/scripts/validate-provider-nonzero.mjs +45 -0
  111. package/templates/.agents/dashboard/packages/local-runtime/scripts/verify-dashboard-executable-startup.mjs +107 -0
  112. package/templates/.agents/dashboard/packages/local-runtime/scripts/verify-dashboard-reconciliation-executable.mjs +271 -0
  113. package/templates/.agents/dashboard/packages/local-runtime/server.mjs +318 -236
  114. package/templates/.agents/dashboard/packages/local-runtime/src/active-codex-telemetry.test.ts +78 -12
  115. package/templates/.agents/dashboard/packages/local-runtime/src/active-codex-telemetry.ts +36 -56
  116. package/templates/.agents/dashboard/packages/local-runtime/src/canonical-source-revision-cache.test.ts +125 -0
  117. package/templates/.agents/dashboard/packages/local-runtime/src/canonical-source-revision-cache.ts +168 -0
  118. package/templates/.agents/dashboard/packages/local-runtime/src/dashboard-diagnostics.test.ts +57 -0
  119. package/templates/.agents/dashboard/packages/local-runtime/src/server-agent-catalog-routes.ts +76 -0
  120. package/templates/.agents/dashboard/packages/local-runtime/src/server-dual-reads-manifest-summaries-without-losing.test.ts +5 -0
  121. package/templates/.agents/dashboard/packages/local-runtime/src/server-reconciliation-diagnostics.test.ts +118 -0
  122. package/templates/.agents/dashboard/packages/local-runtime/src/server-reconciliation-result.test.ts +35 -6
  123. package/templates/.agents/dashboard/packages/local-runtime/src/server-reconciliation-route.ts +105 -0
  124. package/templates/.agents/dashboard/packages/local-runtime/src/server-snapshot-synchronization.test.ts +64 -9
  125. package/templates/.agents/dashboard/packages/local-runtime/src/server-snapshot-synchronization.ts +19 -27
  126. package/templates/.agents/dashboard/packages/local-runtime/src/server-telemetry-command.test.ts +111 -0
  127. package/templates/.agents/dashboard/packages/local-runtime/src/server-telemetry-refresh.test.ts +59 -0
  128. package/templates/.agents/dashboard/packages/local-runtime/src/server-telemetry-refresh.ts +44 -0
  129. package/templates/.agents/dashboard/packages/local-runtime/src/server-work-reconciliation.test.ts +20 -0
  130. package/templates/.agents/dashboard/packages/local-runtime/src/server-work-reconciliation.ts +12 -7
  131. package/templates/.agents/dashboard/packages/local-runtime/src/server-workspace.ts +48 -6
  132. package/templates/.agents/dashboard/packages/local-runtime/src/server.test.ts +2 -2
  133. package/templates/.agents/dashboard/packages/local-runtime/src/server.ts +126 -134
  134. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-cost-agent-page.ts +14 -2
  135. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-cost-summary-elapsed.ts +107 -0
  136. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-dashboard-startup.test.ts +163 -0
  137. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-dashboard-startup.ts +78 -0
  138. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-header-timestamp.test.ts +31 -0
  139. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-provider-nonzero.test.ts +271 -0
  140. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-provider-visibility.ts +32 -0
  141. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-query-operations-duration.ts +58 -0
  142. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-query-provider-dimension.ts +12 -3
  143. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-query-summary.ts +41 -54
  144. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-query-view-page.ts +9 -58
  145. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-read-model-canonical.ts +7 -7
  146. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-read-model-counts-every-root-execution-day-returns.test.ts +5 -5
  147. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-read-model-multi-provider.test.ts +4 -4
  148. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-read-model-rebuild.ts +9 -35
  149. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-read-model-schema.ts +13 -9
  150. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-read-model-source-fingerprint.ts +43 -45
  151. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-read-model.test.ts +5 -9
  152. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-read-model.ts +5 -1
  153. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-schema-base-views.ts +9 -7
  154. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-schema-dashboard-views.ts +10 -8
  155. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-spec-root-elapsed-view.ts +61 -0
  156. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-spec-root-elapsed.test.ts +210 -0
  157. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-startup-seed.test.ts +87 -0
  158. package/templates/.agents/dashboard/packages/local-runtime/start-dashboard-logging.mjs +6 -2
  159. package/templates/.agents/dashboard/packages/local-runtime/start-dashboard-server.mjs +7 -1
  160. package/templates/.agents/dashboard/packages/local-runtime/start-dashboard.mjs +4 -5
  161. package/templates/.agents/dashboard/packages/ui/package.json +1 -1
  162. package/templates/.agents/dashboard/packages/ui/src/DashboardHeader.test.mjs +33 -4
  163. package/templates/.agents/dashboard/packages/ui/src/DashboardHeader.tsx +8 -7
  164. package/templates/.agents/dashboard/packages/windows-launcher/Program.cs +46 -3
  165. package/templates/.agents/dashboard/tests/e2e/package.json +1 -1
  166. package/templates/.agents/dashboard/tests/e2e/qualify-kanban-popup.mjs +56 -0
  167. package/templates/.agents/dashboard/tests/e2e/specs/dashboard-update-label.spec.ts +49 -0
  168. package/templates/.agents/design/.zaip-managed.json +17 -5
  169. package/templates/.agents/design/apps/catalog/dist/assets/index-D3uhpycC.js +72 -0
  170. package/templates/.agents/design/apps/catalog/dist/index.html +1 -1
  171. package/templates/.agents/design/apps/catalog/package.json +1 -1
  172. package/templates/.agents/design/apps/catalog/src/pages/ComponentsPage.tsx +13 -1
  173. package/templates/.agents/design/apps/catalog/src/specimens/ReconciliationSpecimen.stories.tsx +6 -0
  174. package/templates/.agents/design/apps/catalog/src/specimens/ReconciliationSpecimen.tsx +101 -0
  175. package/templates/.agents/design/packages/local-runtime/package.json +1 -1
  176. package/templates/.agents/requirements/autonomous-assurance.md +1 -1
  177. package/templates/.agents/requirements/cloud-hostinger-autonomy.md +1 -1
  178. package/templates/.agents/requirements/comparative-audit.md +1 -1
  179. package/templates/.agents/requirements/context-engine.md +1 -1
  180. package/templates/.agents/requirements/cost-observability.md +1 -1
  181. package/templates/.agents/requirements/dashboard-launch.md +1 -1
  182. package/templates/.agents/requirements/data-export.md +1 -1
  183. package/templates/.agents/requirements/design-system.md +1 -1
  184. package/templates/.agents/requirements/execution-engine.md +1 -1
  185. package/templates/.agents/requirements/kanban-dashboard.md +1 -1
  186. package/templates/.agents/requirements/model-orchestration.md +21 -6
  187. package/templates/.agents/requirements/public-distribution.md +4 -4
  188. package/templates/.agents/requirements/software-architecture.md +1 -1
  189. package/templates/.agents/requirements/strategic-improvements.md +1 -1
  190. package/templates/.agents/requirements/work-lifecycle.md +1 -1
  191. package/templates/.agents/requirements/zaip-strategic-95.md +1 -1
  192. package/templates/.agents/rules/core.md +3 -1
  193. package/templates/.agents/rules/workflow.md +3 -2
  194. package/templates/.agents/runtime/VERSION +1 -1
  195. package/templates/.agents/runtime/bin/zaip.mjs +368 -199
  196. package/templates/.agents/runtime/checksums.sha256 +5 -5
  197. package/templates/.agents/runtime/manifest.json +1 -1
  198. package/templates/.agents/runtime/provenance.json +4 -4
  199. package/templates/.agents/runtime/sbom.cdx.json +1 -1
  200. package/templates/.agents/skills/zaip-skill-config/SKILL.md +25 -3
  201. package/templates/.agents/skills/zaip-skill-execution/SKILL.md +4 -0
  202. package/templates/.agents/skills/zaip-skill-execution/references/autonomous-continuation.md +4 -0
  203. package/templates/.agents/skills/zaip-skill-execution/references/candidate-workflow.md +1 -1
  204. package/templates/.agents/skills/zaip-skill-execution/references/runtime-governance.md +2 -2
  205. package/templates/.agents/skills/zaip-skill-general-operations/SKILL.md +1 -0
  206. package/templates/.agents/skills/zaip-skill-github-deploy/SKILL.md +38 -48
  207. package/templates/.agents/skills/zaip-skill-github-deploy/agents/openai.yaml +2 -2
  208. package/templates/.agents/skills/zaip-skill-github-deploy/references/deployment-workflow.md +67 -99
  209. package/templates/.agents/skills/zaip-skill-github-deploy/references/release-identity.md +33 -30
  210. package/templates/.agents/skills/zaip-skill-github-deploy/scripts/deploy-contract.mjs +307 -0
  211. package/templates/.agents/skills/zaip-skill-github-deploy/scripts/deploy-contract.test.mjs +337 -0
  212. package/templates/.agents/skills/zaip-skill-github-deploy/scripts/github-deploy-contract.test.mjs +41 -28
  213. package/templates/.agents/skills/zaip-skill-github-deploy/scripts/guarded-commits.mjs +212 -0
  214. package/templates/.agents/skills/zaip-skill-lifecycle-closure/SKILL.md +4 -0
  215. package/templates/.agents/skills/zaip-skill-prd/SKILL.md +4 -0
  216. package/templates/.agents/skills/zaip-skill-release-qualification/SKILL.md +46 -68
  217. package/templates/.agents/skills/zaip-skill-release-qualification/references/local-preparation.md +100 -44
  218. package/templates/.agents/skills/zaip-skill-send-email/SKILL.md +15 -0
  219. package/templates/.agents/skills/zaip-skill-send-email/agents/openai.yaml +4 -0
  220. package/templates/.agents/skills/zaip-skill-setup/SKILL.md +5 -2
  221. package/templates/.agents/skills/zaip-skill-spec/SKILL.md +4 -0
  222. package/templates/.agents/skills/zaip-skill-task/SKILL.md +4 -0
  223. package/templates/.agents/skills/zaip-skill-testing-strategy/SKILL.md +19 -0
  224. package/templates/.agents/skills/zaip-skill-tool-setup/SKILL.md +2 -0
  225. package/templates/.agents/skills/zaip-skill-use-connectors/SKILL.md +15 -0
  226. package/templates/.agents/skills/zaip-skill-use-connectors/agents/openai.yaml +4 -0
  227. package/templates/.agents/skills/zaip-skill-use-connectors/scripts/connector-guidance.test.mjs +29 -0
  228. package/templates/.agents/skills/zaip-skill-use-connectors/scripts/verify-connector-lock.mjs +30 -0
  229. package/templates/.agents/tools/cloud-runner.integration.test.mjs +3 -0
  230. package/templates/.agents/zaip-aios/VERSION +1 -1
  231. package/templates/.agents/zaip-aios/guides/03-tasks.md +6 -0
  232. package/templates/.agents/zaip-aios/guides/04-specs.md +6 -0
  233. package/templates/.agents/zaip-aios/guides/05-prds.md +4 -0
  234. package/templates/.agents/zaip-aios/guides/06-execucao.md +6 -0
  235. package/templates/.agents/zaip-aios/guides/16-lifecycle-e-reconciliacao.md +6 -0
  236. package/templates/.agents/zaip-aios/guides/19-assurance-e-autonomia.md +4 -0
  237. package/templates/.agents/zaip-aios/guides/22-fluxos-e-roteamento.md +6 -0
  238. package/templates/.agents/zaip-aios/guides/23-orquestracao-de-modelos.md +36 -4
  239. package/templates/.agents/zaip-aios/guides/26-qualidade-e-release.md +15 -4
  240. package/templates/.agents/zaip-aios/guides/28-lifecycle-completo.md +8 -0
  241. package/templates/template-manifest.json +354 -150
  242. package/templates/.agents/dashboard/apps/frontend/dist/assets/cost-monitor-D7PrDZy9.js +0 -68
  243. package/templates/.agents/dashboard/apps/frontend/dist/assets/index-OBJXZOwe.css +0 -1
  244. package/templates/.agents/dashboard/apps/frontend/dist/assets/index-Rbmpz4UO.js +0 -2
@@ -1,6 +1,10 @@
1
1
  import type { CostSummarySource } from "@zaip-ai/dashboard-cost-monitor/server";
2
2
  import { artifactStructuralRowsSql } from "./sqlite-artifact-counts.js";
3
3
  import { buildCostSummaryDetails } from "./sqlite-cost-summary-details.js";
4
+ import {
5
+ costSummaryElapsedByDay,
6
+ filteredCostSummaryElapsedByDay,
7
+ } from "./sqlite-cost-summary-elapsed.js";
4
8
  import type { SqliteQueryContext } from "./sqlite-query-context.js";
5
9
  import { costMonitorFilterDay } from "./sqlite-read-model-canonical.js";
6
10
  import type { SqliteReadModelFileSystem } from "./sqlite-read-model-filesystem.js";
@@ -11,21 +15,8 @@ export function queryCostMonitorSummaryAggregate(
11
15
  const { from, to, providers = [], models = [] } = filter;
12
16
  const sourceFrom = costMonitorFilterDay(from);
13
17
  const sourceTo = costMonitorFilterDay(to);
14
- const cacheKey = JSON.stringify({ ...filter, from: sourceFrom, to: sourceTo });
15
- const generationCached = context.costMonitorSummaryCache.get(cacheKey);
16
- if (generationCached !== undefined) return structuredClone(generationCached);
17
- const remember = (value: CostSummarySource) => {
18
- if (context.costMonitorSummaryCache.size >= 64) {
19
- const oldest = context.costMonitorSummaryCache.keys().next().value as string | undefined;
20
- if (oldest !== undefined) context.costMonitorSummaryCache.delete(oldest);
21
- }
22
- context.costMonitorSummaryCache.set(cacheKey, structuredClone(value));
23
- return structuredClone(value);
24
- };
25
- const unfilteredAllTime =
18
+ const noDimensionFilters =
26
19
  !filter.query &&
27
- sourceFrom === null &&
28
- sourceTo === null &&
29
20
  [
30
21
  providers,
31
22
  models,
@@ -41,7 +32,19 @@ export function queryCostMonitorSummaryAggregate(
41
32
  filter.risks,
42
33
  filter.origins,
43
34
  filter.workTypes,
44
- ].every((values) => values === undefined || values.length === 0);
35
+ ].every((items) => items === undefined || items.length === 0);
36
+ const cacheKey = JSON.stringify({ ...filter, from: sourceFrom, to: sourceTo });
37
+ const generationCached = context.costMonitorSummaryCache.get(cacheKey);
38
+ if (generationCached !== undefined) return structuredClone(generationCached);
39
+ const remember = (value: CostSummarySource) => {
40
+ if (context.costMonitorSummaryCache.size >= 64) {
41
+ const oldest = context.costMonitorSummaryCache.keys().next().value as string | undefined;
42
+ if (oldest !== undefined) context.costMonitorSummaryCache.delete(oldest);
43
+ }
44
+ context.costMonitorSummaryCache.set(cacheKey, structuredClone(value));
45
+ return structuredClone(value);
46
+ };
47
+ const unfilteredAllTime = noDimensionFilters && sourceFrom === null && sourceTo === null;
45
48
  if (unfilteredAllTime) {
46
49
  const cached = context.database
47
50
  .prepare(
@@ -55,27 +58,7 @@ export function queryCostMonitorSummaryAggregate(
55
58
  if (summary.filters_available !== undefined) return remember(summary);
56
59
  }
57
60
  }
58
- if (
59
- !filter.query &&
60
- sourceFrom !== null &&
61
- sourceTo !== null &&
62
- [
63
- providers,
64
- models,
65
- filter.agents,
66
- filter.phases,
67
- filter.statuses,
68
- filter.tasks,
69
- filter.specs,
70
- filter.prds,
71
- filter.executions,
72
- filter.changes,
73
- filter.priorities,
74
- filter.risks,
75
- filter.origins,
76
- filter.workTypes,
77
- ].every((values) => values === undefined || values.length === 0)
78
- ) {
61
+ if (noDimensionFilters && sourceFrom !== null && sourceTo !== null) {
79
62
  const cached = context.database
80
63
  .prepare(
81
64
  "SELECT payload_json FROM zaip_aios_activities WHERE payload_type='cost_monitor_summary_month_cache' ORDER BY recorded_at DESC LIMIT 1",
@@ -384,12 +367,25 @@ export function queryCostMonitorSummaryAggregate(
384
367
  );
385
368
  const financialByDay = new Map(dailyRows.map((row) => [String(row.key), row]));
386
369
  const structuralByDay = new Map(structuralRows.map((row) => [String(row.day), row]));
387
- const days = [...new Set([...financialByDay.keys(), ...structuralByDay.keys()])].sort();
370
+ // Financial events belong to their source start day. Elapsed wall time
371
+ // belongs to every local day actually crossed by its observed interval.
372
+ const elapsedByDay = noDimensionFilters
373
+ ? costSummaryElapsedByDay(context.database, sourceFrom, sourceTo)
374
+ : filteredCostSummaryElapsedByDay(
375
+ context.database,
376
+ sourceFrom,
377
+ sourceTo,
378
+ clauses.slice(2),
379
+ values.slice(4),
380
+ );
381
+ const days = [
382
+ ...new Set([...financialByDay.keys(), ...structuralByDay.keys(), ...elapsedByDay.keys()]),
383
+ ].sort();
388
384
  const compactDailyRows = days.map((day) => {
389
385
  const financial = financialByDay.get(day);
390
386
  const structural = structuralByDay.get(day);
391
387
  const eligible = number(financial?.eligible_runs);
392
- const knownDuration = number(financial?.duration_known);
388
+ const elapsed = elapsedByDay.get(day);
393
389
  return {
394
390
  key: day,
395
391
  prd_count: number(structural?.prd_count),
@@ -398,9 +394,12 @@ export function queryCostMonitorSummaryAggregate(
398
394
  validation_count: number(structural?.validation_count),
399
395
  evidence_count: number(structural?.evidence_count),
400
396
  total_run_count: eligible,
401
- total_duration: nullable(financial?.duration_ms),
402
- duration_known: number(financial?.duration_ms),
403
- duration_unknown: Math.max(0, eligible - knownDuration),
397
+ total_duration: elapsed?.known_runs ? elapsed.known_sum : null,
398
+ duration_known: elapsed?.known_sum ?? 0,
399
+ duration_unknown: Math.max(
400
+ 0,
401
+ (elapsed?.eligible_runs ?? eligible) - (elapsed?.known_runs ?? 0),
402
+ ),
404
403
  total_tokens: nullable(financial?.total_tokens),
405
404
  total_cost_usd: nullable(financial?.cost_usd),
406
405
  total_cost_brl: nullable(financial?.cost_brl),
@@ -447,19 +446,7 @@ export function queryCostMonitorSummaryAggregate(
447
446
  ];
448
447
  }),
449
448
  );
450
- const durations = Object.fromEntries(
451
- dailyRows.map((row) => {
452
- const value = metric(row);
453
- return [
454
- String(row.key),
455
- {
456
- known_sum: number(row.duration_ms),
457
- eligible_runs: value._duration_population.eligible_runs,
458
- known_runs: value._duration_population.known_runs,
459
- },
460
- ];
461
- }),
462
- );
449
+ const durations = Object.fromEntries(elapsedByDay);
463
450
  const { model_groups, filters_available } = buildCostSummaryDetails(
464
451
  context,
465
452
  selected,
@@ -4,6 +4,7 @@ import {
4
4
  operationsWorkListPayloadPage,
5
5
  operationsWorkListSqlPage,
6
6
  } from "./sqlite-operations-work-list.js";
7
+ import { hasVisibleProviderMetrics } from "./sqlite-provider-visibility.js";
7
8
  import { readAgentDimension } from "./sqlite-query-agent-dimension.js";
8
9
  import {
9
10
  readCachedCostViewPage,
@@ -13,6 +14,7 @@ import type { SqliteQueryContext, SqliteViewPage } from "./sqlite-query-context.
13
14
  import { readEntityGroup } from "./sqlite-query-entity-group.js";
14
15
  import { readFilteredDimension } from "./sqlite-query-filtered-dimension.js";
15
16
  import { readModelDimension } from "./sqlite-query-model-dimension.js";
17
+ import { attachOperationsDuration } from "./sqlite-query-operations-duration.js";
16
18
  import { readProviderDimension } from "./sqlite-query-provider-dimension.js";
17
19
  import { readTemporalDimension } from "./sqlite-query-temporal-dimension.js";
18
20
  import { costMonitorFilterDay } from "./sqlite-read-model-canonical.js";
@@ -239,7 +241,7 @@ export function queryUncachedViewPage(
239
241
  const visiblePayloadRows =
240
242
  dimension === "model"
241
243
  ? payloadRows.filter((row) => row.key !== "unavailable" && row.key !== null)
242
- : payloadRows;
244
+ : payloadRows.filter(hasVisibleProviderMetrics);
243
245
  const limit = Math.min(200, Math.max(1, input.limit ?? 50));
244
246
  const offset = Math.max(0, input.offset ?? 0);
245
247
  const rows = visiblePayloadRows.slice(offset, offset + limit);
@@ -268,9 +270,11 @@ export function queryUncachedViewPage(
268
270
  for (const field of modelNumericFields)
269
271
  row[field] = typeof row[field] === "number" ? row[field] : 0;
270
272
  }
273
+ // Provider visibility must not change factual totals or known-zero coverage.
274
+ const totalRows = dimension === "provider" ? payloadRows : visiblePayloadRows;
271
275
  const nullableSum = (field: string) =>
272
- visiblePayloadRows.some((row) => typeof row[field] === "number")
273
- ? visiblePayloadRows.reduce(
276
+ totalRows.some((row) => typeof row[field] === "number")
277
+ ? totalRows.reduce(
274
278
  (sum, row) => sum + (typeof row[field] === "number" ? Number(row[field]) : 0),
275
279
  0,
276
280
  )
@@ -284,7 +288,7 @@ export function queryUncachedViewPage(
284
288
  total_count: visiblePayloadRows.length,
285
289
  has_more: offset + rows.length < visiblePayloadRows.length,
286
290
  totals: {
287
- invocation_count: visiblePayloadRows.reduce(
291
+ invocation_count: totalRows.reduce(
288
292
  (sum, row) => sum + Number(row.invocation_count ?? 0),
289
293
  0,
290
294
  ),
@@ -490,60 +494,7 @@ export function queryUncachedViewPage(
490
494
  const rows = context.database
491
495
  .prepare(`SELECT * FROM ${view}${where} ORDER BY ${meta.order} LIMIT ? OFFSET ?`)
492
496
  .all(...values, limit, offset) as Array<Record<string, unknown>>;
493
- if (
494
- view === "operations_daily" ||
495
- view === "operations_monthly" ||
496
- view === "operations_annual"
497
- ) {
498
- // A partial date interval intersects a coarse month/year row but does
499
- // not make its all-time additive metrics applicable. Recompose it from
500
- // the same daily facts used by Operations cards.
501
- const partialCoarse = view !== "operations_daily" && (input.from || input.to);
502
- const partialFromDay = input.from?.slice(0, 10) ?? null;
503
- const partialToDay = input.to?.slice(0, 10) ?? null;
504
- const dailyRows = partialCoarse
505
- ? (context.database
506
- .prepare(
507
- `SELECT * FROM operations_daily WHERE (? IS NULL OR day >= ?) AND (? IS NULL OR day <= ?)`,
508
- )
509
- .all(partialFromDay, partialFromDay, partialToDay, partialToDay) as Array<
510
- Record<string, unknown>
511
- >)
512
- : [];
513
- const daily = context.executionDurationByDay({
514
- from: input.from ?? null,
515
- to: input.to ?? null,
516
- });
517
- for (const row of rows) {
518
- const key = String(row.day ?? row.period_key);
519
- if (partialCoarse) {
520
- const matching = dailyRows.filter((day) => String(day.day).startsWith(key));
521
- const matchingDays = matching.map((day) => String(day.day)).sort();
522
- row.start_day = matchingDays[0] ?? null;
523
- row.end_day = matchingDays.at(-1) ?? null;
524
- for (const column of [
525
- "prd_count",
526
- "spec_count",
527
- "task_count",
528
- "validation_count",
529
- "evidence_count",
530
- "total_run_count",
531
- "invocation_count",
532
- "llm_call_count",
533
- "total_tokens",
534
- "total_cost_usd",
535
- "total_cost_brl",
536
- ] as const)
537
- row[column] = matching.reduce(
538
- (sum, day) => sum + (typeof day[column] === "number" ? Number(day[column]) : 0),
539
- 0,
540
- );
541
- }
542
- row.operations_total_duration = [...daily]
543
- .filter(([day]) => (view === "operations_daily" ? day === key : day.startsWith(key)))
544
- .reduce((sum, [, duration]) => sum + duration, 0);
545
- }
546
- }
497
+ attachOperationsDuration(context, view, input, rows);
547
498
  return {
548
499
  data_source: "sqlite_read_model" as const,
549
500
  view,
@@ -26,11 +26,13 @@ import {
26
26
  } from "@zaip-ai/costs";
27
27
  import type { RunV2Fold, UsageRecordedV2 } from "../../../../../packages/decisions/src/index.js";
28
28
  import { saoPauloDay } from "./sqlite-calendar.js";
29
+ import {
30
+ MAX_RECONCILED_RUN_INTERVAL_MS,
31
+ MAX_SOURCE_INTERVAL_MS,
32
+ } from "./sqlite-read-model-schema.js";
29
33
 
30
34
  export { saoPauloDay } from "./sqlite-calendar.js";
31
35
 
32
- const MAX_RECONCILED_RUN_INTERVAL_MS = 2 * 3_600_000;
33
-
34
36
  export function saoPauloCurrentMonth(): { from: string; to: string } {
35
37
  const parts = new Intl.DateTimeFormat("en-CA", {
36
38
  timeZone: "America/Sao_Paulo",
@@ -362,9 +364,7 @@ export function factualDuration(record: UsageRecordedV2): boolean {
362
364
  export type ResolvedSourceInterval = { start: string; end: string; duration: number };
363
365
 
364
366
  /**
365
- * Reconciled usage is the closest canonical observation of when model work
366
- * happened. It wins over Run finalization elapsed time, which may include
367
- * orchestration/capture lag or be recorded much later.
367
+ * Observed source boundaries outrank delayed administrative Run finalization.
368
368
  */
369
369
  export function resolvedSourceInterval(
370
370
  records: readonly UsageRecordedV2[],
@@ -379,7 +379,7 @@ export function resolvedSourceInterval(
379
379
  Number.isFinite(startMs) &&
380
380
  Number.isFinite(endMs) &&
381
381
  endMs > startMs &&
382
- endMs - startMs <= MAX_RECONCILED_RUN_INTERVAL_MS
382
+ endMs - startMs <= MAX_SOURCE_INTERVAL_MS
383
383
  ? [{ start, end, startMs, endMs }]
384
384
  : [];
385
385
  });
@@ -390,7 +390,7 @@ export function resolvedSourceInterval(
390
390
  const end = intervals.reduce((latest, interval) =>
391
391
  interval.endMs > latest.endMs ? interval : latest,
392
392
  );
393
- return end.endMs - start.startMs <= MAX_RECONCILED_RUN_INTERVAL_MS
393
+ return end.endMs - start.startMs <= MAX_SOURCE_INTERVAL_MS
394
394
  ? { start: start.start, end: end.end, duration: end.endMs - start.startMs }
395
395
  : null;
396
396
  }
@@ -49,7 +49,7 @@ describe("SQLite dashboard read model", () => {
49
49
  expect.objectContaining({
50
50
  id: "SPEC-B",
51
51
  invocation_count: 2,
52
- total_duration: 1_800_000,
52
+ total_duration: 2_400_000,
53
53
  total_tokens: 4,
54
54
  total_cost_brl: 10,
55
55
  }),
@@ -189,13 +189,13 @@ describe("SQLite dashboard read model", () => {
189
189
  status_counts: { planning: 3 },
190
190
  classification_counts: { structured: 2 },
191
191
  total_run_count: 3,
192
- total_duration_ms: 2_400_000,
192
+ total_duration_ms: 3_000_000,
193
193
  total_tokens: 6,
194
194
  total_cost_brl: 15,
195
- average_duration_ms: 800_000,
195
+ average_duration_ms: 1_000_000,
196
196
  });
197
197
  expect(allTime.macro_metrics.specs.top_entities.slice(0, 2)).toMatchObject([
198
- { rank: 1, entity_id: "SPEC-B", total_duration_ms: 1_800_000 },
198
+ { rank: 1, entity_id: "SPEC-B", total_duration_ms: 2_400_000 },
199
199
  { rank: 2, entity_id: "SPEC-A", total_duration_ms: 600_000 },
200
200
  ]);
201
201
  expect(allTime.macro_metrics.prds).toMatchObject({
@@ -314,7 +314,7 @@ describe("SQLite dashboard read model", () => {
314
314
  expect(pagedDailyCostPage.totals).toMatchObject({
315
315
  total_tokens: dailyCostRows.reduce((sum, row) => sum + Number(row.total_tokens ?? 0), 0),
316
316
  total_cost_brl: dailyCostRows.reduce((sum, row) => sum + Number(row.total_cost_brl ?? 0), 0),
317
- total_duration: dailyCostRows.reduce((sum, row) => sum + Number(row.total_duration ?? 0), 0),
317
+ total_duration: null,
318
318
  });
319
319
  expect(annualCostPage).toMatchObject({
320
320
  total_count: 1,
@@ -130,10 +130,10 @@ describe("SQLite dashboard read model multi-provider", () => {
130
130
  expect(
131
131
  allAgentGroups.rows.reduce((total, row) => total + Number(row.total_cost_usd ?? 0), 0),
132
132
  ).toBe(allCostTotals.total_cost_usd);
133
- // Shared Cost Tempo is factual at the activity grain. It remains available
134
- // in the agent-table total even where a multi-dimension invocation cannot
135
- // be assigned to an individual Agent row.
136
- expect(allCostTotals.cost_monitor_total_duration).toBe(120_000);
133
+ // The calendar overview uses three observed one-minute source intervals.
134
+ // The agent table reports 120 seconds attributable to token-active facts;
135
+ // it does not assign the remaining source elapsed minute to an Agent.
136
+ expect(allCostTotals.cost_monitor_total_duration).toBe(180_000);
137
137
  expect(allAgentGroups.totals).toMatchObject({
138
138
  invocation_count: allCostTotals.total_run_count,
139
139
  total_duration: 120_000,
@@ -42,16 +42,10 @@ export async function rebuildSqliteReadModel(
42
42
  options: SqliteRebuildOptions = {},
43
43
  ): Promise<SqliteRebuildResult> {
44
44
  const path = sqliteReadModelPath(workspaceRoot);
45
- // Reuse only when schema and canonical fingerprint still match; otherwise rebuild.
45
+ // Startup serves a validated generation; the worker verifies canonical freshness.
46
46
  if (options.preferExisting && !options.forceCanonical) {
47
47
  try {
48
- validateSqliteReadModelSchema(path);
49
- const fingerprintStartedAt = performance.now();
50
- const sourceSnapshot = await sourceFingerprint(
51
- workspaceRoot,
52
- rebuildFingerprintOptions(path, options),
53
- );
54
- const fingerprint = `${sourceSnapshot.fingerprint}\ncurrent_day=${saoPauloDay(new Date().toISOString())}`;
48
+ validateSqliteReadModelSchema(path, { integrity: "quick" });
55
49
  const database = new DatabaseSync(path, { readOnly: true });
56
50
  try {
57
51
  if (hasReusableSnapshotPayloads(database)) {
@@ -60,38 +54,18 @@ export async function rebuildSqliteReadModel(
60
54
  "SELECT value FROM zaip_vw_read_model_metadata WHERE key = 'source_file_count'",
61
55
  )
62
56
  .get() as { value: string };
63
- const files = readSnapshotFileCount(
64
- path,
65
- fingerprint,
66
- readModelSchemaVersion,
67
- hasReusableSnapshotPayloads,
68
- );
69
- if (files !== null) {
70
- return {
71
- files,
72
- path,
73
- cache: "generation",
74
- fingerprint_ms: Math.round(performance.now() - fingerprintStartedAt),
75
- materialize_ms: 0,
76
- refresh: {
77
- reused_sources: sourceSnapshot.reusedFiles,
78
- processed_sources: sourceSnapshot.hashedFiles,
79
- added_sources: 0,
80
- changed_sources: 0,
81
- removed_sources: 0,
82
- canonical_runs_refolded: 0,
83
- },
84
- };
85
- }
57
+ const files = Number(row?.value);
58
+ if (!Number.isSafeInteger(files) || files < 0)
59
+ throw new Error("Invalid snapshot file count");
86
60
  return {
87
- files: Number(row.value),
61
+ files,
88
62
  path,
89
63
  cache: "stale_seed",
90
- fingerprint_ms: Math.round(performance.now() - fingerprintStartedAt),
64
+ fingerprint_ms: 0,
91
65
  materialize_ms: 0,
92
66
  refresh: {
93
- reused_sources: sourceSnapshot.reusedFiles,
94
- processed_sources: sourceSnapshot.hashedFiles,
67
+ reused_sources: 0,
68
+ processed_sources: 0,
95
69
  added_sources: 0,
96
70
  changed_sources: 0,
97
71
  removed_sources: 0,
@@ -7,7 +7,7 @@ export const dbRelativePath = ".local/sqlite/zaip-aios-dashboard.sqlite";
7
7
  // Schema 83 adds canonical priority, Run work type and reconciled macro stages.
8
8
  // Old snapshots must rebuild even when their canonical hashes still match.
9
9
  // Refresh persisted first-fold summaries after factual duration/coverage semantics change.
10
- export const readModelSchemaVersion = "84";
10
+ export const readModelSchemaVersion = "85";
11
11
  export const requiredPersistedTables = [
12
12
  "zaip_aios_activities",
13
13
  "zaip_dim_llm_model",
@@ -51,6 +51,8 @@ export const requiredViews = [
51
51
  "zaip_vw_operations_agent_invocations",
52
52
  ] as const;
53
53
  export const MAX_RECONCILED_RUN_INTERVAL_MS = 2 * 3_600_000;
54
+ /** Explicit source observations can span a workday; inferred Run pairs cannot. */
55
+ export const MAX_SOURCE_INTERVAL_MS = 48 * 3_600_000;
54
56
  export function sqliteReadModelPath(workspaceRoot: string): string {
55
57
  return resolve(process.env.ZAIP_SQLITE_DB_PATH ?? resolve(workspaceRoot, dbRelativePath));
56
58
  }
@@ -122,7 +124,10 @@ export function persistedSourceCheckpoint(
122
124
  * is intentionally independent of source fingerprints: a matching cache key
123
125
  * is not evidence that a partially copied or old-schema file is safe.
124
126
  */
125
- export function validateSqliteReadModelSchema(path: string): {
127
+ export function validateSqliteReadModelSchema(
128
+ path: string,
129
+ options: { integrity?: "full" | "quick" } = {},
130
+ ): {
126
131
  schemaVersion: string;
127
132
  tableCount: number;
128
133
  viewCount: number;
@@ -130,13 +135,12 @@ export function validateSqliteReadModelSchema(path: string): {
130
135
  } {
131
136
  const database = new DatabaseSync(path, { readOnly: true });
132
137
  try {
133
- const integrity = database.prepare("PRAGMA integrity_check").get() as {
134
- integrity_check?: string;
135
- };
136
- if (integrity.integrity_check !== "ok")
137
- throw new Error(
138
- `SQLite integrity check failed for ${path}: ${integrity.integrity_check ?? "unknown"}`,
139
- );
138
+ // Only startup opts into quick_check. Workers and qualification retain the
139
+ // full cross-index integrity check, independently of source cache reuse.
140
+ const pragma = options.integrity === "quick" ? "quick_check" : "integrity_check";
141
+ const integrity = database.prepare(`PRAGMA ${pragma}`).get() as Record<string, unknown>;
142
+ if (integrity[pragma] !== "ok")
143
+ throw new Error(`SQLite ${pragma} failed for ${path}: ${integrity[pragma] ?? "unknown"}`);
140
144
  const objects = database
141
145
  .prepare("SELECT name, type FROM sqlite_master WHERE type IN ('table', 'view', 'index')")
142
146
  .all() as Array<{ name: string; type: "table" | "view" | "index" }>;
@@ -3,6 +3,7 @@ import { createHash } from "node:crypto";
3
3
  import { readdir, readFile, stat } from "node:fs/promises";
4
4
  import { join, relative, resolve } from "node:path";
5
5
  import { promisify } from "node:util";
6
+ import { CanonicalSourceRevisionCache } from "./canonical-source-revision-cache.js";
6
7
 
7
8
  const execFileAsync = promisify(execFile);
8
9
 
@@ -87,23 +88,9 @@ export function isCodexManagedWorktreeRoot(root: string): boolean {
87
88
  return /[\\/]\.codex[\\/]worktrees(?:[\\/][^\\/]+){1,2}$/iu.test(root);
88
89
  }
89
90
 
90
- function canonicalRevision(content: string, source: string): [string, number, number] {
91
- if (source.endsWith(".jsonl")) {
92
- const dates = [...content.matchAll(/"occurred_at"\s*:\s*"([^"]+)"/gu)];
93
- return [
94
- dates.reduce((latest, match) => ((match[1] ?? "") > latest ? (match[1] ?? "") : latest), ""),
95
- dates.length,
96
- content.length,
97
- ];
98
- }
99
- const updated = /^updated_at:\s*([^\r\n]+)/mu.exec(content)?.[1]?.trim() ?? "";
100
- const created = /^created_at:\s*([^\r\n]+)/mu.exec(content)?.[1]?.trim() ?? "";
101
- const revision = Number(/^revision:\s*(\d+)/mu.exec(content)?.[1] ?? 0);
102
- return [updated || created, revision, content.length];
103
- }
104
-
105
91
  async function selectCanonicalSources(
106
92
  roots: readonly string[],
93
+ cache: CanonicalSourceRevisionCache,
107
94
  ): Promise<Array<{ path: string; source: string }>> {
108
95
  const locations = new Map<string, string[]>();
109
96
  for (const [index, root] of roots.entries()) {
@@ -149,36 +136,37 @@ async function selectCanonicalSources(
149
136
  const entries = [...locations.entries()];
150
137
  for (let offset = 0; offset < entries.length; offset += 32) {
151
138
  selected.push(
152
- ...(await Promise.all(
153
- entries.slice(offset, offset + 32).map(async ([source, paths]) => {
154
- const firstPath = paths[0];
155
- if (firstPath === undefined) throw new Error(`No canonical source for ${source}`);
156
- if (paths.length === 1) return { source, path: firstPath };
157
- // Worktree checkout mtimes do not represent canonical record recency.
158
- // Compare embedded revision timestamps and prefer the first registered
159
- // source on an exact tie, so repeated scans remain deterministic.
160
- const revisions = await Promise.all(
161
- paths.map(async (path) => canonicalRevision(await readFile(path, "utf8"), source)),
162
- );
163
- let winner = 0;
164
- for (let index = 1; index < paths.length; index += 1) {
165
- const left = revisions[index];
166
- const right = revisions[winner];
167
- if (left === undefined || right === undefined)
168
- throw new Error(`Incomplete canonical revisions for ${source}`);
169
- if (
170
- left[0] > right[0] ||
171
- (left[0] === right[0] &&
172
- (left[1] > right[1] || (left[1] === right[1] && left[2] > right[2])))
173
- )
174
- winner = index;
175
- }
176
- const selectedPath = paths[winner];
177
- if (selectedPath === undefined)
178
- throw new Error(`No selected canonical source for ${source}`);
179
- return { source, path: selectedPath };
180
- }),
181
- )),
139
+ ...(
140
+ await Promise.all(
141
+ entries.slice(offset, offset + 32).map(async ([source, paths]) => {
142
+ const firstPath = paths[0];
143
+ if (firstPath === undefined) throw new Error(`No canonical source for ${source}`);
144
+ if (paths.length === 1) return { source, path: firstPath };
145
+ // Worktree checkout mtimes do not represent canonical record recency.
146
+ // Compare embedded revision timestamps and prefer the first registered
147
+ // source on an exact tie, so repeated scans remain deterministic.
148
+ const revisions = await Promise.all(paths.map((path) => cache.read(path, source)));
149
+ let winner = revisions.findIndex((item) => item !== null);
150
+ if (winner < 0) return null;
151
+ for (let index = 1; index < paths.length; index += 1) {
152
+ const left = revisions[index]?.entry.revision;
153
+ const right = revisions[winner]?.entry.revision;
154
+ if (!left) continue;
155
+ if (!right) throw new Error(`Incomplete canonical revisions for ${source}`);
156
+ if (
157
+ left[0] > right[0] ||
158
+ (left[0] === right[0] &&
159
+ (left[1] > right[1] || (left[1] === right[1] && left[2] > right[2])))
160
+ )
161
+ winner = index;
162
+ }
163
+ const selectedPath = paths[winner];
164
+ if (selectedPath === undefined)
165
+ throw new Error(`No selected canonical source for ${source}`);
166
+ return { source, path: selectedPath };
167
+ }),
168
+ )
169
+ ).filter((item): item is { source: string; path: string } => item !== null),
182
170
  );
183
171
  }
184
172
  return selected.sort((left, right) => left.source.localeCompare(right.source));
@@ -192,9 +180,16 @@ export async function sourceFingerprint(
192
180
  candidates: SqliteReadModelSourceCandidate[];
193
181
  hashedFiles: number;
194
182
  reusedFiles: number;
183
+ duplicateReads: number;
184
+ duplicateReused: number;
195
185
  }> {
186
+ const revisionCache = await CanonicalSourceRevisionCache.open(
187
+ workspaceRoot,
188
+ options.verifyContent,
189
+ );
196
190
  const candidates = await selectCanonicalSources(
197
191
  options.sourceRoots ?? (await dashboardCanonicalSourceRoots(workspaceRoot)),
192
+ revisionCache,
198
193
  );
199
194
  const candidatesWithMetadata: SqliteReadModelSourceCandidate[] = [];
200
195
  let hashedFiles = 0;
@@ -261,6 +256,7 @@ export async function sourceFingerprint(
261
256
  ).filter((candidate): candidate is SqliteReadModelSourceCandidate => candidate !== null),
262
257
  );
263
258
  }
259
+ await revisionCache.persist();
264
260
  return {
265
261
  fingerprint: candidatesWithMetadata
266
262
  .map(
@@ -271,6 +267,8 @@ export async function sourceFingerprint(
271
267
  candidates: candidatesWithMetadata,
272
268
  hashedFiles,
273
269
  reusedFiles,
270
+ duplicateReads: revisionCache.reads,
271
+ duplicateReused: revisionCache.reused,
274
272
  };
275
273
  }
276
274
 
@@ -23,13 +23,7 @@ async function createSqliteTestRoot(): Promise<string> {
23
23
  afterEach(async () => {
24
24
  vi.useRealTimers();
25
25
  vi.restoreAllMocks();
26
- await Promise.all(
27
- roots
28
- .splice(0)
29
- .map((root) =>
30
- import("node:fs/promises").then(({ rm }) => rm(root, { recursive: true, force: true })),
31
- ),
32
- );
26
+ await Promise.all(roots.splice(0).map((root) => rm(root, { recursive: true, force: true })));
33
27
  });
34
28
  describe("SQLite dashboard read model", () => {
35
29
  it("materializes a fresh workspace without opening a nonexistent checkpoint database", async () => {
@@ -87,7 +81,7 @@ describe("SQLite dashboard read model", () => {
87
81
  filesystem.close();
88
82
  }
89
83
  });
90
- it("reuses an unchanged verified fingerprint as the current generation", async () => {
84
+ it("serves an unchanged validated snapshot as a stale seed pending canonical verification", async () => {
91
85
  const root = await createSqliteTestRoot();
92
86
  roots.push(root);
93
87
  await mkdir(join(root, ".agents/config"), { recursive: true });
@@ -95,7 +89,9 @@ describe("SQLite dashboard read model", () => {
95
89
  vi.spyOn(Date, "now").mockReturnValue(Date.now() + 5_000);
96
90
  await rebuildSqliteReadModel(root);
97
91
  await expect(rebuildSqliteReadModel(root, { preferExisting: true })).resolves.toMatchObject({
98
- cache: "generation",
92
+ cache: "stale_seed",
93
+ fingerprint_ms: 0,
94
+ materialize_ms: 0,
99
95
  });
100
96
  });
101
97
  it("rolls back a failed refresh including first-fold payloads while a reader remains open", async () => {
@@ -9,7 +9,11 @@ export type {
9
9
  SqliteFirstFoldMonthlyQuick,
10
10
  } from "./sqlite-read-model-first-fold-types.js";
11
11
  export { rebuildSqliteReadModel } from "./sqlite-read-model-rebuild.js";
12
- export { sqliteReadModelPath, validateSqliteReadModelSchema } from "./sqlite-read-model-schema.js";
12
+ export {
13
+ hasReusableSnapshotPayloads,
14
+ sqliteReadModelPath,
15
+ validateSqliteReadModelSchema,
16
+ } from "./sqlite-read-model-schema.js";
13
17
  export {
14
18
  type SqliteReadModelSourceCandidate,
15
19
  sourceFingerprint,