sdd-cli 0.1.28 → 0.1.30

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.
@@ -4,10 +4,25 @@ type ReviewerFinding = {
4
4
  severity: "high" | "medium";
5
5
  message: string;
6
6
  };
7
+ export type UserStory = {
8
+ id: string;
9
+ priority: "P0" | "P1";
10
+ persona: string;
11
+ story: string;
12
+ acceptanceCriteria: string[];
13
+ sourceReviewer: string;
14
+ };
7
15
  export type DigitalReviewResult = {
8
16
  passed: boolean;
9
17
  findings: ReviewerFinding[];
10
18
  diagnostics: string[];
19
+ score: number;
20
+ threshold: number;
21
+ summary: string;
11
22
  };
12
23
  export declare function runDigitalHumanReview(appDir: string, context?: LifecycleContext): DigitalReviewResult;
24
+ export declare function convertFindingsToUserStories(findings: ReviewerFinding[]): UserStory[];
25
+ export declare function storiesToDiagnostics(stories: UserStory[]): string[];
26
+ export declare function writeUserStoriesBacklog(appDir: string, stories: UserStory[]): string | null;
27
+ export declare function writeDigitalReviewReport(appDir: string, review: DigitalReviewResult): string | null;
13
28
  export {};
@@ -4,6 +4,10 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.runDigitalHumanReview = runDigitalHumanReview;
7
+ exports.convertFindingsToUserStories = convertFindingsToUserStories;
8
+ exports.storiesToDiagnostics = storiesToDiagnostics;
9
+ exports.writeUserStoriesBacklog = writeUserStoriesBacklog;
10
+ exports.writeDigitalReviewReport = writeDigitalReviewReport;
7
11
  const fs_1 = __importDefault(require("fs"));
8
12
  const path_1 = __importDefault(require("path"));
9
13
  function normalizeText(input) {
@@ -89,6 +93,52 @@ function hasUserFlowDocs(root, readme) {
89
93
  }
90
94
  return Boolean(findDoc(root, ["user-flow.md", "ux-notes.md", "experience.md"]));
91
95
  }
96
+ function hasAccessibilityEvidence(root, readme) {
97
+ if (/\ba11y\b|\baccessibility\b|\bwcag\b|\bkeyboard\b/.test(readme)) {
98
+ return true;
99
+ }
100
+ return Boolean(findDoc(root, ["accessibility.md", "a11y.md"]));
101
+ }
102
+ function hasPerformanceEvidence(root, readme) {
103
+ if (/\bperformance\b|\blatency\b|\bthroughput\b|\bp95\b|\bp99\b/.test(readme)) {
104
+ return true;
105
+ }
106
+ return Boolean(findDoc(root, ["performance.md", "performance-budget.md", "scalability.md"]));
107
+ }
108
+ function hasSupportEvidence(root, readme) {
109
+ if (/\btroubleshoot\b|\bsupport\b|\bfaq\b/.test(readme)) {
110
+ return true;
111
+ }
112
+ return Boolean(findDoc(root, ["troubleshooting.md", "support.md", "faq.md"]));
113
+ }
114
+ function hasApiContracts(root) {
115
+ return Boolean(findDoc(root, ["openapi.yaml", "openapi.yml", "api-contract.md", "api.md"]));
116
+ }
117
+ function hasReleaseNotes(root) {
118
+ return Boolean(findDoc(root, ["release-notes.md", "changelog.md"]));
119
+ }
120
+ function parseThreshold() {
121
+ const raw = Number.parseInt(process.env.SDD_DIGITAL_REVIEW_MIN_SCORE ?? "", 10);
122
+ if (!Number.isFinite(raw)) {
123
+ return 85;
124
+ }
125
+ return Math.max(60, Math.min(98, raw));
126
+ }
127
+ function scoreForFindings(findings) {
128
+ let score = 100;
129
+ for (const finding of findings) {
130
+ score -= finding.severity === "high" ? 20 : 8;
131
+ }
132
+ return Math.max(0, score);
133
+ }
134
+ function hasArchitectureAndExecutionDocs(root) {
135
+ const architecture = findDoc(root, ["architecture.md"]);
136
+ const execution = findDoc(root, ["execution-guide.md", "runbook.md", "operations-runbook.md"]);
137
+ return Boolean(architecture && execution);
138
+ }
139
+ function hasLicense(root) {
140
+ return fs_1.default.existsSync(path_1.default.join(root, "LICENSE"));
141
+ }
92
142
  function hasSecretLeak(root) {
93
143
  const files = collectFilesRecursive(root, 8).filter((rel) => /\.(env|txt|md|json|yml|yaml|properties|ts|js|py|java)$/i.test(rel));
94
144
  const patterns = [/api[_-]?key\s*[:=]\s*[^\s]+/i, /secret\s*[:=]\s*[^\s]+/i, /password\s*[:=]\s*[^\s]+/i];
@@ -101,11 +151,16 @@ function hasSecretLeak(root) {
101
151
  }
102
152
  function runDigitalHumanReview(appDir, context) {
103
153
  const findings = [];
154
+ const threshold = parseThreshold();
104
155
  if (!fs_1.default.existsSync(appDir)) {
156
+ const diagnostics = ["[DigitalReviewer:program_manager][high] Generated app directory is missing."];
105
157
  return {
106
158
  passed: false,
107
159
  findings: [{ reviewer: "program_manager", severity: "high", message: "Generated app directory is missing." }],
108
- diagnostics: ["[DigitalReviewer:program_manager][high] Generated app directory is missing."]
160
+ diagnostics,
161
+ score: 0,
162
+ threshold,
163
+ summary: "failed: app directory missing"
109
164
  };
110
165
  }
111
166
  const readmePath = path_1.default.join(appDir, "README.md");
@@ -130,6 +185,20 @@ function runDigitalHumanReview(appDir, context) {
130
185
  message: `Automated test depth is low (${totalTests}). Minimum expected is 8 tests for acceptance.`
131
186
  });
132
187
  }
188
+ if (!hasArchitectureAndExecutionDocs(appDir)) {
189
+ findings.push({
190
+ reviewer: "program_manager",
191
+ severity: "medium",
192
+ message: "Architecture and execution/runbook docs are required for production readiness."
193
+ });
194
+ }
195
+ if (!hasLicense(appDir)) {
196
+ findings.push({
197
+ reviewer: "program_manager",
198
+ severity: "medium",
199
+ message: "Project should include a LICENSE file for delivery readiness."
200
+ });
201
+ }
133
202
  if (!hasUserFlowDocs(appDir, readme)) {
134
203
  findings.push({
135
204
  reviewer: "ux_researcher",
@@ -137,6 +206,41 @@ function runDigitalHumanReview(appDir, context) {
137
206
  message: "User experience flow is unclear. Add user-flow/UX notes and acceptance of critical journeys."
138
207
  });
139
208
  }
209
+ if (!hasAccessibilityEvidence(appDir, readme)) {
210
+ findings.push({
211
+ reviewer: "accessibility_tester",
212
+ severity: "medium",
213
+ message: "Accessibility evidence missing. Add keyboard/contrast/screen-reader validation notes."
214
+ });
215
+ }
216
+ if (!hasPerformanceEvidence(appDir, readme)) {
217
+ findings.push({
218
+ reviewer: "performance_engineer",
219
+ severity: "medium",
220
+ message: "Performance expectations are unclear. Add performance budget and baseline measurements."
221
+ });
222
+ }
223
+ if (!hasSupportEvidence(appDir, readme)) {
224
+ findings.push({
225
+ reviewer: "support_agent",
226
+ severity: "medium",
227
+ message: "Support/troubleshooting guidance is missing for operators and end users."
228
+ });
229
+ }
230
+ if (!hasApiContracts(appDir)) {
231
+ findings.push({
232
+ reviewer: "integrator_partner",
233
+ severity: "medium",
234
+ message: "API contract/documentation missing. Add OpenAPI or API contract document for integrators."
235
+ });
236
+ }
237
+ if (!hasReleaseNotes(appDir)) {
238
+ findings.push({
239
+ reviewer: "release_manager",
240
+ severity: "medium",
241
+ message: "Release notes/changelog missing. Add release documentation for change visibility."
242
+ });
243
+ }
140
244
  if (hasSecretLeak(appDir)) {
141
245
  findings.push({
142
246
  reviewer: "security_reviewer",
@@ -169,9 +273,103 @@ function runDigitalHumanReview(appDir, context) {
169
273
  }
170
274
  }
171
275
  const diagnostics = findings.map((finding) => `[DigitalReviewer:${finding.reviewer}][${finding.severity}] ${finding.message}`);
276
+ const score = scoreForFindings(findings);
277
+ const highCount = findings.filter((finding) => finding.severity === "high").length;
278
+ const mediumCount = findings.length - highCount;
279
+ const passed = findings.length === 0 || (highCount === 0 && score >= threshold);
280
+ const summary = passed
281
+ ? `passed: score ${score}/${threshold} (high=${highCount}, medium=${mediumCount})`
282
+ : `failed: score ${score}/${threshold} (high=${highCount}, medium=${mediumCount})`;
172
283
  return {
173
- passed: findings.length === 0,
284
+ passed,
174
285
  findings,
175
- diagnostics
286
+ diagnostics,
287
+ score,
288
+ threshold,
289
+ summary
176
290
  };
177
291
  }
292
+ function slugReviewer(reviewer) {
293
+ return reviewer.replace(/[^a-z0-9]+/gi, "_").toLowerCase();
294
+ }
295
+ function acceptanceCriteriaFromFinding(finding) {
296
+ const base = finding.message.replace(/\.$/, "");
297
+ return [
298
+ `Given the generated app, when quality review runs, then ${base.toLowerCase()}.`,
299
+ "Given CI validation, when documentation/tests are checked, then evidence is discoverable and actionable."
300
+ ];
301
+ }
302
+ function convertFindingsToUserStories(findings) {
303
+ const deduped = new Map();
304
+ for (const finding of findings) {
305
+ const key = `${finding.reviewer}::${finding.message}`.toLowerCase();
306
+ if (!deduped.has(key)) {
307
+ deduped.set(key, finding);
308
+ }
309
+ }
310
+ let index = 1;
311
+ return [...deduped.values()].map((finding) => {
312
+ const id = `US-${String(index).padStart(3, "0")}`;
313
+ index += 1;
314
+ const persona = slugReviewer(finding.reviewer);
315
+ const priority = finding.severity === "high" ? "P0" : "P1";
316
+ return {
317
+ id,
318
+ priority,
319
+ persona,
320
+ sourceReviewer: finding.reviewer,
321
+ story: `As a ${persona}, I need ${finding.message.toLowerCase()} so that the delivery is production-ready.`,
322
+ acceptanceCriteria: acceptanceCriteriaFromFinding(finding)
323
+ };
324
+ });
325
+ }
326
+ function storiesToDiagnostics(stories) {
327
+ return stories.map((story) => `[UserStory:${story.id}][${story.priority}] ${story.story}`);
328
+ }
329
+ function writeUserStoriesBacklog(appDir, stories) {
330
+ if (!fs_1.default.existsSync(appDir)) {
331
+ return null;
332
+ }
333
+ const deployDir = path_1.default.join(appDir, "deploy");
334
+ fs_1.default.mkdirSync(deployDir, { recursive: true });
335
+ const jsonPath = path_1.default.join(deployDir, "digital-review-user-stories.json");
336
+ fs_1.default.writeFileSync(jsonPath, JSON.stringify({
337
+ generatedAt: new Date().toISOString(),
338
+ count: stories.length,
339
+ stories
340
+ }, null, 2), "utf-8");
341
+ const mdPath = path_1.default.join(deployDir, "digital-review-user-stories.md");
342
+ const lines = [
343
+ "# Digital Review User Stories",
344
+ "",
345
+ ...stories.flatMap((story) => [
346
+ `## ${story.id} (${story.priority})`,
347
+ `- Persona: ${story.persona}`,
348
+ `- Source reviewer: ${story.sourceReviewer}`,
349
+ `- Story: ${story.story}`,
350
+ "- Acceptance criteria:",
351
+ ...story.acceptanceCriteria.map((criterion) => ` - ${criterion}`),
352
+ ""
353
+ ])
354
+ ];
355
+ fs_1.default.writeFileSync(mdPath, `${lines.join("\n")}\n`, "utf-8");
356
+ return jsonPath;
357
+ }
358
+ function writeDigitalReviewReport(appDir, review) {
359
+ if (!fs_1.default.existsSync(appDir)) {
360
+ return null;
361
+ }
362
+ const deployDir = path_1.default.join(appDir, "deploy");
363
+ fs_1.default.mkdirSync(deployDir, { recursive: true });
364
+ const reportPath = path_1.default.join(deployDir, "digital-review-report.json");
365
+ fs_1.default.writeFileSync(reportPath, JSON.stringify({
366
+ generatedAt: new Date().toISOString(),
367
+ passed: review.passed,
368
+ score: review.score,
369
+ threshold: review.threshold,
370
+ summary: review.summary,
371
+ findings: review.findings,
372
+ diagnostics: review.diagnostics
373
+ }, null, 2), "utf-8");
374
+ return reportPath;
375
+ }
@@ -457,12 +457,22 @@ async function runHello(input, runQuestions) {
457
457
  intentDomain: intent.domain,
458
458
  intentFlow: intent.flow
459
459
  });
460
+ let stories = (0, digital_reviewers_1.convertFindingsToUserStories)(review.findings);
461
+ const initialReviewReport = (0, digital_reviewers_1.writeDigitalReviewReport)(appDir, review);
462
+ const initialStoriesPath = (0, digital_reviewers_1.writeUserStoriesBacklog)(appDir, stories);
463
+ if (initialReviewReport) {
464
+ printWhy(`Digital-review report: ${initialReviewReport}`);
465
+ }
466
+ if (initialStoriesPath) {
467
+ printWhy(`Digital-review user stories: ${initialStoriesPath} (${stories.length} stories)`);
468
+ }
460
469
  if (!review.passed) {
461
- printWhy("Digital human reviewers found delivery issues. Applying targeted refinements.");
470
+ printWhy(`Digital human reviewers found delivery issues (${review.summary}). Applying targeted refinements.`);
462
471
  review.diagnostics.forEach((issue) => printWhy(`Reviewer issue: ${issue}`));
463
472
  }
464
473
  for (let attempt = 1; attempt <= maxReviewAttempts && !review.passed; attempt += 1) {
465
- const repair = (0, ai_autopilot_1.improveGeneratedApp)(appDir, text, provider, review.diagnostics, intent.domain);
474
+ const storyDiagnostics = (0, digital_reviewers_1.storiesToDiagnostics)(stories);
475
+ const repair = (0, ai_autopilot_1.improveGeneratedApp)(appDir, text, provider, [...review.diagnostics, ...storyDiagnostics, "Implement all user stories from digital review backlog."], intent.domain);
466
476
  if (!repair.attempted || !repair.applied) {
467
477
  printWhy(`Digital-review repair attempt ${attempt} skipped: ${repair.reason || "unknown reason"}`);
468
478
  break;
@@ -497,6 +507,9 @@ async function runHello(input, runQuestions) {
497
507
  intentDomain: intent.domain,
498
508
  intentFlow: intent.flow
499
509
  });
510
+ stories = (0, digital_reviewers_1.convertFindingsToUserStories)(review.findings);
511
+ (0, digital_reviewers_1.writeDigitalReviewReport)(appDir, review);
512
+ (0, digital_reviewers_1.writeUserStoriesBacklog)(appDir, stories);
500
513
  if (!review.passed) {
501
514
  review.diagnostics.forEach((issue) => printWhy(`Reviewer issue (retry ${attempt}): ${issue}`));
502
515
  }
@@ -506,7 +519,7 @@ async function runHello(input, runQuestions) {
506
519
  printRecoveryNext(activeProject, "finish", text);
507
520
  return;
508
521
  }
509
- printWhy("Digital reviewers approved delivery quality.");
522
+ printWhy(`Digital reviewers approved delivery quality (${review.summary}).`);
510
523
  }
511
524
  (0, local_metrics_1.recordActivationMetric)("completed", {
512
525
  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.30",
4
4
  "description": "AI-orchestrated specification-driven delivery CLI that plans, validates, and ships production-ready software projects.",
5
5
  "keywords": [
6
6
  "cli",