@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.
@@ -1,5 +1,7 @@
1
1
  import { getDb } from "../db/connection";
2
- import { initSchema, getRelatedDecisions, getRelatedFiles, findContradictions } from "../db/schema";
2
+ import { initSchema, getRelatedDecisions, getRelatedFiles } from "../db/schema";
3
+ import { resolveSpec } from "./spec-resolver";
4
+ import { CodexaError, ValidationError } from "../errors";
3
5
 
4
6
  type KnowledgeCategory = "discovery" | "decision" | "blocker" | "pattern" | "constraint";
5
7
  type KnowledgeSeverity = "info" | "warning" | "critical";
@@ -9,13 +11,7 @@ interface AddKnowledgeOptions {
9
11
  category: KnowledgeCategory;
10
12
  severity?: KnowledgeSeverity;
11
13
  broadcastTo?: string;
12
- }
13
-
14
- function getActiveSpec(): any {
15
- const db = getDb();
16
- return db
17
- .query("SELECT * FROM specs WHERE phase NOT IN ('completed', 'cancelled') ORDER BY created_at DESC LIMIT 1")
18
- .get();
14
+ specId?: string;
19
15
  }
20
16
 
21
17
  function getCurrentTask(specId: string): any {
@@ -31,24 +27,16 @@ export function addKnowledge(options: AddKnowledgeOptions): void {
31
27
  const db = getDb();
32
28
  const now = new Date().toISOString();
33
29
 
34
- const spec = getActiveSpec();
35
- if (!spec) {
36
- console.error("\nNenhuma feature ativa.\n");
37
- process.exit(1);
38
- }
30
+ const spec = resolveSpec(options.specId);
39
31
 
40
32
  const currentTask = getCurrentTask(spec.id);
41
33
  if (!currentTask) {
42
- console.error("\nNenhuma task em execucao.");
43
- console.error("Knowledge so pode ser adicionado durante execucao de uma task.\n");
44
- process.exit(1);
34
+ throw new CodexaError("Nenhuma task em execucao.\nKnowledge so pode ser adicionado durante execucao de uma task.");
45
35
  }
46
36
 
47
37
  const validCategories: KnowledgeCategory[] = ["discovery", "decision", "blocker", "pattern", "constraint"];
48
38
  if (!validCategories.includes(options.category)) {
49
- console.error(`\nCategoria invalida: ${options.category}`);
50
- console.error(`Validas: ${validCategories.join(", ")}\n`);
51
- process.exit(1);
39
+ throw new ValidationError(`Categoria invalida: ${options.category}\nValidas: ${validCategories.join(", ")}`);
52
40
  }
53
41
 
54
42
  const severity = options.severity || "info";
@@ -79,20 +67,13 @@ export function listKnowledge(options: {
79
67
  category?: string;
80
68
  severity?: string; // v8.0: Filtro por severidade
81
69
  json?: boolean;
70
+ specId?: string;
82
71
  }): void {
83
72
  initSchema();
84
73
 
85
74
  const db = getDb();
86
75
 
87
- const spec = getActiveSpec();
88
- if (!spec) {
89
- if (options.json) {
90
- console.log(JSON.stringify({ knowledge: [], message: "Nenhuma feature ativa" }));
91
- return;
92
- }
93
- console.error("\nNenhuma feature ativa.\n");
94
- process.exit(1);
95
- }
76
+ const spec = resolveSpec(options.specId);
96
77
 
97
78
  let query = "SELECT * FROM knowledge WHERE spec_id = ?";
98
79
  const params: any[] = [spec.id];
@@ -122,9 +103,10 @@ export function listKnowledge(options: {
122
103
  const currentTask = getCurrentTask(spec.id);
123
104
  if (currentTask) {
124
105
  filtered = knowledge.filter((k) => {
125
- if (!k.acknowledged_by) return true;
126
- const acked = JSON.parse(k.acknowledged_by) as number[];
127
- return !acked.includes(currentTask.id);
106
+ const isAcked = db.query(
107
+ "SELECT 1 FROM knowledge_acknowledgments WHERE knowledge_id = ? AND task_id = ?"
108
+ ).get(k.id, currentTask.id);
109
+ return !isAcked;
128
110
  });
129
111
  }
130
112
  }
@@ -158,42 +140,29 @@ export function listKnowledge(options: {
158
140
  }
159
141
  }
160
142
 
161
- export function acknowledgeKnowledge(knowledgeId: string): void {
143
+ export function acknowledgeKnowledge(knowledgeId: string, specId?: string): void {
162
144
  initSchema();
163
145
 
164
146
  const db = getDb();
165
147
 
166
- const spec = getActiveSpec();
167
- if (!spec) {
168
- console.error("\nNenhuma feature ativa.\n");
169
- process.exit(1);
170
- }
148
+ const spec = resolveSpec(specId);
171
149
 
172
150
  const currentTask = getCurrentTask(spec.id);
173
151
  if (!currentTask) {
174
- console.error("\nNenhuma task em execucao.\n");
175
- process.exit(1);
152
+ throw new CodexaError("Nenhuma task em execucao.");
176
153
  }
177
154
 
178
155
  const kid = parseInt(knowledgeId);
179
156
  const knowledge = db.query("SELECT * FROM knowledge WHERE id = ?").get(kid) as any;
180
157
 
181
158
  if (!knowledge) {
182
- console.error(`\nKnowledge #${kid} nao encontrado.\n`);
183
- process.exit(1);
159
+ throw new CodexaError(`Knowledge #${kid} nao encontrado.`);
184
160
  }
185
161
 
186
- const acknowledged = knowledge.acknowledged_by
187
- ? (JSON.parse(knowledge.acknowledged_by) as number[])
188
- : [];
189
-
190
- if (!acknowledged.includes(currentTask.id)) {
191
- acknowledged.push(currentTask.id);
192
- db.run("UPDATE knowledge SET acknowledged_by = ? WHERE id = ?", [
193
- JSON.stringify(acknowledged),
194
- kid,
195
- ]);
196
- }
162
+ db.run(
163
+ "INSERT OR IGNORE INTO knowledge_acknowledgments (knowledge_id, task_id) VALUES (?, ?)",
164
+ [kid, currentTask.id]
165
+ );
197
166
 
198
167
  console.log(`\nKnowledge #${kid} marcado como lido pela Task #${currentTask.number}.\n`);
199
168
  }
@@ -221,11 +190,13 @@ export function getKnowledgeForTask(specId: string, taskId: number): any[] {
221
190
 
222
191
  export function getUnreadKnowledgeForTask(specId: string, taskId: number): any[] {
223
192
  const all = getKnowledgeForTask(specId, taskId);
193
+ const db = getDb();
224
194
 
225
195
  return all.filter((k) => {
226
- if (!k.acknowledged_by) return true;
227
- const acked = JSON.parse(k.acknowledged_by) as number[];
228
- return !acked.includes(taskId);
196
+ const isAcked = db.query(
197
+ "SELECT 1 FROM knowledge_acknowledgments WHERE knowledge_id = ? AND task_id = ?"
198
+ ).get(k.id, taskId);
199
+ return !isAcked;
229
200
  });
230
201
  }
231
202
 
@@ -233,40 +204,13 @@ export function getUnreadKnowledgeForTask(specId: string, taskId: number): any[]
233
204
  export function queryGraph(options: {
234
205
  file?: string;
235
206
  decision?: string;
236
- contradictions?: boolean;
237
207
  json?: boolean;
208
+ specId?: string;
238
209
  }): void {
239
210
  initSchema();
240
211
  const db = getDb();
241
212
 
242
- const spec = getActiveSpec();
243
- if (!spec) {
244
- console.error("\nNenhuma feature ativa.\n");
245
- process.exit(1);
246
- }
247
-
248
- // Buscar contradicoes
249
- if (options.contradictions) {
250
- const contradictions = findContradictions(spec.id);
251
-
252
- if (options.json) {
253
- console.log(JSON.stringify({ contradictions }));
254
- return;
255
- }
256
-
257
- if (contradictions.length === 0) {
258
- console.log("\nNenhuma contradicao detectada.\n");
259
- return;
260
- }
261
-
262
- console.log(`\nContradicoes detectadas (${contradictions.length}):`);
263
- console.log(`${"─".repeat(50)}`);
264
- for (const c of contradictions) {
265
- console.log(` [!] "${c.decision1}" <-> "${c.decision2}"`);
266
- console.log(` Detectada em: ${c.createdAt}\n`);
267
- }
268
- return;
269
- }
213
+ const spec = resolveSpec(options.specId);
270
214
 
271
215
  // Buscar relacoes para um arquivo
272
216
  if (options.file) {
@@ -357,21 +301,175 @@ export function queryGraph(options: {
357
301
  }
358
302
  console.log(`\nComandos:`);
359
303
  console.log(` knowledge graph --file <path> Relacoes de um arquivo`);
360
- console.log(` knowledge graph --decision <id> Arquivos afetados por decisao`);
361
- console.log(` knowledge graph --contradictions Detectar contradicoes\n`);
304
+ console.log(` knowledge graph --decision <id> Arquivos afetados por decisao\n`);
305
+ }
306
+
307
+ // v9.2: Jaccard similarity for knowledge compaction
308
+ export function jaccardSimilarity(a: string, b: string): number {
309
+ const tokenize = (s: string): Set<string> => {
310
+ const words = s
311
+ .toLowerCase()
312
+ .replace(/[^\w\s-]/g, " ")
313
+ .split(/\s+/)
314
+ .filter((w) => w.length > 2);
315
+ return new Set(words);
316
+ };
317
+
318
+ const setA = tokenize(a);
319
+ const setB = tokenize(b);
320
+
321
+ if (setA.size === 0 && setB.size === 0) return 1;
322
+ if (setA.size === 0 || setB.size === 0) return 0;
323
+
324
+ let intersection = 0;
325
+ for (const word of setA) {
326
+ if (setB.has(word)) intersection++;
327
+ }
328
+
329
+ const union = setA.size + setB.size - intersection;
330
+ return union === 0 ? 0 : intersection / union;
331
+ }
332
+
333
+ // v9.2: Knowledge compaction — merge similar, archive old, archive completed
334
+ export function compactKnowledge(options: {
335
+ specId?: string;
336
+ dryRun?: boolean;
337
+ json?: boolean;
338
+ }): void {
339
+ initSchema();
340
+ const db = getDb();
341
+
342
+ let merged = 0;
343
+ let archivedOld = 0;
344
+ let archivedCompleted = 0;
345
+
346
+ // Phase 1: Merge similar entries (same category + spec_id, Jaccard >= 0.8)
347
+ const specFilter = options.specId
348
+ ? "AND spec_id = ?"
349
+ : "";
350
+ const specParams = options.specId ? [options.specId] : [];
351
+
352
+ const entries = db
353
+ .query(
354
+ `SELECT * FROM knowledge WHERE severity != 'archived' ${specFilter} ORDER BY category, spec_id, created_at DESC`
355
+ )
356
+ .all(...specParams) as any[];
357
+
358
+ // Group by category + spec_id
359
+ const groups = new Map<string, any[]>();
360
+ for (const entry of entries) {
361
+ const key = `${entry.category}:${entry.spec_id}`;
362
+ if (!groups.has(key)) groups.set(key, []);
363
+ groups.get(key)!.push(entry);
364
+ }
365
+
366
+ const toArchive = new Set<number>();
367
+
368
+ for (const [, group] of groups) {
369
+ for (let i = 0; i < group.length; i++) {
370
+ if (toArchive.has(group[i].id)) continue;
371
+ for (let j = i + 1; j < group.length; j++) {
372
+ if (toArchive.has(group[j].id)) continue;
373
+
374
+ const sim = jaccardSimilarity(group[i].content, group[j].content);
375
+ if (sim >= 0.8) {
376
+ // Keep higher severity or newer; archive the other
377
+ const severityOrder: Record<string, number> = { critical: 3, warning: 2, info: 1, archived: 0 };
378
+ const keepI = (severityOrder[group[i].severity] || 0) >= (severityOrder[group[j].severity] || 0);
379
+ const archiveId = keepI ? group[j].id : group[i].id;
380
+ toArchive.add(archiveId);
381
+ merged++;
382
+ }
383
+ }
384
+ }
385
+ }
386
+
387
+ // Phase 2: Archive old info entries (>7 days, no graph references)
388
+ const sevenDaysAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString();
389
+ const oldInfoEntries = db
390
+ .query(
391
+ `SELECT k.id FROM knowledge k
392
+ WHERE k.severity = 'info' AND k.created_at < ? ${specFilter}
393
+ AND NOT EXISTS (
394
+ SELECT 1 FROM knowledge_graph kg
395
+ WHERE kg.source_id = CAST(k.id AS TEXT) AND kg.source_type = 'knowledge'
396
+ )
397
+ AND NOT EXISTS (
398
+ SELECT 1 FROM knowledge_graph kg
399
+ WHERE kg.target_id = CAST(k.id AS TEXT) AND kg.target_type = 'knowledge'
400
+ )`
401
+ )
402
+ .all(sevenDaysAgo, ...specParams) as any[];
403
+
404
+ for (const entry of oldInfoEntries) {
405
+ if (!toArchive.has(entry.id)) {
406
+ toArchive.add(entry.id);
407
+ archivedOld++;
408
+ }
409
+ }
410
+
411
+ // Phase 3: Archive all entries from completed/cancelled specs
412
+ const completedEntries = db
413
+ .query(
414
+ `SELECT k.id FROM knowledge k
415
+ JOIN specs s ON k.spec_id = s.id
416
+ WHERE s.phase IN ('completed', 'cancelled')
417
+ AND k.severity != 'archived'
418
+ ${options.specId ? "AND k.spec_id = ?" : ""}`
419
+ )
420
+ .all(...specParams) as any[];
421
+
422
+ for (const entry of completedEntries) {
423
+ if (!toArchive.has(entry.id)) {
424
+ toArchive.add(entry.id);
425
+ archivedCompleted++;
426
+ }
427
+ }
428
+
429
+ // Apply changes
430
+ if (!options.dryRun && toArchive.size > 0) {
431
+ const ids = Array.from(toArchive);
432
+ const placeholders = ids.map(() => "?").join(",");
433
+ db.run(
434
+ `UPDATE knowledge SET severity = 'archived' WHERE id IN (${placeholders})`,
435
+ ids
436
+ );
437
+ }
438
+
439
+ // Output
440
+ const result = {
441
+ merged,
442
+ archivedOld,
443
+ archivedCompleted,
444
+ totalArchived: toArchive.size,
445
+ dryRun: !!options.dryRun,
446
+ };
447
+
448
+ if (options.json) {
449
+ console.log(JSON.stringify(result));
450
+ return;
451
+ }
452
+
453
+ const mode = options.dryRun ? "[DRY RUN] " : "";
454
+ console.log(`\n${mode}Knowledge Compaction:`);
455
+ console.log(`${"─".repeat(50)}`);
456
+ console.log(` Similares mesclados: ${merged}`);
457
+ console.log(` Info antigos (>7d): ${archivedOld}`);
458
+ console.log(` Specs finalizados: ${archivedCompleted}`);
459
+ console.log(` Total arquivado: ${toArchive.size}`);
460
+ if (options.dryRun) {
461
+ console.log(`\nPara aplicar: knowledge compact${options.specId ? ` --spec ${options.specId}` : ""}`);
462
+ }
463
+ console.log();
362
464
  }
363
465
 
364
466
  // v9.0: Resolver/reconhecer knowledge critico (para desbloquear task start)
365
- export function resolveKnowledge(ids: string, resolution?: string): void {
467
+ export function resolveKnowledge(ids: string, resolution?: string, specId?: string): void {
366
468
  initSchema();
367
469
  const db = getDb();
368
470
  const now = new Date().toISOString();
369
471
 
370
- const spec = getActiveSpec();
371
- if (!spec) {
372
- console.error("\nNenhuma feature ativa.\n");
373
- process.exit(1);
374
- }
472
+ const spec = resolveSpec(specId);
375
473
 
376
474
  const knowledgeIds = ids.split(",").map(s => parseInt(s.trim()));
377
475
 
@@ -383,18 +481,10 @@ export function resolveKnowledge(ids: string, resolution?: string): void {
383
481
  continue;
384
482
  }
385
483
 
386
- const acknowledged = knowledge.acknowledged_by
387
- ? (JSON.parse(knowledge.acknowledged_by) as number[])
388
- : [];
389
-
390
484
  // Usar -1 como marker de "resolvido pelo orquestrador"
391
- if (!acknowledged.includes(-1)) {
392
- acknowledged.push(-1);
393
- }
394
-
395
485
  db.run(
396
- "UPDATE knowledge SET acknowledged_by = ? WHERE id = ?",
397
- [JSON.stringify(acknowledged), kid]
486
+ "INSERT OR IGNORE INTO knowledge_acknowledgments (knowledge_id, task_id) VALUES (?, ?)",
487
+ [kid, -1]
398
488
  );
399
489
 
400
490
  console.log(`Knowledge #${kid} resolvido.`);
@@ -0,0 +1,169 @@
1
+ import { describe, it, expect, afterAll } from "bun:test";
2
+ import { extractUtilitiesFromFile, inferScopeFromPath } from "./patterns";
3
+ import { writeFileSync, mkdirSync, rmSync } from "fs";
4
+ import { join } from "path";
5
+
6
+ const TMP_DIR = join(import.meta.dir, "__test_tmp__");
7
+
8
+ function setupTmpDir() {
9
+ mkdirSync(TMP_DIR, { recursive: true });
10
+ }
11
+
12
+ function cleanTmpDir() {
13
+ try { rmSync(TMP_DIR, { recursive: true }); } catch {}
14
+ }
15
+
16
+ function writeTmpFile(name: string, content: string): string {
17
+ setupTmpDir();
18
+ const path = join(TMP_DIR, name);
19
+ writeFileSync(path, content);
20
+ return path;
21
+ }
22
+
23
+ describe("extractUtilitiesFromFile", () => {
24
+ afterAll(() => cleanTmpDir());
25
+
26
+ it("extracts exported function", () => {
27
+ const path = writeTmpFile("func.ts", 'export function createUser(name: string): User {\n return { name };\n}');
28
+ const utils = extractUtilitiesFromFile(path);
29
+ expect(utils).toHaveLength(1);
30
+ expect(utils[0].name).toBe("createUser");
31
+ expect(utils[0].type).toBe("function");
32
+ expect(utils[0].signature).toContain("(name: string)");
33
+ });
34
+
35
+ it("extracts exported async function", () => {
36
+ const path = writeTmpFile("async.ts", 'export async function fetchData(url: string): Promise<Data> {\n return await fetch(url);\n}');
37
+ const utils = extractUtilitiesFromFile(path);
38
+ expect(utils).toHaveLength(1);
39
+ expect(utils[0].name).toBe("fetchData");
40
+ expect(utils[0].type).toBe("function");
41
+ });
42
+
43
+ it("extracts exported const", () => {
44
+ const path = writeTmpFile("const.ts", 'export const MAX_RETRIES: number = 3;');
45
+ const utils = extractUtilitiesFromFile(path);
46
+ expect(utils).toHaveLength(1);
47
+ expect(utils[0].name).toBe("MAX_RETRIES");
48
+ expect(utils[0].type).toBe("const");
49
+ expect(utils[0].signature).toBe("number");
50
+ });
51
+
52
+ it("extracts exported const without type annotation", () => {
53
+ const path = writeTmpFile("const2.ts", 'export const config = { port: 3000 };');
54
+ const utils = extractUtilitiesFromFile(path);
55
+ expect(utils).toHaveLength(1);
56
+ expect(utils[0].name).toBe("config");
57
+ expect(utils[0].type).toBe("const");
58
+ });
59
+
60
+ it("extracts exported class", () => {
61
+ const path = writeTmpFile("class.ts", 'export class UserService {\n constructor() {}\n}');
62
+ const utils = extractUtilitiesFromFile(path);
63
+ expect(utils).toHaveLength(1);
64
+ expect(utils[0].name).toBe("UserService");
65
+ expect(utils[0].type).toBe("class");
66
+ });
67
+
68
+ it("extracts exported interface", () => {
69
+ const path = writeTmpFile("iface.ts", 'export interface UserProps {\n name: string;\n}');
70
+ const utils = extractUtilitiesFromFile(path);
71
+ expect(utils).toHaveLength(1);
72
+ expect(utils[0].name).toBe("UserProps");
73
+ expect(utils[0].type).toBe("interface");
74
+ });
75
+
76
+ it("extracts exported type", () => {
77
+ const path = writeTmpFile("type.ts", 'export type Status = "active" | "inactive";');
78
+ const utils = extractUtilitiesFromFile(path);
79
+ expect(utils).toHaveLength(1);
80
+ expect(utils[0].name).toBe("Status");
81
+ expect(utils[0].type).toBe("type");
82
+ });
83
+
84
+ it("extracts multiple exports from one file", () => {
85
+ const path = writeTmpFile("multi.ts", [
86
+ 'export function foo() {}',
87
+ 'export const BAR = 1;',
88
+ 'export class Baz {}',
89
+ 'export interface Qux {}',
90
+ 'export type Quux = string;',
91
+ ].join("\n"));
92
+ const utils = extractUtilitiesFromFile(path);
93
+ expect(utils).toHaveLength(5);
94
+ expect(utils.map(u => u.name)).toEqual(["foo", "BAR", "Baz", "Qux", "Quux"]);
95
+ });
96
+
97
+ it("returns empty array for non-existent file", () => {
98
+ const utils = extractUtilitiesFromFile("/nonexistent/path.ts");
99
+ expect(utils).toEqual([]);
100
+ });
101
+
102
+ it("returns empty array for file without exports", () => {
103
+ const path = writeTmpFile("noexport.ts", 'const internal = 1;\nfunction helper() {}');
104
+ const utils = extractUtilitiesFromFile(path);
105
+ expect(utils).toEqual([]);
106
+ });
107
+
108
+ it("ignores non-export declarations", () => {
109
+ const path = writeTmpFile("mixed.ts", [
110
+ 'const private1 = 1;',
111
+ 'function private2() {}',
112
+ 'export function public1() {}',
113
+ ].join("\n"));
114
+ const utils = extractUtilitiesFromFile(path);
115
+ expect(utils).toHaveLength(1);
116
+ expect(utils[0].name).toBe("public1");
117
+ });
118
+ });
119
+
120
+ describe("inferScopeFromPath", () => {
121
+ it("identifies backend paths", () => {
122
+ expect(inferScopeFromPath("src/app/api/users/route.ts")).toBe("backend");
123
+ expect(inferScopeFromPath("src/server/index.ts")).toBe("backend");
124
+ expect(inferScopeFromPath("src/backend/auth.ts")).toBe("backend");
125
+ expect(inferScopeFromPath("src/services/user.ts")).toBe("backend");
126
+ expect(inferScopeFromPath("src/actions/submit.ts")).toBe("backend");
127
+ });
128
+
129
+ it("identifies frontend paths", () => {
130
+ expect(inferScopeFromPath("src/components/Button.tsx")).toBe("frontend");
131
+ expect(inferScopeFromPath("src/hooks/useAuth.ts")).toBe("frontend");
132
+ expect(inferScopeFromPath("src/pages/index.tsx")).toBe("frontend");
133
+ expect(inferScopeFromPath("src/frontend/App.tsx")).toBe("frontend");
134
+ });
135
+
136
+ it("identifies frontend for /app/ paths without /api/", () => {
137
+ expect(inferScopeFromPath("src/app/dashboard/page.tsx")).toBe("frontend");
138
+ });
139
+
140
+ it("identifies database paths", () => {
141
+ expect(inferScopeFromPath("src/db/schema.ts")).toBe("database");
142
+ expect(inferScopeFromPath("src/schema/users.ts")).toBe("database");
143
+ expect(inferScopeFromPath("src/migrations/001.sql")).toBe("database");
144
+ expect(inferScopeFromPath("src/database/connection.ts")).toBe("database");
145
+ });
146
+
147
+ it("identifies testing paths", () => {
148
+ expect(inferScopeFromPath("src/tests/user.test.ts")).toBe("testing");
149
+ // Note: /components/ matches frontend before .test. matches testing
150
+ expect(inferScopeFromPath("src/components/Button.test.tsx")).toBe("frontend");
151
+ expect(inferScopeFromPath("src/lib/auth.spec.ts")).toBe("testing");
152
+ });
153
+
154
+ it("returns shared for unrecognized paths", () => {
155
+ expect(inferScopeFromPath("src/lib/utils.ts")).toBe("shared");
156
+ expect(inferScopeFromPath("src/config/env.ts")).toBe("shared");
157
+ expect(inferScopeFromPath("package.json")).toBe("shared");
158
+ });
159
+
160
+ it("handles Windows-style paths (backslashes)", () => {
161
+ expect(inferScopeFromPath("src\\components\\Button.tsx")).toBe("frontend");
162
+ expect(inferScopeFromPath("src\\db\\schema.ts")).toBe("database");
163
+ });
164
+
165
+ it("is case-insensitive", () => {
166
+ expect(inferScopeFromPath("src/Components/Button.tsx")).toBe("frontend");
167
+ expect(inferScopeFromPath("src/DB/Schema.ts")).toBe("database");
168
+ });
169
+ });
@@ -8,6 +8,7 @@
8
8
  import { execSync, spawnSync } from "child_process";
9
9
  import { readFileSync, existsSync } from "fs";
10
10
  import { extname, basename, dirname } from "path";
11
+ import { CodexaError, ValidationError } from "../errors";
11
12
  import { getDb } from "../db/connection";
12
13
  import { initSchema } from "../db/schema";
13
14
 
@@ -601,10 +602,7 @@ export function patternsExtract(options: ExtractOptions): void {
601
602
 
602
603
  // Verificar grepai
603
604
  if (!isGrepaiAvailable()) {
604
- console.error("\n[ERRO] grepai nao encontrado.");
605
- console.error("Instale com: go install github.com/your-org/grepai@latest");
606
- console.error("Ou configure no PATH.\n");
607
- process.exit(1);
605
+ throw new CodexaError("grepai nao encontrado.\nInstale com: go install github.com/your-org/grepai@latest\nOu configure no PATH.");
608
606
  }
609
607
 
610
608
  const db = getDb();
@@ -617,9 +615,7 @@ export function patternsExtract(options: ExtractOptions): void {
617
615
  }
618
616
 
619
617
  if (queries.length === 0) {
620
- console.error(`\n[ERRO] Escopo '${options.scope}' nao reconhecido.`);
621
- console.error("Escopos validos: frontend, backend, database, testing\n");
622
- process.exit(1);
618
+ throw new ValidationError("Escopo '" + options.scope + "' nao reconhecido.\nEscopos validos: frontend, backend, database, testing");
623
619
  }
624
620
 
625
621
  console.log(`\nšŸ” Extraindo patterns via grepai...`);
@@ -737,15 +733,13 @@ export function patternsExtract(options: ExtractOptions): void {
737
733
 
738
734
  export function patternsAnalyze(filePath: string, json: boolean = false): void {
739
735
  if (!existsSync(filePath)) {
740
- console.error(`\n[ERRO] Arquivo nao encontrado: ${filePath}\n`);
741
- process.exit(1);
736
+ throw new CodexaError("Arquivo nao encontrado: " + filePath);
742
737
  }
743
738
 
744
739
  const analysis = analyzeFile(filePath);
745
740
 
746
741
  if (!analysis) {
747
- console.error(`\n[ERRO] Nao foi possivel analisar o arquivo.\n`);
748
- process.exit(1);
742
+ throw new CodexaError("Nao foi possivel analisar o arquivo.");
749
743
  }
750
744
 
751
745
  if (json) {
@@ -855,8 +849,7 @@ export function patternsAnalyzeDeep(files: string[], json: boolean = false): voi
855
849
  }
856
850
 
857
851
  if (analyses.length === 0) {
858
- console.error("\nNenhum arquivo analisado.\n");
859
- process.exit(1);
852
+ throw new CodexaError("Nenhum arquivo analisado.");
860
853
  }
861
854
 
862
855
  const summary = {