@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.
@@ -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. Session Summaries (v8.0 - Resumos de sessao para continuidade)
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
- // v8.4: Migracao - adicionar analysis_id na tabela specs
381
- try {
382
- db.exec(`ALTER TABLE specs ADD COLUMN analysis_id TEXT`);
383
- } catch {
384
- // Coluna ja existe - ignorar
385
- }
361
+ // Executar migracoes versionadas
362
+ runMigrations();
363
+ }
386
364
 
387
- // v8.7: Migracao - adicionar cli_version na tabela project
388
- try {
389
- db.exec(`ALTER TABLE project ADD COLUMN cli_version TEXT`);
390
- } catch {
391
- // Coluna ja existe - ignorar
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
- const patterns = db.query("SELECT * FROM implementation_patterns").all() as any[];
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
- // Filtrar patterns que correspondem aos arquivos da task
407
- return patterns.filter(pattern => {
518
+ // Regex match apenas nos candidatos pre-filtrados
519
+ return candidates.filter(pattern => {
408
520
  const glob = pattern.applies_to;
409
- return files.some(file => {
410
- // Simplificado: verificar se o arquivo corresponde ao glob pattern
411
- // Ex: "app/**/page.tsx" deve corresponder a "app/dashboard/page.tsx"
412
- const regexPattern = glob
413
- .replace(/\*\*/g, '.*')
414
- .replace(/\*/g, '[^/]*')
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 {