sdd-cli 0.1.25 → 0.1.26
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.
|
@@ -146,6 +146,59 @@ function detectBaselineKind(intent) {
|
|
|
146
146
|
}
|
|
147
147
|
return "generic";
|
|
148
148
|
}
|
|
149
|
+
function normalizeIntentText(intent) {
|
|
150
|
+
return intent
|
|
151
|
+
.toLowerCase()
|
|
152
|
+
.normalize("NFD")
|
|
153
|
+
.replace(/[\u0300-\u036f]/g, "");
|
|
154
|
+
}
|
|
155
|
+
function intentRequiresJavaReactFullstack(intent) {
|
|
156
|
+
const lower = normalizeIntentText(intent);
|
|
157
|
+
return /\bjava\b/.test(lower) && /\breact\b/.test(lower);
|
|
158
|
+
}
|
|
159
|
+
function intentSuggestsRelationalDataDomain(intent) {
|
|
160
|
+
const lower = normalizeIntentText(intent);
|
|
161
|
+
return [
|
|
162
|
+
"library",
|
|
163
|
+
"biblioteca",
|
|
164
|
+
"inventario",
|
|
165
|
+
"inventory",
|
|
166
|
+
"prestamo",
|
|
167
|
+
"prestamos",
|
|
168
|
+
"loan",
|
|
169
|
+
"loans",
|
|
170
|
+
"usuario",
|
|
171
|
+
"usuarios",
|
|
172
|
+
"user",
|
|
173
|
+
"users",
|
|
174
|
+
"book",
|
|
175
|
+
"books",
|
|
176
|
+
"cita",
|
|
177
|
+
"citas",
|
|
178
|
+
"appointment",
|
|
179
|
+
"appointments",
|
|
180
|
+
"hospital"
|
|
181
|
+
].some((token) => lower.includes(token));
|
|
182
|
+
}
|
|
183
|
+
function extraPromptConstraints(intent) {
|
|
184
|
+
const constraints = [];
|
|
185
|
+
if (intentRequiresJavaReactFullstack(intent)) {
|
|
186
|
+
constraints.push("Use split structure: backend/ (Java Spring Boot) and frontend/ (React + Vite).");
|
|
187
|
+
constraints.push("Backend must expose REST APIs for users, books, loans, and inventory.");
|
|
188
|
+
constraints.push("Frontend must consume backend APIs (do not keep data only in static mocks).");
|
|
189
|
+
constraints.push("Use modern React data layer: @tanstack/react-query (not react-query).");
|
|
190
|
+
constraints.push("Backend architecture must include DTO classes, service interfaces, and repository interfaces.");
|
|
191
|
+
constraints.push("Use Java records for immutable request/response or transport models.");
|
|
192
|
+
constraints.push("Frontend architecture must include src/api, src/hooks (use*.ts/tsx), and src/components layers.");
|
|
193
|
+
constraints.push("Include frontend tests and backend tests that run in local CI.");
|
|
194
|
+
}
|
|
195
|
+
if (intentSuggestsRelationalDataDomain(intent)) {
|
|
196
|
+
constraints.push("Use a scalable relational database default (prefer PostgreSQL).");
|
|
197
|
+
constraints.push("Include SQL schema file named schema.sql (or db/schema.sql) with tables, keys, indexes, and constraints.");
|
|
198
|
+
constraints.push("Document local database strategy in README and DummyLocal docs.");
|
|
199
|
+
}
|
|
200
|
+
return constraints;
|
|
201
|
+
}
|
|
149
202
|
function commonPackageJson(projectName) {
|
|
150
203
|
return `{
|
|
151
204
|
"name": "${projectName.toLowerCase().replace(/[^a-z0-9-]+/g, "-")}",
|
|
@@ -1087,12 +1140,14 @@ function bootstrapProjectCode(projectRoot, projectName, intent, providerRequeste
|
|
|
1087
1140
|
let files = [];
|
|
1088
1141
|
let fallbackReason;
|
|
1089
1142
|
if (resolution.ok) {
|
|
1143
|
+
const constraints = extraPromptConstraints(intent);
|
|
1090
1144
|
const prompt = [
|
|
1091
1145
|
"Generate a production-lean starter app from user intent.",
|
|
1092
1146
|
"The project must be executable fully in local development.",
|
|
1093
1147
|
"Use DummyLocal adapters for integrations (databases, external APIs, queues) so everything runs locally.",
|
|
1094
1148
|
"Add a schema document named schemas.md with entities, fields, relations, and constraints.",
|
|
1095
1149
|
"Add regression tests and regression notes/documentation.",
|
|
1150
|
+
...constraints,
|
|
1096
1151
|
"Do not mix unrelated runtime stacks unless the intent explicitly requests a multi-tier architecture.",
|
|
1097
1152
|
"Return ONLY valid JSON with this shape:",
|
|
1098
1153
|
'{"files":[{"path":"relative/path","content":"file content"}],"run_command":"...","deploy_steps":["..."],"publish_steps":["..."]}',
|
|
@@ -1120,11 +1175,13 @@ function bootstrapProjectCode(projectRoot, projectName, intent, providerRequeste
|
|
|
1120
1175
|
}
|
|
1121
1176
|
}
|
|
1122
1177
|
if (files.length === 0) {
|
|
1178
|
+
const fallbackConstraints = extraPromptConstraints(intent);
|
|
1123
1179
|
const fallbackPrompt = [
|
|
1124
1180
|
"Return ONLY valid JSON. No markdown.",
|
|
1125
1181
|
"Schema: {\"files\":[{\"path\":\"relative/path\",\"content\":\"...\"}]}",
|
|
1126
1182
|
"Generate only essential starter files to run locally with quality-first defaults.",
|
|
1127
1183
|
"Must include: README.md, schemas.md, regression notes, and DummyLocal integration docs.",
|
|
1184
|
+
...fallbackConstraints,
|
|
1128
1185
|
`Project: ${projectName}`,
|
|
1129
1186
|
`Intent: ${intent}`
|
|
1130
1187
|
].join("\n");
|
|
@@ -1186,7 +1243,7 @@ function bootstrapProjectCode(projectRoot, projectName, intent, providerRequeste
|
|
|
1186
1243
|
};
|
|
1187
1244
|
}
|
|
1188
1245
|
function compactFilesForPrompt(files) {
|
|
1189
|
-
const maxFiles =
|
|
1246
|
+
const maxFiles = 8;
|
|
1190
1247
|
const maxChars = 700;
|
|
1191
1248
|
return files.slice(0, maxFiles).map((file) => ({
|
|
1192
1249
|
path: file.path,
|
|
@@ -1231,6 +1288,12 @@ function improveGeneratedApp(appDir, intent, providerRequested, qualityDiagnosti
|
|
|
1231
1288
|
return { attempted: false, applied: false, fileCount: 0, reason: "provider unavailable" };
|
|
1232
1289
|
}
|
|
1233
1290
|
const currentFiles = compactFilesForPrompt(collectProjectFiles(appDir));
|
|
1291
|
+
const compactDiagnostics = (qualityDiagnostics ?? [])
|
|
1292
|
+
.map((line) => line.replace(/\u001b\[[0-9;]*m/g, "").replace(/\s+/g, " ").trim())
|
|
1293
|
+
.filter((line) => line.length > 0)
|
|
1294
|
+
.slice(0, 8)
|
|
1295
|
+
.map((line) => (line.length > 280 ? `${line.slice(0, 280)}...[truncated]` : line));
|
|
1296
|
+
const constraints = extraPromptConstraints(intent);
|
|
1234
1297
|
const prompt = [
|
|
1235
1298
|
"Improve this generated app to production-lean quality.",
|
|
1236
1299
|
"Requirements:",
|
|
@@ -1238,17 +1301,19 @@ function improveGeneratedApp(appDir, intent, providerRequested, qualityDiagnosti
|
|
|
1238
1301
|
"- Ensure tests pass for the selected stack.",
|
|
1239
1302
|
"- Ensure code is clear and maintainable.",
|
|
1240
1303
|
"- Ensure schemas.md exists and documents data schemas.",
|
|
1304
|
+
"- Ensure relational-data apps include schema.sql with proper keys/indexes.",
|
|
1241
1305
|
"- Ensure DummyLocal integration exists and is documented.",
|
|
1242
1306
|
"- Ensure regression tests (or explicit regression test documentation) exists.",
|
|
1307
|
+
...constraints.map((line) => `- ${line}`),
|
|
1243
1308
|
"- Fix every listed quality diagnostic failure.",
|
|
1244
1309
|
"Return ONLY JSON with shape:",
|
|
1245
1310
|
'{"files":[{"path":"relative/path","content":"full file content"}]}',
|
|
1246
1311
|
`Intent: ${intent}`,
|
|
1247
|
-
`Quality diagnostics: ${JSON.stringify(
|
|
1312
|
+
`Quality diagnostics: ${JSON.stringify(compactDiagnostics)}`,
|
|
1248
1313
|
`Current files JSON: ${JSON.stringify(currentFiles)}`
|
|
1249
1314
|
].join("\n");
|
|
1250
1315
|
let parsed = askProviderForJson(resolution.provider.exec, prompt);
|
|
1251
|
-
if ((!parsed || !Array.isArray(parsed.files)) &&
|
|
1316
|
+
if ((!parsed || !Array.isArray(parsed.files)) && compactDiagnostics.length > 0) {
|
|
1252
1317
|
const fileNames = collectProjectFiles(appDir).map((f) => f.path).slice(0, 120);
|
|
1253
1318
|
const targetedPrompt = [
|
|
1254
1319
|
"Return ONLY valid JSON. No markdown.",
|
|
@@ -1256,11 +1321,22 @@ function improveGeneratedApp(appDir, intent, providerRequested, qualityDiagnosti
|
|
|
1256
1321
|
"Fix exactly the listed quality diagnostics with minimal file edits.",
|
|
1257
1322
|
"If diagnostics mention missing docs/tests, generate them.",
|
|
1258
1323
|
`Intent: ${intent}`,
|
|
1259
|
-
`Quality diagnostics: ${JSON.stringify(
|
|
1324
|
+
`Quality diagnostics: ${JSON.stringify(compactDiagnostics)}`,
|
|
1260
1325
|
`Current file names: ${JSON.stringify(fileNames)}`
|
|
1261
1326
|
].join("\n");
|
|
1262
1327
|
parsed = askProviderForJson(resolution.provider.exec, targetedPrompt);
|
|
1263
1328
|
}
|
|
1329
|
+
if (!parsed || !Array.isArray(parsed.files)) {
|
|
1330
|
+
const minimalPrompt = [
|
|
1331
|
+
"Return ONLY valid JSON. No markdown.",
|
|
1332
|
+
'Schema: {"files":[{"path":"relative/path","content":"..."}]}',
|
|
1333
|
+
"Apply minimal patch set: 1 to 5 files only.",
|
|
1334
|
+
"Prioritize fixing the first quality diagnostic immediately.",
|
|
1335
|
+
`Intent: ${intent}`,
|
|
1336
|
+
`Top quality diagnostics: ${JSON.stringify(compactDiagnostics.slice(0, 2))}`
|
|
1337
|
+
].join("\n");
|
|
1338
|
+
parsed = askProviderForJson(resolution.provider.exec, minimalPrompt);
|
|
1339
|
+
}
|
|
1264
1340
|
if (!parsed || !Array.isArray(parsed.files)) {
|
|
1265
1341
|
return { attempted: true, applied: false, fileCount: 0, reason: "provider response unusable" };
|
|
1266
1342
|
}
|
|
@@ -9,6 +9,41 @@ const fs_1 = __importDefault(require("fs"));
|
|
|
9
9
|
const path_1 = __importDefault(require("path"));
|
|
10
10
|
const child_process_1 = require("child_process");
|
|
11
11
|
const config_1 = require("../config");
|
|
12
|
+
function collectFilesRecursive(root, maxDepth = 8) {
|
|
13
|
+
const results = [];
|
|
14
|
+
const walk = (current, depth) => {
|
|
15
|
+
if (depth > maxDepth) {
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
const entries = fs_1.default.readdirSync(current, { withFileTypes: true });
|
|
19
|
+
for (const entry of entries) {
|
|
20
|
+
const full = path_1.default.join(current, entry.name);
|
|
21
|
+
const rel = path_1.default.relative(root, full).replace(/\\/g, "/");
|
|
22
|
+
if (entry.isDirectory()) {
|
|
23
|
+
if ([".git", "node_modules", "dist", "build", "target", "__pycache__", ".venv", "venv"].includes(entry.name.toLowerCase())) {
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
walk(full, depth + 1);
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
results.push(rel);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
walk(root, 0);
|
|
34
|
+
return results;
|
|
35
|
+
}
|
|
36
|
+
function countJsTsTests(root, maxDepth = 8) {
|
|
37
|
+
const files = collectFilesRecursive(root, maxDepth)
|
|
38
|
+
.filter((rel) => /\.(jsx?|tsx?)$/i.test(rel))
|
|
39
|
+
.filter((rel) => /\.test\.|\.spec\.|__tests__\//i.test(rel));
|
|
40
|
+
let count = 0;
|
|
41
|
+
for (const rel of files) {
|
|
42
|
+
const raw = fs_1.default.readFileSync(path_1.default.join(root, rel), "utf-8");
|
|
43
|
+
count += (raw.match(/\b(test|it)\s*\(/g) || []).length;
|
|
44
|
+
}
|
|
45
|
+
return count;
|
|
46
|
+
}
|
|
12
47
|
function findFileRecursive(root, predicate, maxDepth = 4) {
|
|
13
48
|
const walk = (current, depth) => {
|
|
14
49
|
if (depth > maxDepth) {
|
|
@@ -73,14 +108,21 @@ function countTestsRecursive(root, maxDepth = 8) {
|
|
|
73
108
|
}
|
|
74
109
|
function run(command, args, cwd) {
|
|
75
110
|
let resolved = command;
|
|
76
|
-
if (process.platform === "win32"
|
|
77
|
-
|
|
111
|
+
if (process.platform === "win32") {
|
|
112
|
+
if (command === "npm") {
|
|
113
|
+
resolved = "npm.cmd";
|
|
114
|
+
}
|
|
115
|
+
else if (command === "mvn") {
|
|
116
|
+
resolved = "mvn.cmd";
|
|
117
|
+
}
|
|
78
118
|
}
|
|
79
119
|
const useShell = process.platform === "win32" && resolved.toLowerCase().endsWith(".cmd");
|
|
80
120
|
const result = useShell
|
|
81
121
|
? (0, child_process_1.spawnSync)([resolved, ...args].join(" "), { cwd, encoding: "utf-8", shell: true })
|
|
82
122
|
: (0, child_process_1.spawnSync)(resolved, args, { cwd, encoding: "utf-8", shell: false });
|
|
83
|
-
const
|
|
123
|
+
const rawOutput = `${result.stdout || ""}${result.stderr || ""}`.trim();
|
|
124
|
+
const merged = rawOutput || (result.error ? String(result.error.message || result.error) : "");
|
|
125
|
+
const output = merged.length > 3500 ? `${merged.slice(0, 3500)}\n...[truncated]` : merged;
|
|
84
126
|
return {
|
|
85
127
|
ok: result.status === 0,
|
|
86
128
|
command: [resolved, ...args].join(" "),
|
|
@@ -107,19 +149,58 @@ function runIfScript(cwd, script) {
|
|
|
107
149
|
return null;
|
|
108
150
|
}
|
|
109
151
|
}
|
|
110
|
-
function
|
|
152
|
+
function readPackageJson(cwd) {
|
|
111
153
|
const pkgPath = path_1.default.join(cwd, "package.json");
|
|
112
154
|
if (!fs_1.default.existsSync(pkgPath)) {
|
|
113
|
-
return
|
|
155
|
+
return null;
|
|
114
156
|
}
|
|
115
157
|
try {
|
|
116
|
-
|
|
117
|
-
const depCount = Object.keys(pkg.dependencies ?? {}).length + Object.keys(pkg.devDependencies ?? {}).length;
|
|
118
|
-
return depCount > 0;
|
|
158
|
+
return JSON.parse(fs_1.default.readFileSync(pkgPath, "utf-8"));
|
|
119
159
|
}
|
|
120
160
|
catch {
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
function packageNeedsInstall(cwd) {
|
|
165
|
+
const pkg = readPackageJson(cwd);
|
|
166
|
+
if (!pkg) {
|
|
121
167
|
return false;
|
|
122
168
|
}
|
|
169
|
+
const depCount = Object.keys(pkg.dependencies ?? {}).length + Object.keys(pkg.devDependencies ?? {}).length;
|
|
170
|
+
return depCount > 0;
|
|
171
|
+
}
|
|
172
|
+
function parseGoalProfile(context) {
|
|
173
|
+
const goal = normalizeText(context?.goalText ?? "");
|
|
174
|
+
const hasJava = /\bjava\b/.test(goal);
|
|
175
|
+
const hasReact = /\breact\b/.test(goal);
|
|
176
|
+
const relationalHints = [
|
|
177
|
+
"library",
|
|
178
|
+
"biblioteca",
|
|
179
|
+
"inventario",
|
|
180
|
+
"inventory",
|
|
181
|
+
"prestamo",
|
|
182
|
+
"prestamos",
|
|
183
|
+
"loan",
|
|
184
|
+
"loans",
|
|
185
|
+
"usuario",
|
|
186
|
+
"usuarios",
|
|
187
|
+
"user",
|
|
188
|
+
"users",
|
|
189
|
+
"book",
|
|
190
|
+
"books",
|
|
191
|
+
"cita",
|
|
192
|
+
"citas",
|
|
193
|
+
"appointment",
|
|
194
|
+
"appointments",
|
|
195
|
+
"hospital",
|
|
196
|
+
"gestion",
|
|
197
|
+
"management"
|
|
198
|
+
];
|
|
199
|
+
const relationalDataApp = relationalHints.some((hint) => goal.includes(hint));
|
|
200
|
+
return {
|
|
201
|
+
javaReactFullstack: hasJava && hasReact,
|
|
202
|
+
relationalDataApp
|
|
203
|
+
};
|
|
123
204
|
}
|
|
124
205
|
function basicQualityCheck(appDir) {
|
|
125
206
|
const required = ["README.md"];
|
|
@@ -192,6 +273,112 @@ function advancedQualityCheck(appDir, context) {
|
|
|
192
273
|
output: "Missing schemas.md (or equivalent schema markdown document)"
|
|
193
274
|
};
|
|
194
275
|
}
|
|
276
|
+
const profile = parseGoalProfile(context);
|
|
277
|
+
if (profile.relationalDataApp) {
|
|
278
|
+
const sqlSchema = findFileRecursive(appDir, (rel) => rel === "schema.sql" || rel.endsWith("/schema.sql")) ??
|
|
279
|
+
findFileRecursive(appDir, (rel) => rel.endsWith(".sql") && (rel.includes("schema") || rel.includes("init") || rel.includes("migration")), 10);
|
|
280
|
+
if (!sqlSchema) {
|
|
281
|
+
return {
|
|
282
|
+
ok: false,
|
|
283
|
+
command: "advanced-quality-check",
|
|
284
|
+
output: "Missing SQL schema file (expected schema.sql or migration .sql) for relational data app"
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
const schemaText = normalizeText(fs_1.default.readFileSync(path_1.default.join(appDir, schemaDoc), "utf-8"));
|
|
288
|
+
const readmeText = readme;
|
|
289
|
+
if (!/(postgres|postgresql|mysql|mariadb|sqlite)/.test(`${schemaText}\n${readmeText}`)) {
|
|
290
|
+
return {
|
|
291
|
+
ok: false,
|
|
292
|
+
command: "advanced-quality-check",
|
|
293
|
+
output: "Database technology not explicit in README/schemas (expected PostgreSQL/MySQL/MariaDB/SQLite)"
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
if (profile.javaReactFullstack) {
|
|
298
|
+
const hasBackendPom = fs_1.default.existsSync(path_1.default.join(appDir, "backend", "pom.xml"));
|
|
299
|
+
const hasFrontendPkg = fs_1.default.existsSync(path_1.default.join(appDir, "frontend", "package.json"));
|
|
300
|
+
if (!hasBackendPom || !hasFrontendPkg) {
|
|
301
|
+
return {
|
|
302
|
+
ok: false,
|
|
303
|
+
command: "advanced-quality-check",
|
|
304
|
+
output: "Expected Java+React fullstack structure: backend/pom.xml and frontend/package.json"
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
const frontendPkg = readPackageJson(path_1.default.join(appDir, "frontend"));
|
|
308
|
+
if (frontendPkg) {
|
|
309
|
+
const deps = { ...(frontendPkg.dependencies ?? {}), ...(frontendPkg.devDependencies ?? {}) };
|
|
310
|
+
if (typeof deps["react-query"] === "string") {
|
|
311
|
+
return {
|
|
312
|
+
ok: false,
|
|
313
|
+
command: "advanced-quality-check",
|
|
314
|
+
output: "Outdated frontend dependency detected: react-query. Use @tanstack/react-query."
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
const backendRoot = path_1.default.join(appDir, "backend", "src", "main", "java");
|
|
319
|
+
if (!fs_1.default.existsSync(backendRoot)) {
|
|
320
|
+
return {
|
|
321
|
+
ok: false,
|
|
322
|
+
command: "advanced-quality-check",
|
|
323
|
+
output: "Missing backend/src/main/java for Java backend implementation"
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
const backendFiles = collectFilesRecursive(backendRoot, 12).filter((rel) => rel.toLowerCase().endsWith(".java"));
|
|
327
|
+
const hasDto = backendFiles.some((rel) => /\/dto\//i.test(`/${rel}`) || /dto\.java$/i.test(rel));
|
|
328
|
+
if (!hasDto) {
|
|
329
|
+
return {
|
|
330
|
+
ok: false,
|
|
331
|
+
command: "advanced-quality-check",
|
|
332
|
+
output: "Missing Java DTO layer (expected dto package and *Dto.java files)"
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
const hasRecord = backendFiles.some((rel) => /\brecord\b/.test(fs_1.default.readFileSync(path_1.default.join(backendRoot, rel), "utf-8")));
|
|
336
|
+
if (!hasRecord) {
|
|
337
|
+
return {
|
|
338
|
+
ok: false,
|
|
339
|
+
command: "advanced-quality-check",
|
|
340
|
+
output: "Missing Java record usage (expected at least one record for immutable transport/domain models)"
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
const serviceFiles = backendFiles.filter((rel) => /\/service\//i.test(`/${rel}`));
|
|
344
|
+
const repositoryFiles = backendFiles.filter((rel) => /\/repository\//i.test(`/${rel}`));
|
|
345
|
+
const hasServiceInterface = serviceFiles.some((rel) => /\binterface\b/.test(fs_1.default.readFileSync(path_1.default.join(backendRoot, rel), "utf-8")));
|
|
346
|
+
const hasRepositoryInterface = repositoryFiles.some((rel) => /\binterface\b/.test(fs_1.default.readFileSync(path_1.default.join(backendRoot, rel), "utf-8")));
|
|
347
|
+
if (!hasServiceInterface || !hasRepositoryInterface) {
|
|
348
|
+
return {
|
|
349
|
+
ok: false,
|
|
350
|
+
command: "advanced-quality-check",
|
|
351
|
+
output: "Missing service/repository interfaces in Java backend architecture"
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
const frontendRoot = path_1.default.join(appDir, "frontend", "src");
|
|
355
|
+
if (!fs_1.default.existsSync(frontendRoot)) {
|
|
356
|
+
return {
|
|
357
|
+
ok: false,
|
|
358
|
+
command: "advanced-quality-check",
|
|
359
|
+
output: "Missing frontend/src for React frontend implementation"
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
const frontendFiles = collectFilesRecursive(frontendRoot, 10);
|
|
363
|
+
const hasApiLayer = frontendFiles.some((rel) => /^api\//i.test(rel) || /\/api\//i.test(`/${rel}`));
|
|
364
|
+
const hasHooksLayer = frontendFiles.some((rel) => /^hooks\/use[A-Z].*\.(t|j)sx?$/i.test(rel) || /\/hooks\/use[A-Z]/.test(rel));
|
|
365
|
+
const hasComponentsLayer = frontendFiles.some((rel) => /^components\//i.test(rel) || /\/components\//i.test(`/${rel}`));
|
|
366
|
+
if (!hasApiLayer || !hasHooksLayer || !hasComponentsLayer) {
|
|
367
|
+
return {
|
|
368
|
+
ok: false,
|
|
369
|
+
command: "advanced-quality-check",
|
|
370
|
+
output: "Missing frontend layers (expected src/api, src/hooks/use*.ts(x), and src/components)"
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
const frontendTestCount = countJsTsTests(path_1.default.join(appDir, "frontend"), 10);
|
|
374
|
+
if (frontendTestCount < 3) {
|
|
375
|
+
return {
|
|
376
|
+
ok: false,
|
|
377
|
+
command: "advanced-quality-check",
|
|
378
|
+
output: `Expected at least 3 frontend tests for Java+React profile, found ${frontendTestCount}`
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
}
|
|
195
382
|
const dummyLocalDoc = findFileRecursive(appDir, (rel) => rel.includes("dummylocal") && rel.endsWith(".md")) ??
|
|
196
383
|
findFileRecursive(appDir, (rel) => rel.includes("dummy-local") && rel.endsWith(".md")) ??
|
|
197
384
|
findFileRecursive(appDir, (rel) => rel.includes("dummy_local") && rel.endsWith(".md"));
|
|
@@ -463,6 +650,34 @@ function runAppLifecycle(projectRoot, projectName, context) {
|
|
|
463
650
|
const build = runIfScript(appDir, "build");
|
|
464
651
|
if (build)
|
|
465
652
|
qualitySteps.push(build);
|
|
653
|
+
const backendDir = path_1.default.join(appDir, "backend");
|
|
654
|
+
if (fs_1.default.existsSync(path_1.default.join(backendDir, "pom.xml"))) {
|
|
655
|
+
if (hasCommand("mvn")) {
|
|
656
|
+
qualitySteps.push(run("mvn", ["-q", "test"], backendDir));
|
|
657
|
+
}
|
|
658
|
+
else {
|
|
659
|
+
qualitySteps.push({
|
|
660
|
+
ok: false,
|
|
661
|
+
command: "mvn -q test",
|
|
662
|
+
output: "Maven not available to validate Java backend"
|
|
663
|
+
});
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
const frontendDir = path_1.default.join(appDir, "frontend");
|
|
667
|
+
if (fs_1.default.existsSync(path_1.default.join(frontendDir, "package.json"))) {
|
|
668
|
+
if (packageNeedsInstall(frontendDir)) {
|
|
669
|
+
qualitySteps.push(run("npm", ["install"], frontendDir));
|
|
670
|
+
}
|
|
671
|
+
const feLint = runIfScript(frontendDir, "lint");
|
|
672
|
+
if (feLint)
|
|
673
|
+
qualitySteps.push(feLint);
|
|
674
|
+
const feTest = runIfScript(frontendDir, "test");
|
|
675
|
+
if (feTest)
|
|
676
|
+
qualitySteps.push(feTest);
|
|
677
|
+
const feBuild = runIfScript(frontendDir, "build");
|
|
678
|
+
if (feBuild)
|
|
679
|
+
qualitySteps.push(feBuild);
|
|
680
|
+
}
|
|
466
681
|
qualitySteps.push(advancedQualityCheck(appDir, context));
|
|
467
682
|
if (qualitySteps.length === 0) {
|
|
468
683
|
qualitySteps.push(basicQualityCheck(appDir));
|
package/dist/commands/hello.js
CHANGED
|
@@ -418,7 +418,7 @@ async function runHello(input, runQuestions) {
|
|
|
418
418
|
if (!lifecycleDisabled && !lifecycle.qualityPassed) {
|
|
419
419
|
const appDir = path_1.default.join(projectRoot, "generated-app");
|
|
420
420
|
const parsedAttempts = Number.parseInt(process.env.SDD_AI_REPAIR_MAX_ATTEMPTS ?? "", 10);
|
|
421
|
-
const maxRepairAttempts = Number.isFinite(parsedAttempts) && parsedAttempts > 0 ? parsedAttempts :
|
|
421
|
+
const maxRepairAttempts = Number.isFinite(parsedAttempts) && parsedAttempts > 0 ? parsedAttempts : 10;
|
|
422
422
|
printWhy("Quality gates failed. Attempting AI repair iterations.");
|
|
423
423
|
lifecycle.qualityDiagnostics.forEach((issue) => printWhy(`Quality issue: ${issue}`));
|
|
424
424
|
for (let attempt = 1; attempt <= maxRepairAttempts && !lifecycle.qualityPassed; attempt += 1) {
|