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