sdd-cli 0.1.28 → 0.1.29

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.
@@ -8,6 +8,10 @@ export type DigitalReviewResult = {
8
8
  passed: boolean;
9
9
  findings: ReviewerFinding[];
10
10
  diagnostics: string[];
11
+ score: number;
12
+ threshold: number;
13
+ summary: string;
11
14
  };
12
15
  export declare function runDigitalHumanReview(appDir: string, context?: LifecycleContext): DigitalReviewResult;
16
+ export declare function writeDigitalReviewReport(appDir: string, review: DigitalReviewResult): string | null;
13
17
  export {};
@@ -4,6 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.runDigitalHumanReview = runDigitalHumanReview;
7
+ exports.writeDigitalReviewReport = writeDigitalReviewReport;
7
8
  const fs_1 = __importDefault(require("fs"));
8
9
  const path_1 = __importDefault(require("path"));
9
10
  function normalizeText(input) {
@@ -89,6 +90,28 @@ function hasUserFlowDocs(root, readme) {
89
90
  }
90
91
  return Boolean(findDoc(root, ["user-flow.md", "ux-notes.md", "experience.md"]));
91
92
  }
93
+ function parseThreshold() {
94
+ const raw = Number.parseInt(process.env.SDD_DIGITAL_REVIEW_MIN_SCORE ?? "", 10);
95
+ if (!Number.isFinite(raw)) {
96
+ return 85;
97
+ }
98
+ return Math.max(60, Math.min(98, raw));
99
+ }
100
+ function scoreForFindings(findings) {
101
+ let score = 100;
102
+ for (const finding of findings) {
103
+ score -= finding.severity === "high" ? 20 : 8;
104
+ }
105
+ return Math.max(0, score);
106
+ }
107
+ function hasArchitectureAndExecutionDocs(root) {
108
+ const architecture = findDoc(root, ["architecture.md"]);
109
+ const execution = findDoc(root, ["execution-guide.md", "runbook.md", "operations-runbook.md"]);
110
+ return Boolean(architecture && execution);
111
+ }
112
+ function hasLicense(root) {
113
+ return fs_1.default.existsSync(path_1.default.join(root, "LICENSE"));
114
+ }
92
115
  function hasSecretLeak(root) {
93
116
  const files = collectFilesRecursive(root, 8).filter((rel) => /\.(env|txt|md|json|yml|yaml|properties|ts|js|py|java)$/i.test(rel));
94
117
  const patterns = [/api[_-]?key\s*[:=]\s*[^\s]+/i, /secret\s*[:=]\s*[^\s]+/i, /password\s*[:=]\s*[^\s]+/i];
@@ -101,11 +124,16 @@ function hasSecretLeak(root) {
101
124
  }
102
125
  function runDigitalHumanReview(appDir, context) {
103
126
  const findings = [];
127
+ const threshold = parseThreshold();
104
128
  if (!fs_1.default.existsSync(appDir)) {
129
+ const diagnostics = ["[DigitalReviewer:program_manager][high] Generated app directory is missing."];
105
130
  return {
106
131
  passed: false,
107
132
  findings: [{ reviewer: "program_manager", severity: "high", message: "Generated app directory is missing." }],
108
- diagnostics: ["[DigitalReviewer:program_manager][high] Generated app directory is missing."]
133
+ diagnostics,
134
+ score: 0,
135
+ threshold,
136
+ summary: "failed: app directory missing"
109
137
  };
110
138
  }
111
139
  const readmePath = path_1.default.join(appDir, "README.md");
@@ -130,6 +158,20 @@ function runDigitalHumanReview(appDir, context) {
130
158
  message: `Automated test depth is low (${totalTests}). Minimum expected is 8 tests for acceptance.`
131
159
  });
132
160
  }
161
+ if (!hasArchitectureAndExecutionDocs(appDir)) {
162
+ findings.push({
163
+ reviewer: "program_manager",
164
+ severity: "medium",
165
+ message: "Architecture and execution/runbook docs are required for production readiness."
166
+ });
167
+ }
168
+ if (!hasLicense(appDir)) {
169
+ findings.push({
170
+ reviewer: "program_manager",
171
+ severity: "medium",
172
+ message: "Project should include a LICENSE file for delivery readiness."
173
+ });
174
+ }
133
175
  if (!hasUserFlowDocs(appDir, readme)) {
134
176
  findings.push({
135
177
  reviewer: "ux_researcher",
@@ -169,9 +211,37 @@ function runDigitalHumanReview(appDir, context) {
169
211
  }
170
212
  }
171
213
  const diagnostics = findings.map((finding) => `[DigitalReviewer:${finding.reviewer}][${finding.severity}] ${finding.message}`);
214
+ const score = scoreForFindings(findings);
215
+ const highCount = findings.filter((finding) => finding.severity === "high").length;
216
+ const mediumCount = findings.length - highCount;
217
+ const passed = findings.length === 0 || (highCount === 0 && score >= threshold);
218
+ const summary = passed
219
+ ? `passed: score ${score}/${threshold} (high=${highCount}, medium=${mediumCount})`
220
+ : `failed: score ${score}/${threshold} (high=${highCount}, medium=${mediumCount})`;
172
221
  return {
173
- passed: findings.length === 0,
222
+ passed,
174
223
  findings,
175
- diagnostics
224
+ diagnostics,
225
+ score,
226
+ threshold,
227
+ summary
176
228
  };
177
229
  }
230
+ function writeDigitalReviewReport(appDir, review) {
231
+ if (!fs_1.default.existsSync(appDir)) {
232
+ return null;
233
+ }
234
+ const deployDir = path_1.default.join(appDir, "deploy");
235
+ fs_1.default.mkdirSync(deployDir, { recursive: true });
236
+ const reportPath = path_1.default.join(deployDir, "digital-review-report.json");
237
+ fs_1.default.writeFileSync(reportPath, JSON.stringify({
238
+ generatedAt: new Date().toISOString(),
239
+ passed: review.passed,
240
+ score: review.score,
241
+ threshold: review.threshold,
242
+ summary: review.summary,
243
+ findings: review.findings,
244
+ diagnostics: review.diagnostics
245
+ }, null, 2), "utf-8");
246
+ return reportPath;
247
+ }
@@ -457,8 +457,12 @@ async function runHello(input, runQuestions) {
457
457
  intentDomain: intent.domain,
458
458
  intentFlow: intent.flow
459
459
  });
460
+ const initialReviewReport = (0, digital_reviewers_1.writeDigitalReviewReport)(appDir, review);
461
+ if (initialReviewReport) {
462
+ printWhy(`Digital-review report: ${initialReviewReport}`);
463
+ }
460
464
  if (!review.passed) {
461
- printWhy("Digital human reviewers found delivery issues. Applying targeted refinements.");
465
+ printWhy(`Digital human reviewers found delivery issues (${review.summary}). Applying targeted refinements.`);
462
466
  review.diagnostics.forEach((issue) => printWhy(`Reviewer issue: ${issue}`));
463
467
  }
464
468
  for (let attempt = 1; attempt <= maxReviewAttempts && !review.passed; attempt += 1) {
@@ -497,6 +501,7 @@ async function runHello(input, runQuestions) {
497
501
  intentDomain: intent.domain,
498
502
  intentFlow: intent.flow
499
503
  });
504
+ (0, digital_reviewers_1.writeDigitalReviewReport)(appDir, review);
500
505
  if (!review.passed) {
501
506
  review.diagnostics.forEach((issue) => printWhy(`Reviewer issue (retry ${attempt}): ${issue}`));
502
507
  }
@@ -506,7 +511,7 @@ async function runHello(input, runQuestions) {
506
511
  printRecoveryNext(activeProject, "finish", text);
507
512
  return;
508
513
  }
509
- printWhy("Digital reviewers approved delivery quality.");
514
+ printWhy(`Digital reviewers approved delivery quality (${review.summary}).`);
510
515
  }
511
516
  (0, local_metrics_1.recordActivationMetric)("completed", {
512
517
  project: activeProject,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sdd-cli",
3
- "version": "0.1.28",
3
+ "version": "0.1.29",
4
4
  "description": "AI-orchestrated specification-driven delivery CLI that plans, validates, and ships production-ready software projects.",
5
5
  "keywords": [
6
6
  "cli",