@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,345 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/**
|
|
3
|
+
* scripts/lib/state/state-backend.cjs - Phase 57 SQLite State Backbone.
|
|
4
|
+
*
|
|
5
|
+
* Backend probe and database helpers. Mirrors the probe pattern from
|
|
6
|
+
* scripts/lib/instinct-store.cjs (Phase 51) and scripts/lib/design-search.cjs:
|
|
7
|
+
* 1. probeOptional('better-sqlite3') -> Database class or null
|
|
8
|
+
* 2. In-memory CREATE VIRTUAL TABLE _p USING fts5(t) probe -> _sqliteOk flag
|
|
9
|
+
* 3. BACKEND = 'sqlite' when both succeed, 'markdown' otherwise
|
|
10
|
+
* 4. GDD_STATE_BACKEND=markdown env override forces the markdown floor (tests use this)
|
|
11
|
+
*
|
|
12
|
+
* Exports: { Database, BACKEND, openStateDb, openQueryDb, checkIntegrity, sqlitePath, loadSchema }
|
|
13
|
+
*
|
|
14
|
+
* R4/R9/R10 compliance:
|
|
15
|
+
* - R4: better-sqlite3 only (no node:sqlite - no FTS5 support in official Node 22/24 builds)
|
|
16
|
+
* - R9: WAL journal + synchronous=NORMAL + busy_timeout=5000 + foreign_keys=ON
|
|
17
|
+
* - R10: openQueryDb uses engine-level readonly:true (engine rejects writes with SQLITE_READONLY)
|
|
18
|
+
*
|
|
19
|
+
* Package-root resolution: walk-up from __dirname to find package.json with
|
|
20
|
+
* name === '@hegemonart/get-design-done', then resolve sdk/state/schema.sql
|
|
21
|
+
* from that root. This is the Phase 53 lesson - never use __dirname-relative
|
|
22
|
+
* cross-tree jumps; esbuild rewrites __dirname so fixed relative paths break.
|
|
23
|
+
*
|
|
24
|
+
* NEVER throws on a missing better-sqlite3 module. Always degrades to markdown floor.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
const fs = require('node:fs');
|
|
28
|
+
const path = require('node:path');
|
|
29
|
+
|
|
30
|
+
const { probeOptional } = require('../probe-optional.cjs');
|
|
31
|
+
const { resolveRepoRoot } = require('../worktree-resolve.cjs');
|
|
32
|
+
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
// better-sqlite3 + FTS5 backend probe (evaluated once at module load).
|
|
35
|
+
// Mirrors instinct-store.cjs and design-search.cjs backend selection exactly.
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
|
|
38
|
+
/** The Database constructor from better-sqlite3, or null if unavailable. */
|
|
39
|
+
const Database = probeOptional('better-sqlite3');
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* True when better-sqlite3 is present AND its FTS5 extension is compiled in.
|
|
43
|
+
* The in-memory probe matches instinct-store.cjs lines 101-110 verbatim.
|
|
44
|
+
*/
|
|
45
|
+
let _sqliteOk = false;
|
|
46
|
+
if (Database) {
|
|
47
|
+
try {
|
|
48
|
+
const probe = new Database(':memory:');
|
|
49
|
+
probe.exec('CREATE VIRTUAL TABLE _p USING fts5(t)');
|
|
50
|
+
probe.close();
|
|
51
|
+
_sqliteOk = true;
|
|
52
|
+
} catch {
|
|
53
|
+
/* fts5 extension not compiled in - fall back to markdown floor */
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* GDD_STATE_BACKEND=markdown forces the markdown floor regardless of whether
|
|
59
|
+
* better-sqlite3 is present. Tests use this to exercise the markdown path
|
|
60
|
+
* even in environments where the native module is installed.
|
|
61
|
+
*/
|
|
62
|
+
const _envForceMarkdown = process.env.GDD_STATE_BACKEND === 'markdown';
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* 'sqlite' when better-sqlite3 + FTS5 is available AND not overridden by env.
|
|
66
|
+
* 'markdown' otherwise (the guaranteed fallback / CI surface).
|
|
67
|
+
*/
|
|
68
|
+
const BACKEND = (!_envForceMarkdown && _sqliteOk) ? 'sqlite' : 'markdown';
|
|
69
|
+
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
// Package-root walk-up (Phase 53 lesson: never use __dirname-relative jumps).
|
|
72
|
+
// Walk up from __dirname to find the GDD package root, then resolve schema.sql.
|
|
73
|
+
// Memoized per process.
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
|
|
76
|
+
/** @type {string|null} */
|
|
77
|
+
let _cachedPkgRoot = null;
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Find the GDD package root by walking up from startDir.
|
|
81
|
+
* Looks for package.json with name === '@hegemonart/get-design-done'.
|
|
82
|
+
* Falls back to the first directory with any package.json, then startDir.
|
|
83
|
+
*
|
|
84
|
+
* @param {string} startDir
|
|
85
|
+
* @returns {string}
|
|
86
|
+
*/
|
|
87
|
+
function _findPackageRoot(startDir) {
|
|
88
|
+
let dir = path.resolve(startDir);
|
|
89
|
+
let firstWithPkg = null;
|
|
90
|
+
for (let i = 0; i < 12; i++) {
|
|
91
|
+
const pkgPath = path.join(dir, 'package.json');
|
|
92
|
+
let pkg = null;
|
|
93
|
+
try {
|
|
94
|
+
pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
95
|
+
} catch {
|
|
96
|
+
pkg = null;
|
|
97
|
+
}
|
|
98
|
+
if (pkg) {
|
|
99
|
+
if (firstWithPkg === null) firstWithPkg = dir;
|
|
100
|
+
if (pkg.name === '@hegemonart/get-design-done') return dir;
|
|
101
|
+
}
|
|
102
|
+
const parent = path.dirname(dir);
|
|
103
|
+
if (parent === dir) break;
|
|
104
|
+
dir = parent;
|
|
105
|
+
}
|
|
106
|
+
return firstWithPkg || path.resolve(startDir);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Resolved GDD package root, memoized. Computed from __dirname so it is
|
|
111
|
+
* correct regardless of the caller's cwd (and survives esbuild bundling where
|
|
112
|
+
* __dirname is rewritten to the bundle output location).
|
|
113
|
+
*
|
|
114
|
+
* @returns {string}
|
|
115
|
+
*/
|
|
116
|
+
function _packageRoot() {
|
|
117
|
+
if (_cachedPkgRoot === null) _cachedPkgRoot = _findPackageRoot(__dirname);
|
|
118
|
+
return _cachedPkgRoot;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ---------------------------------------------------------------------------
|
|
122
|
+
// Schema loading
|
|
123
|
+
// ---------------------------------------------------------------------------
|
|
124
|
+
|
|
125
|
+
/** @type {string|null} Memoized schema SQL (base section only). */
|
|
126
|
+
let _baseSchemaSql = null;
|
|
127
|
+
/** @type {string|null} Memoized FTS5 section SQL. */
|
|
128
|
+
let _fts5SchemaSql = null;
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Read sdk/state/schema.sql from the package root and split into two sections:
|
|
132
|
+
* - base: everything before GDD_FTS5_SECTION_START
|
|
133
|
+
* - fts5: everything between GDD_FTS5_SECTION_START and GDD_FTS5_SECTION_END
|
|
134
|
+
*
|
|
135
|
+
* Memoized; file is read once per process.
|
|
136
|
+
*
|
|
137
|
+
* @returns {{ base: string, fts5: string }}
|
|
138
|
+
*/
|
|
139
|
+
function _readSchemaSql() {
|
|
140
|
+
if (_baseSchemaSql !== null) return { base: _baseSchemaSql, fts5: _fts5SchemaSql || '' };
|
|
141
|
+
|
|
142
|
+
const schemaPath = path.join(_packageRoot(), 'sdk', 'state', 'schema.sql');
|
|
143
|
+
const raw = fs.readFileSync(schemaPath, 'utf8');
|
|
144
|
+
|
|
145
|
+
const startMarker = '-- GDD_FTS5_SECTION_START';
|
|
146
|
+
const endMarker = '-- GDD_FTS5_SECTION_END';
|
|
147
|
+
const startIdx = raw.indexOf(startMarker);
|
|
148
|
+
const endIdx = raw.indexOf(endMarker);
|
|
149
|
+
|
|
150
|
+
if (startIdx !== -1 && endIdx !== -1 && endIdx > startIdx) {
|
|
151
|
+
_baseSchemaSql = raw.slice(0, startIdx).trim();
|
|
152
|
+
_fts5SchemaSql = raw.slice(startIdx + startMarker.length, endIdx).trim();
|
|
153
|
+
} else {
|
|
154
|
+
// No FTS5 section markers found - treat the whole file as base schema.
|
|
155
|
+
_baseSchemaSql = raw;
|
|
156
|
+
_fts5SchemaSql = '';
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return { base: _baseSchemaSql, fts5: _fts5SchemaSql };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Execute schema.sql against the given database, creating all base tables.
|
|
164
|
+
* FTS5 virtual tables are created only when _sqliteOk (and not env-overridden).
|
|
165
|
+
*
|
|
166
|
+
* Safe to call on both fresh and existing databases (CREATE TABLE IF NOT EXISTS).
|
|
167
|
+
*
|
|
168
|
+
* @param {import('better-sqlite3').Database} db
|
|
169
|
+
*/
|
|
170
|
+
function loadSchema(db) {
|
|
171
|
+
const { base, fts5 } = _readSchemaSql();
|
|
172
|
+
db.exec(base);
|
|
173
|
+
// FTS5 section only when the probe confirmed FTS5 is available.
|
|
174
|
+
// Do NOT use BACKEND here - loadSchema is called from openStateDb before BACKEND
|
|
175
|
+
// matters. Use _sqliteOk directly (probe result, not env override).
|
|
176
|
+
if (_sqliteOk && fts5) {
|
|
177
|
+
try {
|
|
178
|
+
db.exec(fts5);
|
|
179
|
+
} catch {
|
|
180
|
+
/* FTS5 section failed - base tables are still created; FTS5 is optional */
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// ---------------------------------------------------------------------------
|
|
186
|
+
// sqlitePath - resolve the state.sqlite path in the main repo root.
|
|
187
|
+
// Uses worktree-resolve.cjs (Phase 49) to find .design/ in the main checkout,
|
|
188
|
+
// not in a throwaway worktree. Matches the pattern used by instinct-store.cjs.
|
|
189
|
+
// ---------------------------------------------------------------------------
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Resolve the path to state.sqlite in the main repo root's .design/ directory.
|
|
193
|
+
* Worktree-safe: uses resolveRepoRoot so writes land in the main checkout even
|
|
194
|
+
* when called from inside a linked worktree.
|
|
195
|
+
*
|
|
196
|
+
* @param {string} [projectRoot] starting directory for repo-root resolution
|
|
197
|
+
* (defaults to process.cwd())
|
|
198
|
+
* @returns {string} absolute path to <repoRoot>/.design/state.sqlite
|
|
199
|
+
*/
|
|
200
|
+
function sqlitePath(projectRoot) {
|
|
201
|
+
const root = resolveRepoRoot(projectRoot || process.cwd());
|
|
202
|
+
return path.join(root, '.design', 'state.sqlite');
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// ---------------------------------------------------------------------------
|
|
206
|
+
// openStateDb - open a writer database with WAL pragmas (R9).
|
|
207
|
+
// ---------------------------------------------------------------------------
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Open state.sqlite at dbPath for read-write access.
|
|
211
|
+
*
|
|
212
|
+
* PRAGMAs applied (R9):
|
|
213
|
+
* journal_mode=WAL - concurrent readers while writer is active
|
|
214
|
+
* synchronous=NORMAL - safe with WAL; fsync on checkpoints not every write
|
|
215
|
+
* busy_timeout=5000 - wait up to 5s instead of failing immediately on lock
|
|
216
|
+
* foreign_keys=ON - enforce FK constraints
|
|
217
|
+
*
|
|
218
|
+
* WAL is skipped when readonly=true (cannot set WAL on a readonly connection;
|
|
219
|
+
* the writer is responsible for enabling WAL on first open).
|
|
220
|
+
*
|
|
221
|
+
* Calls loadSchema(db) to ensure all tables exist before returning.
|
|
222
|
+
*
|
|
223
|
+
* NEVER call this when BACKEND === 'markdown'; callers must guard.
|
|
224
|
+
*
|
|
225
|
+
* @param {string} dbPath absolute path to state.sqlite
|
|
226
|
+
* @param {{ readonly?: boolean }} [opts]
|
|
227
|
+
* @returns {import('better-sqlite3').Database}
|
|
228
|
+
*/
|
|
229
|
+
function openStateDb(dbPath, opts = {}) {
|
|
230
|
+
if (!Database) {
|
|
231
|
+
throw new Error('state-backend: better-sqlite3 not available (BACKEND=markdown)');
|
|
232
|
+
}
|
|
233
|
+
const readonly = opts.readonly === true;
|
|
234
|
+
const db = new Database(dbPath, readonly ? { readonly: true } : {});
|
|
235
|
+
if (!readonly) {
|
|
236
|
+
db.pragma('journal_mode = WAL');
|
|
237
|
+
db.pragma('synchronous = NORMAL');
|
|
238
|
+
db.pragma('busy_timeout = 5000');
|
|
239
|
+
db.pragma('foreign_keys = ON');
|
|
240
|
+
loadSchema(db);
|
|
241
|
+
}
|
|
242
|
+
return db;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// ---------------------------------------------------------------------------
|
|
246
|
+
// openQueryDb - engine-level read-only connection (R10).
|
|
247
|
+
// ---------------------------------------------------------------------------
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Open state.sqlite at dbPath as a read-only connection.
|
|
251
|
+
*
|
|
252
|
+
* Uses engine-level readonly:true (better-sqlite3 passes SQLITE_OPEN_READONLY
|
|
253
|
+
* to sqlite3_open_v2). The engine rejects all write operations with
|
|
254
|
+
* SQLITE_READONLY - no schema execution is performed.
|
|
255
|
+
*
|
|
256
|
+
* This is the correct path for /gdd:state query (R10).
|
|
257
|
+
*
|
|
258
|
+
* @param {string} dbPath absolute path to state.sqlite
|
|
259
|
+
* @returns {import('better-sqlite3').Database}
|
|
260
|
+
*/
|
|
261
|
+
function openQueryDb(dbPath) {
|
|
262
|
+
if (!Database) {
|
|
263
|
+
throw new Error('state-backend: better-sqlite3 not available (BACKEND=markdown)');
|
|
264
|
+
}
|
|
265
|
+
return new Database(dbPath, { readonly: true });
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// ---------------------------------------------------------------------------
|
|
269
|
+
// checkIntegrity - PRAGMA integrity_check (R11 boot check).
|
|
270
|
+
// ---------------------------------------------------------------------------
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Run PRAGMA integrity_check on the database.
|
|
274
|
+
* Returns true only when the result is a single row containing 'ok'.
|
|
275
|
+
* Any corruption, error, or unexpected result returns false.
|
|
276
|
+
*
|
|
277
|
+
* Used by the migration boot check (R11): degrade to markdown on failure.
|
|
278
|
+
*
|
|
279
|
+
* @param {import('better-sqlite3').Database} db
|
|
280
|
+
* @returns {boolean}
|
|
281
|
+
*/
|
|
282
|
+
function checkIntegrity(db) {
|
|
283
|
+
try {
|
|
284
|
+
const rows = db.pragma('integrity_check');
|
|
285
|
+
// better-sqlite3 pragma() returns an array of row objects.
|
|
286
|
+
// integrity_check returns [{ integrity_check: 'ok' }] on success.
|
|
287
|
+
if (!Array.isArray(rows) || rows.length !== 1) return false;
|
|
288
|
+
const val = rows[0];
|
|
289
|
+
// The column name is 'integrity_check'.
|
|
290
|
+
if (typeof val === 'object' && val !== null) {
|
|
291
|
+
const v = val['integrity_check'];
|
|
292
|
+
return v === 'ok';
|
|
293
|
+
}
|
|
294
|
+
return false;
|
|
295
|
+
} catch {
|
|
296
|
+
return false;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// ---------------------------------------------------------------------------
|
|
301
|
+
// Exports
|
|
302
|
+
// ---------------------------------------------------------------------------
|
|
303
|
+
|
|
304
|
+
module.exports = {
|
|
305
|
+
/**
|
|
306
|
+
* The Database constructor from better-sqlite3, or null when unavailable.
|
|
307
|
+
* Guards on this before calling any SQLite operation.
|
|
308
|
+
*/
|
|
309
|
+
Database,
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* 'sqlite' when better-sqlite3+FTS5 is available and GDD_STATE_BACKEND!=markdown.
|
|
313
|
+
* 'markdown' otherwise (the CI surface and guaranteed fallback).
|
|
314
|
+
*/
|
|
315
|
+
BACKEND,
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Open a read-write database connection with WAL pragmas + schema applied.
|
|
319
|
+
* Throws when Database is null (callers should guard on BACKEND).
|
|
320
|
+
*/
|
|
321
|
+
openStateDb,
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Open a read-only database connection (engine-level readonly).
|
|
325
|
+
* Throws when Database is null.
|
|
326
|
+
*/
|
|
327
|
+
openQueryDb,
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Check database integrity via PRAGMA integrity_check.
|
|
331
|
+
* Returns true only for a clean 'ok' result.
|
|
332
|
+
*/
|
|
333
|
+
checkIntegrity,
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Resolve <repoRoot>/.design/state.sqlite (worktree-safe).
|
|
337
|
+
*/
|
|
338
|
+
sqlitePath,
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Execute schema.sql DDL against db (base tables always; FTS5 when _sqliteOk).
|
|
342
|
+
* Safe to call on existing databases (CREATE TABLE IF NOT EXISTS).
|
|
343
|
+
*/
|
|
344
|
+
loadSchema,
|
|
345
|
+
};
|