gsd-pi 2.19.0 → 2.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -1
- package/dist/cli.js +3 -3
- package/dist/onboarding.d.ts +3 -1
- package/dist/onboarding.js +77 -3
- package/dist/remote-questions-config.d.ts +1 -1
- package/dist/resources/extensions/google-search/index.ts +164 -47
- package/dist/resources/extensions/gsd/auto-prompts.ts +103 -24
- package/dist/resources/extensions/gsd/auto-worktree.ts +93 -9
- package/dist/resources/extensions/gsd/auto.ts +424 -30
- package/dist/resources/extensions/gsd/commands.ts +518 -36
- package/dist/resources/extensions/gsd/context-budget.ts +243 -0
- package/dist/resources/extensions/gsd/context-store.ts +195 -0
- package/dist/resources/extensions/gsd/dashboard-overlay.ts +41 -3
- package/dist/resources/extensions/gsd/db-writer.ts +341 -0
- package/dist/resources/extensions/gsd/debug-logger.ts +178 -0
- package/dist/resources/extensions/gsd/dispatch-guard.ts +0 -1
- package/dist/resources/extensions/gsd/docs/preferences-reference.md +54 -0
- package/dist/resources/extensions/gsd/doctor-proactive.ts +286 -0
- package/dist/resources/extensions/gsd/doctor.ts +283 -2
- package/dist/resources/extensions/gsd/export.ts +81 -2
- package/dist/resources/extensions/gsd/files.ts +39 -9
- package/dist/resources/extensions/gsd/git-service.ts +6 -0
- package/dist/resources/extensions/gsd/gsd-db.ts +752 -0
- package/dist/resources/extensions/gsd/guided-flow.ts +26 -1
- package/dist/resources/extensions/gsd/history.ts +0 -1
- package/dist/resources/extensions/gsd/index.ts +277 -1
- package/dist/resources/extensions/gsd/md-importer.ts +526 -0
- package/dist/resources/extensions/gsd/metrics.ts +39 -3
- package/dist/resources/extensions/gsd/notifications.ts +0 -1
- package/dist/resources/extensions/gsd/post-unit-hooks.ts +70 -1
- package/dist/resources/extensions/gsd/preferences.ts +125 -150
- package/dist/resources/extensions/gsd/prompts/execute-task.md +3 -5
- package/dist/resources/extensions/gsd/prompts/heal-skill.md +45 -0
- package/dist/resources/extensions/gsd/prompts/plan-slice.md +5 -1
- package/dist/resources/extensions/gsd/prompts/quick-task.md +48 -0
- package/dist/resources/extensions/gsd/prompts/system.md +2 -1
- package/dist/resources/extensions/gsd/quick.ts +156 -0
- package/dist/resources/extensions/gsd/skill-discovery.ts +5 -3
- package/dist/resources/extensions/gsd/skill-health.ts +417 -0
- package/dist/resources/extensions/gsd/skill-telemetry.ts +127 -0
- package/dist/resources/extensions/gsd/state.ts +30 -0
- package/dist/resources/extensions/gsd/templates/preferences.md +1 -0
- package/dist/resources/extensions/gsd/tests/context-budget.test.ts +283 -0
- package/dist/resources/extensions/gsd/tests/context-compression.test.ts +1 -1
- package/dist/resources/extensions/gsd/tests/context-store.test.ts +462 -0
- package/dist/resources/extensions/gsd/tests/continue-here.test.ts +204 -0
- package/dist/resources/extensions/gsd/tests/dashboard-budget.test.ts +346 -0
- package/dist/resources/extensions/gsd/tests/db-writer.test.ts +602 -0
- package/dist/resources/extensions/gsd/tests/debug-logger.test.ts +185 -0
- package/dist/resources/extensions/gsd/tests/derive-state-db.test.ts +406 -0
- package/dist/resources/extensions/gsd/tests/dispatch-guard.test.ts +0 -1
- package/dist/resources/extensions/gsd/tests/dist-redirect.mjs +22 -0
- package/dist/resources/extensions/gsd/tests/doctor-proactive.test.ts +244 -0
- package/dist/resources/extensions/gsd/tests/doctor-runtime.test.ts +303 -0
- package/dist/resources/extensions/gsd/tests/gsd-db.test.ts +353 -0
- package/dist/resources/extensions/gsd/tests/gsd-inspect.test.ts +125 -0
- package/dist/resources/extensions/gsd/tests/gsd-tools.test.ts +326 -0
- package/dist/resources/extensions/gsd/tests/integration-edge.test.ts +228 -0
- package/dist/resources/extensions/gsd/tests/integration-lifecycle.test.ts +277 -0
- package/dist/resources/extensions/gsd/tests/md-importer.test.ts +411 -0
- package/dist/resources/extensions/gsd/tests/metrics.test.ts +197 -0
- package/dist/resources/extensions/gsd/tests/model-isolation.test.ts +99 -0
- package/dist/resources/extensions/gsd/tests/parsers.test.ts +40 -0
- package/dist/resources/extensions/gsd/tests/post-unit-hooks.test.ts +41 -1
- package/dist/resources/extensions/gsd/tests/preferences-git.test.ts +0 -1
- package/dist/resources/extensions/gsd/tests/preferences-hooks.test.ts +0 -1
- package/dist/resources/extensions/gsd/tests/preferences-mode.test.ts +110 -0
- package/dist/resources/extensions/gsd/tests/preferences-models.test.ts +0 -1
- package/dist/resources/extensions/gsd/tests/prompt-budget-enforcement.test.ts +464 -0
- package/dist/resources/extensions/gsd/tests/prompt-db.test.ts +385 -0
- package/dist/resources/extensions/gsd/tests/remote-questions.test.ts +262 -1
- package/dist/resources/extensions/gsd/tests/resolve-ts-hooks.mjs +17 -29
- package/dist/resources/extensions/gsd/tests/resolve-ts.mjs +2 -8
- package/dist/resources/extensions/gsd/tests/skill-lifecycle.test.ts +126 -0
- package/dist/resources/extensions/gsd/tests/stop-auto-remote.test.ts +31 -8
- package/dist/resources/extensions/gsd/tests/token-savings.test.ts +366 -0
- package/dist/resources/extensions/gsd/tests/unit-runtime.test.ts +25 -1
- package/dist/resources/extensions/gsd/tests/visualizer-critical-path.test.ts +145 -0
- package/dist/resources/extensions/gsd/tests/visualizer-data.test.ts +92 -0
- package/dist/resources/extensions/gsd/tests/visualizer-overlay.test.ts +120 -0
- package/dist/resources/extensions/gsd/tests/visualizer-views.test.ts +228 -5
- package/dist/resources/extensions/gsd/tests/worktree-db-integration.test.ts +205 -0
- package/dist/resources/extensions/gsd/tests/worktree-db.test.ts +442 -0
- package/dist/resources/extensions/gsd/tests/worktree-post-create-hook.test.ts +165 -0
- package/dist/resources/extensions/gsd/types.ts +29 -0
- package/dist/resources/extensions/gsd/undo.ts +0 -1
- package/dist/resources/extensions/gsd/unit-runtime.ts +5 -1
- package/dist/resources/extensions/gsd/visualizer-data.ts +352 -1
- package/dist/resources/extensions/gsd/visualizer-overlay.ts +166 -22
- package/dist/resources/extensions/gsd/visualizer-views.ts +464 -2
- package/dist/resources/extensions/gsd/worktree-command.ts +18 -0
- package/dist/resources/extensions/gsd/worktree-manager.ts +11 -4
- package/dist/resources/extensions/remote-questions/config.ts +4 -2
- package/dist/resources/extensions/remote-questions/discord-adapter.ts +2 -4
- package/dist/resources/extensions/remote-questions/format.ts +154 -8
- package/dist/resources/extensions/remote-questions/manager.ts +9 -7
- package/dist/resources/extensions/remote-questions/remote-command.ts +100 -4
- package/dist/resources/extensions/remote-questions/slack-adapter.ts +58 -2
- package/dist/resources/extensions/remote-questions/telegram-adapter.ts +161 -0
- package/dist/resources/extensions/remote-questions/types.ts +2 -1
- package/dist/resources/extensions/ttsr/ttsr-manager.ts +26 -0
- package/dist/resources/extensions/voice/index.ts +4 -3
- package/package.json +1 -1
- package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/agent-session.js +12 -1
- package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/loader.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/loader.js +5 -0
- package/packages/pi-coding-agent/dist/core/extensions/loader.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/lsp/client.d.ts +6 -0
- package/packages/pi-coding-agent/dist/core/lsp/client.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/lsp/client.js +25 -0
- package/packages/pi-coding-agent/dist/core/lsp/client.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/lsp/index.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/lsp/index.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/lsp/index.js +106 -3
- package/packages/pi-coding-agent/dist/core/lsp/index.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/lsp/lsp.md +6 -0
- package/packages/pi-coding-agent/dist/core/lsp/types.d.ts +35 -0
- package/packages/pi-coding-agent/dist/core/lsp/types.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/lsp/types.js +6 -0
- package/packages/pi-coding-agent/dist/core/lsp/types.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/lsp/utils.d.ts +3 -1
- package/packages/pi-coding-agent/dist/core/lsp/utils.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/lsp/utils.js +45 -0
- package/packages/pi-coding-agent/dist/core/lsp/utils.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/settings-manager.d.ts +6 -0
- package/packages/pi-coding-agent/dist/core/settings-manager.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/settings-manager.js +43 -11
- package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/system-prompt.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/system-prompt.js +7 -1
- package/packages/pi-coding-agent/dist/core/system-prompt.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/edit.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/edit.js +5 -0
- package/packages/pi-coding-agent/dist/core/tools/edit.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/index.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/tools/index.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/write.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/write.js +5 -0
- package/packages/pi-coding-agent/dist/core/tools/write.js.map +1 -1
- package/packages/pi-coding-agent/src/core/agent-session.ts +13 -1
- package/packages/pi-coding-agent/src/core/extensions/loader.ts +6 -0
- package/packages/pi-coding-agent/src/core/lsp/client.ts +26 -0
- package/packages/pi-coding-agent/src/core/lsp/index.ts +157 -2
- package/packages/pi-coding-agent/src/core/lsp/lsp.md +6 -0
- package/packages/pi-coding-agent/src/core/lsp/types.ts +53 -0
- package/packages/pi-coding-agent/src/core/lsp/utils.ts +56 -0
- package/packages/pi-coding-agent/src/core/settings-manager.ts +41 -11
- package/packages/pi-coding-agent/src/core/system-prompt.ts +7 -1
- package/packages/pi-coding-agent/src/core/tools/edit.ts +3 -0
- package/packages/pi-coding-agent/src/core/tools/write.ts +3 -0
- package/src/resources/extensions/google-search/index.ts +164 -47
- package/src/resources/extensions/gsd/auto-prompts.ts +103 -24
- package/src/resources/extensions/gsd/auto-worktree.ts +93 -9
- package/src/resources/extensions/gsd/auto.ts +424 -30
- package/src/resources/extensions/gsd/commands.ts +518 -36
- package/src/resources/extensions/gsd/context-budget.ts +243 -0
- package/src/resources/extensions/gsd/context-store.ts +195 -0
- package/src/resources/extensions/gsd/dashboard-overlay.ts +41 -3
- package/src/resources/extensions/gsd/db-writer.ts +341 -0
- package/src/resources/extensions/gsd/debug-logger.ts +178 -0
- package/src/resources/extensions/gsd/dispatch-guard.ts +0 -1
- package/src/resources/extensions/gsd/docs/preferences-reference.md +54 -0
- package/src/resources/extensions/gsd/doctor-proactive.ts +286 -0
- package/src/resources/extensions/gsd/doctor.ts +283 -2
- package/src/resources/extensions/gsd/export.ts +81 -2
- package/src/resources/extensions/gsd/files.ts +39 -9
- package/src/resources/extensions/gsd/git-service.ts +6 -0
- package/src/resources/extensions/gsd/gsd-db.ts +752 -0
- package/src/resources/extensions/gsd/guided-flow.ts +26 -1
- package/src/resources/extensions/gsd/history.ts +0 -1
- package/src/resources/extensions/gsd/index.ts +277 -1
- package/src/resources/extensions/gsd/md-importer.ts +526 -0
- package/src/resources/extensions/gsd/metrics.ts +39 -3
- package/src/resources/extensions/gsd/notifications.ts +0 -1
- package/src/resources/extensions/gsd/post-unit-hooks.ts +70 -1
- package/src/resources/extensions/gsd/preferences.ts +125 -150
- package/src/resources/extensions/gsd/prompts/execute-task.md +3 -5
- package/src/resources/extensions/gsd/prompts/heal-skill.md +45 -0
- package/src/resources/extensions/gsd/prompts/plan-slice.md +5 -1
- package/src/resources/extensions/gsd/prompts/quick-task.md +48 -0
- package/src/resources/extensions/gsd/prompts/system.md +2 -1
- package/src/resources/extensions/gsd/quick.ts +156 -0
- package/src/resources/extensions/gsd/skill-discovery.ts +5 -3
- package/src/resources/extensions/gsd/skill-health.ts +417 -0
- package/src/resources/extensions/gsd/skill-telemetry.ts +127 -0
- package/src/resources/extensions/gsd/state.ts +30 -0
- package/src/resources/extensions/gsd/templates/preferences.md +1 -0
- package/src/resources/extensions/gsd/tests/context-budget.test.ts +283 -0
- package/src/resources/extensions/gsd/tests/context-compression.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/context-store.test.ts +462 -0
- package/src/resources/extensions/gsd/tests/continue-here.test.ts +204 -0
- package/src/resources/extensions/gsd/tests/dashboard-budget.test.ts +346 -0
- package/src/resources/extensions/gsd/tests/db-writer.test.ts +602 -0
- package/src/resources/extensions/gsd/tests/debug-logger.test.ts +185 -0
- package/src/resources/extensions/gsd/tests/derive-state-db.test.ts +406 -0
- package/src/resources/extensions/gsd/tests/dispatch-guard.test.ts +0 -1
- package/src/resources/extensions/gsd/tests/dist-redirect.mjs +22 -0
- package/src/resources/extensions/gsd/tests/doctor-proactive.test.ts +244 -0
- package/src/resources/extensions/gsd/tests/doctor-runtime.test.ts +303 -0
- package/src/resources/extensions/gsd/tests/gsd-db.test.ts +353 -0
- package/src/resources/extensions/gsd/tests/gsd-inspect.test.ts +125 -0
- package/src/resources/extensions/gsd/tests/gsd-tools.test.ts +326 -0
- package/src/resources/extensions/gsd/tests/integration-edge.test.ts +228 -0
- package/src/resources/extensions/gsd/tests/integration-lifecycle.test.ts +277 -0
- package/src/resources/extensions/gsd/tests/md-importer.test.ts +411 -0
- package/src/resources/extensions/gsd/tests/metrics.test.ts +197 -0
- package/src/resources/extensions/gsd/tests/model-isolation.test.ts +99 -0
- package/src/resources/extensions/gsd/tests/parsers.test.ts +40 -0
- package/src/resources/extensions/gsd/tests/post-unit-hooks.test.ts +41 -1
- package/src/resources/extensions/gsd/tests/preferences-git.test.ts +0 -1
- package/src/resources/extensions/gsd/tests/preferences-hooks.test.ts +0 -1
- package/src/resources/extensions/gsd/tests/preferences-mode.test.ts +110 -0
- package/src/resources/extensions/gsd/tests/preferences-models.test.ts +0 -1
- package/src/resources/extensions/gsd/tests/prompt-budget-enforcement.test.ts +464 -0
- package/src/resources/extensions/gsd/tests/prompt-db.test.ts +385 -0
- package/src/resources/extensions/gsd/tests/remote-questions.test.ts +262 -1
- package/src/resources/extensions/gsd/tests/resolve-ts-hooks.mjs +17 -29
- package/src/resources/extensions/gsd/tests/resolve-ts.mjs +2 -8
- package/src/resources/extensions/gsd/tests/skill-lifecycle.test.ts +126 -0
- package/src/resources/extensions/gsd/tests/stop-auto-remote.test.ts +31 -8
- package/src/resources/extensions/gsd/tests/token-savings.test.ts +366 -0
- package/src/resources/extensions/gsd/tests/unit-runtime.test.ts +25 -1
- package/src/resources/extensions/gsd/tests/visualizer-critical-path.test.ts +145 -0
- package/src/resources/extensions/gsd/tests/visualizer-data.test.ts +92 -0
- package/src/resources/extensions/gsd/tests/visualizer-overlay.test.ts +120 -0
- package/src/resources/extensions/gsd/tests/visualizer-views.test.ts +228 -5
- package/src/resources/extensions/gsd/tests/worktree-db-integration.test.ts +205 -0
- package/src/resources/extensions/gsd/tests/worktree-db.test.ts +442 -0
- package/src/resources/extensions/gsd/tests/worktree-post-create-hook.test.ts +165 -0
- package/src/resources/extensions/gsd/types.ts +29 -0
- package/src/resources/extensions/gsd/undo.ts +0 -1
- package/src/resources/extensions/gsd/unit-runtime.ts +5 -1
- package/src/resources/extensions/gsd/visualizer-data.ts +352 -1
- package/src/resources/extensions/gsd/visualizer-overlay.ts +166 -22
- package/src/resources/extensions/gsd/visualizer-views.ts +464 -2
- package/src/resources/extensions/gsd/worktree-command.ts +18 -0
- package/src/resources/extensions/gsd/worktree-manager.ts +11 -4
- package/src/resources/extensions/remote-questions/config.ts +4 -2
- package/src/resources/extensions/remote-questions/discord-adapter.ts +2 -4
- package/src/resources/extensions/remote-questions/format.ts +154 -8
- package/src/resources/extensions/remote-questions/manager.ts +9 -7
- package/src/resources/extensions/remote-questions/remote-command.ts +100 -4
- package/src/resources/extensions/remote-questions/slack-adapter.ts +58 -2
- package/src/resources/extensions/remote-questions/telegram-adapter.ts +161 -0
- package/src/resources/extensions/remote-questions/types.ts +2 -1
- package/src/resources/extensions/ttsr/ttsr-manager.ts +26 -0
- package/src/resources/extensions/voice/index.ts +4 -3
|
@@ -0,0 +1,752 @@
|
|
|
1
|
+
// GSD Database Abstraction Layer
|
|
2
|
+
// Provides a SQLite database with provider fallback chain:
|
|
3
|
+
// node:sqlite (built-in) → better-sqlite3 (npm) → null (unavailable)
|
|
4
|
+
//
|
|
5
|
+
// Exposes a unified sync API for decisions and requirements storage.
|
|
6
|
+
// Schema is initialized on first open with WAL mode for file-backed DBs.
|
|
7
|
+
|
|
8
|
+
import { createRequire } from 'node:module';
|
|
9
|
+
import { copyFileSync, existsSync, mkdirSync } from 'node:fs';
|
|
10
|
+
import { dirname } from 'node:path';
|
|
11
|
+
import type { Decision, Requirement } from './types.js';
|
|
12
|
+
|
|
13
|
+
// Create a require function for loading native modules in ESM context
|
|
14
|
+
const _require = createRequire(import.meta.url);
|
|
15
|
+
|
|
16
|
+
// ─── Provider Abstraction ──────────────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Minimal interface over both node:sqlite DatabaseSync and better-sqlite3 Database.
|
|
20
|
+
* Both expose prepare().run/get/all — the adapter normalizes row objects.
|
|
21
|
+
*/
|
|
22
|
+
interface DbStatement {
|
|
23
|
+
run(...params: unknown[]): void;
|
|
24
|
+
get(...params: unknown[]): Record<string, unknown> | undefined;
|
|
25
|
+
all(...params: unknown[]): Record<string, unknown>[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface DbAdapter {
|
|
29
|
+
exec(sql: string): void;
|
|
30
|
+
prepare(sql: string): DbStatement;
|
|
31
|
+
close(): void;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
type ProviderName = 'node:sqlite' | 'better-sqlite3';
|
|
35
|
+
|
|
36
|
+
let providerName: ProviderName | null = null;
|
|
37
|
+
let providerModule: unknown = null;
|
|
38
|
+
let loadAttempted = false;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Suppress the ExperimentalWarning for SQLite from node:sqlite.
|
|
42
|
+
* Must be called before require('node:sqlite').
|
|
43
|
+
*/
|
|
44
|
+
function suppressSqliteWarning(): void {
|
|
45
|
+
const origEmit = process.emit;
|
|
46
|
+
// @ts-expect-error — overriding process.emit with filtered version
|
|
47
|
+
process.emit = function (event: string, ...args: unknown[]): boolean {
|
|
48
|
+
if (
|
|
49
|
+
event === 'warning' &&
|
|
50
|
+
args[0] &&
|
|
51
|
+
typeof args[0] === 'object' &&
|
|
52
|
+
'name' in args[0] &&
|
|
53
|
+
(args[0] as { name: string }).name === 'ExperimentalWarning' &&
|
|
54
|
+
'message' in args[0] &&
|
|
55
|
+
typeof (args[0] as { message: string }).message === 'string' &&
|
|
56
|
+
(args[0] as { message: string }).message.includes('SQLite')
|
|
57
|
+
) {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
return origEmit.apply(process, [event, ...args] as Parameters<typeof process.emit>) as unknown as boolean;
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function loadProvider(): void {
|
|
65
|
+
if (loadAttempted) return;
|
|
66
|
+
loadAttempted = true;
|
|
67
|
+
|
|
68
|
+
// Try node:sqlite first
|
|
69
|
+
try {
|
|
70
|
+
suppressSqliteWarning();
|
|
71
|
+
const mod = _require('node:sqlite');
|
|
72
|
+
if (mod.DatabaseSync) {
|
|
73
|
+
providerModule = mod;
|
|
74
|
+
providerName = 'node:sqlite';
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
} catch {
|
|
78
|
+
// node:sqlite not available
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Try better-sqlite3
|
|
82
|
+
try {
|
|
83
|
+
const mod = _require('better-sqlite3');
|
|
84
|
+
if (typeof mod === 'function' || (mod && mod.default)) {
|
|
85
|
+
providerModule = mod.default || mod;
|
|
86
|
+
providerName = 'better-sqlite3';
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
} catch {
|
|
90
|
+
// better-sqlite3 not available
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
process.stderr.write('gsd-db: No SQLite provider available (tried node:sqlite, better-sqlite3)\n');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ─── Database Adapter ──────────────────────────────────────────────────────
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Normalize a row from node:sqlite (null-prototype) to a plain object.
|
|
100
|
+
*/
|
|
101
|
+
function normalizeRow(row: unknown): Record<string, unknown> | undefined {
|
|
102
|
+
if (row == null) return undefined;
|
|
103
|
+
if (Object.getPrototypeOf(row) === null) {
|
|
104
|
+
return { ...row as Record<string, unknown> };
|
|
105
|
+
}
|
|
106
|
+
return row as Record<string, unknown>;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function normalizeRows(rows: unknown[]): Record<string, unknown>[] {
|
|
110
|
+
return rows.map(r => normalizeRow(r)!);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function createAdapter(rawDb: unknown): DbAdapter {
|
|
114
|
+
const db = rawDb as {
|
|
115
|
+
exec(sql: string): void;
|
|
116
|
+
prepare(sql: string): {
|
|
117
|
+
run(...args: unknown[]): unknown;
|
|
118
|
+
get(...args: unknown[]): unknown;
|
|
119
|
+
all(...args: unknown[]): unknown[];
|
|
120
|
+
};
|
|
121
|
+
close(): void;
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
exec(sql: string): void {
|
|
126
|
+
db.exec(sql);
|
|
127
|
+
},
|
|
128
|
+
prepare(sql: string): DbStatement {
|
|
129
|
+
const stmt = db.prepare(sql);
|
|
130
|
+
return {
|
|
131
|
+
run(...params: unknown[]): void {
|
|
132
|
+
stmt.run(...params);
|
|
133
|
+
},
|
|
134
|
+
get(...params: unknown[]): Record<string, unknown> | undefined {
|
|
135
|
+
return normalizeRow(stmt.get(...params));
|
|
136
|
+
},
|
|
137
|
+
all(...params: unknown[]): Record<string, unknown>[] {
|
|
138
|
+
return normalizeRows(stmt.all(...params));
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
},
|
|
142
|
+
close(): void {
|
|
143
|
+
db.close();
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function openRawDb(path: string): unknown {
|
|
149
|
+
loadProvider();
|
|
150
|
+
if (!providerModule || !providerName) return null;
|
|
151
|
+
|
|
152
|
+
if (providerName === 'node:sqlite') {
|
|
153
|
+
const { DatabaseSync } = providerModule as { DatabaseSync: new (path: string) => unknown };
|
|
154
|
+
return new DatabaseSync(path);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// better-sqlite3
|
|
158
|
+
const Database = providerModule as new (path: string) => unknown;
|
|
159
|
+
return new Database(path);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// ─── Schema ────────────────────────────────────────────────────────────────
|
|
163
|
+
|
|
164
|
+
const SCHEMA_VERSION = 2;
|
|
165
|
+
|
|
166
|
+
function initSchema(db: DbAdapter, fileBacked: boolean): void {
|
|
167
|
+
// WAL mode for file-backed databases (must be outside transaction)
|
|
168
|
+
if (fileBacked) {
|
|
169
|
+
db.exec('PRAGMA journal_mode=WAL');
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
db.exec('BEGIN');
|
|
173
|
+
try {
|
|
174
|
+
db.exec(`
|
|
175
|
+
CREATE TABLE IF NOT EXISTS schema_version (
|
|
176
|
+
version INTEGER NOT NULL,
|
|
177
|
+
applied_at TEXT NOT NULL
|
|
178
|
+
)
|
|
179
|
+
`);
|
|
180
|
+
|
|
181
|
+
db.exec(`
|
|
182
|
+
CREATE TABLE IF NOT EXISTS decisions (
|
|
183
|
+
seq INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
184
|
+
id TEXT NOT NULL UNIQUE,
|
|
185
|
+
when_context TEXT NOT NULL DEFAULT '',
|
|
186
|
+
scope TEXT NOT NULL DEFAULT '',
|
|
187
|
+
decision TEXT NOT NULL DEFAULT '',
|
|
188
|
+
choice TEXT NOT NULL DEFAULT '',
|
|
189
|
+
rationale TEXT NOT NULL DEFAULT '',
|
|
190
|
+
revisable TEXT NOT NULL DEFAULT '',
|
|
191
|
+
superseded_by TEXT DEFAULT NULL
|
|
192
|
+
)
|
|
193
|
+
`);
|
|
194
|
+
|
|
195
|
+
db.exec(`
|
|
196
|
+
CREATE TABLE IF NOT EXISTS requirements (
|
|
197
|
+
id TEXT PRIMARY KEY,
|
|
198
|
+
class TEXT NOT NULL DEFAULT '',
|
|
199
|
+
status TEXT NOT NULL DEFAULT '',
|
|
200
|
+
description TEXT NOT NULL DEFAULT '',
|
|
201
|
+
why TEXT NOT NULL DEFAULT '',
|
|
202
|
+
source TEXT NOT NULL DEFAULT '',
|
|
203
|
+
primary_owner TEXT NOT NULL DEFAULT '',
|
|
204
|
+
supporting_slices TEXT NOT NULL DEFAULT '',
|
|
205
|
+
validation TEXT NOT NULL DEFAULT '',
|
|
206
|
+
notes TEXT NOT NULL DEFAULT '',
|
|
207
|
+
full_content TEXT NOT NULL DEFAULT '',
|
|
208
|
+
superseded_by TEXT DEFAULT NULL
|
|
209
|
+
)
|
|
210
|
+
`);
|
|
211
|
+
|
|
212
|
+
db.exec(`
|
|
213
|
+
CREATE TABLE IF NOT EXISTS artifacts (
|
|
214
|
+
path TEXT PRIMARY KEY,
|
|
215
|
+
artifact_type TEXT NOT NULL DEFAULT '',
|
|
216
|
+
milestone_id TEXT DEFAULT NULL,
|
|
217
|
+
slice_id TEXT DEFAULT NULL,
|
|
218
|
+
task_id TEXT DEFAULT NULL,
|
|
219
|
+
full_content TEXT NOT NULL DEFAULT '',
|
|
220
|
+
imported_at TEXT NOT NULL DEFAULT ''
|
|
221
|
+
)
|
|
222
|
+
`);
|
|
223
|
+
|
|
224
|
+
// Views — DROP + CREATE since CREATE VIEW IF NOT EXISTS doesn't update definitions
|
|
225
|
+
db.exec(`CREATE VIEW IF NOT EXISTS active_decisions AS SELECT * FROM decisions WHERE superseded_by IS NULL`);
|
|
226
|
+
db.exec(`CREATE VIEW IF NOT EXISTS active_requirements AS SELECT * FROM requirements WHERE superseded_by IS NULL`);
|
|
227
|
+
|
|
228
|
+
// Insert schema version if not already present
|
|
229
|
+
const existing = db.prepare('SELECT count(*) as cnt FROM schema_version').get();
|
|
230
|
+
if (existing && (existing['cnt'] as number) === 0) {
|
|
231
|
+
db.prepare('INSERT INTO schema_version (version, applied_at) VALUES (:version, :applied_at)').run(
|
|
232
|
+
{ ':version': SCHEMA_VERSION, ':applied_at': new Date().toISOString() },
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
db.exec('COMMIT');
|
|
237
|
+
} catch (err) {
|
|
238
|
+
db.exec('ROLLBACK');
|
|
239
|
+
throw err;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Run incremental migrations for existing databases
|
|
243
|
+
migrateSchema(db);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Incremental schema migration. Reads current version from schema_version table
|
|
248
|
+
* and applies DDL for each version step up to SCHEMA_VERSION.
|
|
249
|
+
*/
|
|
250
|
+
function migrateSchema(db: DbAdapter): void {
|
|
251
|
+
const row = db.prepare('SELECT MAX(version) as v FROM schema_version').get();
|
|
252
|
+
const currentVersion = row ? (row['v'] as number) : 0;
|
|
253
|
+
|
|
254
|
+
if (currentVersion >= SCHEMA_VERSION) return;
|
|
255
|
+
|
|
256
|
+
db.exec('BEGIN');
|
|
257
|
+
try {
|
|
258
|
+
// v1 → v2: add artifacts table
|
|
259
|
+
if (currentVersion < 2) {
|
|
260
|
+
db.exec(`
|
|
261
|
+
CREATE TABLE IF NOT EXISTS artifacts (
|
|
262
|
+
path TEXT PRIMARY KEY,
|
|
263
|
+
artifact_type TEXT NOT NULL DEFAULT '',
|
|
264
|
+
milestone_id TEXT DEFAULT NULL,
|
|
265
|
+
slice_id TEXT DEFAULT NULL,
|
|
266
|
+
task_id TEXT DEFAULT NULL,
|
|
267
|
+
full_content TEXT NOT NULL DEFAULT '',
|
|
268
|
+
imported_at TEXT NOT NULL DEFAULT ''
|
|
269
|
+
)
|
|
270
|
+
`);
|
|
271
|
+
|
|
272
|
+
db.prepare('INSERT INTO schema_version (version, applied_at) VALUES (:version, :applied_at)').run(
|
|
273
|
+
{ ':version': 2, ':applied_at': new Date().toISOString() },
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
db.exec('COMMIT');
|
|
278
|
+
} catch (err) {
|
|
279
|
+
db.exec('ROLLBACK');
|
|
280
|
+
throw err;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// ─── Module State ──────────────────────────────────────────────────────────
|
|
285
|
+
|
|
286
|
+
let currentDb: DbAdapter | null = null;
|
|
287
|
+
let currentPath: string | null = null;
|
|
288
|
+
|
|
289
|
+
// ─── Public API ────────────────────────────────────────────────────────────
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Returns which SQLite provider is available, or null if none.
|
|
293
|
+
*/
|
|
294
|
+
export function getDbProvider(): ProviderName | null {
|
|
295
|
+
loadProvider();
|
|
296
|
+
return providerName;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Returns true if a database is currently open and usable.
|
|
301
|
+
*/
|
|
302
|
+
export function isDbAvailable(): boolean {
|
|
303
|
+
return currentDb !== null;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Opens (or creates) a SQLite database at the given path.
|
|
308
|
+
* Initializes schema if needed. Sets WAL mode for file-backed DBs.
|
|
309
|
+
* Returns true on success, false if no provider is available.
|
|
310
|
+
*/
|
|
311
|
+
export function openDatabase(path: string): boolean {
|
|
312
|
+
// Close existing if different path
|
|
313
|
+
if (currentDb && currentPath !== path) {
|
|
314
|
+
closeDatabase();
|
|
315
|
+
}
|
|
316
|
+
if (currentDb && currentPath === path) {
|
|
317
|
+
return true; // already open
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const rawDb = openRawDb(path);
|
|
321
|
+
if (!rawDb) return false;
|
|
322
|
+
|
|
323
|
+
const adapter = createAdapter(rawDb);
|
|
324
|
+
const fileBacked = path !== ':memory:';
|
|
325
|
+
|
|
326
|
+
try {
|
|
327
|
+
initSchema(adapter, fileBacked);
|
|
328
|
+
} catch (err) {
|
|
329
|
+
try { adapter.close(); } catch { /* swallow */ }
|
|
330
|
+
throw err;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
currentDb = adapter;
|
|
334
|
+
currentPath = path;
|
|
335
|
+
return true;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Closes the current database connection.
|
|
340
|
+
*/
|
|
341
|
+
export function closeDatabase(): void {
|
|
342
|
+
if (currentDb) {
|
|
343
|
+
try {
|
|
344
|
+
currentDb.close();
|
|
345
|
+
} catch {
|
|
346
|
+
// swallow close errors
|
|
347
|
+
}
|
|
348
|
+
currentDb = null;
|
|
349
|
+
currentPath = null;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Runs a function inside a transaction. Rolls back on error.
|
|
355
|
+
*/
|
|
356
|
+
export function transaction<T>(fn: () => T): T {
|
|
357
|
+
if (!currentDb) throw new Error('gsd-db: No database open');
|
|
358
|
+
currentDb.exec('BEGIN');
|
|
359
|
+
try {
|
|
360
|
+
const result = fn();
|
|
361
|
+
currentDb.exec('COMMIT');
|
|
362
|
+
return result;
|
|
363
|
+
} catch (err) {
|
|
364
|
+
currentDb.exec('ROLLBACK');
|
|
365
|
+
throw err;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// ─── Decision Wrappers ────────────────────────────────────────────────────
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Insert a decision. The `seq` field is auto-generated.
|
|
373
|
+
*/
|
|
374
|
+
export function insertDecision(d: Omit<Decision, 'seq'>): void {
|
|
375
|
+
if (!currentDb) throw new Error('gsd-db: No database open');
|
|
376
|
+
currentDb.prepare(
|
|
377
|
+
`INSERT INTO decisions (id, when_context, scope, decision, choice, rationale, revisable, superseded_by)
|
|
378
|
+
VALUES (:id, :when_context, :scope, :decision, :choice, :rationale, :revisable, :superseded_by)`,
|
|
379
|
+
).run({
|
|
380
|
+
':id': d.id,
|
|
381
|
+
':when_context': d.when_context,
|
|
382
|
+
':scope': d.scope,
|
|
383
|
+
':decision': d.decision,
|
|
384
|
+
':choice': d.choice,
|
|
385
|
+
':rationale': d.rationale,
|
|
386
|
+
':revisable': d.revisable,
|
|
387
|
+
':superseded_by': d.superseded_by,
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Get a decision by its ID (e.g. "D001"). Returns null if not found.
|
|
393
|
+
*/
|
|
394
|
+
export function getDecisionById(id: string): Decision | null {
|
|
395
|
+
if (!currentDb) return null;
|
|
396
|
+
const row = currentDb.prepare('SELECT * FROM decisions WHERE id = ?').get(id);
|
|
397
|
+
if (!row) return null;
|
|
398
|
+
return {
|
|
399
|
+
seq: row['seq'] as number,
|
|
400
|
+
id: row['id'] as string,
|
|
401
|
+
when_context: row['when_context'] as string,
|
|
402
|
+
scope: row['scope'] as string,
|
|
403
|
+
decision: row['decision'] as string,
|
|
404
|
+
choice: row['choice'] as string,
|
|
405
|
+
rationale: row['rationale'] as string,
|
|
406
|
+
revisable: row['revisable'] as string,
|
|
407
|
+
superseded_by: (row['superseded_by'] as string) ?? null,
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Get all active (non-superseded) decisions.
|
|
413
|
+
*/
|
|
414
|
+
export function getActiveDecisions(): Decision[] {
|
|
415
|
+
if (!currentDb) return [];
|
|
416
|
+
const rows = currentDb.prepare('SELECT * FROM active_decisions').all();
|
|
417
|
+
return rows.map(row => ({
|
|
418
|
+
seq: row['seq'] as number,
|
|
419
|
+
id: row['id'] as string,
|
|
420
|
+
when_context: row['when_context'] as string,
|
|
421
|
+
scope: row['scope'] as string,
|
|
422
|
+
decision: row['decision'] as string,
|
|
423
|
+
choice: row['choice'] as string,
|
|
424
|
+
rationale: row['rationale'] as string,
|
|
425
|
+
revisable: row['revisable'] as string,
|
|
426
|
+
superseded_by: null,
|
|
427
|
+
}));
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// ─── Requirement Wrappers ─────────────────────────────────────────────────
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Insert a requirement.
|
|
434
|
+
*/
|
|
435
|
+
export function insertRequirement(r: Requirement): void {
|
|
436
|
+
if (!currentDb) throw new Error('gsd-db: No database open');
|
|
437
|
+
currentDb.prepare(
|
|
438
|
+
`INSERT INTO requirements (id, class, status, description, why, source, primary_owner, supporting_slices, validation, notes, full_content, superseded_by)
|
|
439
|
+
VALUES (:id, :class, :status, :description, :why, :source, :primary_owner, :supporting_slices, :validation, :notes, :full_content, :superseded_by)`,
|
|
440
|
+
).run({
|
|
441
|
+
':id': r.id,
|
|
442
|
+
':class': r.class,
|
|
443
|
+
':status': r.status,
|
|
444
|
+
':description': r.description,
|
|
445
|
+
':why': r.why,
|
|
446
|
+
':source': r.source,
|
|
447
|
+
':primary_owner': r.primary_owner,
|
|
448
|
+
':supporting_slices': r.supporting_slices,
|
|
449
|
+
':validation': r.validation,
|
|
450
|
+
':notes': r.notes,
|
|
451
|
+
':full_content': r.full_content,
|
|
452
|
+
':superseded_by': r.superseded_by,
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* Get a requirement by its ID (e.g. "R001"). Returns null if not found.
|
|
458
|
+
*/
|
|
459
|
+
export function getRequirementById(id: string): Requirement | null {
|
|
460
|
+
if (!currentDb) return null;
|
|
461
|
+
const row = currentDb.prepare('SELECT * FROM requirements WHERE id = ?').get(id);
|
|
462
|
+
if (!row) return null;
|
|
463
|
+
return {
|
|
464
|
+
id: row['id'] as string,
|
|
465
|
+
class: row['class'] as string,
|
|
466
|
+
status: row['status'] as string,
|
|
467
|
+
description: row['description'] as string,
|
|
468
|
+
why: row['why'] as string,
|
|
469
|
+
source: row['source'] as string,
|
|
470
|
+
primary_owner: row['primary_owner'] as string,
|
|
471
|
+
supporting_slices: row['supporting_slices'] as string,
|
|
472
|
+
validation: row['validation'] as string,
|
|
473
|
+
notes: row['notes'] as string,
|
|
474
|
+
full_content: row['full_content'] as string,
|
|
475
|
+
superseded_by: (row['superseded_by'] as string) ?? null,
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* Get all active (non-superseded) requirements.
|
|
481
|
+
*/
|
|
482
|
+
export function getActiveRequirements(): Requirement[] {
|
|
483
|
+
if (!currentDb) return [];
|
|
484
|
+
const rows = currentDb.prepare('SELECT * FROM active_requirements').all();
|
|
485
|
+
return rows.map(row => ({
|
|
486
|
+
id: row['id'] as string,
|
|
487
|
+
class: row['class'] as string,
|
|
488
|
+
status: row['status'] as string,
|
|
489
|
+
description: row['description'] as string,
|
|
490
|
+
why: row['why'] as string,
|
|
491
|
+
source: row['source'] as string,
|
|
492
|
+
primary_owner: row['primary_owner'] as string,
|
|
493
|
+
supporting_slices: row['supporting_slices'] as string,
|
|
494
|
+
validation: row['validation'] as string,
|
|
495
|
+
notes: row['notes'] as string,
|
|
496
|
+
full_content: row['full_content'] as string,
|
|
497
|
+
superseded_by: null,
|
|
498
|
+
}));
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// ─── Worktree DB Operations ────────────────────────────────────────────────
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* Copy a gsd.db file to a new worktree location.
|
|
505
|
+
* Copies only the .db file — skips -wal and -shm files so the copy starts clean.
|
|
506
|
+
* Returns true on success, false on failure (never throws).
|
|
507
|
+
*/
|
|
508
|
+
export function copyWorktreeDb(srcDbPath: string, destDbPath: string): boolean {
|
|
509
|
+
try {
|
|
510
|
+
if (!existsSync(srcDbPath)) {
|
|
511
|
+
return false; // source doesn't exist — expected when no DB yet
|
|
512
|
+
}
|
|
513
|
+
const destDir = dirname(destDbPath);
|
|
514
|
+
mkdirSync(destDir, { recursive: true });
|
|
515
|
+
copyFileSync(srcDbPath, destDbPath);
|
|
516
|
+
return true;
|
|
517
|
+
} catch (err) {
|
|
518
|
+
process.stderr.write(`gsd-db: failed to copy DB to worktree: ${(err as Error).message}\n`);
|
|
519
|
+
return false;
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/**
|
|
524
|
+
* Reconcile rows from a worktree DB back into the main DB using ATTACH DATABASE.
|
|
525
|
+
* Merges all three tables (decisions, requirements, artifacts) via INSERT OR REPLACE.
|
|
526
|
+
* Detects conflicts where both DBs modified the same row.
|
|
527
|
+
*
|
|
528
|
+
* ATTACH must happen outside any transaction. INSERT OR REPLACE runs inside a transaction.
|
|
529
|
+
* DETACH happens after commit (or rollback on error).
|
|
530
|
+
*/
|
|
531
|
+
export function reconcileWorktreeDb(
|
|
532
|
+
mainDbPath: string,
|
|
533
|
+
worktreeDbPath: string,
|
|
534
|
+
): { decisions: number; requirements: number; artifacts: number; conflicts: string[] } {
|
|
535
|
+
const zero = { decisions: 0, requirements: 0, artifacts: 0, conflicts: [] as string[] };
|
|
536
|
+
|
|
537
|
+
// Validate worktree DB exists
|
|
538
|
+
if (!existsSync(worktreeDbPath)) {
|
|
539
|
+
return zero;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// Safety: reject single quotes which could break the ATTACH DATABASE '...' SQL literal.
|
|
543
|
+
// SQLite ATTACH doesn't support parameterized binding. We block the one dangerous char
|
|
544
|
+
// rather than allowlisting, since OS temp paths vary widely (tildes, parens, unicode).
|
|
545
|
+
if (worktreeDbPath.includes("'")) {
|
|
546
|
+
process.stderr.write(`gsd-db: worktree DB reconciliation failed: path contains unsafe characters\n`);
|
|
547
|
+
return zero;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// Ensure main DB is open
|
|
551
|
+
if (!currentDb) {
|
|
552
|
+
const opened = openDatabase(mainDbPath);
|
|
553
|
+
if (!opened) {
|
|
554
|
+
process.stderr.write(`gsd-db: worktree DB reconciliation failed: cannot open main DB\n`);
|
|
555
|
+
return zero;
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
const adapter = currentDb!;
|
|
560
|
+
const conflicts: string[] = [];
|
|
561
|
+
|
|
562
|
+
try {
|
|
563
|
+
// ATTACH must be outside transaction
|
|
564
|
+
adapter.exec(`ATTACH DATABASE '${worktreeDbPath}' AS wt`);
|
|
565
|
+
|
|
566
|
+
try {
|
|
567
|
+
// ── Conflict detection phase ──
|
|
568
|
+
// Decisions: same id, different content
|
|
569
|
+
const decisionConflicts = adapter.prepare(
|
|
570
|
+
`SELECT m.id FROM decisions m
|
|
571
|
+
INNER JOIN wt.decisions w ON m.id = w.id
|
|
572
|
+
WHERE m.decision != w.decision
|
|
573
|
+
OR m.choice != w.choice
|
|
574
|
+
OR m.rationale != w.rationale
|
|
575
|
+
OR m.superseded_by IS NOT w.superseded_by`,
|
|
576
|
+
).all();
|
|
577
|
+
for (const row of decisionConflicts) {
|
|
578
|
+
conflicts.push(`decision ${row['id']}: modified in both main and worktree`);
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
// Requirements: same id, different content
|
|
582
|
+
const reqConflicts = adapter.prepare(
|
|
583
|
+
`SELECT m.id FROM requirements m
|
|
584
|
+
INNER JOIN wt.requirements w ON m.id = w.id
|
|
585
|
+
WHERE m.description != w.description
|
|
586
|
+
OR m.status != w.status
|
|
587
|
+
OR m.notes != w.notes
|
|
588
|
+
OR m.superseded_by IS NOT w.superseded_by`,
|
|
589
|
+
).all();
|
|
590
|
+
for (const row of reqConflicts) {
|
|
591
|
+
conflicts.push(`requirement ${row['id']}: modified in both main and worktree`);
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// Artifacts: same path, different content
|
|
595
|
+
const artifactConflicts = adapter.prepare(
|
|
596
|
+
`SELECT m.path FROM artifacts m
|
|
597
|
+
INNER JOIN wt.artifacts w ON m.path = w.path
|
|
598
|
+
WHERE m.full_content != w.full_content
|
|
599
|
+
OR m.artifact_type != w.artifact_type`,
|
|
600
|
+
).all();
|
|
601
|
+
for (const row of artifactConflicts) {
|
|
602
|
+
conflicts.push(`artifact ${row['path']}: modified in both main and worktree`);
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
// ── Merge phase (inside manual transaction) ──
|
|
606
|
+
adapter.exec('BEGIN');
|
|
607
|
+
try {
|
|
608
|
+
// Decisions: exclude seq to let main auto-assign
|
|
609
|
+
adapter.exec(
|
|
610
|
+
`INSERT OR REPLACE INTO decisions (id, when_context, scope, decision, choice, rationale, revisable, superseded_by)
|
|
611
|
+
SELECT id, when_context, scope, decision, choice, rationale, revisable, superseded_by FROM wt.decisions`,
|
|
612
|
+
);
|
|
613
|
+
const dCount = adapter.prepare('SELECT changes() as cnt').get();
|
|
614
|
+
|
|
615
|
+
// Requirements: full row copy
|
|
616
|
+
adapter.exec(
|
|
617
|
+
`INSERT OR REPLACE INTO requirements (id, class, status, description, why, source, primary_owner, supporting_slices, validation, notes, full_content, superseded_by)
|
|
618
|
+
SELECT id, class, status, description, why, source, primary_owner, supporting_slices, validation, notes, full_content, superseded_by FROM wt.requirements`,
|
|
619
|
+
);
|
|
620
|
+
const rCount = adapter.prepare('SELECT changes() as cnt').get();
|
|
621
|
+
|
|
622
|
+
// Artifacts: copy with fresh imported_at timestamp
|
|
623
|
+
adapter.exec(
|
|
624
|
+
`INSERT OR REPLACE INTO artifacts (path, artifact_type, milestone_id, slice_id, task_id, full_content, imported_at)
|
|
625
|
+
SELECT path, artifact_type, milestone_id, slice_id, task_id, full_content, datetime('now') FROM wt.artifacts`,
|
|
626
|
+
);
|
|
627
|
+
const aCount = adapter.prepare('SELECT changes() as cnt').get();
|
|
628
|
+
|
|
629
|
+
adapter.exec('COMMIT');
|
|
630
|
+
|
|
631
|
+
const result = {
|
|
632
|
+
decisions: (dCount?.['cnt'] as number) || 0,
|
|
633
|
+
requirements: (rCount?.['cnt'] as number) || 0,
|
|
634
|
+
artifacts: (aCount?.['cnt'] as number) || 0,
|
|
635
|
+
conflicts,
|
|
636
|
+
};
|
|
637
|
+
|
|
638
|
+
if (conflicts.length > 0) {
|
|
639
|
+
process.stderr.write(`gsd-db: reconciliation conflicts:\n${conflicts.map(c => ` - ${c}`).join('\n')}\n`);
|
|
640
|
+
}
|
|
641
|
+
process.stderr.write(
|
|
642
|
+
`gsd-db: reconciled ${result.decisions} decisions, ${result.requirements} requirements, ${result.artifacts} artifacts (${conflicts.length} conflicts)\n`,
|
|
643
|
+
);
|
|
644
|
+
|
|
645
|
+
return result;
|
|
646
|
+
} catch (err) {
|
|
647
|
+
adapter.exec('ROLLBACK');
|
|
648
|
+
throw err;
|
|
649
|
+
}
|
|
650
|
+
} finally {
|
|
651
|
+
// DETACH always, even on error
|
|
652
|
+
try {
|
|
653
|
+
adapter.exec('DETACH DATABASE wt');
|
|
654
|
+
} catch {
|
|
655
|
+
// swallow — may already be detached
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
} catch (err) {
|
|
659
|
+
process.stderr.write(`gsd-db: worktree DB reconciliation failed: ${(err as Error).message}\n`);
|
|
660
|
+
return zero;
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
// ─── Internal Access (for testing) ─────────────────────────────────────────
|
|
665
|
+
|
|
666
|
+
/**
|
|
667
|
+
* Get the raw adapter for direct queries (testing only).
|
|
668
|
+
*/
|
|
669
|
+
export function _getAdapter(): DbAdapter | null {
|
|
670
|
+
return currentDb;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
/**
|
|
674
|
+
* Reset provider state (testing only — allows re-detection).
|
|
675
|
+
*/
|
|
676
|
+
export function _resetProvider(): void {
|
|
677
|
+
loadAttempted = false;
|
|
678
|
+
providerModule = null;
|
|
679
|
+
providerName = null;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
// ─── Upsert Wrappers (for idempotent import) ─────────────────────────────
|
|
683
|
+
|
|
684
|
+
/**
|
|
685
|
+
* Insert or replace a decision. Uses the `id` UNIQUE constraint for idempotency.
|
|
686
|
+
*/
|
|
687
|
+
export function upsertDecision(d: Omit<Decision, 'seq'>): void {
|
|
688
|
+
if (!currentDb) throw new Error('gsd-db: No database open');
|
|
689
|
+
currentDb.prepare(
|
|
690
|
+
`INSERT OR REPLACE INTO decisions (id, when_context, scope, decision, choice, rationale, revisable, superseded_by)
|
|
691
|
+
VALUES (:id, :when_context, :scope, :decision, :choice, :rationale, :revisable, :superseded_by)`,
|
|
692
|
+
).run({
|
|
693
|
+
':id': d.id,
|
|
694
|
+
':when_context': d.when_context,
|
|
695
|
+
':scope': d.scope,
|
|
696
|
+
':decision': d.decision,
|
|
697
|
+
':choice': d.choice,
|
|
698
|
+
':rationale': d.rationale,
|
|
699
|
+
':revisable': d.revisable,
|
|
700
|
+
':superseded_by': d.superseded_by ?? null,
|
|
701
|
+
});
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
/**
|
|
705
|
+
* Insert or replace a requirement. Uses the `id` PK for idempotency.
|
|
706
|
+
*/
|
|
707
|
+
export function upsertRequirement(r: Requirement): void {
|
|
708
|
+
if (!currentDb) throw new Error('gsd-db: No database open');
|
|
709
|
+
currentDb.prepare(
|
|
710
|
+
`INSERT OR REPLACE INTO requirements (id, class, status, description, why, source, primary_owner, supporting_slices, validation, notes, full_content, superseded_by)
|
|
711
|
+
VALUES (:id, :class, :status, :description, :why, :source, :primary_owner, :supporting_slices, :validation, :notes, :full_content, :superseded_by)`,
|
|
712
|
+
).run({
|
|
713
|
+
':id': r.id,
|
|
714
|
+
':class': r.class,
|
|
715
|
+
':status': r.status,
|
|
716
|
+
':description': r.description,
|
|
717
|
+
':why': r.why,
|
|
718
|
+
':source': r.source,
|
|
719
|
+
':primary_owner': r.primary_owner,
|
|
720
|
+
':supporting_slices': r.supporting_slices,
|
|
721
|
+
':validation': r.validation,
|
|
722
|
+
':notes': r.notes,
|
|
723
|
+
':full_content': r.full_content,
|
|
724
|
+
':superseded_by': r.superseded_by ?? null,
|
|
725
|
+
});
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
/**
|
|
729
|
+
* Insert or replace an artifact. Uses the `path` PK for idempotency.
|
|
730
|
+
*/
|
|
731
|
+
export function insertArtifact(a: {
|
|
732
|
+
path: string;
|
|
733
|
+
artifact_type: string;
|
|
734
|
+
milestone_id: string | null;
|
|
735
|
+
slice_id: string | null;
|
|
736
|
+
task_id: string | null;
|
|
737
|
+
full_content: string;
|
|
738
|
+
}): void {
|
|
739
|
+
if (!currentDb) throw new Error('gsd-db: No database open');
|
|
740
|
+
currentDb.prepare(
|
|
741
|
+
`INSERT OR REPLACE INTO artifacts (path, artifact_type, milestone_id, slice_id, task_id, full_content, imported_at)
|
|
742
|
+
VALUES (:path, :artifact_type, :milestone_id, :slice_id, :task_id, :full_content, :imported_at)`,
|
|
743
|
+
).run({
|
|
744
|
+
':path': a.path,
|
|
745
|
+
':artifact_type': a.artifact_type,
|
|
746
|
+
':milestone_id': a.milestone_id,
|
|
747
|
+
':slice_id': a.slice_id,
|
|
748
|
+
':task_id': a.task_id,
|
|
749
|
+
':full_content': a.full_content,
|
|
750
|
+
':imported_at': new Date().toISOString(),
|
|
751
|
+
});
|
|
752
|
+
}
|