sdd-cli 0.1.29 → 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,6 +4,14 @@ 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[];
@@ -13,5 +21,8 @@ export type DigitalReviewResult = {
13
21
  summary: string;
14
22
  };
15
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;
16
27
  export declare function writeDigitalReviewReport(appDir: string, review: DigitalReviewResult): string | null;
17
28
  export {};
@@ -4,6 +4,9 @@ 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;
7
10
  exports.writeDigitalReviewReport = writeDigitalReviewReport;
8
11
  const fs_1 = __importDefault(require("fs"));
9
12
  const path_1 = __importDefault(require("path"));
@@ -90,6 +93,30 @@ function hasUserFlowDocs(root, readme) {
90
93
  }
91
94
  return Boolean(findDoc(root, ["user-flow.md", "ux-notes.md", "experience.md"]));
92
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
+ }
93
120
  function parseThreshold() {
94
121
  const raw = Number.parseInt(process.env.SDD_DIGITAL_REVIEW_MIN_SCORE ?? "", 10);
95
122
  if (!Number.isFinite(raw)) {
@@ -179,6 +206,41 @@ function runDigitalHumanReview(appDir, context) {
179
206
  message: "User experience flow is unclear. Add user-flow/UX notes and acceptance of critical journeys."
180
207
  });
181
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
+ }
182
244
  if (hasSecretLeak(appDir)) {
183
245
  findings.push({
184
246
  reviewer: "security_reviewer",
@@ -227,6 +289,72 @@ function runDigitalHumanReview(appDir, context) {
227
289
  summary
228
290
  };
229
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
+ }
230
358
  function writeDigitalReviewReport(appDir, review) {
231
359
  if (!fs_1.default.existsSync(appDir)) {
232
360
  return null;
@@ -457,16 +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);
460
461
  const initialReviewReport = (0, digital_reviewers_1.writeDigitalReviewReport)(appDir, review);
462
+ const initialStoriesPath = (0, digital_reviewers_1.writeUserStoriesBacklog)(appDir, stories);
461
463
  if (initialReviewReport) {
462
464
  printWhy(`Digital-review report: ${initialReviewReport}`);
463
465
  }
466
+ if (initialStoriesPath) {
467
+ printWhy(`Digital-review user stories: ${initialStoriesPath} (${stories.length} stories)`);
468
+ }
464
469
  if (!review.passed) {
465
470
  printWhy(`Digital human reviewers found delivery issues (${review.summary}). Applying targeted refinements.`);
466
471
  review.diagnostics.forEach((issue) => printWhy(`Reviewer issue: ${issue}`));
467
472
  }
468
473
  for (let attempt = 1; attempt <= maxReviewAttempts && !review.passed; attempt += 1) {
469
- 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);
470
476
  if (!repair.attempted || !repair.applied) {
471
477
  printWhy(`Digital-review repair attempt ${attempt} skipped: ${repair.reason || "unknown reason"}`);
472
478
  break;
@@ -501,7 +507,9 @@ async function runHello(input, runQuestions) {
501
507
  intentDomain: intent.domain,
502
508
  intentFlow: intent.flow
503
509
  });
510
+ stories = (0, digital_reviewers_1.convertFindingsToUserStories)(review.findings);
504
511
  (0, digital_reviewers_1.writeDigitalReviewReport)(appDir, review);
512
+ (0, digital_reviewers_1.writeUserStoriesBacklog)(appDir, stories);
505
513
  if (!review.passed) {
506
514
  review.diagnostics.forEach((issue) => printWhy(`Reviewer issue (retry ${attempt}): ${issue}`));
507
515
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sdd-cli",
3
- "version": "0.1.29",
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",