@produtype/core 0.16.2 → 0.18.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.
@@ -127,18 +127,6 @@ function buildReport(analysis, options) {
127
127
  || analysis.stack.databases.length > 0
128
128
  || mobileDetected
129
129
  || gameEngineDetected;
130
- const inconclusive = !stackDetected && analysis.workspaceStacks.length === 0;
131
- const inconclusiveReasons = [];
132
- if (inconclusive) {
133
- inconclusiveReasons.push('No frontend, backend, or database stack signals were detected.');
134
- inconclusiveReasons.push('No package manifest was found in any format this analyzer reads.');
135
- if (analysis.files.source.length === 0) {
136
- inconclusiveReasons.push('No recognizable source files were found.');
137
- }
138
- }
139
- // An unrecognized project has almost no applicable detectors, so the absence
140
- // of findings must not be rewarded with a high score: cap it at prototype.
141
- const overallScore = inconclusive ? Math.min(combinedScore, 39) : combinedScore;
142
130
  /**
143
131
  * Coverage counts the expectations too, not only the observed rules.
144
132
  *
@@ -152,10 +140,63 @@ function buildReport(analysis, options) {
152
140
  const passedChecksForMaturity = findings.filter((f) => f.status === 'passed').length + satisfiedExpectations;
153
141
  const assessedChecks = findings.filter((f) => f.status !== 'unknown').length
154
142
  + Math.max(applicableExpectations - expectationFindings.length, 0);
155
- const maturityLevel = (0, score_1.computeMaturity)(overallScore, {
156
- passed: passedChecksForMaturity,
157
- assessed: assessedChecks,
158
- });
143
+ const coverage = { passed: passedChecksForMaturity, assessed: assessedChecks };
144
+ const nothingIdentified = !stackDetected && analysis.workspaceStacks.length === 0;
145
+ /**
146
+ * Too little reached a verdict to say anything about this project.
147
+ *
148
+ * The flag used to ask whether a manifest existed, and the claim it makes is about
149
+ * what was learned. Two repositories with the same three verdicts came out at 39 and
150
+ * at 84, and the only difference between them was a `requirements.txt`: a Python
151
+ * script with one was judged, an equally small one without it was called unreadable.
152
+ *
153
+ * Five is where the corpus splits. Of seventy-eight repositories, eleven reach four
154
+ * verdicts or fewer and thirty reach ten or more; not one lands in between. Four
155
+ * verdicts cannot characterise a product, however many files it has.
156
+ */
157
+ const tooLittleAssessed = assessedChecks < score_1.MIN_ASSESSED_FOR_A_READING;
158
+ const inconclusive = nothingIdentified || tooLittleAssessed;
159
+ // Each reason says which of the two it was, because they call for different things:
160
+ // one is a repository this analyzer cannot read, the other is one there is barely
161
+ // anything to read.
162
+ const inconclusiveReasons = [];
163
+ if (nothingIdentified) {
164
+ inconclusiveReasons.push('No frontend, backend, or database stack signals were detected.');
165
+ inconclusiveReasons.push('No package manifest was found in any format this analyzer reads.');
166
+ if (analysis.files.source.length === 0) {
167
+ inconclusiveReasons.push('No recognizable source files were found.');
168
+ }
169
+ }
170
+ if (tooLittleAssessed) {
171
+ inconclusiveReasons.push(`Only ${assessedChecks} check${assessedChecks === 1 ? '' : 's'} reached a verdict, which is too few to characterise this project.`);
172
+ }
173
+ // An unrecognized project has almost no applicable detectors, so the absence
174
+ // of findings must not be rewarded with a high score: cap it at prototype.
175
+ const scoreAfterInconclusive = inconclusive ? Math.min(combinedScore, 39) : combinedScore;
176
+ /**
177
+ * The number obeys the same rule as the label.
178
+ *
179
+ * The score starts at 100 and only ever subtracts, so a repository small enough that
180
+ * almost nothing applies to it keeps most of what it was handed. The maturity label
181
+ * has refused to call that production ready since the coverage rule was added — and
182
+ * the number beside it went on saying 98, above every real product in the verification
183
+ * corpus. A Python transcription script rested on three passing checks out of four and
184
+ * outscored this product's own web application, which rests on thirty.
185
+ *
186
+ * Nothing is subtracted for thin coverage, because there is nothing to subtract for:
187
+ * the report simply may not enter the top band on evidence that cannot support it.
188
+ */
189
+ /**
190
+ * An unresolved critical bars the top band too.
191
+ *
192
+ * Counted before the score is capped, because it is one of the two reasons the top
193
+ * band can be refused and both must reach the number, not only the label.
194
+ */
195
+ const openCriticals = findings.filter((f) => f.severity === 'critical' && f.status !== 'passed' && f.status !== 'unknown').length;
196
+ const overallScore = (0, score_1.mayClaimTopBand)(coverage, openCriticals)
197
+ ? scoreAfterInconclusive
198
+ : Math.min(scoreAfterInconclusive, score_1.TOP_BAND_CEILING);
199
+ const maturityLevel = (0, score_1.computeMaturity)(overallScore, coverage, openCriticals);
159
200
  const findingsByCategory = Object.fromEntries(types_1.CATEGORIES.map((c) => [c, []]));
160
201
  for (const f of findings)
161
202
  findingsByCategory[f.category].push(f);
@@ -10,4 +10,51 @@ export interface ScoreCoverage {
10
10
  passed: number;
11
11
  assessed: number;
12
12
  }
13
- export declare function computeMaturity(score: number, coverage?: ScoreCoverage): MaturityLevel;
13
+ /**
14
+ * The fewest verdicts that can carry a reading at all.
15
+ *
16
+ * Below this the report has nothing to characterise: not a weak product, but a project
17
+ * this analyzer barely had anything to say about. In a corpus of seventy-eight
18
+ * repositories, eleven reach four verdicts or fewer and thirty reach ten or more —
19
+ * nothing lands in between, so the line sits in the gap rather than in the middle of a
20
+ * distribution.
21
+ */
22
+ export declare const MIN_ASSESSED_FOR_A_READING = 5;
23
+ /**
24
+ * The highest score a report can reach when it may not claim the top band.
25
+ *
26
+ * The band boundary itself, deliberately: a report that cannot support the words
27
+ * "production ready" must not print a number that reads like it either. Two things bar
28
+ * the top band — evidence too thin to support it, and an unresolved critical — and both
29
+ * land here.
30
+ */
31
+ export declare const TOP_BAND_CEILING = 84;
32
+ /**
33
+ * Whether the report checked enough to stand behind a top-band reading.
34
+ *
35
+ * Its own function because two things ask the question — the label and the number — and
36
+ * they were answering it differently. A repository small enough that almost nothing
37
+ * applies to it was labelled `partial` and scored 98, above every real product in the
38
+ * corpus; the label had learned that absence of findings is not evidence of quality and
39
+ * the score had not.
40
+ */
41
+ export declare function coverageSupportsTopBand(coverage: ScoreCoverage): boolean;
42
+ /**
43
+ * Whether a report may print a top-band number at all.
44
+ *
45
+ * The single place the two bars are stated, so the score and the label cannot drift
46
+ * apart again: they did, and the number went on reading 98 above every real product in
47
+ * the corpus while the label said `partial`.
48
+ */
49
+ export declare function mayClaimTopBand(coverage: ScoreCoverage, openCriticals: number): boolean;
50
+ /**
51
+ * How many critical findings are open.
52
+ *
53
+ * A separate argument rather than something derived from the score, because the score
54
+ * subtracts a fixed weight and then moves on: a project with forty-two passing checks
55
+ * and one unresolved critical came out at 91 and was called production ready. The
56
+ * category scores have refused to let a critical hide behind passing checks since the
57
+ * severity ceiling was added — "a category with one critical and nine passing checks is
58
+ * not 90% healthy" — and the report's own headline had not learned it.
59
+ */
60
+ export declare function computeMaturity(score: number, coverage?: ScoreCoverage, openCriticals?: number): MaturityLevel;
@@ -1,6 +1,9 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.TOP_BAND_CEILING = exports.MIN_ASSESSED_FOR_A_READING = void 0;
3
4
  exports.computeScore = computeScore;
5
+ exports.coverageSupportsTopBand = coverageSupportsTopBand;
6
+ exports.mayClaimTopBand = mayClaimTopBand;
4
7
  exports.computeMaturity = computeMaturity;
5
8
  const penaltyBySeverity = {
6
9
  critical: 15,
@@ -25,7 +28,58 @@ function computeScore(findings) {
25
28
  * nothing.
26
29
  */
27
30
  const MIN_VERIFIED_FOR_PRODUCTION = 6;
28
- function computeMaturity(score, coverage) {
31
+ /**
32
+ * The fewest verdicts that can carry a reading at all.
33
+ *
34
+ * Below this the report has nothing to characterise: not a weak product, but a project
35
+ * this analyzer barely had anything to say about. In a corpus of seventy-eight
36
+ * repositories, eleven reach four verdicts or fewer and thirty reach ten or more —
37
+ * nothing lands in between, so the line sits in the gap rather than in the middle of a
38
+ * distribution.
39
+ */
40
+ exports.MIN_ASSESSED_FOR_A_READING = 5;
41
+ /**
42
+ * The highest score a report can reach when it may not claim the top band.
43
+ *
44
+ * The band boundary itself, deliberately: a report that cannot support the words
45
+ * "production ready" must not print a number that reads like it either. Two things bar
46
+ * the top band — evidence too thin to support it, and an unresolved critical — and both
47
+ * land here.
48
+ */
49
+ exports.TOP_BAND_CEILING = 84;
50
+ /**
51
+ * Whether the report checked enough to stand behind a top-band reading.
52
+ *
53
+ * Its own function because two things ask the question — the label and the number — and
54
+ * they were answering it differently. A repository small enough that almost nothing
55
+ * applies to it was labelled `partial` and scored 98, above every real product in the
56
+ * corpus; the label had learned that absence of findings is not evidence of quality and
57
+ * the score had not.
58
+ */
59
+ function coverageSupportsTopBand(coverage) {
60
+ return coverage.passed >= MIN_VERIFIED_FOR_PRODUCTION && coverage.passed * 2 >= coverage.assessed;
61
+ }
62
+ /**
63
+ * Whether a report may print a top-band number at all.
64
+ *
65
+ * The single place the two bars are stated, so the score and the label cannot drift
66
+ * apart again: they did, and the number went on reading 98 above every real product in
67
+ * the corpus while the label said `partial`.
68
+ */
69
+ function mayClaimTopBand(coverage, openCriticals) {
70
+ return coverageSupportsTopBand(coverage) && openCriticals === 0;
71
+ }
72
+ /**
73
+ * How many critical findings are open.
74
+ *
75
+ * A separate argument rather than something derived from the score, because the score
76
+ * subtracts a fixed weight and then moves on: a project with forty-two passing checks
77
+ * and one unresolved critical came out at 91 and was called production ready. The
78
+ * category scores have refused to let a critical hide behind passing checks since the
79
+ * severity ceiling was added — "a category with one critical and nine passing checks is
80
+ * not 90% healthy" — and the report's own headline had not learned it.
81
+ */
82
+ function computeMaturity(score, coverage, openCriticals = 0) {
29
83
  if (score <= 39)
30
84
  return 'prototype';
31
85
  if (score <= 64)
@@ -45,8 +99,12 @@ function computeMaturity(score, coverage) {
45
99
  * Absence of findings is not evidence of quality. The top band now requires the
46
100
  * report to be able to point at things it checked and found right.
47
101
  */
48
- if (coverage && (coverage.passed < MIN_VERIFIED_FOR_PRODUCTION || coverage.passed * 2 < coverage.assessed)) {
102
+ if (coverage && !coverageSupportsTopBand(coverage)) {
49
103
  return 'partial';
50
104
  }
105
+ // Nothing is production ready while something critical is open in it. This is the one
106
+ // claim in the report a reader acts on without reading further.
107
+ if (openCriticals > 0)
108
+ return 'partial';
51
109
  return 'production_ready';
52
110
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@produtype/core",
3
- "version": "0.16.2",
3
+ "version": "0.18.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": {