@codexa/cli 9.0.7 → 9.0.8
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/commands/decide.ts +120 -3
- package/commands/discover.ts +18 -9
- package/commands/integration.test.ts +754 -0
- package/commands/knowledge.test.ts +2 -6
- package/commands/knowledge.ts +20 -4
- package/commands/patterns.ts +8 -644
- package/commands/product.ts +41 -104
- package/commands/spec-resolver.test.ts +2 -13
- package/commands/standards.ts +33 -3
- package/commands/task.ts +21 -4
- package/commands/utils.test.ts +25 -87
- package/commands/utils.ts +20 -82
- package/context/assembly.ts +11 -12
- package/context/domains.test.ts +278 -0
- package/context/domains.ts +156 -0
- package/context/generator.ts +12 -13
- package/context/index.ts +3 -1
- package/context/sections.ts +2 -1
- package/db/schema.ts +40 -5
- package/db/test-helpers.ts +33 -0
- package/gates/standards-validator.test.ts +447 -0
- package/gates/standards-validator.ts +164 -125
- package/gates/typecheck-validator.ts +296 -92
- package/gates/validator.ts +93 -8
- package/package.json +1 -1
- package/protocol/process-return.ts +39 -4
- package/workflow.ts +54 -84
|
@@ -1,14 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Gate 4.5: Typecheck Validator
|
|
2
|
+
* Gate 4.5: Typecheck Validator (Stack-Agnostic)
|
|
3
3
|
*
|
|
4
4
|
* Executa verificacao de tipos nos arquivos criados/modificados pelo subagent.
|
|
5
|
-
*
|
|
5
|
+
* Suporta multiplas stacks via presets automaticos + comando custom configuravel.
|
|
6
|
+
*
|
|
7
|
+
* Prioridade: custom_command > preset da stack > skip
|
|
6
8
|
*/
|
|
7
9
|
|
|
8
10
|
import { existsSync } from "fs";
|
|
9
11
|
import { spawnSync } from "child_process";
|
|
10
12
|
import { resolve, dirname } from "path";
|
|
11
13
|
|
|
14
|
+
// ═══════════════════════════════════════════════════════════════
|
|
15
|
+
// INTERFACES
|
|
16
|
+
// ═══════════════════════════════════════════════════════════════
|
|
17
|
+
|
|
12
18
|
export interface TypecheckResult {
|
|
13
19
|
passed: boolean;
|
|
14
20
|
errors: TypecheckError[];
|
|
@@ -24,136 +30,334 @@ export interface TypecheckError {
|
|
|
24
30
|
code: string;
|
|
25
31
|
}
|
|
26
32
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
33
|
+
interface TypecheckPreset {
|
|
34
|
+
fileFilter: RegExp;
|
|
35
|
+
setup: (files: string[]) => string[] | null; // Returns command args or null if unavailable
|
|
36
|
+
errorParser: (output: string, files: string[]) => TypecheckError[];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ═══════════════════════════════════════════════════════════════
|
|
40
|
+
// PRESETS POR STACK
|
|
41
|
+
// ═══════════════════════════════════════════════════════════════
|
|
42
|
+
|
|
43
|
+
const TYPECHECK_PRESETS: Record<string, TypecheckPreset> = {
|
|
44
|
+
node: {
|
|
45
|
+
fileFilter: /\.(ts|tsx)$/,
|
|
46
|
+
setup: (files) => {
|
|
47
|
+
const tsconfig = findConfigFile(files, "tsconfig.json");
|
|
48
|
+
if (!tsconfig) return null;
|
|
49
|
+
const tscCmd = detectCommand(["bunx tsc", "npx tsc", "tsc"]);
|
|
50
|
+
if (!tscCmd) return null;
|
|
51
|
+
return [...tscCmd.split(" "), "--noEmit", "--project", tsconfig];
|
|
52
|
+
},
|
|
53
|
+
errorParser: parseColonSeparatedErrors(/^(.+)\((\d+),(\d+)\):\s+error\s+(TS\d+):\s+(.+)$/),
|
|
54
|
+
},
|
|
55
|
+
|
|
56
|
+
go: {
|
|
57
|
+
fileFilter: /\.go$/,
|
|
58
|
+
setup: () => {
|
|
59
|
+
if (!isCommandAvailable("go")) return null;
|
|
60
|
+
return ["go", "vet", "./..."];
|
|
61
|
+
},
|
|
62
|
+
errorParser: parseColonSeparatedErrors(/^(.+):(\d+):(\d+):\s+(.+)$/),
|
|
63
|
+
},
|
|
64
|
+
|
|
65
|
+
dotnet: {
|
|
66
|
+
fileFilter: /\.cs$/,
|
|
67
|
+
setup: (files) => {
|
|
68
|
+
const csproj = findConfigFile(files, "*.csproj");
|
|
69
|
+
if (!csproj && !isCommandAvailable("dotnet")) return null;
|
|
70
|
+
return ["dotnet", "build", "--no-restore", "--nologo", "-v", "q"];
|
|
71
|
+
},
|
|
72
|
+
errorParser: parseColonSeparatedErrors(/^(.+)\((\d+),(\d+)\):\s+error\s+(CS\d+):\s+(.+)$/),
|
|
73
|
+
},
|
|
74
|
+
|
|
75
|
+
rust: {
|
|
76
|
+
fileFilter: /\.rs$/,
|
|
77
|
+
setup: () => {
|
|
78
|
+
if (!isCommandAvailable("cargo")) return null;
|
|
79
|
+
return ["cargo", "check", "--message-format", "short"];
|
|
80
|
+
},
|
|
81
|
+
errorParser: parseColonSeparatedErrors(/^(.+):(\d+):(\d+):\s+error(?:\[(\w+)\])?:\s+(.+)$/),
|
|
82
|
+
},
|
|
83
|
+
|
|
84
|
+
python: {
|
|
85
|
+
fileFilter: /\.py$/,
|
|
86
|
+
setup: () => {
|
|
87
|
+
// Tentar mypy primeiro, depois pyright
|
|
88
|
+
const checker = detectCommand(["mypy", "pyright"]);
|
|
89
|
+
if (!checker) return null;
|
|
90
|
+
if (checker === "mypy") return ["mypy", "--no-error-summary"];
|
|
91
|
+
return ["pyright"];
|
|
92
|
+
},
|
|
93
|
+
errorParser: parseColonSeparatedErrors(/^(.+):(\d+):(?:(\d+):)?\s+error:\s+(.+)$/),
|
|
94
|
+
},
|
|
95
|
+
|
|
96
|
+
flutter: {
|
|
97
|
+
fileFilter: /\.dart$/,
|
|
98
|
+
setup: () => {
|
|
99
|
+
if (!isCommandAvailable("dart")) return null;
|
|
100
|
+
return ["dart", "analyze", "--no-preamble"];
|
|
101
|
+
},
|
|
102
|
+
errorParser: parseColonSeparatedErrors(/^\s*(.+):(\d+):(\d+)\s+.*error.*[-•]\s+(.+)$/),
|
|
103
|
+
},
|
|
104
|
+
|
|
105
|
+
jvm: {
|
|
106
|
+
fileFilter: /\.(java|kt|kts)$/,
|
|
107
|
+
setup: () => {
|
|
108
|
+
// Gradle ou Maven
|
|
109
|
+
if (existsSync("gradlew") || existsSync("gradlew.bat")) {
|
|
110
|
+
const cmd = process.platform === "win32" ? "gradlew.bat" : "./gradlew";
|
|
111
|
+
return [cmd, "compileJava", "-q"];
|
|
112
|
+
}
|
|
113
|
+
if (existsSync("pom.xml") && isCommandAvailable("mvn")) {
|
|
114
|
+
return ["mvn", "compile", "-q"];
|
|
115
|
+
}
|
|
116
|
+
return null;
|
|
117
|
+
},
|
|
118
|
+
errorParser: parseColonSeparatedErrors(/^(.+):(\d+):\s+error:\s+(.+)$/),
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
// ═══════════════════════════════════════════════════════════════
|
|
123
|
+
// HELPERS
|
|
124
|
+
// ═══════════════════════════════════════════════════════════════
|
|
125
|
+
|
|
126
|
+
function isCommandAvailable(cmd: string): boolean {
|
|
127
|
+
const result = spawnSync(cmd, ["--version"], { encoding: "utf-8", timeout: 3000 });
|
|
128
|
+
return !result.error;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function detectCommand(candidates: string[]): string | null {
|
|
132
|
+
for (const cmd of candidates) {
|
|
133
|
+
const bin = cmd.split(" ")[0];
|
|
134
|
+
if (isCommandAvailable(bin)) return cmd;
|
|
135
|
+
}
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function findConfigFile(files: string[], pattern: string): string | null {
|
|
140
|
+
// Buscar no cwd primeiro
|
|
141
|
+
const cwdCandidate = resolve(process.cwd(), pattern.replace("*.", ""));
|
|
142
|
+
if (!pattern.includes("*") && existsSync(cwdCandidate)) return cwdCandidate;
|
|
143
|
+
|
|
144
|
+
// Buscar subindo diretorios a partir dos arquivos
|
|
145
|
+
const existingFiles = files.filter(f => existsSync(f));
|
|
146
|
+
if (existingFiles.length === 0) return null;
|
|
147
|
+
|
|
148
|
+
let dir = dirname(resolve(existingFiles[0]));
|
|
32
149
|
const root = resolve("/");
|
|
33
150
|
|
|
34
151
|
while (dir !== root) {
|
|
35
|
-
|
|
36
|
-
|
|
152
|
+
if (pattern.includes("*")) {
|
|
153
|
+
// Glob simples: *.csproj
|
|
154
|
+
try {
|
|
155
|
+
const entries = require("fs").readdirSync(dir) as string[];
|
|
156
|
+
const ext = pattern.replace("*", "");
|
|
157
|
+
const match = entries.find((e: string) => e.endsWith(ext));
|
|
158
|
+
if (match) return resolve(dir, match);
|
|
159
|
+
} catch { /* continue */ }
|
|
160
|
+
} else {
|
|
161
|
+
const candidate = resolve(dir, pattern);
|
|
162
|
+
if (existsSync(candidate)) return candidate;
|
|
163
|
+
}
|
|
37
164
|
dir = dirname(dir);
|
|
38
165
|
}
|
|
39
166
|
return null;
|
|
40
167
|
}
|
|
41
168
|
|
|
169
|
+
function isFileRelevant(errorFile: string, relevantFiles: string[]): boolean {
|
|
170
|
+
const normalized = errorFile.replace(/\\/g, "/");
|
|
171
|
+
return relevantFiles.some(rf => {
|
|
172
|
+
const normalizedRf = rf.replace(/\\/g, "/");
|
|
173
|
+
return (
|
|
174
|
+
normalized.endsWith(normalizedRf) ||
|
|
175
|
+
normalized.includes(normalizedRf) ||
|
|
176
|
+
normalizedRf.endsWith(normalized) ||
|
|
177
|
+
normalizedRf.includes(normalized)
|
|
178
|
+
);
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
42
182
|
/**
|
|
43
|
-
*
|
|
183
|
+
* Factory para parsers de erro com formato "file:line:col: message"
|
|
184
|
+
* Aceita regex com 4 ou 5 capture groups:
|
|
185
|
+
* 5 groups: (file, line, col, code, message)
|
|
186
|
+
* 4 groups: (file, line, col, message) — code sera "ERROR"
|
|
44
187
|
*/
|
|
45
|
-
function
|
|
46
|
-
|
|
47
|
-
|
|
188
|
+
function parseColonSeparatedErrors(regex: RegExp) {
|
|
189
|
+
return (output: string, relevantFiles: string[]): TypecheckError[] => {
|
|
190
|
+
const errors: TypecheckError[] = [];
|
|
191
|
+
const lines = output.split(/\r?\n/);
|
|
48
192
|
|
|
49
|
-
|
|
50
|
-
|
|
193
|
+
for (const line of lines) {
|
|
194
|
+
const match = line.match(regex);
|
|
195
|
+
if (!match) continue;
|
|
51
196
|
|
|
52
|
-
|
|
53
|
-
if (!tscCheck.error) return ["tsc"];
|
|
197
|
+
let file: string, lineNum: string, col: string, code: string, message: string;
|
|
54
198
|
|
|
55
|
-
|
|
56
|
-
|
|
199
|
+
if (match.length >= 6) {
|
|
200
|
+
[, file, lineNum, col, code, message] = match;
|
|
201
|
+
} else if (match.length >= 5) {
|
|
202
|
+
[, file, lineNum, col, message] = match;
|
|
203
|
+
code = "ERROR";
|
|
204
|
+
} else {
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
57
207
|
|
|
58
|
-
|
|
59
|
-
* Parseia output do tsc em erros estruturados
|
|
60
|
-
*/
|
|
61
|
-
function parseTscOutput(output: string, relevantFiles: string[]): TypecheckError[] {
|
|
62
|
-
const errors: TypecheckError[] = [];
|
|
63
|
-
const lines = output.split(/\r?\n/);
|
|
64
|
-
|
|
65
|
-
// Formato: file(line,col): error TSxxxx: message
|
|
66
|
-
const errorRegex = /^(.+)\((\d+),(\d+)\):\s+error\s+(TS\d+):\s+(.+)$/;
|
|
67
|
-
|
|
68
|
-
for (const line of lines) {
|
|
69
|
-
const match = line.match(errorRegex);
|
|
70
|
-
if (!match) continue;
|
|
71
|
-
|
|
72
|
-
const [, file, lineNum, col, code, message] = match;
|
|
73
|
-
const normalizedFile = file.replace(/\\/g, "/");
|
|
74
|
-
|
|
75
|
-
// Filtrar: apenas erros nos arquivos relevantes
|
|
76
|
-
const isRelevant = relevantFiles.some(rf => {
|
|
77
|
-
const normalizedRf = rf.replace(/\\/g, "/");
|
|
78
|
-
return (
|
|
79
|
-
normalizedFile.endsWith(normalizedRf) ||
|
|
80
|
-
normalizedFile.includes(normalizedRf) ||
|
|
81
|
-
normalizedRf.endsWith(normalizedFile) ||
|
|
82
|
-
normalizedRf.includes(normalizedFile)
|
|
83
|
-
);
|
|
84
|
-
});
|
|
208
|
+
if (!isFileRelevant(file, relevantFiles)) continue;
|
|
85
209
|
|
|
86
|
-
if (isRelevant) {
|
|
87
210
|
errors.push({
|
|
88
|
-
file:
|
|
89
|
-
line: parseInt(lineNum),
|
|
90
|
-
column: parseInt(col),
|
|
91
|
-
message,
|
|
92
|
-
code,
|
|
211
|
+
file: file.replace(/\\/g, "/"),
|
|
212
|
+
line: parseInt(lineNum) || 0,
|
|
213
|
+
column: parseInt(col) || 0,
|
|
214
|
+
message: message?.trim() || "",
|
|
215
|
+
code: code || "ERROR",
|
|
93
216
|
});
|
|
94
217
|
}
|
|
95
|
-
}
|
|
96
218
|
|
|
97
|
-
|
|
219
|
+
return errors;
|
|
220
|
+
};
|
|
98
221
|
}
|
|
99
222
|
|
|
100
223
|
/**
|
|
101
|
-
*
|
|
224
|
+
* Resolve ecosystems da stack JSON do projeto
|
|
102
225
|
*/
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
226
|
+
function resolveEcosystems(stackJson: string | null): string[] {
|
|
227
|
+
if (!stackJson) return [];
|
|
228
|
+
try {
|
|
229
|
+
const stack = JSON.parse(stackJson);
|
|
230
|
+
// Formato novo (detectUniversal): tem ecosystems array
|
|
231
|
+
if (Array.isArray(stack.ecosystems)) return stack.ecosystems;
|
|
232
|
+
// Formato legado: tem chaves frontend/backend
|
|
233
|
+
if (stack.frontend || stack.backend) {
|
|
234
|
+
const ecosystems: string[] = [];
|
|
235
|
+
const stackStr = JSON.stringify(stack).toLowerCase();
|
|
236
|
+
if (stackStr.includes("typescript") || stackStr.includes("next") || stackStr.includes("react") || stackStr.includes("node") || stackStr.includes("bun")) ecosystems.push("node");
|
|
237
|
+
if (stackStr.includes("go") || stackStr.includes("golang")) ecosystems.push("go");
|
|
238
|
+
if (stackStr.includes("dotnet") || stackStr.includes("c#") || stackStr.includes("csharp")) ecosystems.push("dotnet");
|
|
239
|
+
if (stackStr.includes("python") || stackStr.includes("django") || stackStr.includes("flask") || stackStr.includes("fastapi")) ecosystems.push("python");
|
|
240
|
+
if (stackStr.includes("rust") || stackStr.includes("cargo")) ecosystems.push("rust");
|
|
241
|
+
if (stackStr.includes("flutter") || stackStr.includes("dart")) ecosystems.push("flutter");
|
|
242
|
+
if (stackStr.includes("java") || stackStr.includes("kotlin") || stackStr.includes("spring") || stackStr.includes("gradle")) ecosystems.push("jvm");
|
|
243
|
+
return ecosystems;
|
|
244
|
+
}
|
|
245
|
+
return [];
|
|
246
|
+
} catch {
|
|
247
|
+
return [];
|
|
109
248
|
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// ═══════════════════════════════════════════════════════════════
|
|
252
|
+
// EXECUCAO DO TYPECHECK
|
|
253
|
+
// ═══════════════════════════════════════════════════════════════
|
|
254
|
+
|
|
255
|
+
function runCustomCommand(customCommand: string, files: string[]): TypecheckResult {
|
|
256
|
+
const parts = customCommand.trim().split(/\s+/);
|
|
257
|
+
const [cmd, ...args] = parts;
|
|
110
258
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
if (!tsconfig) {
|
|
114
|
-
const firstFileDir = dirname(resolve(tsFiles[0]));
|
|
115
|
-
tsconfig = findTsConfig(firstFileDir);
|
|
259
|
+
if (!isCommandAvailable(cmd)) {
|
|
260
|
+
return { passed: true, errors: [], skipped: true, reason: `Comando custom '${cmd}' nao encontrado no PATH` };
|
|
116
261
|
}
|
|
117
|
-
|
|
118
|
-
|
|
262
|
+
|
|
263
|
+
const result = spawnSync(cmd, args, {
|
|
264
|
+
encoding: "utf-8",
|
|
265
|
+
timeout: 60000,
|
|
266
|
+
maxBuffer: 5 * 1024 * 1024,
|
|
267
|
+
cwd: process.cwd(),
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
if (result.error) {
|
|
271
|
+
return { passed: true, errors: [], skipped: true, reason: `Erro ao executar '${customCommand}': ${result.error.message}` };
|
|
119
272
|
}
|
|
120
273
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
274
|
+
const output = (result.stdout || "") + (result.stderr || "");
|
|
275
|
+
// Comando custom: verificar exit code (0 = pass, non-zero = fail)
|
|
276
|
+
// Parsear erros com formato generico file:line:col
|
|
277
|
+
const genericParser = parseColonSeparatedErrors(/^(.+):(\d+):(?:(\d+):?)?\s+(?:error|Error):\s+(.+)$/);
|
|
278
|
+
const errors = genericParser(output, files);
|
|
279
|
+
|
|
280
|
+
// Se nao encontrou erros parseados mas exit code != 0, reportar output bruto
|
|
281
|
+
if (result.status !== 0 && errors.length === 0) {
|
|
282
|
+
const trimmedOutput = output.trim().slice(0, 500);
|
|
283
|
+
return {
|
|
284
|
+
passed: false,
|
|
285
|
+
errors: [{ file: "typecheck", line: 0, column: 0, message: trimmedOutput, code: `EXIT_${result.status}` }],
|
|
286
|
+
skipped: false,
|
|
287
|
+
};
|
|
125
288
|
}
|
|
126
289
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
290
|
+
return { passed: errors.length === 0, errors, skipped: false };
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function runPresetTypecheck(ecosystems: string[], files: string[]): TypecheckResult {
|
|
294
|
+
// Tentar cada ecosystem detectado ate encontrar um que funcione
|
|
295
|
+
for (const eco of ecosystems) {
|
|
296
|
+
const preset = TYPECHECK_PRESETS[eco];
|
|
297
|
+
if (!preset) continue;
|
|
298
|
+
|
|
299
|
+
const relevantFiles = files.filter(f => preset.fileFilter.test(f) && existsSync(f));
|
|
300
|
+
if (relevantFiles.length === 0) continue;
|
|
301
|
+
|
|
302
|
+
const cmdArgs = preset.setup(relevantFiles);
|
|
303
|
+
if (!cmdArgs) continue;
|
|
304
|
+
|
|
305
|
+
const result = spawnSync(cmdArgs[0], cmdArgs.slice(1), {
|
|
132
306
|
encoding: "utf-8",
|
|
133
307
|
timeout: 60000,
|
|
134
308
|
maxBuffer: 5 * 1024 * 1024,
|
|
135
|
-
cwd:
|
|
309
|
+
cwd: process.cwd(),
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
if (result.error) {
|
|
313
|
+
return { passed: true, errors: [], skipped: true, reason: `Erro ao executar ${cmdArgs[0]}: ${result.error.message}` };
|
|
136
314
|
}
|
|
137
|
-
);
|
|
138
315
|
|
|
139
|
-
|
|
140
|
-
|
|
316
|
+
const output = (result.stdout || "") + (result.stderr || "");
|
|
317
|
+
const errors = preset.errorParser(output, relevantFiles);
|
|
318
|
+
|
|
319
|
+
return { passed: errors.length === 0, errors, skipped: false };
|
|
141
320
|
}
|
|
142
321
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
const errors = parseTscOutput(output, tsFiles);
|
|
322
|
+
return { passed: true, errors: [], skipped: true, reason: "Nenhum typechecker disponivel para a stack detectada" };
|
|
323
|
+
}
|
|
146
324
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
325
|
+
// ═══════════════════════════════════════════════════════════════
|
|
326
|
+
// API PUBLICA
|
|
327
|
+
// ═══════════════════════════════════════════════════════════════
|
|
328
|
+
|
|
329
|
+
export function runTypecheck(files: string[], options?: { customCommand?: string | null; stackJson?: string | null }): TypecheckResult {
|
|
330
|
+
const existingFiles = files.filter(f => existsSync(f));
|
|
331
|
+
if (existingFiles.length === 0) {
|
|
332
|
+
return { passed: true, errors: [], skipped: true, reason: "Nenhum arquivo encontrado no disco" };
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Prioridade 1: Comando custom configurado pelo usuario
|
|
336
|
+
if (options?.customCommand) {
|
|
337
|
+
return runCustomCommand(options.customCommand, existingFiles);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Prioridade 2: Preset automatico baseado na stack detectada
|
|
341
|
+
const ecosystems = resolveEcosystems(options?.stackJson || null);
|
|
342
|
+
if (ecosystems.length > 0) {
|
|
343
|
+
return runPresetTypecheck(ecosystems, existingFiles);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// Prioridade 3: Fallback — tentar detectar stack pelos arquivos
|
|
347
|
+
const fallbackEcosystems: string[] = [];
|
|
348
|
+
for (const [eco, preset] of Object.entries(TYPECHECK_PRESETS)) {
|
|
349
|
+
if (existingFiles.some(f => preset.fileFilter.test(f))) {
|
|
350
|
+
fallbackEcosystems.push(eco);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
if (fallbackEcosystems.length > 0) {
|
|
355
|
+
return runPresetTypecheck(fallbackEcosystems, existingFiles);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
return { passed: true, errors: [], skipped: true, reason: "Stack nao detectada e nenhum typechecker configurado" };
|
|
152
359
|
}
|
|
153
360
|
|
|
154
|
-
/**
|
|
155
|
-
* Formata resultado para exibicao no CLI
|
|
156
|
-
*/
|
|
157
361
|
export function printTypecheckResult(result: TypecheckResult): void {
|
|
158
362
|
if (result.skipped) {
|
|
159
363
|
console.log(`\n[i] Typecheck pulado: ${result.reason}`);
|
package/gates/validator.ts
CHANGED
|
@@ -3,10 +3,12 @@ import { existsSync, readFileSync, statSync } from "fs";
|
|
|
3
3
|
import { extname } from "path";
|
|
4
4
|
import { validateAgainstStandards, printValidationResult } from "./standards-validator";
|
|
5
5
|
import { runTypecheck, printTypecheckResult } from "./typecheck-validator";
|
|
6
|
-
import { extractUtilitiesFromFile } from "../commands/patterns";
|
|
6
|
+
import { extractUtilitiesFromFile, inferScopeFromPath } from "../commands/patterns";
|
|
7
7
|
import { findDuplicateUtilities } from "../db/schema";
|
|
8
8
|
import { GateError, RecoverySuggestion } from "../errors";
|
|
9
9
|
import { resolveSpecOrNull } from "../commands/spec-resolver";
|
|
10
|
+
import { detectConflicts } from "../commands/decide";
|
|
11
|
+
import { getAgentDomain, domainToScope } from "../context/domains";
|
|
10
12
|
|
|
11
13
|
export interface GateResult {
|
|
12
14
|
passed: boolean;
|
|
@@ -92,7 +94,12 @@ const GATES: Record<string, GateCheck[]> = {
|
|
|
92
94
|
{
|
|
93
95
|
check: "typecheck-pass",
|
|
94
96
|
message: "Erros de tipo encontrados nos arquivos da task",
|
|
95
|
-
resolution: "Corrija os erros
|
|
97
|
+
resolution: "Corrija os erros de tipo ou use --force --force-reason para bypass",
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
check: "decision-conflicts",
|
|
101
|
+
message: "Conflito entre decisoes do subagent e decisoes ativas",
|
|
102
|
+
resolution: "Revise com: decisions. Revogue: decide:revoke <id>. Substitua: decide:supersede <id> <title> <decision>. Ou: --force --force-reason",
|
|
96
103
|
},
|
|
97
104
|
],
|
|
98
105
|
"review-start": [
|
|
@@ -131,11 +138,12 @@ const RECOVERY_STRATEGIES: Record<string, (details?: string) => RecoverySuggesti
|
|
|
131
138
|
],
|
|
132
139
|
}),
|
|
133
140
|
"typecheck-pass": (details) => ({
|
|
134
|
-
diagnostic: `Erros
|
|
141
|
+
diagnostic: `Erros de tipo encontrados:\n${details || "Detalhes nao disponiveis"}`,
|
|
135
142
|
steps: [
|
|
136
143
|
"Corrija os erros de tipo listados",
|
|
137
144
|
"Verifique imports e definicoes de tipo",
|
|
138
145
|
"Se erros em deps externas, use --force --force-reason 'motivo'",
|
|
146
|
+
"Para configurar typechecker custom: codexa project set --typecheck-command '<comando>'",
|
|
139
147
|
],
|
|
140
148
|
}),
|
|
141
149
|
"files-exist": (details) => ({
|
|
@@ -153,6 +161,16 @@ const RECOVERY_STRATEGIES: Record<string, (details?: string) => RecoverySuggesti
|
|
|
153
161
|
"Descreva O QUE foi feito, nao apenas 'feito' ou 'ok'",
|
|
154
162
|
],
|
|
155
163
|
}),
|
|
164
|
+
"decision-conflicts": (details) => ({
|
|
165
|
+
diagnostic: `Conflitos de decisao:\n${details || "Detalhes nao disponiveis"}`,
|
|
166
|
+
steps: [
|
|
167
|
+
"Revise decisoes existentes: decisions",
|
|
168
|
+
"Revogue a antiga: decide:revoke <id> --reason 'motivo'",
|
|
169
|
+
"Ou substitua: decide:supersede <id> <title> <decision>",
|
|
170
|
+
"Se intencional: --force --force-reason 'motivo'",
|
|
171
|
+
],
|
|
172
|
+
command: "codexa decisions",
|
|
173
|
+
}),
|
|
156
174
|
"reasoning-provided": () => ({
|
|
157
175
|
diagnostic: "Subagent retornou sem reasoning.approach adequado",
|
|
158
176
|
steps: [
|
|
@@ -249,9 +267,17 @@ function executeCheck(check: string, context: any): { passed: boolean; details?:
|
|
|
249
267
|
// v8.0: Validar não apenas existência, mas conteúdo mínimo
|
|
250
268
|
// v9.2: Validar que arquivo foi modificado DURANTE a task
|
|
251
269
|
// v9.3: Tolerancia de 5s para clock skew em sandbox
|
|
270
|
+
// v9.6: Confiar no protocolo quando subagent reporta arquivos (sandbox)
|
|
252
271
|
const MTIME_TOLERANCE_MS = 5000;
|
|
253
272
|
const issues: string[] = [];
|
|
254
273
|
|
|
274
|
+
// Construir set de arquivos reportados pelo subagent (podem estar em sandbox)
|
|
275
|
+
const subagentReportedFiles = new Set<string>();
|
|
276
|
+
if (context.subagentData) {
|
|
277
|
+
for (const f of context.subagentData.files_created || []) subagentReportedFiles.add(f);
|
|
278
|
+
for (const f of context.subagentData.files_modified || []) subagentReportedFiles.add(f);
|
|
279
|
+
}
|
|
280
|
+
|
|
255
281
|
// Buscar started_at da task para comparacao temporal
|
|
256
282
|
let taskStartTime: number | null = null;
|
|
257
283
|
if (context.taskId) {
|
|
@@ -262,6 +288,17 @@ function executeCheck(check: string, context: any): { passed: boolean; details?:
|
|
|
262
288
|
}
|
|
263
289
|
|
|
264
290
|
for (const file of context.files) {
|
|
291
|
+
if (!existsSync(file)) {
|
|
292
|
+
// Arquivo nao existe no disco — verificar se veio de subagent
|
|
293
|
+
if (subagentReportedFiles.has(file)) {
|
|
294
|
+
// Subagent reportou o arquivo via protocolo → confiar
|
|
295
|
+
// (arquivo pode estar em sandbox, sera materializado no commit)
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
issues.push(`${file}: arquivo nao encontrado no disco`);
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
|
|
265
302
|
const validation = validateFileContent(file);
|
|
266
303
|
if (!validation.valid) {
|
|
267
304
|
issues.push(`${file}: ${validation.reason}`);
|
|
@@ -277,7 +314,7 @@ function executeCheck(check: string, context: any): { passed: boolean; details?:
|
|
|
277
314
|
issues.push(`${file}: arquivo nao foi modificado durante esta task (mtime ${diffSec}s anterior ao start)`);
|
|
278
315
|
}
|
|
279
316
|
} catch {
|
|
280
|
-
// statSync falhou — arquivo pode nao existir (ja reportado
|
|
317
|
+
// statSync falhou — arquivo pode nao existir (ja reportado acima)
|
|
281
318
|
}
|
|
282
319
|
}
|
|
283
320
|
}
|
|
@@ -300,7 +337,7 @@ function executeCheck(check: string, context: any): { passed: boolean; details?:
|
|
|
300
337
|
|
|
301
338
|
// Obter agente da task
|
|
302
339
|
const task = db.query("SELECT * FROM tasks WHERE id = ?").get(context.taskId) as any;
|
|
303
|
-
const agentDomain = task?.agent
|
|
340
|
+
const agentDomain = domainToScope(getAgentDomain(task?.agent));
|
|
304
341
|
|
|
305
342
|
// Validar arquivos contra standards
|
|
306
343
|
const result = validateAgainstStandards(context.files, agentDomain);
|
|
@@ -331,13 +368,22 @@ function executeCheck(check: string, context: any): { passed: boolean; details?:
|
|
|
331
368
|
|
|
332
369
|
for (const file of context.files) {
|
|
333
370
|
if (!existsSync(file)) continue;
|
|
371
|
+
|
|
372
|
+
const fileScope = inferScopeFromPath(file);
|
|
373
|
+
// Arquivos de teste nao conflitam com production code
|
|
374
|
+
if (fileScope === "testing") continue;
|
|
375
|
+
|
|
334
376
|
const utilities = extractUtilitiesFromFile(file);
|
|
335
377
|
|
|
336
378
|
for (const util of utilities) {
|
|
337
379
|
const existing = findDuplicateUtilities(util.name, file);
|
|
338
|
-
|
|
380
|
+
// Filtrar: duplicatas em scope "testing" nao contam
|
|
381
|
+
const realDuplicates = existing.filter(
|
|
382
|
+
(e: any) => inferScopeFromPath(e.file_path) !== "testing"
|
|
383
|
+
);
|
|
384
|
+
if (realDuplicates.length > 0) {
|
|
339
385
|
duplicates.push(
|
|
340
|
-
`"${util.name}" (${util.type}) em ${file} -- ja existe em: ${
|
|
386
|
+
`"${util.name}" (${util.type}) em ${file} -- ja existe em: ${realDuplicates.map((e: any) => e.file_path).join(", ")}`
|
|
341
387
|
);
|
|
342
388
|
}
|
|
343
389
|
}
|
|
@@ -380,7 +426,12 @@ function executeCheck(check: string, context: any): { passed: boolean; details?:
|
|
|
380
426
|
|
|
381
427
|
if (!context.files || context.files.length === 0) return { passed: true };
|
|
382
428
|
|
|
383
|
-
|
|
429
|
+
// Buscar stack e custom command do projeto
|
|
430
|
+
const projectRow = db.query("SELECT stack, typecheck_command FROM project WHERE id = 'default'").get() as any;
|
|
431
|
+
const typecheckResult = runTypecheck(context.files, {
|
|
432
|
+
customCommand: projectRow?.typecheck_command || null,
|
|
433
|
+
stackJson: projectRow?.stack || null,
|
|
434
|
+
});
|
|
384
435
|
|
|
385
436
|
printTypecheckResult(typecheckResult);
|
|
386
437
|
|
|
@@ -426,6 +477,40 @@ function executeCheck(check: string, context: any): { passed: boolean; details?:
|
|
|
426
477
|
};
|
|
427
478
|
}
|
|
428
479
|
|
|
480
|
+
case "decision-conflicts": {
|
|
481
|
+
if (context.force) {
|
|
482
|
+
logGateBypass(context.taskId, "decision-conflicts", context.forceReason);
|
|
483
|
+
return { passed: true };
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
if (!context.subagentData?.decisions_made?.length) return { passed: true };
|
|
487
|
+
|
|
488
|
+
const dcTask = db.query("SELECT * FROM tasks WHERE id = ?").get(context.taskId) as any;
|
|
489
|
+
if (!dcTask) return { passed: true };
|
|
490
|
+
|
|
491
|
+
const existingDecisions = db
|
|
492
|
+
.query("SELECT * FROM decisions WHERE spec_id = ? AND status = 'active'")
|
|
493
|
+
.all(dcTask.spec_id) as any[];
|
|
494
|
+
if (existingDecisions.length === 0) return { passed: true };
|
|
495
|
+
|
|
496
|
+
const allConflicts: string[] = [];
|
|
497
|
+
for (const dec of context.subagentData.decisions_made) {
|
|
498
|
+
const analysis = detectConflicts(dec.title, dec.decision, existingDecisions);
|
|
499
|
+
if (analysis.hasConflict) {
|
|
500
|
+
for (const conflict of analysis.conflictingDecisions) {
|
|
501
|
+
allConflicts.push(
|
|
502
|
+
`"${dec.title}" conflita com ${conflict.id} ("${conflict.title}"): ${conflict.reason}`
|
|
503
|
+
);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
if (allConflicts.length > 0) {
|
|
509
|
+
return { passed: false, details: allConflicts.join("\n") };
|
|
510
|
+
}
|
|
511
|
+
return { passed: true };
|
|
512
|
+
}
|
|
513
|
+
|
|
429
514
|
case "reasoning-provided": {
|
|
430
515
|
if (!context.subagentData) return { passed: true };
|
|
431
516
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@codexa/cli",
|
|
3
|
-
"version": "9.0.
|
|
3
|
+
"version": "9.0.8",
|
|
4
4
|
"description": "Orchestrated workflow system for Claude Code - manages feature development through parallel subagents with structured phases, gates, and quality enforcement.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|