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
|
|
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
|
|
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
|
+
}
|
package/dist/commands/hello.js
CHANGED
|
@@ -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(
|
|
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
|
|
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(
|
|
522
|
+
printWhy(`Digital reviewers approved delivery quality (${review.summary}).`);
|
|
510
523
|
}
|
|
511
524
|
(0, local_metrics_1.recordActivationMetric)("completed", {
|
|
512
525
|
project: activeProject,
|