gsd-pi 2.18.0 → 2.20.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 (289) hide show
  1. package/README.md +5 -1
  2. package/dist/cli.js +3 -3
  3. package/dist/onboarding.d.ts +3 -1
  4. package/dist/onboarding.js +77 -3
  5. package/dist/remote-questions-config.d.ts +1 -1
  6. package/dist/resources/extensions/google-search/index.ts +164 -47
  7. package/dist/resources/extensions/gsd/auto-dashboard.ts +14 -2
  8. package/dist/resources/extensions/gsd/auto-prompts.ts +148 -39
  9. package/dist/resources/extensions/gsd/auto-worktree.ts +93 -9
  10. package/dist/resources/extensions/gsd/auto.ts +690 -39
  11. package/dist/resources/extensions/gsd/captures.ts +384 -0
  12. package/dist/resources/extensions/gsd/commands.ts +654 -36
  13. package/dist/resources/extensions/gsd/complexity-classifier.ts +322 -0
  14. package/dist/resources/extensions/gsd/context-budget.ts +243 -0
  15. package/dist/resources/extensions/gsd/context-store.ts +195 -0
  16. package/dist/resources/extensions/gsd/dashboard-overlay.ts +51 -3
  17. package/dist/resources/extensions/gsd/db-writer.ts +341 -0
  18. package/dist/resources/extensions/gsd/debug-logger.ts +178 -0
  19. package/dist/resources/extensions/gsd/dispatch-guard.ts +0 -1
  20. package/dist/resources/extensions/gsd/docs/preferences-reference.md +54 -0
  21. package/dist/resources/extensions/gsd/doctor-proactive.ts +286 -0
  22. package/dist/resources/extensions/gsd/doctor.ts +283 -2
  23. package/dist/resources/extensions/gsd/export.ts +81 -2
  24. package/dist/resources/extensions/gsd/files.ts +39 -9
  25. package/dist/resources/extensions/gsd/git-service.ts +6 -0
  26. package/dist/resources/extensions/gsd/gsd-db.ts +752 -0
  27. package/dist/resources/extensions/gsd/guided-flow.ts +26 -1
  28. package/dist/resources/extensions/gsd/history.ts +0 -1
  29. package/dist/resources/extensions/gsd/index.ts +277 -1
  30. package/dist/resources/extensions/gsd/md-importer.ts +526 -0
  31. package/dist/resources/extensions/gsd/metrics.ts +84 -0
  32. package/dist/resources/extensions/gsd/model-cost-table.ts +65 -0
  33. package/dist/resources/extensions/gsd/model-router.ts +256 -0
  34. package/dist/resources/extensions/gsd/notifications.ts +0 -1
  35. package/dist/resources/extensions/gsd/post-unit-hooks.ts +72 -2
  36. package/dist/resources/extensions/gsd/preferences.ts +198 -150
  37. package/dist/resources/extensions/gsd/prompt-loader.ts +45 -9
  38. package/dist/resources/extensions/gsd/prompts/execute-task.md +3 -5
  39. package/dist/resources/extensions/gsd/prompts/heal-skill.md +45 -0
  40. package/dist/resources/extensions/gsd/prompts/plan-slice.md +5 -1
  41. package/dist/resources/extensions/gsd/prompts/quick-task.md +48 -0
  42. package/dist/resources/extensions/gsd/prompts/reassess-roadmap.md +6 -0
  43. package/dist/resources/extensions/gsd/prompts/replan-slice.md +8 -0
  44. package/dist/resources/extensions/gsd/prompts/system.md +2 -1
  45. package/dist/resources/extensions/gsd/prompts/triage-captures.md +62 -0
  46. package/dist/resources/extensions/gsd/quick.ts +156 -0
  47. package/dist/resources/extensions/gsd/skill-discovery.ts +5 -3
  48. package/dist/resources/extensions/gsd/skill-health.ts +417 -0
  49. package/dist/resources/extensions/gsd/skill-telemetry.ts +127 -0
  50. package/dist/resources/extensions/gsd/state.ts +30 -0
  51. package/dist/resources/extensions/gsd/templates/preferences.md +1 -0
  52. package/dist/resources/extensions/gsd/tests/captures.test.ts +438 -0
  53. package/dist/resources/extensions/gsd/tests/complexity-classifier.test.ts +181 -0
  54. package/dist/resources/extensions/gsd/tests/context-budget.test.ts +283 -0
  55. package/dist/resources/extensions/gsd/tests/context-compression.test.ts +1 -1
  56. package/dist/resources/extensions/gsd/tests/context-store.test.ts +462 -0
  57. package/dist/resources/extensions/gsd/tests/continue-here.test.ts +204 -0
  58. package/dist/resources/extensions/gsd/tests/dashboard-budget.test.ts +346 -0
  59. package/dist/resources/extensions/gsd/tests/db-writer.test.ts +602 -0
  60. package/dist/resources/extensions/gsd/tests/debug-logger.test.ts +185 -0
  61. package/dist/resources/extensions/gsd/tests/derive-state-db.test.ts +406 -0
  62. package/dist/resources/extensions/gsd/tests/dispatch-guard.test.ts +0 -1
  63. package/dist/resources/extensions/gsd/tests/dist-redirect.mjs +22 -0
  64. package/dist/resources/extensions/gsd/tests/doctor-proactive.test.ts +244 -0
  65. package/dist/resources/extensions/gsd/tests/doctor-runtime.test.ts +303 -0
  66. package/dist/resources/extensions/gsd/tests/feature-branch-lifecycle-integration.test.ts +434 -0
  67. package/dist/resources/extensions/gsd/tests/gsd-db.test.ts +353 -0
  68. package/dist/resources/extensions/gsd/tests/gsd-inspect.test.ts +125 -0
  69. package/dist/resources/extensions/gsd/tests/gsd-tools.test.ts +326 -0
  70. package/dist/resources/extensions/gsd/tests/integration-edge.test.ts +228 -0
  71. package/dist/resources/extensions/gsd/tests/integration-lifecycle.test.ts +277 -0
  72. package/dist/resources/extensions/gsd/tests/md-importer.test.ts +411 -0
  73. package/dist/resources/extensions/gsd/tests/metrics.test.ts +197 -0
  74. package/dist/resources/extensions/gsd/tests/milestone-transition-worktree.test.ts +144 -0
  75. package/dist/resources/extensions/gsd/tests/model-cost-table.test.ts +69 -0
  76. package/dist/resources/extensions/gsd/tests/model-isolation.test.ts +99 -0
  77. package/dist/resources/extensions/gsd/tests/model-router.test.ts +167 -0
  78. package/dist/resources/extensions/gsd/tests/parsers.test.ts +40 -0
  79. package/dist/resources/extensions/gsd/tests/post-unit-hooks.test.ts +41 -1
  80. package/dist/resources/extensions/gsd/tests/preferences-git.test.ts +0 -1
  81. package/dist/resources/extensions/gsd/tests/preferences-hooks.test.ts +0 -1
  82. package/dist/resources/extensions/gsd/tests/preferences-mode.test.ts +110 -0
  83. package/dist/resources/extensions/gsd/tests/preferences-models.test.ts +0 -1
  84. package/dist/resources/extensions/gsd/tests/prompt-budget-enforcement.test.ts +464 -0
  85. package/dist/resources/extensions/gsd/tests/prompt-db.test.ts +385 -0
  86. package/dist/resources/extensions/gsd/tests/remote-questions.test.ts +488 -1
  87. package/dist/resources/extensions/gsd/tests/resolve-ts-hooks.mjs +17 -29
  88. package/dist/resources/extensions/gsd/tests/resolve-ts.mjs +2 -8
  89. package/dist/resources/extensions/gsd/tests/routing-history.test.ts +215 -62
  90. package/dist/resources/extensions/gsd/tests/skill-lifecycle.test.ts +126 -0
  91. package/dist/resources/extensions/gsd/tests/stop-auto-remote.test.ts +31 -8
  92. package/dist/resources/extensions/gsd/tests/token-savings.test.ts +366 -0
  93. package/dist/resources/extensions/gsd/tests/triage-dispatch.test.ts +224 -0
  94. package/dist/resources/extensions/gsd/tests/triage-resolution.test.ts +215 -0
  95. package/dist/resources/extensions/gsd/tests/unit-runtime.test.ts +25 -1
  96. package/dist/resources/extensions/gsd/tests/visualizer-critical-path.test.ts +145 -0
  97. package/dist/resources/extensions/gsd/tests/visualizer-data.test.ts +290 -0
  98. package/dist/resources/extensions/gsd/tests/visualizer-overlay.test.ts +120 -0
  99. package/dist/resources/extensions/gsd/tests/visualizer-views.test.ts +478 -0
  100. package/dist/resources/extensions/gsd/tests/worktree-db-integration.test.ts +205 -0
  101. package/dist/resources/extensions/gsd/tests/worktree-db.test.ts +442 -0
  102. package/dist/resources/extensions/gsd/tests/worktree-post-create-hook.test.ts +165 -0
  103. package/dist/resources/extensions/gsd/triage-resolution.ts +200 -0
  104. package/dist/resources/extensions/gsd/triage-ui.ts +175 -0
  105. package/dist/resources/extensions/gsd/types.ts +29 -0
  106. package/dist/resources/extensions/gsd/undo.ts +0 -1
  107. package/dist/resources/extensions/gsd/unit-runtime.ts +5 -1
  108. package/dist/resources/extensions/gsd/visualizer-data.ts +505 -0
  109. package/dist/resources/extensions/gsd/visualizer-overlay.ts +337 -0
  110. package/dist/resources/extensions/gsd/visualizer-views.ts +755 -0
  111. package/dist/resources/extensions/gsd/worktree-command.ts +18 -0
  112. package/dist/resources/extensions/gsd/worktree-manager.ts +11 -4
  113. package/dist/resources/extensions/remote-questions/config.ts +4 -2
  114. package/dist/resources/extensions/remote-questions/discord-adapter.ts +35 -4
  115. package/dist/resources/extensions/remote-questions/format.ts +166 -14
  116. package/dist/resources/extensions/remote-questions/manager.ts +14 -4
  117. package/dist/resources/extensions/remote-questions/remote-command.ts +100 -4
  118. package/dist/resources/extensions/remote-questions/slack-adapter.ts +58 -2
  119. package/dist/resources/extensions/remote-questions/telegram-adapter.ts +161 -0
  120. package/dist/resources/extensions/remote-questions/types.ts +2 -1
  121. package/dist/resources/extensions/ttsr/ttsr-manager.ts +26 -0
  122. package/dist/resources/extensions/voice/index.ts +4 -3
  123. package/package.json +1 -1
  124. package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
  125. package/packages/pi-coding-agent/dist/core/agent-session.js +12 -1
  126. package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
  127. package/packages/pi-coding-agent/dist/core/extensions/loader.d.ts.map +1 -1
  128. package/packages/pi-coding-agent/dist/core/extensions/loader.js +5 -0
  129. package/packages/pi-coding-agent/dist/core/extensions/loader.js.map +1 -1
  130. package/packages/pi-coding-agent/dist/core/lsp/client.d.ts +6 -0
  131. package/packages/pi-coding-agent/dist/core/lsp/client.d.ts.map +1 -1
  132. package/packages/pi-coding-agent/dist/core/lsp/client.js +25 -0
  133. package/packages/pi-coding-agent/dist/core/lsp/client.js.map +1 -1
  134. package/packages/pi-coding-agent/dist/core/lsp/index.d.ts +2 -0
  135. package/packages/pi-coding-agent/dist/core/lsp/index.d.ts.map +1 -1
  136. package/packages/pi-coding-agent/dist/core/lsp/index.js +106 -3
  137. package/packages/pi-coding-agent/dist/core/lsp/index.js.map +1 -1
  138. package/packages/pi-coding-agent/dist/core/lsp/lsp.md +6 -0
  139. package/packages/pi-coding-agent/dist/core/lsp/types.d.ts +35 -0
  140. package/packages/pi-coding-agent/dist/core/lsp/types.d.ts.map +1 -1
  141. package/packages/pi-coding-agent/dist/core/lsp/types.js +6 -0
  142. package/packages/pi-coding-agent/dist/core/lsp/types.js.map +1 -1
  143. package/packages/pi-coding-agent/dist/core/lsp/utils.d.ts +3 -1
  144. package/packages/pi-coding-agent/dist/core/lsp/utils.d.ts.map +1 -1
  145. package/packages/pi-coding-agent/dist/core/lsp/utils.js +45 -0
  146. package/packages/pi-coding-agent/dist/core/lsp/utils.js.map +1 -1
  147. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts +6 -0
  148. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts.map +1 -1
  149. package/packages/pi-coding-agent/dist/core/settings-manager.js +43 -11
  150. package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
  151. package/packages/pi-coding-agent/dist/core/system-prompt.d.ts.map +1 -1
  152. package/packages/pi-coding-agent/dist/core/system-prompt.js +7 -1
  153. package/packages/pi-coding-agent/dist/core/system-prompt.js.map +1 -1
  154. package/packages/pi-coding-agent/dist/core/tools/edit.d.ts.map +1 -1
  155. package/packages/pi-coding-agent/dist/core/tools/edit.js +5 -0
  156. package/packages/pi-coding-agent/dist/core/tools/edit.js.map +1 -1
  157. package/packages/pi-coding-agent/dist/core/tools/index.d.ts +2 -0
  158. package/packages/pi-coding-agent/dist/core/tools/index.d.ts.map +1 -1
  159. package/packages/pi-coding-agent/dist/core/tools/write.d.ts.map +1 -1
  160. package/packages/pi-coding-agent/dist/core/tools/write.js +5 -0
  161. package/packages/pi-coding-agent/dist/core/tools/write.js.map +1 -1
  162. package/packages/pi-coding-agent/src/core/agent-session.ts +13 -1
  163. package/packages/pi-coding-agent/src/core/extensions/loader.ts +6 -0
  164. package/packages/pi-coding-agent/src/core/lsp/client.ts +26 -0
  165. package/packages/pi-coding-agent/src/core/lsp/index.ts +157 -2
  166. package/packages/pi-coding-agent/src/core/lsp/lsp.md +6 -0
  167. package/packages/pi-coding-agent/src/core/lsp/types.ts +53 -0
  168. package/packages/pi-coding-agent/src/core/lsp/utils.ts +56 -0
  169. package/packages/pi-coding-agent/src/core/settings-manager.ts +41 -11
  170. package/packages/pi-coding-agent/src/core/system-prompt.ts +7 -1
  171. package/packages/pi-coding-agent/src/core/tools/edit.ts +3 -0
  172. package/packages/pi-coding-agent/src/core/tools/write.ts +3 -0
  173. package/src/resources/extensions/google-search/index.ts +164 -47
  174. package/src/resources/extensions/gsd/auto-dashboard.ts +14 -2
  175. package/src/resources/extensions/gsd/auto-prompts.ts +148 -39
  176. package/src/resources/extensions/gsd/auto-worktree.ts +93 -9
  177. package/src/resources/extensions/gsd/auto.ts +690 -39
  178. package/src/resources/extensions/gsd/captures.ts +384 -0
  179. package/src/resources/extensions/gsd/commands.ts +654 -36
  180. package/src/resources/extensions/gsd/complexity-classifier.ts +322 -0
  181. package/src/resources/extensions/gsd/context-budget.ts +243 -0
  182. package/src/resources/extensions/gsd/context-store.ts +195 -0
  183. package/src/resources/extensions/gsd/dashboard-overlay.ts +51 -3
  184. package/src/resources/extensions/gsd/db-writer.ts +341 -0
  185. package/src/resources/extensions/gsd/debug-logger.ts +178 -0
  186. package/src/resources/extensions/gsd/dispatch-guard.ts +0 -1
  187. package/src/resources/extensions/gsd/docs/preferences-reference.md +54 -0
  188. package/src/resources/extensions/gsd/doctor-proactive.ts +286 -0
  189. package/src/resources/extensions/gsd/doctor.ts +283 -2
  190. package/src/resources/extensions/gsd/export.ts +81 -2
  191. package/src/resources/extensions/gsd/files.ts +39 -9
  192. package/src/resources/extensions/gsd/git-service.ts +6 -0
  193. package/src/resources/extensions/gsd/gsd-db.ts +752 -0
  194. package/src/resources/extensions/gsd/guided-flow.ts +26 -1
  195. package/src/resources/extensions/gsd/history.ts +0 -1
  196. package/src/resources/extensions/gsd/index.ts +277 -1
  197. package/src/resources/extensions/gsd/md-importer.ts +526 -0
  198. package/src/resources/extensions/gsd/metrics.ts +84 -0
  199. package/src/resources/extensions/gsd/model-cost-table.ts +65 -0
  200. package/src/resources/extensions/gsd/model-router.ts +256 -0
  201. package/src/resources/extensions/gsd/notifications.ts +0 -1
  202. package/src/resources/extensions/gsd/post-unit-hooks.ts +72 -2
  203. package/src/resources/extensions/gsd/preferences.ts +198 -150
  204. package/src/resources/extensions/gsd/prompt-loader.ts +45 -9
  205. package/src/resources/extensions/gsd/prompts/execute-task.md +3 -5
  206. package/src/resources/extensions/gsd/prompts/heal-skill.md +45 -0
  207. package/src/resources/extensions/gsd/prompts/plan-slice.md +5 -1
  208. package/src/resources/extensions/gsd/prompts/quick-task.md +48 -0
  209. package/src/resources/extensions/gsd/prompts/reassess-roadmap.md +6 -0
  210. package/src/resources/extensions/gsd/prompts/replan-slice.md +8 -0
  211. package/src/resources/extensions/gsd/prompts/system.md +2 -1
  212. package/src/resources/extensions/gsd/prompts/triage-captures.md +62 -0
  213. package/src/resources/extensions/gsd/quick.ts +156 -0
  214. package/src/resources/extensions/gsd/skill-discovery.ts +5 -3
  215. package/src/resources/extensions/gsd/skill-health.ts +417 -0
  216. package/src/resources/extensions/gsd/skill-telemetry.ts +127 -0
  217. package/src/resources/extensions/gsd/state.ts +30 -0
  218. package/src/resources/extensions/gsd/templates/preferences.md +1 -0
  219. package/src/resources/extensions/gsd/tests/captures.test.ts +438 -0
  220. package/src/resources/extensions/gsd/tests/complexity-classifier.test.ts +181 -0
  221. package/src/resources/extensions/gsd/tests/context-budget.test.ts +283 -0
  222. package/src/resources/extensions/gsd/tests/context-compression.test.ts +1 -1
  223. package/src/resources/extensions/gsd/tests/context-store.test.ts +462 -0
  224. package/src/resources/extensions/gsd/tests/continue-here.test.ts +204 -0
  225. package/src/resources/extensions/gsd/tests/dashboard-budget.test.ts +346 -0
  226. package/src/resources/extensions/gsd/tests/db-writer.test.ts +602 -0
  227. package/src/resources/extensions/gsd/tests/debug-logger.test.ts +185 -0
  228. package/src/resources/extensions/gsd/tests/derive-state-db.test.ts +406 -0
  229. package/src/resources/extensions/gsd/tests/dispatch-guard.test.ts +0 -1
  230. package/src/resources/extensions/gsd/tests/dist-redirect.mjs +22 -0
  231. package/src/resources/extensions/gsd/tests/doctor-proactive.test.ts +244 -0
  232. package/src/resources/extensions/gsd/tests/doctor-runtime.test.ts +303 -0
  233. package/src/resources/extensions/gsd/tests/feature-branch-lifecycle-integration.test.ts +434 -0
  234. package/src/resources/extensions/gsd/tests/gsd-db.test.ts +353 -0
  235. package/src/resources/extensions/gsd/tests/gsd-inspect.test.ts +125 -0
  236. package/src/resources/extensions/gsd/tests/gsd-tools.test.ts +326 -0
  237. package/src/resources/extensions/gsd/tests/integration-edge.test.ts +228 -0
  238. package/src/resources/extensions/gsd/tests/integration-lifecycle.test.ts +277 -0
  239. package/src/resources/extensions/gsd/tests/md-importer.test.ts +411 -0
  240. package/src/resources/extensions/gsd/tests/metrics.test.ts +197 -0
  241. package/src/resources/extensions/gsd/tests/milestone-transition-worktree.test.ts +144 -0
  242. package/src/resources/extensions/gsd/tests/model-cost-table.test.ts +69 -0
  243. package/src/resources/extensions/gsd/tests/model-isolation.test.ts +99 -0
  244. package/src/resources/extensions/gsd/tests/model-router.test.ts +167 -0
  245. package/src/resources/extensions/gsd/tests/parsers.test.ts +40 -0
  246. package/src/resources/extensions/gsd/tests/post-unit-hooks.test.ts +41 -1
  247. package/src/resources/extensions/gsd/tests/preferences-git.test.ts +0 -1
  248. package/src/resources/extensions/gsd/tests/preferences-hooks.test.ts +0 -1
  249. package/src/resources/extensions/gsd/tests/preferences-mode.test.ts +110 -0
  250. package/src/resources/extensions/gsd/tests/preferences-models.test.ts +0 -1
  251. package/src/resources/extensions/gsd/tests/prompt-budget-enforcement.test.ts +464 -0
  252. package/src/resources/extensions/gsd/tests/prompt-db.test.ts +385 -0
  253. package/src/resources/extensions/gsd/tests/remote-questions.test.ts +488 -1
  254. package/src/resources/extensions/gsd/tests/resolve-ts-hooks.mjs +17 -29
  255. package/src/resources/extensions/gsd/tests/resolve-ts.mjs +2 -8
  256. package/src/resources/extensions/gsd/tests/routing-history.test.ts +215 -62
  257. package/src/resources/extensions/gsd/tests/skill-lifecycle.test.ts +126 -0
  258. package/src/resources/extensions/gsd/tests/stop-auto-remote.test.ts +31 -8
  259. package/src/resources/extensions/gsd/tests/token-savings.test.ts +366 -0
  260. package/src/resources/extensions/gsd/tests/triage-dispatch.test.ts +224 -0
  261. package/src/resources/extensions/gsd/tests/triage-resolution.test.ts +215 -0
  262. package/src/resources/extensions/gsd/tests/unit-runtime.test.ts +25 -1
  263. package/src/resources/extensions/gsd/tests/visualizer-critical-path.test.ts +145 -0
  264. package/src/resources/extensions/gsd/tests/visualizer-data.test.ts +290 -0
  265. package/src/resources/extensions/gsd/tests/visualizer-overlay.test.ts +120 -0
  266. package/src/resources/extensions/gsd/tests/visualizer-views.test.ts +478 -0
  267. package/src/resources/extensions/gsd/tests/worktree-db-integration.test.ts +205 -0
  268. package/src/resources/extensions/gsd/tests/worktree-db.test.ts +442 -0
  269. package/src/resources/extensions/gsd/tests/worktree-post-create-hook.test.ts +165 -0
  270. package/src/resources/extensions/gsd/triage-resolution.ts +200 -0
  271. package/src/resources/extensions/gsd/triage-ui.ts +175 -0
  272. package/src/resources/extensions/gsd/types.ts +29 -0
  273. package/src/resources/extensions/gsd/undo.ts +0 -1
  274. package/src/resources/extensions/gsd/unit-runtime.ts +5 -1
  275. package/src/resources/extensions/gsd/visualizer-data.ts +505 -0
  276. package/src/resources/extensions/gsd/visualizer-overlay.ts +337 -0
  277. package/src/resources/extensions/gsd/visualizer-views.ts +755 -0
  278. package/src/resources/extensions/gsd/worktree-command.ts +18 -0
  279. package/src/resources/extensions/gsd/worktree-manager.ts +11 -4
  280. package/src/resources/extensions/remote-questions/config.ts +4 -2
  281. package/src/resources/extensions/remote-questions/discord-adapter.ts +35 -4
  282. package/src/resources/extensions/remote-questions/format.ts +166 -14
  283. package/src/resources/extensions/remote-questions/manager.ts +14 -4
  284. package/src/resources/extensions/remote-questions/remote-command.ts +100 -4
  285. package/src/resources/extensions/remote-questions/slack-adapter.ts +58 -2
  286. package/src/resources/extensions/remote-questions/telegram-adapter.ts +161 -0
  287. package/src/resources/extensions/remote-questions/types.ts +2 -1
  288. package/src/resources/extensions/ttsr/ttsr-manager.ts +26 -0
  289. package/src/resources/extensions/voice/index.ts +4 -3
@@ -0,0 +1,602 @@
1
+ import { createTestContext } from './test-helpers.ts';
2
+ import * as path from 'node:path';
3
+ import * as os from 'node:os';
4
+ import * as fs from 'node:fs';
5
+ import {
6
+ openDatabase,
7
+ closeDatabase,
8
+ upsertDecision,
9
+ upsertRequirement,
10
+ insertArtifact,
11
+ getDecisionById,
12
+ getRequirementById,
13
+ _getAdapter,
14
+ } from '../gsd-db.ts';
15
+ import {
16
+ parseDecisionsTable,
17
+ parseRequirementsSections,
18
+ } from '../md-importer.ts';
19
+ import {
20
+ generateDecisionsMd,
21
+ generateRequirementsMd,
22
+ nextDecisionId,
23
+ saveDecisionToDb,
24
+ updateRequirementInDb,
25
+ saveArtifactToDb,
26
+ } from '../db-writer.ts';
27
+ import type { Decision, Requirement } from '../types.ts';
28
+
29
+ const { assertEq, assertTrue, assertMatch, report } = createTestContext();
30
+
31
+ // ═══════════════════════════════════════════════════════════════════════════
32
+ // Helpers
33
+ // ═══════════════════════════════════════════════════════════════════════════
34
+
35
+ function makeTmpDir(): string {
36
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-dbwriter-'));
37
+ // Create .gsd directory structure
38
+ fs.mkdirSync(path.join(dir, '.gsd'), { recursive: true });
39
+ return dir;
40
+ }
41
+
42
+ function cleanupDir(dir: string): void {
43
+ try {
44
+ fs.rmSync(dir, { recursive: true, force: true });
45
+ } catch { /* swallow */ }
46
+ }
47
+
48
+ // ═══════════════════════════════════════════════════════════════════════════
49
+ // Test Fixtures
50
+ // ═══════════════════════════════════════════════════════════════════════════
51
+
52
+ const SAMPLE_DECISIONS: Decision[] = [
53
+ {
54
+ seq: 1,
55
+ id: 'D001',
56
+ when_context: 'M001',
57
+ scope: 'library',
58
+ decision: 'SQLite library',
59
+ choice: 'better-sqlite3',
60
+ rationale: 'Sync API',
61
+ revisable: 'No',
62
+ superseded_by: null,
63
+ },
64
+ {
65
+ seq: 2,
66
+ id: 'D002',
67
+ when_context: 'M001',
68
+ scope: 'arch',
69
+ decision: 'DB location',
70
+ choice: '.gsd/gsd.db',
71
+ rationale: 'Derived state',
72
+ revisable: 'No',
73
+ superseded_by: null,
74
+ },
75
+ {
76
+ seq: 3,
77
+ id: 'D003',
78
+ when_context: 'M001/S01',
79
+ scope: 'impl',
80
+ decision: 'Provider strategy (amends D001)',
81
+ choice: 'node:sqlite fallback',
82
+ rationale: 'Zero deps',
83
+ revisable: 'Yes',
84
+ superseded_by: null,
85
+ },
86
+ ];
87
+
88
+ const SAMPLE_REQUIREMENTS: Requirement[] = [
89
+ {
90
+ id: 'R001',
91
+ class: 'core-capability',
92
+ status: 'active',
93
+ description: 'A SQLite database with typed wrappers',
94
+ why: 'Foundation for storage',
95
+ source: 'user',
96
+ primary_owner: 'M001/S01',
97
+ supporting_slices: 'none',
98
+ validation: 'S01 verified',
99
+ notes: 'WAL mode enabled',
100
+ full_content: '',
101
+ superseded_by: null,
102
+ },
103
+ {
104
+ id: 'R002',
105
+ class: 'failure-visibility',
106
+ status: 'validated',
107
+ description: 'Falls back to markdown if SQLite unavailable',
108
+ why: 'Must not break on exotic platforms',
109
+ source: 'user',
110
+ primary_owner: 'M001/S01',
111
+ supporting_slices: 'M001/S03',
112
+ validation: 'S03 validated',
113
+ notes: 'Transparent fallback',
114
+ full_content: '',
115
+ superseded_by: null,
116
+ },
117
+ {
118
+ id: 'R030',
119
+ class: 'differentiator',
120
+ status: 'deferred',
121
+ description: 'Vector search support',
122
+ why: 'Semantic retrieval',
123
+ source: 'user',
124
+ primary_owner: 'none',
125
+ supporting_slices: 'none',
126
+ validation: 'unmapped',
127
+ notes: 'Deferred to M002',
128
+ full_content: '',
129
+ superseded_by: null,
130
+ },
131
+ {
132
+ id: 'R040',
133
+ class: 'anti-feature',
134
+ status: 'out-of-scope',
135
+ description: 'GUI dashboard',
136
+ why: 'CLI-first design',
137
+ source: 'user',
138
+ primary_owner: 'none',
139
+ supporting_slices: 'none',
140
+ validation: '',
141
+ notes: '',
142
+ full_content: '',
143
+ superseded_by: null,
144
+ },
145
+ ];
146
+
147
+ // ═══════════════════════════════════════════════════════════════════════════
148
+ // Round-Trip Tests: Decisions
149
+ // ═══════════════════════════════════════════════════════════════════════════
150
+
151
+ console.log('\n── generateDecisionsMd round-trip ──');
152
+
153
+ {
154
+ const md = generateDecisionsMd(SAMPLE_DECISIONS);
155
+ const parsed = parseDecisionsTable(md);
156
+
157
+ assertEq(parsed.length, SAMPLE_DECISIONS.length, 'decisions count matches');
158
+
159
+ for (let i = 0; i < SAMPLE_DECISIONS.length; i++) {
160
+ const orig = SAMPLE_DECISIONS[i];
161
+ const rt = parsed[i];
162
+ assertEq(rt.id, orig.id, `decision ${orig.id} id round-trips`);
163
+ assertEq(rt.when_context, orig.when_context, `decision ${orig.id} when_context round-trips`);
164
+ assertEq(rt.scope, orig.scope, `decision ${orig.id} scope round-trips`);
165
+ assertEq(rt.decision, orig.decision, `decision ${orig.id} decision round-trips`);
166
+ assertEq(rt.choice, orig.choice, `decision ${orig.id} choice round-trips`);
167
+ assertEq(rt.rationale, orig.rationale, `decision ${orig.id} rationale round-trips`);
168
+ assertEq(rt.revisable, orig.revisable, `decision ${orig.id} revisable round-trips`);
169
+ }
170
+ }
171
+
172
+ console.log('\n── generateDecisionsMd format ──');
173
+
174
+ {
175
+ const md = generateDecisionsMd(SAMPLE_DECISIONS);
176
+ assertTrue(md.startsWith('# Decisions Register\n'), 'starts with H1 header');
177
+ assertTrue(md.includes('<!-- Append-only'), 'contains HTML comment block');
178
+ assertTrue(md.includes('| # | When | Scope'), 'contains table header');
179
+ assertTrue(md.includes('|---|------|-------'), 'contains separator row');
180
+ }
181
+
182
+ console.log('\n── generateDecisionsMd empty input ──');
183
+
184
+ {
185
+ const md = generateDecisionsMd([]);
186
+ const parsed = parseDecisionsTable(md);
187
+ assertEq(parsed.length, 0, 'empty decisions produces empty parse');
188
+ assertTrue(md.includes('| # | When | Scope'), 'still has table header even when empty');
189
+ }
190
+
191
+ console.log('\n── generateDecisionsMd pipe escaping ──');
192
+
193
+ {
194
+ const withPipe: Decision = {
195
+ seq: 1,
196
+ id: 'D001',
197
+ when_context: 'M001',
198
+ scope: 'arch',
199
+ decision: 'Choice A | Choice B comparison',
200
+ choice: 'A',
201
+ rationale: 'Better',
202
+ revisable: 'No',
203
+ superseded_by: null,
204
+ };
205
+ const md = generateDecisionsMd([withPipe]);
206
+ // Should not break the table — pipe in decision text should be escaped
207
+ const parsed = parseDecisionsTable(md);
208
+ assertTrue(parsed.length >= 1, 'pipe-containing decision parses without breaking table');
209
+ }
210
+
211
+ // ═══════════════════════════════════════════════════════════════════════════
212
+ // Round-Trip Tests: Requirements
213
+ // ═══════════════════════════════════════════════════════════════════════════
214
+
215
+ console.log('\n── generateRequirementsMd round-trip ──');
216
+
217
+ {
218
+ const md = generateRequirementsMd(SAMPLE_REQUIREMENTS);
219
+ const parsed = parseRequirementsSections(md);
220
+
221
+ assertEq(parsed.length, SAMPLE_REQUIREMENTS.length, 'requirements count matches');
222
+
223
+ for (const orig of SAMPLE_REQUIREMENTS) {
224
+ const rt = parsed.find(r => r.id === orig.id);
225
+ assertTrue(!!rt, `requirement ${orig.id} found in parsed output`);
226
+ if (rt) {
227
+ assertEq(rt.class, orig.class, `requirement ${orig.id} class round-trips`);
228
+ assertEq(rt.description, orig.description, `requirement ${orig.id} description round-trips`);
229
+ assertEq(rt.why, orig.why, `requirement ${orig.id} why round-trips`);
230
+ assertEq(rt.source, orig.source, `requirement ${orig.id} source round-trips`);
231
+ assertEq(rt.primary_owner, orig.primary_owner, `requirement ${orig.id} primary_owner round-trips`);
232
+ assertEq(rt.supporting_slices, orig.supporting_slices, `requirement ${orig.id} supporting_slices round-trips`);
233
+ if (orig.notes) {
234
+ assertEq(rt.notes, orig.notes, `requirement ${orig.id} notes round-trips`);
235
+ }
236
+ }
237
+ }
238
+ }
239
+
240
+ console.log('\n── generateRequirementsMd sections ──');
241
+
242
+ {
243
+ const md = generateRequirementsMd(SAMPLE_REQUIREMENTS);
244
+ assertTrue(md.includes('## Active'), 'has Active section');
245
+ assertTrue(md.includes('## Validated'), 'has Validated section');
246
+ assertTrue(md.includes('## Deferred'), 'has Deferred section');
247
+ assertTrue(md.includes('## Out of Scope'), 'has Out of Scope section');
248
+ assertTrue(md.includes('## Traceability'), 'has Traceability section');
249
+ assertTrue(md.includes('## Coverage Summary'), 'has Coverage Summary section');
250
+ }
251
+
252
+ console.log('\n── generateRequirementsMd only populated sections ──');
253
+
254
+ {
255
+ // Only active requirements — should only have Active section
256
+ const activeOnly = SAMPLE_REQUIREMENTS.filter(r => r.status === 'active');
257
+ const md = generateRequirementsMd(activeOnly);
258
+ assertTrue(md.includes('## Active'), 'has Active section');
259
+ assertTrue(!md.includes('## Validated'), 'no Validated section when no validated reqs');
260
+ assertTrue(!md.includes('## Deferred'), 'no Deferred section when no deferred reqs');
261
+ assertTrue(!md.includes('## Out of Scope'), 'no Out of Scope section when no out-of-scope reqs');
262
+ }
263
+
264
+ console.log('\n── generateRequirementsMd empty input ──');
265
+
266
+ {
267
+ const md = generateRequirementsMd([]);
268
+ const parsed = parseRequirementsSections(md);
269
+ assertEq(parsed.length, 0, 'empty requirements produces empty parse');
270
+ }
271
+
272
+ // ═══════════════════════════════════════════════════════════════════════════
273
+ // nextDecisionId Tests
274
+ // ═══════════════════════════════════════════════════════════════════════════
275
+
276
+ console.log('\n── nextDecisionId ──');
277
+
278
+ {
279
+ // Open in-memory DB
280
+ openDatabase(':memory:');
281
+
282
+ const id1 = await nextDecisionId();
283
+ assertEq(id1, 'D001', 'first ID when no decisions exist');
284
+
285
+ // Insert some decisions
286
+ upsertDecision({
287
+ id: 'D001',
288
+ when_context: 'M001',
289
+ scope: 'test',
290
+ decision: 'test decision',
291
+ choice: 'test choice',
292
+ rationale: 'test',
293
+ revisable: 'No',
294
+ superseded_by: null,
295
+ });
296
+ upsertDecision({
297
+ id: 'D005',
298
+ when_context: 'M001',
299
+ scope: 'test',
300
+ decision: 'test decision 5',
301
+ choice: 'test choice',
302
+ rationale: 'test',
303
+ revisable: 'No',
304
+ superseded_by: null,
305
+ });
306
+
307
+ const id2 = await nextDecisionId();
308
+ assertEq(id2, 'D006', 'next ID after D005 is D006');
309
+
310
+ closeDatabase();
311
+ }
312
+
313
+ // ═══════════════════════════════════════════════════════════════════════════
314
+ // saveDecisionToDb Tests
315
+ // ═══════════════════════════════════════════════════════════════════════════
316
+
317
+ console.log('\n── saveDecisionToDb ──');
318
+
319
+ {
320
+ const tmpDir = makeTmpDir();
321
+ const dbPath = path.join(tmpDir, '.gsd', 'gsd.db');
322
+ openDatabase(dbPath);
323
+
324
+ try {
325
+ const result = await saveDecisionToDb({
326
+ scope: 'arch',
327
+ decision: 'Test decision',
328
+ choice: 'Option A',
329
+ rationale: 'Best option',
330
+ when_context: 'M001',
331
+ }, tmpDir);
332
+
333
+ assertEq(result.id, 'D001', 'saveDecisionToDb returns D001 as first ID');
334
+
335
+ // Verify DB state
336
+ const dbDecision = getDecisionById('D001');
337
+ assertTrue(!!dbDecision, 'decision exists in DB after save');
338
+ assertEq(dbDecision?.scope, 'arch', 'DB decision has correct scope');
339
+ assertEq(dbDecision?.choice, 'Option A', 'DB decision has correct choice');
340
+
341
+ // Verify markdown file was written
342
+ const mdPath = path.join(tmpDir, '.gsd', 'DECISIONS.md');
343
+ assertTrue(fs.existsSync(mdPath), 'DECISIONS.md file created');
344
+
345
+ const mdContent = fs.readFileSync(mdPath, 'utf-8');
346
+ assertTrue(mdContent.includes('D001'), 'DECISIONS.md contains new decision ID');
347
+ assertTrue(mdContent.includes('Test decision'), 'DECISIONS.md contains decision text');
348
+
349
+ // Verify round-trip of the written file
350
+ const parsed = parseDecisionsTable(mdContent);
351
+ assertEq(parsed.length, 1, 'written DECISIONS.md parses to 1 decision');
352
+ assertEq(parsed[0].id, 'D001', 'parsed decision has correct ID');
353
+
354
+ // Add second decision
355
+ const result2 = await saveDecisionToDb({
356
+ scope: 'impl',
357
+ decision: 'Second decision',
358
+ choice: 'Option B',
359
+ rationale: 'Also good',
360
+ }, tmpDir);
361
+
362
+ assertEq(result2.id, 'D002', 'second decision gets D002');
363
+
364
+ const mdContent2 = fs.readFileSync(mdPath, 'utf-8');
365
+ const parsed2 = parseDecisionsTable(mdContent2);
366
+ assertEq(parsed2.length, 2, 'DECISIONS.md now has 2 decisions');
367
+ } finally {
368
+ closeDatabase();
369
+ cleanupDir(tmpDir);
370
+ }
371
+ }
372
+
373
+ // ═══════════════════════════════════════════════════════════════════════════
374
+ // updateRequirementInDb Tests
375
+ // ═══════════════════════════════════════════════════════════════════════════
376
+
377
+ console.log('\n── updateRequirementInDb ──');
378
+
379
+ {
380
+ const tmpDir = makeTmpDir();
381
+ const dbPath = path.join(tmpDir, '.gsd', 'gsd.db');
382
+ openDatabase(dbPath);
383
+
384
+ try {
385
+ // Seed a requirement
386
+ upsertRequirement({
387
+ id: 'R001',
388
+ class: 'core-capability',
389
+ status: 'active',
390
+ description: 'Test requirement',
391
+ why: 'Testing',
392
+ source: 'test',
393
+ primary_owner: 'M001/S01',
394
+ supporting_slices: 'none',
395
+ validation: 'unmapped',
396
+ notes: '',
397
+ full_content: '',
398
+ superseded_by: null,
399
+ });
400
+
401
+ // Update it
402
+ await updateRequirementInDb('R001', {
403
+ status: 'validated',
404
+ validation: 'S01 — all tests pass',
405
+ notes: 'Validated in S01',
406
+ }, tmpDir);
407
+
408
+ // Verify DB state
409
+ const updated = getRequirementById('R001');
410
+ assertTrue(!!updated, 'requirement still exists after update');
411
+ assertEq(updated?.status, 'validated', 'status updated in DB');
412
+ assertEq(updated?.validation, 'S01 — all tests pass', 'validation updated in DB');
413
+ assertEq(updated?.description, 'Test requirement', 'description preserved after update');
414
+
415
+ // Verify markdown file was written
416
+ const mdPath = path.join(tmpDir, '.gsd', 'REQUIREMENTS.md');
417
+ assertTrue(fs.existsSync(mdPath), 'REQUIREMENTS.md file created');
418
+
419
+ const mdContent = fs.readFileSync(mdPath, 'utf-8');
420
+ assertTrue(mdContent.includes('R001'), 'REQUIREMENTS.md contains requirement ID');
421
+ assertTrue(mdContent.includes('validated'), 'REQUIREMENTS.md shows updated status');
422
+
423
+ // Verify round-trip
424
+ const parsed = parseRequirementsSections(mdContent);
425
+ assertEq(parsed.length, 1, 'parsed 1 requirement from written file');
426
+ assertEq(parsed[0].status, 'validated', 'parsed status matches update');
427
+ } finally {
428
+ closeDatabase();
429
+ cleanupDir(tmpDir);
430
+ }
431
+ }
432
+
433
+ console.log('\n── updateRequirementInDb — not found ──');
434
+
435
+ {
436
+ const tmpDir = makeTmpDir();
437
+ const dbPath = path.join(tmpDir, '.gsd', 'gsd.db');
438
+ openDatabase(dbPath);
439
+
440
+ try {
441
+ let threw = false;
442
+ try {
443
+ await updateRequirementInDb('R999', { status: 'validated' }, tmpDir);
444
+ } catch (err) {
445
+ threw = true;
446
+ assertTrue(
447
+ (err as Error).message.includes('R999'),
448
+ 'error message mentions the missing ID',
449
+ );
450
+ }
451
+ assertTrue(threw, 'throws when requirement not found');
452
+ } finally {
453
+ closeDatabase();
454
+ cleanupDir(tmpDir);
455
+ }
456
+ }
457
+
458
+ // ═══════════════════════════════════════════════════════════════════════════
459
+ // saveArtifactToDb Tests
460
+ // ═══════════════════════════════════════════════════════════════════════════
461
+
462
+ console.log('\n── saveArtifactToDb ──');
463
+
464
+ {
465
+ const tmpDir = makeTmpDir();
466
+ const dbPath = path.join(tmpDir, '.gsd', 'gsd.db');
467
+ openDatabase(dbPath);
468
+
469
+ try {
470
+ const content = '# Task Summary\n\nTest content\n';
471
+ await saveArtifactToDb({
472
+ path: 'milestones/M001/slices/S06/tasks/T01-SUMMARY.md',
473
+ artifact_type: 'SUMMARY',
474
+ content,
475
+ milestone_id: 'M001',
476
+ slice_id: 'S06',
477
+ task_id: 'T01',
478
+ }, tmpDir);
479
+
480
+ // Verify DB state
481
+ const adapter = _getAdapter();
482
+ assertTrue(!!adapter, 'adapter available');
483
+ const row = adapter!
484
+ .prepare('SELECT * FROM artifacts WHERE path = ?')
485
+ .get('milestones/M001/slices/S06/tasks/T01-SUMMARY.md');
486
+ assertTrue(!!row, 'artifact exists in DB');
487
+ assertEq(row!['artifact_type'], 'SUMMARY', 'artifact type correct in DB');
488
+ assertEq(row!['milestone_id'], 'M001', 'milestone_id correct in DB');
489
+ assertEq(row!['slice_id'], 'S06', 'slice_id correct in DB');
490
+ assertEq(row!['task_id'], 'T01', 'task_id correct in DB');
491
+
492
+ // Verify file on disk
493
+ const filePath = path.join(
494
+ tmpDir, '.gsd', 'milestones', 'M001', 'slices', 'S06', 'tasks', 'T01-SUMMARY.md',
495
+ );
496
+ assertTrue(fs.existsSync(filePath), 'artifact file written to disk');
497
+ assertEq(fs.readFileSync(filePath, 'utf-8'), content, 'file content matches');
498
+ } finally {
499
+ closeDatabase();
500
+ cleanupDir(tmpDir);
501
+ }
502
+ }
503
+
504
+ // ═══════════════════════════════════════════════════════════════════════════
505
+ // Full Round-Trip: DB → Markdown → Parse → Compare
506
+ // ═══════════════════════════════════════════════════════════════════════════
507
+
508
+ console.log('\n── Full DB round-trip: decisions ──');
509
+
510
+ {
511
+ openDatabase(':memory:');
512
+
513
+ // Insert via DB
514
+ for (const d of SAMPLE_DECISIONS) {
515
+ upsertDecision({
516
+ id: d.id,
517
+ when_context: d.when_context,
518
+ scope: d.scope,
519
+ decision: d.decision,
520
+ choice: d.choice,
521
+ rationale: d.rationale,
522
+ revisable: d.revisable,
523
+ superseded_by: d.superseded_by,
524
+ });
525
+ }
526
+
527
+ // Generate markdown from DB state
528
+ const adapter = _getAdapter()!;
529
+ const rows = adapter.prepare('SELECT * FROM decisions ORDER BY seq').all();
530
+ const dbDecisions: Decision[] = rows.map(row => ({
531
+ seq: row['seq'] as number,
532
+ id: row['id'] as string,
533
+ when_context: row['when_context'] as string,
534
+ scope: row['scope'] as string,
535
+ decision: row['decision'] as string,
536
+ choice: row['choice'] as string,
537
+ rationale: row['rationale'] as string,
538
+ revisable: row['revisable'] as string,
539
+ superseded_by: (row['superseded_by'] as string) ?? null,
540
+ }));
541
+
542
+ const md = generateDecisionsMd(dbDecisions);
543
+ const parsed = parseDecisionsTable(md);
544
+
545
+ assertEq(parsed.length, SAMPLE_DECISIONS.length, 'DB round-trip decision count');
546
+ for (const orig of SAMPLE_DECISIONS) {
547
+ const rt = parsed.find(p => p.id === orig.id);
548
+ assertTrue(!!rt, `DB round-trip: ${orig.id} found`);
549
+ if (rt) {
550
+ assertEq(rt.scope, orig.scope, `DB round-trip: ${orig.id} scope`);
551
+ assertEq(rt.choice, orig.choice, `DB round-trip: ${orig.id} choice`);
552
+ }
553
+ }
554
+
555
+ closeDatabase();
556
+ }
557
+
558
+ console.log('\n── Full DB round-trip: requirements ──');
559
+
560
+ {
561
+ openDatabase(':memory:');
562
+
563
+ for (const r of SAMPLE_REQUIREMENTS) {
564
+ upsertRequirement(r);
565
+ }
566
+
567
+ const adapter = _getAdapter()!;
568
+ const rows = adapter.prepare('SELECT * FROM requirements ORDER BY id').all();
569
+ const dbReqs: Requirement[] = rows.map(row => ({
570
+ id: row['id'] as string,
571
+ class: row['class'] as string,
572
+ status: row['status'] as string,
573
+ description: row['description'] as string,
574
+ why: row['why'] as string,
575
+ source: row['source'] as string,
576
+ primary_owner: row['primary_owner'] as string,
577
+ supporting_slices: row['supporting_slices'] as string,
578
+ validation: row['validation'] as string,
579
+ notes: row['notes'] as string,
580
+ full_content: row['full_content'] as string,
581
+ superseded_by: (row['superseded_by'] as string) ?? null,
582
+ }));
583
+
584
+ const md = generateRequirementsMd(dbReqs);
585
+ const parsed = parseRequirementsSections(md);
586
+
587
+ assertEq(parsed.length, SAMPLE_REQUIREMENTS.length, 'DB round-trip requirement count');
588
+ for (const orig of SAMPLE_REQUIREMENTS) {
589
+ const rt = parsed.find(p => p.id === orig.id);
590
+ assertTrue(!!rt, `DB round-trip: ${orig.id} found`);
591
+ if (rt) {
592
+ assertEq(rt.class, orig.class, `DB round-trip: ${orig.id} class`);
593
+ assertEq(rt.description, orig.description, `DB round-trip: ${orig.id} description`);
594
+ }
595
+ }
596
+
597
+ closeDatabase();
598
+ }
599
+
600
+ // ═══════════════════════════════════════════════════════════════════════════
601
+
602
+ report();