@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.
@@ -11,6 +11,7 @@ import {
11
11
  getDetailedTechnologies,
12
12
  type UnifiedDetectionResult,
13
13
  } from "../detectors/loader";
14
+ import { CodexaError } from "../errors";
14
15
 
15
16
  interface StackDetection {
16
17
  frontend?: string;
@@ -172,9 +173,7 @@ export function discoverConfirm(): void {
172
173
 
173
174
  const pending = db.query("SELECT * FROM project WHERE id = 'pending'").get() as any;
174
175
  if (!pending) {
175
- console.error("\nNenhuma descoberta pendente.");
176
- console.error("Execute: discover start primeiro\n");
177
- process.exit(1);
176
+ throw new CodexaError("Nenhuma descoberta pendente.\nExecute: discover start primeiro");
178
177
  }
179
178
 
180
179
  const data = JSON.parse(pending.stack);
@@ -183,9 +182,9 @@ export function discoverConfirm(): void {
183
182
  // Mover de pending para default
184
183
  db.run("DELETE FROM project WHERE id = 'pending'");
185
184
  db.run(
186
- `INSERT INTO project (id, name, stack, discovered_at, updated_at, cli_version)
187
- VALUES ('default', ?, ?, ?, ?, ?)`,
188
- ["Projeto", JSON.stringify(data.stack), now, now, pkg.version]
185
+ `INSERT INTO project (id, name, stack, discovered_at, updated_at, cli_version, last_discover_at)
186
+ VALUES ('default', ?, ?, ?, ?, ?, ?)`,
187
+ ["Projeto", JSON.stringify(data.stack), now, now, pkg.version, now]
189
188
  );
190
189
 
191
190
  // Criar standards baseados na estrutura detectada
@@ -283,9 +282,7 @@ export function discoverShow(json: boolean = false): void {
283
282
  const project = db.query("SELECT * FROM project WHERE id = 'default'").get() as any;
284
283
 
285
284
  if (!project) {
286
- console.error("\nProjeto nao descoberto.");
287
- console.error("Execute: discover start\n");
288
- process.exit(1);
285
+ throw new CodexaError("Projeto nao descoberto.\nExecute: discover start");
289
286
  }
290
287
 
291
288
  const standards = db.query("SELECT * FROM standards ORDER BY category, scope").all() as any[];
@@ -381,9 +378,7 @@ export async function discoverRefresh(options: { force?: boolean } = {}): Promis
381
378
 
382
379
  const currentProject = db.query("SELECT * FROM project WHERE id = 'default'").get() as any;
383
380
  if (!currentProject) {
384
- console.error("\nProjeto nao descoberto.");
385
- console.error("Execute: discover start primeiro\n");
386
- process.exit(1);
381
+ throw new CodexaError("Projeto nao descoberto.\nExecute: discover start primeiro");
387
382
  }
388
383
 
389
384
  const currentStack = JSON.parse(currentProject.stack);
@@ -460,6 +455,129 @@ export async function discoverRefresh(options: { force?: boolean } = {}): Promis
460
455
  }
461
456
  }
462
457
 
458
+ export async function discoverIncremental(): Promise<void> {
459
+ initSchema();
460
+ const db = getDb();
461
+
462
+ const project = db.query("SELECT * FROM project WHERE id = 'default'").get() as any;
463
+ if (!project) {
464
+ throw new CodexaError("Projeto nao descoberto.\nExecute: discover start primeiro");
465
+ }
466
+
467
+ // Data de referencia: last_discover_at > discovered_at > fallback 30 dias
468
+ const sinceDate = project.last_discover_at || project.discovered_at ||
469
+ new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString();
470
+
471
+ console.log(`\nDiscover incremental desde: ${sinceDate}\n`);
472
+
473
+ // Listar arquivos modificados via git
474
+ const gitResult = spawnSync("git", [
475
+ "diff", "--name-only", "--diff-filter=ACMR",
476
+ `--since=${sinceDate}`, "HEAD"
477
+ ], { encoding: "utf-8", timeout: 10000 });
478
+
479
+ // Fallback: se --since nao funciona com diff, usar log
480
+ let modifiedFiles: string[] = [];
481
+ if (gitResult.status !== 0 || !gitResult.stdout.trim()) {
482
+ const logResult = spawnSync("git", [
483
+ "log", "--since", sinceDate, "--name-only", "--pretty=format:", "--diff-filter=ACMR"
484
+ ], { encoding: "utf-8", timeout: 10000 });
485
+
486
+ if (logResult.status === 0 && logResult.stdout.trim()) {
487
+ modifiedFiles = [...new Set(logResult.stdout.trim().split("\n").filter(f => f.trim()))];
488
+ }
489
+ } else {
490
+ modifiedFiles = [...new Set(gitResult.stdout.trim().split("\n").filter(f => f.trim()))];
491
+ }
492
+
493
+ // Filtrar apenas arquivos que ainda existem e sao codigo
494
+ const codeExtensions = [".ts", ".tsx", ".js", ".jsx", ".py", ".go", ".cs", ".dart"];
495
+ const existingFiles = modifiedFiles.filter(f => {
496
+ const fullPath = join(process.cwd(), f);
497
+ return existsSync(fullPath) && codeExtensions.some(ext => f.endsWith(ext));
498
+ });
499
+
500
+ if (existingFiles.length === 0) {
501
+ console.log("Nenhum arquivo de codigo modificado desde ultimo discover.");
502
+ console.log("Stack e utilities estao atualizados.\n");
503
+
504
+ // Atualizar timestamp mesmo sem mudancas
505
+ const now = new Date().toISOString();
506
+ db.run("UPDATE project SET last_discover_at = ? WHERE id = 'default'", [now]);
507
+ return;
508
+ }
509
+
510
+ console.log(`[1/3] Analisando ${existingFiles.length} arquivo(s) modificado(s)...`);
511
+
512
+ // Importar funcoes de patterns
513
+ const { extractUtilitiesFromFile, inferScopeFromPath } = await import("./patterns");
514
+ const { upsertUtility } = await import("../db/schema");
515
+
516
+ // Extrair utilities dos arquivos modificados
517
+ let utilitiesUpdated = 0;
518
+ for (const file of existingFiles) {
519
+ const fullPath = join(process.cwd(), file);
520
+ const utilities = extractUtilitiesFromFile(fullPath);
521
+ const scope = inferScopeFromPath(file);
522
+
523
+ for (const util of utilities) {
524
+ upsertUtility({
525
+ filePath: file,
526
+ utilityName: util.name,
527
+ utilityType: util.type,
528
+ scope,
529
+ signature: util.signature,
530
+ });
531
+ utilitiesUpdated++;
532
+ }
533
+ }
534
+
535
+ console.log(` ✓ ${utilitiesUpdated} utilities registradas/atualizadas`);
536
+
537
+ // Re-detectar stack e comparar
538
+ console.log("\n[2/3] Verificando mudancas no stack...");
539
+ const currentStack = JSON.parse(project.stack);
540
+ const newStack = await detectStack();
541
+
542
+ const stackChanges: { key: string; from: string | undefined; to: string | undefined }[] = [];
543
+ const allKeys = new Set([...Object.keys(currentStack), ...Object.keys(newStack)]);
544
+
545
+ for (const key of allKeys) {
546
+ const current = currentStack[key as keyof StackDetection];
547
+ const detected = newStack[key as keyof StackDetection];
548
+ if (current !== detected) {
549
+ stackChanges.push({ key, from: current, to: detected });
550
+ }
551
+ }
552
+
553
+ if (stackChanges.length > 0) {
554
+ console.log(" ⚠ Mudancas detectadas no stack:");
555
+ for (const change of stackChanges) {
556
+ const from = change.from || "(nao definido)";
557
+ const to = change.to || "(removido)";
558
+ console.log(` ${change.key}: ${from} → ${to}`);
559
+ }
560
+ console.log(" Use: discover refresh --force para aplicar");
561
+ } else {
562
+ console.log(" ✓ Stack inalterado");
563
+ }
564
+
565
+ // Atualizar timestamp
566
+ console.log("\n[3/3] Atualizando timestamp...");
567
+ const now = new Date().toISOString();
568
+ db.run("UPDATE project SET last_discover_at = ?, updated_at = ? WHERE id = 'default'", [now, now]);
569
+
570
+ // Resumo
571
+ console.log("\n" + "═".repeat(50));
572
+ console.log("DISCOVER INCREMENTAL CONCLUIDO");
573
+ console.log("═".repeat(50));
574
+ console.log(` Arquivos analisados: ${existingFiles.length}`);
575
+ console.log(` Utilities atualizadas: ${utilitiesUpdated}`);
576
+ console.log(` Mudancas no stack: ${stackChanges.length}`);
577
+ console.log(` Timestamp atualizado: ${now}`);
578
+ console.log("═".repeat(50) + "\n");
579
+ }
580
+
463
581
  function generateStandardsMarkdown(): void {
464
582
  const db = getDb();
465
583
  const project = db.query("SELECT * FROM project WHERE id = 'default'").get() as any;
@@ -649,9 +767,7 @@ export function discoverPatternsShow(name: string, json: boolean = false): void
649
767
  const pattern = db.query("SELECT * FROM implementation_patterns WHERE name = ?").get(name) as any;
650
768
 
651
769
  if (!pattern) {
652
- console.error(`\nPattern '${name}' nao encontrado.`);
653
- console.error("Use: discover patterns para listar todos\n");
654
- process.exit(1);
770
+ throw new CodexaError(`Pattern '${name}' nao encontrado.\nUse: discover patterns para listar todos`);
655
771
  }
656
772
 
657
773
  const structure = JSON.parse(pattern.structure || "{}");
@@ -751,9 +867,7 @@ export function discoverPatternAdd(options: {
751
867
  // Verificar se pattern ja existe
752
868
  const existing = db.query("SELECT id FROM implementation_patterns WHERE name = ?").get(options.name);
753
869
  if (existing) {
754
- console.error(`\nPattern '${options.name}' ja existe.`);
755
- console.error("Use: discover pattern-edit para modificar\n");
756
- process.exit(1);
870
+ throw new CodexaError(`Pattern '${options.name}' ja existe.\nUse: discover pattern-edit para modificar`);
757
871
  }
758
872
 
759
873
  const now = new Date().toISOString();
@@ -800,9 +914,7 @@ export function discoverPatternEdit(name: string, options: {
800
914
 
801
915
  const existing = db.query("SELECT * FROM implementation_patterns WHERE name = ?").get(name) as any;
802
916
  if (!existing) {
803
- console.error(`\nPattern '${name}' nao encontrado.`);
804
- console.error("Use: discover patterns para listar todos\n");
805
- process.exit(1);
917
+ throw new CodexaError(`Pattern '${name}' nao encontrado.\nUse: discover patterns para listar todos`);
806
918
  }
807
919
 
808
920
  const updates: string[] = [];
@@ -842,8 +954,7 @@ export function discoverPatternEdit(name: string, options: {
842
954
  }
843
955
 
844
956
  if (updates.length === 0) {
845
- console.error("\nNenhuma opcao de atualizacao fornecida.\n");
846
- process.exit(1);
957
+ throw new CodexaError("Nenhuma opcao de atualizacao fornecida.");
847
958
  }
848
959
 
849
960
  updates.push("updated_at = ?");
@@ -868,8 +979,7 @@ export function discoverPatternRemove(name: string): void {
868
979
 
869
980
  const existing = db.query("SELECT id FROM implementation_patterns WHERE name = ?").get(name);
870
981
  if (!existing) {
871
- console.error(`\nPattern '${name}' nao encontrado.\n`);
872
- process.exit(1);
982
+ throw new CodexaError(`Pattern '${name}' nao encontrado.`);
873
983
  }
874
984
 
875
985
  db.run("DELETE FROM implementation_patterns WHERE name = ?", [name]);
@@ -888,9 +998,7 @@ export function discoverRefreshPatterns(): void {
888
998
  // Verificar se projeto foi descoberto
889
999
  const project = db.query("SELECT * FROM project WHERE id = 'default'").get() as any;
890
1000
  if (!project) {
891
- console.error("\nProjeto nao descoberto.");
892
- console.error("Execute: discover start primeiro\n");
893
- process.exit(1);
1001
+ throw new CodexaError("Projeto nao descoberto.\nExecute: discover start primeiro");
894
1002
  }
895
1003
 
896
1004
  console.log("\n⚠ Refresh de patterns requer analise manual do codigo.");
@@ -0,0 +1,160 @@
1
+ import { describe, it, expect, beforeEach } from "bun:test";
2
+ import { jaccardSimilarity, compactKnowledge } from "./knowledge";
3
+ import { getDb } from "../db/connection";
4
+ import { initSchema } from "../db/schema";
5
+
6
+ describe("jaccardSimilarity", () => {
7
+ it("returns 1 for identical strings", () => {
8
+ expect(jaccardSimilarity("hello world test", "hello world test")).toBe(1);
9
+ });
10
+
11
+ it("returns 0 for completely different strings", () => {
12
+ expect(jaccardSimilarity("apple banana cherry", "delta echo foxtrot")).toBe(0);
13
+ });
14
+
15
+ it("returns high similarity for near-duplicates", () => {
16
+ const sim = jaccardSimilarity(
17
+ "Implemented user authentication with JWT tokens",
18
+ "Implemented user authentication using JWT tokens"
19
+ );
20
+ expect(sim).toBeGreaterThan(0.7);
21
+ });
22
+
23
+ it("returns 1 for two empty strings", () => {
24
+ expect(jaccardSimilarity("", "")).toBe(1);
25
+ });
26
+
27
+ it("returns 0 when one string is empty", () => {
28
+ expect(jaccardSimilarity("hello world test", "")).toBe(0);
29
+ });
30
+
31
+ it("is case insensitive", () => {
32
+ expect(jaccardSimilarity("Hello World Test", "hello world test")).toBe(1);
33
+ });
34
+
35
+ it("ignores short words (<=2 chars)", () => {
36
+ // "a" and "is" are filtered out, only "cat" and "dog" matter
37
+ const sim = jaccardSimilarity("a cat is here", "a dog is here");
38
+ // "cat" vs "dog" + "here" in common
39
+ expect(sim).toBeGreaterThan(0);
40
+ expect(sim).toBeLessThan(1);
41
+ });
42
+
43
+ it("strips punctuation", () => {
44
+ const sim = jaccardSimilarity("hello, world! test.", "hello world test");
45
+ expect(sim).toBe(1);
46
+ });
47
+ });
48
+
49
+ describe("compactKnowledge", () => {
50
+ beforeEach(() => {
51
+ initSchema();
52
+ const db = getDb();
53
+ db.run("DELETE FROM knowledge_graph");
54
+ db.run("DELETE FROM knowledge");
55
+ db.run("DELETE FROM tasks");
56
+ db.run("DELETE FROM context");
57
+ db.run("DELETE FROM specs");
58
+ });
59
+
60
+ function createSpec(id: string, phase: string) {
61
+ const db = getDb();
62
+ const now = new Date().toISOString();
63
+ db.run(
64
+ "INSERT INTO specs (id, name, phase, created_at, updated_at) VALUES (?, ?, ?, ?, ?)",
65
+ [id, `Feature ${id}`, phase, now, now]
66
+ );
67
+ }
68
+
69
+ function addKnowledgeEntry(specId: string, content: string, category: string, severity: string, createdAt?: string) {
70
+ const db = getDb();
71
+ const now = createdAt || new Date().toISOString();
72
+ db.run(
73
+ `INSERT INTO knowledge (spec_id, task_origin, category, content, severity, broadcast_to, created_at)
74
+ VALUES (?, 0, ?, ?, ?, 'all', ?)`,
75
+ [specId, category, content, severity, now]
76
+ );
77
+ }
78
+
79
+ it("merges similar entries in same category and spec", () => {
80
+ createSpec("spec-a", "implementing");
81
+ addKnowledgeEntry("spec-a", "Implemented user authentication with JWT tokens for the login flow", "discovery", "info");
82
+ addKnowledgeEntry("spec-a", "Implemented user authentication with JWT tokens for login flow", "discovery", "info");
83
+
84
+ // Dry run first
85
+ compactKnowledge({ dryRun: true, json: true });
86
+
87
+ // Actual compact
88
+ compactKnowledge({ json: true });
89
+
90
+ const db = getDb();
91
+ const archived = db.query("SELECT COUNT(*) as c FROM knowledge WHERE severity = 'archived'").get() as any;
92
+ expect(archived.c).toBe(1);
93
+ });
94
+
95
+ it("keeps higher severity entry when merging", () => {
96
+ createSpec("spec-a", "implementing");
97
+ addKnowledgeEntry("spec-a", "Database connection timeout causing failures in production", "blocker", "critical");
98
+ addKnowledgeEntry("spec-a", "Database connection timeout causing failures in production system", "blocker", "info");
99
+
100
+ compactKnowledge({ json: true });
101
+
102
+ const db = getDb();
103
+ const remaining = db.query("SELECT * FROM knowledge WHERE severity != 'archived'").all() as any[];
104
+ expect(remaining.length).toBe(1);
105
+ expect(remaining[0].severity).toBe("critical");
106
+ });
107
+
108
+ it("archives old info entries (>7 days)", () => {
109
+ createSpec("spec-a", "implementing");
110
+ const oldDate = new Date(Date.now() - 10 * 24 * 60 * 60 * 1000).toISOString();
111
+ addKnowledgeEntry("spec-a", "Some old discovery that is not referenced anywhere in the graph", "discovery", "info", oldDate);
112
+ addKnowledgeEntry("spec-a", "A recent discovery that should not be archived by this compaction", "discovery", "info");
113
+
114
+ compactKnowledge({ json: true });
115
+
116
+ const db = getDb();
117
+ const archived = db.query("SELECT COUNT(*) as c FROM knowledge WHERE severity = 'archived'").get() as any;
118
+ expect(archived.c).toBe(1);
119
+ });
120
+
121
+ it("archives entries from completed specs", () => {
122
+ createSpec("spec-done", "completed");
123
+ addKnowledgeEntry("spec-done", "Something learned during completed feature implementation", "discovery", "info");
124
+ addKnowledgeEntry("spec-done", "A critical blocker that was found during the implementation phase", "blocker", "critical");
125
+
126
+ compactKnowledge({ json: true });
127
+
128
+ const db = getDb();
129
+ const archived = db.query("SELECT COUNT(*) as c FROM knowledge WHERE severity = 'archived'").get() as any;
130
+ expect(archived.c).toBe(2);
131
+ });
132
+
133
+ it("dry-run mode does not modify DB", () => {
134
+ createSpec("spec-done", "completed");
135
+ addKnowledgeEntry("spec-done", "Something that would be archived in a real compaction run", "discovery", "info");
136
+
137
+ compactKnowledge({ dryRun: true, json: true });
138
+
139
+ const db = getDb();
140
+ const archived = db.query("SELECT COUNT(*) as c FROM knowledge WHERE severity = 'archived'").get() as any;
141
+ expect(archived.c).toBe(0);
142
+ });
143
+
144
+ it("respects --spec filter", () => {
145
+ createSpec("spec-a", "implementing");
146
+ createSpec("spec-b", "implementing");
147
+ addKnowledgeEntry("spec-a", "Implemented user authentication with JWT tokens for the system", "discovery", "info");
148
+ addKnowledgeEntry("spec-a", "Implemented user authentication with JWT tokens for the system flow", "discovery", "info");
149
+ addKnowledgeEntry("spec-b", "Implemented user authentication with JWT tokens for the login", "discovery", "info");
150
+ addKnowledgeEntry("spec-b", "Implemented user authentication with JWT tokens for the login flow", "discovery", "info");
151
+
152
+ compactKnowledge({ specId: "spec-a", json: true });
153
+
154
+ const db = getDb();
155
+ const archivedA = db.query("SELECT COUNT(*) as c FROM knowledge WHERE severity = 'archived' AND spec_id = 'spec-a'").get() as any;
156
+ const archivedB = db.query("SELECT COUNT(*) as c FROM knowledge WHERE severity = 'archived' AND spec_id = 'spec-b'").get() as any;
157
+ expect(archivedA.c).toBe(1);
158
+ expect(archivedB.c).toBe(0);
159
+ });
160
+ });