@zaip-ai/cli 4.9.0 → 5.1.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 (213) hide show
  1. package/dist/services/adapter-hook-sources.d.ts +3 -0
  2. package/dist/services/adapter-hook-sources.d.ts.map +1 -1
  3. package/dist/services/adapter-hook-sources.js +3 -0
  4. package/dist/services/adapter-hook-sources.js.map +1 -1
  5. package/dist/services/adapters.d.ts +1 -1
  6. package/dist/services/adapters.d.ts.map +1 -1
  7. package/dist/services/adapters.js +5 -2
  8. package/dist/services/adapters.js.map +1 -1
  9. package/dist/services/adapters.test.js +1 -1
  10. package/dist/services/adapters.test.js.map +1 -1
  11. package/dist/services/codex-conversation-lifecycle-hook.d.ts.map +1 -1
  12. package/dist/services/codex-conversation-lifecycle-hook.js +16 -6
  13. package/dist/services/codex-conversation-lifecycle-hook.js.map +1 -1
  14. package/dist/services/codex-hook-activation.d.ts +1 -1
  15. package/dist/services/codex-hook-activation.d.ts.map +1 -1
  16. package/dist/services/codex-hook-activation.js +116 -10
  17. package/dist/services/codex-hook-activation.js.map +1 -1
  18. package/dist/services/codex-pre-tool-cli-capture-template.d.ts +2 -0
  19. package/dist/services/codex-pre-tool-cli-capture-template.d.ts.map +1 -0
  20. package/dist/services/codex-pre-tool-cli-capture-template.js +104 -0
  21. package/dist/services/codex-pre-tool-cli-capture-template.js.map +1 -0
  22. package/dist/services/codex-pre-tool-cloud-template.d.ts +2 -0
  23. package/dist/services/codex-pre-tool-cloud-template.d.ts.map +1 -0
  24. package/dist/services/codex-pre-tool-cloud-template.js +190 -0
  25. package/dist/services/codex-pre-tool-cloud-template.js.map +1 -0
  26. package/dist/services/codex-pre-tool-dependencies.test-support.d.ts +2 -0
  27. package/dist/services/codex-pre-tool-dependencies.test-support.d.ts.map +1 -0
  28. package/dist/services/codex-pre-tool-dependencies.test-support.js +9 -0
  29. package/dist/services/codex-pre-tool-dependencies.test-support.js.map +1 -0
  30. package/dist/services/codex-pre-tool-hook-fixture.test-support.d.ts +5 -0
  31. package/dist/services/codex-pre-tool-hook-fixture.test-support.d.ts.map +1 -0
  32. package/dist/services/codex-pre-tool-hook-fixture.test-support.js +19 -0
  33. package/dist/services/codex-pre-tool-hook-fixture.test-support.js.map +1 -0
  34. package/dist/services/codex-pre-tool-hook-template-bootstrap.test.d.ts +2 -0
  35. package/dist/services/codex-pre-tool-hook-template-bootstrap.test.d.ts.map +1 -0
  36. package/dist/services/codex-pre-tool-hook-template-bootstrap.test.js +107 -0
  37. package/dist/services/codex-pre-tool-hook-template-bootstrap.test.js.map +1 -0
  38. package/dist/services/codex-pre-tool-hook-template.d.ts.map +1 -1
  39. package/dist/services/codex-pre-tool-hook-template.js +96 -210
  40. package/dist/services/codex-pre-tool-hook-template.js.map +1 -1
  41. package/dist/services/codex-pre-tool-hook-template.test.js +32 -80
  42. package/dist/services/codex-pre-tool-hook-template.test.js.map +1 -1
  43. package/dist/services/codex-pre-tool-host-context-template.d.ts +2 -0
  44. package/dist/services/codex-pre-tool-host-context-template.d.ts.map +1 -0
  45. package/dist/services/codex-pre-tool-host-context-template.js +139 -0
  46. package/dist/services/codex-pre-tool-host-context-template.js.map +1 -0
  47. package/dist/services/codex-pre-tool-support-template.d.ts.map +1 -1
  48. package/dist/services/codex-pre-tool-support-template.js +61 -101
  49. package/dist/services/codex-pre-tool-support-template.js.map +1 -1
  50. package/dist/services/codex-subagent-hook-template.d.ts +1 -1
  51. package/dist/services/codex-subagent-hook-template.d.ts.map +1 -1
  52. package/dist/services/codex-subagent-hook-template.js +13 -2
  53. package/dist/services/codex-subagent-hook-template.js.map +1 -1
  54. package/dist/services/codex-validation-runtime-template.d.ts.map +1 -1
  55. package/dist/services/codex-validation-runtime-template.js +10 -4
  56. package/dist/services/codex-validation-runtime-template.js.map +1 -1
  57. package/dist/services/runtime-template-test-support.d.ts +1 -0
  58. package/dist/services/runtime-template-test-support.d.ts.map +1 -1
  59. package/dist/services/runtime-template-test-support.js +23 -2
  60. package/dist/services/runtime-template-test-support.js.map +1 -1
  61. package/dist/services/runtime-template.test.js +4 -23
  62. package/dist/services/runtime-template.test.js.map +1 -1
  63. package/package.json +1 -1
  64. package/templates/.agents/README-ENG.md +58 -7
  65. package/templates/.agents/README.md +46 -10
  66. package/templates/.agents/config/agents/discovery.json +1 -1
  67. package/templates/.agents/dashboard/.zaip-managed.json +174 -86
  68. package/templates/.agents/dashboard/apps/frontend/dist/assets/{CostCalculationGuidePage-BW7Vy1cp.js → CostCalculationGuidePage-uR-asJv_.js} +1 -1
  69. package/templates/.agents/dashboard/apps/frontend/dist/assets/{DashboardHelpDialog-Cp_hEVd8.js → DashboardHelpDialog-CngnpoLL.js} +87 -13
  70. package/templates/.agents/dashboard/apps/frontend/dist/assets/{PricingSettingsDialog-Prcs-ydy.js → PricingSettingsDialog-Bszw3ekd.js} +1 -1
  71. package/templates/.agents/dashboard/apps/frontend/dist/assets/{ProjectSettingsPage-Cu40wAFF.js → ProjectSettingsPage-IJ1znG4w.js} +1 -1
  72. package/templates/.agents/dashboard/apps/frontend/dist/assets/{TraceabilityWorkspace-Bh3YBd3C.js → TraceabilityWorkspace-CsBpHqq_.js} +1 -1
  73. package/templates/.agents/dashboard/apps/frontend/dist/assets/cost-monitor-B0Jwz_rk.js +68 -0
  74. package/templates/.agents/dashboard/apps/frontend/dist/assets/{dashboard-localization-CF7B1Hjn.js → dashboard-localization-D41g5rdo.js} +58 -7
  75. package/templates/.agents/dashboard/apps/frontend/dist/assets/{icons-vendor-DOhy6D2-.js → icons-vendor-CjGGux6h.js} +1 -1
  76. package/templates/.agents/dashboard/apps/frontend/dist/assets/index-Bedlm0f7.js +2 -0
  77. package/templates/.agents/dashboard/apps/frontend/dist/assets/{react-vendor-CkPC-yMz.js → react-vendor-B9jZSAKf.js} +1 -1
  78. package/templates/.agents/dashboard/apps/frontend/dist/assets/{traceability-BEdGoIDm.js → traceability-DFpwb0TN.js} +1 -1
  79. package/templates/.agents/dashboard/apps/frontend/dist/index.html +5 -5
  80. package/templates/.agents/dashboard/apps/frontend/package.json +2 -2
  81. package/templates/.agents/dashboard/apps/frontend/src/AppDashboardContent.tsx +3 -0
  82. package/templates/.agents/dashboard/apps/frontend/src/AppOverlays.tsx +16 -7
  83. package/templates/.agents/dashboard/apps/frontend/src/__tests__/app-composition-support.ts +1 -0
  84. package/templates/.agents/dashboard/apps/frontend/src/cost-period-refresh.test.ts +1 -1
  85. package/templates/.agents/dashboard/apps/frontend/src/operations-period-render.test.ts +2 -0
  86. package/templates/.agents/dashboard/apps/frontend/src/use-app-trace-documents.navigation.test.tsx +125 -0
  87. package/templates/.agents/dashboard/apps/frontend/src/use-app-trace-documents.ts +67 -9
  88. package/templates/.agents/dashboard/apps/frontend/src/use-dashboard-app-model.tsx +15 -15
  89. package/templates/.agents/dashboard/apps/frontend/src/use-dashboard-locale.ts +18 -0
  90. package/templates/.agents/dashboard/modules/cost-monitor/package.json +1 -1
  91. package/templates/.agents/dashboard/modules/cost-monitor/src/contracts.ts +10 -3
  92. package/templates/.agents/dashboard/modules/cost-monitor/src/react/CostFilters.run-page.test.ts +5 -1
  93. package/templates/.agents/dashboard/modules/cost-monitor/src/react/CostGroupsTable.period-values.test.tsx +2 -2
  94. package/templates/.agents/dashboard/modules/cost-monitor/src/react/CostTables-keeps-legacy-codex-identity-fields-blank.test.ts +5 -5
  95. package/templates/.agents/dashboard/modules/cost-monitor/src/react/CostTables.test.ts +36 -43
  96. package/templates/.agents/dashboard/modules/cost-monitor/src/react/cost-monitor-loaders.ts +1 -0
  97. package/templates/.agents/dashboard/modules/cost-monitor/src/react/cost-table-formatters.ts +6 -4
  98. package/templates/.agents/dashboard/modules/cost-monitor/src/server/aggregate.ts +2 -2
  99. package/templates/.agents/dashboard/modules/cost-monitor/src/server/build-cost-monitor-projection.ts +1 -1
  100. package/templates/.agents/dashboard/modules/cost-monitor/src/server/build-cost-summary-projection.ts +11 -25
  101. package/templates/.agents/dashboard/modules/cost-monitor/src/server/cost-summary-defaults.ts +26 -0
  102. package/templates/.agents/dashboard/modules/operations/package.json +1 -1
  103. package/templates/.agents/dashboard/modules/operations/src/contracts.ts +11 -16
  104. package/templates/.agents/dashboard/modules/operations/src/planning-contracts.ts +14 -0
  105. package/templates/.agents/dashboard/modules/operations/src/react/DecisionTable.tsx +65 -0
  106. package/templates/.agents/dashboard/modules/operations/src/react/EntityTables.tsx +5 -71
  107. package/templates/.agents/dashboard/modules/operations/src/react/OperationsOverview.tsx +4 -39
  108. package/templates/.agents/dashboard/modules/operations/src/react/OperationsOverviewTopPanel.navigation.test.tsx +150 -0
  109. package/templates/.agents/dashboard/modules/operations/src/react/OperationsOverviewTopPanel.tsx +7 -3
  110. package/templates/.agents/dashboard/modules/operations/src/react/OperationsPage.tsx +14 -43
  111. package/templates/.agents/dashboard/modules/operations/src/react/OperationsPageOverview.tsx +3 -0
  112. package/templates/.agents/dashboard/modules/operations/src/react/operations-overview-contract.ts +51 -0
  113. package/templates/.agents/dashboard/modules/operations/src/react/operations-overview-period-filter.test.ts +12 -3
  114. package/templates/.agents/dashboard/modules/operations/src/react/operations-page-contract.ts +1 -0
  115. package/templates/.agents/dashboard/modules/operations/src/react/use-operations-default-range.ts +16 -0
  116. package/templates/.agents/dashboard/modules/operations/src/react/use-operations-filter-sync.ts +66 -0
  117. package/templates/.agents/dashboard/modules/traceability/package.json +1 -1
  118. package/templates/.agents/dashboard/packages/branding/package.json +1 -1
  119. package/templates/.agents/dashboard/packages/contracts/package.json +1 -1
  120. package/templates/.agents/dashboard/packages/data-export/package.json +1 -1
  121. package/templates/.agents/dashboard/packages/local-runtime/dashboard-build-fingerprint.mjs +87 -0
  122. package/templates/.agents/dashboard/packages/local-runtime/dashboard-build-fingerprint.test.mjs +48 -0
  123. package/templates/.agents/dashboard/packages/local-runtime/dist/server.cjs +193 -191
  124. package/templates/.agents/dashboard/packages/local-runtime/dist/sqlite-read-model.cjs +169 -169
  125. package/templates/.agents/dashboard/packages/local-runtime/package.json +1 -1
  126. package/templates/.agents/dashboard/packages/local-runtime/scripts/build-frontend.mjs +29 -0
  127. package/templates/.agents/dashboard/packages/local-runtime/scripts/build-sqlite-runtime.mjs +5 -0
  128. package/templates/.agents/dashboard/packages/local-runtime/server.mjs +193 -191
  129. package/templates/.agents/dashboard/packages/local-runtime/src/active-capture-failure.test.ts +38 -0
  130. package/templates/.agents/dashboard/packages/local-runtime/src/active-capture-failure.ts +35 -0
  131. package/templates/.agents/dashboard/packages/local-runtime/src/active-codex-telemetry.test.ts +53 -2
  132. package/templates/.agents/dashboard/packages/local-runtime/src/active-codex-telemetry.ts +18 -1
  133. package/templates/.agents/dashboard/packages/local-runtime/src/manifest-runs.ts +8 -2
  134. package/templates/.agents/dashboard/packages/local-runtime/src/server-capture-projection-order.test.ts +118 -0
  135. package/templates/.agents/dashboard/packages/local-runtime/src/server-cost-run-search.test.ts +90 -0
  136. package/templates/.agents/dashboard/packages/local-runtime/src/server-cost-run-search.ts +86 -0
  137. package/templates/.agents/dashboard/packages/local-runtime/src/server-operations-routes.ts +8 -1
  138. package/templates/.agents/dashboard/packages/local-runtime/src/server-projection-loaders.ts +13 -0
  139. package/templates/.agents/dashboard/packages/local-runtime/src/server-projections.ts +30 -74
  140. package/templates/.agents/dashboard/packages/local-runtime/src/server-route-handlers.ts +42 -0
  141. package/templates/.agents/dashboard/packages/local-runtime/src/server-routes.test.ts +60 -0
  142. package/templates/.agents/dashboard/packages/local-runtime/src/server-run-projections.test.ts +12 -2
  143. package/templates/.agents/dashboard/packages/local-runtime/src/server-telemetry-command.test.ts +29 -4
  144. package/templates/.agents/dashboard/packages/local-runtime/src/server-telemetry-refresh.ts +7 -0
  145. package/templates/.agents/dashboard/packages/local-runtime/src/server.test.ts +51 -54
  146. package/templates/.agents/dashboard/packages/local-runtime/src/server.ts +28 -45
  147. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-cost-filter-matrix.test.ts +5 -0
  148. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-cost-invocation-page.test.ts +49 -0
  149. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-cost-invocation-page.ts +44 -1
  150. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-query-cached-cost-page.ts +11 -10
  151. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-query-summary.ts +1 -1
  152. package/templates/.agents/dashboard/packages/local-runtime/src/sqlite-work-item-index.test.ts +18 -1
  153. package/templates/.agents/dashboard/packages/local-runtime/start-dashboard-build.mjs +12 -5
  154. package/templates/.agents/dashboard/packages/local-runtime/start-dashboard-paths.mjs +8 -0
  155. package/templates/.agents/dashboard/packages/local-runtime/start-dashboard.test.mjs +1 -0
  156. package/templates/.agents/dashboard/packages/ui/package.json +1 -1
  157. package/templates/.agents/dashboard/tests/e2e/package.json +1 -1
  158. package/templates/.agents/dashboard/tests/e2e/qualify-top-five-trace.mjs +53 -0
  159. package/templates/.agents/dashboard/tests/e2e/verify-top-five-served.mjs +78 -0
  160. package/templates/.agents/design/.zaip-managed.json +14 -6
  161. package/templates/.agents/design/apps/catalog/dist/assets/index-BGxXxXTo.js +72 -0
  162. package/templates/.agents/design/apps/catalog/dist/index.html +1 -1
  163. package/templates/.agents/design/apps/catalog/package.json +1 -1
  164. package/templates/.agents/design/apps/catalog/src/components/TopEntitiesSpecimen.tsx +45 -0
  165. package/templates/.agents/design/apps/catalog/src/pages/ComponentsPage.tsx +3 -1
  166. package/templates/.agents/design/apps/catalog/src/stories/Components.stories.tsx +2 -0
  167. package/templates/.agents/design/packages/local-runtime/package.json +1 -1
  168. package/templates/.agents/requirements/autonomous-assurance.md +11 -2
  169. package/templates/.agents/requirements/cloud-hostinger-autonomy.md +1 -1
  170. package/templates/.agents/requirements/comparative-audit.md +1 -1
  171. package/templates/.agents/requirements/context-engine.md +1 -1
  172. package/templates/.agents/requirements/cost-observability.md +1 -1
  173. package/templates/.agents/requirements/dashboard-launch.md +1 -1
  174. package/templates/.agents/requirements/data-export.md +1 -1
  175. package/templates/.agents/requirements/design-system.md +1 -1
  176. package/templates/.agents/requirements/execution-engine.md +1 -1
  177. package/templates/.agents/requirements/kanban-dashboard.md +1 -1
  178. package/templates/.agents/requirements/model-orchestration.md +1 -1
  179. package/templates/.agents/requirements/public-distribution.md +1 -1
  180. package/templates/.agents/requirements/software-architecture.md +1 -1
  181. package/templates/.agents/requirements/strategic-improvements.md +1 -1
  182. package/templates/.agents/requirements/work-lifecycle.md +2 -1
  183. package/templates/.agents/requirements/zaip-strategic-95.md +1 -1
  184. package/templates/.agents/rules/run-lifecycle-closure.md +2 -1
  185. package/templates/.agents/rules/workflow.md +1 -0
  186. package/templates/.agents/runtime/VERSION +1 -1
  187. package/templates/.agents/runtime/bin/zaip.mjs +1178 -764
  188. package/templates/.agents/runtime/checksums.sha256 +6 -6
  189. package/templates/.agents/runtime/manifest.json +1 -1
  190. package/templates/.agents/runtime/pnpm-lock.sha256 +1 -1
  191. package/templates/.agents/runtime/provenance.json +5 -5
  192. package/templates/.agents/runtime/sbom.cdx.json +59 -59
  193. package/templates/.agents/skills/zaip-skill-execution/SKILL.md +1 -1
  194. package/templates/.agents/skills/zaip-skill-execution/references/controlled-release-recovery.md +14 -0
  195. package/templates/.agents/skills/zaip-skill-execution/references/runtime-governance.md +1 -1
  196. package/templates/.agents/skills/zaip-skill-github-deploy/SKILL.md +5 -1
  197. package/templates/.agents/skills/zaip-skill-github-deploy/scripts/deploy-contract-cli.test.mjs +53 -0
  198. package/templates/.agents/skills/zaip-skill-github-deploy/scripts/deploy-contract.mjs +12 -0
  199. package/templates/.agents/skills/zaip-skill-github-deploy/scripts/deploy-contract.test.mjs +26 -32
  200. package/templates/.agents/skills/zaip-skill-maintenance-enter/SKILL.md +23 -0
  201. package/templates/.agents/skills/zaip-skill-maintenance-enter/agents/openai.yaml +4 -0
  202. package/templates/.agents/skills/zaip-skill-maintenance-enter/scripts/maintenance.mjs +213 -0
  203. package/templates/.agents/skills/zaip-skill-maintenance-enter/scripts/maintenance.test.mjs +207 -0
  204. package/templates/.agents/skills/zaip-skill-maintenance-exit/SKILL.md +21 -0
  205. package/templates/.agents/skills/zaip-skill-maintenance-exit/agents/openai.yaml +4 -0
  206. package/templates/.agents/skills/zaip-skill-release-qualification/SKILL.md +2 -0
  207. package/templates/.agents/skills/zaip-skill-release-qualification/references/local-preparation.md +37 -8
  208. package/templates/.agents/zaip-aios/VERSION +1 -1
  209. package/templates/.agents/zaip-aios/guides/10-seguranca.md +2 -2
  210. package/templates/.agents/zaip-aios/guides/26-qualidade-e-release.md +38 -0
  211. package/templates/template-manifest.json +259 -135
  212. package/templates/.agents/dashboard/apps/frontend/dist/assets/cost-monitor-Co2Jiuzh.js +0 -68
  213. package/templates/.agents/dashboard/apps/frontend/dist/assets/index-qVSJnJ0_.js +0 -2
@@ -0,0 +1,150 @@
1
+ import { createRequire } from "node:module";
2
+ import { fileURLToPath } from "node:url";
3
+ import type { Browser } from "@playwright/test";
4
+ import { afterAll, beforeAll, describe, expect, it } from "vitest";
5
+
6
+ declare global {
7
+ interface Window {
8
+ __topFiveNavigationProbe: {
9
+ tasks: string[];
10
+ documents: { kind: "prd" | "spec"; id: string }[];
11
+ };
12
+ }
13
+ }
14
+
15
+ const browserRequire = createRequire(
16
+ fileURLToPath(new URL("../../../../tests/e2e/package.json", import.meta.url)),
17
+ );
18
+ const { chromium, expect: expectPlaywright } = browserRequire("@playwright/test");
19
+
20
+ describe("Operations Top 5 navigation", () => {
21
+ let browser: Browser;
22
+ let bundle: string;
23
+
24
+ beforeAll(async () => {
25
+ const esbuild = browserRequire("esbuild");
26
+ const harness = await esbuild.build({
27
+ bundle: true,
28
+ define: { "import.meta.url": '"http://localhost/"' },
29
+ format: "iife",
30
+ loader: { ".css": "text" },
31
+ logLevel: "silent",
32
+ platform: "browser",
33
+ alias: {
34
+ "@zaip-ai/dashboard-ui": fileURLToPath(
35
+ new URL("../../../../packages/ui/src/index.ts", import.meta.url),
36
+ ),
37
+ react: fileURLToPath(
38
+ new URL(
39
+ "../../../../../../node_modules/.pnpm/react@19.2.7/node_modules/react",
40
+ import.meta.url,
41
+ ),
42
+ ),
43
+ "react-dom": fileURLToPath(
44
+ new URL(
45
+ "../../../../../../node_modules/.pnpm/react-dom@19.2.7_react@19.2.7/node_modules/react-dom",
46
+ import.meta.url,
47
+ ),
48
+ ),
49
+ },
50
+ stdin: {
51
+ contents: `
52
+ import React, { useState } from "react";
53
+ import { createRoot } from "react-dom/client";
54
+ import { OperationsOverviewTopPanel } from "./OperationsOverviewTopPanel.js";
55
+
56
+ const entities = {
57
+ tasks: [
58
+ { id: "TASK-TRACE-FIRST", title: "First task", durationMs: 120000, tokens: 1, costBrl: 1, runCount: 1 },
59
+ { id: "TASK-TRACE-SECOND", title: "Second task", durationMs: 60000, tokens: 1, costBrl: 1, runCount: 1 },
60
+ ],
61
+ specs: [
62
+ { id: "SPEC-TRACE-FIRST", title: "First spec", durationMs: 120000, tokens: 1, costBrl: 1, runCount: 1 },
63
+ { id: "SPEC-TRACE-SECOND", title: "Second spec", durationMs: 60000, tokens: 1, costBrl: 1, runCount: 1 },
64
+ ],
65
+ prds: [
66
+ { id: "PRD-TRACE-FIRST", title: "First prd", durationMs: 120000, tokens: 1, costBrl: 1, runCount: 1 },
67
+ { id: "PRD-TRACE-SECOND", title: "Second prd", durationMs: 60000, tokens: 1, costBrl: 1, runCount: 1 },
68
+ ],
69
+ };
70
+ const probe = { tasks: [], documents: [] };
71
+ window.__topFiveNavigationProbe = probe;
72
+
73
+ function Harness() {
74
+ const [view, setView] = useState("tasks");
75
+ return <>
76
+ <nav aria-label="Overview mode">
77
+ {Object.keys(entities).map((mode) => <button key={mode} type="button" onClick={() => setView(mode)}>{mode}</button>)}
78
+ </nav>
79
+ <OperationsOverviewTopPanel
80
+ copy={{ empty: "Empty", hint: "Duration", title: "Top 5" }}
81
+ currency="BRL" locale="en-US" maxDuration={120000}
82
+ onOpenTask={async (id) => { probe.tasks.push(id); return true; }}
83
+ onOpenMacro={(kind, id) => probe.documents.push({ kind, id })}
84
+ summary={{ durationMs: 180000, durationShare: 1, runCount: 2, tokens: 2 }}
85
+ text={{ accumulatedTime: "Time", associatedRuns: "runs", concentration: "Concentration", totalMeasuredTime: "Total", topFiveCost: "Cost", tokensUnavailable: "Unavailable" }}
86
+ topCost={2} topEntities={entities[view]} view={view}
87
+ />
88
+ </>;
89
+ }
90
+ createRoot(document.getElementById("root")).render(<Harness />);
91
+ `,
92
+ loader: "tsx",
93
+ resolveDir: fileURLToPath(new URL(".", import.meta.url)),
94
+ sourcefile: "OperationsOverviewTopPanel.navigation-harness.tsx",
95
+ },
96
+ write: false,
97
+ });
98
+ bundle = harness.outputFiles[0].text;
99
+ browser = await chromium.launch({ headless: true });
100
+ }, 60_000);
101
+
102
+ afterAll(async () => {
103
+ await browser?.close();
104
+ }, 60_000);
105
+
106
+ it("opens the selected Task, SPEC, and PRD after switching modes by mouse and keyboard", async () => {
107
+ const page = await browser.newPage();
108
+ const pageErrors: string[] = [];
109
+ page.on("pageerror", (error) => pageErrors.push(error.message));
110
+ try {
111
+ await page.setContent('<div id="root"></div>');
112
+ await page.addScriptTag({ content: bundle });
113
+ await expectPlaywright(page.locator(".operations-run-bar")).toHaveCount(2);
114
+
115
+ await page.getByRole("button", { name: /Open task details for Second task/ }).click();
116
+ await page
117
+ .getByRole("navigation", { name: "Overview mode" })
118
+ .getByRole("button", { name: "specs" })
119
+ .click();
120
+ const spec = page.getByRole("button", { name: /Open details for SPEC First spec/ });
121
+ await expectPlaywright(spec).toBeEnabled();
122
+ await spec.focus();
123
+ await page.keyboard.press("Enter");
124
+ await page
125
+ .getByRole("navigation", { name: "Overview mode" })
126
+ .getByRole("button", { name: "prds" })
127
+ .click();
128
+ const prd = page.getByRole("button", { name: /Open details for PRD Second prd/ });
129
+ await expectPlaywright(prd).toBeEnabled();
130
+ await prd.focus();
131
+ await page.keyboard.press("Space");
132
+ await page
133
+ .getByRole("navigation", { name: "Overview mode" })
134
+ .getByRole("button", { name: "tasks" })
135
+ .click();
136
+ await page.getByRole("button", { name: /Open task details for First task/ }).click();
137
+
138
+ expect(await page.evaluate(() => window.__topFiveNavigationProbe)).toEqual({
139
+ tasks: ["TASK-TRACE-SECOND", "TASK-TRACE-FIRST"],
140
+ documents: [
141
+ { kind: "spec", id: "SPEC-TRACE-FIRST" },
142
+ { kind: "prd", id: "PRD-TRACE-SECOND" },
143
+ ],
144
+ });
145
+ expect(pageErrors).toEqual([]);
146
+ } finally {
147
+ await page.close();
148
+ }
149
+ }, 60_000);
150
+ });
@@ -18,6 +18,7 @@ export function OperationsOverviewTopPanel({
18
18
  currency,
19
19
  locale,
20
20
  maxDuration,
21
+ onOpenMacro,
21
22
  onOpenTask,
22
23
  summary,
23
24
  text,
@@ -30,6 +31,7 @@ export function OperationsOverviewTopPanel({
30
31
  currency: "BRL" | "USD";
31
32
  locale: OperationsLocale;
32
33
  maxDuration: number;
34
+ onOpenMacro: (kind: "prd" | "spec", id: string) => void;
33
35
  onOpenTask: (taskId: string) => Promise<boolean>;
34
36
  summary: {
35
37
  durationMs: number | null;
@@ -68,14 +70,16 @@ export function OperationsOverviewTopPanel({
68
70
  const label =
69
71
  view === "tasks"
70
72
  ? `${locale === "pt-BR" ? "Abrir detalhes da tarefa" : "Open task details for"} ${entity.title}: ${measuredDuration}`
71
- : `${entityKind} ${entity.title}: ${measuredDuration}`;
73
+ : `${locale === "pt-BR" ? "Abrir detalhes da" : "Open details for"} ${entityKind} ${entity.title}: ${measuredDuration}`;
72
74
  return (
73
75
  <button
74
76
  aria-label={label}
75
77
  className={`operations-run-bar${entity.durationMs === null ? " is-duration-unavailable" : ""}`}
76
- disabled={view !== "tasks"}
77
78
  key={entity.id}
78
- onClick={() => void onOpenTask(entity.id)}
79
+ onClick={() => {
80
+ if (view === "tasks") void onOpenTask(entity.id);
81
+ else onOpenMacro(view === "specs" ? "spec" : "prd", entity.id);
82
+ }}
79
83
  title={`${entity.title}: ${measuredDuration}`}
80
84
  type="button"
81
85
  >
@@ -20,6 +20,8 @@ import {
20
20
  workListTypeOnViewChange,
21
21
  } from "./operations-page-contract.js";
22
22
  import { useOperationFilterFeedback } from "./use-operation-filter-feedback.js";
23
+ import { useOperationsDefaultRange } from "./use-operations-default-range.js";
24
+ import { useOperationsFilterSync } from "./use-operations-filter-sync.js";
23
25
 
24
26
  export {
25
27
  type DashboardGlobalFilters,
@@ -85,6 +87,7 @@ export function OperationsPage({
85
87
  locale,
86
88
  onLocaleChange,
87
89
  onOpenDocument,
90
+ onOpenMacro,
88
91
  onOpenTask,
89
92
  onPriorityChange,
90
93
  onLoadMacroMetrics,
@@ -124,13 +127,7 @@ export function OperationsPage({
124
127
  const [originFilter, setOriginFilter] = useState<TaskOrigin | "">("");
125
128
  const { filterLoading, showFilterLoading } = useOperationFilterFeedback();
126
129
  const searchInputRef = useRef<HTMLInputElement>(null);
127
- const defaultDateRange = useMemo(() => {
128
- const operationsRange = initialOperationsDateRange(
129
- data.tasks,
130
- Object.keys(data.summary.periods?.daily ?? {}),
131
- );
132
- return factualCostDateRange(factualCostPeriods?.day, operationsRange);
133
- }, [data.summary.periods, data.tasks, factualCostPeriods?.day]);
130
+ const defaultDateRange = useOperationsDefaultRange(data, factualCostPeriods);
134
131
  const initialDateRange = useMemo(() => dateRangeForPreset("this-month"), []);
135
132
  const [localDateRange, setLocalDateRange] = useState(() => initialDateRange);
136
133
  const [localDatePreset, setLocalDatePreset] = useState<OperationDatePreset>("all");
@@ -179,43 +176,17 @@ export function OperationsPage({
179
176
  setRiskFilter("");
180
177
  setOriginFilter("");
181
178
  };
182
- useEffect(() => {
183
- // The parent replaces the aggregate snapshot while a period transition is
184
- // loading. Defer paged entity hydration until that replacement is stable,
185
- // otherwise the aggregate response can overwrite a newer entity page.
186
- if (loading) return;
187
- if (view === "work-kanban" || view === "work-list") {
188
- const timeout = window.setTimeout(() => {
189
- const filters = {
190
- query,
191
- status: statusFilter,
192
- risk: riskFilter,
193
- origin: originFilter,
194
- };
195
- if (view === "work-kanban") onBoardFiltersChange?.(filters);
196
- else onWorkListFiltersChange?.(filters);
197
- }, 180);
198
- return () => window.clearTimeout(timeout);
199
- }
200
- if (!["runs", "conversations", "executions", "changes", "decisions"].includes(view)) return;
201
- const timeout = window.setTimeout(() => {
202
- onEntityFiltersChange?.(
203
- view as "runs" | "conversations" | "executions" | "changes" | "decisions",
204
- { query, status: statusFilter, risk: riskFilter },
205
- );
206
- }, 180);
207
- return () => window.clearTimeout(timeout);
208
- }, [
209
- onBoardFiltersChange,
210
- onWorkListFiltersChange,
211
- onEntityFiltersChange,
179
+ useOperationsFilterSync({
212
180
  loading,
213
- originFilter,
181
+ view,
214
182
  query,
215
- riskFilter,
216
183
  statusFilter,
217
- view,
218
- ]);
184
+ riskFilter,
185
+ originFilter,
186
+ onBoardFiltersChange,
187
+ onWorkListFiltersChange,
188
+ onEntityFiltersChange,
189
+ });
219
190
  // Only work views interpret a status option as a canonical work stage.
220
191
  // Runs, Changes and Executions use their respective persisted statuses.
221
192
  const hasEntityStageStatus =
@@ -329,6 +300,7 @@ export function OperationsPage({
329
300
  (row) =>
330
301
  row.entrypoint === "conversation" &&
331
302
  row.task_id === null &&
303
+ row.conversation_id !== null &&
332
304
  conversationIds.has(row.conversation_id),
333
305
  );
334
306
  const decisions = data.decisions.filter(
@@ -657,6 +629,7 @@ export function OperationsPage({
657
629
  locale={locale}
658
630
  onOpenTask={onOpenTask}
659
631
  onOpenDocument={onOpenDocument}
632
+ onOpenMacro={onOpenMacro}
660
633
  onLoadStatus={onLoadStatusPage}
661
634
  prds={prds}
662
635
  query={query}
@@ -756,5 +729,3 @@ export function OperationsPage({
756
729
  </main>
757
730
  );
758
731
  }
759
-
760
- /** Loading is never a valid zero-valued Operations projection. */
@@ -25,6 +25,7 @@ export function OperationsPageOverview({
25
25
  locale,
26
26
  onOpenTask,
27
27
  onOpenDocument,
28
+ onOpenMacro,
28
29
  onLoadStatus,
29
30
  statusPage,
30
31
  onLoadStatusPage,
@@ -55,6 +56,7 @@ export function OperationsPageOverview({
55
56
  locale: OperationsLocale;
56
57
  onOpenTask: OverviewProps["onOpenTask"];
57
58
  onOpenDocument: OverviewProps["onOpenDocument"];
59
+ onOpenMacro: OverviewProps["onOpenMacro"];
58
60
  onLoadStatus: OverviewProps["onLoadStatus"];
59
61
  statusPage: OverviewProps["statusPage"];
60
62
  onLoadStatusPage: OverviewProps["onLoadStatusPage"];
@@ -96,6 +98,7 @@ export function OperationsPageOverview({
96
98
  }}
97
99
  onOpenTask={onOpenTask}
98
100
  onOpenDocument={onOpenDocument}
101
+ onOpenMacro={onOpenMacro}
99
102
  onLoadStatus={onLoadStatus}
100
103
  statusPage={statusPage}
101
104
  onLoadStatusPage={onLoadStatusPage}
@@ -0,0 +1,51 @@
1
+ import type { OperationsProjection } from "../contracts.js";
2
+ import type { OperationDatePreset, OperationDateRange } from "./operation-date-range.js";
3
+ import type {
4
+ FactualCostFact,
5
+ OverviewClassification,
6
+ OverviewClassificationPage,
7
+ OverviewStatusKind,
8
+ OverviewStatusPage,
9
+ } from "./operations-page-contract.js";
10
+ import type { OperationsLocale } from "./status-labels.js";
11
+ import type { WorkStage } from "./work-board-model.js";
12
+ export interface OperationsOverviewProps {
13
+ currency: "BRL" | "USD";
14
+ data: OperationsProjection;
15
+ /** App-controlled range paired with datePreset; retained by Page while loading. */
16
+ dateRange?: OperationDateRange;
17
+ datePreset: OperationDatePreset;
18
+ locale: OperationsLocale;
19
+ onDatePresetChange: (preset: OperationDatePreset) => void;
20
+ onOpenTask: (taskId: string) => Promise<boolean>;
21
+ onOpenDocument: (kind: "prd" | "spec", id: string) => void;
22
+ onOpenMacro: (kind: "prd" | "spec", id: string) => void;
23
+ onLoadStatus?: ((stage: WorkStage, kind: OverviewStatusKind, offset: number) => void) | undefined;
24
+ statusPage?:
25
+ | ((stage: WorkStage, kind: OverviewStatusKind) => OverviewStatusPage | undefined)
26
+ | undefined;
27
+ onLoadStatusPage?:
28
+ | ((stage: WorkStage, kind: OverviewStatusKind, offset: number) => void)
29
+ | undefined;
30
+ classificationPage?:
31
+ | ((
32
+ kind: OverviewStatusKind,
33
+ classification: OverviewClassification,
34
+ ) => OverviewClassificationPage | undefined)
35
+ | undefined;
36
+ onLoadClassificationPage?:
37
+ | ((kind: OverviewStatusKind, classification: OverviewClassification, offset: number) => void)
38
+ | undefined;
39
+ onLoadMacroMetrics?: (() => void) | undefined;
40
+ prds?: OperationsProjection["prds"];
41
+ runs?: OperationsProjection["runs"];
42
+ specs?: OperationsProjection["specs"];
43
+ tasks?: OperationsProjection["tasks"];
44
+ /** Factual Cost Monitor snapshot total for the selected locale/currency. */
45
+ factualCostTotal?: number | null | undefined;
46
+ /** Measurement completeness for the matching Cost Monitor snapshot. */
47
+ factualCostFact?: FactualCostFact | undefined;
48
+ costLoading?: boolean | undefined;
49
+ resolveRunCost?: ((runId: string) => number | null) | undefined;
50
+ includeLegacyEffort?: boolean;
51
+ }
@@ -118,7 +118,11 @@ describe("overview period filter", () => {
118
118
  const periods = readFileSync(new URL("PeriodSummaryTable.tsx", import.meta.url), "utf8");
119
119
  const costCard = readFileSync(new URL("OverviewCostKpi.tsx", import.meta.url), "utf8");
120
120
 
121
- expect(source).toContain('currency: "BRL" | "USD"');
121
+ const contract = readFileSync(
122
+ new URL("operations-overview-contract.ts", import.meta.url),
123
+ "utf8",
124
+ );
125
+ expect(contract).toContain('currency: "BRL" | "USD"');
122
126
  expect(source).toContain("const selectedCurrencyCost =");
123
127
  expect(source).toContain("formatCurrency(value, currency, locale)");
124
128
  expect(source).toContain("<OverviewCostKpi");
@@ -188,13 +192,18 @@ describe("overview period filter", () => {
188
192
 
189
193
  it("resets the real List navigation callback on re-entry without changing Kanban routing", () => {
190
194
  const source = readFileSync(new URL("OperationsPage.tsx", import.meta.url), "utf8");
195
+ const filterSync = readFileSync(
196
+ new URL("use-operations-filter-sync.ts", import.meta.url),
197
+ "utf8",
198
+ );
191
199
 
192
200
  expect(workListTypeOnViewChange("work-list")).toBe("all");
193
201
  expect(workListTypeOnViewChange("work-kanban")).toBeNull();
194
202
  expect(source).toContain("const nextWorkListType = workListTypeOnViewChange(nextView)");
195
203
  expect(source).toContain("onWorkListTypeChange?.(nextWorkListType)");
196
- expect(source).toContain('if (view === "work-kanban") onBoardFiltersChange?.(filters)');
197
- expect(source).toContain("else onWorkListFiltersChange?.(filters)");
204
+ expect(source).toContain("useOperationsFilterSync({");
205
+ expect(filterSync).toContain('if (view === "work-kanban") onBoardFiltersChange?.(filters)');
206
+ expect(filterSync).toContain("else onWorkListFiltersChange?.(filters)");
198
207
  });
199
208
 
200
209
  it("scopes task and macro metrics to the active overview entity kind", () => {
@@ -103,6 +103,7 @@ export interface OperationsPageProps {
103
103
  locale: OperationsLocale;
104
104
  onLocaleChange: (locale: OperationsLocale) => void;
105
105
  onOpenDocument: (kind: "prd" | "spec", id: string) => void;
106
+ onOpenMacro: (kind: "prd" | "spec", id: string) => void;
106
107
  onOpenTask: (taskId: string) => Promise<boolean>;
107
108
  onPriorityChange: (itemIds: readonly string[]) => Promise<void>;
108
109
  onDetailDataRequest?: (view: OperationsView) => void;
@@ -0,0 +1,16 @@
1
+ import { useMemo } from "react";
2
+ import { factualCostDateRange, initialOperationsDateRange } from "./operation-date-range.js";
3
+ import type { OperationsPageProps } from "./operations-page-contract.js";
4
+ export function useOperationsDefaultRange(
5
+ data: OperationsPageProps["data"],
6
+ factualCostPeriods: OperationsPageProps["factualCostPeriods"],
7
+ ) {
8
+ const defaultDateRange = useMemo(() => {
9
+ const operationsRange = initialOperationsDateRange(
10
+ data.tasks,
11
+ Object.keys(data.summary.periods?.daily ?? {}),
12
+ );
13
+ return factualCostDateRange(factualCostPeriods?.day, operationsRange);
14
+ }, [data.summary.periods, data.tasks, factualCostPeriods?.day]);
15
+ return defaultDateRange;
16
+ }
@@ -0,0 +1,66 @@
1
+ import { useEffect } from "react";
2
+ import type { OperationsPageProps } from "./operations-page-contract.js";
3
+ import type { OperationsView } from "./operations-page-filters.js";
4
+ import type { TaskOrigin } from "./task-origin.js";
5
+
6
+ interface OperationsFilterSyncInput {
7
+ loading: boolean;
8
+ view: OperationsView;
9
+ query: string;
10
+ statusFilter: string;
11
+ riskFilter: string;
12
+ originFilter: TaskOrigin | "";
13
+ onBoardFiltersChange: OperationsPageProps["onBoardFiltersChange"];
14
+ onWorkListFiltersChange: OperationsPageProps["onWorkListFiltersChange"];
15
+ onEntityFiltersChange: OperationsPageProps["onEntityFiltersChange"];
16
+ }
17
+
18
+ export function useOperationsFilterSync({
19
+ loading,
20
+ view,
21
+ query,
22
+ statusFilter,
23
+ riskFilter,
24
+ originFilter,
25
+ onBoardFiltersChange,
26
+ onWorkListFiltersChange,
27
+ onEntityFiltersChange,
28
+ }: OperationsFilterSyncInput): void {
29
+ useEffect(() => {
30
+ // The parent replaces the aggregate snapshot while a period transition is
31
+ // loading. Defer paged entity hydration until that replacement is stable,
32
+ // otherwise the aggregate response can overwrite a newer entity page.
33
+ if (loading) return;
34
+ if (view === "work-kanban" || view === "work-list") {
35
+ const timeout = window.setTimeout(() => {
36
+ const filters = {
37
+ query,
38
+ status: statusFilter,
39
+ risk: riskFilter,
40
+ origin: originFilter,
41
+ };
42
+ if (view === "work-kanban") onBoardFiltersChange?.(filters);
43
+ else onWorkListFiltersChange?.(filters);
44
+ }, 180);
45
+ return () => window.clearTimeout(timeout);
46
+ }
47
+ if (!["runs", "conversations", "executions", "changes", "decisions"].includes(view)) return;
48
+ const timeout = window.setTimeout(() => {
49
+ onEntityFiltersChange?.(
50
+ view as "runs" | "conversations" | "executions" | "changes" | "decisions",
51
+ { query, status: statusFilter, risk: riskFilter },
52
+ );
53
+ }, 180);
54
+ return () => window.clearTimeout(timeout);
55
+ }, [
56
+ onBoardFiltersChange,
57
+ onWorkListFiltersChange,
58
+ onEntityFiltersChange,
59
+ loading,
60
+ originFilter,
61
+ query,
62
+ riskFilter,
63
+ statusFilter,
64
+ view,
65
+ ]);
66
+ }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zaip-ai/dashboard-traceability",
3
- "version": "4.9.0",
3
+ "version": "5.1.0",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "exports": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zaip-ai/dashboard-branding",
3
- "version": "4.9.0",
3
+ "version": "5.1.0",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "exports": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zaip-ai/dashboard-contracts",
3
- "version": "4.9.0",
3
+ "version": "5.1.0",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "exports": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zaip-ai/dashboard-data-export",
3
- "version": "4.9.0",
3
+ "version": "5.1.0",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "exports": {
@@ -0,0 +1,87 @@
1
+ import { createHash } from "node:crypto";
2
+ import { mkdir, readdir, readFile, rename, stat, writeFile } from "node:fs/promises";
3
+ import { dirname, join, relative, resolve } from "node:path";
4
+ import { dashboardBuildTargets, workspaceRoot } from "./start-dashboard-paths.mjs";
5
+
6
+ export async function contentFingerprint(paths, excluded = [], root = workspaceRoot) {
7
+ const entries = new Map();
8
+ async function visit(path) {
9
+ const key = relative(root, path).replaceAll("\\", "/");
10
+ const name = path.replaceAll("\\", "/").split("/").at(-1);
11
+ if (
12
+ ["node_modules", ".turbo"].includes(name) ||
13
+ /\.(test|spec)\./.test(name) ||
14
+ excluded.some((part) => `/${key}/`.includes(part))
15
+ )
16
+ return;
17
+ const details = await stat(path).catch((error) => {
18
+ if (error.code === "ENOENT") return null;
19
+ throw error;
20
+ });
21
+ if (!details) {
22
+ entries.set(key, "missing");
23
+ return;
24
+ }
25
+ if (details.isDirectory()) {
26
+ entries.set(`${key}/`, "directory");
27
+ for (const entry of await readdir(path)) await visit(join(path, entry));
28
+ } else
29
+ entries.set(
30
+ key,
31
+ createHash("sha256")
32
+ .update(await readFile(path))
33
+ .digest("hex"),
34
+ );
35
+ }
36
+ for (const path of paths) await visit(path);
37
+ return createHash("sha256")
38
+ .update(JSON.stringify([...entries].sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))))
39
+ .digest("hex");
40
+ }
41
+
42
+ const receiptPath = (name, root) => resolve(root, ".local/dashboard-build", `${name}.json`);
43
+ export const sourceFingerprint = (target, root = workspaceRoot) =>
44
+ contentFingerprint(target.sourceRoots, ["/dist/", ...target.excludedSourcePaths], root);
45
+ const outputFingerprint = (target, root) =>
46
+ contentFingerprint(target.outputRoots ?? target.artifacts, [], root);
47
+
48
+ export async function inspectBuildFingerprint(name, target, root = workspaceRoot) {
49
+ const receipt = await readFile(receiptPath(name, root), "utf8")
50
+ .then(JSON.parse)
51
+ .catch(() => null);
52
+ if (receipt?.version !== 1) return false;
53
+ return (
54
+ receipt.source === (await sourceFingerprint(target, root)) &&
55
+ receipt.output === (await outputFingerprint(target, root))
56
+ );
57
+ }
58
+
59
+ export async function recordDashboardBuild(
60
+ name,
61
+ before,
62
+ target = dashboardBuildTargets[name],
63
+ root = workspaceRoot,
64
+ ) {
65
+ const source = await sourceFingerprint(target, root);
66
+ if (source !== before)
67
+ throw new Error(
68
+ `Dashboard ${name} inputs changed during build; no reusable receipt was recorded`,
69
+ );
70
+ for (const path of target.artifacts) await stat(path);
71
+ const receipt = { version: 1, source, output: await outputFingerprint(target, root) };
72
+ const path = receiptPath(name, root);
73
+ await mkdir(dirname(path), { recursive: true });
74
+ const temporary = `${path}.${process.pid}.tmp`;
75
+ await writeFile(temporary, `${JSON.stringify(receipt)}\n`);
76
+ await rename(temporary, path);
77
+ }
78
+
79
+ export async function dashboardReleaseFrozen(root = workspaceRoot) {
80
+ const text = await readFile(resolve(root, ".agents/config/release-cycle.json"), "utf8").catch(
81
+ (error) => {
82
+ if (error.code === "ENOENT") return null;
83
+ throw error;
84
+ },
85
+ );
86
+ return text !== null && JSON.parse(text).state === "frozen";
87
+ }
@@ -0,0 +1,48 @@
1
+ import assert from "node:assert/strict";
2
+ import { mkdir, mkdtemp, rm, utimes, writeFile } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { test } from "vitest";
6
+ import {
7
+ inspectBuildFingerprint,
8
+ recordDashboardBuild,
9
+ sourceFingerprint,
10
+ } from "./dashboard-build-fingerprint.mjs";
11
+ import { refreshDashboardBuildTargets } from "./start-dashboard-build.mjs";
12
+
13
+ test("build reuse follows bytes, includes missing/deleted inputs, and freezes without mutation", async (context) => {
14
+ const root = await mkdtemp(join(tmpdir(), "zaip-build-fingerprint-"));
15
+ context.onTestFinished(() => rm(root, { recursive: true, force: true }));
16
+ await mkdir(join(root, "src"));
17
+ const source = join(root, "src/readme.md"),
18
+ output = join(root, "bundle.js");
19
+ await writeFile(source, "help");
20
+ await writeFile(output, "bundle");
21
+ const target = { sourceRoots: [join(root, "src")], excludedSourcePaths: [], artifacts: [output] };
22
+ await recordDashboardBuild("fixture", await sourceFingerprint(target, root), target, root);
23
+ await utimes(source, new Date(), new Date(Date.now() + 60_000));
24
+ assert.equal(await inspectBuildFingerprint("fixture", target, root), true);
25
+ await writeFile(source, "new help");
26
+ assert.equal(await inspectBuildFingerprint("fixture", target, root), false);
27
+ const before = await sourceFingerprint(target, root);
28
+ await rm(source);
29
+ await assert.rejects(
30
+ recordDashboardBuild("fixture", before, target, root),
31
+ /changed during build/,
32
+ );
33
+ let builds = 0;
34
+ await assert.rejects(
35
+ refreshDashboardBuildTargets({
36
+ targets: { fixture: target },
37
+ frozen: true,
38
+ inspectTarget: async () => ({
39
+ missingArtifacts: [],
40
+ artifactsModifiedAt: 1,
41
+ sourcesModifiedAt: 2,
42
+ }),
43
+ runBuild: async () => builds++,
44
+ }),
45
+ /release is frozen/,
46
+ );
47
+ assert.equal(builds, 0);
48
+ });