@produtype/core 0.7.1 → 0.8.0
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.
|
@@ -287,19 +287,51 @@ function evidenceQualityFor(detectors) {
|
|
|
287
287
|
}
|
|
288
288
|
return 'weak';
|
|
289
289
|
}
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
290
|
+
/**
|
|
291
|
+
* How sure the analyzer is about *this* finding.
|
|
292
|
+
*
|
|
293
|
+
* It used to fold in how the profile was chosen: on `auto` — which is what the hosted
|
|
294
|
+
* product uses by default — every finding came out low or medium however strong its
|
|
295
|
+
* evidence. Two different uncertainties were being multiplied into one number, and the
|
|
296
|
+
* profile's own uncertainty is already reported separately as `inferenceConfidence`.
|
|
297
|
+
*
|
|
298
|
+
* An independent review of a real report put it plainly: eleven findings of
|
|
299
|
+
* thirty-two carried "no direct evidence captured" at low confidence and weak
|
|
300
|
+
* evidence, and still arrived as critical or high.
|
|
301
|
+
*/
|
|
302
|
+
function confidenceFor(status, evidenceQuality) {
|
|
303
|
+
// Nothing was determined, so there is nothing to be confident about.
|
|
304
|
+
if (status === 'unknown')
|
|
305
|
+
return 'low';
|
|
297
306
|
if (evidenceQuality === 'strong')
|
|
298
307
|
return 'high';
|
|
299
308
|
if (evidenceQuality === 'medium')
|
|
300
309
|
return 'medium';
|
|
301
310
|
return 'low';
|
|
302
311
|
}
|
|
312
|
+
/**
|
|
313
|
+
* Severity says how bad it would be if true. Confidence says whether it is.
|
|
314
|
+
*
|
|
315
|
+
* A reader treats `critical` as "stop and fix this", and spending that word on a claim
|
|
316
|
+
* the analyzer itself is unsure of is how a tool becomes a checklist nobody trusts.
|
|
317
|
+
* The claim stays, at the weight the evidence supports.
|
|
318
|
+
*/
|
|
319
|
+
function severityForConfidence(severity, confidence) {
|
|
320
|
+
if (confidence !== 'low')
|
|
321
|
+
return severity;
|
|
322
|
+
/**
|
|
323
|
+
* `critical` only. Not a ceiling at `medium`, which was the first attempt and was
|
|
324
|
+
* wrong for a reason worth writing down: a missing capability cannot carry direct
|
|
325
|
+
* evidence — there is no line to point at for something that is not there — so every
|
|
326
|
+
* absence reads as low confidence, and capping them all at `medium` would leave the
|
|
327
|
+
* report unable to say anything is serious.
|
|
328
|
+
*
|
|
329
|
+
* What it can stop doing is shouting. `critical` is read as "stop and fix this
|
|
330
|
+
* before anything else", and the analyzer should not spend that word on a claim it
|
|
331
|
+
* could not evidence.
|
|
332
|
+
*/
|
|
333
|
+
return severity === 'critical' ? 'high' : severity;
|
|
334
|
+
}
|
|
303
335
|
/**
|
|
304
336
|
* Which capabilities each declaration makes required.
|
|
305
337
|
*
|
|
@@ -390,10 +422,11 @@ function evaluateExpectedCapabilities(args) {
|
|
|
390
422
|
effectiveImportance = 'not_applicable';
|
|
391
423
|
}
|
|
392
424
|
const findingId = toFindingId(cap, effectiveImportance);
|
|
393
|
-
const
|
|
425
|
+
const claimedSeverity = severityFor(cap, status, effectiveImportance);
|
|
394
426
|
const detectorEvidence = evidenceFor(args.analysis, cap).flatMap((d) => d.evidence);
|
|
395
427
|
const quality = evidenceQualityFor(evidenceFor(args.analysis, cap));
|
|
396
|
-
const confidence = confidenceFor(status,
|
|
428
|
+
const confidence = confidenceFor(status, quality);
|
|
429
|
+
const severity = severityForConfidence(claimedSeverity, confidence);
|
|
397
430
|
const evaluation = {
|
|
398
431
|
capabilityId: cap.id,
|
|
399
432
|
title: cap.title,
|
|
@@ -52,7 +52,16 @@ function buildCategoryScores(findings) {
|
|
|
52
52
|
findingCount: actionable.length,
|
|
53
53
|
criticalCount: actionable.filter((finding) => finding.severity === 'critical').length,
|
|
54
54
|
highCount: actionable.filter((finding) => finding.severity === 'high').length,
|
|
55
|
-
|
|
55
|
+
/**
|
|
56
|
+
* "I don't know" is not an assessment.
|
|
57
|
+
*
|
|
58
|
+
* This was `categoryFindings.length === 0`, so a single `info` finding with
|
|
59
|
+
* status `unknown` — the analyzer recording that it could not tell — made the
|
|
60
|
+
* category count as assessed, with zero actionable findings and a score of 100.
|
|
61
|
+
* A Django school platform with no payments anywhere was reported as having
|
|
62
|
+
* "no issues found in taking payments", as a strength.
|
|
63
|
+
*/
|
|
64
|
+
notAssessed: categoryFindings.every((finding) => finding.status === 'unknown'),
|
|
56
65
|
};
|
|
57
66
|
});
|
|
58
67
|
}
|
|
@@ -83,8 +83,19 @@ function buildVerdict(args) {
|
|
|
83
83
|
}
|
|
84
84
|
function buildStrengths(categoryScores, findings) {
|
|
85
85
|
const passed = findings.filter((finding) => finding.status === 'passed');
|
|
86
|
+
/**
|
|
87
|
+
* A strength is something verified, not something absent.
|
|
88
|
+
*
|
|
89
|
+
* Filtering on "assessed, scoring well, nothing open" let a category qualify on the
|
|
90
|
+
* strength of having nothing to say about it. Requiring a passed check means the
|
|
91
|
+
* report only calls something a strength when it watched it work.
|
|
92
|
+
*/
|
|
93
|
+
const passedCategories = new Set(passed.map((finding) => finding.category));
|
|
86
94
|
const strongCategories = categoryScores
|
|
87
|
-
.filter((entry) => !entry.notAssessed
|
|
95
|
+
.filter((entry) => !entry.notAssessed
|
|
96
|
+
&& entry.score >= 80
|
|
97
|
+
&& entry.findingCount === 0
|
|
98
|
+
&& passedCategories.has(entry.category))
|
|
88
99
|
.map((entry) => CATEGORY_LABEL[entry.category]);
|
|
89
100
|
const strengths = strongCategories.slice(0, 3).map((label) => `no issues found in ${label}`);
|
|
90
101
|
if (strengths.length === 0 && passed.length > 0) {
|
package/dist/rules/rules.js
CHANGED
|
@@ -46,6 +46,23 @@ function evidenceQualityFor(evidence) {
|
|
|
46
46
|
}
|
|
47
47
|
return 'weak';
|
|
48
48
|
}
|
|
49
|
+
/** Severity is capped by how sure the analyzer is. See the note where it is used. */
|
|
50
|
+
function severityForConfidence(severity, confidence) {
|
|
51
|
+
if (confidence !== 'low')
|
|
52
|
+
return severity;
|
|
53
|
+
/**
|
|
54
|
+
* `critical` only. Not a ceiling at `medium`, which was the first attempt and was
|
|
55
|
+
* wrong for a reason worth writing down: a missing capability cannot carry direct
|
|
56
|
+
* evidence — there is no line to point at for something that is not there — so every
|
|
57
|
+
* absence reads as low confidence, and capping them all at `medium` would leave the
|
|
58
|
+
* report unable to say anything is serious.
|
|
59
|
+
*
|
|
60
|
+
* What it can stop doing is shouting. `critical` is read as "stop and fix this
|
|
61
|
+
* before anything else", and the analyzer should not spend that word on a claim it
|
|
62
|
+
* could not evidence.
|
|
63
|
+
*/
|
|
64
|
+
return severity === 'critical' ? 'high' : severity;
|
|
65
|
+
}
|
|
49
66
|
function confidenceFor(status, evidenceQuality) {
|
|
50
67
|
if (status === 'passed' && evidenceQuality === 'strong') {
|
|
51
68
|
return 'high';
|
|
@@ -60,16 +77,24 @@ function confidenceFor(status, evidenceQuality) {
|
|
|
60
77
|
}
|
|
61
78
|
function mkFinding(args) {
|
|
62
79
|
const evidenceQuality = evidenceQualityFor(args.evidence);
|
|
80
|
+
const confidence = confidenceFor(args.status, evidenceQuality);
|
|
63
81
|
return {
|
|
64
82
|
id: args.id,
|
|
65
83
|
title: args.title,
|
|
66
84
|
category: args.category,
|
|
67
85
|
status: args.status,
|
|
68
|
-
|
|
86
|
+
/**
|
|
87
|
+
* The same rule the expectations follow: severity says how bad it would be if true,
|
|
88
|
+
* confidence says whether it is, and a reader treats `critical` as "stop and fix
|
|
89
|
+
* this". Spending that word on something the analyzer is unsure of is how a report
|
|
90
|
+
* stops being believed — and it was doing it on both sides of the report, not only
|
|
91
|
+
* in the expectation half.
|
|
92
|
+
*/
|
|
93
|
+
severity: severityForConfidence(args.severity, confidence),
|
|
69
94
|
description: args.description,
|
|
70
95
|
recommendation: args.recommendation,
|
|
71
96
|
evidence: detectorEvidence(args.evidence),
|
|
72
|
-
confidence
|
|
97
|
+
confidence,
|
|
73
98
|
evidenceQuality,
|
|
74
99
|
};
|
|
75
100
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@produtype/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "Deterministic CLI and library that analyzes a web application repository and reports how far it is from production-ready for the kind of product it is meant to be.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"bin": {
|