@hegemonart/get-design-done 1.55.0 → 1.57.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/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +90 -0
- package/README.md +6 -0
- package/SKILL.md +2 -0
- package/agents/design-fixer.md +16 -0
- package/dist/claude-code/.claude/skills/override/SKILL.md +86 -0
- package/dist/claude-code/.claude/skills/state/SKILL.md +106 -0
- package/hooks/gdd-decision-injector.js +58 -0
- package/hooks/gdd-fact-force.js +434 -0
- package/hooks/gdd-risk-gate.js +406 -0
- package/hooks/hooks.json +18 -0
- package/package.json +1 -1
- package/reference/schemas/events.schema.json +61 -1
- package/reference/skill-graph.md +3 -1
- package/scripts/lib/manifest/skills.json +16 -0
- package/scripts/lib/risk/calibration.cjs +385 -0
- package/scripts/lib/risk/compute-risk.cjs +229 -0
- package/scripts/lib/risk/consumers.cjs +211 -0
- package/scripts/lib/risk/override.cjs +87 -0
- package/scripts/lib/risk/route.cjs +59 -0
- package/scripts/lib/risk/tables.cjs +221 -0
- package/scripts/lib/state/migrate-to-sqlite.cjs +664 -0
- package/scripts/lib/state/query-surface.cjs +391 -0
- package/scripts/lib/state/render-markdown.cjs +717 -0
- package/scripts/lib/state/state-backend.cjs +345 -0
- package/scripts/lib/state/state-store.cjs +735 -0
- package/sdk/cli/index.js +193 -96
- package/sdk/dashboard/data/source.cjs +44 -5
- package/sdk/mcp/gdd-state/server.js +127 -30
- package/sdk/mcp/gdd-state/tools/get.ts +8 -0
- package/sdk/state/index.ts +267 -13
- package/sdk/state/lockfile.ts +48 -0
- package/sdk/state/schema.sql +218 -0
- package/skills/override/SKILL.md +86 -0
- package/skills/state/SKILL.md +106 -0
|
@@ -0,0 +1,664 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/**
|
|
3
|
+
* scripts/lib/state/migrate-to-sqlite.cjs - Phase 57 (SQL-02)
|
|
4
|
+
*
|
|
5
|
+
* Reads .design/STATE.md, parses it via sdk/state/parser.ts (dynamic import,
|
|
6
|
+
* never require a .ts), and UPSERTs all parsed blocks into the PINNED SQLite
|
|
7
|
+
* tables. Also pulls Phase 19.5 recall records and Phase 51 instincts into
|
|
8
|
+
* their respective tables (best-effort, degrades gracefully when absent).
|
|
9
|
+
*
|
|
10
|
+
* Programmatic API:
|
|
11
|
+
* migrateToSqlite({ projectRoot, dryRun, force })
|
|
12
|
+
* -> { migrated, tables:{...counts}, dryRun, skipped, reason }
|
|
13
|
+
*
|
|
14
|
+
* CLI usage:
|
|
15
|
+
* node migrate-to-sqlite.cjs [--migrate-state] [--dry-run] [--project-root=<path>]
|
|
16
|
+
*
|
|
17
|
+
* Critical invariants:
|
|
18
|
+
* - IDEMPOTENT: INSERT ... ON CONFLICT(id) DO UPDATE SET body_md=excluded.body_md
|
|
19
|
+
* etc. ordinal/created_at are NOT updated on conflict (preserve insert order).
|
|
20
|
+
* - --migrate-state (or force:true) is required; without it the CLI exits 0 with a
|
|
21
|
+
* notice (opt-in in v1.57.0).
|
|
22
|
+
* - --dry-run: wraps all writes in BEGIN ... ROLLBACK and prints a diff to stdout.
|
|
23
|
+
* - When BACKEND==='markdown': prints a notice and returns {skipped:true, reason:...}.
|
|
24
|
+
* - On boot: runs PRAGMA integrity_check if the db file already exists; refuses and
|
|
25
|
+
* prints a recovery hint if the db is corrupt.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
const fs = require('node:fs');
|
|
29
|
+
const path = require('node:path');
|
|
30
|
+
const crypto = require('node:crypto');
|
|
31
|
+
const { pathToFileURL } = require('node:url');
|
|
32
|
+
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
// Package-root walk-up (same pattern as sdk/dashboard/data/_pkg-root.cjs).
|
|
35
|
+
// Never use __dirname-relative cross-tree jumps - esbuild rewrites __dirname
|
|
36
|
+
// (Phase 53 lesson).
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
|
|
39
|
+
let _cachedPkgRoot = null;
|
|
40
|
+
|
|
41
|
+
function findPackageRoot(startDir) {
|
|
42
|
+
let dir = path.resolve(startDir);
|
|
43
|
+
let firstWithPkg = null;
|
|
44
|
+
for (let i = 0; i < 12; i++) {
|
|
45
|
+
const pkgPath = path.join(dir, 'package.json');
|
|
46
|
+
let pkg = null;
|
|
47
|
+
try { pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8')); } catch { pkg = null; }
|
|
48
|
+
if (pkg) {
|
|
49
|
+
if (firstWithPkg === null) firstWithPkg = dir;
|
|
50
|
+
if (pkg.name === 'get-design-done') return dir;
|
|
51
|
+
}
|
|
52
|
+
const parent = path.dirname(dir);
|
|
53
|
+
if (parent === dir) break;
|
|
54
|
+
dir = parent;
|
|
55
|
+
}
|
|
56
|
+
return firstWithPkg || path.resolve(startDir);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function packageRoot() {
|
|
60
|
+
if (_cachedPkgRoot === null) _cachedPkgRoot = findPackageRoot(__dirname);
|
|
61
|
+
return _cachedPkgRoot;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function resolveFromPkgRoot(relPath) {
|
|
65
|
+
return path.join(packageRoot(), relPath);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
// Dynamic import of sdk/state/parser.ts.
|
|
70
|
+
// NEVER require() a .ts file - always dynamic import(pathToFileURL).
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
|
|
73
|
+
let _parserPromise = null;
|
|
74
|
+
|
|
75
|
+
function importParser() {
|
|
76
|
+
if (_parserPromise === null) {
|
|
77
|
+
const absPath = resolveFromPkgRoot('sdk/state/parser.ts');
|
|
78
|
+
const url = pathToFileURL(absPath).href;
|
|
79
|
+
_parserPromise = import(url).catch((err) => {
|
|
80
|
+
_parserPromise = null;
|
|
81
|
+
throw new Error(`migrate-to-sqlite: failed to import parser.ts: ${err.message}`);
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
return _parserPromise;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ---------------------------------------------------------------------------
|
|
88
|
+
// Load state-backend (Executor A's file). Required at call-time, not at module
|
|
89
|
+
// load, so tests can inject a stub or the file can be absent during testing.
|
|
90
|
+
// ---------------------------------------------------------------------------
|
|
91
|
+
|
|
92
|
+
let _backend = null;
|
|
93
|
+
|
|
94
|
+
function loadBackend() {
|
|
95
|
+
if (_backend !== null) return _backend;
|
|
96
|
+
try {
|
|
97
|
+
_backend = require('./state-backend.cjs');
|
|
98
|
+
} catch (err) {
|
|
99
|
+
// state-backend.cjs doesn't exist yet (Executor A hasn't run) OR
|
|
100
|
+
// better-sqlite3 is absent. Return a markdown-floor stub.
|
|
101
|
+
_backend = {
|
|
102
|
+
Database: null,
|
|
103
|
+
BACKEND: 'markdown',
|
|
104
|
+
openStateDb: null,
|
|
105
|
+
checkIntegrity: null,
|
|
106
|
+
sqlitePath: (root) => path.join(root, '.design', 'state.sqlite'),
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
return _backend;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ---------------------------------------------------------------------------
|
|
113
|
+
// SHA-256 helper for last_render_sha256.
|
|
114
|
+
// ---------------------------------------------------------------------------
|
|
115
|
+
|
|
116
|
+
function sha256hex(str) {
|
|
117
|
+
return crypto.createHash('sha256').update(str, 'utf8').digest('hex');
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ---------------------------------------------------------------------------
|
|
121
|
+
// Resolve project root (used by CLI and programmatic callers).
|
|
122
|
+
// ---------------------------------------------------------------------------
|
|
123
|
+
|
|
124
|
+
function resolveProjectRoot(explicitRoot) {
|
|
125
|
+
if (explicitRoot) return path.resolve(explicitRoot);
|
|
126
|
+
if (process.env.GDD_PROJECT_ROOT) return path.resolve(process.env.GDD_PROJECT_ROOT);
|
|
127
|
+
// Walk up from cwd to find the nearest .design/STATE.md.
|
|
128
|
+
let dir = process.cwd();
|
|
129
|
+
for (let i = 0; i < 8; i++) {
|
|
130
|
+
if (fs.existsSync(path.join(dir, '.design', 'STATE.md'))) return dir;
|
|
131
|
+
const parent = path.dirname(dir);
|
|
132
|
+
if (parent === dir) break;
|
|
133
|
+
dir = parent;
|
|
134
|
+
}
|
|
135
|
+
return process.cwd();
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ---------------------------------------------------------------------------
|
|
139
|
+
// Load Phase 19.5 recall records from design-search index / recall json.
|
|
140
|
+
// Best-effort: returns [] on any error or absent file.
|
|
141
|
+
// ---------------------------------------------------------------------------
|
|
142
|
+
|
|
143
|
+
function loadRecallRecords(projectRoot) {
|
|
144
|
+
try {
|
|
145
|
+
// Phase 19.5 stores recall in .design/archive/**/*.md and LEARNINGS.md.
|
|
146
|
+
// We do a best-effort scan: look for .design/recall.json or LEARNINGS lines.
|
|
147
|
+
const recallJson = path.join(projectRoot, '.design', 'recall.json');
|
|
148
|
+
if (fs.existsSync(recallJson)) {
|
|
149
|
+
const data = JSON.parse(fs.readFileSync(recallJson, 'utf8'));
|
|
150
|
+
if (Array.isArray(data.records)) return data.records;
|
|
151
|
+
if (Array.isArray(data)) return data;
|
|
152
|
+
}
|
|
153
|
+
// Try CYCLES.md / LEARNINGS.md as a secondary source.
|
|
154
|
+
const learnings = path.join(projectRoot, '.design', 'learnings', 'LEARNINGS.md');
|
|
155
|
+
if (fs.existsSync(learnings)) {
|
|
156
|
+
const text = fs.readFileSync(learnings, 'utf8');
|
|
157
|
+
const lines = text.split('\n').filter((l) => l.trim().startsWith('- '));
|
|
158
|
+
return lines.map((l, idx) => ({
|
|
159
|
+
id: `recall-${idx}`,
|
|
160
|
+
kind: 'learning',
|
|
161
|
+
body_md: l.replace(/^-\s*/, '').trim(),
|
|
162
|
+
tags: null,
|
|
163
|
+
created_at: null,
|
|
164
|
+
}));
|
|
165
|
+
}
|
|
166
|
+
return [];
|
|
167
|
+
} catch {
|
|
168
|
+
return [];
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// ---------------------------------------------------------------------------
|
|
173
|
+
// Load Phase 51 instincts via instinct-store.load().
|
|
174
|
+
// Best-effort: returns [] on any error or absent file.
|
|
175
|
+
// ---------------------------------------------------------------------------
|
|
176
|
+
|
|
177
|
+
function loadInstincts(projectRoot) {
|
|
178
|
+
try {
|
|
179
|
+
const instinctStorePath = resolveFromPkgRoot('scripts/lib/instinct-store.cjs');
|
|
180
|
+
if (!fs.existsSync(instinctStorePath)) return [];
|
|
181
|
+
const instinctStore = require(instinctStorePath);
|
|
182
|
+
const data = instinctStore.load({ scope: 'project', baseDir: projectRoot });
|
|
183
|
+
if (Array.isArray(data.instincts)) return data.instincts;
|
|
184
|
+
return [];
|
|
185
|
+
} catch {
|
|
186
|
+
return [];
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// ---------------------------------------------------------------------------
|
|
191
|
+
// Build a dry-run diff string from pending upsert operations.
|
|
192
|
+
// ---------------------------------------------------------------------------
|
|
193
|
+
|
|
194
|
+
function buildDryRunDiff(ops) {
|
|
195
|
+
if (ops.length === 0) return '(no changes would be made)';
|
|
196
|
+
const lines = [`Dry-run diff - ${ops.length} row(s) would be inserted/updated:\n`];
|
|
197
|
+
for (const op of ops) {
|
|
198
|
+
lines.push(` [${op.action}] ${op.table} id=${op.id}`);
|
|
199
|
+
if (op.fields) {
|
|
200
|
+
for (const [k, v] of Object.entries(op.fields)) {
|
|
201
|
+
const preview = String(v).slice(0, 80).replace(/\n/g, ' ');
|
|
202
|
+
lines.push(` ${k}: ${preview}`);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return lines.join('\n');
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// ---------------------------------------------------------------------------
|
|
210
|
+
// Core migration logic.
|
|
211
|
+
// ---------------------------------------------------------------------------
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Migrate .design/STATE.md (and supplementary stores) into the SQLite database.
|
|
215
|
+
*
|
|
216
|
+
* @param {object} opts
|
|
217
|
+
* @param {string} [opts.projectRoot] - project root dir (defaults to cwd / env)
|
|
218
|
+
* @param {boolean} [opts.dryRun=false] - wrap writes in BEGIN/ROLLBACK + print diff
|
|
219
|
+
* @param {boolean} [opts.force=false] - same as --migrate-state flag; required to actually write
|
|
220
|
+
* @returns {Promise<{migrated:boolean, tables:object, dryRun:boolean, skipped:boolean, reason:string}>}
|
|
221
|
+
*/
|
|
222
|
+
async function migrateToSqlite(opts = {}) {
|
|
223
|
+
const { dryRun = false, force = false } = opts;
|
|
224
|
+
const projectRoot = resolveProjectRoot(opts.projectRoot);
|
|
225
|
+
|
|
226
|
+
// Opt-in guard: --migrate-state / force required.
|
|
227
|
+
// This fires first (before the SQLite probe) so the message is consistent
|
|
228
|
+
// regardless of whether better-sqlite3 is installed.
|
|
229
|
+
if (!force) {
|
|
230
|
+
const notice =
|
|
231
|
+
'Migration is opt-in in v1.57.0. Re-run with --migrate-state to proceed.';
|
|
232
|
+
return {
|
|
233
|
+
migrated: false,
|
|
234
|
+
tables: {},
|
|
235
|
+
dryRun,
|
|
236
|
+
skipped: true,
|
|
237
|
+
reason: notice,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const backend = loadBackend();
|
|
242
|
+
const { Database, BACKEND, openStateDb, checkIntegrity, sqlitePath } = backend;
|
|
243
|
+
|
|
244
|
+
// Markdown floor: better-sqlite3 not available.
|
|
245
|
+
if (BACKEND !== 'sqlite' || !Database) {
|
|
246
|
+
const msg = 'better-sqlite3 not available - migration skipped, markdown remains source of truth';
|
|
247
|
+
return {
|
|
248
|
+
migrated: false,
|
|
249
|
+
tables: {},
|
|
250
|
+
dryRun,
|
|
251
|
+
skipped: true,
|
|
252
|
+
reason: msg,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Read STATE.md.
|
|
257
|
+
const statePath = path.join(projectRoot, '.design', 'STATE.md');
|
|
258
|
+
if (!fs.existsSync(statePath)) {
|
|
259
|
+
return {
|
|
260
|
+
migrated: false,
|
|
261
|
+
tables: {},
|
|
262
|
+
dryRun,
|
|
263
|
+
skipped: true,
|
|
264
|
+
reason: `.design/STATE.md not found at ${statePath}`,
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const rawState = fs.readFileSync(statePath, 'utf8');
|
|
269
|
+
|
|
270
|
+
// Parse via sdk/state/parser.ts (dynamic import - never require a .ts).
|
|
271
|
+
const parserMod = await importParser();
|
|
272
|
+
const { parse } = parserMod;
|
|
273
|
+
|
|
274
|
+
let parsed;
|
|
275
|
+
try {
|
|
276
|
+
parsed = parse(rawState);
|
|
277
|
+
} catch (err) {
|
|
278
|
+
return {
|
|
279
|
+
migrated: false,
|
|
280
|
+
tables: {},
|
|
281
|
+
dryRun,
|
|
282
|
+
skipped: true,
|
|
283
|
+
reason: `STATE.md parse error: ${err.message}`,
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const { state, raw_frontmatter, raw_bodies, block_gaps } = parsed;
|
|
288
|
+
const { frontmatter, position, decisions, must_haves, blockers, prototyping, quality_gate } = state;
|
|
289
|
+
|
|
290
|
+
const dbPath = sqlitePath(projectRoot);
|
|
291
|
+
|
|
292
|
+
// Boot integrity check if db already exists.
|
|
293
|
+
if (fs.existsSync(dbPath) && checkIntegrity) {
|
|
294
|
+
const integrityDb = openStateDb(dbPath, { readonly: true });
|
|
295
|
+
try {
|
|
296
|
+
const ok = checkIntegrity(integrityDb);
|
|
297
|
+
if (!ok) {
|
|
298
|
+
return {
|
|
299
|
+
migrated: false,
|
|
300
|
+
tables: {},
|
|
301
|
+
dryRun,
|
|
302
|
+
skipped: true,
|
|
303
|
+
reason:
|
|
304
|
+
'SQLite database failed integrity_check. Run /gdd:state recover to rebuild from markdown.',
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
} finally {
|
|
308
|
+
integrityDb.close();
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Open the db for writing (schema is created by Executor A's openStateDb).
|
|
313
|
+
const db = openStateDb(dbPath, { readonly: false });
|
|
314
|
+
|
|
315
|
+
const now = new Date().toISOString();
|
|
316
|
+
const renderSha = sha256hex(rawState);
|
|
317
|
+
const cycleId = frontmatter.cycle || '';
|
|
318
|
+
|
|
319
|
+
// Collect ops for dry-run diff output.
|
|
320
|
+
const ops = [];
|
|
321
|
+
|
|
322
|
+
// Accumulate row counts per table.
|
|
323
|
+
const counts = {
|
|
324
|
+
state_position: 0,
|
|
325
|
+
decisions: 0,
|
|
326
|
+
blockers: 0,
|
|
327
|
+
must_haves: 0,
|
|
328
|
+
_block_meta: 0,
|
|
329
|
+
_meta: 0,
|
|
330
|
+
recall_records: 0,
|
|
331
|
+
instincts: 0,
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
// Wrap everything in a transaction. For dry-run we ROLLBACK; for real we COMMIT.
|
|
335
|
+
const migrate = db.transaction(() => {
|
|
336
|
+
// --- state_position ---
|
|
337
|
+
const posBodyTrailer = state.body_trailer || '';
|
|
338
|
+
const posBodyPreamble = state.body_preamble || '';
|
|
339
|
+
const lineEnding = parsed.line_ending || '\n';
|
|
340
|
+
db.prepare(`
|
|
341
|
+
INSERT INTO state_position
|
|
342
|
+
(cycle_id, stage, wave, task_progress, status, branch, raw_frontmatter,
|
|
343
|
+
body_preamble, body_trailer, line_ending, last_render_sha256, updated_at)
|
|
344
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
345
|
+
ON CONFLICT(cycle_id) DO UPDATE SET
|
|
346
|
+
stage = excluded.stage,
|
|
347
|
+
wave = excluded.wave,
|
|
348
|
+
task_progress = excluded.task_progress,
|
|
349
|
+
status = excluded.status,
|
|
350
|
+
raw_frontmatter = excluded.raw_frontmatter,
|
|
351
|
+
body_preamble = excluded.body_preamble,
|
|
352
|
+
body_trailer = excluded.body_trailer,
|
|
353
|
+
line_ending = excluded.line_ending,
|
|
354
|
+
last_render_sha256 = excluded.last_render_sha256,
|
|
355
|
+
updated_at = excluded.updated_at
|
|
356
|
+
`).run(
|
|
357
|
+
cycleId,
|
|
358
|
+
position.stage,
|
|
359
|
+
position.wave,
|
|
360
|
+
position.task_progress,
|
|
361
|
+
position.status,
|
|
362
|
+
'',
|
|
363
|
+
raw_frontmatter,
|
|
364
|
+
posBodyPreamble,
|
|
365
|
+
posBodyTrailer,
|
|
366
|
+
lineEnding,
|
|
367
|
+
renderSha,
|
|
368
|
+
now,
|
|
369
|
+
);
|
|
370
|
+
counts.state_position++;
|
|
371
|
+
ops.push({
|
|
372
|
+
action: 'upsert',
|
|
373
|
+
table: 'state_position',
|
|
374
|
+
id: cycleId,
|
|
375
|
+
fields: { stage: position.stage, wave: position.wave, status: position.status },
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
// --- _meta ---
|
|
379
|
+
db.prepare(`
|
|
380
|
+
INSERT INTO _meta (key, value) VALUES (?, ?)
|
|
381
|
+
ON CONFLICT(key) DO UPDATE SET value = excluded.value
|
|
382
|
+
`).run('schema_version', '57.0');
|
|
383
|
+
db.prepare(`
|
|
384
|
+
INSERT INTO _meta (key, value) VALUES (?, ?)
|
|
385
|
+
ON CONFLICT(key) DO UPDATE SET value = excluded.value
|
|
386
|
+
`).run('last_render_sha256', renderSha);
|
|
387
|
+
counts._meta += 2;
|
|
388
|
+
|
|
389
|
+
// --- _block_meta (gap whitespace + raw_body per block for full round-trip fidelity) ---
|
|
390
|
+
// Store EVERY block that was present in the parsed result: gap (preceding whitespace),
|
|
391
|
+
// raw_body (verbatim block body for unstructured blocks), and ordinal (emit order).
|
|
392
|
+
// This enables renderStateMarkdown to round-trip ALL blocks byte-for-byte.
|
|
393
|
+
const BLOCK_ORDER_LOCAL = [
|
|
394
|
+
'position', 'decisions', 'must_haves', 'prototyping', 'quality_gate',
|
|
395
|
+
'connections', 'blockers', 'parallelism_decision', 'todos', 'timestamps',
|
|
396
|
+
];
|
|
397
|
+
for (let blockOrdinal = 0; blockOrdinal < BLOCK_ORDER_LOCAL.length; blockOrdinal++) {
|
|
398
|
+
const block = BLOCK_ORDER_LOCAL[blockOrdinal];
|
|
399
|
+
const gap = block_gaps[block];
|
|
400
|
+
const rawBody = raw_bodies[block];
|
|
401
|
+
// Only store if the block was present (gap !== '' means it was in the file, or
|
|
402
|
+
// raw_body is non-null meaning the block was parsed).
|
|
403
|
+
if (gap === undefined && rawBody === null) continue;
|
|
404
|
+
db.prepare(`
|
|
405
|
+
INSERT INTO _block_meta (cycle_id, block, gap, raw_body, ordinal)
|
|
406
|
+
VALUES (?, ?, ?, ?, ?)
|
|
407
|
+
ON CONFLICT(cycle_id, block) DO UPDATE SET
|
|
408
|
+
gap = excluded.gap,
|
|
409
|
+
raw_body = CASE WHEN excluded.raw_body IS NOT NULL THEN excluded.raw_body ELSE raw_body END,
|
|
410
|
+
ordinal = excluded.ordinal
|
|
411
|
+
`).run(cycleId, block, gap !== undefined ? gap : '', rawBody !== null ? rawBody : null, blockOrdinal);
|
|
412
|
+
counts._block_meta++;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// --- decisions ---
|
|
416
|
+
for (let i = 0; i < decisions.length; i++) {
|
|
417
|
+
const d = decisions[i];
|
|
418
|
+
// Reconstruct the raw_line from the parsed fields to preserve verbatim format.
|
|
419
|
+
const rawLine = `${d.id}: ${d.text} (${d.status})`;
|
|
420
|
+
db.prepare(`
|
|
421
|
+
INSERT INTO decisions
|
|
422
|
+
(id, cycle_id, phase_id, status, body_md, tags, ordinal, raw_line, created_at, last_referenced_at)
|
|
423
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
424
|
+
ON CONFLICT(cycle_id, id) DO UPDATE SET
|
|
425
|
+
body_md = excluded.body_md,
|
|
426
|
+
status = excluded.status,
|
|
427
|
+
raw_line = excluded.raw_line,
|
|
428
|
+
last_referenced_at = excluded.last_referenced_at
|
|
429
|
+
`).run(
|
|
430
|
+
d.id,
|
|
431
|
+
cycleId,
|
|
432
|
+
'',
|
|
433
|
+
d.status,
|
|
434
|
+
d.text,
|
|
435
|
+
null,
|
|
436
|
+
i,
|
|
437
|
+
rawLine,
|
|
438
|
+
now,
|
|
439
|
+
now,
|
|
440
|
+
);
|
|
441
|
+
counts.decisions++;
|
|
442
|
+
ops.push({ action: 'upsert', table: 'decisions', id: d.id, fields: { status: d.status, body_md: d.text, raw_line: rawLine } });
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// --- blockers ---
|
|
446
|
+
for (let i = 0; i < blockers.length; i++) {
|
|
447
|
+
const b = blockers[i];
|
|
448
|
+
const rawLine = `[${b.stage}] [${b.date}]: ${b.text}`;
|
|
449
|
+
db.prepare(`
|
|
450
|
+
INSERT INTO blockers
|
|
451
|
+
(cycle_id, stage, date, severity, body_md, ordinal, raw_line, resolved_at)
|
|
452
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
453
|
+
`).run(
|
|
454
|
+
cycleId,
|
|
455
|
+
b.stage,
|
|
456
|
+
b.date,
|
|
457
|
+
null,
|
|
458
|
+
b.text,
|
|
459
|
+
i,
|
|
460
|
+
rawLine,
|
|
461
|
+
null,
|
|
462
|
+
);
|
|
463
|
+
counts.blockers++;
|
|
464
|
+
ops.push({ action: 'insert', table: 'blockers', id: `${b.stage}:${b.date}`, fields: { stage: b.stage, date: b.date, body_md: b.text } });
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// --- must_haves ---
|
|
468
|
+
for (let i = 0; i < must_haves.length; i++) {
|
|
469
|
+
const m = must_haves[i];
|
|
470
|
+
const rawLine = `${m.id}: ${m.text} | status: ${m.status}`;
|
|
471
|
+
db.prepare(`
|
|
472
|
+
INSERT INTO must_haves (id, cycle_id, body_md, status, ordinal, raw_line)
|
|
473
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
474
|
+
ON CONFLICT(cycle_id, id) DO UPDATE SET
|
|
475
|
+
body_md = excluded.body_md,
|
|
476
|
+
status = excluded.status,
|
|
477
|
+
raw_line = excluded.raw_line
|
|
478
|
+
`).run(
|
|
479
|
+
m.id,
|
|
480
|
+
cycleId,
|
|
481
|
+
m.text,
|
|
482
|
+
m.status,
|
|
483
|
+
i,
|
|
484
|
+
rawLine,
|
|
485
|
+
);
|
|
486
|
+
counts.must_haves++;
|
|
487
|
+
ops.push({ action: 'upsert', table: 'must_haves', id: m.id, fields: { status: m.status, body_md: m.text } });
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// --- prototyping -> findings (best-effort; prototyping block is optional) ---
|
|
491
|
+
// Prototyping sketches/spikes are stored as plan entries in state for now;
|
|
492
|
+
// the main table for prototyping data is addressed by consumers directly.
|
|
493
|
+
// No dedicated prototyping table in the PINNED schema - store nothing here.
|
|
494
|
+
|
|
495
|
+
// --- recall_records (Phase 19.5 - best-effort) ---
|
|
496
|
+
const recalls = loadRecallRecords(projectRoot);
|
|
497
|
+
for (let i = 0; i < recalls.length; i++) {
|
|
498
|
+
const r = recalls[i];
|
|
499
|
+
const recallId = r.id || `recall-${i}`;
|
|
500
|
+
db.prepare(`
|
|
501
|
+
INSERT INTO recall_records (id, cycle_id, kind, body_md, tags, created_at)
|
|
502
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
503
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
504
|
+
body_md = excluded.body_md,
|
|
505
|
+
tags = excluded.tags
|
|
506
|
+
`).run(
|
|
507
|
+
recallId,
|
|
508
|
+
cycleId,
|
|
509
|
+
r.kind || 'unknown',
|
|
510
|
+
r.body_md || '',
|
|
511
|
+
typeof r.tags === 'string' ? r.tags : (r.tags ? JSON.stringify(r.tags) : null),
|
|
512
|
+
r.created_at || now,
|
|
513
|
+
);
|
|
514
|
+
counts.recall_records++;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// --- instincts (Phase 51 - best-effort) ---
|
|
518
|
+
const instincts = loadInstincts(projectRoot);
|
|
519
|
+
for (const inst of instincts) {
|
|
520
|
+
const instId = inst.id || `instinct-${Math.random().toString(36).slice(2)}`;
|
|
521
|
+
db.prepare(`
|
|
522
|
+
INSERT INTO instincts
|
|
523
|
+
(id, scope, domain, body_md, confidence, cycles_seen, project_ids, last_seen)
|
|
524
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
525
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
526
|
+
body_md = excluded.body_md,
|
|
527
|
+
confidence = excluded.confidence,
|
|
528
|
+
cycles_seen = excluded.cycles_seen,
|
|
529
|
+
project_ids = excluded.project_ids,
|
|
530
|
+
last_seen = excluded.last_seen
|
|
531
|
+
`).run(
|
|
532
|
+
instId,
|
|
533
|
+
inst.scope || 'project',
|
|
534
|
+
inst.domain || null,
|
|
535
|
+
inst.body || inst.body_md || '',
|
|
536
|
+
typeof inst.confidence === 'number' ? inst.confidence : null,
|
|
537
|
+
typeof inst.cycles_seen === 'number' ? inst.cycles_seen : 1,
|
|
538
|
+
Array.isArray(inst.project_ids) ? JSON.stringify(inst.project_ids) : null,
|
|
539
|
+
inst.last_seen || now,
|
|
540
|
+
);
|
|
541
|
+
counts.instincts++;
|
|
542
|
+
}
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
if (dryRun) {
|
|
546
|
+
// Dry-run: build the op list (which happens inside the transaction callback),
|
|
547
|
+
// then ROLLBACK so nothing is persisted.
|
|
548
|
+
// better-sqlite3 transactions commit automatically when the wrapper function
|
|
549
|
+
// returns. To dry-run, we wrap the whole thing in an outer savepoint that
|
|
550
|
+
// we always roll back. We call db.transaction() around a manual SAVEPOINT
|
|
551
|
+
// using the lower-level exec approach.
|
|
552
|
+
try {
|
|
553
|
+
db.exec('SAVEPOINT gdd_dryrun');
|
|
554
|
+
migrate(); // runs all UPSERTs but they land inside the savepoint
|
|
555
|
+
db.exec('ROLLBACK TO SAVEPOINT gdd_dryrun');
|
|
556
|
+
db.exec('RELEASE SAVEPOINT gdd_dryrun');
|
|
557
|
+
} catch {
|
|
558
|
+
// If there was a SQL error (e.g. table doesn't exist yet), still rollback.
|
|
559
|
+
try { db.exec('ROLLBACK TO SAVEPOINT gdd_dryrun'); } catch { /* ignore */ }
|
|
560
|
+
try { db.exec('RELEASE SAVEPOINT gdd_dryrun'); } catch { /* ignore */ }
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
const diff = buildDryRunDiff(ops);
|
|
564
|
+
process.stdout.write(diff + '\n');
|
|
565
|
+
|
|
566
|
+
db.close();
|
|
567
|
+
return {
|
|
568
|
+
migrated: false,
|
|
569
|
+
tables: counts,
|
|
570
|
+
dryRun: true,
|
|
571
|
+
skipped: false,
|
|
572
|
+
reason: 'dry-run: no changes persisted',
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// Real migration: run the transaction.
|
|
577
|
+
migrate();
|
|
578
|
+
db.close();
|
|
579
|
+
|
|
580
|
+
return {
|
|
581
|
+
migrated: true,
|
|
582
|
+
tables: counts,
|
|
583
|
+
dryRun: false,
|
|
584
|
+
skipped: false,
|
|
585
|
+
reason: 'ok',
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// ---------------------------------------------------------------------------
|
|
590
|
+
// CLI entry point.
|
|
591
|
+
// ---------------------------------------------------------------------------
|
|
592
|
+
|
|
593
|
+
function parseArgs(argv) {
|
|
594
|
+
const result = { force: false, dryRun: false, projectRoot: undefined };
|
|
595
|
+
for (const arg of argv) {
|
|
596
|
+
if (arg === '--migrate-state') result.force = true;
|
|
597
|
+
else if (arg === '--dry-run') result.dryRun = true;
|
|
598
|
+
else if (arg.startsWith('--project-root=')) result.projectRoot = arg.slice('--project-root='.length);
|
|
599
|
+
else if (arg === '--project-root' || arg === '-p') {
|
|
600
|
+
// next arg handled below via index
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
// Handle --project-root <value> (two-arg form).
|
|
604
|
+
for (let i = 0; i < argv.length - 1; i++) {
|
|
605
|
+
if (argv[i] === '--project-root' || argv[i] === '-p') {
|
|
606
|
+
result.projectRoot = argv[i + 1];
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
return result;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
async function main(argv) {
|
|
613
|
+
const args = parseArgs(argv);
|
|
614
|
+
|
|
615
|
+
if (!args.force) {
|
|
616
|
+
process.stdout.write(
|
|
617
|
+
'Migration is opt-in in v1.57.0. Re-run with --migrate-state to proceed.\n',
|
|
618
|
+
);
|
|
619
|
+
process.exitCode = 0;
|
|
620
|
+
return;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
let result;
|
|
624
|
+
try {
|
|
625
|
+
result = await migrateToSqlite({
|
|
626
|
+
projectRoot: args.projectRoot,
|
|
627
|
+
dryRun: args.dryRun,
|
|
628
|
+
force: args.force,
|
|
629
|
+
});
|
|
630
|
+
} catch (err) {
|
|
631
|
+
process.stderr.write(`migrate-to-sqlite error: ${err.message}\n`);
|
|
632
|
+
process.exitCode = 1;
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
if (result.skipped) {
|
|
637
|
+
process.stdout.write(`${result.reason}\n`);
|
|
638
|
+
process.exitCode = 0;
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
if (result.dryRun) {
|
|
643
|
+
// diff was already printed by migrateToSqlite.
|
|
644
|
+
process.stdout.write(
|
|
645
|
+
`\nDry-run complete. Tables that WOULD be written: ${JSON.stringify(result.tables)}\n`,
|
|
646
|
+
);
|
|
647
|
+
process.exitCode = 0;
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
process.stdout.write(
|
|
652
|
+
`Migration complete. Tables written: ${JSON.stringify(result.tables)}\n`,
|
|
653
|
+
);
|
|
654
|
+
process.exitCode = 0;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
if (require.main === module) {
|
|
658
|
+
main(process.argv.slice(2)).catch((err) => {
|
|
659
|
+
process.stderr.write(`migrate-to-sqlite fatal: ${err.message}\n`);
|
|
660
|
+
process.exitCode = 1;
|
|
661
|
+
});
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
module.exports = { migrateToSqlite };
|