@codexa/cli 9.0.2 → 9.0.3
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/commands/architect.test.ts +531 -0
- package/commands/architect.ts +68 -11
- package/commands/clear.ts +0 -1
- package/commands/decide.ts +28 -28
- package/commands/discover.ts +128 -3
- package/commands/knowledge.ts +2 -27
- package/commands/patterns.test.ts +169 -0
- package/commands/plan.test.ts +73 -0
- package/commands/plan.ts +4 -2
- package/commands/sync.ts +90 -0
- package/commands/task.ts +43 -159
- package/commands/utils.ts +251 -249
- package/db/schema.test.ts +333 -0
- package/db/schema.ts +160 -130
- package/gates/validator.test.ts +617 -0
- package/gates/validator.ts +42 -10
- package/package.json +3 -1
- package/protocol/process-return.ts +25 -93
- package/protocol/subagent-protocol.test.ts +936 -0
- package/protocol/subagent-protocol.ts +19 -1
- package/workflow.ts +85 -27
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from "bun:test";
|
|
2
|
+
import { Database } from "bun:sqlite";
|
|
3
|
+
import { mkdirSync, rmSync, existsSync } from "fs";
|
|
4
|
+
import { join } from "path";
|
|
5
|
+
|
|
6
|
+
// Setup: use a temporary directory for test databases
|
|
7
|
+
const TEST_DIR = join(import.meta.dir, "..", ".test-db");
|
|
8
|
+
const TEST_DB_PATH = join(TEST_DIR, "test.db");
|
|
9
|
+
|
|
10
|
+
// We need to override the DB path before importing schema functions.
|
|
11
|
+
// The connection module uses process.cwd() so we mock via direct DB manipulation.
|
|
12
|
+
|
|
13
|
+
describe("Migration System", () => {
|
|
14
|
+
let db: Database;
|
|
15
|
+
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
// Create in-memory database with same schema setup
|
|
18
|
+
db = new Database(":memory:");
|
|
19
|
+
db.exec("PRAGMA journal_mode = WAL");
|
|
20
|
+
db.exec("PRAGMA foreign_keys = ON");
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
afterEach(() => {
|
|
24
|
+
db.close();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
function createBaseTables(db: Database) {
|
|
28
|
+
// Minimal table creation to test migrations against
|
|
29
|
+
db.exec(`
|
|
30
|
+
CREATE TABLE IF NOT EXISTS specs (
|
|
31
|
+
id TEXT PRIMARY KEY,
|
|
32
|
+
name TEXT NOT NULL,
|
|
33
|
+
phase TEXT NOT NULL DEFAULT 'planning',
|
|
34
|
+
approved_at TEXT,
|
|
35
|
+
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
|
36
|
+
updated_at TEXT
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
CREATE TABLE IF NOT EXISTS project (
|
|
40
|
+
id TEXT PRIMARY KEY DEFAULT 'default',
|
|
41
|
+
name TEXT,
|
|
42
|
+
stack TEXT NOT NULL,
|
|
43
|
+
discovered_at TEXT,
|
|
44
|
+
updated_at TEXT
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
CREATE TABLE IF NOT EXISTS tasks (
|
|
48
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
49
|
+
spec_id TEXT NOT NULL REFERENCES specs(id),
|
|
50
|
+
number INTEGER NOT NULL,
|
|
51
|
+
name TEXT NOT NULL,
|
|
52
|
+
depends_on TEXT,
|
|
53
|
+
can_parallel INTEGER DEFAULT 1,
|
|
54
|
+
agent TEXT,
|
|
55
|
+
files TEXT,
|
|
56
|
+
status TEXT DEFAULT 'pending',
|
|
57
|
+
checkpoint TEXT,
|
|
58
|
+
completed_at TEXT,
|
|
59
|
+
UNIQUE(spec_id, number)
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
CREATE TABLE IF NOT EXISTS decisions (
|
|
63
|
+
id TEXT PRIMARY KEY,
|
|
64
|
+
spec_id TEXT NOT NULL REFERENCES specs(id),
|
|
65
|
+
task_ref INTEGER,
|
|
66
|
+
title TEXT NOT NULL,
|
|
67
|
+
decision TEXT NOT NULL,
|
|
68
|
+
rationale TEXT,
|
|
69
|
+
status TEXT DEFAULT 'active',
|
|
70
|
+
created_at TEXT DEFAULT CURRENT_TIMESTAMP
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
CREATE TABLE IF NOT EXISTS schema_migrations (
|
|
74
|
+
version TEXT PRIMARY KEY,
|
|
75
|
+
description TEXT NOT NULL,
|
|
76
|
+
applied_at TEXT DEFAULT CURRENT_TIMESTAMP
|
|
77
|
+
);
|
|
78
|
+
`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ═══════════════════════════════════════════════════════════════
|
|
82
|
+
// runMigrations() tests
|
|
83
|
+
// ═══════════════════════════════════════════════════════════════
|
|
84
|
+
|
|
85
|
+
describe("runMigrations()", () => {
|
|
86
|
+
it("should apply all migrations on a fresh database", () => {
|
|
87
|
+
createBaseTables(db);
|
|
88
|
+
|
|
89
|
+
// Define test migrations
|
|
90
|
+
const migrations = [
|
|
91
|
+
{
|
|
92
|
+
version: "8.4.0",
|
|
93
|
+
description: "Add analysis_id to specs",
|
|
94
|
+
up: (d: Database) => d.exec("ALTER TABLE specs ADD COLUMN analysis_id TEXT"),
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
version: "8.7.0",
|
|
98
|
+
description: "Add cli_version to project",
|
|
99
|
+
up: (d: Database) => d.exec("ALTER TABLE project ADD COLUMN cli_version TEXT"),
|
|
100
|
+
},
|
|
101
|
+
];
|
|
102
|
+
|
|
103
|
+
// Run migrations manually (simulating runMigrations logic)
|
|
104
|
+
for (const migration of migrations) {
|
|
105
|
+
const existing = db.query("SELECT version FROM schema_migrations WHERE version = ?").get(migration.version);
|
|
106
|
+
if (existing) continue;
|
|
107
|
+
|
|
108
|
+
migration.up(db);
|
|
109
|
+
db.run(
|
|
110
|
+
"INSERT INTO schema_migrations (version, description) VALUES (?, ?)",
|
|
111
|
+
[migration.version, migration.description]
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Verify all migrations were recorded
|
|
116
|
+
const applied = db.query("SELECT * FROM schema_migrations ORDER BY version").all() as any[];
|
|
117
|
+
expect(applied.length).toBe(2);
|
|
118
|
+
expect(applied[0].version).toBe("8.4.0");
|
|
119
|
+
expect(applied[1].version).toBe("8.7.0");
|
|
120
|
+
|
|
121
|
+
// Verify columns were added
|
|
122
|
+
const columns = db.query("PRAGMA table_info(specs)").all() as any[];
|
|
123
|
+
const colNames = columns.map((c: any) => c.name);
|
|
124
|
+
expect(colNames).toContain("analysis_id");
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("should be idempotent — calling twice applies each migration only once", () => {
|
|
128
|
+
createBaseTables(db);
|
|
129
|
+
|
|
130
|
+
const migration = {
|
|
131
|
+
version: "8.4.0",
|
|
132
|
+
description: "Add analysis_id to specs",
|
|
133
|
+
up: (d: Database) => d.exec("ALTER TABLE specs ADD COLUMN analysis_id TEXT"),
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
// First run
|
|
137
|
+
migration.up(db);
|
|
138
|
+
db.run(
|
|
139
|
+
"INSERT INTO schema_migrations (version, description) VALUES (?, ?)",
|
|
140
|
+
[migration.version, migration.description]
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
// Second run — should skip because already applied
|
|
144
|
+
const existing = db.query("SELECT version FROM schema_migrations WHERE version = ?").get(migration.version);
|
|
145
|
+
expect(existing).not.toBeNull();
|
|
146
|
+
|
|
147
|
+
// Verify only 1 record
|
|
148
|
+
const applied = db.query("SELECT COUNT(*) as c FROM schema_migrations").get() as any;
|
|
149
|
+
expect(applied.c).toBe(1);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("should absorb 'duplicate column name' errors from pre-migration databases", () => {
|
|
153
|
+
createBaseTables(db);
|
|
154
|
+
|
|
155
|
+
// Manually add the column first (simulating pre-migration DB)
|
|
156
|
+
db.exec("ALTER TABLE specs ADD COLUMN analysis_id TEXT");
|
|
157
|
+
|
|
158
|
+
// Now try to run the migration — should NOT throw
|
|
159
|
+
const migration = {
|
|
160
|
+
version: "8.4.0",
|
|
161
|
+
description: "Add analysis_id to specs",
|
|
162
|
+
up: (d: Database) => d.exec("ALTER TABLE specs ADD COLUMN analysis_id TEXT"),
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
let absorbed = false;
|
|
166
|
+
try {
|
|
167
|
+
migration.up(db);
|
|
168
|
+
} catch (e: any) {
|
|
169
|
+
if (e.message.includes("duplicate column name")) {
|
|
170
|
+
absorbed = true;
|
|
171
|
+
} else {
|
|
172
|
+
throw e;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
expect(absorbed).toBe(true);
|
|
177
|
+
|
|
178
|
+
// Record it anyway
|
|
179
|
+
db.run(
|
|
180
|
+
"INSERT INTO schema_migrations (version, description) VALUES (?, ?)",
|
|
181
|
+
[migration.version, migration.description]
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
const applied = db.query("SELECT COUNT(*) as c FROM schema_migrations").get() as any;
|
|
185
|
+
expect(applied.c).toBe(1);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it("should propagate real errors (not duplicate column)", () => {
|
|
189
|
+
createBaseTables(db);
|
|
190
|
+
|
|
191
|
+
const badMigration = {
|
|
192
|
+
version: "99.0.0",
|
|
193
|
+
description: "Bad migration",
|
|
194
|
+
up: (d: Database) => d.exec("ALTER TABLE nonexistent_table ADD COLUMN foo TEXT"),
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
expect(() => {
|
|
198
|
+
badMigration.up(db);
|
|
199
|
+
}).toThrow();
|
|
200
|
+
|
|
201
|
+
// Migration should NOT be recorded
|
|
202
|
+
const applied = db.query("SELECT COUNT(*) as c FROM schema_migrations").get() as any;
|
|
203
|
+
expect(applied.c).toBe(0);
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
// ═══════════════════════════════════════════════════════════════
|
|
208
|
+
// getNextDecisionId() tests
|
|
209
|
+
// ═══════════════════════════════════════════════════════════════
|
|
210
|
+
|
|
211
|
+
describe("getNextDecisionId()", () => {
|
|
212
|
+
function getNextDecisionId(specId: string): string {
|
|
213
|
+
const result = db.query(
|
|
214
|
+
`SELECT MAX(CAST(REPLACE(id, 'DEC-', '') AS INTEGER)) as max_num
|
|
215
|
+
FROM decisions WHERE spec_id = ?`
|
|
216
|
+
).get(specId) as any;
|
|
217
|
+
|
|
218
|
+
const nextNum = (result?.max_num || 0) + 1;
|
|
219
|
+
return `DEC-${nextNum.toString().padStart(3, "0")}`;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
beforeEach(() => {
|
|
223
|
+
createBaseTables(db);
|
|
224
|
+
db.run("INSERT INTO specs (id, name, phase) VALUES ('SPEC-001', 'test', 'planning')");
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it("should return DEC-001 for empty decisions", () => {
|
|
228
|
+
expect(getNextDecisionId("SPEC-001")).toBe("DEC-001");
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it("should return sequential IDs", () => {
|
|
232
|
+
const now = new Date().toISOString();
|
|
233
|
+
db.run(
|
|
234
|
+
"INSERT INTO decisions (id, spec_id, title, decision, created_at) VALUES (?, ?, ?, ?, ?)",
|
|
235
|
+
["DEC-001", "SPEC-001", "test", "test", now]
|
|
236
|
+
);
|
|
237
|
+
expect(getNextDecisionId("SPEC-001")).toBe("DEC-002");
|
|
238
|
+
|
|
239
|
+
db.run(
|
|
240
|
+
"INSERT INTO decisions (id, spec_id, title, decision, created_at) VALUES (?, ?, ?, ?, ?)",
|
|
241
|
+
["DEC-002", "SPEC-001", "test2", "test2", now]
|
|
242
|
+
);
|
|
243
|
+
expect(getNextDecisionId("SPEC-001")).toBe("DEC-003");
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it("should handle gaps in IDs (e.g., DEC-001, DEC-003 → DEC-004)", () => {
|
|
247
|
+
const now = new Date().toISOString();
|
|
248
|
+
db.run(
|
|
249
|
+
"INSERT INTO decisions (id, spec_id, title, decision, created_at) VALUES (?, ?, ?, ?, ?)",
|
|
250
|
+
["DEC-001", "SPEC-001", "test", "test", now]
|
|
251
|
+
);
|
|
252
|
+
db.run(
|
|
253
|
+
"INSERT INTO decisions (id, spec_id, title, decision, created_at) VALUES (?, ?, ?, ?, ?)",
|
|
254
|
+
["DEC-003", "SPEC-001", "test3", "test3", now]
|
|
255
|
+
);
|
|
256
|
+
expect(getNextDecisionId("SPEC-001")).toBe("DEC-004");
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it("should scope to spec — different specs have independent IDs", () => {
|
|
260
|
+
db.run("INSERT INTO specs (id, name, phase) VALUES ('SPEC-002', 'other', 'planning')");
|
|
261
|
+
const now = new Date().toISOString();
|
|
262
|
+
db.run(
|
|
263
|
+
"INSERT INTO decisions (id, spec_id, title, decision, created_at) VALUES (?, ?, ?, ?, ?)",
|
|
264
|
+
["DEC-005", "SPEC-001", "test", "test", now]
|
|
265
|
+
);
|
|
266
|
+
|
|
267
|
+
expect(getNextDecisionId("SPEC-001")).toBe("DEC-006");
|
|
268
|
+
expect(getNextDecisionId("SPEC-002")).toBe("DEC-001");
|
|
269
|
+
});
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
// ═══════════════════════════════════════════════════════════════
|
|
273
|
+
// claimTask() tests
|
|
274
|
+
// ═══════════════════════════════════════════════════════════════
|
|
275
|
+
|
|
276
|
+
describe("claimTask()", () => {
|
|
277
|
+
function claimTask(taskId: number): boolean {
|
|
278
|
+
const result = db.run(
|
|
279
|
+
"UPDATE tasks SET status = 'running' WHERE id = ? AND status = 'pending'",
|
|
280
|
+
[taskId]
|
|
281
|
+
);
|
|
282
|
+
return result.changes > 0;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
beforeEach(() => {
|
|
286
|
+
createBaseTables(db);
|
|
287
|
+
db.run("INSERT INTO specs (id, name, phase) VALUES ('SPEC-001', 'test', 'implementing')");
|
|
288
|
+
db.run(
|
|
289
|
+
"INSERT INTO tasks (spec_id, number, name, status) VALUES (?, ?, ?, ?)",
|
|
290
|
+
["SPEC-001", 1, "Test task", "pending"]
|
|
291
|
+
);
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
it("should return true and set status to running for a pending task", () => {
|
|
295
|
+
const taskId = (db.query("SELECT id FROM tasks WHERE number = 1").get() as any).id;
|
|
296
|
+
expect(claimTask(taskId)).toBe(true);
|
|
297
|
+
|
|
298
|
+
const task = db.query("SELECT status FROM tasks WHERE id = ?").get(taskId) as any;
|
|
299
|
+
expect(task.status).toBe("running");
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
it("should return false for a task already in running status", () => {
|
|
303
|
+
const taskId = (db.query("SELECT id FROM tasks WHERE number = 1").get() as any).id;
|
|
304
|
+
|
|
305
|
+
// First claim succeeds
|
|
306
|
+
expect(claimTask(taskId)).toBe(true);
|
|
307
|
+
// Second claim fails
|
|
308
|
+
expect(claimTask(taskId)).toBe(false);
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
it("should return false for a task in done status", () => {
|
|
312
|
+
const taskId = (db.query("SELECT id FROM tasks WHERE number = 1").get() as any).id;
|
|
313
|
+
db.run("UPDATE tasks SET status = 'done' WHERE id = ?", [taskId]);
|
|
314
|
+
|
|
315
|
+
expect(claimTask(taskId)).toBe(false);
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
it("should return false for a non-existent task ID", () => {
|
|
319
|
+
expect(claimTask(99999)).toBe(false);
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
it("should prevent double-claim (only first caller wins)", () => {
|
|
323
|
+
const taskId = (db.query("SELECT id FROM tasks WHERE number = 1").get() as any).id;
|
|
324
|
+
|
|
325
|
+
// Simulate two concurrent claims
|
|
326
|
+
const claim1 = claimTask(taskId);
|
|
327
|
+
const claim2 = claimTask(taskId);
|
|
328
|
+
|
|
329
|
+
expect(claim1).toBe(true);
|
|
330
|
+
expect(claim2).toBe(false);
|
|
331
|
+
});
|
|
332
|
+
});
|
|
333
|
+
});
|
package/db/schema.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Database } from "bun:sqlite";
|
|
1
2
|
import { getDb } from "./connection";
|
|
2
3
|
|
|
3
4
|
export function initSchema(): void {
|
|
@@ -263,30 +264,7 @@ export function initSchema(): void {
|
|
|
263
264
|
created_at TEXT DEFAULT CURRENT_TIMESTAMP
|
|
264
265
|
);
|
|
265
266
|
|
|
266
|
-
-- 20.
|
|
267
|
-
CREATE TABLE IF NOT EXISTS session_summaries (
|
|
268
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
269
|
-
spec_id TEXT NOT NULL REFERENCES specs(id),
|
|
270
|
-
|
|
271
|
-
-- Periodo
|
|
272
|
-
start_time TEXT NOT NULL,
|
|
273
|
-
end_time TEXT NOT NULL,
|
|
274
|
-
|
|
275
|
-
-- Conteudo
|
|
276
|
-
summary TEXT NOT NULL, -- O que foi feito
|
|
277
|
-
decisions TEXT, -- JSON array de decisoes tomadas
|
|
278
|
-
blockers TEXT, -- JSON array de problemas encontrados
|
|
279
|
-
next_steps TEXT, -- JSON array de recomendacoes
|
|
280
|
-
|
|
281
|
-
-- Estatisticas
|
|
282
|
-
tasks_completed INTEGER DEFAULT 0,
|
|
283
|
-
files_created INTEGER DEFAULT 0,
|
|
284
|
-
files_modified INTEGER DEFAULT 0,
|
|
285
|
-
|
|
286
|
-
created_at TEXT DEFAULT CURRENT_TIMESTAMP
|
|
287
|
-
);
|
|
288
|
-
|
|
289
|
-
-- 21. Architectural Analyses (v8.1 - migrado de architect.ts para schema central)
|
|
267
|
+
-- 20. Architectural Analyses (v8.1 - migrado de architect.ts para schema central)
|
|
290
268
|
CREATE TABLE IF NOT EXISTS architectural_analyses (
|
|
291
269
|
id TEXT PRIMARY KEY,
|
|
292
270
|
name TEXT NOT NULL,
|
|
@@ -329,6 +307,13 @@ export function initSchema(): void {
|
|
|
329
307
|
UNIQUE(file_path, utility_name)
|
|
330
308
|
);
|
|
331
309
|
|
|
310
|
+
-- 23. Schema Migrations tracking (v9.2)
|
|
311
|
+
CREATE TABLE IF NOT EXISTS schema_migrations (
|
|
312
|
+
version TEXT PRIMARY KEY,
|
|
313
|
+
description TEXT NOT NULL,
|
|
314
|
+
applied_at TEXT DEFAULT CURRENT_TIMESTAMP
|
|
315
|
+
);
|
|
316
|
+
|
|
332
317
|
-- Indices para performance
|
|
333
318
|
CREATE INDEX IF NOT EXISTS idx_tasks_spec ON tasks(spec_id);
|
|
334
319
|
CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(status);
|
|
@@ -363,10 +348,6 @@ export function initSchema(): void {
|
|
|
363
348
|
CREATE INDEX IF NOT EXISTS idx_reasoning_category ON reasoning_log(category);
|
|
364
349
|
CREATE INDEX IF NOT EXISTS idx_reasoning_importance ON reasoning_log(importance);
|
|
365
350
|
|
|
366
|
-
-- v8.0: Indices para Session Summaries
|
|
367
|
-
CREATE INDEX IF NOT EXISTS idx_session_spec ON session_summaries(spec_id);
|
|
368
|
-
CREATE INDEX IF NOT EXISTS idx_session_time ON session_summaries(end_time);
|
|
369
|
-
|
|
370
351
|
-- v8.1: Indices para Architectural Analyses
|
|
371
352
|
CREATE INDEX IF NOT EXISTS idx_arch_status ON architectural_analyses(status);
|
|
372
353
|
CREATE INDEX IF NOT EXISTS idx_arch_created ON architectural_analyses(created_at);
|
|
@@ -377,21 +358,124 @@ export function initSchema(): void {
|
|
|
377
358
|
CREATE INDEX IF NOT EXISTS idx_utils_file ON project_utilities(file_path);
|
|
378
359
|
`);
|
|
379
360
|
|
|
380
|
-
//
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
} catch {
|
|
384
|
-
// Coluna ja existe - ignorar
|
|
385
|
-
}
|
|
361
|
+
// Executar migracoes versionadas
|
|
362
|
+
runMigrations();
|
|
363
|
+
}
|
|
386
364
|
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
365
|
+
// ═══════════════════════════════════════════════════════════════
|
|
366
|
+
// v9.2: Sistema de Migracoes Versionado
|
|
367
|
+
// ═══════════════════════════════════════════════════════════════
|
|
368
|
+
|
|
369
|
+
interface Migration {
|
|
370
|
+
version: string;
|
|
371
|
+
description: string;
|
|
372
|
+
up: (db: Database) => void;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const MIGRATIONS: Migration[] = [
|
|
376
|
+
{
|
|
377
|
+
version: "8.4.0",
|
|
378
|
+
description: "Adicionar analysis_id na tabela specs",
|
|
379
|
+
up: (db) => {
|
|
380
|
+
db.exec(`ALTER TABLE specs ADD COLUMN analysis_id TEXT`);
|
|
381
|
+
},
|
|
382
|
+
},
|
|
383
|
+
{
|
|
384
|
+
version: "8.7.0",
|
|
385
|
+
description: "Adicionar cli_version na tabela project",
|
|
386
|
+
up: (db) => {
|
|
387
|
+
db.exec(`ALTER TABLE project ADD COLUMN cli_version TEXT`);
|
|
388
|
+
},
|
|
389
|
+
},
|
|
390
|
+
{
|
|
391
|
+
version: "9.1.0",
|
|
392
|
+
description: "Adicionar last_discover_at na tabela project",
|
|
393
|
+
up: (db) => {
|
|
394
|
+
db.exec(`ALTER TABLE project ADD COLUMN last_discover_at TEXT`);
|
|
395
|
+
},
|
|
396
|
+
},
|
|
397
|
+
{
|
|
398
|
+
version: "9.1.1",
|
|
399
|
+
description: "Remover tabela session_summaries",
|
|
400
|
+
up: (db) => {
|
|
401
|
+
db.exec(`DROP TABLE IF EXISTS session_summaries`);
|
|
402
|
+
},
|
|
403
|
+
},
|
|
404
|
+
{
|
|
405
|
+
version: "9.2.0",
|
|
406
|
+
description: "Adicionar started_at na tabela tasks para validacao temporal de arquivos",
|
|
407
|
+
up: (db) => {
|
|
408
|
+
db.exec(`ALTER TABLE tasks ADD COLUMN started_at TEXT`);
|
|
409
|
+
},
|
|
410
|
+
},
|
|
411
|
+
];
|
|
412
|
+
|
|
413
|
+
export function runMigrations(): void {
|
|
414
|
+
const db = getDb();
|
|
415
|
+
|
|
416
|
+
for (const migration of MIGRATIONS) {
|
|
417
|
+
// Verificar se ja foi aplicada
|
|
418
|
+
const existing = db
|
|
419
|
+
.query("SELECT version FROM schema_migrations WHERE version = ?")
|
|
420
|
+
.get(migration.version);
|
|
421
|
+
|
|
422
|
+
if (existing) continue;
|
|
423
|
+
|
|
424
|
+
// Executar migracao
|
|
425
|
+
try {
|
|
426
|
+
migration.up(db);
|
|
427
|
+
} catch (e: any) {
|
|
428
|
+
const msg = (e as Error).message || "";
|
|
429
|
+
// "duplicate column name" e esperado em DBs criadas antes do sistema de migracoes
|
|
430
|
+
if (msg.includes("duplicate column name") || msg.includes("already exists")) {
|
|
431
|
+
// Marcar como aplicada mesmo assim — DB ja tem a mudanca
|
|
432
|
+
} else {
|
|
433
|
+
throw new Error(
|
|
434
|
+
`Migracao ${migration.version} falhou: ${msg}\n` +
|
|
435
|
+
`Descricao: ${migration.description}`
|
|
436
|
+
);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// Registrar migracao como aplicada
|
|
441
|
+
const now = new Date().toISOString();
|
|
442
|
+
db.run(
|
|
443
|
+
"INSERT INTO schema_migrations (version, description, applied_at) VALUES (?, ?, ?)",
|
|
444
|
+
[migration.version, migration.description, now]
|
|
445
|
+
);
|
|
392
446
|
}
|
|
393
447
|
}
|
|
394
448
|
|
|
449
|
+
// Exportar MIGRATIONS para testes
|
|
450
|
+
export { MIGRATIONS };
|
|
451
|
+
|
|
452
|
+
// Gera proximo ID de decisao para um spec (DEC-001, DEC-002, ...)
|
|
453
|
+
// Usa MAX() atomico para evitar race condition entre tasks paralelas
|
|
454
|
+
export function getNextDecisionId(specId: string): string {
|
|
455
|
+
const db = getDb();
|
|
456
|
+
const result = db
|
|
457
|
+
.query(
|
|
458
|
+
`SELECT MAX(CAST(REPLACE(id, 'DEC-', '') AS INTEGER)) as max_num
|
|
459
|
+
FROM decisions WHERE spec_id = ?`
|
|
460
|
+
)
|
|
461
|
+
.get(specId) as any;
|
|
462
|
+
|
|
463
|
+
const nextNum = (result?.max_num || 0) + 1;
|
|
464
|
+
return `DEC-${nextNum.toString().padStart(3, "0")}`;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// Claim atomico de task: retorna true se task estava pending e agora esta running.
|
|
468
|
+
// Retorna false se outra instancia ja pegou a task.
|
|
469
|
+
export function claimTask(taskId: number): boolean {
|
|
470
|
+
const db = getDb();
|
|
471
|
+
const now = new Date().toISOString();
|
|
472
|
+
const result = db.run(
|
|
473
|
+
"UPDATE tasks SET status = 'running', started_at = ? WHERE id = ? AND status = 'pending'",
|
|
474
|
+
[now, taskId]
|
|
475
|
+
);
|
|
476
|
+
return result.changes > 0;
|
|
477
|
+
}
|
|
478
|
+
|
|
395
479
|
export function getPatternsByScope(scope: string): any[] {
|
|
396
480
|
const db = getDb();
|
|
397
481
|
return db.query(
|
|
@@ -401,21 +485,45 @@ export function getPatternsByScope(scope: string): any[] {
|
|
|
401
485
|
|
|
402
486
|
export function getPatternsForFiles(files: string[]): any[] {
|
|
403
487
|
const db = getDb();
|
|
404
|
-
|
|
488
|
+
if (files.length === 0) return [];
|
|
489
|
+
|
|
490
|
+
// Pre-filtrar no SQL usando extensoes e diretorios para reduzir candidatos
|
|
491
|
+
const extensions = new Set<string>();
|
|
492
|
+
const dirs = new Set<string>();
|
|
493
|
+
for (const f of files) {
|
|
494
|
+
const ext = f.split('.').pop();
|
|
495
|
+
if (ext) extensions.add(ext);
|
|
496
|
+
const parts = f.split('/');
|
|
497
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
498
|
+
dirs.add(parts[i]);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
const conditions: string[] = [];
|
|
503
|
+
const params: string[] = [];
|
|
504
|
+
for (const ext of extensions) {
|
|
505
|
+
conditions.push("applies_to LIKE ?");
|
|
506
|
+
params.push(`%${ext}%`);
|
|
507
|
+
}
|
|
508
|
+
for (const dir of dirs) {
|
|
509
|
+
conditions.push("applies_to LIKE ?");
|
|
510
|
+
params.push(`%${dir}%`);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// Se nao ha condicoes, buscar tudo (fallback)
|
|
514
|
+
const candidates = conditions.length > 0
|
|
515
|
+
? db.query(`SELECT * FROM implementation_patterns WHERE ${conditions.join(' OR ')}`).all(...params) as any[]
|
|
516
|
+
: db.query("SELECT * FROM implementation_patterns").all() as any[];
|
|
405
517
|
|
|
406
|
-
//
|
|
407
|
-
return
|
|
518
|
+
// Regex match apenas nos candidatos pre-filtrados
|
|
519
|
+
return candidates.filter(pattern => {
|
|
408
520
|
const glob = pattern.applies_to;
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
.replace(/\//g, '\\/');
|
|
416
|
-
const regex = new RegExp(`^${regexPattern}$`);
|
|
417
|
-
return regex.test(file);
|
|
418
|
-
});
|
|
521
|
+
const regexPattern = glob
|
|
522
|
+
.replace(/\*\*/g, '.*')
|
|
523
|
+
.replace(/\*/g, '[^/]*')
|
|
524
|
+
.replace(/\//g, '\\/');
|
|
525
|
+
const regex = new RegExp(`^${regexPattern}$`);
|
|
526
|
+
return files.some(file => regex.test(file));
|
|
419
527
|
});
|
|
420
528
|
}
|
|
421
529
|
|
|
@@ -478,22 +586,6 @@ export function getRelatedFiles(decisionOrPattern: string, type: "decision" | "p
|
|
|
478
586
|
return results.map(r => r.file);
|
|
479
587
|
}
|
|
480
588
|
|
|
481
|
-
export function findContradictions(specId: string): Array<{ decision1: string; decision2: string; createdAt: string }> {
|
|
482
|
-
const db = getDb();
|
|
483
|
-
|
|
484
|
-
return db.query(`
|
|
485
|
-
SELECT
|
|
486
|
-
d1.title as decision1,
|
|
487
|
-
d2.title as decision2,
|
|
488
|
-
kg.created_at as createdAt
|
|
489
|
-
FROM knowledge_graph kg
|
|
490
|
-
JOIN decisions d1 ON kg.source_id = d1.id
|
|
491
|
-
JOIN decisions d2 ON kg.target_id = d2.id
|
|
492
|
-
WHERE kg.relation = 'contradicts'
|
|
493
|
-
AND kg.spec_id = ?
|
|
494
|
-
`).all(specId) as any[];
|
|
495
|
-
}
|
|
496
|
-
|
|
497
589
|
// ═══════════════════════════════════════════════════════════════
|
|
498
590
|
// v8.0: Reasoning Log Helpers
|
|
499
591
|
// ═══════════════════════════════════════════════════════════════
|
|
@@ -539,68 +631,6 @@ export function getRecentReasoning(specId: string, limit: number = 20): any[] {
|
|
|
539
631
|
`).all(specId, limit) as any[];
|
|
540
632
|
}
|
|
541
633
|
|
|
542
|
-
// ═══════════════════════════════════════════════════════════════
|
|
543
|
-
// v8.0: Session Summary Helpers
|
|
544
|
-
// ═══════════════════════════════════════════════════════════════
|
|
545
|
-
|
|
546
|
-
export interface SessionSummaryData {
|
|
547
|
-
startTime: string;
|
|
548
|
-
endTime: string;
|
|
549
|
-
summary: string;
|
|
550
|
-
decisions?: string[];
|
|
551
|
-
blockers?: string[];
|
|
552
|
-
nextSteps?: string[];
|
|
553
|
-
tasksCompleted?: number;
|
|
554
|
-
filesCreated?: number;
|
|
555
|
-
filesModified?: number;
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
export function addSessionSummary(specId: string, data: SessionSummaryData): void {
|
|
559
|
-
const db = getDb();
|
|
560
|
-
const now = new Date().toISOString();
|
|
561
|
-
|
|
562
|
-
db.run(
|
|
563
|
-
`INSERT INTO session_summaries (spec_id, start_time, end_time, summary, decisions, blockers, next_steps, tasks_completed, files_created, files_modified, created_at)
|
|
564
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
565
|
-
[
|
|
566
|
-
specId,
|
|
567
|
-
data.startTime,
|
|
568
|
-
data.endTime,
|
|
569
|
-
data.summary,
|
|
570
|
-
data.decisions ? JSON.stringify(data.decisions) : null,
|
|
571
|
-
data.blockers ? JSON.stringify(data.blockers) : null,
|
|
572
|
-
data.nextSteps ? JSON.stringify(data.nextSteps) : null,
|
|
573
|
-
data.tasksCompleted || 0,
|
|
574
|
-
data.filesCreated || 0,
|
|
575
|
-
data.filesModified || 0,
|
|
576
|
-
now,
|
|
577
|
-
]
|
|
578
|
-
);
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
export function getLastSessionSummary(specId: string): any {
|
|
582
|
-
const db = getDb();
|
|
583
|
-
|
|
584
|
-
return db.query(`
|
|
585
|
-
SELECT * FROM session_summaries
|
|
586
|
-
WHERE spec_id = ?
|
|
587
|
-
ORDER BY created_at DESC
|
|
588
|
-
LIMIT 1
|
|
589
|
-
`).get(specId);
|
|
590
|
-
}
|
|
591
|
-
|
|
592
|
-
// v8.3: Retorna ultimas N sessoes para contexto composto
|
|
593
|
-
export function getSessionSummaries(specId: string, limit: number = 5): any[] {
|
|
594
|
-
const db = getDb();
|
|
595
|
-
|
|
596
|
-
return db.query(`
|
|
597
|
-
SELECT * FROM session_summaries
|
|
598
|
-
WHERE spec_id = ?
|
|
599
|
-
ORDER BY created_at DESC
|
|
600
|
-
LIMIT ?
|
|
601
|
-
`).all(specId, limit) as any[];
|
|
602
|
-
}
|
|
603
|
-
|
|
604
634
|
// v8.4: Busca analise arquitetural associada a um spec
|
|
605
635
|
// Prioridade: analysis_id (link explicito) > nome exato > LIKE parcial
|
|
606
636
|
export function getArchitecturalAnalysisForSpec(specName: string, specId?: string): any {
|