sdd-cli 0.1.25 → 0.1.27
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,66 @@ 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("Use Lombok in backend entities/DTOs where appropriate (builder/getter/setter/constructor patterns).");
|
|
193
|
+
constraints.push("Use Jakarta/Javax Bean Validation annotations and @Valid in request boundaries.");
|
|
194
|
+
constraints.push("Include @RestControllerAdvice for global exception handling.");
|
|
195
|
+
constraints.push("Add Spring Actuator telemetry and basic Prometheus-friendly metrics configuration.");
|
|
196
|
+
constraints.push("Frontend architecture must include src/api, src/hooks (use*.ts/tsx), and src/components layers.");
|
|
197
|
+
constraints.push("Frontend bootstrap must use React.StrictMode.");
|
|
198
|
+
constraints.push("Frontend should include safe input validation and avoid direct unsafe HTML rendering.");
|
|
199
|
+
constraints.push("Include frontend tests and backend tests that run in local CI.");
|
|
200
|
+
constraints.push("Include architecture.md and execution-guide.md with clear local run instructions.");
|
|
201
|
+
}
|
|
202
|
+
if (intentSuggestsRelationalDataDomain(intent)) {
|
|
203
|
+
constraints.push("Use a scalable relational database default (prefer PostgreSQL).");
|
|
204
|
+
constraints.push("Include SQL schema file named schema.sql (or db/schema.sql) with tables, keys, indexes, and constraints.");
|
|
205
|
+
constraints.push("Document local database strategy in README and DummyLocal docs.");
|
|
206
|
+
}
|
|
207
|
+
return constraints;
|
|
208
|
+
}
|
|
149
209
|
function commonPackageJson(projectName) {
|
|
150
210
|
return `{
|
|
151
211
|
"name": "${projectName.toLowerCase().replace(/[^a-z0-9-]+/g, "-")}",
|
|
@@ -1087,12 +1147,14 @@ function bootstrapProjectCode(projectRoot, projectName, intent, providerRequeste
|
|
|
1087
1147
|
let files = [];
|
|
1088
1148
|
let fallbackReason;
|
|
1089
1149
|
if (resolution.ok) {
|
|
1150
|
+
const constraints = extraPromptConstraints(intent);
|
|
1090
1151
|
const prompt = [
|
|
1091
1152
|
"Generate a production-lean starter app from user intent.",
|
|
1092
1153
|
"The project must be executable fully in local development.",
|
|
1093
1154
|
"Use DummyLocal adapters for integrations (databases, external APIs, queues) so everything runs locally.",
|
|
1094
1155
|
"Add a schema document named schemas.md with entities, fields, relations, and constraints.",
|
|
1095
1156
|
"Add regression tests and regression notes/documentation.",
|
|
1157
|
+
...constraints,
|
|
1096
1158
|
"Do not mix unrelated runtime stacks unless the intent explicitly requests a multi-tier architecture.",
|
|
1097
1159
|
"Return ONLY valid JSON with this shape:",
|
|
1098
1160
|
'{"files":[{"path":"relative/path","content":"file content"}],"run_command":"...","deploy_steps":["..."],"publish_steps":["..."]}',
|
|
@@ -1120,11 +1182,13 @@ function bootstrapProjectCode(projectRoot, projectName, intent, providerRequeste
|
|
|
1120
1182
|
}
|
|
1121
1183
|
}
|
|
1122
1184
|
if (files.length === 0) {
|
|
1185
|
+
const fallbackConstraints = extraPromptConstraints(intent);
|
|
1123
1186
|
const fallbackPrompt = [
|
|
1124
1187
|
"Return ONLY valid JSON. No markdown.",
|
|
1125
1188
|
"Schema: {\"files\":[{\"path\":\"relative/path\",\"content\":\"...\"}]}",
|
|
1126
1189
|
"Generate only essential starter files to run locally with quality-first defaults.",
|
|
1127
1190
|
"Must include: README.md, schemas.md, regression notes, and DummyLocal integration docs.",
|
|
1191
|
+
...fallbackConstraints,
|
|
1128
1192
|
`Project: ${projectName}`,
|
|
1129
1193
|
`Intent: ${intent}`
|
|
1130
1194
|
].join("\n");
|
|
@@ -1186,7 +1250,7 @@ function bootstrapProjectCode(projectRoot, projectName, intent, providerRequeste
|
|
|
1186
1250
|
};
|
|
1187
1251
|
}
|
|
1188
1252
|
function compactFilesForPrompt(files) {
|
|
1189
|
-
const maxFiles =
|
|
1253
|
+
const maxFiles = 8;
|
|
1190
1254
|
const maxChars = 700;
|
|
1191
1255
|
return files.slice(0, maxFiles).map((file) => ({
|
|
1192
1256
|
path: file.path,
|
|
@@ -1231,6 +1295,12 @@ function improveGeneratedApp(appDir, intent, providerRequested, qualityDiagnosti
|
|
|
1231
1295
|
return { attempted: false, applied: false, fileCount: 0, reason: "provider unavailable" };
|
|
1232
1296
|
}
|
|
1233
1297
|
const currentFiles = compactFilesForPrompt(collectProjectFiles(appDir));
|
|
1298
|
+
const compactDiagnostics = (qualityDiagnostics ?? [])
|
|
1299
|
+
.map((line) => line.replace(/\u001b\[[0-9;]*m/g, "").replace(/\s+/g, " ").trim())
|
|
1300
|
+
.filter((line) => line.length > 0)
|
|
1301
|
+
.slice(0, 8)
|
|
1302
|
+
.map((line) => (line.length > 280 ? `${line.slice(0, 280)}...[truncated]` : line));
|
|
1303
|
+
const constraints = extraPromptConstraints(intent);
|
|
1234
1304
|
const prompt = [
|
|
1235
1305
|
"Improve this generated app to production-lean quality.",
|
|
1236
1306
|
"Requirements:",
|
|
@@ -1238,17 +1308,19 @@ function improveGeneratedApp(appDir, intent, providerRequested, qualityDiagnosti
|
|
|
1238
1308
|
"- Ensure tests pass for the selected stack.",
|
|
1239
1309
|
"- Ensure code is clear and maintainable.",
|
|
1240
1310
|
"- Ensure schemas.md exists and documents data schemas.",
|
|
1311
|
+
"- Ensure relational-data apps include schema.sql with proper keys/indexes.",
|
|
1241
1312
|
"- Ensure DummyLocal integration exists and is documented.",
|
|
1242
1313
|
"- Ensure regression tests (or explicit regression test documentation) exists.",
|
|
1314
|
+
...constraints.map((line) => `- ${line}`),
|
|
1243
1315
|
"- Fix every listed quality diagnostic failure.",
|
|
1244
1316
|
"Return ONLY JSON with shape:",
|
|
1245
1317
|
'{"files":[{"path":"relative/path","content":"full file content"}]}',
|
|
1246
1318
|
`Intent: ${intent}`,
|
|
1247
|
-
`Quality diagnostics: ${JSON.stringify(
|
|
1319
|
+
`Quality diagnostics: ${JSON.stringify(compactDiagnostics)}`,
|
|
1248
1320
|
`Current files JSON: ${JSON.stringify(currentFiles)}`
|
|
1249
1321
|
].join("\n");
|
|
1250
1322
|
let parsed = askProviderForJson(resolution.provider.exec, prompt);
|
|
1251
|
-
if ((!parsed || !Array.isArray(parsed.files)) &&
|
|
1323
|
+
if ((!parsed || !Array.isArray(parsed.files)) && compactDiagnostics.length > 0) {
|
|
1252
1324
|
const fileNames = collectProjectFiles(appDir).map((f) => f.path).slice(0, 120);
|
|
1253
1325
|
const targetedPrompt = [
|
|
1254
1326
|
"Return ONLY valid JSON. No markdown.",
|
|
@@ -1256,11 +1328,22 @@ function improveGeneratedApp(appDir, intent, providerRequested, qualityDiagnosti
|
|
|
1256
1328
|
"Fix exactly the listed quality diagnostics with minimal file edits.",
|
|
1257
1329
|
"If diagnostics mention missing docs/tests, generate them.",
|
|
1258
1330
|
`Intent: ${intent}`,
|
|
1259
|
-
`Quality diagnostics: ${JSON.stringify(
|
|
1331
|
+
`Quality diagnostics: ${JSON.stringify(compactDiagnostics)}`,
|
|
1260
1332
|
`Current file names: ${JSON.stringify(fileNames)}`
|
|
1261
1333
|
].join("\n");
|
|
1262
1334
|
parsed = askProviderForJson(resolution.provider.exec, targetedPrompt);
|
|
1263
1335
|
}
|
|
1336
|
+
if (!parsed || !Array.isArray(parsed.files)) {
|
|
1337
|
+
const minimalPrompt = [
|
|
1338
|
+
"Return ONLY valid JSON. No markdown.",
|
|
1339
|
+
'Schema: {"files":[{"path":"relative/path","content":"..."}]}',
|
|
1340
|
+
"Apply minimal patch set: 1 to 5 files only.",
|
|
1341
|
+
"Prioritize fixing the first quality diagnostic immediately.",
|
|
1342
|
+
`Intent: ${intent}`,
|
|
1343
|
+
`Top quality diagnostics: ${JSON.stringify(compactDiagnostics.slice(0, 2))}`
|
|
1344
|
+
].join("\n");
|
|
1345
|
+
parsed = askProviderForJson(resolution.provider.exec, minimalPrompt);
|
|
1346
|
+
}
|
|
1264
1347
|
if (!parsed || !Array.isArray(parsed.files)) {
|
|
1265
1348
|
return { attempted: true, applied: false, fileCount: 0, reason: "provider response unusable" };
|
|
1266
1349
|
}
|
|
@@ -9,6 +9,50 @@ 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
|
+
}
|
|
47
|
+
function fileExistsAny(root, candidates) {
|
|
48
|
+
for (const candidate of candidates) {
|
|
49
|
+
const full = path_1.default.join(root, candidate);
|
|
50
|
+
if (fs_1.default.existsSync(full)) {
|
|
51
|
+
return full;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
12
56
|
function findFileRecursive(root, predicate, maxDepth = 4) {
|
|
13
57
|
const walk = (current, depth) => {
|
|
14
58
|
if (depth > maxDepth) {
|
|
@@ -73,14 +117,21 @@ function countTestsRecursive(root, maxDepth = 8) {
|
|
|
73
117
|
}
|
|
74
118
|
function run(command, args, cwd) {
|
|
75
119
|
let resolved = command;
|
|
76
|
-
if (process.platform === "win32"
|
|
77
|
-
|
|
120
|
+
if (process.platform === "win32") {
|
|
121
|
+
if (command === "npm") {
|
|
122
|
+
resolved = "npm.cmd";
|
|
123
|
+
}
|
|
124
|
+
else if (command === "mvn") {
|
|
125
|
+
resolved = "mvn.cmd";
|
|
126
|
+
}
|
|
78
127
|
}
|
|
79
128
|
const useShell = process.platform === "win32" && resolved.toLowerCase().endsWith(".cmd");
|
|
80
129
|
const result = useShell
|
|
81
130
|
? (0, child_process_1.spawnSync)([resolved, ...args].join(" "), { cwd, encoding: "utf-8", shell: true })
|
|
82
131
|
: (0, child_process_1.spawnSync)(resolved, args, { cwd, encoding: "utf-8", shell: false });
|
|
83
|
-
const
|
|
132
|
+
const rawOutput = `${result.stdout || ""}${result.stderr || ""}`.trim();
|
|
133
|
+
const merged = rawOutput || (result.error ? String(result.error.message || result.error) : "");
|
|
134
|
+
const output = merged.length > 3500 ? `${merged.slice(0, 3500)}\n...[truncated]` : merged;
|
|
84
135
|
return {
|
|
85
136
|
ok: result.status === 0,
|
|
86
137
|
command: [resolved, ...args].join(" "),
|
|
@@ -107,19 +158,58 @@ function runIfScript(cwd, script) {
|
|
|
107
158
|
return null;
|
|
108
159
|
}
|
|
109
160
|
}
|
|
110
|
-
function
|
|
161
|
+
function readPackageJson(cwd) {
|
|
111
162
|
const pkgPath = path_1.default.join(cwd, "package.json");
|
|
112
163
|
if (!fs_1.default.existsSync(pkgPath)) {
|
|
113
|
-
return
|
|
164
|
+
return null;
|
|
114
165
|
}
|
|
115
166
|
try {
|
|
116
|
-
|
|
117
|
-
const depCount = Object.keys(pkg.dependencies ?? {}).length + Object.keys(pkg.devDependencies ?? {}).length;
|
|
118
|
-
return depCount > 0;
|
|
167
|
+
return JSON.parse(fs_1.default.readFileSync(pkgPath, "utf-8"));
|
|
119
168
|
}
|
|
120
169
|
catch {
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
function packageNeedsInstall(cwd) {
|
|
174
|
+
const pkg = readPackageJson(cwd);
|
|
175
|
+
if (!pkg) {
|
|
121
176
|
return false;
|
|
122
177
|
}
|
|
178
|
+
const depCount = Object.keys(pkg.dependencies ?? {}).length + Object.keys(pkg.devDependencies ?? {}).length;
|
|
179
|
+
return depCount > 0;
|
|
180
|
+
}
|
|
181
|
+
function parseGoalProfile(context) {
|
|
182
|
+
const goal = normalizeText(context?.goalText ?? "");
|
|
183
|
+
const hasJava = /\bjava\b/.test(goal);
|
|
184
|
+
const hasReact = /\breact\b/.test(goal);
|
|
185
|
+
const relationalHints = [
|
|
186
|
+
"library",
|
|
187
|
+
"biblioteca",
|
|
188
|
+
"inventario",
|
|
189
|
+
"inventory",
|
|
190
|
+
"prestamo",
|
|
191
|
+
"prestamos",
|
|
192
|
+
"loan",
|
|
193
|
+
"loans",
|
|
194
|
+
"usuario",
|
|
195
|
+
"usuarios",
|
|
196
|
+
"user",
|
|
197
|
+
"users",
|
|
198
|
+
"book",
|
|
199
|
+
"books",
|
|
200
|
+
"cita",
|
|
201
|
+
"citas",
|
|
202
|
+
"appointment",
|
|
203
|
+
"appointments",
|
|
204
|
+
"hospital",
|
|
205
|
+
"gestion",
|
|
206
|
+
"management"
|
|
207
|
+
];
|
|
208
|
+
const relationalDataApp = relationalHints.some((hint) => goal.includes(hint));
|
|
209
|
+
return {
|
|
210
|
+
javaReactFullstack: hasJava && hasReact,
|
|
211
|
+
relationalDataApp
|
|
212
|
+
};
|
|
123
213
|
}
|
|
124
214
|
function basicQualityCheck(appDir) {
|
|
125
215
|
const required = ["README.md"];
|
|
@@ -192,6 +282,207 @@ function advancedQualityCheck(appDir, context) {
|
|
|
192
282
|
output: "Missing schemas.md (or equivalent schema markdown document)"
|
|
193
283
|
};
|
|
194
284
|
}
|
|
285
|
+
const profile = parseGoalProfile(context);
|
|
286
|
+
if (profile.relationalDataApp) {
|
|
287
|
+
const sqlSchema = findFileRecursive(appDir, (rel) => rel === "schema.sql" || rel.endsWith("/schema.sql")) ??
|
|
288
|
+
findFileRecursive(appDir, (rel) => rel.endsWith(".sql") && (rel.includes("schema") || rel.includes("init") || rel.includes("migration")), 10);
|
|
289
|
+
if (!sqlSchema) {
|
|
290
|
+
return {
|
|
291
|
+
ok: false,
|
|
292
|
+
command: "advanced-quality-check",
|
|
293
|
+
output: "Missing SQL schema file (expected schema.sql or migration .sql) for relational data app"
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
const schemaText = normalizeText(fs_1.default.readFileSync(path_1.default.join(appDir, schemaDoc), "utf-8"));
|
|
297
|
+
const readmeText = readme;
|
|
298
|
+
if (!/(postgres|postgresql|mysql|mariadb|sqlite)/.test(`${schemaText}\n${readmeText}`)) {
|
|
299
|
+
return {
|
|
300
|
+
ok: false,
|
|
301
|
+
command: "advanced-quality-check",
|
|
302
|
+
output: "Database technology not explicit in README/schemas (expected PostgreSQL/MySQL/MariaDB/SQLite)"
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
if (profile.javaReactFullstack) {
|
|
307
|
+
const hasBackendPom = fs_1.default.existsSync(path_1.default.join(appDir, "backend", "pom.xml"));
|
|
308
|
+
const hasFrontendPkg = fs_1.default.existsSync(path_1.default.join(appDir, "frontend", "package.json"));
|
|
309
|
+
if (!hasBackendPom || !hasFrontendPkg) {
|
|
310
|
+
return {
|
|
311
|
+
ok: false,
|
|
312
|
+
command: "advanced-quality-check",
|
|
313
|
+
output: "Expected Java+React fullstack structure: backend/pom.xml and frontend/package.json"
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
const frontendPkg = readPackageJson(path_1.default.join(appDir, "frontend"));
|
|
317
|
+
if (frontendPkg) {
|
|
318
|
+
const deps = { ...(frontendPkg.dependencies ?? {}), ...(frontendPkg.devDependencies ?? {}) };
|
|
319
|
+
if (typeof deps["react-query"] === "string") {
|
|
320
|
+
return {
|
|
321
|
+
ok: false,
|
|
322
|
+
command: "advanced-quality-check",
|
|
323
|
+
output: "Outdated frontend dependency detected: react-query. Use @tanstack/react-query."
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
const requiredFrontendDeps = ["react-router-dom", "@tanstack/react-query"];
|
|
327
|
+
const missingFrontendDeps = requiredFrontendDeps.filter((dep) => typeof deps[dep] !== "string");
|
|
328
|
+
if (missingFrontendDeps.length > 0) {
|
|
329
|
+
return {
|
|
330
|
+
ok: false,
|
|
331
|
+
command: "advanced-quality-check",
|
|
332
|
+
output: `Missing modern frontend dependencies: ${missingFrontendDeps.join(", ")}`
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
const backendRoot = path_1.default.join(appDir, "backend", "src", "main", "java");
|
|
337
|
+
if (!fs_1.default.existsSync(backendRoot)) {
|
|
338
|
+
return {
|
|
339
|
+
ok: false,
|
|
340
|
+
command: "advanced-quality-check",
|
|
341
|
+
output: "Missing backend/src/main/java for Java backend implementation"
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
const backendFiles = collectFilesRecursive(backendRoot, 12).filter((rel) => rel.toLowerCase().endsWith(".java"));
|
|
345
|
+
const hasDto = backendFiles.some((rel) => /\/dto\//i.test(`/${rel}`) || /dto\.java$/i.test(rel));
|
|
346
|
+
if (!hasDto) {
|
|
347
|
+
return {
|
|
348
|
+
ok: false,
|
|
349
|
+
command: "advanced-quality-check",
|
|
350
|
+
output: "Missing Java DTO layer (expected dto package and *Dto.java files)"
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
const hasRecord = backendFiles.some((rel) => /\brecord\b/.test(fs_1.default.readFileSync(path_1.default.join(backendRoot, rel), "utf-8")));
|
|
354
|
+
if (!hasRecord) {
|
|
355
|
+
return {
|
|
356
|
+
ok: false,
|
|
357
|
+
command: "advanced-quality-check",
|
|
358
|
+
output: "Missing Java record usage (expected at least one record for immutable transport/domain models)"
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
const serviceFiles = backendFiles.filter((rel) => /\/service\//i.test(`/${rel}`));
|
|
362
|
+
const repositoryFiles = backendFiles.filter((rel) => /\/repository\//i.test(`/${rel}`));
|
|
363
|
+
const hasServiceInterface = serviceFiles.some((rel) => /\binterface\b/.test(fs_1.default.readFileSync(path_1.default.join(backendRoot, rel), "utf-8")));
|
|
364
|
+
const hasRepositoryInterface = repositoryFiles.some((rel) => /\binterface\b/.test(fs_1.default.readFileSync(path_1.default.join(backendRoot, rel), "utf-8")));
|
|
365
|
+
if (!hasServiceInterface || !hasRepositoryInterface) {
|
|
366
|
+
return {
|
|
367
|
+
ok: false,
|
|
368
|
+
command: "advanced-quality-check",
|
|
369
|
+
output: "Missing service/repository interfaces in Java backend architecture"
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
const hasControllerAdvice = backendFiles.some((rel) => /@RestControllerAdvice\b/.test(fs_1.default.readFileSync(path_1.default.join(backendRoot, rel), "utf-8")));
|
|
373
|
+
if (!hasControllerAdvice) {
|
|
374
|
+
return {
|
|
375
|
+
ok: false,
|
|
376
|
+
command: "advanced-quality-check",
|
|
377
|
+
output: "Missing global exception handling (expected @RestControllerAdvice)"
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
const hasValidationUsage = backendFiles.some((rel) => {
|
|
381
|
+
const raw = fs_1.default.readFileSync(path_1.default.join(backendRoot, rel), "utf-8");
|
|
382
|
+
return /\b@(Valid|NotNull|NotBlank|Size|Email)\b/.test(raw) && /(jakarta|javax)\.validation/.test(raw);
|
|
383
|
+
});
|
|
384
|
+
if (!hasValidationUsage) {
|
|
385
|
+
return {
|
|
386
|
+
ok: false,
|
|
387
|
+
command: "advanced-quality-check",
|
|
388
|
+
output: "Missing bean validation usage (expected @Valid/@NotBlank with jakarta/javax.validation imports)"
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
const pomPath = path_1.default.join(appDir, "backend", "pom.xml");
|
|
392
|
+
const pomRaw = fs_1.default.existsSync(pomPath) ? fs_1.default.readFileSync(pomPath, "utf-8").toLowerCase() : "";
|
|
393
|
+
const requiredBackendDeps = [
|
|
394
|
+
"lombok",
|
|
395
|
+
"spring-boot-starter-validation",
|
|
396
|
+
"spring-boot-starter-actuator"
|
|
397
|
+
];
|
|
398
|
+
const missingBackendDeps = requiredBackendDeps.filter((dep) => !pomRaw.includes(dep));
|
|
399
|
+
if (missingBackendDeps.length > 0) {
|
|
400
|
+
return {
|
|
401
|
+
ok: false,
|
|
402
|
+
command: "advanced-quality-check",
|
|
403
|
+
output: `Missing backend dependencies for production quality: ${missingBackendDeps.join(", ")}`
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
const hasMetricsConfig = (() => {
|
|
407
|
+
const metricsFile = fileExistsAny(path_1.default.join(appDir, "backend"), [
|
|
408
|
+
"src/main/resources/application.yml",
|
|
409
|
+
"src/main/resources/application.yaml",
|
|
410
|
+
"src/main/resources/application.properties"
|
|
411
|
+
]);
|
|
412
|
+
if (!metricsFile) {
|
|
413
|
+
return false;
|
|
414
|
+
}
|
|
415
|
+
const text = normalizeText(fs_1.default.readFileSync(metricsFile, "utf-8"));
|
|
416
|
+
return /management\.endpoints|prometheus|actuator/.test(text);
|
|
417
|
+
})();
|
|
418
|
+
if (!hasMetricsConfig) {
|
|
419
|
+
return {
|
|
420
|
+
ok: false,
|
|
421
|
+
command: "advanced-quality-check",
|
|
422
|
+
output: "Missing backend telemetry config (expected actuator/prometheus management settings)"
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
const frontendRoot = path_1.default.join(appDir, "frontend", "src");
|
|
426
|
+
if (!fs_1.default.existsSync(frontendRoot)) {
|
|
427
|
+
return {
|
|
428
|
+
ok: false,
|
|
429
|
+
command: "advanced-quality-check",
|
|
430
|
+
output: "Missing frontend/src for React frontend implementation"
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
const frontendFiles = collectFilesRecursive(frontendRoot, 10);
|
|
434
|
+
const hasApiLayer = frontendFiles.some((rel) => /^api\//i.test(rel) || /\/api\//i.test(`/${rel}`));
|
|
435
|
+
const hasHooksLayer = frontendFiles.some((rel) => /^hooks\/use[A-Z].*\.(t|j)sx?$/i.test(rel) || /\/hooks\/use[A-Z]/.test(rel));
|
|
436
|
+
const hasComponentsLayer = frontendFiles.some((rel) => /^components\//i.test(rel) || /\/components\//i.test(`/${rel}`));
|
|
437
|
+
if (!hasApiLayer || !hasHooksLayer || !hasComponentsLayer) {
|
|
438
|
+
return {
|
|
439
|
+
ok: false,
|
|
440
|
+
command: "advanced-quality-check",
|
|
441
|
+
output: "Missing frontend layers (expected src/api, src/hooks/use*.ts(x), and src/components)"
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
const frontendTestCount = countJsTsTests(path_1.default.join(appDir, "frontend"), 10);
|
|
445
|
+
if (frontendTestCount < 3) {
|
|
446
|
+
return {
|
|
447
|
+
ok: false,
|
|
448
|
+
command: "advanced-quality-check",
|
|
449
|
+
output: `Expected at least 3 frontend tests for Java+React profile, found ${frontendTestCount}`
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
const frontendUsesStrictMode = (() => {
|
|
453
|
+
const mainCandidate = fileExistsAny(path_1.default.join(appDir, "frontend"), ["src/main.tsx", "src/main.jsx", "src/index.tsx", "src/index.jsx"]);
|
|
454
|
+
if (!mainCandidate) {
|
|
455
|
+
return false;
|
|
456
|
+
}
|
|
457
|
+
const raw = fs_1.default.readFileSync(mainCandidate, "utf-8");
|
|
458
|
+
return /StrictMode/.test(raw);
|
|
459
|
+
})();
|
|
460
|
+
if (!frontendUsesStrictMode) {
|
|
461
|
+
return {
|
|
462
|
+
ok: false,
|
|
463
|
+
command: "advanced-quality-check",
|
|
464
|
+
output: "Missing React StrictMode in frontend bootstrap"
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
const executionGuideExists = findFileRecursive(appDir, (rel) => rel === "execution-guide.md" || rel.endsWith("/execution-guide.md"), 8) ??
|
|
468
|
+
findFileRecursive(appDir, (rel) => rel.includes("runbook") && rel.endsWith(".md"), 8);
|
|
469
|
+
if (!executionGuideExists) {
|
|
470
|
+
return {
|
|
471
|
+
ok: false,
|
|
472
|
+
command: "advanced-quality-check",
|
|
473
|
+
output: "Missing execution guide/runbook markdown (expected execution-guide.md or runbook*.md)"
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
const architectureDocExists = findFileRecursive(appDir, (rel) => rel === "architecture.md" || rel.endsWith("/architecture.md"), 8) ??
|
|
477
|
+
findFileRecursive(appDir, (rel) => rel.includes("architecture") && rel.endsWith(".md"), 8);
|
|
478
|
+
if (!architectureDocExists) {
|
|
479
|
+
return {
|
|
480
|
+
ok: false,
|
|
481
|
+
command: "advanced-quality-check",
|
|
482
|
+
output: "Missing architecture documentation markdown (expected architecture.md)"
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
}
|
|
195
486
|
const dummyLocalDoc = findFileRecursive(appDir, (rel) => rel.includes("dummylocal") && rel.endsWith(".md")) ??
|
|
196
487
|
findFileRecursive(appDir, (rel) => rel.includes("dummy-local") && rel.endsWith(".md")) ??
|
|
197
488
|
findFileRecursive(appDir, (rel) => rel.includes("dummy_local") && rel.endsWith(".md"));
|
|
@@ -329,7 +620,7 @@ function deriveRepoMetadata(projectName, appDir, context) {
|
|
|
329
620
|
const goalSeed = context?.goalText ? tokenizeIntent(context.goalText).slice(0, 6).join("-") : "";
|
|
330
621
|
const projectSeed = slugify(rawBase);
|
|
331
622
|
const intentSeed = slugify(goalSeed);
|
|
332
|
-
const base =
|
|
623
|
+
const base = intentSeed || projectSeed || "sdd-project";
|
|
333
624
|
const cleaned = base.replace(/-app$/g, "");
|
|
334
625
|
const repoName = `${cleaned}-app`.slice(0, 63).replace(/-+$/g, "");
|
|
335
626
|
let description = context?.goalText?.trim()
|
|
@@ -463,6 +754,34 @@ function runAppLifecycle(projectRoot, projectName, context) {
|
|
|
463
754
|
const build = runIfScript(appDir, "build");
|
|
464
755
|
if (build)
|
|
465
756
|
qualitySteps.push(build);
|
|
757
|
+
const backendDir = path_1.default.join(appDir, "backend");
|
|
758
|
+
if (fs_1.default.existsSync(path_1.default.join(backendDir, "pom.xml"))) {
|
|
759
|
+
if (hasCommand("mvn")) {
|
|
760
|
+
qualitySteps.push(run("mvn", ["-q", "test"], backendDir));
|
|
761
|
+
}
|
|
762
|
+
else {
|
|
763
|
+
qualitySteps.push({
|
|
764
|
+
ok: false,
|
|
765
|
+
command: "mvn -q test",
|
|
766
|
+
output: "Maven not available to validate Java backend"
|
|
767
|
+
});
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
const frontendDir = path_1.default.join(appDir, "frontend");
|
|
771
|
+
if (fs_1.default.existsSync(path_1.default.join(frontendDir, "package.json"))) {
|
|
772
|
+
if (packageNeedsInstall(frontendDir)) {
|
|
773
|
+
qualitySteps.push(run("npm", ["install"], frontendDir));
|
|
774
|
+
}
|
|
775
|
+
const feLint = runIfScript(frontendDir, "lint");
|
|
776
|
+
if (feLint)
|
|
777
|
+
qualitySteps.push(feLint);
|
|
778
|
+
const feTest = runIfScript(frontendDir, "test");
|
|
779
|
+
if (feTest)
|
|
780
|
+
qualitySteps.push(feTest);
|
|
781
|
+
const feBuild = runIfScript(frontendDir, "build");
|
|
782
|
+
if (feBuild)
|
|
783
|
+
qualitySteps.push(feBuild);
|
|
784
|
+
}
|
|
466
785
|
qualitySteps.push(advancedQualityCheck(appDir, context));
|
|
467
786
|
if (qualitySteps.length === 0) {
|
|
468
787
|
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) {
|