gsd-pi 2.44.0-dev.62b5d6c → 2.44.0-dev.848dd4c

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 (190) hide show
  1. package/README.md +30 -12
  2. package/dist/resources/extensions/gsd/auto-start.js +10 -0
  3. package/dist/resources/extensions/gsd/commands/handlers/workflow.js +5 -0
  4. package/dist/web/standalone/.next/BUILD_ID +1 -1
  5. package/dist/web/standalone/.next/app-path-routes-manifest.json +18 -18
  6. package/dist/web/standalone/.next/build-manifest.json +2 -2
  7. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  8. package/dist/web/standalone/.next/server/app/_global-error.html +2 -2
  9. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  10. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  11. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  12. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  13. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  14. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  15. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  16. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  17. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  18. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  19. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  20. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  21. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  22. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  23. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  24. package/dist/web/standalone/.next/server/app/index.html +1 -1
  25. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  26. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  27. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  28. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  29. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  30. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  31. package/dist/web/standalone/.next/server/app-paths-manifest.json +18 -18
  32. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  33. package/dist/web/standalone/.next/server/pages/500.html +2 -2
  34. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  35. package/package.json +1 -1
  36. package/packages/pi-coding-agent/dist/core/auth-storage.test.js +6 -8
  37. package/packages/pi-coding-agent/dist/core/auth-storage.test.js.map +1 -1
  38. package/packages/pi-coding-agent/dist/core/extensions/runner.test.js +24 -26
  39. package/packages/pi-coding-agent/dist/core/extensions/runner.test.js.map +1 -1
  40. package/packages/pi-coding-agent/dist/core/fs-utils.test.js +29 -48
  41. package/packages/pi-coding-agent/dist/core/fs-utils.test.js.map +1 -1
  42. package/packages/pi-coding-agent/dist/core/resolve-config-value.test.js +34 -44
  43. package/packages/pi-coding-agent/dist/core/resolve-config-value.test.js.map +1 -1
  44. package/packages/pi-coding-agent/dist/core/session-manager.test.js +30 -34
  45. package/packages/pi-coding-agent/dist/core/session-manager.test.js.map +1 -1
  46. package/packages/pi-coding-agent/dist/core/tools/edit-diff.test.js +10 -12
  47. package/packages/pi-coding-agent/dist/core/tools/edit-diff.test.js.map +1 -1
  48. package/packages/pi-coding-agent/dist/resources/extensions/memory/storage.test.js +43 -47
  49. package/packages/pi-coding-agent/dist/resources/extensions/memory/storage.test.js.map +1 -1
  50. package/packages/pi-coding-agent/src/core/auth-storage.test.ts +7 -7
  51. package/packages/pi-coding-agent/src/core/extensions/runner.test.ts +26 -26
  52. package/packages/pi-coding-agent/src/core/fs-utils.test.ts +31 -43
  53. package/packages/pi-coding-agent/src/core/resolve-config-value.test.ts +40 -45
  54. package/packages/pi-coding-agent/src/core/session-manager.test.ts +33 -33
  55. package/packages/pi-coding-agent/src/core/tools/edit-diff.test.ts +17 -17
  56. package/packages/pi-coding-agent/src/resources/extensions/memory/storage.test.ts +74 -74
  57. package/src/resources/extensions/gsd/auto-start.ts +14 -0
  58. package/src/resources/extensions/gsd/commands/handlers/workflow.ts +8 -0
  59. package/src/resources/extensions/gsd/tests/all-milestones-complete-merge.test.ts +99 -99
  60. package/src/resources/extensions/gsd/tests/auto-lock-creation.test.ts +14 -16
  61. package/src/resources/extensions/gsd/tests/auto-paused-session-validation.test.ts +43 -57
  62. package/src/resources/extensions/gsd/tests/auto-preflight.test.ts +11 -13
  63. package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +465 -523
  64. package/src/resources/extensions/gsd/tests/auto-secrets-gate.test.ts +73 -75
  65. package/src/resources/extensions/gsd/tests/auto-start-needs-discussion.test.ts +34 -56
  66. package/src/resources/extensions/gsd/tests/auto-worktree-milestone-merge.test.ts +533 -656
  67. package/src/resources/extensions/gsd/tests/auto-worktree.test.ts +165 -143
  68. package/src/resources/extensions/gsd/tests/cache-staleness-regression.test.ts +29 -52
  69. package/src/resources/extensions/gsd/tests/captures.test.ts +148 -176
  70. package/src/resources/extensions/gsd/tests/claude-import-tui.test.ts +32 -33
  71. package/src/resources/extensions/gsd/tests/collect-from-manifest.test.ts +141 -143
  72. package/src/resources/extensions/gsd/tests/commands-inspect-open-db.test.ts +25 -25
  73. package/src/resources/extensions/gsd/tests/commands-logs.test.ts +81 -81
  74. package/src/resources/extensions/gsd/tests/complete-milestone.test.ts +38 -59
  75. package/src/resources/extensions/gsd/tests/complete-slice.test.ts +228 -263
  76. package/src/resources/extensions/gsd/tests/complete-task.test.ts +250 -302
  77. package/src/resources/extensions/gsd/tests/context-store.test.ts +354 -367
  78. package/src/resources/extensions/gsd/tests/continue-here.test.ts +68 -72
  79. package/src/resources/extensions/gsd/tests/cost-projection.test.ts +92 -106
  80. package/src/resources/extensions/gsd/tests/crash-recovery.test.ts +27 -35
  81. package/src/resources/extensions/gsd/tests/dashboard-budget.test.ts +220 -237
  82. package/src/resources/extensions/gsd/tests/db-writer.test.ts +390 -420
  83. package/src/resources/extensions/gsd/tests/definition-loader.test.ts +76 -92
  84. package/src/resources/extensions/gsd/tests/derive-state-crossval.test.ts +68 -83
  85. package/src/resources/extensions/gsd/tests/derive-state-db.test.ts +152 -183
  86. package/src/resources/extensions/gsd/tests/derive-state-deps.test.ts +78 -101
  87. package/src/resources/extensions/gsd/tests/derive-state.test.ts +192 -227
  88. package/src/resources/extensions/gsd/tests/detection.test.ts +232 -278
  89. package/src/resources/extensions/gsd/tests/dev-engine-wrapper.test.ts +30 -34
  90. package/src/resources/extensions/gsd/tests/dispatch-guard.test.ts +164 -180
  91. package/src/resources/extensions/gsd/tests/dispatch-missing-task-plans.test.ts +43 -49
  92. package/src/resources/extensions/gsd/tests/dispatch-uat-last-completed.test.ts +28 -32
  93. package/src/resources/extensions/gsd/tests/doctor-completion-deferral.test.ts +27 -29
  94. package/src/resources/extensions/gsd/tests/doctor-delimiter-fix.test.ts +34 -38
  95. package/src/resources/extensions/gsd/tests/doctor-enhancements.test.ts +54 -75
  96. package/src/resources/extensions/gsd/tests/doctor-environment-worktree.test.ts +21 -32
  97. package/src/resources/extensions/gsd/tests/doctor-environment.test.ts +72 -97
  98. package/src/resources/extensions/gsd/tests/doctor-fixlevel.test.ts +38 -44
  99. package/src/resources/extensions/gsd/tests/doctor-git.test.ts +104 -145
  100. package/src/resources/extensions/gsd/tests/doctor-proactive.test.ts +84 -106
  101. package/src/resources/extensions/gsd/tests/doctor-roadmap-summary-atomicity.test.ts +54 -60
  102. package/src/resources/extensions/gsd/tests/doctor-runtime.test.ts +72 -93
  103. package/src/resources/extensions/gsd/tests/doctor.test.ts +104 -134
  104. package/src/resources/extensions/gsd/tests/ensure-db-open.test.ts +123 -131
  105. package/src/resources/extensions/gsd/tests/exit-command.test.ts +20 -24
  106. package/src/resources/extensions/gsd/tests/feature-branch-lifecycle-integration.test.ts +48 -57
  107. package/src/resources/extensions/gsd/tests/files-loadfile-eisdir.test.ts +5 -7
  108. package/src/resources/extensions/gsd/tests/flag-file-db.test.ts +30 -42
  109. package/src/resources/extensions/gsd/tests/freeform-decisions.test.ts +198 -206
  110. package/src/resources/extensions/gsd/tests/git-locale.test.ts +13 -27
  111. package/src/resources/extensions/gsd/tests/git-service.test.ts +285 -388
  112. package/src/resources/extensions/gsd/tests/gitignore-tracked-gsd.test.ts +31 -39
  113. package/src/resources/extensions/gsd/tests/graph-operations.test.ts +63 -69
  114. package/src/resources/extensions/gsd/tests/gsd-db.test.ts +255 -264
  115. package/src/resources/extensions/gsd/tests/gsd-inspect.test.ts +108 -119
  116. package/src/resources/extensions/gsd/tests/gsd-recover.test.ts +81 -103
  117. package/src/resources/extensions/gsd/tests/gsd-tools.test.ts +229 -262
  118. package/src/resources/extensions/gsd/tests/headless-answers.test.ts +13 -13
  119. package/src/resources/extensions/gsd/tests/health-widget.test.ts +29 -37
  120. package/src/resources/extensions/gsd/tests/idle-recovery.test.ts +81 -102
  121. package/src/resources/extensions/gsd/tests/init-wizard.test.ts +16 -18
  122. package/src/resources/extensions/gsd/tests/integration-edge.test.ts +41 -46
  123. package/src/resources/extensions/gsd/tests/integration-lifecycle.test.ts +42 -53
  124. package/src/resources/extensions/gsd/tests/integration-mixed-milestones.test.ts +75 -91
  125. package/src/resources/extensions/gsd/tests/integration-proof.test.ts +18 -18
  126. package/src/resources/extensions/gsd/tests/markdown-renderer.test.ts +150 -194
  127. package/src/resources/extensions/gsd/tests/md-importer.test.ts +101 -125
  128. package/src/resources/extensions/gsd/tests/memory-extractor.test.ts +45 -54
  129. package/src/resources/extensions/gsd/tests/memory-store.test.ts +80 -93
  130. package/src/resources/extensions/gsd/tests/migrate-command.test.ts +57 -66
  131. package/src/resources/extensions/gsd/tests/migrate-hierarchy.test.ts +83 -93
  132. package/src/resources/extensions/gsd/tests/migrate-parser.test.ts +161 -170
  133. package/src/resources/extensions/gsd/tests/migrate-transformer.test.ts +125 -141
  134. package/src/resources/extensions/gsd/tests/migrate-validator-parsers.test.ts +107 -131
  135. package/src/resources/extensions/gsd/tests/migrate-writer-integration.test.ts +87 -96
  136. package/src/resources/extensions/gsd/tests/migrate-writer.test.ts +125 -164
  137. package/src/resources/extensions/gsd/tests/must-have-parser.test.ts +81 -94
  138. package/src/resources/extensions/gsd/tests/none-mode-gates.test.ts +35 -36
  139. package/src/resources/extensions/gsd/tests/overrides.test.ts +99 -106
  140. package/src/resources/extensions/gsd/tests/parallel-crash-recovery.test.ts +40 -47
  141. package/src/resources/extensions/gsd/tests/parallel-worker-monitoring.test.ts +25 -28
  142. package/src/resources/extensions/gsd/tests/parallel-workers-multi-milestone-e2e.test.ts +66 -83
  143. package/src/resources/extensions/gsd/tests/park-edge-cases.test.ts +54 -77
  144. package/src/resources/extensions/gsd/tests/park-milestone.test.ts +68 -115
  145. package/src/resources/extensions/gsd/tests/parsers.test.ts +546 -611
  146. package/src/resources/extensions/gsd/tests/paths.test.ts +72 -87
  147. package/src/resources/extensions/gsd/tests/post-unit-hooks.test.ts +77 -117
  148. package/src/resources/extensions/gsd/tests/prompt-db.test.ts +56 -56
  149. package/src/resources/extensions/gsd/tests/queue-draft-detection.test.ts +93 -119
  150. package/src/resources/extensions/gsd/tests/queue-order.test.ts +70 -82
  151. package/src/resources/extensions/gsd/tests/queue-reorder-e2e.test.ts +42 -55
  152. package/src/resources/extensions/gsd/tests/quick-auto-guard.test.ts +100 -0
  153. package/src/resources/extensions/gsd/tests/quick-branch-lifecycle.test.ts +45 -73
  154. package/src/resources/extensions/gsd/tests/reassess-prompt.test.ts +28 -38
  155. package/src/resources/extensions/gsd/tests/replan-slice.test.ts +73 -80
  156. package/src/resources/extensions/gsd/tests/repo-identity-worktree.test.ts +71 -74
  157. package/src/resources/extensions/gsd/tests/requirements.test.ts +70 -75
  158. package/src/resources/extensions/gsd/tests/retry-state-reset.test.ts +44 -66
  159. package/src/resources/extensions/gsd/tests/roadmap-parse-regression.test.ts +114 -181
  160. package/src/resources/extensions/gsd/tests/rule-registry.test.ts +63 -65
  161. package/src/resources/extensions/gsd/tests/run-uat.test.ts +66 -128
  162. package/src/resources/extensions/gsd/tests/session-lock-multipath.test.ts +18 -25
  163. package/src/resources/extensions/gsd/tests/session-lock-regression.test.ts +37 -44
  164. package/src/resources/extensions/gsd/tests/shared-wal.test.ts +19 -26
  165. package/src/resources/extensions/gsd/tests/sqlite-unavailable-gate.test.ts +63 -0
  166. package/src/resources/extensions/gsd/tests/stalled-tool-recovery.test.ts +6 -8
  167. package/src/resources/extensions/gsd/tests/symlink-numbered-variants.test.ts +22 -28
  168. package/src/resources/extensions/gsd/tests/token-savings.test.ts +54 -56
  169. package/src/resources/extensions/gsd/tests/tool-call-loop-guard.test.ts +23 -25
  170. package/src/resources/extensions/gsd/tests/tool-naming.test.ts +9 -11
  171. package/src/resources/extensions/gsd/tests/unique-milestone-ids.test.ts +66 -82
  172. package/src/resources/extensions/gsd/tests/unit-runtime.test.ts +46 -47
  173. package/src/resources/extensions/gsd/tests/visualizer-critical-path.test.ts +20 -22
  174. package/src/resources/extensions/gsd/tests/visualizer-data.test.ts +84 -86
  175. package/src/resources/extensions/gsd/tests/visualizer-overlay.test.ts +41 -43
  176. package/src/resources/extensions/gsd/tests/visualizer-views.test.ts +94 -96
  177. package/src/resources/extensions/gsd/tests/windows-path-normalization.test.ts +11 -13
  178. package/src/resources/extensions/gsd/tests/worker-registry.test.ts +27 -29
  179. package/src/resources/extensions/gsd/tests/workflow-templates.test.ts +50 -52
  180. package/src/resources/extensions/gsd/tests/worktree-bugfix.test.ts +10 -13
  181. package/src/resources/extensions/gsd/tests/worktree-db-integration.test.ts +14 -18
  182. package/src/resources/extensions/gsd/tests/worktree-db.test.ts +38 -39
  183. package/src/resources/extensions/gsd/tests/worktree-e2e.test.ts +17 -21
  184. package/src/resources/extensions/gsd/tests/worktree-health.test.ts +25 -30
  185. package/src/resources/extensions/gsd/tests/worktree-integration.test.ts +30 -37
  186. package/src/resources/extensions/gsd/tests/worktree-symlink-removal.test.ts +15 -22
  187. package/src/resources/extensions/gsd/tests/worktree-sync-milestones.test.ts +59 -66
  188. package/src/resources/extensions/gsd/tests/worktree.test.ts +44 -50
  189. /package/dist/web/standalone/.next/static/{fOnWQBjWXMKUs4bqTg530 → -zps1Q9mQmioAKLcQiCr8}/_buildManifest.js +0 -0
  190. /package/dist/web/standalone/.next/static/{fOnWQBjWXMKUs4bqTg530 → -zps1Q9mQmioAKLcQiCr8}/_ssgManifest.js +0 -0
@@ -1,4 +1,5 @@
1
- import { createTestContext } from './test-helpers.ts';
1
+ import { describe, test } from 'node:test';
2
+ import assert from 'node:assert/strict';
2
3
  import * as path from 'node:path';
3
4
  import * as os from 'node:os';
4
5
  import * as fs from 'node:fs';
@@ -26,8 +27,6 @@ import {
26
27
  } from '../db-writer.ts';
27
28
  import type { Decision, Requirement } from '../types.ts';
28
29
 
29
- const { assertEq, assertTrue, assertMatch, report } = createTestContext();
30
-
31
30
  // ═══════════════════════════════════════════════════════════════════════════
32
31
  // Helpers
33
32
  // ═══════════════════════════════════════════════════════════════════════════
@@ -151,462 +150,433 @@ const SAMPLE_REQUIREMENTS: Requirement[] = [
151
150
  // Round-Trip Tests: Decisions
152
151
  // ═══════════════════════════════════════════════════════════════════════════
153
152
 
154
- console.log('\n── generateDecisionsMd round-trip ──');
155
-
156
- {
157
- const md = generateDecisionsMd(SAMPLE_DECISIONS);
158
- const parsed = parseDecisionsTable(md);
159
-
160
- assertEq(parsed.length, SAMPLE_DECISIONS.length, 'decisions count matches');
161
-
162
- for (let i = 0; i < SAMPLE_DECISIONS.length; i++) {
163
- const orig = SAMPLE_DECISIONS[i];
164
- const rt = parsed[i];
165
- assertEq(rt.id, orig.id, `decision ${orig.id} id round-trips`);
166
- assertEq(rt.when_context, orig.when_context, `decision ${orig.id} when_context round-trips`);
167
- assertEq(rt.scope, orig.scope, `decision ${orig.id} scope round-trips`);
168
- assertEq(rt.decision, orig.decision, `decision ${orig.id} decision round-trips`);
169
- assertEq(rt.choice, orig.choice, `decision ${orig.id} choice round-trips`);
170
- assertEq(rt.rationale, orig.rationale, `decision ${orig.id} rationale round-trips`);
171
- assertEq(rt.revisable, orig.revisable, `decision ${orig.id} revisable round-trips`);
172
- assertEq(rt.made_by, orig.made_by, `decision ${orig.id} made_by round-trips`);
173
- }
174
- }
175
-
176
- console.log('\n── generateDecisionsMd format ──');
177
-
178
- {
179
- const md = generateDecisionsMd(SAMPLE_DECISIONS);
180
- assertTrue(md.startsWith('# Decisions Register\n'), 'starts with H1 header');
181
- assertTrue(md.includes('<!-- Append-only'), 'contains HTML comment block');
182
- assertTrue(md.includes('| # | When | Scope'), 'contains table header');
183
- assertTrue(md.includes('|---|------|-------'), 'contains separator row');
184
- assertTrue(md.includes('| Made By |'), 'contains Made By column header');
185
- }
186
-
187
- console.log('\n── generateDecisionsMd empty input ──');
188
-
189
- {
190
- const md = generateDecisionsMd([]);
191
- const parsed = parseDecisionsTable(md);
192
- assertEq(parsed.length, 0, 'empty decisions produces empty parse');
193
- assertTrue(md.includes('| # | When | Scope'), 'still has table header even when empty');
194
- }
153
+ describe('db-writer', () => {
154
+ test('generateDecisionsMd round-trip', () => {
155
+ const md = generateDecisionsMd(SAMPLE_DECISIONS);
156
+ const parsed = parseDecisionsTable(md);
157
+
158
+ assert.deepStrictEqual(parsed.length, SAMPLE_DECISIONS.length, 'decisions count matches');
159
+
160
+ for (let i = 0; i < SAMPLE_DECISIONS.length; i++) {
161
+ const orig = SAMPLE_DECISIONS[i];
162
+ const rt = parsed[i];
163
+ assert.deepStrictEqual(rt.id, orig.id, `decision ${orig.id} id round-trips`);
164
+ assert.deepStrictEqual(rt.when_context, orig.when_context, `decision ${orig.id} when_context round-trips`);
165
+ assert.deepStrictEqual(rt.scope, orig.scope, `decision ${orig.id} scope round-trips`);
166
+ assert.deepStrictEqual(rt.decision, orig.decision, `decision ${orig.id} decision round-trips`);
167
+ assert.deepStrictEqual(rt.choice, orig.choice, `decision ${orig.id} choice round-trips`);
168
+ assert.deepStrictEqual(rt.rationale, orig.rationale, `decision ${orig.id} rationale round-trips`);
169
+ assert.deepStrictEqual(rt.revisable, orig.revisable, `decision ${orig.id} revisable round-trips`);
170
+ assert.deepStrictEqual(rt.made_by, orig.made_by, `decision ${orig.id} made_by round-trips`);
171
+ }
172
+ });
195
173
 
196
- console.log('\n── generateDecisionsMd pipe escaping ──');
174
+ test('generateDecisionsMd format', () => {
175
+ const md = generateDecisionsMd(SAMPLE_DECISIONS);
176
+ assert.ok(md.startsWith('# Decisions Register\n'), 'starts with H1 header');
177
+ assert.ok(md.includes('<!-- Append-only'), 'contains HTML comment block');
178
+ assert.ok(md.includes('| # | When | Scope'), 'contains table header');
179
+ assert.ok(md.includes('|---|------|-------'), 'contains separator row');
180
+ assert.ok(md.includes('| Made By |'), 'contains Made By column header');
181
+ });
197
182
 
198
- {
199
- const withPipe: Decision = {
200
- seq: 1,
201
- id: 'D001',
202
- when_context: 'M001',
203
- scope: 'arch',
204
- decision: 'Choice A | Choice B comparison',
205
- choice: 'A',
206
- rationale: 'Better',
207
- revisable: 'No',
208
- made_by: 'agent',
209
- superseded_by: null,
210
- };
211
- const md = generateDecisionsMd([withPipe]);
212
- // Should not break the table — pipe in decision text should be escaped
213
- const parsed = parseDecisionsTable(md);
214
- assertTrue(parsed.length >= 1, 'pipe-containing decision parses without breaking table');
215
- }
183
+ test('generateDecisionsMd empty input', () => {
184
+ const md = generateDecisionsMd([]);
185
+ const parsed = parseDecisionsTable(md);
186
+ assert.deepStrictEqual(parsed.length, 0, 'empty decisions produces empty parse');
187
+ assert.ok(md.includes('| # | When | Scope'), 'still has table header even when empty');
188
+ });
216
189
 
217
- // ═══════════════════════════════════════════════════════════════════════════
218
- // Round-Trip Tests: Requirements
219
- // ═══════════════════════════════════════════════════════════════════════════
190
+ test('generateDecisionsMd pipe escaping', () => {
191
+ const withPipe: Decision = {
192
+ seq: 1,
193
+ id: 'D001',
194
+ when_context: 'M001',
195
+ scope: 'arch',
196
+ decision: 'Choice A | Choice B comparison',
197
+ choice: 'A',
198
+ rationale: 'Better',
199
+ revisable: 'No',
200
+ made_by: 'agent',
201
+ superseded_by: null,
202
+ };
203
+ const md = generateDecisionsMd([withPipe]);
204
+ // Should not break the table — pipe in decision text should be escaped
205
+ const parsed = parseDecisionsTable(md);
206
+ assert.ok(parsed.length >= 1, 'pipe-containing decision parses without breaking table');
207
+ });
220
208
 
221
- console.log('\n── generateRequirementsMd round-trip ──');
222
-
223
- {
224
- const md = generateRequirementsMd(SAMPLE_REQUIREMENTS);
225
- const parsed = parseRequirementsSections(md);
226
-
227
- assertEq(parsed.length, SAMPLE_REQUIREMENTS.length, 'requirements count matches');
228
-
229
- for (const orig of SAMPLE_REQUIREMENTS) {
230
- const rt = parsed.find(r => r.id === orig.id);
231
- assertTrue(!!rt, `requirement ${orig.id} found in parsed output`);
232
- if (rt) {
233
- assertEq(rt.class, orig.class, `requirement ${orig.id} class round-trips`);
234
- assertEq(rt.description, orig.description, `requirement ${orig.id} description round-trips`);
235
- assertEq(rt.why, orig.why, `requirement ${orig.id} why round-trips`);
236
- assertEq(rt.source, orig.source, `requirement ${orig.id} source round-trips`);
237
- assertEq(rt.primary_owner, orig.primary_owner, `requirement ${orig.id} primary_owner round-trips`);
238
- assertEq(rt.supporting_slices, orig.supporting_slices, `requirement ${orig.id} supporting_slices round-trips`);
239
- if (orig.notes) {
240
- assertEq(rt.notes, orig.notes, `requirement ${orig.id} notes round-trips`);
209
+ // ═══════════════════════════════════════════════════════════════════════════
210
+ // Round-Trip Tests: Requirements
211
+ // ═══════════════════════════════════════════════════════════════════════════
212
+
213
+ test('generateRequirementsMd round-trip', () => {
214
+ const md = generateRequirementsMd(SAMPLE_REQUIREMENTS);
215
+ const parsed = parseRequirementsSections(md);
216
+
217
+ assert.deepStrictEqual(parsed.length, SAMPLE_REQUIREMENTS.length, 'requirements count matches');
218
+
219
+ for (const orig of SAMPLE_REQUIREMENTS) {
220
+ const rt = parsed.find(r => r.id === orig.id);
221
+ assert.ok(!!rt, `requirement ${orig.id} found in parsed output`);
222
+ if (rt) {
223
+ assert.deepStrictEqual(rt.class, orig.class, `requirement ${orig.id} class round-trips`);
224
+ assert.deepStrictEqual(rt.description, orig.description, `requirement ${orig.id} description round-trips`);
225
+ assert.deepStrictEqual(rt.why, orig.why, `requirement ${orig.id} why round-trips`);
226
+ assert.deepStrictEqual(rt.source, orig.source, `requirement ${orig.id} source round-trips`);
227
+ assert.deepStrictEqual(rt.primary_owner, orig.primary_owner, `requirement ${orig.id} primary_owner round-trips`);
228
+ assert.deepStrictEqual(rt.supporting_slices, orig.supporting_slices, `requirement ${orig.id} supporting_slices round-trips`);
229
+ if (orig.notes) {
230
+ assert.deepStrictEqual(rt.notes, orig.notes, `requirement ${orig.id} notes round-trips`);
231
+ }
241
232
  }
242
233
  }
243
- }
244
- }
245
-
246
- console.log('\n── generateRequirementsMd sections ──');
247
-
248
- {
249
- const md = generateRequirementsMd(SAMPLE_REQUIREMENTS);
250
- assertTrue(md.includes('## Active'), 'has Active section');
251
- assertTrue(md.includes('## Validated'), 'has Validated section');
252
- assertTrue(md.includes('## Deferred'), 'has Deferred section');
253
- assertTrue(md.includes('## Out of Scope'), 'has Out of Scope section');
254
- assertTrue(md.includes('## Traceability'), 'has Traceability section');
255
- assertTrue(md.includes('## Coverage Summary'), 'has Coverage Summary section');
256
- }
234
+ });
257
235
 
258
- console.log('\n── generateRequirementsMd only populated sections ──');
236
+ test('generateRequirementsMd sections', () => {
237
+ const md = generateRequirementsMd(SAMPLE_REQUIREMENTS);
238
+ assert.ok(md.includes('## Active'), 'has Active section');
239
+ assert.ok(md.includes('## Validated'), 'has Validated section');
240
+ assert.ok(md.includes('## Deferred'), 'has Deferred section');
241
+ assert.ok(md.includes('## Out of Scope'), 'has Out of Scope section');
242
+ assert.ok(md.includes('## Traceability'), 'has Traceability section');
243
+ assert.ok(md.includes('## Coverage Summary'), 'has Coverage Summary section');
244
+ });
259
245
 
260
- {
261
- // Only active requirements — should only have Active section
262
- const activeOnly = SAMPLE_REQUIREMENTS.filter(r => r.status === 'active');
263
- const md = generateRequirementsMd(activeOnly);
264
- assertTrue(md.includes('## Active'), 'has Active section');
265
- assertTrue(!md.includes('## Validated'), 'no Validated section when no validated reqs');
266
- assertTrue(!md.includes('## Deferred'), 'no Deferred section when no deferred reqs');
267
- assertTrue(!md.includes('## Out of Scope'), 'no Out of Scope section when no out-of-scope reqs');
268
- }
246
+ test('generateRequirementsMd only populated sections', () => {
247
+ // Only active requirements — should only have Active section
248
+ const activeOnly = SAMPLE_REQUIREMENTS.filter(r => r.status === 'active');
249
+ const md = generateRequirementsMd(activeOnly);
250
+ assert.ok(md.includes('## Active'), 'has Active section');
251
+ assert.ok(!md.includes('## Validated'), 'no Validated section when no validated reqs');
252
+ assert.ok(!md.includes('## Deferred'), 'no Deferred section when no deferred reqs');
253
+ assert.ok(!md.includes('## Out of Scope'), 'no Out of Scope section when no out-of-scope reqs');
254
+ });
269
255
 
270
- console.log('\n── generateRequirementsMd empty input ──');
256
+ test('generateRequirementsMd empty input', () => {
257
+ const md = generateRequirementsMd([]);
258
+ const parsed = parseRequirementsSections(md);
259
+ assert.deepStrictEqual(parsed.length, 0, 'empty requirements produces empty parse');
260
+ });
271
261
 
272
- {
273
- const md = generateRequirementsMd([]);
274
- const parsed = parseRequirementsSections(md);
275
- assertEq(parsed.length, 0, 'empty requirements produces empty parse');
276
- }
262
+ // ═══════════════════════════════════════════════════════════════════════════
263
+ // nextDecisionId Tests
264
+ // ═══════════════════════════════════════════════════════════════════════════
277
265
 
278
- // ═══════════════════════════════════════════════════════════════════════════
279
- // nextDecisionId Tests
280
- // ═══════════════════════════════════════════════════════════════════════════
266
+ test('nextDecisionId', async () => {
267
+ // Open in-memory DB
268
+ openDatabase(':memory:');
281
269
 
282
- console.log('\n── nextDecisionId ──');
270
+ const id1 = await nextDecisionId();
271
+ assert.deepStrictEqual(id1, 'D001', 'first ID when no decisions exist');
283
272
 
284
- {
285
- // Open in-memory DB
286
- openDatabase(':memory:');
273
+ // Insert some decisions
274
+ upsertDecision({
275
+ id: 'D001',
276
+ when_context: 'M001',
277
+ scope: 'test',
278
+ decision: 'test decision',
279
+ choice: 'test choice',
280
+ rationale: 'test',
281
+ revisable: 'No',
282
+ made_by: 'agent',
283
+ superseded_by: null,
284
+ });
285
+ upsertDecision({
286
+ id: 'D005',
287
+ when_context: 'M001',
288
+ scope: 'test',
289
+ decision: 'test decision 5',
290
+ choice: 'test choice',
291
+ rationale: 'test',
292
+ revisable: 'No',
293
+ made_by: 'agent',
294
+ superseded_by: null,
295
+ });
287
296
 
288
- const id1 = await nextDecisionId();
289
- assertEq(id1, 'D001', 'first ID when no decisions exist');
297
+ const id2 = await nextDecisionId();
298
+ assert.deepStrictEqual(id2, 'D006', 'next ID after D005 is D006');
290
299
 
291
- // Insert some decisions
292
- upsertDecision({
293
- id: 'D001',
294
- when_context: 'M001',
295
- scope: 'test',
296
- decision: 'test decision',
297
- choice: 'test choice',
298
- rationale: 'test',
299
- revisable: 'No',
300
- made_by: 'agent',
301
- superseded_by: null,
302
- });
303
- upsertDecision({
304
- id: 'D005',
305
- when_context: 'M001',
306
- scope: 'test',
307
- decision: 'test decision 5',
308
- choice: 'test choice',
309
- rationale: 'test',
310
- revisable: 'No',
311
- made_by: 'agent',
312
- superseded_by: null,
300
+ closeDatabase();
313
301
  });
314
302
 
315
- const id2 = await nextDecisionId();
316
- assertEq(id2, 'D006', 'next ID after D005 is D006');
317
-
318
- closeDatabase();
319
- }
320
-
321
- // ═══════════════════════════════════════════════════════════════════════════
322
- // saveDecisionToDb Tests
323
- // ═══════════════════════════════════════════════════════════════════════════
324
-
325
- console.log('\n── saveDecisionToDb ──');
303
+ // ═══════════════════════════════════════════════════════════════════════════
304
+ // saveDecisionToDb Tests
305
+ // ═══════════════════════════════════════════════════════════════════════════
326
306
 
327
- {
328
- const tmpDir = makeTmpDir();
329
- const dbPath = path.join(tmpDir, '.gsd', 'gsd.db');
330
- openDatabase(dbPath);
307
+ test('saveDecisionToDb', async () => {
308
+ const tmpDir = makeTmpDir();
309
+ const dbPath = path.join(tmpDir, '.gsd', 'gsd.db');
310
+ openDatabase(dbPath);
331
311
 
332
- try {
333
- const result = await saveDecisionToDb({
334
- scope: 'arch',
335
- decision: 'Test decision',
336
- choice: 'Option A',
337
- rationale: 'Best option',
338
- when_context: 'M001',
339
- }, tmpDir);
340
-
341
- assertEq(result.id, 'D001', 'saveDecisionToDb returns D001 as first ID');
342
-
343
- // Verify DB state
344
- const dbDecision = getDecisionById('D001');
345
- assertTrue(!!dbDecision, 'decision exists in DB after save');
346
- assertEq(dbDecision?.scope, 'arch', 'DB decision has correct scope');
347
- assertEq(dbDecision?.choice, 'Option A', 'DB decision has correct choice');
348
-
349
- // Verify markdown file was written
350
- const mdPath = path.join(tmpDir, '.gsd', 'DECISIONS.md');
351
- assertTrue(fs.existsSync(mdPath), 'DECISIONS.md file created');
352
-
353
- const mdContent = fs.readFileSync(mdPath, 'utf-8');
354
- assertTrue(mdContent.includes('D001'), 'DECISIONS.md contains new decision ID');
355
- assertTrue(mdContent.includes('Test decision'), 'DECISIONS.md contains decision text');
356
-
357
- // Verify round-trip of the written file
358
- const parsed = parseDecisionsTable(mdContent);
359
- assertEq(parsed.length, 1, 'written DECISIONS.md parses to 1 decision');
360
- assertEq(parsed[0].id, 'D001', 'parsed decision has correct ID');
361
-
362
- // Add second decision
363
- const result2 = await saveDecisionToDb({
364
- scope: 'impl',
365
- decision: 'Second decision',
366
- choice: 'Option B',
367
- rationale: 'Also good',
368
- }, tmpDir);
369
-
370
- assertEq(result2.id, 'D002', 'second decision gets D002');
371
-
372
- const mdContent2 = fs.readFileSync(mdPath, 'utf-8');
373
- const parsed2 = parseDecisionsTable(mdContent2);
374
- assertEq(parsed2.length, 2, 'DECISIONS.md now has 2 decisions');
375
- } finally {
376
- closeDatabase();
377
- cleanupDir(tmpDir);
378
- }
379
- }
312
+ try {
313
+ const result = await saveDecisionToDb({
314
+ scope: 'arch',
315
+ decision: 'Test decision',
316
+ choice: 'Option A',
317
+ rationale: 'Best option',
318
+ when_context: 'M001',
319
+ }, tmpDir);
320
+
321
+ assert.deepStrictEqual(result.id, 'D001', 'saveDecisionToDb returns D001 as first ID');
322
+
323
+ // Verify DB state
324
+ const dbDecision = getDecisionById('D001');
325
+ assert.ok(!!dbDecision, 'decision exists in DB after save');
326
+ assert.deepStrictEqual(dbDecision?.scope, 'arch', 'DB decision has correct scope');
327
+ assert.deepStrictEqual(dbDecision?.choice, 'Option A', 'DB decision has correct choice');
328
+
329
+ // Verify markdown file was written
330
+ const mdPath = path.join(tmpDir, '.gsd', 'DECISIONS.md');
331
+ assert.ok(fs.existsSync(mdPath), 'DECISIONS.md file created');
332
+
333
+ const mdContent = fs.readFileSync(mdPath, 'utf-8');
334
+ assert.ok(mdContent.includes('D001'), 'DECISIONS.md contains new decision ID');
335
+ assert.ok(mdContent.includes('Test decision'), 'DECISIONS.md contains decision text');
336
+
337
+ // Verify round-trip of the written file
338
+ const parsed = parseDecisionsTable(mdContent);
339
+ assert.deepStrictEqual(parsed.length, 1, 'written DECISIONS.md parses to 1 decision');
340
+ assert.deepStrictEqual(parsed[0].id, 'D001', 'parsed decision has correct ID');
341
+
342
+ // Add second decision
343
+ const result2 = await saveDecisionToDb({
344
+ scope: 'impl',
345
+ decision: 'Second decision',
346
+ choice: 'Option B',
347
+ rationale: 'Also good',
348
+ }, tmpDir);
349
+
350
+ assert.deepStrictEqual(result2.id, 'D002', 'second decision gets D002');
351
+
352
+ const mdContent2 = fs.readFileSync(mdPath, 'utf-8');
353
+ const parsed2 = parseDecisionsTable(mdContent2);
354
+ assert.deepStrictEqual(parsed2.length, 2, 'DECISIONS.md now has 2 decisions');
355
+ } finally {
356
+ closeDatabase();
357
+ cleanupDir(tmpDir);
358
+ }
359
+ });
380
360
 
381
- // ═══════════════════════════════════════════════════════════════════════════
382
- // updateRequirementInDb Tests
383
- // ═══════════════════════════════════════════════════════════════════════════
361
+ // ═══════════════════════════════════════════════════════════════════════════
362
+ // updateRequirementInDb Tests
363
+ // ═══════════════════════════════════════════════════════════════════════════
384
364
 
385
- console.log('\n── updateRequirementInDb ──');
365
+ test('updateRequirementInDb', async () => {
366
+ const tmpDir = makeTmpDir();
367
+ const dbPath = path.join(tmpDir, '.gsd', 'gsd.db');
368
+ openDatabase(dbPath);
386
369
 
387
- {
388
- const tmpDir = makeTmpDir();
389
- const dbPath = path.join(tmpDir, '.gsd', 'gsd.db');
390
- openDatabase(dbPath);
370
+ try {
371
+ // Seed a requirement
372
+ upsertRequirement({
373
+ id: 'R001',
374
+ class: 'core-capability',
375
+ status: 'active',
376
+ description: 'Test requirement',
377
+ why: 'Testing',
378
+ source: 'test',
379
+ primary_owner: 'M001/S01',
380
+ supporting_slices: 'none',
381
+ validation: 'unmapped',
382
+ notes: '',
383
+ full_content: '',
384
+ superseded_by: null,
385
+ });
386
+
387
+ // Update it
388
+ await updateRequirementInDb('R001', {
389
+ status: 'validated',
390
+ validation: 'S01 — all tests pass',
391
+ notes: 'Validated in S01',
392
+ }, tmpDir);
393
+
394
+ // Verify DB state
395
+ const updated = getRequirementById('R001');
396
+ assert.ok(!!updated, 'requirement still exists after update');
397
+ assert.deepStrictEqual(updated?.status, 'validated', 'status updated in DB');
398
+ assert.deepStrictEqual(updated?.validation, 'S01 — all tests pass', 'validation updated in DB');
399
+ assert.deepStrictEqual(updated?.description, 'Test requirement', 'description preserved after update');
400
+
401
+ // Verify markdown file was written
402
+ const mdPath = path.join(tmpDir, '.gsd', 'REQUIREMENTS.md');
403
+ assert.ok(fs.existsSync(mdPath), 'REQUIREMENTS.md file created');
404
+
405
+ const mdContent = fs.readFileSync(mdPath, 'utf-8');
406
+ assert.ok(mdContent.includes('R001'), 'REQUIREMENTS.md contains requirement ID');
407
+ assert.ok(mdContent.includes('validated'), 'REQUIREMENTS.md shows updated status');
408
+
409
+ // Verify round-trip
410
+ const parsed = parseRequirementsSections(mdContent);
411
+ assert.deepStrictEqual(parsed.length, 1, 'parsed 1 requirement from written file');
412
+ assert.deepStrictEqual(parsed[0].status, 'validated', 'parsed status matches update');
413
+ } finally {
414
+ closeDatabase();
415
+ cleanupDir(tmpDir);
416
+ }
417
+ });
391
418
 
392
- try {
393
- // Seed a requirement
394
- upsertRequirement({
395
- id: 'R001',
396
- class: 'core-capability',
397
- status: 'active',
398
- description: 'Test requirement',
399
- why: 'Testing',
400
- source: 'test',
401
- primary_owner: 'M001/S01',
402
- supporting_slices: 'none',
403
- validation: 'unmapped',
404
- notes: '',
405
- full_content: '',
406
- superseded_by: null,
407
- });
419
+ test('updateRequirementInDb — not found', async () => {
420
+ const tmpDir = makeTmpDir();
421
+ const dbPath = path.join(tmpDir, '.gsd', 'gsd.db');
422
+ openDatabase(dbPath);
408
423
 
409
- // Update it
410
- await updateRequirementInDb('R001', {
411
- status: 'validated',
412
- validation: 'S01 all tests pass',
413
- notes: 'Validated in S01',
414
- }, tmpDir);
415
-
416
- // Verify DB state
417
- const updated = getRequirementById('R001');
418
- assertTrue(!!updated, 'requirement still exists after update');
419
- assertEq(updated?.status, 'validated', 'status updated in DB');
420
- assertEq(updated?.validation, 'S01 all tests pass', 'validation updated in DB');
421
- assertEq(updated?.description, 'Test requirement', 'description preserved after update');
422
-
423
- // Verify markdown file was written
424
- const mdPath = path.join(tmpDir, '.gsd', 'REQUIREMENTS.md');
425
- assertTrue(fs.existsSync(mdPath), 'REQUIREMENTS.md file created');
426
-
427
- const mdContent = fs.readFileSync(mdPath, 'utf-8');
428
- assertTrue(mdContent.includes('R001'), 'REQUIREMENTS.md contains requirement ID');
429
- assertTrue(mdContent.includes('validated'), 'REQUIREMENTS.md shows updated status');
430
-
431
- // Verify round-trip
432
- const parsed = parseRequirementsSections(mdContent);
433
- assertEq(parsed.length, 1, 'parsed 1 requirement from written file');
434
- assertEq(parsed[0].status, 'validated', 'parsed status matches update');
435
- } finally {
436
- closeDatabase();
437
- cleanupDir(tmpDir);
438
- }
439
- }
424
+ try {
425
+ let threw = false;
426
+ try {
427
+ await updateRequirementInDb('R999', { status: 'validated' }, tmpDir);
428
+ } catch (err) {
429
+ threw = true;
430
+ assert.ok(
431
+ (err as Error).message.includes('R999'),
432
+ 'error message mentions the missing ID',
433
+ );
434
+ }
435
+ assert.ok(threw, 'throws when requirement not found');
436
+ } finally {
437
+ closeDatabase();
438
+ cleanupDir(tmpDir);
439
+ }
440
+ });
440
441
 
441
- console.log('\n── updateRequirementInDb — not found ──');
442
+ // ═══════════════════════════════════════════════════════════════════════════
443
+ // saveArtifactToDb Tests
444
+ // ═══════════════════════════════════════════════════════════════════════════
442
445
 
443
- {
444
- const tmpDir = makeTmpDir();
445
- const dbPath = path.join(tmpDir, '.gsd', 'gsd.db');
446
- openDatabase(dbPath);
446
+ test('saveArtifactToDb', async () => {
447
+ const tmpDir = makeTmpDir();
448
+ const dbPath = path.join(tmpDir, '.gsd', 'gsd.db');
449
+ openDatabase(dbPath);
447
450
 
448
- try {
449
- let threw = false;
450
451
  try {
451
- await updateRequirementInDb('R999', { status: 'validated' }, tmpDir);
452
- } catch (err) {
453
- threw = true;
454
- assertTrue(
455
- (err as Error).message.includes('R999'),
456
- 'error message mentions the missing ID',
452
+ const content = '# Task Summary\n\nTest content\n';
453
+ await saveArtifactToDb({
454
+ path: 'milestones/M001/slices/S06/tasks/T01-SUMMARY.md',
455
+ artifact_type: 'SUMMARY',
456
+ content,
457
+ milestone_id: 'M001',
458
+ slice_id: 'S06',
459
+ task_id: 'T01',
460
+ }, tmpDir);
461
+
462
+ // Verify DB state
463
+ const adapter = _getAdapter();
464
+ assert.ok(!!adapter, 'adapter available');
465
+ const row = adapter!
466
+ .prepare('SELECT * FROM artifacts WHERE path = ?')
467
+ .get('milestones/M001/slices/S06/tasks/T01-SUMMARY.md');
468
+ assert.ok(!!row, 'artifact exists in DB');
469
+ assert.deepStrictEqual(row!['artifact_type'], 'SUMMARY', 'artifact type correct in DB');
470
+ assert.deepStrictEqual(row!['milestone_id'], 'M001', 'milestone_id correct in DB');
471
+ assert.deepStrictEqual(row!['slice_id'], 'S06', 'slice_id correct in DB');
472
+ assert.deepStrictEqual(row!['task_id'], 'T01', 'task_id correct in DB');
473
+
474
+ // Verify file on disk
475
+ const filePath = path.join(
476
+ tmpDir, '.gsd', 'milestones', 'M001', 'slices', 'S06', 'tasks', 'T01-SUMMARY.md',
457
477
  );
478
+ assert.ok(fs.existsSync(filePath), 'artifact file written to disk');
479
+ assert.deepStrictEqual(fs.readFileSync(filePath, 'utf-8'), content, 'file content matches');
480
+ } finally {
481
+ closeDatabase();
482
+ cleanupDir(tmpDir);
458
483
  }
459
- assertTrue(threw, 'throws when requirement not found');
460
- } finally {
461
- closeDatabase();
462
- cleanupDir(tmpDir);
463
- }
464
- }
465
-
466
- // ═══════════════════════════════════════════════════════════════════════════
467
- // saveArtifactToDb Tests
468
- // ═══════════════════════════════════════════════════════════════════════════
484
+ });
469
485
 
470
- console.log('\n── saveArtifactToDb ──');
486
+ // ═══════════════════════════════════════════════════════════════════════════
487
+ // Full Round-Trip: DB → Markdown → Parse → Compare
488
+ // ═══════════════════════════════════════════════════════════════════════════
489
+
490
+ test('Full DB round-trip: decisions', () => {
491
+ openDatabase(':memory:');
492
+
493
+ // Insert via DB
494
+ for (const d of SAMPLE_DECISIONS) {
495
+ upsertDecision({
496
+ id: d.id,
497
+ when_context: d.when_context,
498
+ scope: d.scope,
499
+ decision: d.decision,
500
+ choice: d.choice,
501
+ rationale: d.rationale,
502
+ revisable: d.revisable,
503
+ made_by: d.made_by,
504
+ superseded_by: d.superseded_by,
505
+ });
506
+ }
471
507
 
472
- {
473
- const tmpDir = makeTmpDir();
474
- const dbPath = path.join(tmpDir, '.gsd', 'gsd.db');
475
- openDatabase(dbPath);
508
+ // Generate markdown from DB state
509
+ const adapter = _getAdapter()!;
510
+ const rows = adapter.prepare('SELECT * FROM decisions ORDER BY seq').all();
511
+ const dbDecisions: Decision[] = rows.map(row => ({
512
+ seq: row['seq'] as number,
513
+ id: row['id'] as string,
514
+ when_context: row['when_context'] as string,
515
+ scope: row['scope'] as string,
516
+ decision: row['decision'] as string,
517
+ choice: row['choice'] as string,
518
+ rationale: row['rationale'] as string,
519
+ revisable: row['revisable'] as string,
520
+ made_by: (row['made_by'] as string as import('../types.js').DecisionMadeBy) ?? 'agent',
521
+ superseded_by: (row['superseded_by'] as string) ?? null,
522
+ }));
523
+
524
+ const md = generateDecisionsMd(dbDecisions);
525
+ const parsed = parseDecisionsTable(md);
526
+
527
+ assert.deepStrictEqual(parsed.length, SAMPLE_DECISIONS.length, 'DB round-trip decision count');
528
+ for (const orig of SAMPLE_DECISIONS) {
529
+ const rt = parsed.find(p => p.id === orig.id);
530
+ assert.ok(!!rt, `DB round-trip: ${orig.id} found`);
531
+ if (rt) {
532
+ assert.deepStrictEqual(rt.scope, orig.scope, `DB round-trip: ${orig.id} scope`);
533
+ assert.deepStrictEqual(rt.choice, orig.choice, `DB round-trip: ${orig.id} choice`);
534
+ }
535
+ }
476
536
 
477
- try {
478
- const content = '# Task Summary\n\nTest content\n';
479
- await saveArtifactToDb({
480
- path: 'milestones/M001/slices/S06/tasks/T01-SUMMARY.md',
481
- artifact_type: 'SUMMARY',
482
- content,
483
- milestone_id: 'M001',
484
- slice_id: 'S06',
485
- task_id: 'T01',
486
- }, tmpDir);
487
-
488
- // Verify DB state
489
- const adapter = _getAdapter();
490
- assertTrue(!!adapter, 'adapter available');
491
- const row = adapter!
492
- .prepare('SELECT * FROM artifacts WHERE path = ?')
493
- .get('milestones/M001/slices/S06/tasks/T01-SUMMARY.md');
494
- assertTrue(!!row, 'artifact exists in DB');
495
- assertEq(row!['artifact_type'], 'SUMMARY', 'artifact type correct in DB');
496
- assertEq(row!['milestone_id'], 'M001', 'milestone_id correct in DB');
497
- assertEq(row!['slice_id'], 'S06', 'slice_id correct in DB');
498
- assertEq(row!['task_id'], 'T01', 'task_id correct in DB');
499
-
500
- // Verify file on disk
501
- const filePath = path.join(
502
- tmpDir, '.gsd', 'milestones', 'M001', 'slices', 'S06', 'tasks', 'T01-SUMMARY.md',
503
- );
504
- assertTrue(fs.existsSync(filePath), 'artifact file written to disk');
505
- assertEq(fs.readFileSync(filePath, 'utf-8'), content, 'file content matches');
506
- } finally {
507
537
  closeDatabase();
508
- cleanupDir(tmpDir);
509
- }
510
- }
511
-
512
- // ═══════════════════════════════════════════════════════════════════════════
513
- // Full Round-Trip: DB → Markdown → Parse → Compare
514
- // ═══════════════════════════════════════════════════════════════════════════
515
-
516
- console.log('\n── Full DB round-trip: decisions ──');
538
+ });
517
539
 
518
- {
519
- openDatabase(':memory:');
540
+ test('Full DB round-trip: requirements', () => {
541
+ openDatabase(':memory:');
520
542
 
521
- // Insert via DB
522
- for (const d of SAMPLE_DECISIONS) {
523
- upsertDecision({
524
- id: d.id,
525
- when_context: d.when_context,
526
- scope: d.scope,
527
- decision: d.decision,
528
- choice: d.choice,
529
- rationale: d.rationale,
530
- revisable: d.revisable,
531
- made_by: d.made_by,
532
- superseded_by: d.superseded_by,
533
- });
534
- }
535
-
536
- // Generate markdown from DB state
537
- const adapter = _getAdapter()!;
538
- const rows = adapter.prepare('SELECT * FROM decisions ORDER BY seq').all();
539
- const dbDecisions: Decision[] = rows.map(row => ({
540
- seq: row['seq'] as number,
541
- id: row['id'] as string,
542
- when_context: row['when_context'] as string,
543
- scope: row['scope'] as string,
544
- decision: row['decision'] as string,
545
- choice: row['choice'] as string,
546
- rationale: row['rationale'] as string,
547
- revisable: row['revisable'] as string,
548
- made_by: (row['made_by'] as string as import('../types.js').DecisionMadeBy) ?? 'agent',
549
- superseded_by: (row['superseded_by'] as string) ?? null,
550
- }));
551
-
552
- const md = generateDecisionsMd(dbDecisions);
553
- const parsed = parseDecisionsTable(md);
554
-
555
- assertEq(parsed.length, SAMPLE_DECISIONS.length, 'DB round-trip decision count');
556
- for (const orig of SAMPLE_DECISIONS) {
557
- const rt = parsed.find(p => p.id === orig.id);
558
- assertTrue(!!rt, `DB round-trip: ${orig.id} found`);
559
- if (rt) {
560
- assertEq(rt.scope, orig.scope, `DB round-trip: ${orig.id} scope`);
561
- assertEq(rt.choice, orig.choice, `DB round-trip: ${orig.id} choice`);
543
+ for (const r of SAMPLE_REQUIREMENTS) {
544
+ upsertRequirement(r);
562
545
  }
563
- }
564
546
 
565
- closeDatabase();
566
- }
567
-
568
- console.log('\n── Full DB round-trip: requirements ──');
569
-
570
- {
571
- openDatabase(':memory:');
572
-
573
- for (const r of SAMPLE_REQUIREMENTS) {
574
- upsertRequirement(r);
575
- }
576
-
577
- const adapter = _getAdapter()!;
578
- const rows = adapter.prepare('SELECT * FROM requirements ORDER BY id').all();
579
- const dbReqs: Requirement[] = rows.map(row => ({
580
- id: row['id'] as string,
581
- class: row['class'] as string,
582
- status: row['status'] as string,
583
- description: row['description'] as string,
584
- why: row['why'] as string,
585
- source: row['source'] as string,
586
- primary_owner: row['primary_owner'] as string,
587
- supporting_slices: row['supporting_slices'] as string,
588
- validation: row['validation'] as string,
589
- notes: row['notes'] as string,
590
- full_content: row['full_content'] as string,
591
- superseded_by: (row['superseded_by'] as string) ?? null,
592
- }));
593
-
594
- const md = generateRequirementsMd(dbReqs);
595
- const parsed = parseRequirementsSections(md);
596
-
597
- assertEq(parsed.length, SAMPLE_REQUIREMENTS.length, 'DB round-trip requirement count');
598
- for (const orig of SAMPLE_REQUIREMENTS) {
599
- const rt = parsed.find(p => p.id === orig.id);
600
- assertTrue(!!rt, `DB round-trip: ${orig.id} found`);
601
- if (rt) {
602
- assertEq(rt.class, orig.class, `DB round-trip: ${orig.id} class`);
603
- assertEq(rt.description, orig.description, `DB round-trip: ${orig.id} description`);
547
+ const adapter = _getAdapter()!;
548
+ const rows = adapter.prepare('SELECT * FROM requirements ORDER BY id').all();
549
+ const dbReqs: Requirement[] = rows.map(row => ({
550
+ id: row['id'] as string,
551
+ class: row['class'] as string,
552
+ status: row['status'] as string,
553
+ description: row['description'] as string,
554
+ why: row['why'] as string,
555
+ source: row['source'] as string,
556
+ primary_owner: row['primary_owner'] as string,
557
+ supporting_slices: row['supporting_slices'] as string,
558
+ validation: row['validation'] as string,
559
+ notes: row['notes'] as string,
560
+ full_content: row['full_content'] as string,
561
+ superseded_by: (row['superseded_by'] as string) ?? null,
562
+ }));
563
+
564
+ const md = generateRequirementsMd(dbReqs);
565
+ const parsed = parseRequirementsSections(md);
566
+
567
+ assert.deepStrictEqual(parsed.length, SAMPLE_REQUIREMENTS.length, 'DB round-trip requirement count');
568
+ for (const orig of SAMPLE_REQUIREMENTS) {
569
+ const rt = parsed.find(p => p.id === orig.id);
570
+ assert.ok(!!rt, `DB round-trip: ${orig.id} found`);
571
+ if (rt) {
572
+ assert.deepStrictEqual(rt.class, orig.class, `DB round-trip: ${orig.id} class`);
573
+ assert.deepStrictEqual(rt.description, orig.description, `DB round-trip: ${orig.id} description`);
574
+ }
604
575
  }
605
- }
606
576
 
607
- closeDatabase();
608
- }
577
+ closeDatabase();
578
+ });
609
579
 
610
- // ═══════════════════════════════════════════════════════════════════════════
580
+ // ═══════════════════════════════════════════════════════════════════════════
611
581
 
612
- report();
582
+ });