sdd-cli 0.1.24 → 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.
@@ -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,7 +1,7 @@
1
1
  {
2
2
  "name": "sdd-cli",
3
- "version": "0.1.24",
4
- "description": "AI-orchestrated specification-driven delivery CLI that plans, validates, and ships production-ready software projects.",
3
+ "version": "0.1.26",
4
+ "description": "AI-orchestrated specification-driven delivery CLI that plans, validates, and ships production-ready software projects.",
5
5
  "keywords": [
6
6
  "cli",
7
7
  "specification-driven-development",
@@ -25,11 +25,11 @@
25
25
  "bugs": {
26
26
  "url": "https://github.com/jdsalasca/sdd-tool/issues"
27
27
  },
28
- "bin": {
29
- "sdd-cli": "dist/cli.js",
30
- "sdd": "dist/cli.js",
31
- "sdd-tool": "dist/cli.js"
32
- },
28
+ "bin": {
29
+ "sdd-cli": "dist/cli.js",
30
+ "sdd": "dist/cli.js",
31
+ "sdd-tool": "dist/cli.js"
32
+ },
33
33
  "main": "dist/cli.js",
34
34
  "types": "dist/cli.d.ts",
35
35
  "files": [