gsd-pi 2.78.1-dev.b0759e59b → 2.78.1-dev.e9d88a536

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 (203) hide show
  1. package/README.md +8 -5
  2. package/dist/headless-recover.d.ts +23 -0
  3. package/dist/headless-recover.js +93 -0
  4. package/dist/headless.js +9 -0
  5. package/dist/help-text.js +1 -0
  6. package/dist/resources/.managed-resources-content-hash +1 -1
  7. package/dist/resources/extensions/browser-tools/tools/intent.js +8 -1
  8. package/dist/resources/extensions/gsd/auto-dispatch.js +4 -56
  9. package/dist/resources/extensions/gsd/auto-post-unit.js +7 -27
  10. package/dist/resources/extensions/gsd/auto-start.js +1 -8
  11. package/dist/resources/extensions/gsd/auto-worktree.js +59 -176
  12. package/dist/resources/extensions/gsd/auto.js +24 -6
  13. package/dist/resources/extensions/gsd/bootstrap/dynamic-tools.js +9 -77
  14. package/dist/resources/extensions/gsd/commands-codebase.js +2 -2
  15. package/dist/resources/extensions/gsd/commands-handlers.js +5 -5
  16. package/dist/resources/extensions/gsd/commands-logs.js +2 -2
  17. package/dist/resources/extensions/gsd/commands-scan.js +2 -2
  18. package/dist/resources/extensions/gsd/commands-ship.js +2 -2
  19. package/dist/resources/extensions/gsd/commands-workflow-templates.js +5 -5
  20. package/dist/resources/extensions/gsd/db-writer.js +16 -85
  21. package/dist/resources/extensions/gsd/dispatch-guard.js +6 -10
  22. package/dist/resources/extensions/gsd/doctor-engine-checks.js +2 -2
  23. package/dist/resources/extensions/gsd/gsd-db.js +74 -8
  24. package/dist/resources/extensions/gsd/guided-flow.js +31 -8
  25. package/dist/resources/extensions/gsd/markdown-renderer.js +14 -51
  26. package/dist/resources/extensions/gsd/parallel-merge.js +14 -13
  27. package/dist/resources/extensions/gsd/parallel-monitor-overlay.js +5 -2
  28. package/dist/resources/extensions/gsd/paths.js +35 -1
  29. package/dist/resources/extensions/gsd/prompts/plan-milestone.md +6 -0
  30. package/dist/resources/extensions/gsd/queue-order.js +6 -1
  31. package/dist/resources/extensions/gsd/rethink.js +2 -2
  32. package/dist/resources/extensions/gsd/state.js +91 -372
  33. package/dist/resources/extensions/gsd/tools/complete-milestone.js +6 -5
  34. package/dist/resources/extensions/gsd/tools/complete-slice.js +7 -12
  35. package/dist/resources/extensions/gsd/tools/complete-task.js +19 -31
  36. package/dist/resources/extensions/gsd/tools/validate-milestone.js +7 -5
  37. package/dist/resources/extensions/gsd/workflow-manifest.js +2 -1
  38. package/dist/resources/extensions/gsd/workflow-mcp-auto-prep.js +3 -21
  39. package/dist/resources/extensions/gsd/workflow-reconcile.js +3 -3
  40. package/dist/resources/extensions/gsd/worktree-command.js +4 -3
  41. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  42. package/dist/web/standalone/.next/BUILD_ID +1 -1
  43. package/dist/web/standalone/.next/app-path-routes-manifest.json +12 -12
  44. package/dist/web/standalone/.next/build-manifest.json +2 -2
  45. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  46. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  47. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  48. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  49. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  50. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  51. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  52. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  53. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  54. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  55. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  56. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  57. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  58. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  59. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  60. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  61. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  62. package/dist/web/standalone/.next/server/app/api/boot/route.js.nft.json +1 -1
  63. package/dist/web/standalone/.next/server/app/api/bridge-terminal/input/route.js.nft.json +1 -1
  64. package/dist/web/standalone/.next/server/app/api/bridge-terminal/resize/route.js.nft.json +1 -1
  65. package/dist/web/standalone/.next/server/app/api/bridge-terminal/stream/route.js.nft.json +1 -1
  66. package/dist/web/standalone/.next/server/app/api/captures/route.js.nft.json +1 -1
  67. package/dist/web/standalone/.next/server/app/api/cleanup/route.js.nft.json +1 -1
  68. package/dist/web/standalone/.next/server/app/api/doctor/route.js.nft.json +1 -1
  69. package/dist/web/standalone/.next/server/app/api/export-data/route.js.nft.json +1 -1
  70. package/dist/web/standalone/.next/server/app/api/files/route.js.nft.json +1 -1
  71. package/dist/web/standalone/.next/server/app/api/forensics/route.js.nft.json +1 -1
  72. package/dist/web/standalone/.next/server/app/api/git/route.js.nft.json +1 -1
  73. package/dist/web/standalone/.next/server/app/api/history/route.js.nft.json +1 -1
  74. package/dist/web/standalone/.next/server/app/api/hooks/route.js.nft.json +1 -1
  75. package/dist/web/standalone/.next/server/app/api/inspect/route.js.nft.json +1 -1
  76. package/dist/web/standalone/.next/server/app/api/knowledge/route.js.nft.json +1 -1
  77. package/dist/web/standalone/.next/server/app/api/live-state/route.js.nft.json +1 -1
  78. package/dist/web/standalone/.next/server/app/api/notifications/route.js.nft.json +1 -1
  79. package/dist/web/standalone/.next/server/app/api/onboarding/route.js.nft.json +1 -1
  80. package/dist/web/standalone/.next/server/app/api/projects/route.js.nft.json +1 -1
  81. package/dist/web/standalone/.next/server/app/api/recovery/route.js.nft.json +1 -1
  82. package/dist/web/standalone/.next/server/app/api/session/browser/route.js.nft.json +1 -1
  83. package/dist/web/standalone/.next/server/app/api/session/command/route.js.nft.json +1 -1
  84. package/dist/web/standalone/.next/server/app/api/session/events/route.js.nft.json +1 -1
  85. package/dist/web/standalone/.next/server/app/api/session/manage/route.js.nft.json +1 -1
  86. package/dist/web/standalone/.next/server/app/api/settings-data/route.js.nft.json +1 -1
  87. package/dist/web/standalone/.next/server/app/api/skill-health/route.js.nft.json +1 -1
  88. package/dist/web/standalone/.next/server/app/api/steer/route.js.nft.json +1 -1
  89. package/dist/web/standalone/.next/server/app/api/switch-root/route.js.nft.json +1 -1
  90. package/dist/web/standalone/.next/server/app/api/terminal/sessions/route.js.nft.json +1 -1
  91. package/dist/web/standalone/.next/server/app/api/terminal/stream/route.js.nft.json +1 -1
  92. package/dist/web/standalone/.next/server/app/api/undo/route.js.nft.json +1 -1
  93. package/dist/web/standalone/.next/server/app/api/visualizer/route.js.nft.json +1 -1
  94. package/dist/web/standalone/.next/server/app/index.html +1 -1
  95. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  96. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  97. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  98. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  99. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  100. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  101. package/dist/web/standalone/.next/server/app-paths-manifest.json +12 -12
  102. package/dist/web/standalone/.next/server/chunks/6336.js +1 -0
  103. package/dist/web/standalone/.next/server/chunks/6897.js +1 -1
  104. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  105. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  106. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  107. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  108. package/package.json +1 -1
  109. package/packages/mcp-server/dist/workflow-tools.d.ts +6 -0
  110. package/packages/mcp-server/dist/workflow-tools.d.ts.map +1 -1
  111. package/packages/mcp-server/dist/workflow-tools.js +56 -2
  112. package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
  113. package/packages/mcp-server/src/parse-workflow-args.test.ts +80 -0
  114. package/packages/mcp-server/src/workflow-tools.ts +61 -2
  115. package/packages/mcp-server/tsconfig.tsbuildinfo +1 -1
  116. package/src/resources/extensions/browser-tools/tools/intent.ts +13 -2
  117. package/src/resources/extensions/gsd/auto-dispatch.ts +4 -60
  118. package/src/resources/extensions/gsd/auto-post-unit.ts +7 -26
  119. package/src/resources/extensions/gsd/auto-start.ts +1 -8
  120. package/src/resources/extensions/gsd/auto-worktree.ts +61 -204
  121. package/src/resources/extensions/gsd/auto.ts +23 -6
  122. package/src/resources/extensions/gsd/bootstrap/dynamic-tools.ts +9 -84
  123. package/src/resources/extensions/gsd/commands-codebase.ts +2 -2
  124. package/src/resources/extensions/gsd/commands-handlers.ts +5 -5
  125. package/src/resources/extensions/gsd/commands-logs.ts +2 -2
  126. package/src/resources/extensions/gsd/commands-scan.ts +2 -2
  127. package/src/resources/extensions/gsd/commands-ship.ts +2 -2
  128. package/src/resources/extensions/gsd/commands-workflow-templates.ts +5 -5
  129. package/src/resources/extensions/gsd/db-writer.ts +16 -83
  130. package/src/resources/extensions/gsd/dispatch-guard.ts +6 -11
  131. package/src/resources/extensions/gsd/doctor-engine-checks.ts +2 -2
  132. package/src/resources/extensions/gsd/gsd-db.ts +85 -8
  133. package/src/resources/extensions/gsd/guided-flow.ts +35 -8
  134. package/src/resources/extensions/gsd/markdown-renderer.ts +13 -64
  135. package/src/resources/extensions/gsd/parallel-merge.ts +14 -13
  136. package/src/resources/extensions/gsd/parallel-monitor-overlay.ts +5 -2
  137. package/src/resources/extensions/gsd/paths.ts +55 -1
  138. package/src/resources/extensions/gsd/prompts/plan-milestone.md +6 -0
  139. package/src/resources/extensions/gsd/queue-order.ts +6 -1
  140. package/src/resources/extensions/gsd/rethink.ts +2 -2
  141. package/src/resources/extensions/gsd/state.ts +91 -389
  142. package/src/resources/extensions/gsd/tests/artifact-corruption-2630.test.ts +1 -0
  143. package/src/resources/extensions/gsd/tests/auto-paused-session-validation.test.ts +6 -0
  144. package/src/resources/extensions/gsd/tests/auto-remediate-slice-status.test.ts +21 -34
  145. package/src/resources/extensions/gsd/tests/complete-task-rollback-evidence.test.ts +6 -7
  146. package/src/resources/extensions/gsd/tests/complete-task.test.ts +8 -6
  147. package/src/resources/extensions/gsd/tests/completed-at-reconcile.test.ts +12 -27
  148. package/src/resources/extensions/gsd/tests/completed-units-metrics-sync.test.ts +18 -5
  149. package/src/resources/extensions/gsd/tests/db-path-worktree-symlink.test.ts +4 -4
  150. package/src/resources/extensions/gsd/tests/db-writer.test.ts +14 -16
  151. package/src/resources/extensions/gsd/tests/derive-state-crossval.test.ts +6 -5
  152. package/src/resources/extensions/gsd/tests/derive-state-db-disk-reconcile.test.ts +10 -38
  153. package/src/resources/extensions/gsd/tests/derive-state-db.test.ts +136 -56
  154. package/src/resources/extensions/gsd/tests/derive-state-draft.test.ts +3 -0
  155. package/src/resources/extensions/gsd/tests/derive-state-helpers.test.ts +119 -61
  156. package/src/resources/extensions/gsd/tests/derive-state.test.ts +4 -0
  157. package/src/resources/extensions/gsd/tests/dispatch-complete-milestone-guard.test.ts +6 -20
  158. package/src/resources/extensions/gsd/tests/dispatch-guard.test.ts +4 -5
  159. package/src/resources/extensions/gsd/tests/dispatcher-stuck-planning.test.ts +14 -15
  160. package/src/resources/extensions/gsd/tests/ensure-db-open.test.ts +11 -16
  161. package/src/resources/extensions/gsd/tests/escalation.test.ts +2 -1
  162. package/src/resources/extensions/gsd/tests/gsd-db.test.ts +2 -1
  163. package/src/resources/extensions/gsd/tests/gsdroot-worktree-detection.test.ts +15 -36
  164. package/src/resources/extensions/gsd/tests/handler-worktree-write-isolation.test.ts +57 -0
  165. package/src/resources/extensions/gsd/tests/integration/parallel-merge.test.ts +15 -15
  166. package/src/resources/extensions/gsd/tests/integration/state-machine-edge-cases.test.ts +15 -5
  167. package/src/resources/extensions/gsd/tests/markdown-renderer.test.ts +14 -8
  168. package/src/resources/extensions/gsd/tests/md-importer.test.ts +2 -1
  169. package/src/resources/extensions/gsd/tests/memory-store.test.ts +3 -2
  170. package/src/resources/extensions/gsd/tests/park-milestone.test.ts +2 -0
  171. package/src/resources/extensions/gsd/tests/progressive-planning.test.ts +25 -16
  172. package/src/resources/extensions/gsd/tests/projection-regression.test.ts +1 -0
  173. package/src/resources/extensions/gsd/tests/ready-phrase-no-files-4573.test.ts +184 -0
  174. package/src/resources/extensions/gsd/tests/register-hooks-compaction-checkpoint.test.ts +6 -1
  175. package/src/resources/extensions/gsd/tests/replan-slice.test.ts +3 -0
  176. package/src/resources/extensions/gsd/tests/resolve-ts.mjs +4 -0
  177. package/src/resources/extensions/gsd/tests/rogue-file-detection.test.ts +3 -4
  178. package/src/resources/extensions/gsd/tests/slice-disk-reconcile.test.ts +10 -56
  179. package/src/resources/extensions/gsd/tests/stale-slice-rows.test.ts +15 -16
  180. package/src/resources/extensions/gsd/tests/state-corruption-2945.test.ts +1 -0
  181. package/src/resources/extensions/gsd/tests/state-machine-full-walkthrough.test.ts +23 -27
  182. package/src/resources/extensions/gsd/tests/steer-worktree-path.test.ts +13 -14
  183. package/src/resources/extensions/gsd/tests/stop-auto-remote.test.ts +4 -3
  184. package/src/resources/extensions/gsd/tests/sync-worktree-skip-current.test.ts +10 -33
  185. package/src/resources/extensions/gsd/tests/validate-milestone-write-order.test.ts +7 -8
  186. package/src/resources/extensions/gsd/tests/validate-milestone.test.ts +9 -15
  187. package/src/resources/extensions/gsd/tests/workflow-logger-wiring.test.ts +12 -7
  188. package/src/resources/extensions/gsd/tests/workflow-mcp-auto-prep.test.ts +4 -4
  189. package/src/resources/extensions/gsd/tests/workflow-mcp.test.ts +24 -1
  190. package/src/resources/extensions/gsd/tests/worktree-db-same-file.test.ts +13 -0
  191. package/src/resources/extensions/gsd/tests/worktree-sync-milestones.test.ts +65 -71
  192. package/src/resources/extensions/gsd/tests/worktree-sync-tasks.test.ts +26 -151
  193. package/src/resources/extensions/gsd/tools/complete-milestone.ts +7 -5
  194. package/src/resources/extensions/gsd/tools/complete-slice.ts +7 -14
  195. package/src/resources/extensions/gsd/tools/complete-task.ts +19 -34
  196. package/src/resources/extensions/gsd/tools/validate-milestone.ts +7 -5
  197. package/src/resources/extensions/gsd/workflow-manifest.ts +4 -1
  198. package/src/resources/extensions/gsd/workflow-mcp-auto-prep.ts +2 -18
  199. package/src/resources/extensions/gsd/workflow-reconcile.ts +3 -3
  200. package/src/resources/extensions/gsd/worktree-command.ts +4 -3
  201. package/dist/web/standalone/.next/server/chunks/8527.js +0 -1
  202. /package/dist/web/standalone/.next/static/{rk1EN3FQTE6Z1yalkW_GE → oZGTPvJBQX_IDKKnuV8Bt}/_buildManifest.js +0 -0
  203. /package/dist/web/standalone/.next/static/{rk1EN3FQTE6Z1yalkW_GE → oZGTPvJBQX_IDKKnuV8Bt}/_ssgManifest.js +0 -0
@@ -1,12 +1,9 @@
1
- // GSD Extension — Tests for extracted deriveStateFromDb helper functions
1
+ // GSD Extension — Tests for DB-authoritative deriveStateFromDb behavior
2
2
  // Copyright (c) 2026 Jeremy McSpadden <jeremy@fluxlabs.net>
3
3
  //
4
- // Tests the composable helpers extracted from deriveStateFromDb:
5
- // reconcileDiskToDb, buildCompletenessSet, buildRegistryAndFindActive,
6
- // handleNoActiveMilestone, resolveSliceDependencies, reconcileSliceTasks,
7
- // detectBlockers, checkReplanTrigger, checkInterruptedWork
8
- //
9
- // Helpers are private — exercised through deriveStateFromDb integration.
4
+ // Private helper behavior is exercised through deriveStateFromDb integration.
5
+ // Markdown files in these tests are projections unless the DB row explicitly
6
+ // makes them authoritative.
10
7
 
11
8
  import { describe, test, beforeEach, afterEach } from 'node:test';
12
9
  import assert from 'node:assert/strict';
@@ -14,13 +11,16 @@ import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'node:fs';
14
11
  import { join } from 'node:path';
15
12
  import { tmpdir } from 'node:os';
16
13
 
17
- import { invalidateStateCache, deriveStateFromDb } from '../state.ts';
14
+ import { invalidateStateCache, deriveStateFromDb, getActiveMilestoneId } from '../state.ts';
18
15
  import {
19
16
  openDatabase,
20
17
  closeDatabase,
18
+ insertAssessment,
21
19
  insertMilestone,
20
+ insertRequirement,
22
21
  insertSlice,
23
22
  insertTask,
23
+ setMilestoneQueueOrder,
24
24
  updateTaskStatus,
25
25
  } from '../gsd-db.ts';
26
26
 
@@ -112,6 +112,20 @@ describe('derive-state-helpers', () => {
112
112
 
113
113
  openDatabase(':memory:');
114
114
  insertMilestone({ id: 'M001', title: 'First', status: 'complete' });
115
+ insertRequirement({
116
+ id: 'R001',
117
+ class: 'functional',
118
+ status: 'active',
119
+ description: 'Unmapped',
120
+ why: 'test',
121
+ source: 'test',
122
+ primary_owner: '',
123
+ supporting_slices: '',
124
+ validation: '',
125
+ notes: '',
126
+ full_content: '',
127
+ superseded_by: null,
128
+ });
115
129
 
116
130
  invalidateStateCache();
117
131
  const state = await deriveStateFromDb(base);
@@ -126,10 +140,11 @@ describe('derive-state-helpers', () => {
126
140
  });
127
141
 
128
142
  // ─── resolveSliceDependencies: GSD_SLICE_LOCK with missing slice ────
129
- test('resolveSliceDependencies: GSD_SLICE_LOCK pointing to non-existent slice returns blocked', async () => {
130
- const base = createFixtureBase();
131
- const origLock = process.env.GSD_SLICE_LOCK;
132
- try {
143
+ test('resolveSliceDependencies: GSD_SLICE_LOCK pointing to non-existent slice returns blocked', async () => {
144
+ const base = createFixtureBase();
145
+ const origLock = process.env.GSD_SLICE_LOCK;
146
+ const origWorker = process.env.GSD_PARALLEL_WORKER;
147
+ try {
133
148
  writeFile(base, 'milestones/M001/M001-ROADMAP.md', ROADMAP_CONTENT);
134
149
  writeFile(base, 'milestones/M001/slices/S01/S01-PLAN.md', PLAN_CONTENT);
135
150
  writeFile(base, 'milestones/M001/slices/S01/tasks/.gitkeep', '');
@@ -140,26 +155,30 @@ describe('derive-state-helpers', () => {
140
155
  insertSlice({ id: 'S01', milestoneId: 'M001', title: 'First', status: 'active', risk: 'low', depends: [] });
141
156
  insertTask({ id: 'T01', sliceId: 'S01', milestoneId: 'M001', title: 'First Task', status: 'pending' });
142
157
 
143
- process.env.GSD_SLICE_LOCK = 'S99';
158
+ process.env.GSD_SLICE_LOCK = 'S99';
159
+ process.env.GSD_PARALLEL_WORKER = '1';
144
160
 
145
161
  invalidateStateCache();
146
162
  const state = await deriveStateFromDb(base);
147
163
 
148
164
  assert.equal(state.phase, 'blocked', 'slice-lock-miss: phase is blocked');
149
165
  assert.ok(state.blockers.some(b => b.includes('GSD_SLICE_LOCK=S99')), 'slice-lock-miss: blocker mentions lock');
150
- } finally {
151
- if (origLock !== undefined) process.env.GSD_SLICE_LOCK = origLock;
152
- else delete process.env.GSD_SLICE_LOCK;
153
- closeDatabase();
166
+ } finally {
167
+ if (origLock !== undefined) process.env.GSD_SLICE_LOCK = origLock;
168
+ else delete process.env.GSD_SLICE_LOCK;
169
+ if (origWorker !== undefined) process.env.GSD_PARALLEL_WORKER = origWorker;
170
+ else delete process.env.GSD_PARALLEL_WORKER;
171
+ closeDatabase();
154
172
  cleanup(base);
155
173
  }
156
174
  });
157
175
 
158
176
  // ─── resolveSliceDependencies: GSD_SLICE_LOCK with valid slice ──────
159
- test('resolveSliceDependencies: GSD_SLICE_LOCK targeting valid slice bypasses deps', async () => {
160
- const base = createFixtureBase();
161
- const origLock = process.env.GSD_SLICE_LOCK;
162
- try {
177
+ test('resolveSliceDependencies: GSD_SLICE_LOCK targeting valid slice bypasses deps', async () => {
178
+ const base = createFixtureBase();
179
+ const origLock = process.env.GSD_SLICE_LOCK;
180
+ const origWorker = process.env.GSD_PARALLEL_WORKER;
181
+ try {
163
182
  writeFile(base, 'milestones/M001/M001-ROADMAP.md', ROADMAP_CONTENT);
164
183
  // S02 depends on S01 but we lock to S02 directly
165
184
  writeFile(base, 'milestones/M001/slices/S02/S02-PLAN.md', `# S02\n\n**Goal:** Test.\n**Demo:** Pass.\n\n## Tasks\n\n- [ ] **T01: Task** \`est:5m\`\n Do thing.\n`);
@@ -172,23 +191,26 @@ describe('derive-state-helpers', () => {
172
191
  insertSlice({ id: 'S02', milestoneId: 'M001', title: 'Second', status: 'pending', risk: 'low', depends: ['S01'] });
173
192
  insertTask({ id: 'T01', sliceId: 'S02', milestoneId: 'M001', title: 'Task', status: 'pending' });
174
193
 
175
- process.env.GSD_SLICE_LOCK = 'S02';
194
+ process.env.GSD_SLICE_LOCK = 'S02';
195
+ process.env.GSD_PARALLEL_WORKER = '1';
176
196
 
177
197
  invalidateStateCache();
178
198
  const state = await deriveStateFromDb(base);
179
199
 
180
200
  assert.equal(state.activeSlice?.id, 'S02', 'slice-lock-valid: activeSlice is S02 (locked)');
181
201
  assert.equal(state.phase, 'executing', 'slice-lock-valid: phase is executing');
182
- } finally {
183
- if (origLock !== undefined) process.env.GSD_SLICE_LOCK = origLock;
184
- else delete process.env.GSD_SLICE_LOCK;
185
- closeDatabase();
202
+ } finally {
203
+ if (origLock !== undefined) process.env.GSD_SLICE_LOCK = origLock;
204
+ else delete process.env.GSD_SLICE_LOCK;
205
+ if (origWorker !== undefined) process.env.GSD_PARALLEL_WORKER = origWorker;
206
+ else delete process.env.GSD_PARALLEL_WORKER;
207
+ closeDatabase();
186
208
  cleanup(base);
187
209
  }
188
210
  });
189
211
 
190
- // ─── reconcileSliceTasks: plan file imports tasks when DB empty ──────
191
- test('reconcileSliceTasks: imports tasks from plan file when DB has zero tasks (#3600)', async () => {
212
+ // ─── DB-authoritative tasks: plan projection does not import tasks ──────
213
+ test('deriveStateFromDb: DB-empty task list does not import PLAN tasks', async () => {
192
214
  const base = createFixtureBase();
193
215
  try {
194
216
  writeFile(base, 'milestones/M001/M001-ROADMAP.md', ROADMAP_CONTENT);
@@ -200,24 +222,23 @@ describe('derive-state-helpers', () => {
200
222
  insertMilestone({ id: 'M001', title: 'Test', status: 'active' });
201
223
  insertSlice({ id: 'S01', milestoneId: 'M001', title: 'First', status: 'active', risk: 'low', depends: [] });
202
224
  insertSlice({ id: 'S02', milestoneId: 'M001', title: 'Second', status: 'pending', risk: 'low', depends: ['S01'] });
203
- // No tasks inserted — reconcileSliceTasks should import from plan file
225
+ // No tasks inserted — PLAN.md is a projection and must not be imported.
204
226
 
205
227
  invalidateStateCache();
206
228
  const state = await deriveStateFromDb(base);
207
229
 
208
- // Plan has T01 (pending) and T02 (done) — reconciliation imports both
209
- assert.equal(state.phase, 'executing', 'task-reconcile: phase is executing (tasks imported)');
210
- assert.equal(state.activeTask?.id, 'T01', 'task-reconcile: activeTask is T01');
211
- assert.equal(state.progress?.tasks?.total, 2, 'task-reconcile: total tasks = 2');
212
- assert.equal(state.progress?.tasks?.done, 1, 'task-reconcile: done tasks = 1 (T02 was [x])');
230
+ assert.equal(state.phase, 'planning', 'db-empty-tasks: phase is planning');
231
+ assert.equal(state.activeTask, null, 'db-empty-tasks: no active task');
232
+ assert.equal(state.progress?.tasks?.total, 0, 'db-empty-tasks: no tasks imported');
233
+ assert.equal(state.progress?.tasks?.done, 0, 'db-empty-tasks: no completed tasks imported');
213
234
  } finally {
214
235
  closeDatabase();
215
236
  cleanup(base);
216
237
  }
217
238
  });
218
239
 
219
- // ─── reconcileSliceTasks: stale task reconciled from disk summary ────
220
- test('reconcileSliceTasks: stale pending task reconciled to complete when disk SUMMARY exists (#2514)', async () => {
240
+ // ─── DB-authoritative tasks: SUMMARY projection does not complete task ────
241
+ test('deriveStateFromDb: disk SUMMARY does not reconcile pending task', async () => {
221
242
  const base = createFixtureBase();
222
243
  try {
223
244
  writeFile(base, 'milestones/M001/M001-ROADMAP.md', ROADMAP_CONTENT);
@@ -237,11 +258,9 @@ describe('derive-state-helpers', () => {
237
258
  invalidateStateCache();
238
259
  const state = await deriveStateFromDb(base);
239
260
 
240
- // T01 should have been reconciled to complete (SUMMARY exists on disk)
241
- // Both tasks complete phase should be summarizing
242
- assert.equal(state.phase, 'summarizing', 'stale-task: phase is summarizing (T01 reconciled)');
243
- assert.equal(state.activeTask, null, 'stale-task: no active task (all done)');
244
- assert.equal(state.progress?.tasks?.done, 2, 'stale-task: tasks.done = 2');
261
+ assert.equal(state.phase, 'executing', 'disk-summary-ignored: phase is executing');
262
+ assert.equal(state.activeTask?.id, 'T01', 'disk-summary-ignored: T01 remains active');
263
+ assert.equal(state.progress?.tasks?.done, 1, 'disk-summary-ignored: only DB-complete task is done');
245
264
  } finally {
246
265
  closeDatabase();
247
266
  cleanup(base);
@@ -256,7 +275,8 @@ describe('derive-state-helpers', () => {
256
275
  writeFile(base, 'milestones/M001/slices/S01/S01-PLAN.md', PLAN_CONTENT);
257
276
  writeFile(base, 'milestones/M001/slices/S01/tasks/.gitkeep', '');
258
277
  writeFile(base, 'milestones/M001/slices/S01/tasks/T01-PLAN.md', '# T01 Plan');
259
- // T02 completed with blocker discovered written in summary frontmatter
278
+ // T02 completed with blocker discovered. The disk summary is a projection;
279
+ // only the DB blocker flag is authoritative for deriveStateFromDb().
260
280
  writeFile(base, 'milestones/M001/slices/S01/tasks/T02-SUMMARY.md',
261
281
  '---\nblocker_discovered: true\n---\n\n# T02 Summary\n\nFound a blocker.');
262
282
 
@@ -265,7 +285,7 @@ describe('derive-state-helpers', () => {
265
285
  insertSlice({ id: 'S01', milestoneId: 'M001', title: 'First', status: 'active', risk: 'low', depends: [] });
266
286
  insertSlice({ id: 'S02', milestoneId: 'M001', title: 'Second', status: 'pending', risk: 'low', depends: ['S01'] });
267
287
  insertTask({ id: 'T01', sliceId: 'S01', milestoneId: 'M001', title: 'First Task', status: 'pending' });
268
- insertTask({ id: 'T02', sliceId: 'S01', milestoneId: 'M001', title: 'Done Task', status: 'complete' });
288
+ insertTask({ id: 'T02', sliceId: 'S01', milestoneId: 'M001', title: 'Done Task', status: 'complete', blockerDiscovered: true });
269
289
 
270
290
  invalidateStateCache();
271
291
  const state = await deriveStateFromDb(base);
@@ -278,8 +298,8 @@ describe('derive-state-helpers', () => {
278
298
  }
279
299
  });
280
300
 
281
- // ─── checkInterruptedWork: continue.md triggers resume hint ─────────
282
- test('checkInterruptedWork: continue.md present triggers resume nextAction', async () => {
301
+ // ─── CONTINUE.md projection is ignored by DB derive ─────────────────
302
+ test('deriveStateFromDb: continue.md projection does not trigger resume nextAction', async () => {
283
303
  const base = createFixtureBase();
284
304
  try {
285
305
  writeFile(base, 'milestones/M001/M001-ROADMAP.md', ROADMAP_CONTENT);
@@ -299,8 +319,8 @@ describe('derive-state-helpers', () => {
299
319
  const state = await deriveStateFromDb(base);
300
320
 
301
321
  assert.equal(state.phase, 'executing', 'continue: phase is still executing');
302
- assert.ok(state.nextAction.includes('Resume interrupted work'), 'continue: nextAction mentions resume');
303
- assert.ok(state.nextAction.includes('continue.md'), 'continue: nextAction mentions continue.md');
322
+ assert.ok(!state.nextAction.includes('Resume interrupted work'), 'continue: nextAction does not mention resume');
323
+ assert.ok(!state.nextAction.includes('continue.md'), 'continue: nextAction does not mention continue.md');
304
324
  } finally {
305
325
  closeDatabase();
306
326
  cleanup(base);
@@ -380,6 +400,13 @@ describe('derive-state-helpers', () => {
380
400
  insertMilestone({ id: 'M001', title: 'First', status: 'active' });
381
401
  insertSlice({ id: 'S01', milestoneId: 'M001', title: 'First', status: 'complete', risk: 'low', depends: [] });
382
402
  insertSlice({ id: 'S02', milestoneId: 'M001', title: 'Second', status: 'complete', risk: 'low', depends: ['S01'] });
403
+ insertAssessment({
404
+ path: 'milestones/M001/M001-VALIDATION.md',
405
+ milestoneId: 'M001',
406
+ status: 'pass',
407
+ scope: 'milestone-validation',
408
+ fullContent: 'verdict: passed',
409
+ });
383
410
 
384
411
  invalidateStateCache();
385
412
  const state = await deriveStateFromDb(base);
@@ -394,34 +421,34 @@ describe('derive-state-helpers', () => {
394
421
  }
395
422
  });
396
423
 
397
- // ─── reconcileDiskToDb: disk slices synced into DB (#2533) ──────────
398
- test('reconcileDiskToDb: slices in ROADMAP.md but missing from DB are auto-inserted (#2533)', async () => {
424
+ // ─── DB-authoritative slices: roadmap projection does not insert slices ───
425
+ test('deriveStateFromDb: ROADMAP slices missing from DB are not auto-inserted', async () => {
399
426
  const base = createFixtureBase();
400
427
  try {
401
428
  writeFile(base, 'milestones/M001/M001-ROADMAP.md', ROADMAP_CONTENT);
402
429
 
403
430
  openDatabase(':memory:');
404
431
  insertMilestone({ id: 'M001', title: 'Test', status: 'active' });
405
- // No slices inserted — reconcileDiskToDb should insert from roadmap
432
+ // No slices inserted — ROADMAP.md is a projection and must not be imported.
406
433
 
407
434
  invalidateStateCache();
408
435
  const state = await deriveStateFromDb(base);
409
436
 
410
- // Slices should have been reconciled from roadmap, S01 should be the active slice
411
- assert.equal(state.activeMilestone?.id, 'M001', 'slice-reconcile: M001 is active');
412
- assert.equal(state.activeSlice?.id, 'S01', 'slice-reconcile: S01 reconciled and active');
413
- assert.ok((state.progress?.slices?.total ?? 0) >= 2, 'slice-reconcile: at least 2 slices reconciled');
437
+ assert.equal(state.activeMilestone?.id, 'M001', 'roadmap-projection: M001 is active');
438
+ assert.equal(state.activeSlice, null, 'roadmap-projection: no active slice imported');
439
+ assert.equal(state.phase, 'pre-planning', 'roadmap-projection: no DB slices routes to pre-planning');
440
+ assert.equal(state.progress?.slices, undefined, 'roadmap-projection: no slice progress from projection');
414
441
  } finally {
415
442
  closeDatabase();
416
443
  cleanup(base);
417
444
  }
418
445
  });
419
446
 
420
- // ─── Queue order: milestones sorted by custom queue order ───────────
421
- test('deriveStateFromDb respects custom queue order from QUEUE-ORDER.json', async () => {
447
+ // ─── Queue order: DB sequence is authoritative ─────────────────────
448
+ test('deriveStateFromDb ignores QUEUE-ORDER.json and uses DB sequence', async () => {
422
449
  const base = createFixtureBase();
423
450
  try {
424
- // M003 should come first per queue order, M001 second
451
+ // QUEUE-ORDER.json is a projection and should not drive DB derivation.
425
452
  const queueOrder = JSON.stringify({ order: ['M003', 'M001', 'M002'], updatedAt: new Date().toISOString() });
426
453
  writeFileSync(join(base, '.gsd', 'QUEUE-ORDER.json'), queueOrder);
427
454
  writeFile(base, 'milestones/M001/M001-CONTEXT.md', '# M001\n\nContext.');
@@ -429,23 +456,47 @@ describe('derive-state-helpers', () => {
429
456
  writeFile(base, 'milestones/M003/M003-CONTEXT.md', '# M003\n\nContext.');
430
457
 
431
458
  openDatabase(':memory:');
432
- // Insert in natural order queue ordering should override
459
+ // Insert in natural order, then store the authoritative DB sequence.
433
460
  insertMilestone({ id: 'M001', title: 'First', status: 'active' });
434
461
  insertMilestone({ id: 'M002', title: 'Second', status: 'active' });
435
462
  insertMilestone({ id: 'M003', title: 'Third', status: 'active' });
463
+ setMilestoneQueueOrder(['M002', 'M001', 'M003']);
436
464
 
437
465
  invalidateStateCache();
438
466
  const state = await deriveStateFromDb(base);
439
467
 
440
- // M003 should be the active milestone (first in queue)
441
- assert.equal(state.activeMilestone?.id, 'M003', 'queue-order: M003 is active (first in queue)');
442
- assert.equal(state.registry[0]?.id, 'M003', 'queue-order: registry[0] is M003');
468
+ assert.equal(state.activeMilestone?.id, 'M002', 'queue-order: DB sequence chooses M002');
469
+ assert.equal(state.registry[0]?.id, 'M002', 'queue-order: registry[0] follows DB sequence');
443
470
  } finally {
444
471
  closeDatabase();
445
472
  cleanup(base);
446
473
  }
447
474
  });
448
475
 
476
+ test('getActiveMilestoneId: DB lock path ignores PARKED flag projection', async () => {
477
+ const base = createFixtureBase();
478
+ const previousLock = process.env.GSD_MILESTONE_LOCK;
479
+ const previousWorker = process.env.GSD_PARALLEL_WORKER;
480
+ try {
481
+ process.env.GSD_MILESTONE_LOCK = 'M001';
482
+ process.env.GSD_PARALLEL_WORKER = '1';
483
+ writeFile(base, 'milestones/M001/M001-PARKED.md', '# Parked on disk');
484
+
485
+ openDatabase(':memory:');
486
+ insertMilestone({ id: 'M001', title: 'Active in DB', status: 'active' });
487
+
488
+ const id = await getActiveMilestoneId(base);
489
+ assert.equal(id, 'M001', 'DB status remains authoritative despite PARKED projection');
490
+ } finally {
491
+ if (previousLock === undefined) delete process.env.GSD_MILESTONE_LOCK;
492
+ else process.env.GSD_MILESTONE_LOCK = previousLock;
493
+ if (previousWorker === undefined) delete process.env.GSD_PARALLEL_WORKER;
494
+ else process.env.GSD_PARALLEL_WORKER = previousWorker;
495
+ closeDatabase();
496
+ cleanup(base);
497
+ }
498
+ });
499
+
449
500
  // ─── handleAllSlicesDone: needs-remediation + all slices done → blocked (#4506) ──
450
501
  test('handleAllSlicesDone: needs-remediation with all slices done returns blocked', async () => {
451
502
  const base = createFixtureBase();
@@ -458,6 +509,13 @@ describe('derive-state-helpers', () => {
458
509
  openDatabase(':memory:');
459
510
  insertMilestone({ id: 'M001', title: 'Remediation Test', status: 'active' });
460
511
  insertSlice({ id: 'S01', milestoneId: 'M001', title: 'Done', status: 'complete', risk: 'low', depends: [] });
512
+ insertAssessment({
513
+ path: 'milestones/M001/M001-VALIDATION.md',
514
+ milestoneId: 'M001',
515
+ status: 'needs-remediation',
516
+ scope: 'milestone-validation',
517
+ fullContent: 'verdict: needs-remediation',
518
+ });
461
519
 
462
520
  invalidateStateCache();
463
521
  const state = await deriveStateFromDb(base);
@@ -5,6 +5,10 @@ import { join } from 'node:path';
5
5
  import { tmpdir } from 'node:os';
6
6
 
7
7
  import { deriveState, isSliceComplete, isMilestoneComplete, isGhostMilestone } from '../state.ts';
8
+
9
+ // This suite exercises the explicit legacy markdown derivation path.
10
+ process.env.GSD_ALLOW_MARKDOWN_DERIVE_FALLBACK = '1';
11
+
8
12
  // ─── Fixture Helpers ───────────────────────────────────────────────────────
9
13
 
10
14
  function createFixtureBase(): string {
@@ -47,26 +47,12 @@ describe("completing-milestone dispatch guard (#4324)", () => {
47
47
  assert.ok(dispatchIdx > -1, "complete-milestone dispatch should exist after the skip guard");
48
48
  });
49
49
 
50
- test("classifies SUMMARY outcome and conditionally reconciles DB (#4658)", () => {
50
+ test("does not reconcile DB from SUMMARY.md projection (#4658 superseded)", () => {
51
51
  const phaseCheck = source.indexOf('phase !== "completing-milestone"');
52
- // The SUMMARY-exists reconciliation guard must appear in this rule
53
- const summaryGuard = source.indexOf('resolveMilestoneFile(basePath, mid, "SUMMARY")', phaseCheck);
54
- assert.ok(summaryGuard > -1, "SUMMARY file check should exist in the completing-milestone rule");
55
-
56
- const classifyCall = source.indexOf("classifyMilestoneSummaryContent", summaryGuard);
57
- assert.ok(classifyCall > -1, "SUMMARY mismatch handling should classify summary content");
58
- const dbGateBeforeClassify = source.indexOf("existingSummary && isDbAvailable()", summaryGuard);
59
- assert.ok(
60
- dbGateBeforeClassify === -1 || dbGateBeforeClassify > classifyCall,
61
- "SUMMARY classification must not be gated on DB availability",
62
- );
63
-
64
- const reconcileCall = source.indexOf('updateMilestoneStatus(mid, "complete"', summaryGuard);
65
- assert.ok(reconcileCall > -1, "successful SUMMARY should reconcile DB to complete");
66
-
67
- const stopAction = source.indexOf('action: "stop"', summaryGuard);
68
- assert.ok(stopAction > -1, "SUMMARY mismatch should return stop action");
69
- const warningLevel = source.indexOf('level: "warning"', summaryGuard);
70
- assert.ok(warningLevel > -1, "SUMMARY mismatch should be warning-level stop (pauses auto-mode)");
52
+ assert.ok(phaseCheck > -1, "completing-milestone phase check should exist");
53
+ const ruleTail = source.slice(phaseCheck, source.indexOf('name:', phaseCheck + 1));
54
+ assert.doesNotMatch(ruleTail, /resolveMilestoneFile\(basePath,\s*mid,\s*"SUMMARY"\)/);
55
+ assert.doesNotMatch(ruleTail, /classifyMilestoneSummaryContent/);
56
+ assert.doesNotMatch(ruleTail, /updateMilestoneStatus\(mid,\s*"complete"/);
71
57
  });
72
58
  });
@@ -225,14 +225,14 @@ test("dispatch guard allows slice with all declared dependencies complete", (t)
225
225
  );
226
226
  });
227
227
 
228
- test("dispatch guard skips completed milestone with SUMMARY even if it has unchecked remediation slices (#1716)", (t) => {
228
+ test("dispatch guard does not skip prior milestone from SUMMARY projection when DB is not closed", (t) => {
229
229
  const repo = setupRepo();
230
230
  t.after(() => teardownRepo(repo));
231
231
 
232
232
  mkdirSync(join(repo, ".gsd", "milestones", "M001"), { recursive: true });
233
233
  mkdirSync(join(repo, ".gsd", "milestones", "M002"), { recursive: true });
234
234
 
235
- // M001 is complete (has SUMMARY) but has unchecked remediation slices in DB
235
+ // M001 has a successful SUMMARY projection but is not closed in the DB.
236
236
  insertMilestone({ id: "M001", title: "Previous" });
237
237
  insertSlice({ id: "S01", milestoneId: "M001", title: "Core", status: "complete", depends: [], sequence: 1 });
238
238
  insertSlice({ id: "S02", milestoneId: "M001", title: "Tests", status: "complete", depends: ["S01"], sequence: 2 });
@@ -242,16 +242,15 @@ test("dispatch guard skips completed milestone with SUMMARY even if it has unche
242
242
  insertMilestone({ id: "M002", title: "Current" });
243
243
  insertSlice({ id: "S01", milestoneId: "M002", title: "Start", status: "pending", depends: [], sequence: 1 });
244
244
 
245
- // M001 SUMMARY on disk triggers skip
245
+ // M001 SUMMARY on disk must not trigger skip while DB remains open/active.
246
246
  writeFileSync(join(repo, ".gsd", "milestones", "M001", "M001-ROADMAP.md"), "# M001\n");
247
247
  writeFileSync(join(repo, ".gsd", "milestones", "M001", "M001-SUMMARY.md"),
248
248
  "---\nstatus: complete\n---\n# M001 Summary\nDone.\n");
249
249
  writeFileSync(join(repo, ".gsd", "milestones", "M002", "M002-ROADMAP.md"), "# M002\n");
250
250
 
251
- // M001 has SUMMARY — should be skipped, not block M002/S01
252
251
  assert.equal(
253
252
  getPriorSliceCompletionBlocker(repo, "main", "plan-slice", "M002/S01"),
254
- null,
253
+ "Cannot dispatch plan-slice M002/S01: earlier slice M001/S03-R is not complete.",
255
254
  );
256
255
  });
257
256
 
@@ -1,10 +1,9 @@
1
1
  /**
2
- * dispatcher-stuck-planning.test.ts — #3656
2
+ * dispatcher-stuck-planning.test.ts
3
3
  *
4
- * Verify that state.ts contains the disk-to-DB task reconciliation logic
5
- * that prevents the dispatcher from getting stuck in an infinite planning
6
- * loop when the planner writes a PLAN.md but never calls the persistence
7
- * tool, leaving the DB with zero or partial task rows.
4
+ * Verify that state.ts no longer imports disk PLAN.md tasks into the runtime
5
+ * DB. PLAN.md is a projection; task rows must be created through DB-backed
6
+ * planning/import APIs.
8
7
  */
9
8
 
10
9
  import { describe, test } from "node:test";
@@ -16,23 +15,23 @@ import { fileURLToPath } from "node:url";
16
15
  const __dirname = dirname(fileURLToPath(import.meta.url));
17
16
  const sourceFile = join(__dirname, "..", "state.ts");
18
17
 
19
- describe("dispatcher stuck-planning reconciliation (#3656)", () => {
18
+ describe("dispatcher DB-authoritative planning boundary", () => {
20
19
  const source = readFileSync(sourceFile, "utf-8");
21
20
 
22
- test("imports insertTask from gsd-db", () => {
23
- assert.match(source, /import\s*\{[^}]*insertTask[^}]*\}\s*from/);
21
+ test("does not import insertTask into state derivation", () => {
22
+ assert.doesNotMatch(source, /import\s*\{[^}]*insertTask[^}]*\}\s*from/);
24
23
  });
25
24
 
26
- test("contains plan-file task reconciliation block", () => {
27
- assert.match(source, /if\s*\(\s*planFile\s*\)/);
28
- assert.match(source, /dbTaskIds\.has\(t\.id\)/);
25
+ test("does not contain plan-file task reconciliation block", () => {
26
+ assert.doesNotMatch(source, /dbTaskIds\.has\(t\.id\)/);
27
+ assert.match(source, /Slice \$\{activeSlice\.id\} has no DB tasks/);
29
28
  });
30
29
 
31
- test("calls insertTask for each disk plan task", () => {
32
- assert.match(source, /insertTask\(\{/);
30
+ test("does not call insertTask from state derivation", () => {
31
+ assert.doesNotMatch(source, /insertTask\(\{/);
33
32
  });
34
33
 
35
- test("references issue #3600 in reconciliation comment", () => {
36
- assert.match(source, /#3600/);
34
+ test("documents markdown projections as non-authoritative", () => {
35
+ assert.match(source, /Markdown files are projections only/);
37
36
  });
38
37
  });
@@ -1,7 +1,7 @@
1
1
  import { describe, test } from 'node:test';
2
2
  import assert from 'node:assert/strict';
3
- // ensureDbOpen — Tests that the lazy DB opener creates + migrates the database
4
- // when .gsd/ exists with Markdown content but no gsd.db file.
3
+ // ensureDbOpen — Tests that the lazy DB opener creates/opens the authoritative
4
+ // database without implicitly importing markdown projections.
5
5
  //
6
6
  // This covers the bug where interactive (non-auto) sessions got
7
7
  // "GSD database is not available" because ensureDbOpen only opened
@@ -11,7 +11,7 @@ import * as path from 'node:path';
11
11
  import * as os from 'node:os';
12
12
  import * as fs from 'node:fs';
13
13
  import { createRequire } from 'node:module';
14
- import { closeDatabase, isDbAvailable, getDecisionById, _getAdapter } from '../gsd-db.ts';
14
+ import { closeDatabase, isDbAvailable, getDecisionById, SCHEMA_VERSION, _getAdapter } from '../gsd-db.ts';
15
15
 
16
16
  const _require = createRequire(import.meta.url);
17
17
 
@@ -284,11 +284,11 @@ function createLegacyV15Db(dbPath: string): void {
284
284
  }
285
285
 
286
286
  // ═══════════════════════════════════════════════════════════════════════════
287
- // ensureDbOpen creates DB + migrates when .gsd/ has Markdown
287
+ // ensureDbOpen creates DB without implicit Markdown migration
288
288
  // ═══════════════════════════════════════════════════════════════════════════
289
289
 
290
290
  describe('ensure-db-open', () => {
291
- test('ensureDbOpen: creates DB from Markdown', async () => {
291
+ test('ensureDbOpen: creates empty DB without importing Markdown', async () => {
292
292
  const tmpDir = makeTmpDir();
293
293
  const gsdDir = path.join(tmpDir, '.gsd');
294
294
  fs.mkdirSync(gsdDir, { recursive: true });
@@ -319,17 +319,12 @@ describe('ensure-db-open', () => {
319
319
 
320
320
  const result = await ensureDbOpen();
321
321
 
322
- assert.ok(result === true, 'ensureDbOpen should return true when .gsd/ has Markdown');
322
+ assert.ok(result === true, 'ensureDbOpen should return true when .gsd/ exists');
323
323
  assert.ok(fs.existsSync(dbPath), 'DB file should be created after ensureDbOpen');
324
324
  assert.ok(isDbAvailable(), 'DB should be available after ensureDbOpen');
325
325
 
326
- // Verify that Markdown migration actually ran
327
326
  const decision = getDecisionById('D001');
328
- assert.ok(decision !== null, 'D001 should be migrated from DECISIONS.md');
329
- if (decision) {
330
- assert.deepStrictEqual(decision.scope, 'architecture', 'Migrated decision scope should match');
331
- assert.deepStrictEqual(decision.choice, 'SQLite', 'Migrated decision choice should match');
332
- }
327
+ assert.equal(decision, null, 'D001 should not be imported from DECISIONS.md without explicit migration');
333
328
  } finally {
334
329
  process.cwd = origCwd;
335
330
  closeDatabase();
@@ -360,7 +355,7 @@ describe('ensure-db-open', () => {
360
355
  assert.ok(result === true, 'ensureDbOpen should honor explicit basePath');
361
356
  assert.equal(process.cwd(), originalCwd, 'ensureDbOpen should not mutate process.cwd');
362
357
  assert.ok(isDbAvailable(), 'DB should be available after explicit open');
363
- assert.ok(getDecisionById('D777') !== null, 'explicit basePath DB should be opened');
358
+ assert.equal(getDecisionById('D777'), null, 'explicit basePath should not import DECISIONS.md');
364
359
  } finally {
365
360
  closeDatabase();
366
361
  cleanupDir(tmpDir);
@@ -389,7 +384,7 @@ describe('ensure-db-open', () => {
389
384
  assert.ok(db, 'adapter should be available after ensureDbOpen');
390
385
  assert.equal(
391
386
  db.prepare('SELECT MAX(version) as version FROM schema_version').get()?.version,
392
- 22,
387
+ SCHEMA_VERSION,
393
388
  'legacy DB should migrate to current schema version',
394
389
  );
395
390
 
@@ -519,9 +514,9 @@ describe('ensure-db-open', () => {
519
514
  try {
520
515
  const { ensureDbOpen } = await import('../bootstrap/dynamic-tools.ts');
521
516
  assert.equal(await ensureDbOpen(firstDir), true);
522
- assert.ok(getDecisionById('D101') !== null, 'first DB should be active');
517
+ assert.equal(getDecisionById('D101'), null, 'first DB should not import DECISIONS.md');
523
518
  assert.equal(await ensureDbOpen(secondDir), true);
524
- assert.ok(getDecisionById('D202') !== null, 'second DB should be active after switch');
519
+ assert.equal(getDecisionById('D202'), null, 'second DB should not import DECISIONS.md');
525
520
  assert.equal(getDecisionById('D101'), null, 'first DB should no longer be active after switch');
526
521
  } finally {
527
522
  closeDatabase();
@@ -19,6 +19,7 @@ import {
19
19
  claimEscalationOverride,
20
20
  findUnappliedEscalationOverride,
21
21
  listEscalationArtifacts,
22
+ SCHEMA_VERSION,
22
23
  _getAdapter,
23
24
  } from "../gsd-db.ts";
24
25
  import {
@@ -348,7 +349,7 @@ test("ADR-011 P2: schema v20 fresh DB has all escalation columns on tasks + sour
348
349
  assert.ok(decCols.includes("source"), "decisions table must have source column");
349
350
 
350
351
  const version = adapter.prepare("SELECT MAX(version) as v FROM schema_version").get();
351
- assert.equal(version?.["v"], 22);
352
+ assert.equal(version?.["v"], SCHEMA_VERSION);
352
353
  });
353
354
 
354
355
  test("ADR-011 P2: findUnappliedEscalationOverride returns null when escalation_pending=1 (still pending)", (t) => {
@@ -11,6 +11,7 @@ import {
11
11
  wasDbOpenAttempted,
12
12
  getDbProvider,
13
13
  getDbStatus,
14
+ SCHEMA_VERSION,
14
15
  insertDecision,
15
16
  getDecisionById,
16
17
  insertRequirement,
@@ -101,7 +102,7 @@ describe('gsd-db', () => {
101
102
  // Check schema_version table
102
103
  const adapter = _getAdapter()!;
103
104
  const version = adapter.prepare('SELECT MAX(version) as version FROM schema_version').get();
104
- assert.deepStrictEqual(version?.['version'], 22, 'schema version should be 22');
105
+ assert.deepStrictEqual(version?.['version'], SCHEMA_VERSION, `schema version should be ${SCHEMA_VERSION}`);
105
106
 
106
107
  // Check tables exist by querying them
107
108
  const dRows = adapter.prepare('SELECT count(*) as cnt FROM decisions').get();