@codexa/cli 9.0.2 → 9.0.4

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/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,169 @@ 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
+ version: "9.3.0",
413
+ description: "Criar tabela agent_performance para feedback loop",
414
+ up: (db) => {
415
+ db.exec(`
416
+ CREATE TABLE IF NOT EXISTS agent_performance (
417
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
418
+ agent_type TEXT NOT NULL,
419
+ spec_id TEXT NOT NULL,
420
+ task_id INTEGER NOT NULL,
421
+ gates_passed_first_try INTEGER DEFAULT 0,
422
+ gates_total INTEGER DEFAULT 0,
423
+ bypasses_used INTEGER DEFAULT 0,
424
+ files_created INTEGER DEFAULT 0,
425
+ files_modified INTEGER DEFAULT 0,
426
+ context_size_bytes INTEGER DEFAULT 0,
427
+ execution_duration_ms INTEGER DEFAULT 0,
428
+ created_at TEXT DEFAULT CURRENT_TIMESTAMP
429
+ )
430
+ `);
431
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_agent_perf_type ON agent_performance(agent_type)`);
432
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_agent_perf_created ON agent_performance(created_at)`);
433
+ },
434
+ },
435
+ {
436
+ version: "9.4.0",
437
+ description: "Migrar acknowledged_by de JSON para tabela separada",
438
+ up: (db) => {
439
+ db.exec(`
440
+ CREATE TABLE IF NOT EXISTS knowledge_acknowledgments (
441
+ knowledge_id INTEGER NOT NULL REFERENCES knowledge(id) ON DELETE CASCADE,
442
+ task_id INTEGER NOT NULL,
443
+ acknowledged_at TEXT DEFAULT CURRENT_TIMESTAMP,
444
+ PRIMARY KEY (knowledge_id, task_id)
445
+ )
446
+ `);
447
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_ka_task ON knowledge_acknowledgments(task_id)`);
448
+
449
+ // Migrar dados existentes do campo JSON
450
+ const rows = db.query("SELECT id, acknowledged_by FROM knowledge WHERE acknowledged_by IS NOT NULL").all() as any[];
451
+ const insert = db.prepare("INSERT OR IGNORE INTO knowledge_acknowledgments (knowledge_id, task_id) VALUES (?, ?)");
452
+ for (const row of rows) {
453
+ try {
454
+ const taskIds = JSON.parse(row.acknowledged_by) as number[];
455
+ for (const taskId of taskIds) {
456
+ insert.run(row.id, taskId);
457
+ }
458
+ } catch { /* JSON invalido, ignorar */ }
459
+ }
460
+ },
461
+ },
462
+ ];
463
+
464
+ export function runMigrations(): void {
465
+ const db = getDb();
466
+
467
+ for (const migration of MIGRATIONS) {
468
+ // Verificar se ja foi aplicada
469
+ const existing = db
470
+ .query("SELECT version FROM schema_migrations WHERE version = ?")
471
+ .get(migration.version);
472
+
473
+ if (existing) continue;
474
+
475
+ // Executar migracao
476
+ try {
477
+ migration.up(db);
478
+ } catch (e: any) {
479
+ const msg = (e as Error).message || "";
480
+ // "duplicate column name" e esperado em DBs criadas antes do sistema de migracoes
481
+ if (msg.includes("duplicate column name") || msg.includes("already exists")) {
482
+ // Marcar como aplicada mesmo assim — DB ja tem a mudanca
483
+ } else {
484
+ throw new Error(
485
+ `Migracao ${migration.version} falhou: ${msg}\n` +
486
+ `Descricao: ${migration.description}`
487
+ );
488
+ }
489
+ }
490
+
491
+ // Registrar migracao como aplicada
492
+ const now = new Date().toISOString();
493
+ db.run(
494
+ "INSERT INTO schema_migrations (version, description, applied_at) VALUES (?, ?, ?)",
495
+ [migration.version, migration.description, now]
496
+ );
392
497
  }
393
498
  }
394
499
 
500
+ // Exportar MIGRATIONS para testes
501
+ export { MIGRATIONS };
502
+
503
+ // Gera proximo ID de decisao para um spec
504
+ // Usa timestamp + random hash para eliminar race conditions entre tasks paralelas
505
+ export function getNextDecisionId(specId: string): string {
506
+ const slug = specId.split("-").slice(1, 3).join("-");
507
+ const ts = Date.now().toString(36);
508
+ const rand = Math.random().toString(36).substring(2, 6);
509
+ return `DEC-${slug}-${ts}-${rand}`;
510
+ }
511
+
512
+ // Claim atomico de task: retorna true se task estava pending e agora esta running.
513
+ // Retorna false se outra instancia ja pegou a task.
514
+ export function claimTask(taskId: number): boolean {
515
+ const db = getDb();
516
+ const now = new Date().toISOString();
517
+ const result = db.run(
518
+ "UPDATE tasks SET status = 'running', started_at = ? WHERE id = ? AND status = 'pending'",
519
+ [now, taskId]
520
+ );
521
+ return result.changes > 0;
522
+ }
523
+
395
524
  export function getPatternsByScope(scope: string): any[] {
396
525
  const db = getDb();
397
526
  return db.query(
@@ -401,21 +530,45 @@ export function getPatternsByScope(scope: string): any[] {
401
530
 
402
531
  export function getPatternsForFiles(files: string[]): any[] {
403
532
  const db = getDb();
404
- const patterns = db.query("SELECT * FROM implementation_patterns").all() as any[];
533
+ if (files.length === 0) return [];
534
+
535
+ // Pre-filtrar no SQL usando extensoes e diretorios para reduzir candidatos
536
+ const extensions = new Set<string>();
537
+ const dirs = new Set<string>();
538
+ for (const f of files) {
539
+ const ext = f.split('.').pop();
540
+ if (ext) extensions.add(ext);
541
+ const parts = f.split('/');
542
+ for (let i = 0; i < parts.length - 1; i++) {
543
+ dirs.add(parts[i]);
544
+ }
545
+ }
405
546
 
406
- // Filtrar patterns que correspondem aos arquivos da task
407
- return patterns.filter(pattern => {
547
+ const conditions: string[] = [];
548
+ const params: string[] = [];
549
+ for (const ext of extensions) {
550
+ conditions.push("applies_to LIKE ?");
551
+ params.push(`%${ext}%`);
552
+ }
553
+ for (const dir of dirs) {
554
+ conditions.push("applies_to LIKE ?");
555
+ params.push(`%${dir}%`);
556
+ }
557
+
558
+ // Se nao ha condicoes, buscar tudo (fallback)
559
+ const candidates = conditions.length > 0
560
+ ? db.query(`SELECT * FROM implementation_patterns WHERE ${conditions.join(' OR ')}`).all(...params) as any[]
561
+ : db.query("SELECT * FROM implementation_patterns").all() as any[];
562
+
563
+ // Regex match apenas nos candidatos pre-filtrados
564
+ return candidates.filter(pattern => {
408
565
  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
- });
566
+ const regexPattern = glob
567
+ .replace(/\*\*/g, '.*')
568
+ .replace(/\*/g, '[^/]*')
569
+ .replace(/\//g, '\\/');
570
+ const regex = new RegExp(`^${regexPattern}$`);
571
+ return files.some(file => regex.test(file));
419
572
  });
420
573
  }
421
574
 
@@ -478,22 +631,6 @@ export function getRelatedFiles(decisionOrPattern: string, type: "decision" | "p
478
631
  return results.map(r => r.file);
479
632
  }
480
633
 
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
634
  // ═══════════════════════════════════════════════════════════════
498
635
  // v8.0: Reasoning Log Helpers
499
636
  // ═══════════════════════════════════════════════════════════════
@@ -539,68 +676,6 @@ export function getRecentReasoning(specId: string, limit: number = 20): any[] {
539
676
  `).all(specId, limit) as any[];
540
677
  }
541
678
 
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
679
  // v8.4: Busca analise arquitetural associada a um spec
605
680
  // Prioridade: analysis_id (link explicito) > nome exato > LIKE parcial
606
681
  export function getArchitecturalAnalysisForSpec(specName: string, specId?: string): any {
@@ -724,3 +799,82 @@ export function findDuplicateUtilities(
724
799
  "SELECT * FROM project_utilities WHERE utility_name = ?"
725
800
  ).all(utilityName) as any[];
726
801
  }
802
+
803
+ // ═══════════════════════════════════════════════════════════════
804
+ // v9.3: Agent Performance Tracking (Feedback Loop)
805
+ // ═══════════════════════════════════════════════════════════════
806
+
807
+ export interface AgentPerformanceData {
808
+ agentType: string;
809
+ specId: string;
810
+ taskId: number;
811
+ gatesPassedFirstTry: number;
812
+ gatesTotal: number;
813
+ bypassesUsed: number;
814
+ filesCreated: number;
815
+ filesModified: number;
816
+ contextSizeBytes: number;
817
+ executionDurationMs: number;
818
+ }
819
+
820
+ export function recordAgentPerformance(data: AgentPerformanceData): void {
821
+ const db = getDb();
822
+ const now = new Date().toISOString();
823
+ db.run(
824
+ `INSERT INTO agent_performance
825
+ (agent_type, spec_id, task_id, gates_passed_first_try, gates_total, bypasses_used, files_created, files_modified, context_size_bytes, execution_duration_ms, created_at)
826
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
827
+ [
828
+ data.agentType, data.specId, data.taskId,
829
+ data.gatesPassedFirstTry, data.gatesTotal, data.bypassesUsed,
830
+ data.filesCreated, data.filesModified,
831
+ data.contextSizeBytes, data.executionDurationMs, now,
832
+ ]
833
+ );
834
+ }
835
+
836
+ export function getAgentHints(agentType: string, limit: number = 5): string[] {
837
+ const db = getDb();
838
+ const hints: string[] = [];
839
+
840
+ try {
841
+ const recent = db.query(
842
+ `SELECT * FROM agent_performance
843
+ WHERE agent_type = ?
844
+ ORDER BY created_at DESC LIMIT ?`
845
+ ).all(agentType, limit) as any[];
846
+
847
+ if (recent.length === 0) return [];
848
+
849
+ const avgBypass = recent.reduce((sum: number, r: any) => sum + r.bypasses_used, 0) / recent.length;
850
+ const avgGateRate = recent.reduce((sum: number, r: any) => {
851
+ return sum + (r.gates_total > 0 ? r.gates_passed_first_try / r.gates_total : 1);
852
+ }, 0) / recent.length;
853
+
854
+ if (avgBypass > 0.5) {
855
+ hints.push(`ATENCAO: Este agente usa bypasses frequentemente (media ${avgBypass.toFixed(1)}/task). Revise standards antes de iniciar.`);
856
+ }
857
+
858
+ if (avgGateRate < 0.7) {
859
+ hints.push(`ATENCAO: Gate pass rate baixo (${(avgGateRate * 100).toFixed(0)}%). Verifique standards e DRY obrigatorios.`);
860
+ }
861
+
862
+ const bypassTypes = db.query(
863
+ `SELECT gb.gate_name, COUNT(*) as cnt FROM gate_bypasses gb
864
+ JOIN tasks t ON gb.task_id = t.id
865
+ WHERE t.agent = ?
866
+ GROUP BY gb.gate_name
867
+ ORDER BY cnt DESC LIMIT 3`
868
+ ).all(agentType) as any[];
869
+
870
+ for (const bp of bypassTypes) {
871
+ if (bp.cnt >= 2) {
872
+ hints.push(`Gate '${bp.gate_name}' frequentemente ignorado (${bp.cnt}x). Preste atencao especial.`);
873
+ }
874
+ }
875
+ } catch {
876
+ // Tabela pode nao existir ainda
877
+ }
878
+
879
+ return hints;
880
+ }