@zaip-ai/cli 4.5.0 → 4.6.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 (211) hide show
  1. package/README.md +2 -0
  2. package/dist/services/adapters.d.ts +1 -1
  3. package/dist/services/adapters.js +4 -4
  4. package/dist/services/adapters.js.map +1 -1
  5. package/dist/services/codex-native-tool-authority-template.d.ts.map +1 -1
  6. package/dist/services/codex-native-tool-authority-template.js +188 -9
  7. package/dist/services/codex-native-tool-authority-template.js.map +1 -1
  8. package/dist/services/codex-pre-tool-hook-template.d.ts.map +1 -1
  9. package/dist/services/codex-pre-tool-hook-template.js +50 -128
  10. package/dist/services/codex-pre-tool-hook-template.js.map +1 -1
  11. package/dist/services/codex-pre-tool-hook-template.test.js +4 -0
  12. package/dist/services/codex-pre-tool-hook-template.test.js.map +1 -1
  13. package/dist/services/codex-pre-tool-support-template.d.ts.map +1 -1
  14. package/dist/services/codex-pre-tool-support-template.js +1 -4
  15. package/dist/services/codex-pre-tool-support-template.js.map +1 -1
  16. package/dist/services/codex-stale-git-lock-template.d.ts +2 -0
  17. package/dist/services/codex-stale-git-lock-template.d.ts.map +1 -0
  18. package/dist/services/codex-stale-git-lock-template.js +64 -0
  19. package/dist/services/codex-stale-git-lock-template.js.map +1 -0
  20. package/dist/services/global-codex-config-migration.test.js +1 -4
  21. package/dist/services/global-codex-config-migration.test.js.map +1 -1
  22. package/dist/services/install-template-policy.d.ts.map +1 -1
  23. package/dist/services/install-template-policy.js +2 -0
  24. package/dist/services/install-template-policy.js.map +1 -1
  25. package/dist/services/logger.js +2 -1
  26. package/dist/services/logger.js.map +1 -1
  27. package/dist/services/logger.test.js +71 -0
  28. package/dist/services/logger.test.js.map +1 -1
  29. package/dist/services/runtime-template-test-support.d.ts +3 -0
  30. package/dist/services/runtime-template-test-support.d.ts.map +1 -0
  31. package/dist/services/runtime-template-test-support.js +32 -0
  32. package/dist/services/runtime-template-test-support.js.map +1 -0
  33. package/dist/services/runtime-template.test.js +23 -31
  34. package/dist/services/runtime-template.test.js.map +1 -1
  35. package/package.json +1 -1
  36. package/templates/.agents/README.md +8 -0
  37. package/templates/.agents/config/agents/discovery.json +1 -1
  38. package/templates/.agents/config/deploy-policy.json +5 -0
  39. package/templates/.agents/dashboard/.zaip-managed.json +211 -75
  40. package/templates/.agents/dashboard/README.md +2 -0
  41. package/templates/.agents/dashboard/apps/frontend/dist/assets/{CostCalculationGuidePage-f4ZGJRg3.js → CostCalculationGuidePage-BRtiPijr.js} +1 -1
  42. package/templates/.agents/dashboard/apps/frontend/dist/assets/{DashboardHelpDialog-CkQ_y8kg.js → DashboardHelpDialog-BR4ZeYpE.js} +99 -10
  43. package/templates/.agents/dashboard/apps/frontend/dist/assets/{PricingSettingsDialog-v9zGRTlk.js → PricingSettingsDialog-Dr4QDFmn.js} +1 -1
  44. package/templates/.agents/dashboard/apps/frontend/dist/assets/{ProjectSettingsPage-doIMhei7.js → ProjectSettingsPage-BG0N5dw_.js} +1 -1
  45. package/templates/.agents/dashboard/apps/frontend/dist/assets/{TraceabilityWorkspace-C_4ftjyB.js → TraceabilityWorkspace-BvoM-YQ0.js} +1 -1
  46. package/templates/.agents/dashboard/apps/frontend/dist/assets/cost-monitor-fTQhSL85.js +68 -0
  47. package/templates/.agents/dashboard/apps/frontend/dist/assets/{icons-vendor-Cv3WBFIF.js → icons-vendor-Bc6vux5t.js} +1 -1
  48. package/templates/.agents/dashboard/apps/frontend/dist/assets/index-BMfzKaAR.js +2 -0
  49. package/templates/.agents/dashboard/apps/frontend/dist/assets/index-DZhppOIk.css +1 -0
  50. package/templates/.agents/dashboard/apps/frontend/dist/assets/{react-vendor-mHgsmxb6.js → react-vendor-DNJQFov3.js} +1 -1
  51. package/templates/.agents/dashboard/apps/frontend/dist/assets/{traceability-B0CPyEVq.js → traceability-B7tjIB7W.js} +1 -1
  52. package/templates/.agents/dashboard/apps/frontend/dist/index.html +6 -6
  53. package/templates/.agents/dashboard/apps/frontend/package.json +1 -1
  54. package/templates/.agents/dashboard/apps/frontend/src/AppOverlays.tsx +6 -34
  55. package/templates/.agents/dashboard/apps/frontend/src/ReconciliationFeedback.dom.test.ts +130 -0
  56. package/templates/.agents/dashboard/apps/frontend/src/ReconciliationFeedback.tsx +226 -0
  57. package/templates/.agents/dashboard/apps/frontend/src/dashboard-api-sqlite-view.ts +45 -0
  58. package/templates/.agents/dashboard/apps/frontend/src/dashboard-api.test.ts +15 -0
  59. package/templates/.agents/dashboard/apps/frontend/src/dashboard-api.ts +17 -46
  60. package/templates/.agents/dashboard/apps/frontend/src/reconciliation-feedback-fixtures.ts +23 -0
  61. package/templates/.agents/dashboard/apps/frontend/src/reconciliation-feedback-model.test.ts +69 -0
  62. package/templates/.agents/dashboard/apps/frontend/src/reconciliation-feedback-model.ts +59 -0
  63. package/templates/.agents/dashboard/apps/frontend/src/reconciliation-feedback.css +131 -0
  64. package/templates/.agents/dashboard/apps/frontend/src/styles.css +0 -32
  65. package/templates/.agents/dashboard/apps/frontend/src/use-dashboard-app-model.tsx +2 -2
  66. package/templates/.agents/dashboard/modules/cost-monitor/package.json +1 -1
  67. package/templates/.agents/dashboard/modules/cost-monitor/src/react/CostMonitorPage.test.ts +2 -2
  68. package/templates/.agents/dashboard/modules/cost-monitor/src/react/CostMonitorPage.tsx +1 -1
  69. package/templates/.agents/dashboard/modules/operations/package.json +1 -1
  70. package/templates/.agents/dashboard/modules/operations/src/react/OperationsPage.tsx +1 -4
  71. package/templates/.agents/dashboard/modules/operations/src/react/UnifiedWorkBoard.navigation.test.ts +236 -0
  72. package/templates/.agents/dashboard/modules/operations/src/react/UnifiedWorkBoard.tsx +1 -24
  73. package/templates/.agents/dashboard/modules/operations/src/react/operations-overview-period-filter.test.ts +3 -2
  74. package/templates/.agents/dashboard/modules/traceability/package.json +1 -1
  75. package/templates/.agents/dashboard/packages/branding/package.json +1 -1
  76. package/templates/.agents/dashboard/packages/contracts/package.json +1 -1
  77. package/templates/.agents/dashboard/packages/data-export/package.json +1 -1
  78. package/templates/.agents/dashboard/packages/local-runtime/DIAGNOSTICS.md +42 -0
  79. package/templates/.agents/dashboard/packages/local-runtime/dashboard-diagnostics.d.mts +10 -0
  80. package/templates/.agents/dashboard/packages/local-runtime/dashboard-diagnostics.mjs +101 -0
  81. package/templates/.agents/dashboard/packages/local-runtime/dashboard-diagnostics.test.mjs +58 -0
  82. package/templates/.agents/dashboard/packages/local-runtime/dist/server.cjs +232 -217
  83. package/templates/.agents/dashboard/packages/local-runtime/dist/sqlite-read-model.cjs +201 -188
  84. package/templates/.agents/dashboard/packages/local-runtime/package.json +1 -1
  85. package/templates/.agents/dashboard/packages/local-runtime/scripts/test-sqlite-dashboard-startup.mjs +233 -0
  86. package/templates/.agents/dashboard/packages/local-runtime/scripts/validate-provider-nonzero.mjs +45 -0
  87. package/templates/.agents/dashboard/packages/local-runtime/scripts/verify-dashboard-executable-startup.mjs +107 -0
  88. package/templates/.agents/dashboard/packages/local-runtime/scripts/verify-dashboard-reconciliation-executable.mjs +271 -0
  89. package/templates/.agents/dashboard/packages/local-runtime/server.mjs +232 -217
  90. package/templates/.agents/dashboard/packages/local-runtime/src/active-codex-telemetry.test.ts +78 -12
  91. package/templates/.agents/dashboard/packages/local-runtime/src/active-codex-telemetry.ts +36 -56
  92. package/templates/.agents/dashboard/packages/local-runtime/src/canonical-source-revision-cache.test.ts +125 -0
  93. package/templates/.agents/dashboard/packages/local-runtime/src/canonical-source-revision-cache.ts +168 -0
  94. package/templates/.agents/dashboard/packages/local-runtime/src/dashboard-diagnostics.test.ts +57 -0
  95. package/templates/.agents/dashboard/packages/local-runtime/src/server-agent-catalog-routes.ts +76 -0
  96. package/templates/.agents/dashboard/packages/local-runtime/src/server-dual-reads-manifest-summaries-without-losing.test.ts +5 -0
  97. package/templates/.agents/dashboard/packages/local-runtime/src/server-reconciliation-diagnostics.test.ts +118 -0
  98. package/templates/.agents/dashboard/packages/local-runtime/src/server-reconciliation-result.test.ts +35 -6
  99. package/templates/.agents/dashboard/packages/local-runtime/src/server-reconciliation-route.ts +105 -0
  100. package/templates/.agents/dashboard/packages/local-runtime/src/server-snapshot-synchronization.test.ts +64 -9
  101. package/templates/.agents/dashboard/packages/local-runtime/src/server-snapshot-synchronization.ts +19 -27
  102. package/templates/.agents/dashboard/packages/local-runtime/src/server-telemetry-command.test.ts +111 -0
  103. package/templates/.agents/dashboard/packages/local-runtime/src/server-telemetry-refresh.test.ts +59 -0
  104. package/templates/.agents/dashboard/packages/local-runtime/src/server-telemetry-refresh.ts +44 -0
  105. package/templates/.agents/dashboard/packages/local-runtime/src/server-work-reconciliation.test.ts +20 -0
  106. package/templates/.agents/dashboard/packages/local-runtime/src/server-work-reconciliation.ts +12 -7
  107. package/templates/.agents/dashboard/packages/local-runtime/src/server-workspace.ts +48 -6
  108. package/templates/.agents/dashboard/packages/local-runtime/src/server.test.ts +2 -2
  109. package/templates/.agents/dashboard/packages/local-runtime/src/server.ts +126 -134
  110. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-cost-agent-page.ts +14 -2
  111. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-dashboard-startup.test.ts +163 -0
  112. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-dashboard-startup.ts +78 -0
  113. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-header-timestamp.test.ts +31 -0
  114. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-provider-nonzero.test.ts +271 -0
  115. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-provider-visibility.ts +32 -0
  116. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-query-operations-duration.ts +58 -0
  117. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-query-provider-dimension.ts +12 -3
  118. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-query-view-page.ts +9 -58
  119. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-read-model-rebuild.ts +9 -35
  120. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-read-model-schema.ts +10 -8
  121. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-read-model-source-fingerprint.ts +43 -45
  122. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-read-model.test.ts +5 -9
  123. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-read-model.ts +5 -1
  124. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-startup-seed.test.ts +87 -0
  125. package/templates/.agents/dashboard/packages/local-runtime/start-dashboard-logging.mjs +6 -2
  126. package/templates/.agents/dashboard/packages/local-runtime/start-dashboard-server.mjs +7 -1
  127. package/templates/.agents/dashboard/packages/local-runtime/start-dashboard.mjs +4 -5
  128. package/templates/.agents/dashboard/packages/ui/package.json +1 -1
  129. package/templates/.agents/dashboard/packages/ui/src/DashboardHeader.test.mjs +33 -4
  130. package/templates/.agents/dashboard/packages/ui/src/DashboardHeader.tsx +8 -7
  131. package/templates/.agents/dashboard/packages/windows-launcher/Program.cs +46 -3
  132. package/templates/.agents/dashboard/tests/e2e/package.json +1 -1
  133. package/templates/.agents/dashboard/tests/e2e/qualify-kanban-popup.mjs +56 -0
  134. package/templates/.agents/dashboard/tests/e2e/specs/dashboard-update-label.spec.ts +49 -0
  135. package/templates/.agents/design/.zaip-managed.json +17 -5
  136. package/templates/.agents/design/apps/catalog/dist/assets/index-D3uhpycC.js +72 -0
  137. package/templates/.agents/design/apps/catalog/dist/index.html +1 -1
  138. package/templates/.agents/design/apps/catalog/package.json +1 -1
  139. package/templates/.agents/design/apps/catalog/src/pages/ComponentsPage.tsx +13 -1
  140. package/templates/.agents/design/apps/catalog/src/specimens/ReconciliationSpecimen.stories.tsx +6 -0
  141. package/templates/.agents/design/apps/catalog/src/specimens/ReconciliationSpecimen.tsx +101 -0
  142. package/templates/.agents/design/packages/local-runtime/package.json +1 -1
  143. package/templates/.agents/requirements/autonomous-assurance.md +1 -1
  144. package/templates/.agents/requirements/cloud-hostinger-autonomy.md +1 -1
  145. package/templates/.agents/requirements/comparative-audit.md +1 -1
  146. package/templates/.agents/requirements/context-engine.md +1 -1
  147. package/templates/.agents/requirements/cost-observability.md +1 -1
  148. package/templates/.agents/requirements/dashboard-launch.md +1 -1
  149. package/templates/.agents/requirements/data-export.md +1 -1
  150. package/templates/.agents/requirements/design-system.md +1 -1
  151. package/templates/.agents/requirements/execution-engine.md +1 -1
  152. package/templates/.agents/requirements/kanban-dashboard.md +1 -1
  153. package/templates/.agents/requirements/model-orchestration.md +21 -6
  154. package/templates/.agents/requirements/public-distribution.md +4 -4
  155. package/templates/.agents/requirements/software-architecture.md +1 -1
  156. package/templates/.agents/requirements/strategic-improvements.md +1 -1
  157. package/templates/.agents/requirements/work-lifecycle.md +1 -1
  158. package/templates/.agents/requirements/zaip-strategic-95.md +1 -1
  159. package/templates/.agents/rules/core.md +3 -1
  160. package/templates/.agents/rules/workflow.md +3 -2
  161. package/templates/.agents/runtime/VERSION +1 -1
  162. package/templates/.agents/runtime/bin/zaip.mjs +366 -196
  163. package/templates/.agents/runtime/checksums.sha256 +5 -5
  164. package/templates/.agents/runtime/manifest.json +1 -1
  165. package/templates/.agents/runtime/provenance.json +4 -4
  166. package/templates/.agents/runtime/sbom.cdx.json +1 -1
  167. package/templates/.agents/skills/zaip-skill-config/SKILL.md +25 -3
  168. package/templates/.agents/skills/zaip-skill-execution/SKILL.md +4 -0
  169. package/templates/.agents/skills/zaip-skill-execution/references/autonomous-continuation.md +4 -0
  170. package/templates/.agents/skills/zaip-skill-execution/references/candidate-workflow.md +1 -1
  171. package/templates/.agents/skills/zaip-skill-execution/references/runtime-governance.md +2 -2
  172. package/templates/.agents/skills/zaip-skill-general-operations/SKILL.md +1 -0
  173. package/templates/.agents/skills/zaip-skill-github-deploy/SKILL.md +36 -48
  174. package/templates/.agents/skills/zaip-skill-github-deploy/agents/openai.yaml +2 -2
  175. package/templates/.agents/skills/zaip-skill-github-deploy/references/deployment-workflow.md +67 -99
  176. package/templates/.agents/skills/zaip-skill-github-deploy/references/release-identity.md +33 -30
  177. package/templates/.agents/skills/zaip-skill-github-deploy/scripts/deploy-contract.mjs +303 -0
  178. package/templates/.agents/skills/zaip-skill-github-deploy/scripts/deploy-contract.test.mjs +334 -0
  179. package/templates/.agents/skills/zaip-skill-github-deploy/scripts/github-deploy-contract.test.mjs +41 -28
  180. package/templates/.agents/skills/zaip-skill-github-deploy/scripts/guarded-commits.mjs +212 -0
  181. package/templates/.agents/skills/zaip-skill-lifecycle-closure/SKILL.md +4 -0
  182. package/templates/.agents/skills/zaip-skill-prd/SKILL.md +4 -0
  183. package/templates/.agents/skills/zaip-skill-release-qualification/SKILL.md +44 -68
  184. package/templates/.agents/skills/zaip-skill-release-qualification/references/local-preparation.md +91 -44
  185. package/templates/.agents/skills/zaip-skill-send-email/SKILL.md +15 -0
  186. package/templates/.agents/skills/zaip-skill-send-email/agents/openai.yaml +4 -0
  187. package/templates/.agents/skills/zaip-skill-setup/SKILL.md +5 -2
  188. package/templates/.agents/skills/zaip-skill-spec/SKILL.md +4 -0
  189. package/templates/.agents/skills/zaip-skill-task/SKILL.md +4 -0
  190. package/templates/.agents/skills/zaip-skill-testing-strategy/SKILL.md +19 -0
  191. package/templates/.agents/skills/zaip-skill-tool-setup/SKILL.md +2 -0
  192. package/templates/.agents/skills/zaip-skill-use-connectors/SKILL.md +15 -0
  193. package/templates/.agents/skills/zaip-skill-use-connectors/agents/openai.yaml +4 -0
  194. package/templates/.agents/skills/zaip-skill-use-connectors/scripts/connector-guidance.test.mjs +29 -0
  195. package/templates/.agents/skills/zaip-skill-use-connectors/scripts/verify-connector-lock.mjs +30 -0
  196. package/templates/.agents/tools/cloud-runner.integration.test.mjs +3 -0
  197. package/templates/.agents/zaip-aios/VERSION +1 -1
  198. package/templates/.agents/zaip-aios/guides/03-tasks.md +6 -0
  199. package/templates/.agents/zaip-aios/guides/04-specs.md +6 -0
  200. package/templates/.agents/zaip-aios/guides/05-prds.md +4 -0
  201. package/templates/.agents/zaip-aios/guides/06-execucao.md +6 -0
  202. package/templates/.agents/zaip-aios/guides/16-lifecycle-e-reconciliacao.md +6 -0
  203. package/templates/.agents/zaip-aios/guides/19-assurance-e-autonomia.md +4 -0
  204. package/templates/.agents/zaip-aios/guides/22-fluxos-e-roteamento.md +6 -0
  205. package/templates/.agents/zaip-aios/guides/23-orquestracao-de-modelos.md +36 -4
  206. package/templates/.agents/zaip-aios/guides/26-qualidade-e-release.md +15 -4
  207. package/templates/.agents/zaip-aios/guides/28-lifecycle-completo.md +8 -0
  208. package/templates/template-manifest.json +330 -142
  209. package/templates/.agents/dashboard/apps/frontend/dist/assets/cost-monitor-D7PrDZy9.js +0 -68
  210. package/templates/.agents/dashboard/apps/frontend/dist/assets/index-OBJXZOwe.css +0 -1
  211. package/templates/.agents/dashboard/apps/frontend/dist/assets/index-Rbmpz4UO.js +0 -2
@@ -1,4 +1,4 @@
1
- import { readFile } from "node:fs/promises";
1
+ import { randomUUID } from "node:crypto";
2
2
  import { basename, resolve } from "node:path";
3
3
  import { serve } from "@hono/node-server";
4
4
  import { serveStatic } from "@hono/node-server/serve-static";
@@ -6,24 +6,20 @@ import { WorkspaceFileSystem, type WorkspaceLayout } from "@zaip-ai/core";
6
6
  import { fxRateSchema, PricingRepository, pricingCatalogSchema } from "@zaip-ai/costs";
7
7
  import { type Context, Hono } from "hono";
8
8
  import { getCookie, setCookie } from "hono/cookie";
9
+ import { createDashboardDiagnostics } from "../dashboard-diagnostics.mjs";
9
10
  import {
10
11
  type ActiveCodexTelemetryCapture,
11
12
  captureRepositoryActiveCodexTelemetry,
12
13
  } from "./active-codex-telemetry.js";
13
14
  import { runAgentAdapterSync } from "./agent-adapter-sync.js";
14
- import {
15
- type AgentAdapterSyncResult,
16
- AgentCatalogUpdateError,
17
- persistAgentCatalogUpdate,
18
- readAgentCatalogSettings,
19
- } from "./agent-catalog-settings.js";
15
+ import type { AgentAdapterSyncResult } from "./agent-catalog-settings.js";
20
16
  import {
21
17
  persistProjectSettings,
22
18
  readProjectOverview,
23
19
  readProjectSettings,
24
20
  } from "./project-settings.js";
25
21
  import { createProjectionRefreshDiagnostics } from "./projection-refresh-diagnostics.js";
26
- import { agentPortraitFiles } from "./server-agent-assets.js";
22
+ import { registerAgentCatalogRoutes } from "./server-agent-catalog-routes.js";
27
23
  import { registerBrowserLeaseRoute } from "./server-browser-lease.js";
28
24
  import { registerCostsRoutes } from "./server-costs-routes.js";
29
25
  import {
@@ -36,12 +32,17 @@ import { lifecycleManifestState } from "./server-lifecycle-diagnostics.js";
36
32
  import { registerOperationsRoutes } from "./server-operations-routes.js";
37
33
  import { projectionPeriodFilter } from "./server-period.js";
38
34
  import { buildFirstFoldQuickProjection, dashboardSnapshot } from "./server-read-projections.js";
35
+ import {
36
+ createLoggedWorkReconciliation,
37
+ registerReconciliationRoute,
38
+ } from "./server-reconciliation-route.js";
39
39
  import { isAllowedDashboardAuthority } from "./server-request-authority.js";
40
40
  import { refreshSqliteSnapshotInWorker } from "./server-snapshot-refresh.js";
41
41
  import { readCommittedSnapshotRevision } from "./server-snapshot-revision.js";
42
42
  import { startSnapshotSynchronization } from "./server-snapshot-synchronization.js";
43
43
  import { writeReadModelStartup, writeStartupPhase } from "./server-startup.js";
44
44
  import { selectedCurrency } from "./server-summary-helpers.js";
45
+ import { createTelemetryProjectionRefresh } from "./server-telemetry-refresh.js";
45
46
  import { registerTraceRoutes } from "./server-trace-routes.js";
46
47
  import {
47
48
  defaultSessionToken,
@@ -50,20 +51,11 @@ import {
50
51
  isSafeUiPath,
51
52
  shouldBootstrapBrowserSession,
52
53
  } from "./server-ui-security.js";
53
- import {
54
- createWorkReconciliation,
55
- waitForWorkReconciliation,
56
- } from "./server-work-reconciliation.js";
57
54
  import { dashboardCanonicalSourceRoots } from "./sqlite-read-model-source-fingerprint.js";
58
55
 
59
56
  export { reconciliationNeedsRefresh } from "./server-work-reconciliation.js";
60
57
 
61
- import {
62
- dashboardIdentity,
63
- dashboardWorkspaceKey,
64
- ReconciliationCommandTimeoutError,
65
- reconcileOpenWork,
66
- } from "./server-workspace.js";
58
+ import { dashboardIdentity, dashboardWorkspaceKey, reconcileOpenWork } from "./server-workspace.js";
67
59
  import { rebuildSqliteReadModel, SqliteReadModelFileSystem } from "./sqlite-read-model.js";
68
60
 
69
61
  export { manifestRun, mergeManifestRuns } from "./manifest-runs.js";
@@ -95,6 +87,7 @@ interface DashboardAppOptions {
95
87
  snapshotReadOnly?: boolean;
96
88
  reconcileOpenWork?: typeof reconcileOpenWork;
97
89
  refreshReadModel?: () => Promise<void>;
90
+ captureTelemetry?: () => Promise<ActiveCodexTelemetryCapture>;
98
91
  snapshotState?: "current" | "updating";
99
92
  /** Enable filesystem and day rollover synchronization for the serving process. */
100
93
  watchSources?: boolean;
@@ -123,6 +116,8 @@ export function createDashboardApp(
123
116
  options: DashboardAppOptions = {},
124
117
  ): Hono {
125
118
  const app = new Hono();
119
+ const diagnostics = createDashboardDiagnostics(workspaceRoot, "runtime");
120
+ diagnostics.log("runtime_app_started", { snapshot_read_only: options.snapshotReadOnly });
126
121
  const filesystem = options.filesystem ?? new WorkspaceFileSystem(workspaceRoot);
127
122
  const isSqliteSnapshot = filesystem instanceof SqliteReadModelFileSystem;
128
123
  // Cache layout within a committed revision and reload after synchronization.
@@ -145,7 +140,6 @@ export function createDashboardApp(
145
140
  const workspace = resolve(workspaceRoot);
146
141
  const workspaceName = basename(workspace);
147
142
  const syncAgentAdapters = options.syncAgentAdapters ?? (() => runAgentAdapterSync(workspaceRoot));
148
- let catalogWriteTail = Promise.resolve();
149
143
  type DashboardSnapshot = Awaited<ReturnType<typeof dashboardSnapshot>>;
150
144
  const snapshotInFlight = new Map<string, Promise<DashboardSnapshot>>();
151
145
  const cachedSnapshots = new Map<string, { snapshot: DashboardSnapshot; cachedAt: number }>();
@@ -198,12 +192,17 @@ export function createDashboardApp(
198
192
  let activeCapture: ActiveCodexTelemetryCapture | null = null;
199
193
  let projectionRefreshInFlight: Promise<void> | null = null;
200
194
  let projectionRefreshRequested = false;
201
- const rebuildProjection = async () => {
195
+ const associatedRefreshJobs = new Set<string>();
196
+ const rebuildProjection = async (associatedJobId: string | null = null) => {
197
+ if (associatedJobId !== null) associatedRefreshJobs.add(associatedJobId);
202
198
  if (projectionRefreshInFlight !== null) {
203
199
  projectionRefreshRequested = true;
204
200
  return projectionRefreshInFlight;
205
201
  }
206
202
  snapshotState = "updating";
203
+ const refresh_id = randomUUID();
204
+ const job_id = associatedJobId;
205
+ diagnostics.log("projection_refresh_started", { refresh_id, job_id });
207
206
  projectionRefreshInFlight = (async () => {
208
207
  let passes = 0;
209
208
  do {
@@ -217,36 +216,57 @@ export function createDashboardApp(
217
216
  } while (projectionRefreshRequested && passes < 2);
218
217
  snapshotState = "current";
219
218
  projectionRefreshDiagnostics.clear();
220
- })().finally(() => {
221
- projectionRefreshInFlight = null;
222
- // Preserve a source change that arrived during the bounded catch-up.
223
- // It starts after the committed generation becomes observable, rather
224
- // than extending the current `updating` state without bound.
225
- if (projectionRefreshRequested) {
226
- projectionRefreshRequested = false;
227
- // Yield a real event-loop turn with `current` exposed before starting
228
- // another rebuild. `/ready` can then advertise the committed SQLite
229
- // generation and open tabs can read it coherently.
230
- const followUp = setTimeout(() => {
231
- void rebuildProjection().catch((error: unknown) =>
232
- projectionRefreshDiagnostics.record({ trigger: "source_change", error }),
233
- );
234
- }, 250);
235
- followUp.unref();
236
- }
237
- });
219
+ diagnostics.log("projection_refresh_completed", {
220
+ refresh_id,
221
+ job_id: [...associatedRefreshJobs][0] ?? job_id,
222
+ job_ids: [...associatedRefreshJobs],
223
+ projection_updated_at: isSqliteSnapshot ? filesystem.projectionUpdatedAt() : null,
224
+ latest_token_collected_at: isSqliteSnapshot ? filesystem.latestTokenCollectedAt() : null,
225
+ });
226
+ if (!projectionRefreshRequested) associatedRefreshJobs.clear();
227
+ })()
228
+ .catch((error: unknown) => {
229
+ // A failed promotion is still an older snapshot; leave its readiness
230
+ // explicit and retain evidence instead of silently swallowing failure.
231
+ projectionRefreshDiagnostics.record({ trigger: "source_change", error });
232
+ diagnostics.log("projection_refresh_failed", {
233
+ refresh_id,
234
+ job_id: [...associatedRefreshJobs][0] ?? job_id,
235
+ job_ids: [...associatedRefreshJobs],
236
+ error,
237
+ });
238
+ associatedRefreshJobs.clear();
239
+ throw error;
240
+ })
241
+ .finally(() => {
242
+ projectionRefreshInFlight = null;
243
+ // Preserve a source change that arrived during the bounded catch-up.
244
+ // It starts after the committed generation becomes observable, rather
245
+ // than extending the current `updating` state without bound.
246
+ if (projectionRefreshRequested) {
247
+ projectionRefreshRequested = false;
248
+ // Yield a real event-loop turn with `current` exposed before starting
249
+ // another rebuild. `/ready` can then advertise the committed SQLite
250
+ // generation and open tabs can read it coherently.
251
+ const followUp = setTimeout(() => {
252
+ void rebuildProjection().catch((error: unknown) =>
253
+ projectionRefreshDiagnostics.record({ trigger: "source_change", error }),
254
+ );
255
+ }, 250);
256
+ followUp.unref();
257
+ }
258
+ });
238
259
  return projectionRefreshInFlight;
239
260
  };
240
- startSnapshotSynchronization({
241
- workspaceRoot,
242
- discoverSourceRoots: () => dashboardCanonicalSourceRoots(workspaceRoot),
243
- staleSeed: isSqliteSnapshot && snapshotState === "updating",
244
- watchSources: isSqliteSnapshot && options.watchSources === true,
261
+ const telemetryRefresh = createTelemetryProjectionRefresh({
245
262
  refresh: rebuildProjection,
246
- ...(isSqliteSnapshot && options.watchSources === true
247
- ? { captureActiveTelemetry: () => captureRepositoryActiveCodexTelemetry(workspaceRoot) }
248
- : {}),
249
- onRefreshFailure: projectionRefreshDiagnostics.record,
263
+ ...(options.captureTelemetry !== undefined
264
+ ? { capture: options.captureTelemetry }
265
+ : isSqliteSnapshot &&
266
+ options.watchSources === true &&
267
+ process.env.ZAIP_DASHBOARD_WATCH_SOURCES !== "0"
268
+ ? { capture: () => captureRepositoryActiveCodexTelemetry(workspaceRoot) }
269
+ : {}),
250
270
  onCaptureSuccess: (result) => {
251
271
  activeCapture = result;
252
272
  activeCaptureFailure = null;
@@ -259,12 +279,29 @@ export function createDashboardApp(
259
279
  };
260
280
  },
261
281
  });
262
- const reconcileWork = createWorkReconciliation({
263
- reconcile: (mode) =>
282
+ const visibleSnapshotState = () =>
283
+ telemetryRefresh.pending || activeCaptureFailure !== null ? "updating" : snapshotState;
284
+ startSnapshotSynchronization({
285
+ workspaceRoot,
286
+ discoverSourceRoots: () => dashboardCanonicalSourceRoots(workspaceRoot),
287
+ staleSeed: isSqliteSnapshot && snapshotState === "updating",
288
+ watchSources: isSqliteSnapshot && options.watchSources === true,
289
+ refresh: rebuildProjection,
290
+ synchronizeTelemetry: telemetryRefresh.refresh,
291
+ onRefreshFailure: projectionRefreshDiagnostics.record,
292
+ });
293
+ const reconcileWork = createLoggedWorkReconciliation({
294
+ diagnostics,
295
+ reconcile: (mode, jobId) =>
264
296
  reconcileOpen(canonicalFilesystem, workspaceRoot, {
265
297
  hostTerminalOnly: mode === "host-terminal",
298
+ jobId,
266
299
  }),
267
- refresh: rebuildProjection,
300
+ snapshotTimes: () => ({
301
+ projection_updated_at: isSqliteSnapshot ? filesystem.projectionUpdatedAt() : null,
302
+ latest_token_collected_at: isSqliteSnapshot ? filesystem.latestTokenCollectedAt() : null,
303
+ }),
304
+ refresh: (jobId) => rebuildProjection(jobId),
268
305
  });
269
306
  // Terminal usage is persisted by the host/CLI lifecycle boundary. The
270
307
  // source synchronizer above projects those records, but the Dashboard must
@@ -331,7 +368,9 @@ export function createDashboardApp(
331
368
  const browserLeaseWrite =
332
369
  context.req.method === "GET" && context.req.path === `${API_PREFIX}/browser-lease`;
333
370
  const projectionRefreshWrite =
334
- context.req.method === "POST" && context.req.path === `${API_PREFIX}/refresh-projection`;
371
+ context.req.method === "POST" &&
372
+ (context.req.path === `${API_PREFIX}/refresh-projection` ||
373
+ context.req.path === `${API_PREFIX}/capture-telemetry`);
335
374
  if (
336
375
  context.req.method !== "GET" &&
337
376
  context.req.method !== "HEAD" &&
@@ -350,7 +389,8 @@ export function createDashboardApp(
350
389
  ? "GET, HEAD, PUT"
351
390
  : context.req.path === `${API_PREFIX}/reconcile-open-work`
352
391
  ? "POST"
353
- : context.req.path === `${API_PREFIX}/refresh-projection`
392
+ : context.req.path === `${API_PREFIX}/refresh-projection` ||
393
+ context.req.path === `${API_PREFIX}/capture-telemetry`
354
394
  ? "POST"
355
395
  : "GET, HEAD",
356
396
  );
@@ -408,7 +448,7 @@ export function createDashboardApp(
408
448
  lifecycle_manifest_state: manifestState,
409
449
  read_only: true,
410
450
  storage: isSqliteSnapshot ? filesystem.metadata() : { storage_mode: "filesystem" },
411
- snapshot_state: snapshotState,
451
+ snapshot_state: visibleSnapshotState(),
412
452
  projection_refresh_failure: projectionRefreshDiagnostics.failure,
413
453
  active_capture_failure: activeCaptureFailure,
414
454
  active_capture: activeCapture,
@@ -468,11 +508,13 @@ export function createDashboardApp(
468
508
  // monthly Operations projection was materialized during rebuild;
469
509
  // /ready must never start a competing projection in the background.
470
510
  day_count: metadata.daily_aggregate_count,
471
- snapshot_state: snapshotState,
511
+ snapshot_state: visibleSnapshotState(),
472
512
  projection_refresh_failure: projectionRefreshDiagnostics.failure,
473
513
  active_capture_failure: activeCaptureFailure,
474
514
  active_capture: activeCapture,
475
515
  snapshot_revision: revision,
516
+ projection_updated_at: filesystem.projectionUpdatedAt(),
517
+ latest_token_collected_at: filesystem.latestTokenCollectedAt(),
476
518
  });
477
519
  }
478
520
  const projection = await buildFirstFoldQuickProjection(filesystem, layout, {
@@ -518,48 +560,43 @@ export function createDashboardApp(
518
560
  readAggregate,
519
561
  clearSnapshotCache,
520
562
  });
521
- app.post(`${API_PREFIX}/reconcile-open-work`, async (context) => {
563
+ registerReconciliationRoute(app, {
564
+ readLayout,
565
+ diagnostics,
566
+ reconcileWork,
567
+ refresh: (jobId) => rebuildProjection(jobId),
568
+ snapshotState: visibleSnapshotState,
569
+ refreshCompleted: () =>
570
+ visibleSnapshotState() === "current" && projectionRefreshInFlight === null,
571
+ });
572
+ app.post(`${API_PREFIX}/refresh-projection`, async (context) => {
522
573
  try {
523
574
  await readLayout();
524
- // Share the canonical mutation queue with missed host-stop recovery.
525
- // Projection rebuilds run in the background and publish their revision.
526
- const reconciledIds = await waitForWorkReconciliation(reconcileWork("manual"), () => {
527
- void rebuildProjection().catch(() => undefined);
528
- });
529
- return context.json({
530
- reconciliation: reconciledIds,
531
- snapshot_state: snapshotState,
532
- projection_refresh_completed:
533
- snapshotState === "current" && projectionRefreshInFlight === null,
534
- });
535
- } catch (error) {
536
- if (error instanceof V2WorkspaceRequiredError) throw error;
537
- if (error instanceof ReconciliationCommandTimeoutError) {
538
- // SIGTERM can occur after the CLI persisted some records. Start an
539
- // atomic refresh without extending the request past the client's
540
- // deadline, and make that uncertainty explicit to the caller.
575
+ await telemetryRefresh.refresh(true);
576
+ if (activeCaptureFailure !== null)
541
577
  return context.json(
542
578
  {
543
- error: error.message,
544
- reconciliation_may_be_partial: true,
545
- projection_refresh_started: true,
579
+ error: "Telemetry capture is unavailable",
580
+ snapshot_state: visibleSnapshotState(),
581
+ active_capture_failure: activeCaptureFailure,
546
582
  },
547
- 408,
583
+ 503,
548
584
  );
549
- }
550
- return context.json({ error: error instanceof Error ? error.message : String(error) }, 400);
551
- }
552
- });
553
- app.post(`${API_PREFIX}/refresh-projection`, async (context) => {
554
- try {
555
- await readLayout();
556
- await rebuildProjection();
557
585
  return context.json(await readSnapshot(context));
558
586
  } catch (error) {
559
587
  if (error instanceof V2WorkspaceRequiredError) throw error;
560
588
  return context.json({ error: error instanceof Error ? error.message : String(error) }, 400);
561
589
  }
562
590
  });
591
+ app.post(`${API_PREFIX}/capture-telemetry`, async (context) => {
592
+ await readLayout();
593
+ void telemetryRefresh
594
+ .refresh()
595
+ .catch((error: unknown) =>
596
+ projectionRefreshDiagnostics.record({ trigger: "source_change", error }),
597
+ );
598
+ return context.json({ snapshot_state: visibleSnapshotState() }, 202);
599
+ });
563
600
  registerCostsRoutes(app, {
564
601
  filesystem,
565
602
  canonicalFilesystem,
@@ -599,56 +636,11 @@ export function createDashboardApp(
599
636
  clearSnapshotCache();
600
637
  return context.json(rate);
601
638
  });
602
- app.on(["GET", "HEAD"], `${API_PREFIX}/agent-catalog`, async (context) => {
603
- try {
604
- return headOrJson(context, await readAgentCatalogSettings(filesystem));
605
- } catch (error) {
606
- return headOrJson(
607
- context,
608
- { error: error instanceof Error ? error.message : String(error) },
609
- 400,
610
- );
611
- }
612
- });
613
- app.get(`${API_PREFIX}/agent-catalog-assets/:agentId`, async (context) => {
614
- const portrait = agentPortraitFiles[context.req.param("agentId")];
615
- if (portrait === undefined) return context.json({ error: "Agent portrait not found" }, 404);
616
- try {
617
- return context.body(
618
- await readFile(resolve(workspaceRoot, ".agents/config/src", portrait)),
619
- 200,
620
- { "cache-control": "no-store", "content-type": "image/png" },
621
- );
622
- } catch (error) {
623
- if ((error as NodeJS.ErrnoException).code === "ENOENT") {
624
- return context.json({ error: "Agent portrait not found" }, 404);
625
- }
626
- return context.json({ error: "Agent portrait could not be read" }, 500);
627
- }
628
- });
629
- app.put(`${API_PREFIX}/agent-catalog`, async (context) => {
630
- const update = async () => {
631
- try {
632
- const persisted = await persistAgentCatalogUpdate(
633
- filesystem,
634
- await context.req.json(),
635
- syncAgentAdapters,
636
- );
637
- clearSnapshotCache();
638
- return context.json(persisted);
639
- } catch (error) {
640
- if (error instanceof AgentCatalogUpdateError) {
641
- return context.json({ error: error.message }, error.status);
642
- }
643
- return context.json({ error: "Agent catalog update failed" }, 500);
644
- }
645
- };
646
- const result = catalogWriteTail.then(update, update);
647
- catalogWriteTail = result.then(
648
- () => undefined,
649
- () => undefined,
650
- );
651
- return result;
639
+ registerAgentCatalogRoutes(app, {
640
+ filesystem,
641
+ workspaceRoot,
642
+ syncAgentAdapters,
643
+ clearSnapshotCache,
652
644
  });
653
645
  app.on(["GET", "HEAD"], `${API_PREFIX}/project-settings`, async (context) => {
654
646
  try {
@@ -1,3 +1,5 @@
1
+ import { providerVisibleMetricsSql } from "./sqlite-provider-visibility.js";
2
+
1
3
  /**
2
4
  * Date-only Cost pages can use immutable, indexed activity facts. Any
3
5
  * additional filter deliberately falls back to the canonical SQL path, whose
@@ -181,8 +183,18 @@ export function readDateOnlyCostDimensionPage(
181
183
  dimension === "model"
182
184
  ? "COALESCE(task_population.task_count,0) AS task_count,0 AS prd_count,0 AS spec_count,0 AS validation_count,0 AS evidence_count"
183
185
  : "COALESCE(task_population.task_count,0) task_count";
186
+ const aggregatedRows = `${cte}, aggregated_rows AS (
187
+ SELECT keys.key,keys.key AS label,COALESCE(population.invocation_count,0) invocation_count,
188
+ ${rowProductivity},population.total_duration,population.source_event_count,
189
+ financial.total_tokens,financial.total_cost_usd,financial.total_cost_brl
190
+ FROM keys LEFT JOIN population USING(key) LEFT JOIN task_population USING(key)
191
+ LEFT JOIN financial USING(key)
192
+ ), visible_rows AS (SELECT * FROM aggregated_rows${
193
+ dimension === "provider" ? ` WHERE ${providerVisibleMetricsSql}` : ""
194
+ })`;
184
195
  const total_count = Number(
185
- database.prepare(`${cte} SELECT COUNT(*) AS count FROM keys`).get(...args)?.count ?? 0,
196
+ database.prepare(`${aggregatedRows} SELECT COUNT(*) AS count FROM visible_rows`).get(...args)
197
+ ?.count ?? 0,
186
198
  );
187
199
  const totals =
188
200
  database
@@ -192,7 +204,7 @@ export function readDateOnlyCostDimensionPage(
192
204
  .get(...args) ?? {};
193
205
  const rows = database
194
206
  .prepare(
195
- `${cte} SELECT keys.key,keys.key AS label,COALESCE(population.invocation_count,0) invocation_count,${rowProductivity},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 ?`,
207
+ `${aggregatedRows} SELECT * FROM visible_rows ORDER BY total_cost_brl DESC,key LIMIT ? OFFSET ?`,
196
208
  )
197
209
  .all(...args, limit, offset);
198
210
  if (dimension === "model") {
@@ -0,0 +1,163 @@
1
+ import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
2
+ import { dirname, resolve } from "node:path";
3
+ import { DatabaseSync } from "node:sqlite";
4
+ import { afterEach, describe, expect, it, vi } from "vitest";
5
+ import { prepareDashboardSqliteStartup } from "./sqlite-dashboard-startup.js";
6
+ import { rebuildSqliteReadModel } from "./sqlite-read-model-rebuild.js";
7
+ import { sqliteReadModelPath, validateSqliteReadModelSchema } from "./sqlite-read-model-schema.js";
8
+ import { sourceFingerprint } from "./sqlite-read-model-source-fingerprint.js";
9
+
10
+ vi.mock("./sqlite-read-model-source-fingerprint.js", async (importOriginal) => {
11
+ const actual = await importOriginal<typeof import("./sqlite-read-model-source-fingerprint.js")>();
12
+ return { ...actual, sourceFingerprint: vi.fn(actual.sourceFingerprint) };
13
+ });
14
+
15
+ const roots: string[] = [];
16
+ const taskContent = (title: string) =>
17
+ `---\nid: TASK-STARTUP\ntitle: ${title}\nstatus: ready\nrisk: standard\ncreated_at: 2026-08-01T12:00:00.000Z\n---\n`;
18
+
19
+ async function fixture(seed = true) {
20
+ vi.stubEnv("ZAIP_SQLITE_DB_PATH", undefined);
21
+ const fixtures = resolve(import.meta.dirname, "../../../../../.local/sqlite/tests");
22
+ await mkdir(fixtures, { recursive: true });
23
+ const root = await mkdtemp(resolve(fixtures, "startup-"));
24
+ roots.push(root);
25
+ const task = resolve(root, ".agents/records/tasks/task.md");
26
+ await mkdir(dirname(task), { recursive: true });
27
+ await mkdir(resolve(root, ".agents/config"), { recursive: true });
28
+ await writeFile(resolve(root, ".agents/config/project.yaml"), "zaip:\n enabled: true\n");
29
+ await writeFile(task, taskContent("Original seed"));
30
+ if (seed) await rebuildSqliteReadModel(root);
31
+ vi.mocked(sourceFingerprint).mockClear();
32
+ return { root, task, path: sqliteReadModelPath(root) };
33
+ }
34
+
35
+ function editDatabase(path: string, sql: string) {
36
+ const database = new DatabaseSync(path);
37
+ try {
38
+ database.exec(sql);
39
+ } finally {
40
+ database.close();
41
+ }
42
+ }
43
+
44
+ function persistedTask(path: string): string {
45
+ const database = new DatabaseSync(path, { readOnly: true });
46
+ try {
47
+ return (
48
+ database
49
+ .prepare("SELECT content FROM zaip_vw_files WHERE path=?")
50
+ .get(".agents/records/tasks/task.md") as { content: string }
51
+ ).content;
52
+ } finally {
53
+ database.close();
54
+ }
55
+ }
56
+
57
+ afterEach(async () => {
58
+ vi.unstubAllEnvs();
59
+ vi.mocked(sourceFingerprint).mockClear();
60
+ await Promise.all(roots.splice(0).map((root) => rm(root, { recursive: true, force: true })));
61
+ });
62
+
63
+ describe("dashboard SQLite startup", () => {
64
+ it("serves a validated stale seed without source inventory even after canonical changes", async () => {
65
+ const { root, task, path } = await fixture();
66
+ await writeFile(task, taskContent("Changed canonically"));
67
+ const before = await readFile(path);
68
+ for (let attempt = 0; attempt < 2; attempt++) {
69
+ await expect(prepareDashboardSqliteStartup(root)).resolves.toMatchObject({
70
+ path,
71
+ files: 2,
72
+ cache: "stale_seed",
73
+ fingerprint_ms: 0,
74
+ materialize_ms: 0,
75
+ refresh: { processed_sources: 0, reused_sources: 0, canonical_runs_refolded: 0 },
76
+ });
77
+ }
78
+ expect(sourceFingerprint).not.toHaveBeenCalled();
79
+ expect(await readFile(path)).toEqual(before);
80
+ expect(persistedTask(path)).toContain("Original seed");
81
+ // The normal refresh remains canonical and publishes the changed source.
82
+ await rebuildSqliteReadModel(root);
83
+ expect(sourceFingerprint).toHaveBeenCalled();
84
+ expect(persistedTask(path)).toContain("Changed canonically");
85
+ });
86
+
87
+ it.each([
88
+ [
89
+ "schema",
90
+ "UPDATE zaip_aios_activities SET payload_json=json_set(payload_json,'$.value','old') WHERE payload_type='metadata' AND json_extract(payload_json,'$.key')='schema_version'",
91
+ ],
92
+ ["index", "DROP INDEX zaip_activities_work_item_identity"],
93
+ [
94
+ "missing first fold",
95
+ "DELETE FROM zaip_aios_activities WHERE payload_type='first_fold_all_time_cache'",
96
+ ],
97
+ [
98
+ "truncated board",
99
+ "UPDATE zaip_aios_activities SET payload_json=json_set(payload_json,'$.pages.task',json('[]')) WHERE payload_type='first_fold_all_time_board_cache'",
100
+ ],
101
+ [
102
+ "malformed board",
103
+ "UPDATE zaip_aios_activities SET payload_json='invalid' WHERE payload_type='first_fold_all_time_board_cache'",
104
+ ],
105
+ [
106
+ "malformed cost summary",
107
+ "UPDATE zaip_aios_activities SET payload_json='invalid' WHERE payload_type='cost_monitor_summary_cache'",
108
+ ],
109
+ [
110
+ "invalid file count",
111
+ "UPDATE zaip_aios_activities SET payload_json=json_set(payload_json,'$.value','NaN') WHERE payload_type='metadata' AND json_extract(payload_json,'$.key')='source_file_count'",
112
+ ],
113
+ ])("rejects %s and rebuilds changed canonical sources", async (_damage, sql) => {
114
+ const { root, task, path } = await fixture();
115
+ editDatabase(path, sql);
116
+ await writeFile(task, taskContent("Recovered canonical source"));
117
+ await expect(prepareDashboardSqliteStartup(root)).resolves.toMatchObject({
118
+ path,
119
+ cache: "rebuilt",
120
+ });
121
+ expect(sourceFingerprint).toHaveBeenCalled();
122
+ expect(() => validateSqliteReadModelSchema(path)).not.toThrow();
123
+ expect(persistedTask(path)).toContain("Recovered canonical source");
124
+ });
125
+
126
+ it("builds canonical data when the seed is absent", async () => {
127
+ const { root, path } = await fixture(false);
128
+ await expect(prepareDashboardSqliteStartup(root)).resolves.toMatchObject({
129
+ path,
130
+ cache: "rebuilt",
131
+ });
132
+ expect(sourceFingerprint).toHaveBeenCalled();
133
+ expect(() => validateSqliteReadModelSchema(path)).not.toThrow();
134
+ });
135
+
136
+ it("accepts a genuinely empty committed model without fabricating source work", async () => {
137
+ const { root, task, path } = await fixture(false);
138
+ await rm(task);
139
+ await rm(resolve(root, ".agents/config/project.yaml"));
140
+ await expect(rebuildSqliteReadModel(root)).resolves.toMatchObject({ files: 0 });
141
+ vi.mocked(sourceFingerprint).mockClear();
142
+ await expect(prepareDashboardSqliteStartup(root)).resolves.toMatchObject({
143
+ path,
144
+ files: 0,
145
+ cache: "stale_seed",
146
+ fingerprint_ms: 0,
147
+ });
148
+ expect(sourceFingerprint).not.toHaveBeenCalled();
149
+ });
150
+
151
+ it("honors an explicit forced canonical rebuild", async () => {
152
+ const { root, task, path } = await fixture();
153
+ await writeFile(task, taskContent("Forced canonical source"));
154
+ await expect(
155
+ prepareDashboardSqliteStartup(root, { forceCanonical: true }),
156
+ ).resolves.toMatchObject({
157
+ path,
158
+ cache: "rebuilt",
159
+ });
160
+ expect(sourceFingerprint).toHaveBeenCalled();
161
+ expect(persistedTask(path)).toContain("Forced canonical source");
162
+ });
163
+ });