valent-pipeline 0.2.16 → 0.2.18
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/package.json +1 -1
- package/pipeline/docs/lean-spawn-human-tasks.md +1 -1
- package/pipeline/prompts/embed.md +1 -1
- package/pipeline/prompts/lead.md +1 -0
- package/pipeline/prompts/readiness.md +4 -1
- package/pipeline/scripts/db-bootstrap.ts +153 -0
- package/pipeline/scripts/embed-sqlite.ts +4 -4
- package/pipeline/scripts/query-kb.ts +3 -11
- package/pipeline/steps/orchestration/sprint-groom.md +2 -1
- package/skills/valent-configure/SKILL.md +1 -1
- package/skills/valent-help/SKILL.md +1 -1
- package/skills/valent-setup-backlog/SKILL.md +2 -3
- package/src/commands/db-index.js +8 -23
- package/src/commands/db-init.js +2 -107
- package/src/commands/db-query.js +8 -24
- package/src/commands/db-rebuild.js +4 -42
- package/src/commands/init.js +17 -3
- package/src/commands/upgrade.js +44 -0
- package/src/lib/db.js +200 -0
package/package.json
CHANGED
|
@@ -135,7 +135,7 @@ Then test other commands:
|
|
|
135
135
|
```bash
|
|
136
136
|
valent-pipeline config validate # should exit 0
|
|
137
137
|
valent-pipeline upgrade --dry-run # should show no changes (just installed)
|
|
138
|
-
valent-pipeline db
|
|
138
|
+
valent-pipeline db rebuild # indexes story artifacts (auto-creates DB if missing)
|
|
139
139
|
valent-pipeline db rebuild # should complete (no stories to index yet)
|
|
140
140
|
```
|
|
141
141
|
|
|
@@ -62,6 +62,6 @@ Send inbox message to lead: `[EMBED-COMPLETE] Indexed {count} items.` (or `[EMBE
|
|
|
62
62
|
## Error Handling
|
|
63
63
|
|
|
64
64
|
- If embed-instructions.md is missing: send `[BLOCKER]` to lead, terminate.
|
|
65
|
-
- If SQLite database is missing:
|
|
65
|
+
- If SQLite database is missing: it will be auto-created on first write. If the DB file cannot be created, skip DB instructions and index curated files only.
|
|
66
66
|
- If ChromaDB connection fails (legacy mode): skip ChromaDB instructions, index curated files only, report partial completion.
|
|
67
67
|
- If a curated file write fails: log the failure, continue to next instruction, report in completion message.
|
package/pipeline/prompts/lead.md
CHANGED
|
@@ -53,6 +53,7 @@ You receive many message types. Process each by type:
|
|
|
53
53
|
Your outbound messages follow the same terse format:
|
|
54
54
|
- `[SPAWN] Spawning {agent} for {story_id}. Role: {role}. Shared context: {story_output_dir}.`
|
|
55
55
|
- `[CHECK-IN] {agent}: task {task} has been in_progress for {minutes}min. Status?`
|
|
56
|
+
- `[REVIEW-READY] Story {story_id}` — sent to READINESS immediately when a story reaches `readiness-review` during sprint grooming. Do not batch — send as soon as QA-A hands off each story.
|
|
56
57
|
- `[TEARDOWN] Tearing down all teammates for {story_id}.`
|
|
57
58
|
- `[ESCALATION] See escalation block below.`
|
|
58
59
|
|
|
@@ -12,7 +12,10 @@ Read `.valent-pipeline/steps/common/agent-protocol.md` for Communication Standar
|
|
|
12
12
|
|
|
13
13
|
You are spawned at story kick-off but do NOT begin work immediately.
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
**Standalone mode:** Wait for `[HANDOFF]` from QA-A, then review that story.
|
|
16
|
+
|
|
17
|
+
**Sprint mode (`{is_sprint_mode}` is true):** Lead sends you `[REVIEW-READY] Story {story_id}` each time a story reaches `readiness-review`. Begin reviewing that story immediately. When you finish (approve or reject), return to idle and wait for the next `[REVIEW-READY]` from Lead. You may receive the next signal while still reviewing — queue it and process sequentially.
|
|
18
|
+
|
|
16
19
|
- **On approval:** Send `[READINESS-APPROVAL]` to BEND (and FEND if active). Send `[DONE]` to Lead. Mark task completed.
|
|
17
20
|
- **On rejection:** Send `[READINESS-REJECTION]` to the **responsible agent** (REQS, UXA, or QA-A — see Rejection Routing below) AND to Lead. Do NOT send `[DONE]`. Do NOT mark task completed. Task stays `in_progress` — keeps BEND/FEND blocked. After agent revises and downstream re-completes, re-review.
|
|
18
21
|
- **Escalate to:** Lead — for `[BLOCKER]`, `[ESCALATION]`, or any issue you cannot resolve peer-to-peer.
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* db-bootstrap.ts — SQLite schema utilities for pipeline TS scripts.
|
|
3
|
+
*
|
|
4
|
+
* This file is the TypeScript-side copy of the schema defined in
|
|
5
|
+
* src/lib/db.js. Keep both files in sync when modifying the schema
|
|
6
|
+
* (see docs/design/refactor-checklist.md).
|
|
7
|
+
*
|
|
8
|
+
* Imported by embed-sqlite.ts and query-kb.ts to self-bootstrap the
|
|
9
|
+
* database — tables are created automatically if they don't exist.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import Database from 'better-sqlite3';
|
|
13
|
+
import { existsSync, readFileSync } from 'fs';
|
|
14
|
+
import { join } from 'path';
|
|
15
|
+
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
// Schema DDL — must match src/lib/db.js SCHEMA_DDL exactly.
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
|
|
20
|
+
export const SCHEMA_DDL = `
|
|
21
|
+
CREATE TABLE IF NOT EXISTS artifacts (
|
|
22
|
+
id TEXT PRIMARY KEY,
|
|
23
|
+
story_id TEXT NOT NULL,
|
|
24
|
+
agent TEXT NOT NULL,
|
|
25
|
+
artifact_type TEXT NOT NULL,
|
|
26
|
+
content TEXT NOT NULL,
|
|
27
|
+
metadata TEXT,
|
|
28
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
29
|
+
UNIQUE(story_id, artifact_type)
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
CREATE TABLE IF NOT EXISTS correction_directives (
|
|
33
|
+
id TEXT PRIMARY KEY,
|
|
34
|
+
target_agent TEXT NOT NULL,
|
|
35
|
+
directive TEXT NOT NULL,
|
|
36
|
+
reason TEXT,
|
|
37
|
+
status TEXT DEFAULT 'active',
|
|
38
|
+
created_batch INTEGER,
|
|
39
|
+
metadata TEXT
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
CREATE TABLE IF NOT EXISTS calibration (
|
|
43
|
+
story_id TEXT PRIMARY KEY,
|
|
44
|
+
story_points INTEGER,
|
|
45
|
+
ac_count INTEGER,
|
|
46
|
+
surface TEXT,
|
|
47
|
+
estimated_points INTEGER,
|
|
48
|
+
actual_mins REAL,
|
|
49
|
+
rework_cycles INTEGER DEFAULT 0,
|
|
50
|
+
sprint_id TEXT,
|
|
51
|
+
created_at TEXT DEFAULT (datetime('now'))
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
CREATE TABLE IF NOT EXISTS artifacts_working (
|
|
55
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
56
|
+
story_id TEXT NOT NULL,
|
|
57
|
+
agent TEXT NOT NULL,
|
|
58
|
+
artifact_type TEXT NOT NULL,
|
|
59
|
+
content TEXT,
|
|
60
|
+
metadata TEXT,
|
|
61
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
62
|
+
UNIQUE(story_id, artifact_type)
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
CREATE INDEX IF NOT EXISTS idx_artifacts_story ON artifacts(story_id);
|
|
66
|
+
CREATE INDEX IF NOT EXISTS idx_artifacts_type ON artifacts(artifact_type);
|
|
67
|
+
CREATE INDEX IF NOT EXISTS idx_artifacts_agent ON artifacts(agent);
|
|
68
|
+
CREATE INDEX IF NOT EXISTS idx_cd_target ON correction_directives(target_agent);
|
|
69
|
+
CREATE INDEX IF NOT EXISTS idx_cd_status ON correction_directives(status);
|
|
70
|
+
CREATE INDEX IF NOT EXISTS idx_calibration_sprint ON calibration(sprint_id);
|
|
71
|
+
`;
|
|
72
|
+
|
|
73
|
+
export const FTS_DDL = `
|
|
74
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS artifacts_fts USING fts5(
|
|
75
|
+
content,
|
|
76
|
+
story_id UNINDEXED,
|
|
77
|
+
agent UNINDEXED,
|
|
78
|
+
artifact_type UNINDEXED,
|
|
79
|
+
content=artifacts,
|
|
80
|
+
content_rowid=rowid
|
|
81
|
+
);
|
|
82
|
+
`;
|
|
83
|
+
|
|
84
|
+
export const TRIGGERS_DDL = `
|
|
85
|
+
CREATE TRIGGER IF NOT EXISTS artifacts_ai AFTER INSERT ON artifacts BEGIN
|
|
86
|
+
INSERT INTO artifacts_fts(rowid, content, story_id, agent, artifact_type)
|
|
87
|
+
VALUES (new.rowid, new.content, new.story_id, new.agent, new.artifact_type);
|
|
88
|
+
END;
|
|
89
|
+
|
|
90
|
+
CREATE TRIGGER IF NOT EXISTS artifacts_ad AFTER DELETE ON artifacts BEGIN
|
|
91
|
+
INSERT INTO artifacts_fts(artifacts_fts, rowid, content, story_id, agent, artifact_type)
|
|
92
|
+
VALUES ('delete', old.rowid, old.content, old.story_id, old.agent, old.artifact_type);
|
|
93
|
+
END;
|
|
94
|
+
|
|
95
|
+
CREATE TRIGGER IF NOT EXISTS artifacts_au AFTER UPDATE ON artifacts BEGIN
|
|
96
|
+
INSERT INTO artifacts_fts(artifacts_fts, rowid, content, story_id, agent, artifact_type)
|
|
97
|
+
VALUES ('delete', old.rowid, old.content, old.story_id, old.agent, old.artifact_type);
|
|
98
|
+
INSERT INTO artifacts_fts(rowid, content, story_id, agent, artifact_type)
|
|
99
|
+
VALUES (new.rowid, new.content, new.story_id, new.agent, new.artifact_type);
|
|
100
|
+
END;
|
|
101
|
+
`;
|
|
102
|
+
|
|
103
|
+
// ---------------------------------------------------------------------------
|
|
104
|
+
// Schema management
|
|
105
|
+
// ---------------------------------------------------------------------------
|
|
106
|
+
|
|
107
|
+
export function ensureSchema(db: InstanceType<typeof Database>): void {
|
|
108
|
+
db.exec(SCHEMA_DDL);
|
|
109
|
+
db.exec(FTS_DDL);
|
|
110
|
+
db.exec(TRIGGERS_DDL);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ---------------------------------------------------------------------------
|
|
114
|
+
// DB path resolution
|
|
115
|
+
// ---------------------------------------------------------------------------
|
|
116
|
+
|
|
117
|
+
export function resolveDbPath(flagDbPath?: string): string {
|
|
118
|
+
if (flagDbPath) return flagDbPath;
|
|
119
|
+
const configPath = join(process.cwd(), '.valent-pipeline', 'pipeline-config.yaml');
|
|
120
|
+
if (existsSync(configPath)) {
|
|
121
|
+
const content = readFileSync(configPath, 'utf-8');
|
|
122
|
+
const match = content.match(/sqlite_db_path:\s*"?([^"\n]+)"?/);
|
|
123
|
+
if (match) return join(process.cwd(), match[1].trim());
|
|
124
|
+
}
|
|
125
|
+
return join(process.cwd(), '.valent-pipeline', 'pipeline.db');
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ---------------------------------------------------------------------------
|
|
129
|
+
// High-level open helpers
|
|
130
|
+
// ---------------------------------------------------------------------------
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Open a read-write database connection with auto-schema creation.
|
|
134
|
+
* Creates the DB file and all tables if they don't exist.
|
|
135
|
+
*/
|
|
136
|
+
export function openDb(dbPath: string): InstanceType<typeof Database> {
|
|
137
|
+
const db = new Database(dbPath);
|
|
138
|
+
ensureSchema(db);
|
|
139
|
+
return db;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Open a read-only database connection. Does NOT run schema DDL.
|
|
144
|
+
* Errors with a helpful message if the DB file does not exist.
|
|
145
|
+
*/
|
|
146
|
+
export function openReadonlyDb(dbPath: string): InstanceType<typeof Database> {
|
|
147
|
+
if (!existsSync(dbPath)) {
|
|
148
|
+
console.error(`Database not found: ${dbPath}`);
|
|
149
|
+
console.error('Run "valent-pipeline init" or "valent-pipeline db init" to create it.');
|
|
150
|
+
process.exit(1);
|
|
151
|
+
}
|
|
152
|
+
return new Database(dbPath, { readonly: true });
|
|
153
|
+
}
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
|
|
19
19
|
import { readFileSync, existsSync, appendFileSync, mkdirSync, readdirSync, statSync } from 'fs';
|
|
20
20
|
import { join, dirname, basename } from 'path';
|
|
21
|
-
import
|
|
21
|
+
import { openDb } from './db-bootstrap.js';
|
|
22
22
|
|
|
23
23
|
// Parse CLI args
|
|
24
24
|
const args = process.argv.slice(2);
|
|
@@ -99,7 +99,7 @@ async function indexSingleHandoff(
|
|
|
99
99
|
return;
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
-
const db =
|
|
102
|
+
const db = openDb(dbPath);
|
|
103
103
|
const id = `${storyId}:${artifactType}`;
|
|
104
104
|
|
|
105
105
|
db.prepare(`
|
|
@@ -133,7 +133,7 @@ async function rebuildAll(dbPath: string, storiesDir: string) {
|
|
|
133
133
|
'story-report.md': { type: 'story-report', agent: 'JUDGE' },
|
|
134
134
|
};
|
|
135
135
|
|
|
136
|
-
const db =
|
|
136
|
+
const db = openDb(dbPath);
|
|
137
137
|
const insert = db.prepare(`
|
|
138
138
|
INSERT OR REPLACE INTO artifacts (id, story_id, agent, artifact_type, content, metadata)
|
|
139
139
|
VALUES (?, ?, ?, ?, ?, ?)
|
|
@@ -181,7 +181,7 @@ async function processEmbedInstructions(instructionsPath: string, dbPath: string
|
|
|
181
181
|
|
|
182
182
|
console.log(`Parsed ${items.length} embed instructions`);
|
|
183
183
|
|
|
184
|
-
const db = dryRun ? null :
|
|
184
|
+
const db = dryRun ? null : openDb(dbPath);
|
|
185
185
|
let indexed = 0;
|
|
186
186
|
let curatedCount = 0;
|
|
187
187
|
|
|
@@ -25,8 +25,7 @@
|
|
|
25
25
|
* # DB path defaults to .valent-pipeline/pipeline.db, override with --db-path
|
|
26
26
|
*/
|
|
27
27
|
|
|
28
|
-
import
|
|
29
|
-
import { existsSync } from 'fs';
|
|
28
|
+
import { resolveDbPath, openReadonlyDb } from './db-bootstrap.js';
|
|
30
29
|
|
|
31
30
|
const args = process.argv.slice(2);
|
|
32
31
|
const flags: Record<string, string> = {};
|
|
@@ -48,15 +47,8 @@ for (let i = 0; i < args.length; i++) {
|
|
|
48
47
|
}
|
|
49
48
|
}
|
|
50
49
|
|
|
51
|
-
const dbPath = flags['db-path']
|
|
52
|
-
|
|
53
|
-
if (!existsSync(dbPath)) {
|
|
54
|
-
console.error(`Database not found: ${dbPath}`);
|
|
55
|
-
console.error('Run "valent-pipeline db init" to create it.');
|
|
56
|
-
process.exit(1);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
const db = new Database(dbPath, { readonly: true });
|
|
50
|
+
const dbPath = resolveDbPath(flags['db-path']);
|
|
51
|
+
const db = openReadonlyDb(dbPath);
|
|
60
52
|
const mode = modes[0];
|
|
61
53
|
|
|
62
54
|
if (!mode) {
|
|
@@ -43,7 +43,8 @@ Process stories using assembly-line parallelism. Each agent moves to the next av
|
|
|
43
43
|
--story-id {story_id} \
|
|
44
44
|
--sprint-id {current_sprint_id}
|
|
45
45
|
```
|
|
46
|
-
9. READINESS
|
|
46
|
+
9. Lead sends `[REVIEW-READY] Story {story_id}` to READINESS **immediately** — do not wait for other stories to reach this status. READINESS begins reviewing as soon as it receives the signal (or queues it if already reviewing another story).
|
|
47
|
+
10. READINESS reviews specs + cross-story checks
|
|
47
48
|
|
|
48
49
|
**Status-based self-selection with type filtering:**
|
|
49
50
|
|
|
@@ -104,7 +104,7 @@ Ask the user to pick one:
|
|
|
104
104
|
| `local-docker` | Local ChromaDB via Docker (legacy). Pipeline provides a docker-compose file. |
|
|
105
105
|
| `connect-to-existing` | Remote/hosted ChromaDB instance for shared team knowledge (legacy). |
|
|
106
106
|
|
|
107
|
-
**If `sqlite`:** Set `sqlite_db_path` to `./.valent-pipeline/pipeline.db` (default).
|
|
107
|
+
**If `sqlite`:** Set `sqlite_db_path` to `./.valent-pipeline/pipeline.db` (default). The database is auto-created during `valent-pipeline init` and on first use — no separate `db init` step is needed.
|
|
108
108
|
|
|
109
109
|
Also show the `knowledge_base_path` setting:
|
|
110
110
|
- Default: `"./knowledge"`
|
|
@@ -71,7 +71,7 @@ Read these as needed to answer questions:
|
|
|
71
71
|
→ `npx valent-pipeline upgrade` — replaces infrastructure files, never touches your config or knowledge files.
|
|
72
72
|
|
|
73
73
|
**"How do I initialize the SQLite database?"**
|
|
74
|
-
→
|
|
74
|
+
→ The database is auto-created during `init` and on first use. Run `npx valent-pipeline db rebuild` to re-index from existing story artifacts. Run `npx valent-pipeline db init` to manually reset it.
|
|
75
75
|
|
|
76
76
|
## Answer Format
|
|
77
77
|
|
|
@@ -196,9 +196,8 @@ If the repo is empty or brand new (no source code yet), skip the subagents and c
|
|
|
196
196
|
### Step 7b: Initialize and Populate Knowledge Database
|
|
197
197
|
|
|
198
198
|
After writing the curated knowledge files:
|
|
199
|
-
1. Run `
|
|
200
|
-
2.
|
|
201
|
-
3. The database is now ready for the Knowledge Agent to query during story execution
|
|
199
|
+
1. Run `npx valent-pipeline db rebuild` to index any existing story artifacts (the database is auto-created if it doesn't exist)
|
|
200
|
+
2. The database is now ready for the Knowledge Agent to query during story execution
|
|
202
201
|
|
|
203
202
|
## Step 8: Report
|
|
204
203
|
|
package/src/commands/db-index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { join, dirname } from 'path';
|
|
2
2
|
import { readFileSync, existsSync, appendFileSync, mkdirSync, readdirSync, statSync } from 'fs';
|
|
3
|
+
import { resolveDbPath, openDb } from '../lib/db.js';
|
|
3
4
|
|
|
4
5
|
const ARTIFACT_MAP = {
|
|
5
6
|
'reqs-brief.md': { type: 'reqs-brief', agent: 'REQS' },
|
|
@@ -19,22 +20,6 @@ const ARTIFACT_MAP = {
|
|
|
19
20
|
'visual-validation-checklist.md': { type: 'visual-validation-checklist', agent: 'QA-A' },
|
|
20
21
|
};
|
|
21
22
|
|
|
22
|
-
function getDb(dbPath) {
|
|
23
|
-
const Database = require(join(process.cwd(), '.valent-pipeline', 'node_modules', 'better-sqlite3'));
|
|
24
|
-
return new Database(dbPath);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function resolveDbPath(options) {
|
|
28
|
-
if (options.dbPath) return options.dbPath;
|
|
29
|
-
const configPath = join(process.cwd(), '.valent-pipeline', 'pipeline-config.yaml');
|
|
30
|
-
if (existsSync(configPath)) {
|
|
31
|
-
const content = readFileSync(configPath, 'utf-8');
|
|
32
|
-
const match = content.match(/sqlite_db_path:\s*"?([^"\n]+)"?/);
|
|
33
|
-
if (match) return join(process.cwd(), match[1].trim());
|
|
34
|
-
}
|
|
35
|
-
return join(process.cwd(), '.valent-pipeline', 'pipeline.db');
|
|
36
|
-
}
|
|
37
|
-
|
|
38
23
|
export async function dbIndexHandoff(options) {
|
|
39
24
|
const dbPath = resolveDbPath(options);
|
|
40
25
|
const { file, storyId, agent, artifactType } = options;
|
|
@@ -50,7 +35,7 @@ export async function dbIndexHandoff(options) {
|
|
|
50
35
|
return;
|
|
51
36
|
}
|
|
52
37
|
|
|
53
|
-
const db =
|
|
38
|
+
const db = await openDb({ dbPath });
|
|
54
39
|
const id = `${storyId}:${artifactType}`;
|
|
55
40
|
db.prepare(`
|
|
56
41
|
INSERT OR REPLACE INTO artifacts (id, story_id, agent, artifact_type, content, metadata)
|
|
@@ -70,7 +55,7 @@ export async function dbIndexWorking(options) {
|
|
|
70
55
|
}
|
|
71
56
|
|
|
72
57
|
const content = readFileSync(file, 'utf-8');
|
|
73
|
-
const db =
|
|
58
|
+
const db = await openDb({ dbPath });
|
|
74
59
|
db.prepare(`
|
|
75
60
|
INSERT OR REPLACE INTO artifacts_working (story_id, agent, artifact_type, content, metadata)
|
|
76
61
|
VALUES (?, ?, ?, ?, ?)
|
|
@@ -83,7 +68,7 @@ export async function dbFlushWorking(options) {
|
|
|
83
68
|
const dbPath = resolveDbPath(options);
|
|
84
69
|
const { sprintId } = options;
|
|
85
70
|
|
|
86
|
-
const db =
|
|
71
|
+
const db = await openDb({ dbPath });
|
|
87
72
|
const rows = db.prepare('SELECT * FROM artifacts_working').all();
|
|
88
73
|
|
|
89
74
|
const insert = db.prepare(`
|
|
@@ -108,7 +93,7 @@ export async function dbQueryWorking(options) {
|
|
|
108
93
|
const dbPath = resolveDbPath(options);
|
|
109
94
|
const { sprintId, excludeStory } = options;
|
|
110
95
|
|
|
111
|
-
const db =
|
|
96
|
+
const db = await openDb({ dbPath });
|
|
112
97
|
let rows;
|
|
113
98
|
if (excludeStory) {
|
|
114
99
|
rows = db.prepare(
|
|
@@ -133,7 +118,7 @@ export async function dbRecordCalibration(options) {
|
|
|
133
118
|
const dbPath = resolveDbPath(options);
|
|
134
119
|
const { storyId, storyPoints, acCount, surface, estimatedPoints, actualMins, reworkCycles, sprintId } = options;
|
|
135
120
|
|
|
136
|
-
const db =
|
|
121
|
+
const db = await openDb({ dbPath });
|
|
137
122
|
db.prepare(`
|
|
138
123
|
INSERT OR REPLACE INTO calibration (story_id, story_points, ac_count, surface, estimated_points, actual_mins, rework_cycles, sprint_id)
|
|
139
124
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
@@ -144,7 +129,7 @@ export async function dbRecordCalibration(options) {
|
|
|
144
129
|
|
|
145
130
|
export async function dbQueryVelocity(options) {
|
|
146
131
|
const dbPath = resolveDbPath(options);
|
|
147
|
-
const db =
|
|
132
|
+
const db = await openDb({ dbPath });
|
|
148
133
|
|
|
149
134
|
const rows = db.prepare(`
|
|
150
135
|
SELECT sprint_id, SUM(story_points) as points_shipped
|
|
@@ -182,7 +167,7 @@ export async function dbEmbed(options) {
|
|
|
182
167
|
const items = parseEmbedInstructions(content);
|
|
183
168
|
console.log(`Parsed ${items.length} embed instructions`);
|
|
184
169
|
|
|
185
|
-
const db =
|
|
170
|
+
const db = await openDb({ dbPath });
|
|
186
171
|
let indexed = 0;
|
|
187
172
|
let curatedCount = 0;
|
|
188
173
|
|
package/src/commands/db-init.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { openDb, resolveDbPath } from '../lib/db.js';
|
|
1
2
|
import { join } from 'path';
|
|
2
3
|
import { existsSync, readFileSync } from 'fs';
|
|
3
4
|
|
|
@@ -16,113 +17,7 @@ export async function dbInit() {
|
|
|
16
17
|
const dbPath = dbPathMatch ? dbPathMatch[1].trim() : './.valent-pipeline/pipeline.db';
|
|
17
18
|
const fullDbPath = join(projectRoot, dbPath);
|
|
18
19
|
|
|
19
|
-
|
|
20
|
-
let sqliteVec;
|
|
21
|
-
try {
|
|
22
|
-
Database = (await import('better-sqlite3')).default;
|
|
23
|
-
sqliteVec = await import('sqlite-vec');
|
|
24
|
-
} catch (err) {
|
|
25
|
-
console.error('Error: better-sqlite3 or sqlite-vec not installed.');
|
|
26
|
-
console.error('Run: npm install better-sqlite3 sqlite-vec');
|
|
27
|
-
process.exit(1);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
const db = new Database(fullDbPath);
|
|
31
|
-
|
|
32
|
-
// Load sqlite-vec extension
|
|
33
|
-
try {
|
|
34
|
-
sqliteVec.load(db);
|
|
35
|
-
console.log('Loaded sqlite-vec extension');
|
|
36
|
-
} catch (err) {
|
|
37
|
-
console.warn(`Warning: Could not load sqlite-vec: ${err.message}`);
|
|
38
|
-
console.warn('Vector search will not be available. FTS5 will still work.');
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// Create tables
|
|
42
|
-
db.exec(`
|
|
43
|
-
CREATE TABLE IF NOT EXISTS artifacts (
|
|
44
|
-
id TEXT PRIMARY KEY,
|
|
45
|
-
story_id TEXT NOT NULL,
|
|
46
|
-
agent TEXT NOT NULL,
|
|
47
|
-
artifact_type TEXT NOT NULL,
|
|
48
|
-
content TEXT NOT NULL,
|
|
49
|
-
metadata TEXT,
|
|
50
|
-
created_at TEXT DEFAULT (datetime('now')),
|
|
51
|
-
UNIQUE(story_id, artifact_type)
|
|
52
|
-
);
|
|
53
|
-
|
|
54
|
-
CREATE TABLE IF NOT EXISTS correction_directives (
|
|
55
|
-
id TEXT PRIMARY KEY,
|
|
56
|
-
target_agent TEXT NOT NULL,
|
|
57
|
-
directive TEXT NOT NULL,
|
|
58
|
-
reason TEXT,
|
|
59
|
-
status TEXT DEFAULT 'active',
|
|
60
|
-
created_batch INTEGER,
|
|
61
|
-
metadata TEXT
|
|
62
|
-
);
|
|
63
|
-
|
|
64
|
-
CREATE INDEX IF NOT EXISTS idx_artifacts_story ON artifacts(story_id);
|
|
65
|
-
CREATE INDEX IF NOT EXISTS idx_artifacts_type ON artifacts(artifact_type);
|
|
66
|
-
CREATE INDEX IF NOT EXISTS idx_artifacts_agent ON artifacts(agent);
|
|
67
|
-
CREATE INDEX IF NOT EXISTS idx_cd_target ON correction_directives(target_agent);
|
|
68
|
-
CREATE INDEX IF NOT EXISTS idx_cd_status ON correction_directives(status);
|
|
69
|
-
|
|
70
|
-
CREATE TABLE IF NOT EXISTS calibration (
|
|
71
|
-
story_id TEXT PRIMARY KEY,
|
|
72
|
-
story_points INTEGER,
|
|
73
|
-
ac_count INTEGER,
|
|
74
|
-
surface TEXT,
|
|
75
|
-
estimated_points INTEGER,
|
|
76
|
-
actual_mins REAL,
|
|
77
|
-
rework_cycles INTEGER DEFAULT 0,
|
|
78
|
-
sprint_id TEXT,
|
|
79
|
-
created_at TEXT DEFAULT (datetime('now'))
|
|
80
|
-
);
|
|
81
|
-
CREATE INDEX IF NOT EXISTS idx_calibration_sprint ON calibration(sprint_id);
|
|
82
|
-
|
|
83
|
-
CREATE TABLE IF NOT EXISTS artifacts_working (
|
|
84
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
85
|
-
story_id TEXT NOT NULL,
|
|
86
|
-
agent TEXT NOT NULL,
|
|
87
|
-
artifact_type TEXT NOT NULL,
|
|
88
|
-
content TEXT,
|
|
89
|
-
metadata TEXT,
|
|
90
|
-
created_at TEXT DEFAULT (datetime('now')),
|
|
91
|
-
UNIQUE(story_id, artifact_type)
|
|
92
|
-
);
|
|
93
|
-
`);
|
|
94
|
-
|
|
95
|
-
// Create FTS5 virtual table
|
|
96
|
-
db.exec(`
|
|
97
|
-
CREATE VIRTUAL TABLE IF NOT EXISTS artifacts_fts USING fts5(
|
|
98
|
-
content,
|
|
99
|
-
story_id UNINDEXED,
|
|
100
|
-
agent UNINDEXED,
|
|
101
|
-
artifact_type UNINDEXED,
|
|
102
|
-
content=artifacts,
|
|
103
|
-
content_rowid=rowid
|
|
104
|
-
);
|
|
105
|
-
`);
|
|
106
|
-
|
|
107
|
-
// Create FTS triggers for auto-sync
|
|
108
|
-
db.exec(`
|
|
109
|
-
CREATE TRIGGER IF NOT EXISTS artifacts_ai AFTER INSERT ON artifacts BEGIN
|
|
110
|
-
INSERT INTO artifacts_fts(rowid, content, story_id, agent, artifact_type)
|
|
111
|
-
VALUES (new.rowid, new.content, new.story_id, new.agent, new.artifact_type);
|
|
112
|
-
END;
|
|
113
|
-
|
|
114
|
-
CREATE TRIGGER IF NOT EXISTS artifacts_ad AFTER DELETE ON artifacts BEGIN
|
|
115
|
-
INSERT INTO artifacts_fts(artifacts_fts, rowid, content, story_id, agent, artifact_type)
|
|
116
|
-
VALUES ('delete', old.rowid, old.content, old.story_id, old.agent, old.artifact_type);
|
|
117
|
-
END;
|
|
118
|
-
|
|
119
|
-
CREATE TRIGGER IF NOT EXISTS artifacts_au AFTER UPDATE ON artifacts BEGIN
|
|
120
|
-
INSERT INTO artifacts_fts(artifacts_fts, rowid, content, story_id, agent, artifact_type)
|
|
121
|
-
VALUES ('delete', old.rowid, old.content, old.story_id, old.agent, old.artifact_type);
|
|
122
|
-
INSERT INTO artifacts_fts(rowid, content, story_id, agent, artifact_type)
|
|
123
|
-
VALUES (new.rowid, new.content, new.story_id, new.agent, new.artifact_type);
|
|
124
|
-
END;
|
|
125
|
-
`);
|
|
20
|
+
const db = await openDb({ dbPath: fullDbPath, loadVec: true });
|
|
126
21
|
|
|
127
22
|
console.log(`Database initialized at ${fullDbPath}`);
|
|
128
23
|
console.log('Tables: artifacts, artifacts_fts, correction_directives, calibration, artifacts_working');
|
package/src/commands/db-query.js
CHANGED
|
@@ -1,27 +1,10 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { readFileSync, existsSync } from 'fs';
|
|
3
|
-
|
|
4
|
-
function resolveDbPath(options) {
|
|
5
|
-
if (options.dbPath) return options.dbPath;
|
|
6
|
-
const configPath = join(process.cwd(), '.valent-pipeline', 'pipeline-config.yaml');
|
|
7
|
-
if (existsSync(configPath)) {
|
|
8
|
-
const content = readFileSync(configPath, 'utf-8');
|
|
9
|
-
const match = content.match(/sqlite_db_path:\s*"?([^"\n]+)"?/);
|
|
10
|
-
if (match) return join(process.cwd(), match[1].trim());
|
|
11
|
-
}
|
|
12
|
-
return join(process.cwd(), '.valent-pipeline', 'pipeline.db');
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
function getDb(dbPath) {
|
|
16
|
-
const Database = require(join(process.cwd(), '.valent-pipeline', 'node_modules', 'better-sqlite3'));
|
|
17
|
-
return new Database(dbPath, { readonly: true });
|
|
18
|
-
}
|
|
1
|
+
import { resolveDbPath, openReadonlyDb } from '../lib/db.js';
|
|
19
2
|
|
|
20
3
|
export async function dbQueryArtifact(options) {
|
|
21
4
|
const dbPath = resolveDbPath(options);
|
|
5
|
+
const db = await openReadonlyDb({ dbPath });
|
|
22
6
|
const { story, type } = options;
|
|
23
7
|
|
|
24
|
-
const db = getDb(dbPath);
|
|
25
8
|
const row = db.prepare(
|
|
26
9
|
'SELECT content, agent, created_at FROM artifacts WHERE story_id = ? AND artifact_type = ?'
|
|
27
10
|
).get(story, type);
|
|
@@ -37,9 +20,9 @@ export async function dbQueryArtifact(options) {
|
|
|
37
20
|
|
|
38
21
|
export async function dbQueryDirectives(options) {
|
|
39
22
|
const dbPath = resolveDbPath(options);
|
|
23
|
+
const db = await openReadonlyDb({ dbPath });
|
|
40
24
|
const { agent } = options;
|
|
41
25
|
|
|
42
|
-
const db = getDb(dbPath);
|
|
43
26
|
let rows;
|
|
44
27
|
if (agent) {
|
|
45
28
|
rows = db.prepare(
|
|
@@ -65,9 +48,9 @@ export async function dbQueryDirectives(options) {
|
|
|
65
48
|
|
|
66
49
|
export async function dbQuerySearch(options) {
|
|
67
50
|
const dbPath = resolveDbPath(options);
|
|
51
|
+
const db = await openReadonlyDb({ dbPath });
|
|
68
52
|
const { query } = options;
|
|
69
53
|
|
|
70
|
-
const db = getDb(dbPath);
|
|
71
54
|
const rows = db.prepare(
|
|
72
55
|
"SELECT story_id, agent, artifact_type, snippet(artifacts_fts, 0, '>>>', '<<<', '...', 40) as snippet FROM artifacts_fts WHERE artifacts_fts MATCH ? ORDER BY rank LIMIT 10"
|
|
73
56
|
).all(query);
|
|
@@ -84,9 +67,9 @@ export async function dbQuerySearch(options) {
|
|
|
84
67
|
|
|
85
68
|
export async function dbQueryList(options) {
|
|
86
69
|
const dbPath = resolveDbPath(options);
|
|
70
|
+
const db = await openReadonlyDb({ dbPath });
|
|
87
71
|
const { story } = options;
|
|
88
72
|
|
|
89
|
-
const db = getDb(dbPath);
|
|
90
73
|
const rows = db.prepare(
|
|
91
74
|
'SELECT artifact_type, agent, length(content) as size, created_at FROM artifacts WHERE story_id = ? ORDER BY created_at'
|
|
92
75
|
).all(story);
|
|
@@ -104,7 +87,8 @@ export async function dbQueryList(options) {
|
|
|
104
87
|
|
|
105
88
|
export async function dbQueryStories(options) {
|
|
106
89
|
const dbPath = resolveDbPath(options);
|
|
107
|
-
const db =
|
|
90
|
+
const db = await openReadonlyDb({ dbPath });
|
|
91
|
+
|
|
108
92
|
const rows = db.prepare(
|
|
109
93
|
'SELECT DISTINCT story_id, COUNT(*) as artifact_count FROM artifacts GROUP BY story_id ORDER BY story_id'
|
|
110
94
|
).all();
|
|
@@ -121,9 +105,9 @@ export async function dbQueryStories(options) {
|
|
|
121
105
|
|
|
122
106
|
export async function dbQueryBugsSince(options) {
|
|
123
107
|
const dbPath = resolveDbPath(options);
|
|
108
|
+
const db = await openReadonlyDb({ dbPath });
|
|
124
109
|
const { since } = options;
|
|
125
110
|
|
|
126
|
-
const db = getDb(dbPath);
|
|
127
111
|
const rows = db.prepare(
|
|
128
112
|
"SELECT story_id, content FROM artifacts WHERE artifact_type = 'bugs' AND created_at > ? ORDER BY created_at"
|
|
129
113
|
).all(since);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { join } from 'path';
|
|
2
2
|
import { existsSync, readFileSync, readdirSync, statSync } from 'fs';
|
|
3
|
+
import { openDb, resolveDbPath } from '../lib/db.js';
|
|
3
4
|
|
|
4
5
|
// Artifact files we index (filename -> artifact_type mapping)
|
|
5
6
|
const ARTIFACT_MAP = {
|
|
@@ -29,53 +30,14 @@ export async function dbRebuild() {
|
|
|
29
30
|
process.exit(1);
|
|
30
31
|
}
|
|
31
32
|
|
|
32
|
-
// Read db path
|
|
33
|
+
// Read config for db path and stories directory
|
|
33
34
|
const content = readFileSync(configPath, 'utf-8');
|
|
34
35
|
const dbPathMatch = content.match(/sqlite_db_path:\s*"?([^"\n]+)"?/);
|
|
35
36
|
const dbPath = dbPathMatch ? dbPathMatch[1].trim() : './.valent-pipeline/pipeline.db';
|
|
36
37
|
const fullDbPath = join(projectRoot, dbPath);
|
|
37
38
|
|
|
38
|
-
if
|
|
39
|
-
|
|
40
|
-
process.exit(1);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
let Database;
|
|
44
|
-
try {
|
|
45
|
-
Database = (await import('better-sqlite3')).default;
|
|
46
|
-
} catch (err) {
|
|
47
|
-
console.error('Error: better-sqlite3 not installed. Run: npm install better-sqlite3');
|
|
48
|
-
process.exit(1);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
const db = new Database(fullDbPath);
|
|
52
|
-
|
|
53
|
-
// Ensure calibration and working tables exist
|
|
54
|
-
db.exec(`
|
|
55
|
-
CREATE TABLE IF NOT EXISTS calibration (
|
|
56
|
-
story_id TEXT PRIMARY KEY,
|
|
57
|
-
story_points INTEGER,
|
|
58
|
-
ac_count INTEGER,
|
|
59
|
-
surface TEXT,
|
|
60
|
-
estimated_points INTEGER,
|
|
61
|
-
actual_mins REAL,
|
|
62
|
-
rework_cycles INTEGER DEFAULT 0,
|
|
63
|
-
sprint_id TEXT,
|
|
64
|
-
created_at TEXT DEFAULT (datetime('now'))
|
|
65
|
-
);
|
|
66
|
-
CREATE INDEX IF NOT EXISTS idx_calibration_sprint ON calibration(sprint_id);
|
|
67
|
-
|
|
68
|
-
CREATE TABLE IF NOT EXISTS artifacts_working (
|
|
69
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
70
|
-
story_id TEXT NOT NULL,
|
|
71
|
-
agent TEXT NOT NULL,
|
|
72
|
-
artifact_type TEXT NOT NULL,
|
|
73
|
-
content TEXT,
|
|
74
|
-
metadata TEXT,
|
|
75
|
-
created_at TEXT DEFAULT (datetime('now')),
|
|
76
|
-
UNIQUE(story_id, artifact_type)
|
|
77
|
-
);
|
|
78
|
-
`);
|
|
39
|
+
// openDb auto-creates schema if DB is new
|
|
40
|
+
const db = await openDb({ dbPath: fullDbPath });
|
|
79
41
|
|
|
80
42
|
// Find stories directory
|
|
81
43
|
const storyDirMatch = content.match(/story_directory:\s*"?([^"\n]+)"?/);
|
package/src/commands/init.js
CHANGED
|
@@ -89,10 +89,12 @@ export async function init(options = {}) {
|
|
|
89
89
|
writeFileSafe(vpPkgPath, JSON.stringify({
|
|
90
90
|
"name": "valent-pipeline-runtime",
|
|
91
91
|
"private": true,
|
|
92
|
+
"type": "module",
|
|
92
93
|
"description": "Runtime dependencies for valent-pipeline scripts. Do not edit.",
|
|
93
94
|
"dependencies": {
|
|
94
95
|
"better-sqlite3": "^11.0.0",
|
|
95
|
-
"sqlite-vec": "^0.1.0"
|
|
96
|
+
"sqlite-vec": "^0.1.0",
|
|
97
|
+
"tsx": "^4.0.0"
|
|
96
98
|
}
|
|
97
99
|
}, null, 2) + '\n');
|
|
98
100
|
}
|
|
@@ -100,11 +102,23 @@ export async function init(options = {}) {
|
|
|
100
102
|
const { execSync } = await import('child_process');
|
|
101
103
|
try {
|
|
102
104
|
execSync('npm install --production', { cwd: pipelineDest, stdio: 'pipe' });
|
|
103
|
-
console.log(' Installed better-sqlite3 + sqlite-vec');
|
|
105
|
+
console.log(' Installed better-sqlite3 + sqlite-vec + tsx');
|
|
104
106
|
} catch (err) {
|
|
105
107
|
console.warn(' Warning: Failed to install SQLite dependencies. Run "cd .valent-pipeline && npm install" manually.');
|
|
106
108
|
}
|
|
107
|
-
|
|
109
|
+
|
|
110
|
+
// Auto-create the SQLite database (no separate "db init" step needed)
|
|
111
|
+
try {
|
|
112
|
+
const { openDb } = await import('../lib/db.js');
|
|
113
|
+
const dbPathMatch = config.knowledge?.sqlite_db_path;
|
|
114
|
+
const fullDbPath = join(projectRoot, dbPathMatch || './.valent-pipeline/pipeline.db');
|
|
115
|
+
const db = await openDb({ dbPath: fullDbPath });
|
|
116
|
+
db.close();
|
|
117
|
+
console.log(' Created SQLite knowledge database');
|
|
118
|
+
} catch (err) {
|
|
119
|
+
console.warn(` Warning: Could not auto-create database: ${err.message}`);
|
|
120
|
+
console.warn(' Run "valent-pipeline db init" manually.');
|
|
121
|
+
}
|
|
108
122
|
}
|
|
109
123
|
|
|
110
124
|
// 7b. Install browser automation MCP for UI projects
|
package/src/commands/upgrade.js
CHANGED
|
@@ -91,6 +91,50 @@ export async function upgrade(options = {}) {
|
|
|
91
91
|
}
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
+
// Migrate runtime package.json: add "type": "module" and tsx if missing
|
|
95
|
+
const runtimePkgPath = join(pipelineDest, 'package.json');
|
|
96
|
+
if (fileExists(runtimePkgPath)) {
|
|
97
|
+
try {
|
|
98
|
+
const runtimePkg = JSON.parse(readFile(runtimePkgPath));
|
|
99
|
+
let needsInstall = false;
|
|
100
|
+
|
|
101
|
+
if (!runtimePkg.type) {
|
|
102
|
+
runtimePkg.type = 'module';
|
|
103
|
+
console.log('Migrated .valent-pipeline/package.json: added "type": "module"');
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (!runtimePkg.dependencies?.tsx) {
|
|
107
|
+
runtimePkg.dependencies = runtimePkg.dependencies || {};
|
|
108
|
+
runtimePkg.dependencies.tsx = '^4.0.0';
|
|
109
|
+
needsInstall = true;
|
|
110
|
+
console.log('Migrated .valent-pipeline/package.json: added tsx dependency');
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
writeFileSafe(runtimePkgPath, JSON.stringify(runtimePkg, null, 2) + '\n');
|
|
114
|
+
|
|
115
|
+
if (needsInstall) {
|
|
116
|
+
const { execSync } = await import('child_process');
|
|
117
|
+
try {
|
|
118
|
+
execSync('npm install --production', { cwd: pipelineDest, stdio: 'pipe' });
|
|
119
|
+
console.log('Installed new dependencies in .valent-pipeline/');
|
|
120
|
+
} catch (err) {
|
|
121
|
+
console.warn('Warning: Failed to install new dependencies. Run "cd .valent-pipeline && npm install" manually.');
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
} catch { /* skip if parse fails */ }
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Ensure database schema is up to date (adds any new tables from this version)
|
|
128
|
+
try {
|
|
129
|
+
const { openDb, resolveDbPath } = await import('../lib/db.js');
|
|
130
|
+
const dbPath = resolveDbPath();
|
|
131
|
+
const db = await openDb({ dbPath });
|
|
132
|
+
db.close();
|
|
133
|
+
console.log('Ensured database schema is up to date');
|
|
134
|
+
} catch {
|
|
135
|
+
// DB may not be configured — skip silently
|
|
136
|
+
}
|
|
137
|
+
|
|
94
138
|
// Update version file
|
|
95
139
|
writeFileSafe(versionFile, packageVersion);
|
|
96
140
|
console.log(`Updated .valent-pipeline/.valent-version to ${packageVersion}`);
|
package/src/lib/db.js
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* db.js — Shared SQLite database utilities for valent-pipeline.
|
|
3
|
+
*
|
|
4
|
+
* Single source of truth for:
|
|
5
|
+
* - Schema DDL (all tables, indexes, FTS5, triggers)
|
|
6
|
+
* - DB path resolution from config
|
|
7
|
+
* - Database open/close with auto-schema creation
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { join } from 'path';
|
|
11
|
+
import { existsSync, readFileSync } from 'fs';
|
|
12
|
+
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
// Schema DDL — authoritative source. Keep pipeline/scripts/db-bootstrap.ts
|
|
15
|
+
// in sync when modifying (see docs/design/refactor-checklist.md).
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
|
|
18
|
+
export const SCHEMA_DDL = `
|
|
19
|
+
CREATE TABLE IF NOT EXISTS artifacts (
|
|
20
|
+
id TEXT PRIMARY KEY,
|
|
21
|
+
story_id TEXT NOT NULL,
|
|
22
|
+
agent TEXT NOT NULL,
|
|
23
|
+
artifact_type TEXT NOT NULL,
|
|
24
|
+
content TEXT NOT NULL,
|
|
25
|
+
metadata TEXT,
|
|
26
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
27
|
+
UNIQUE(story_id, artifact_type)
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
CREATE TABLE IF NOT EXISTS correction_directives (
|
|
31
|
+
id TEXT PRIMARY KEY,
|
|
32
|
+
target_agent TEXT NOT NULL,
|
|
33
|
+
directive TEXT NOT NULL,
|
|
34
|
+
reason TEXT,
|
|
35
|
+
status TEXT DEFAULT 'active',
|
|
36
|
+
created_batch INTEGER,
|
|
37
|
+
metadata TEXT
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
CREATE TABLE IF NOT EXISTS calibration (
|
|
41
|
+
story_id TEXT PRIMARY KEY,
|
|
42
|
+
story_points INTEGER,
|
|
43
|
+
ac_count INTEGER,
|
|
44
|
+
surface TEXT,
|
|
45
|
+
estimated_points INTEGER,
|
|
46
|
+
actual_mins REAL,
|
|
47
|
+
rework_cycles INTEGER DEFAULT 0,
|
|
48
|
+
sprint_id TEXT,
|
|
49
|
+
created_at TEXT DEFAULT (datetime('now'))
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
CREATE TABLE IF NOT EXISTS artifacts_working (
|
|
53
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
54
|
+
story_id TEXT NOT NULL,
|
|
55
|
+
agent TEXT NOT NULL,
|
|
56
|
+
artifact_type TEXT NOT NULL,
|
|
57
|
+
content TEXT,
|
|
58
|
+
metadata TEXT,
|
|
59
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
60
|
+
UNIQUE(story_id, artifact_type)
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
CREATE INDEX IF NOT EXISTS idx_artifacts_story ON artifacts(story_id);
|
|
64
|
+
CREATE INDEX IF NOT EXISTS idx_artifacts_type ON artifacts(artifact_type);
|
|
65
|
+
CREATE INDEX IF NOT EXISTS idx_artifacts_agent ON artifacts(agent);
|
|
66
|
+
CREATE INDEX IF NOT EXISTS idx_cd_target ON correction_directives(target_agent);
|
|
67
|
+
CREATE INDEX IF NOT EXISTS idx_cd_status ON correction_directives(status);
|
|
68
|
+
CREATE INDEX IF NOT EXISTS idx_calibration_sprint ON calibration(sprint_id);
|
|
69
|
+
`;
|
|
70
|
+
|
|
71
|
+
export const FTS_DDL = `
|
|
72
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS artifacts_fts USING fts5(
|
|
73
|
+
content,
|
|
74
|
+
story_id UNINDEXED,
|
|
75
|
+
agent UNINDEXED,
|
|
76
|
+
artifact_type UNINDEXED,
|
|
77
|
+
content=artifacts,
|
|
78
|
+
content_rowid=rowid
|
|
79
|
+
);
|
|
80
|
+
`;
|
|
81
|
+
|
|
82
|
+
export const TRIGGERS_DDL = `
|
|
83
|
+
CREATE TRIGGER IF NOT EXISTS artifacts_ai AFTER INSERT ON artifacts BEGIN
|
|
84
|
+
INSERT INTO artifacts_fts(rowid, content, story_id, agent, artifact_type)
|
|
85
|
+
VALUES (new.rowid, new.content, new.story_id, new.agent, new.artifact_type);
|
|
86
|
+
END;
|
|
87
|
+
|
|
88
|
+
CREATE TRIGGER IF NOT EXISTS artifacts_ad AFTER DELETE ON artifacts BEGIN
|
|
89
|
+
INSERT INTO artifacts_fts(artifacts_fts, rowid, content, story_id, agent, artifact_type)
|
|
90
|
+
VALUES ('delete', old.rowid, old.content, old.story_id, old.agent, old.artifact_type);
|
|
91
|
+
END;
|
|
92
|
+
|
|
93
|
+
CREATE TRIGGER IF NOT EXISTS artifacts_au AFTER UPDATE ON artifacts BEGIN
|
|
94
|
+
INSERT INTO artifacts_fts(artifacts_fts, rowid, content, story_id, agent, artifact_type)
|
|
95
|
+
VALUES ('delete', old.rowid, old.content, old.story_id, old.agent, old.artifact_type);
|
|
96
|
+
INSERT INTO artifacts_fts(rowid, content, story_id, agent, artifact_type)
|
|
97
|
+
VALUES (new.rowid, new.content, new.story_id, new.agent, new.artifact_type);
|
|
98
|
+
END;
|
|
99
|
+
`;
|
|
100
|
+
|
|
101
|
+
// ---------------------------------------------------------------------------
|
|
102
|
+
// DB path resolution
|
|
103
|
+
// ---------------------------------------------------------------------------
|
|
104
|
+
|
|
105
|
+
export function resolveDbPath(options = {}) {
|
|
106
|
+
if (options.dbPath) return options.dbPath;
|
|
107
|
+
const configPath = join(process.cwd(), '.valent-pipeline', 'pipeline-config.yaml');
|
|
108
|
+
if (existsSync(configPath)) {
|
|
109
|
+
const content = readFileSync(configPath, 'utf-8');
|
|
110
|
+
const match = content.match(/sqlite_db_path:\s*"?([^"\n]+)"?/);
|
|
111
|
+
if (match) return join(process.cwd(), match[1].trim());
|
|
112
|
+
}
|
|
113
|
+
return join(process.cwd(), '.valent-pipeline', 'pipeline.db');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ---------------------------------------------------------------------------
|
|
117
|
+
// Database loading
|
|
118
|
+
// ---------------------------------------------------------------------------
|
|
119
|
+
|
|
120
|
+
export async function loadDatabase() {
|
|
121
|
+
try {
|
|
122
|
+
return (await import('better-sqlite3')).default;
|
|
123
|
+
} catch (err) {
|
|
124
|
+
console.error('Error: better-sqlite3 not installed.');
|
|
125
|
+
console.error('Run: npm install better-sqlite3');
|
|
126
|
+
process.exit(1);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export async function loadSqliteVec() {
|
|
131
|
+
try {
|
|
132
|
+
return await import('sqlite-vec');
|
|
133
|
+
} catch {
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ---------------------------------------------------------------------------
|
|
139
|
+
// Schema management
|
|
140
|
+
// ---------------------------------------------------------------------------
|
|
141
|
+
|
|
142
|
+
export function ensureSchema(db) {
|
|
143
|
+
db.exec(SCHEMA_DDL);
|
|
144
|
+
db.exec(FTS_DDL);
|
|
145
|
+
db.exec(TRIGGERS_DDL);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ---------------------------------------------------------------------------
|
|
149
|
+
// High-level open helpers
|
|
150
|
+
// ---------------------------------------------------------------------------
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Open a read-write database connection with auto-schema creation.
|
|
154
|
+
* Creates the DB file and all tables if they don't exist.
|
|
155
|
+
*
|
|
156
|
+
* @param {object} [options]
|
|
157
|
+
* @param {string} [options.dbPath] — explicit path, overrides config
|
|
158
|
+
* @param {boolean} [options.loadVec] — attempt to load sqlite-vec extension
|
|
159
|
+
* @returns {Promise<import('better-sqlite3').Database>}
|
|
160
|
+
*/
|
|
161
|
+
export async function openDb(options = {}) {
|
|
162
|
+
const dbPath = typeof options === 'string' ? options : resolveDbPath(options);
|
|
163
|
+
const Database = await loadDatabase();
|
|
164
|
+
const db = new Database(dbPath);
|
|
165
|
+
|
|
166
|
+
if (options.loadVec !== false) {
|
|
167
|
+
const sqliteVec = await loadSqliteVec();
|
|
168
|
+
if (sqliteVec) {
|
|
169
|
+
try {
|
|
170
|
+
sqliteVec.load(db);
|
|
171
|
+
} catch {
|
|
172
|
+
// sqlite-vec not available — FTS5 still works
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
ensureSchema(db);
|
|
178
|
+
return db;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Open a read-only database connection. Does NOT run schema DDL.
|
|
183
|
+
* Errors with a helpful message if the DB file does not exist.
|
|
184
|
+
*
|
|
185
|
+
* @param {object} [options]
|
|
186
|
+
* @param {string} [options.dbPath] — explicit path, overrides config
|
|
187
|
+
* @returns {Promise<import('better-sqlite3').Database>}
|
|
188
|
+
*/
|
|
189
|
+
export async function openReadonlyDb(options = {}) {
|
|
190
|
+
const dbPath = typeof options === 'string' ? options : resolveDbPath(options);
|
|
191
|
+
|
|
192
|
+
if (!existsSync(dbPath)) {
|
|
193
|
+
console.error(`Database not found: ${dbPath}`);
|
|
194
|
+
console.error('Run "valent-pipeline init" or "valent-pipeline db init" to create it.');
|
|
195
|
+
process.exit(1);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const Database = await loadDatabase();
|
|
199
|
+
return new Database(dbPath, { readonly: true });
|
|
200
|
+
}
|