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 = 12;
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(qualityDiagnostics ?? [])}`,
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)) && Array.isArray(qualityDiagnostics) && qualityDiagnostics.length > 0) {
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(qualityDiagnostics)}`,
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" && command === "npm") {
77
- resolved = "npm.cmd";
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 output = `${result.stdout || ""}${result.stderr || ""}`.trim();
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 packageNeedsInstall(cwd) {
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 false;
155
+ return null;
114
156
  }
115
157
  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;
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));
@@ -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.26",
4
4
  "description": "AI-orchestrated specification-driven delivery CLI that plans, validates, and ships production-ready software projects.",
5
5
  "keywords": [
6
6
  "cli",