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;
|
package/dist/commands/hello.js
CHANGED
|
@@ -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
|
|
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
|
}
|