@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/commands/architect.test.ts +531 -0
- package/commands/architect.ts +75 -17
- package/commands/check.ts +7 -17
- package/commands/clear.ts +40 -1
- package/commands/decide.ts +37 -49
- package/commands/discover.ts +136 -28
- package/commands/knowledge.test.ts +160 -0
- package/commands/knowledge.ts +192 -102
- package/commands/patterns.test.ts +169 -0
- package/commands/patterns.ts +6 -13
- package/commands/plan.test.ts +73 -0
- package/commands/plan.ts +18 -66
- package/commands/product.ts +8 -17
- package/commands/research.ts +4 -3
- package/commands/review.ts +190 -28
- package/commands/spec-resolver.test.ts +119 -0
- package/commands/spec-resolver.ts +90 -0
- package/commands/standards.ts +7 -15
- package/commands/sync.ts +89 -0
- package/commands/task.ts +72 -167
- package/commands/utils.test.ts +100 -0
- package/commands/utils.ts +78 -706
- package/db/schema.test.ts +760 -0
- package/db/schema.ts +284 -130
- package/gates/validator.test.ts +675 -0
- package/gates/validator.ts +112 -27
- package/package.json +3 -1
- package/protocol/process-return.ts +25 -93
- package/protocol/subagent-protocol.test.ts +936 -0
- package/protocol/subagent-protocol.ts +19 -1
- package/workflow.ts +176 -67
package/gates/validator.ts
CHANGED
|
@@ -5,11 +5,14 @@ import { validateAgainstStandards, printValidationResult } from "./standards-val
|
|
|
5
5
|
import { runTypecheck, printTypecheckResult } from "./typecheck-validator";
|
|
6
6
|
import { extractUtilitiesFromFile } from "../commands/patterns";
|
|
7
7
|
import { findDuplicateUtilities } from "../db/schema";
|
|
8
|
+
import { GateError, RecoverySuggestion } from "../errors";
|
|
9
|
+
import { resolveSpecOrNull } from "../commands/spec-resolver";
|
|
8
10
|
|
|
9
11
|
export interface GateResult {
|
|
10
12
|
passed: boolean;
|
|
11
13
|
reason?: string;
|
|
12
14
|
resolution?: string;
|
|
15
|
+
recovery?: RecoverySuggestion;
|
|
13
16
|
}
|
|
14
17
|
|
|
15
18
|
interface GateCheck {
|
|
@@ -63,8 +66,8 @@ const GATES: Record<string, GateCheck[]> = {
|
|
|
63
66
|
},
|
|
64
67
|
{
|
|
65
68
|
check: "checkpoint-filled",
|
|
66
|
-
message: "Checkpoint obrigatorio",
|
|
67
|
-
resolution: "Forneca --checkpoint 'resumo do que foi feito'",
|
|
69
|
+
message: "Checkpoint obrigatorio (min 10 chars)",
|
|
70
|
+
resolution: "Forneca --checkpoint 'resumo do que foi feito' (min 10 caracteres)",
|
|
68
71
|
},
|
|
69
72
|
{
|
|
70
73
|
check: "files-exist",
|
|
@@ -108,9 +111,59 @@ const GATES: Record<string, GateCheck[]> = {
|
|
|
108
111
|
],
|
|
109
112
|
};
|
|
110
113
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
+
// v9.3: Estrategias de recuperacao por gate — diagnostico + passos concretos
|
|
115
|
+
const RECOVERY_STRATEGIES: Record<string, (details?: string) => RecoverySuggestion> = {
|
|
116
|
+
"standards-follow": (details) => ({
|
|
117
|
+
diagnostic: `Violacoes de standards detectadas:\n${details || "Detalhes nao disponiveis"}`,
|
|
118
|
+
steps: [
|
|
119
|
+
"Revise as violacoes listadas acima",
|
|
120
|
+
"Corrija o codigo para seguir os standards obrigatorios",
|
|
121
|
+
"Ou use --force --force-reason 'motivo' para bypass (auditado no review)",
|
|
122
|
+
],
|
|
123
|
+
command: "codexa context detail standards",
|
|
124
|
+
}),
|
|
125
|
+
"dry-check": (details) => ({
|
|
126
|
+
diagnostic: `Utilities duplicadas encontradas:\n${details || "Detalhes nao disponiveis"}`,
|
|
127
|
+
steps: [
|
|
128
|
+
"Verifique as utilities existentes",
|
|
129
|
+
"Importe do arquivo existente em vez de recriar",
|
|
130
|
+
"Se intencional, use --force --force-reason 'motivo'",
|
|
131
|
+
],
|
|
132
|
+
}),
|
|
133
|
+
"typecheck-pass": (details) => ({
|
|
134
|
+
diagnostic: `Erros TypeScript encontrados:\n${details || "Detalhes nao disponiveis"}`,
|
|
135
|
+
steps: [
|
|
136
|
+
"Corrija os erros de tipo listados",
|
|
137
|
+
"Verifique imports e definicoes de tipo",
|
|
138
|
+
"Se erros em deps externas, use --force --force-reason 'motivo'",
|
|
139
|
+
],
|
|
140
|
+
}),
|
|
141
|
+
"files-exist": (details) => ({
|
|
142
|
+
diagnostic: `Arquivos esperados nao encontrados:\n${details || "Detalhes nao disponiveis"}`,
|
|
143
|
+
steps: [
|
|
144
|
+
"Verifique se o subagent usou Write/Edit para criar os arquivos",
|
|
145
|
+
"Confirme que os caminhos em --files correspondem aos arquivos reais",
|
|
146
|
+
"Verifique conteudo valido (nao vazio, estrutura correta)",
|
|
147
|
+
],
|
|
148
|
+
}),
|
|
149
|
+
"checkpoint-filled": (details) => ({
|
|
150
|
+
diagnostic: `Checkpoint invalido: ${details || "muito curto ou incompleto"}`,
|
|
151
|
+
steps: [
|
|
152
|
+
"Forneca --checkpoint com resumo do que foi feito (min 10 chars)",
|
|
153
|
+
"Descreva O QUE foi feito, nao apenas 'feito' ou 'ok'",
|
|
154
|
+
],
|
|
155
|
+
}),
|
|
156
|
+
"reasoning-provided": () => ({
|
|
157
|
+
diagnostic: "Subagent retornou sem reasoning.approach adequado",
|
|
158
|
+
steps: [
|
|
159
|
+
"Inclua 'reasoning.approach' no retorno JSON do subagent (min 20 chars)",
|
|
160
|
+
"Descreva COMO o problema foi abordado",
|
|
161
|
+
],
|
|
162
|
+
}),
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
function getActiveSpec(specId?: string): any {
|
|
166
|
+
return resolveSpecOrNull(specId);
|
|
114
167
|
}
|
|
115
168
|
|
|
116
169
|
function executeCheck(check: string, context: any): { passed: boolean; details?: string } {
|
|
@@ -118,24 +171,24 @@ function executeCheck(check: string, context: any): { passed: boolean; details?:
|
|
|
118
171
|
|
|
119
172
|
switch (check) {
|
|
120
173
|
case "plan-exists": {
|
|
121
|
-
const spec = getActiveSpec();
|
|
174
|
+
const spec = getActiveSpec(context.specId);
|
|
122
175
|
return { passed: spec !== null };
|
|
123
176
|
}
|
|
124
177
|
|
|
125
178
|
case "has-tasks": {
|
|
126
|
-
const spec = getActiveSpec();
|
|
179
|
+
const spec = getActiveSpec(context.specId);
|
|
127
180
|
if (!spec) return { passed: false };
|
|
128
181
|
const count = db.query("SELECT COUNT(*) as c FROM tasks WHERE spec_id = ?").get(spec.id) as any;
|
|
129
182
|
return { passed: count.c > 0 };
|
|
130
183
|
}
|
|
131
184
|
|
|
132
185
|
case "phase-is-checking": {
|
|
133
|
-
const spec = getActiveSpec();
|
|
186
|
+
const spec = getActiveSpec(context.specId);
|
|
134
187
|
return { passed: spec?.phase === "checking" };
|
|
135
188
|
}
|
|
136
189
|
|
|
137
190
|
case "spec-approved": {
|
|
138
|
-
const spec = getActiveSpec();
|
|
191
|
+
const spec = getActiveSpec(context.specId);
|
|
139
192
|
return { passed: spec?.approved_at !== null };
|
|
140
193
|
}
|
|
141
194
|
|
|
@@ -164,13 +217,15 @@ function executeCheck(check: string, context: any): { passed: boolean; details?:
|
|
|
164
217
|
}
|
|
165
218
|
|
|
166
219
|
case "checkpoint-filled": {
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
220
|
+
const cp = context.checkpoint?.trim() || "";
|
|
221
|
+
if (cp.length < 10) {
|
|
222
|
+
return { passed: false, details: "Checkpoint deve ter pelo menos 10 caracteres" };
|
|
223
|
+
}
|
|
224
|
+
return { passed: true };
|
|
170
225
|
}
|
|
171
226
|
|
|
172
227
|
case "all-tasks-done": {
|
|
173
|
-
const spec = getActiveSpec();
|
|
228
|
+
const spec = getActiveSpec(context.specId);
|
|
174
229
|
if (!spec) return { passed: false };
|
|
175
230
|
const pending = db.query(
|
|
176
231
|
"SELECT number FROM tasks WHERE spec_id = ? AND status != 'done'"
|
|
@@ -182,7 +237,7 @@ function executeCheck(check: string, context: any): { passed: boolean; details?:
|
|
|
182
237
|
}
|
|
183
238
|
|
|
184
239
|
case "review-exists": {
|
|
185
|
-
const spec = getActiveSpec();
|
|
240
|
+
const spec = getActiveSpec(context.specId);
|
|
186
241
|
if (!spec) return { passed: false };
|
|
187
242
|
const review = db.query("SELECT * FROM review WHERE spec_id = ?").get(spec.id);
|
|
188
243
|
return { passed: review !== null };
|
|
@@ -192,12 +247,38 @@ function executeCheck(check: string, context: any): { passed: boolean; details?:
|
|
|
192
247
|
if (!context.files || context.files.length === 0) return { passed: true };
|
|
193
248
|
|
|
194
249
|
// v8.0: Validar não apenas existência, mas conteúdo mínimo
|
|
250
|
+
// v9.2: Validar que arquivo foi modificado DURANTE a task
|
|
251
|
+
// v9.3: Tolerancia de 5s para clock skew em sandbox
|
|
252
|
+
const MTIME_TOLERANCE_MS = 5000;
|
|
195
253
|
const issues: string[] = [];
|
|
196
254
|
|
|
255
|
+
// Buscar started_at da task para comparacao temporal
|
|
256
|
+
let taskStartTime: number | null = null;
|
|
257
|
+
if (context.taskId) {
|
|
258
|
+
const taskRow = db.query("SELECT started_at FROM tasks WHERE id = ?").get(context.taskId) as any;
|
|
259
|
+
if (taskRow?.started_at) {
|
|
260
|
+
taskStartTime = new Date(taskRow.started_at).getTime();
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
197
264
|
for (const file of context.files) {
|
|
198
265
|
const validation = validateFileContent(file);
|
|
199
266
|
if (!validation.valid) {
|
|
200
267
|
issues.push(`${file}: ${validation.reason}`);
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
// Verificar que arquivo foi tocado durante a task (com tolerancia)
|
|
271
|
+
if (taskStartTime) {
|
|
272
|
+
try {
|
|
273
|
+
const stat = statSync(file);
|
|
274
|
+
const mtime = stat.mtimeMs;
|
|
275
|
+
if (mtime < (taskStartTime - MTIME_TOLERANCE_MS)) {
|
|
276
|
+
const diffSec = Math.round((taskStartTime - mtime) / 1000);
|
|
277
|
+
issues.push(`${file}: arquivo nao foi modificado durante esta task (mtime ${diffSec}s anterior ao start)`);
|
|
278
|
+
}
|
|
279
|
+
} catch {
|
|
280
|
+
// statSync falhou — arquivo pode nao existir (ja reportado por validateFileContent)
|
|
281
|
+
}
|
|
201
282
|
}
|
|
202
283
|
}
|
|
203
284
|
|
|
@@ -312,7 +393,7 @@ function executeCheck(check: string, context: any): { passed: boolean; details?:
|
|
|
312
393
|
}
|
|
313
394
|
|
|
314
395
|
case "no-critical-blockers": {
|
|
315
|
-
const spec = getActiveSpec();
|
|
396
|
+
const spec = getActiveSpec(context.specId);
|
|
316
397
|
if (!spec) return { passed: true };
|
|
317
398
|
|
|
318
399
|
const allKnowledge = db
|
|
@@ -326,13 +407,10 @@ function executeCheck(check: string, context: any): { passed: boolean; details?:
|
|
|
326
407
|
.all(spec.id) as any[];
|
|
327
408
|
|
|
328
409
|
const unresolved = allKnowledge.filter((k: any) => {
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
} catch {
|
|
334
|
-
return true;
|
|
335
|
-
}
|
|
410
|
+
const hasAck = db.query(
|
|
411
|
+
"SELECT 1 FROM knowledge_acknowledgments WHERE knowledge_id = ?"
|
|
412
|
+
).get(k.id);
|
|
413
|
+
return !hasAck;
|
|
336
414
|
});
|
|
337
415
|
|
|
338
416
|
if (unresolved.length === 0) return { passed: true };
|
|
@@ -392,12 +470,16 @@ export function validateGate(command: string, context: any = {}): GateResult {
|
|
|
392
470
|
const result = executeCheck(gate.check, context);
|
|
393
471
|
|
|
394
472
|
if (!result.passed) {
|
|
473
|
+
const recoveryFn = RECOVERY_STRATEGIES[gate.check];
|
|
474
|
+
const recovery = recoveryFn ? recoveryFn(result.details) : undefined;
|
|
475
|
+
|
|
395
476
|
return {
|
|
396
477
|
passed: false,
|
|
397
478
|
reason: result.details
|
|
398
479
|
? `${gate.message}: ${result.details}`
|
|
399
480
|
: gate.message,
|
|
400
481
|
resolution: gate.resolution,
|
|
482
|
+
recovery,
|
|
401
483
|
};
|
|
402
484
|
}
|
|
403
485
|
}
|
|
@@ -409,14 +491,17 @@ export function enforceGate(command: string, context: any = {}): void {
|
|
|
409
491
|
const result = validateGate(command, context);
|
|
410
492
|
|
|
411
493
|
if (!result.passed) {
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
494
|
+
throw new GateError(
|
|
495
|
+
result.reason || "Gate falhou",
|
|
496
|
+
result.resolution || "Verifique o estado atual",
|
|
497
|
+
command,
|
|
498
|
+
result.recovery
|
|
499
|
+
);
|
|
415
500
|
}
|
|
416
501
|
}
|
|
417
502
|
|
|
418
503
|
// v8.0: Validar conteúdo de arquivos criados (não apenas existência)
|
|
419
|
-
interface FileValidationResult {
|
|
504
|
+
export interface FileValidationResult {
|
|
420
505
|
valid: boolean;
|
|
421
506
|
reason?: string;
|
|
422
507
|
}
|
|
@@ -456,7 +541,7 @@ function validateFileContent(filePath: string): FileValidationResult {
|
|
|
456
541
|
}
|
|
457
542
|
}
|
|
458
543
|
|
|
459
|
-
function validateByExtension(ext: string, content: string): FileValidationResult {
|
|
544
|
+
export function validateByExtension(ext: string, content: string): FileValidationResult {
|
|
460
545
|
const trimmed = content.trim();
|
|
461
546
|
|
|
462
547
|
switch (ext) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@codexa/cli",
|
|
3
|
-
"version": "9.0.
|
|
3
|
+
"version": "9.0.4",
|
|
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": {
|
|
@@ -16,6 +16,8 @@
|
|
|
16
16
|
],
|
|
17
17
|
"scripts": {
|
|
18
18
|
"cli": "bun run workflow.ts",
|
|
19
|
+
"test": "bun test",
|
|
20
|
+
"test:watch": "bun test --watch",
|
|
19
21
|
"install": "npm publish --access public"
|
|
20
22
|
},
|
|
21
23
|
"engines": {
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
import { getDb } from "../db/connection";
|
|
13
13
|
import { SubagentReturn, Knowledge } from "./subagent-protocol";
|
|
14
|
-
import { addReasoning, addGraphRelation,
|
|
14
|
+
import { addReasoning, addGraphRelation, upsertUtility, getNextDecisionId } from "../db/schema";
|
|
15
15
|
import { extractUtilitiesFromFile, inferScopeFromPath } from "../commands/patterns";
|
|
16
16
|
|
|
17
17
|
interface ProcessResult {
|
|
@@ -26,18 +26,6 @@ interface ProcessResult {
|
|
|
26
26
|
errors: string[];
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
function getNextDecisionId(specId: string): string {
|
|
30
|
-
const db = getDb();
|
|
31
|
-
const last = db
|
|
32
|
-
.query("SELECT id FROM decisions WHERE spec_id = ? ORDER BY created_at DESC LIMIT 1")
|
|
33
|
-
.get(specId) as any;
|
|
34
|
-
|
|
35
|
-
if (!last) return "DEC-001";
|
|
36
|
-
|
|
37
|
-
const num = parseInt(last.id.replace("DEC-", "")) + 1;
|
|
38
|
-
return `DEC-${num.toString().padStart(3, "0")}`;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
29
|
/**
|
|
42
30
|
* Processa o retorno de um subagent e registra automaticamente
|
|
43
31
|
* todos os dados extraidos no banco
|
|
@@ -85,87 +73,32 @@ export function processSubagentReturn(
|
|
|
85
73
|
}
|
|
86
74
|
}
|
|
87
75
|
|
|
88
|
-
// 2. Registrar Decisions
|
|
76
|
+
// 2. Registrar Decisions (com retry para collision de IDs concorrentes)
|
|
77
|
+
const savedDecisionIds: string[] = [];
|
|
89
78
|
if (data.decisions_made && data.decisions_made.length > 0) {
|
|
90
79
|
for (const dec of data.decisions_made) {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
const existingDecisions = db.query(
|
|
109
|
-
"SELECT id, title, decision FROM decisions WHERE spec_id = ? AND status = 'active'"
|
|
110
|
-
).all(specId) as any[];
|
|
111
|
-
|
|
112
|
-
const allTaskFiles = [...data.files_created, ...data.files_modified];
|
|
113
|
-
|
|
114
|
-
for (const newDec of data.decisions_made) {
|
|
115
|
-
for (const existingDec of existingDecisions) {
|
|
116
|
-
// Skip se mesma decisao (recem adicionada)
|
|
117
|
-
if (existingDec.title === newDec.title) continue;
|
|
118
|
-
|
|
119
|
-
const newDecLower = `${newDec.title} ${newDec.decision}`.toLowerCase();
|
|
120
|
-
const existDecLower = `${existingDec.title} ${existingDec.decision}`.toLowerCase();
|
|
121
|
-
|
|
122
|
-
// Verificar overlap de arquivos
|
|
123
|
-
const existingDecFiles = getRelatedFiles(existingDec.id, "decision");
|
|
124
|
-
const fileOverlap = allTaskFiles.some(f => existingDecFiles.includes(f));
|
|
125
|
-
|
|
126
|
-
if (fileOverlap) {
|
|
127
|
-
// Heuristica de keywords opostos
|
|
128
|
-
const opposites = [
|
|
129
|
-
['usar', 'nao usar'], ['usar', 'evitar'],
|
|
130
|
-
['use', 'avoid'], ['add', 'remove'],
|
|
131
|
-
['incluir', 'excluir'], ['habilitar', 'desabilitar'],
|
|
132
|
-
['enable', 'disable'], ['create', 'delete'],
|
|
133
|
-
['client', 'server'], ['sync', 'async'],
|
|
134
|
-
];
|
|
135
|
-
|
|
136
|
-
let isContradiction = false;
|
|
137
|
-
for (const [word1, word2] of opposites) {
|
|
138
|
-
if ((newDecLower.includes(word1) && existDecLower.includes(word2)) ||
|
|
139
|
-
(newDecLower.includes(word2) && existDecLower.includes(word1))) {
|
|
140
|
-
isContradiction = true;
|
|
141
|
-
break;
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
if (isContradiction) {
|
|
146
|
-
const newDecId = db.query(
|
|
147
|
-
"SELECT id FROM decisions WHERE spec_id = ? AND title = ? ORDER BY created_at DESC LIMIT 1"
|
|
148
|
-
).get(specId, newDec.title) as any;
|
|
149
|
-
|
|
150
|
-
if (newDecId) {
|
|
151
|
-
addGraphRelation(specId, {
|
|
152
|
-
sourceType: "decision",
|
|
153
|
-
sourceId: newDecId.id,
|
|
154
|
-
targetType: "decision",
|
|
155
|
-
targetId: existingDec.id,
|
|
156
|
-
relation: "contradicts",
|
|
157
|
-
strength: 0.7,
|
|
158
|
-
metadata: { reason: "Arquivos sobrepostos com semantica oposta" },
|
|
159
|
-
});
|
|
160
|
-
result.relationsAdded++;
|
|
161
|
-
}
|
|
162
|
-
}
|
|
80
|
+
let retries = 3;
|
|
81
|
+
let inserted = false;
|
|
82
|
+
while (retries > 0 && !inserted) {
|
|
83
|
+
try {
|
|
84
|
+
const decisionId = getNextDecisionId(specId);
|
|
85
|
+
db.run(
|
|
86
|
+
`INSERT INTO decisions (id, spec_id, task_ref, title, decision, rationale, status, created_at)
|
|
87
|
+
VALUES (?, ?, ?, ?, ?, ?, 'active', ?)`,
|
|
88
|
+
[decisionId, specId, taskNumber, dec.title, dec.decision, dec.rationale || null, now]
|
|
89
|
+
);
|
|
90
|
+
savedDecisionIds.push(decisionId);
|
|
91
|
+
result.decisionsAdded++;
|
|
92
|
+
inserted = true;
|
|
93
|
+
} catch (e: any) {
|
|
94
|
+
if (e.message?.includes("UNIQUE constraint") && retries > 1) {
|
|
95
|
+
retries--;
|
|
96
|
+
continue;
|
|
163
97
|
}
|
|
98
|
+
result.errors.push(`Erro ao registrar decision: ${(e as Error).message}`);
|
|
99
|
+
break;
|
|
164
100
|
}
|
|
165
101
|
}
|
|
166
|
-
} catch (e) {
|
|
167
|
-
// Nao-critico: nao falhar processamento por deteccao de contradicoes
|
|
168
|
-
result.errors.push(`Aviso contradicoes: ${(e as Error).message}`);
|
|
169
102
|
}
|
|
170
103
|
}
|
|
171
104
|
|
|
@@ -331,10 +264,9 @@ export function processSubagentReturn(
|
|
|
331
264
|
result.relationsAdded++;
|
|
332
265
|
}
|
|
333
266
|
|
|
334
|
-
// Relação: decision -> arquivos (
|
|
335
|
-
if (
|
|
336
|
-
for (const
|
|
337
|
-
const decisionId = getNextDecisionId(specId);
|
|
267
|
+
// Relação: decision -> arquivos (usa IDs salvos na seção 2, não gera novos)
|
|
268
|
+
if (savedDecisionIds.length > 0) {
|
|
269
|
+
for (const decisionId of savedDecisionIds) {
|
|
338
270
|
for (const file of [...data.files_created, ...data.files_modified]) {
|
|
339
271
|
addGraphRelation(specId, {
|
|
340
272
|
sourceType: "decision",
|