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 = 12;
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(qualityDiagnostics ?? [])}`,
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)) && Array.isArray(qualityDiagnostics) && qualityDiagnostics.length > 0) {
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(qualityDiagnostics)}`,
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" && command === "npm") {
77
- resolved = "npm.cmd";
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 output = `${result.stdout || ""}${result.stderr || ""}`.trim();
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 packageNeedsInstall(cwd) {
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 false;
164
+ return null;
114
165
  }
115
166
  try {
116
- const pkg = JSON.parse(fs_1.default.readFileSync(pkgPath, "utf-8"));
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 = projectSeed || intentSeed || "sdd-project";
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));
@@ -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 : 6;
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) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sdd-cli",
3
- "version": "0.1.25",
3
+ "version": "0.1.27",
4
4
  "description": "AI-orchestrated specification-driven delivery CLI that plans, validates, and ships production-ready software projects.",
5
5
  "keywords": [
6
6
  "cli",