@codexa/cli 8.6.0 → 8.6.9

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,249 +1,249 @@
1
- import { getDb } from "../db/connection";
2
- import { initSchema } from "../db/schema";
3
-
4
- function getNextDecisionId(specId: string): string {
5
- const db = getDb();
6
- const last = db
7
- .query("SELECT id FROM decisions WHERE spec_id = ? ORDER BY created_at DESC LIMIT 1")
8
- .get(specId) as any;
9
-
10
- if (!last) return "DEC-001";
11
-
12
- const num = parseInt(last.id.replace("DEC-", "")) + 1;
13
- return `DEC-${num.toString().padStart(3, "0")}`;
14
- }
15
-
16
- interface ConflictAnalysis {
17
- hasConflict: boolean;
18
- conflictingDecisions: Array<{
19
- id: string;
20
- title: string;
21
- decision: string;
22
- reason: string;
23
- }>;
24
- }
25
-
26
- // Extrair keywords relevantes de um texto
27
- function extractKeywords(text: string): string[] {
28
- const stopWords = new Set([
29
- "a", "o", "e", "de", "da", "do", "para", "com", "em", "que", "usar",
30
- "the", "and", "or", "to", "for", "with", "in", "on", "is", "are", "be",
31
- "will", "should", "must", "can", "use", "using", "como", "ser", "sera"
32
- ]);
33
-
34
- return text
35
- .toLowerCase()
36
- .replace(/[^\w\s-]/g, " ")
37
- .split(/\s+/)
38
- .filter((word) => word.length > 2 && !stopWords.has(word));
39
- }
40
-
41
- // Detectar conflitos semanticos entre decisoes
42
- function detectConflicts(
43
- newTitle: string,
44
- newDecision: string,
45
- existingDecisions: any[]
46
- ): ConflictAnalysis {
47
- const conflicts: ConflictAnalysis["conflictingDecisions"] = [];
48
-
49
- const newKeywords = new Set([
50
- ...extractKeywords(newTitle),
51
- ...extractKeywords(newDecision),
52
- ]);
53
-
54
- // Padroes de conflito conhecidos
55
- const conflictPatterns: Array<{ patterns: string[][]; reason: string }> = [
56
- {
57
- patterns: [["rest", "api"], ["graphql"]],
58
- reason: "Conflito de paradigma de API (REST vs GraphQL)",
59
- },
60
- {
61
- patterns: [["jwt", "token"], ["session", "cookie"]],
62
- reason: "Conflito de estrategia de autenticacao",
63
- },
64
- {
65
- patterns: [["zustand"], ["redux"]],
66
- reason: "Conflito de biblioteca de estado",
67
- },
68
- {
69
- patterns: [["prisma"], ["drizzle"]],
70
- reason: "Conflito de ORM",
71
- },
72
- {
73
- patterns: [["mysql"], ["postgres", "postgresql"]],
74
- reason: "Conflito de banco de dados",
75
- },
76
- {
77
- patterns: [["server", "component"], ["client", "component"]],
78
- reason: "Conflito de tipo de componente padrao",
79
- },
80
- {
81
- patterns: [["tailwind"], ["styled", "component"]],
82
- reason: "Conflito de abordagem de styling",
83
- },
84
- {
85
- patterns: [["vitest"], ["jest"]],
86
- reason: "Conflito de framework de testes",
87
- },
88
- ];
89
-
90
- for (const existing of existingDecisions) {
91
- const existingKeywords = new Set([
92
- ...extractKeywords(existing.title),
93
- ...extractKeywords(existing.decision),
94
- ]);
95
-
96
- // Verificar padroes de conflito conhecidos
97
- for (const { patterns, reason } of conflictPatterns) {
98
- const [patternA, patternB] = patterns;
99
-
100
- const newHasA = patternA.some((p) => newKeywords.has(p));
101
- const newHasB = patternB.some((p) => newKeywords.has(p));
102
- const existingHasA = patternA.some((p) => existingKeywords.has(p));
103
- const existingHasB = patternB.some((p) => existingKeywords.has(p));
104
-
105
- // Conflito: nova decisao menciona A, existente menciona B (ou vice-versa)
106
- if ((newHasA && existingHasB) || (newHasB && existingHasA)) {
107
- conflicts.push({
108
- id: existing.id,
109
- title: existing.title,
110
- decision: existing.decision,
111
- reason,
112
- });
113
- break; // Apenas um conflito por decisao existente
114
- }
115
- }
116
-
117
- // Verificar sobreposicao alta de keywords (mesmo topico, decisoes diferentes?)
118
- if (conflicts.every((c) => c.id !== existing.id)) {
119
- const intersection = [...newKeywords].filter((k) => existingKeywords.has(k));
120
- const similarity = intersection.length / Math.max(newKeywords.size, existingKeywords.size);
121
-
122
- if (similarity > 0.4 && intersection.length >= 3) {
123
- conflicts.push({
124
- id: existing.id,
125
- title: existing.title,
126
- decision: existing.decision,
127
- reason: `Alta sobreposicao de topico (${Math.round(similarity * 100)}% similar) - verificar se sao decisoes conflitantes`,
128
- });
129
- }
130
- }
131
- }
132
-
133
- return {
134
- hasConflict: conflicts.length > 0,
135
- conflictingDecisions: conflicts,
136
- };
137
- }
138
-
139
- export function decide(title: string, decision: string, options: { rationale?: string; force?: boolean }): void {
140
- initSchema();
141
-
142
- const db = getDb();
143
- const now = new Date().toISOString();
144
-
145
- const spec = db
146
- .query("SELECT * FROM specs WHERE phase NOT IN ('completed', 'cancelled') ORDER BY created_at DESC LIMIT 1")
147
- .get() as any;
148
-
149
- if (!spec) {
150
- console.error("\nNenhuma feature ativa.\n");
151
- process.exit(1);
152
- }
153
-
154
- // Verificar conflitos com decisoes existentes
155
- const existingDecisions = db
156
- .query("SELECT * FROM decisions WHERE spec_id = ? AND status = 'active'")
157
- .all(spec.id) as any[];
158
-
159
- if (existingDecisions.length > 0 && !options.force) {
160
- const analysis = detectConflicts(title, decision, existingDecisions);
161
-
162
- if (analysis.hasConflict) {
163
- console.warn("\n⚠ POTENCIAL CONFLITO DETECTADO:\n");
164
- console.warn(`Nova decisao: "${title}"`);
165
- console.warn(` ${decision}\n`);
166
-
167
- for (const conflict of analysis.conflictingDecisions) {
168
- console.warn(`Pode conflitar com: ${conflict.id} - "${conflict.title}"`);
169
- console.warn(` ${conflict.decision}`);
170
- console.warn(` Motivo: ${conflict.reason}\n`);
171
- }
172
-
173
- console.warn(`${"─".repeat(50)}`);
174
- console.warn(`Para registrar mesmo assim, use: decide "${title}" "${decision}" --force`);
175
- console.warn(`Ou revise as decisoes existentes com: decisions\n`);
176
- process.exit(1);
177
- }
178
- }
179
-
180
- // Pegar task atual em execucao
181
- const currentTask = db
182
- .query("SELECT * FROM tasks WHERE spec_id = ? AND status = 'running' LIMIT 1")
183
- .get(spec.id) as any;
184
-
185
- const decisionId = getNextDecisionId(spec.id);
186
-
187
- db.run(
188
- `INSERT INTO decisions (id, spec_id, task_ref, title, decision, rationale, status, created_at)
189
- VALUES (?, ?, ?, ?, ?, ?, 'active', ?)`,
190
- [
191
- decisionId,
192
- spec.id,
193
- currentTask?.number || null,
194
- title,
195
- decision,
196
- options.rationale || null,
197
- now,
198
- ]
199
- );
200
-
201
- console.log(`\nDecisao registrada: ${decisionId}`);
202
- console.log(`Titulo: ${title}`);
203
- console.log(`Decisao: ${decision}`);
204
- if (options.rationale) console.log(`Racional: ${options.rationale}`);
205
- if (currentTask) console.log(`Task: #${currentTask.number}`);
206
- if (options.force) console.log(`[!] Registrada com --force (conflito ignorado)`);
207
- console.log();
208
- }
209
-
210
- export function listDecisions(json: boolean = false): void {
211
- initSchema();
212
-
213
- const db = getDb();
214
-
215
- const spec = db
216
- .query("SELECT * FROM specs WHERE phase NOT IN ('completed', 'cancelled') ORDER BY created_at DESC LIMIT 1")
217
- .get() as any;
218
-
219
- if (!spec) {
220
- console.error("\nNenhuma feature ativa.\n");
221
- process.exit(1);
222
- }
223
-
224
- const decisions = db
225
- .query("SELECT * FROM decisions WHERE spec_id = ? ORDER BY created_at")
226
- .all(spec.id) as any[];
227
-
228
- if (json) {
229
- console.log(JSON.stringify({ decisions }));
230
- return;
231
- }
232
-
233
- if (decisions.length === 0) {
234
- console.log("\nNenhuma decisao registrada.\n");
235
- return;
236
- }
237
-
238
- console.log(`\nDecisoes (${decisions.length}):`);
239
- console.log(`${"─".repeat(60)}`);
240
-
241
- for (const dec of decisions) {
242
- const taskRef = dec.task_ref ? ` [Task #${dec.task_ref}]` : "";
243
- const status = dec.status === "active" ? "" : ` (${dec.status})`;
244
- console.log(`${dec.id}: ${dec.title}${taskRef}${status}`);
245
- console.log(` ${dec.decision}`);
246
- if (dec.rationale) console.log(` Racional: ${dec.rationale}`);
247
- console.log();
248
- }
249
- }
1
+ import { getDb } from "../db/connection";
2
+ import { initSchema } from "../db/schema";
3
+
4
+ function getNextDecisionId(specId: string): string {
5
+ const db = getDb();
6
+ const last = db
7
+ .query("SELECT id FROM decisions WHERE spec_id = ? ORDER BY created_at DESC LIMIT 1")
8
+ .get(specId) as any;
9
+
10
+ if (!last) return "DEC-001";
11
+
12
+ const num = parseInt(last.id.replace("DEC-", "")) + 1;
13
+ return `DEC-${num.toString().padStart(3, "0")}`;
14
+ }
15
+
16
+ interface ConflictAnalysis {
17
+ hasConflict: boolean;
18
+ conflictingDecisions: Array<{
19
+ id: string;
20
+ title: string;
21
+ decision: string;
22
+ reason: string;
23
+ }>;
24
+ }
25
+
26
+ // Extrair keywords relevantes de um texto
27
+ function extractKeywords(text: string): string[] {
28
+ const stopWords = new Set([
29
+ "a", "o", "e", "de", "da", "do", "para", "com", "em", "que", "usar",
30
+ "the", "and", "or", "to", "for", "with", "in", "on", "is", "are", "be",
31
+ "will", "should", "must", "can", "use", "using", "como", "ser", "sera"
32
+ ]);
33
+
34
+ return text
35
+ .toLowerCase()
36
+ .replace(/[^\w\s-]/g, " ")
37
+ .split(/\s+/)
38
+ .filter((word) => word.length > 2 && !stopWords.has(word));
39
+ }
40
+
41
+ // Detectar conflitos semanticos entre decisoes
42
+ function detectConflicts(
43
+ newTitle: string,
44
+ newDecision: string,
45
+ existingDecisions: any[]
46
+ ): ConflictAnalysis {
47
+ const conflicts: ConflictAnalysis["conflictingDecisions"] = [];
48
+
49
+ const newKeywords = new Set([
50
+ ...extractKeywords(newTitle),
51
+ ...extractKeywords(newDecision),
52
+ ]);
53
+
54
+ // Padroes de conflito conhecidos
55
+ const conflictPatterns: Array<{ patterns: string[][]; reason: string }> = [
56
+ {
57
+ patterns: [["rest", "api"], ["graphql"]],
58
+ reason: "Conflito de paradigma de API (REST vs GraphQL)",
59
+ },
60
+ {
61
+ patterns: [["jwt", "token"], ["session", "cookie"]],
62
+ reason: "Conflito de estrategia de autenticacao",
63
+ },
64
+ {
65
+ patterns: [["zustand"], ["redux"]],
66
+ reason: "Conflito de biblioteca de estado",
67
+ },
68
+ {
69
+ patterns: [["prisma"], ["drizzle"]],
70
+ reason: "Conflito de ORM",
71
+ },
72
+ {
73
+ patterns: [["mysql"], ["postgres", "postgresql"]],
74
+ reason: "Conflito de banco de dados",
75
+ },
76
+ {
77
+ patterns: [["server", "component"], ["client", "component"]],
78
+ reason: "Conflito de tipo de componente padrao",
79
+ },
80
+ {
81
+ patterns: [["tailwind"], ["styled", "component"]],
82
+ reason: "Conflito de abordagem de styling",
83
+ },
84
+ {
85
+ patterns: [["vitest"], ["jest"]],
86
+ reason: "Conflito de framework de testes",
87
+ },
88
+ ];
89
+
90
+ for (const existing of existingDecisions) {
91
+ const existingKeywords = new Set([
92
+ ...extractKeywords(existing.title),
93
+ ...extractKeywords(existing.decision),
94
+ ]);
95
+
96
+ // Verificar padroes de conflito conhecidos
97
+ for (const { patterns, reason } of conflictPatterns) {
98
+ const [patternA, patternB] = patterns;
99
+
100
+ const newHasA = patternA.some((p) => newKeywords.has(p));
101
+ const newHasB = patternB.some((p) => newKeywords.has(p));
102
+ const existingHasA = patternA.some((p) => existingKeywords.has(p));
103
+ const existingHasB = patternB.some((p) => existingKeywords.has(p));
104
+
105
+ // Conflito: nova decisao menciona A, existente menciona B (ou vice-versa)
106
+ if ((newHasA && existingHasB) || (newHasB && existingHasA)) {
107
+ conflicts.push({
108
+ id: existing.id,
109
+ title: existing.title,
110
+ decision: existing.decision,
111
+ reason,
112
+ });
113
+ break; // Apenas um conflito por decisao existente
114
+ }
115
+ }
116
+
117
+ // Verificar sobreposicao alta de keywords (mesmo topico, decisoes diferentes?)
118
+ if (conflicts.every((c) => c.id !== existing.id)) {
119
+ const intersection = [...newKeywords].filter((k) => existingKeywords.has(k));
120
+ const similarity = intersection.length / Math.max(newKeywords.size, existingKeywords.size);
121
+
122
+ if (similarity > 0.4 && intersection.length >= 3) {
123
+ conflicts.push({
124
+ id: existing.id,
125
+ title: existing.title,
126
+ decision: existing.decision,
127
+ reason: `Alta sobreposicao de topico (${Math.round(similarity * 100)}% similar) - verificar se sao decisoes conflitantes`,
128
+ });
129
+ }
130
+ }
131
+ }
132
+
133
+ return {
134
+ hasConflict: conflicts.length > 0,
135
+ conflictingDecisions: conflicts,
136
+ };
137
+ }
138
+
139
+ export function decide(title: string, decision: string, options: { rationale?: string; force?: boolean }): void {
140
+ initSchema();
141
+
142
+ const db = getDb();
143
+ const now = new Date().toISOString();
144
+
145
+ const spec = db
146
+ .query("SELECT * FROM specs WHERE phase NOT IN ('completed', 'cancelled') ORDER BY created_at DESC LIMIT 1")
147
+ .get() as any;
148
+
149
+ if (!spec) {
150
+ console.error("\nNenhuma feature ativa.\n");
151
+ process.exit(1);
152
+ }
153
+
154
+ // Verificar conflitos com decisoes existentes
155
+ const existingDecisions = db
156
+ .query("SELECT * FROM decisions WHERE spec_id = ? AND status = 'active'")
157
+ .all(spec.id) as any[];
158
+
159
+ if (existingDecisions.length > 0 && !options.force) {
160
+ const analysis = detectConflicts(title, decision, existingDecisions);
161
+
162
+ if (analysis.hasConflict) {
163
+ console.warn("\n⚠ POTENCIAL CONFLITO DETECTADO:\n");
164
+ console.warn(`Nova decisao: "${title}"`);
165
+ console.warn(` ${decision}\n`);
166
+
167
+ for (const conflict of analysis.conflictingDecisions) {
168
+ console.warn(`Pode conflitar com: ${conflict.id} - "${conflict.title}"`);
169
+ console.warn(` ${conflict.decision}`);
170
+ console.warn(` Motivo: ${conflict.reason}\n`);
171
+ }
172
+
173
+ console.warn(`${"─".repeat(50)}`);
174
+ console.warn(`Para registrar mesmo assim, use: decide "${title}" "${decision}" --force`);
175
+ console.warn(`Ou revise as decisoes existentes com: decisions\n`);
176
+ process.exit(1);
177
+ }
178
+ }
179
+
180
+ // Pegar task atual em execucao
181
+ const currentTask = db
182
+ .query("SELECT * FROM tasks WHERE spec_id = ? AND status = 'running' LIMIT 1")
183
+ .get(spec.id) as any;
184
+
185
+ const decisionId = getNextDecisionId(spec.id);
186
+
187
+ db.run(
188
+ `INSERT INTO decisions (id, spec_id, task_ref, title, decision, rationale, status, created_at)
189
+ VALUES (?, ?, ?, ?, ?, ?, 'active', ?)`,
190
+ [
191
+ decisionId,
192
+ spec.id,
193
+ currentTask?.number || null,
194
+ title,
195
+ decision,
196
+ options.rationale || null,
197
+ now,
198
+ ]
199
+ );
200
+
201
+ console.log(`\nDecisao registrada: ${decisionId}`);
202
+ console.log(`Titulo: ${title}`);
203
+ console.log(`Decisao: ${decision}`);
204
+ if (options.rationale) console.log(`Racional: ${options.rationale}`);
205
+ if (currentTask) console.log(`Task: #${currentTask.number}`);
206
+ if (options.force) console.log(`[!] Registrada com --force (conflito ignorado)`);
207
+ console.log();
208
+ }
209
+
210
+ export function listDecisions(json: boolean = false): void {
211
+ initSchema();
212
+
213
+ const db = getDb();
214
+
215
+ const spec = db
216
+ .query("SELECT * FROM specs WHERE phase NOT IN ('completed', 'cancelled') ORDER BY created_at DESC LIMIT 1")
217
+ .get() as any;
218
+
219
+ if (!spec) {
220
+ console.error("\nNenhuma feature ativa.\n");
221
+ process.exit(1);
222
+ }
223
+
224
+ const decisions = db
225
+ .query("SELECT * FROM decisions WHERE spec_id = ? ORDER BY created_at")
226
+ .all(spec.id) as any[];
227
+
228
+ if (json) {
229
+ console.log(JSON.stringify({ decisions }));
230
+ return;
231
+ }
232
+
233
+ if (decisions.length === 0) {
234
+ console.log("\nNenhuma decisao registrada.\n");
235
+ return;
236
+ }
237
+
238
+ console.log(`\nDecisoes (${decisions.length}):`);
239
+ console.log(`${"─".repeat(60)}`);
240
+
241
+ for (const dec of decisions) {
242
+ const taskRef = dec.task_ref ? ` [Task #${dec.task_ref}]` : "";
243
+ const status = dec.status === "active" ? "" : ` (${dec.status})`;
244
+ console.log(`${dec.id}: ${dec.title}${taskRef}${status}`);
245
+ console.log(` ${dec.decision}`);
246
+ if (dec.rationale) console.log(` Racional: ${dec.rationale}`);
247
+ console.log();
248
+ }
249
+ }