@produtype/core 0.44.0 → 0.46.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.
- package/dist/cli.js +11 -4
- package/dist/mcp/server.js +1 -1
- package/dist/planner/types.d.ts +2 -1
- package/dist/report/buildReport.js +32 -9
- package/dist/report/categoryScores.d.ts +8 -1
- package/dist/report/categoryScores.js +4 -3
- package/dist/report/executiveSummary.d.ts +2 -2
- package/dist/report/executiveSummary.js +7 -4
- package/dist/report/markdownReport.js +10 -8
- package/dist/report/types.d.ts +18 -3
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -98,7 +98,12 @@ function parseMinMaturity(value) {
|
|
|
98
98
|
return value;
|
|
99
99
|
}
|
|
100
100
|
function enforceThresholds(report, failUnder, minMaturity) {
|
|
101
|
-
if (failUnder !== undefined && report.overallScore
|
|
101
|
+
if (failUnder !== undefined && report.overallScore === null) {
|
|
102
|
+
// A gate asked for a number and there is none. Passing would say the project
|
|
103
|
+
// cleared a bar nobody measured it against.
|
|
104
|
+
throw new Error(`Score gate failed: this report has no score. ${report.inconclusiveReasons?.[0] ?? 'The reading was inconclusive.'}`);
|
|
105
|
+
}
|
|
106
|
+
if (failUnder !== undefined && report.overallScore !== null && report.overallScore < failUnder) {
|
|
102
107
|
throw new Error(`Score gate failed: overall score ${report.overallScore} is below --fail-under ${failUnder}.`);
|
|
103
108
|
}
|
|
104
109
|
if (minMaturity !== undefined && maturityOrder.indexOf(report.maturityLevel) < maturityOrder.indexOf(minMaturity)) {
|
|
@@ -137,7 +142,7 @@ function summarize(report) {
|
|
|
137
142
|
`Package manager: ${report.detectedStack.packageManager} (${report.detectedStack.packageManagerConfidence})`,
|
|
138
143
|
report.detectedStack.warnings.length > 0 ? `Warnings: ${report.detectedStack.warnings.join('; ')}` : 'Warnings: none',
|
|
139
144
|
`Workspaces: ${workspaceSummary}`,
|
|
140
|
-
`Score: ${report.overallScore}/100`,
|
|
145
|
+
report.overallScore === null ? 'Score: not scored' : `Score: ${report.overallScore}/100`,
|
|
141
146
|
/**
|
|
142
147
|
* The reasons, not a verdict about the project.
|
|
143
148
|
*
|
|
@@ -145,8 +150,10 @@ function summarize(report) {
|
|
|
145
150
|
* "Detected frontend: flutter" — the same summary naming the stack it had just
|
|
146
151
|
* failed to recognise. The report has carried the reasons since the flag existed.
|
|
147
152
|
*/
|
|
148
|
-
`Maturity: ${report.maturityLevel}
|
|
149
|
-
...(report.inconclusive
|
|
153
|
+
`Maturity: ${report.maturityLevel}`,
|
|
154
|
+
...(report.inconclusive
|
|
155
|
+
? ['INCONCLUSIVE — this report has no score, for these reasons:', ...report.inconclusiveReasons.map((reason) => ` - ${reason}`)]
|
|
156
|
+
: []),
|
|
150
157
|
// Printed next to the score it is not allowed to change, so a reader who knows what
|
|
151
158
|
// their project is can act on it in one step.
|
|
152
159
|
...(report.productProfile?.profileSuggestion
|
package/dist/mcp/server.js
CHANGED
|
@@ -201,7 +201,7 @@ async function createProdkitMcpServer() {
|
|
|
201
201
|
requiredTotal: report.productProfile?.gap.requiredTotal ?? 0,
|
|
202
202
|
verdict: report.executiveSummary.verdict,
|
|
203
203
|
};
|
|
204
|
-
}).sort((left, right) => right.overallScore - left.overallScore);
|
|
204
|
+
}).sort((left, right) => (right.overallScore ?? -1) - (left.overallScore ?? -1));
|
|
205
205
|
return textResult({
|
|
206
206
|
observedOnlyScore: observed.observedScore,
|
|
207
207
|
detectedStack: observed.detectedStack,
|
package/dist/planner/types.d.ts
CHANGED
|
@@ -29,7 +29,8 @@ export interface RemediationPhase {
|
|
|
29
29
|
export interface RemediationPlan {
|
|
30
30
|
projectPath: string;
|
|
31
31
|
generatedAt: string;
|
|
32
|
-
|
|
32
|
+
/** `null` where the report could not form a reading. */
|
|
33
|
+
score: number | null;
|
|
33
34
|
maturityLevel: string;
|
|
34
35
|
summary: string;
|
|
35
36
|
phases: RemediationPhase[];
|
|
@@ -246,9 +246,19 @@ function buildReport(analysis, options) {
|
|
|
246
246
|
if (tooLittleAssessed) {
|
|
247
247
|
inconclusiveReasons.push(`Only ${assessedChecks} check${assessedChecks === 1 ? '' : 's'} reached a verdict, which is too few to characterise this project.`);
|
|
248
248
|
}
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
249
|
+
/**
|
|
250
|
+
* An inconclusive reading has no score.
|
|
251
|
+
*
|
|
252
|
+
* This was a cap at 39, so that an unrecognised project could not be rewarded for
|
|
253
|
+
* the absence of findings. The intent was right and the output was not: eleven
|
|
254
|
+
* repositories in the corpus came out at exactly 39, and one of them — a Python
|
|
255
|
+
* transcription script — had no findings at all. A reader saw "39/100, prototype"
|
|
256
|
+
* about a repository whose own report said it could not be read.
|
|
257
|
+
*
|
|
258
|
+
* `null` is the rule this product applies everywhere else. The reasons already
|
|
259
|
+
* collected above are the answer in its place.
|
|
260
|
+
*/
|
|
261
|
+
const scoreAfterInconclusive = combinedScore;
|
|
252
262
|
/**
|
|
253
263
|
* The number obeys the same rule as the label.
|
|
254
264
|
*
|
|
@@ -269,10 +279,23 @@ function buildReport(analysis, options) {
|
|
|
269
279
|
* band can be refused and both must reach the number, not only the label.
|
|
270
280
|
*/
|
|
271
281
|
const openCriticals = findings.filter((f) => f.severity === 'critical' && f.status !== 'passed' && f.status !== 'unknown').length;
|
|
272
|
-
const overallScore =
|
|
273
|
-
?
|
|
274
|
-
:
|
|
275
|
-
|
|
282
|
+
const overallScore = inconclusive
|
|
283
|
+
? null
|
|
284
|
+
: (0, score_1.mayClaimTopBand)(coverage, openCriticals)
|
|
285
|
+
? scoreAfterInconclusive
|
|
286
|
+
: Math.min(scoreAfterInconclusive, score_1.TOP_BAND_CEILING);
|
|
287
|
+
const maturityLevel = overallScore === null
|
|
288
|
+
? 'inconclusive'
|
|
289
|
+
: (0, score_1.computeMaturity)(overallScore, coverage, openCriticals);
|
|
290
|
+
/**
|
|
291
|
+
* The observed half goes with it.
|
|
292
|
+
*
|
|
293
|
+
* "Overall score: not scored" printed directly above "Observed score: 100/100" —
|
|
294
|
+
* the report refusing to characterise a repository and then praising it in the next
|
|
295
|
+
* line. The observed number is the same arithmetic over the same three checks: if
|
|
296
|
+
* it cannot support a reading it cannot support half of one either.
|
|
297
|
+
*/
|
|
298
|
+
const reportedObservedScore = inconclusive ? null : observedScore;
|
|
276
299
|
const findingsByCategory = Object.fromEntries(types_1.CATEGORIES.map((c) => [c, []]));
|
|
277
300
|
for (const f of findings)
|
|
278
301
|
findingsByCategory[f.category].push(f);
|
|
@@ -320,7 +343,7 @@ function buildReport(analysis, options) {
|
|
|
320
343
|
categoryScores,
|
|
321
344
|
profile: productProfile,
|
|
322
345
|
maturity: maturityLevel,
|
|
323
|
-
observedScore,
|
|
346
|
+
observedScore: reportedObservedScore,
|
|
324
347
|
overallScore,
|
|
325
348
|
inconclusive,
|
|
326
349
|
inconclusiveReasons,
|
|
@@ -351,7 +374,7 @@ function buildReport(analysis, options) {
|
|
|
351
374
|
return {
|
|
352
375
|
projectPath: analysis.projectPath,
|
|
353
376
|
generatedAt: new Date().toISOString(),
|
|
354
|
-
observedScore,
|
|
377
|
+
observedScore: reportedObservedScore,
|
|
355
378
|
expectedCapabilityScore: expectationScore,
|
|
356
379
|
overallScore,
|
|
357
380
|
maturityLevel,
|
|
@@ -2,7 +2,14 @@ import type { Category, Finding } from './types';
|
|
|
2
2
|
export interface CategoryScore {
|
|
3
3
|
category: Category;
|
|
4
4
|
/** 0-100. 100 means nothing actionable was found in this category. */
|
|
5
|
-
|
|
5
|
+
/**
|
|
6
|
+
* `null` where nothing in the analysis touched this category.
|
|
7
|
+
*
|
|
8
|
+
* `notAssessed` has been beside it for a while and the number went on saying 100.
|
|
9
|
+
* On a report with no overall score at all, twelve categories still read 100 of 100
|
|
10
|
+
* — a repository the analyzer could not read, presented as perfect in twelve areas.
|
|
11
|
+
*/
|
|
12
|
+
score: number | null;
|
|
6
13
|
findingCount: number;
|
|
7
14
|
/** Checks that ran and found what they were looking for. */
|
|
8
15
|
verifiedCount: number;
|
|
@@ -103,9 +103,10 @@ function buildCategoryScores(findings) {
|
|
|
103
103
|
*/
|
|
104
104
|
const worstOpen = actionable.reduce((worst, finding) => (SEVERITY_WEIGHT[finding.severity] > SEVERITY_WEIGHT[worst] ? finding.severity : worst), 'info');
|
|
105
105
|
const ceiling = SEVERITY_CEILING[worstOpen];
|
|
106
|
+
const notAssessed = categoryFindings.every((finding) => finding.status === 'unknown');
|
|
106
107
|
return {
|
|
107
108
|
category,
|
|
108
|
-
score: Math.max(0, Math.min(100, proportional, ceiling)),
|
|
109
|
+
score: notAssessed ? null : Math.max(0, Math.min(100, proportional, ceiling)),
|
|
109
110
|
findingCount: actionable.length,
|
|
110
111
|
/** Checks in this category that ran and found what they were looking for. */
|
|
111
112
|
verifiedCount: assessed.filter((finding) => finding.status === 'passed').length,
|
|
@@ -122,7 +123,7 @@ function buildCategoryScores(findings) {
|
|
|
122
123
|
* A Django school platform with no payments anywhere was reported as having
|
|
123
124
|
* "no issues found in taking payments", as a strength.
|
|
124
125
|
*/
|
|
125
|
-
notAssessed
|
|
126
|
+
notAssessed,
|
|
126
127
|
};
|
|
127
128
|
});
|
|
128
129
|
}
|
|
@@ -130,6 +131,6 @@ function buildCategoryScores(findings) {
|
|
|
130
131
|
function weakestCategories(scores, limit = 3) {
|
|
131
132
|
return [...scores]
|
|
132
133
|
.filter((entry) => !entry.notAssessed && entry.findingCount > 0)
|
|
133
|
-
.sort((left, right) => left.score - right.score || right.criticalCount - left.criticalCount)
|
|
134
|
+
.sort((left, right) => (left.score ?? 100) - (right.score ?? 100) || right.criticalCount - left.criticalCount)
|
|
134
135
|
.slice(0, limit);
|
|
135
136
|
}
|
|
@@ -20,8 +20,8 @@ export declare function buildExecutiveSummary(args: {
|
|
|
20
20
|
categoryScores: CategoryScore[];
|
|
21
21
|
profile: ProductExpectationResult | undefined;
|
|
22
22
|
maturity: MaturityLevel;
|
|
23
|
-
observedScore: number;
|
|
24
|
-
overallScore: number;
|
|
23
|
+
observedScore: number | null;
|
|
24
|
+
overallScore: number | null;
|
|
25
25
|
inconclusive: boolean;
|
|
26
26
|
/** Why, when the report could not form a reading. Named in the verdict. */
|
|
27
27
|
inconclusiveReasons?: string[];
|
|
@@ -30,6 +30,7 @@ const CATEGORY_LABEL = {
|
|
|
30
30
|
docs: 'saying what this is',
|
|
31
31
|
};
|
|
32
32
|
const MATURITY_LABEL = {
|
|
33
|
+
inconclusive: 'not characterised, because too little of it could be read',
|
|
33
34
|
prototype: 'a prototype',
|
|
34
35
|
early: 'an early build',
|
|
35
36
|
partial: 'partly ready',
|
|
@@ -157,7 +158,7 @@ function buildStrengths(categoryScores, findings) {
|
|
|
157
158
|
const passedCategories = new Set(passed.map((finding) => finding.category));
|
|
158
159
|
const strongCategories = categoryScores
|
|
159
160
|
.filter((entry) => !entry.notAssessed
|
|
160
|
-
&& entry.score >= 80
|
|
161
|
+
&& (entry.score ?? 0) >= 80
|
|
161
162
|
&& entry.findingCount === 0
|
|
162
163
|
&& passedCategories.has(entry.category));
|
|
163
164
|
/**
|
|
@@ -192,9 +193,11 @@ function buildExecutiveSummary(args) {
|
|
|
192
193
|
}
|
|
193
194
|
return `${label}: ${entry.findingCount} ${entry.findingCount === 1 ? 'issue' : 'issues'} to address`;
|
|
194
195
|
});
|
|
195
|
-
const scoreExplanation = args.
|
|
196
|
-
?
|
|
197
|
-
:
|
|
196
|
+
const scoreExplanation = args.overallScore === null
|
|
197
|
+
? `Not scored. ${args.inconclusiveReasons?.[0] ?? 'Too little of this repository could be read to characterise it.'}`
|
|
198
|
+
: args.profile
|
|
199
|
+
? `${args.observedScore} of 100 on what the code does today, ${args.overallScore} of 100 once measured against what ${args.profile.profileTitle} normally requires.`
|
|
200
|
+
: `${args.observedScore} of 100 on what the code does today, with no product expectations applied.`;
|
|
198
201
|
return {
|
|
199
202
|
verdict,
|
|
200
203
|
launchReady,
|
|
@@ -40,9 +40,11 @@ function profileSummary(report) {
|
|
|
40
40
|
`- Selected profile: ${profile.selectedProfile}`,
|
|
41
41
|
`- Profile: ${profile.profileTitle}`,
|
|
42
42
|
`- Description: ${profile.profileDescription}`,
|
|
43
|
-
`- Observed score: ${report.observedScore}/100`,
|
|
44
|
-
|
|
45
|
-
|
|
43
|
+
report.observedScore === null ? '- Observed score: not scored' : `- Observed score: ${report.observedScore}/100`,
|
|
44
|
+
report.expectedCapabilityScore === undefined
|
|
45
|
+
? '- Expected capability score: not measured'
|
|
46
|
+
: `- Expected capability score: ${report.expectedCapabilityScore}/100`,
|
|
47
|
+
report.overallScore === null ? '- Final score: not scored' : `- Final score: ${report.overallScore}/100`,
|
|
46
48
|
'- Capability summary:',
|
|
47
49
|
`- Required: ${requiredPresent} present / ${requiredMissing} missing / ${requiredPartial} partial`,
|
|
48
50
|
`- Recommended: ${recommendedPresent} present / ${recommendedMissing} missing / ${recommendedPartial} partial`,
|
|
@@ -134,7 +136,7 @@ function categoryScoreSection(report) {
|
|
|
134
136
|
*/
|
|
135
137
|
'| Area | Score | Verified | Open findings | Critical |',
|
|
136
138
|
'| --- | --- | --- | --- | --- |',
|
|
137
|
-
...assessed.map((entry) => `| ${entry.category} | ${entry.score}/100 | ${entry.verifiedCount} of ${entry.assessedCount} | ${entry.findingCount} | ${entry.criticalCount} |`),
|
|
139
|
+
...assessed.map((entry) => `| ${entry.category} | ${entry.score === null ? 'not scored' : `${entry.score}/100`} | ${entry.verifiedCount} of ${entry.assessedCount} | ${entry.findingCount} | ${entry.criticalCount} |`),
|
|
138
140
|
'',
|
|
139
141
|
];
|
|
140
142
|
}
|
|
@@ -221,8 +223,8 @@ function renderMarkdown(report) {
|
|
|
221
223
|
'',
|
|
222
224
|
'## Score',
|
|
223
225
|
'',
|
|
224
|
-
`- Overall score: ${report.overallScore}/100`,
|
|
225
|
-
`- Observed score: ${report.observedScore}/100`,
|
|
226
|
+
report.overallScore === null ? '- Overall score: not scored' : `- Overall score: ${report.overallScore}/100`,
|
|
227
|
+
report.observedScore === null ? '- Observed score: not scored' : `- Observed score: ${report.observedScore}/100`,
|
|
226
228
|
...(report.expectedCapabilityScore !== undefined ? [`- Expected capability score: ${report.expectedCapabilityScore}/100`] : []),
|
|
227
229
|
`- Maturity level: ${report.maturityLevel}${report.inconclusive ? ' (inconclusive)' : ''}`,
|
|
228
230
|
/**
|
|
@@ -233,12 +235,12 @@ function renderMarkdown(report) {
|
|
|
233
235
|
* one of them rested on two verified checks while the other rested on twelve.
|
|
234
236
|
*/
|
|
235
237
|
`- Verified: ${report.diagnostics.verifiedChecks} of ${report.diagnostics.assessedChecks} checks that reached a verdict`,
|
|
236
|
-
...(report.overallScore > 84 && report.maturityLevel !== 'production_ready'
|
|
238
|
+
...(report.overallScore !== null && report.overallScore > 84 && report.maturityLevel !== 'production_ready'
|
|
237
239
|
? ['- Note: the score is high because little was found, not because much was verified. Too few checks apply to this repository to call it production ready.']
|
|
238
240
|
: []),
|
|
239
241
|
...(report.inconclusive
|
|
240
242
|
? [
|
|
241
|
-
'- Assessment: INCONCLUSIVE —
|
|
243
|
+
'- Assessment: INCONCLUSIVE — this report has no score, for these reasons:',
|
|
242
244
|
...report.inconclusiveReasons.map((r) => ` - ${r}`),
|
|
243
245
|
]
|
|
244
246
|
: []),
|
package/dist/report/types.d.ts
CHANGED
|
@@ -45,7 +45,16 @@ export interface Finding {
|
|
|
45
45
|
*/
|
|
46
46
|
businessImpact?: string;
|
|
47
47
|
}
|
|
48
|
-
|
|
48
|
+
/**
|
|
49
|
+
* `inconclusive` is a band of its own, not the bottom of the scale.
|
|
50
|
+
*
|
|
51
|
+
* A repository this analyzer could not read is not a prototype. It used to be called
|
|
52
|
+
* one, with a score of 39 beside it — the cap applied so that absence of findings
|
|
53
|
+
* could not be rewarded — and eleven repositories in the corpus came out at exactly
|
|
54
|
+
* 39: a wake-word engine, a database manager, an Advent of Code repository. One of
|
|
55
|
+
* them had no findings at all. The number was not measuring them; it was the cap.
|
|
56
|
+
*/
|
|
57
|
+
export type MaturityLevel = 'inconclusive' | 'prototype' | 'early' | 'partial' | 'production_ready';
|
|
49
58
|
export type ExpectationMode = 'observed-only' | 'explicit-profile' | 'auto-applied' | 'auto-inconclusive';
|
|
50
59
|
import type { LanguageReading } from '../analyzer/readingDepth';
|
|
51
60
|
export interface ReportDiagnostics {
|
|
@@ -79,9 +88,15 @@ export interface ReportDiagnostics {
|
|
|
79
88
|
export interface ProductionReadinessReport {
|
|
80
89
|
projectPath: string;
|
|
81
90
|
generatedAt: string;
|
|
82
|
-
|
|
91
|
+
/** `null` where the reading is inconclusive, for the same reason `overallScore` is. */
|
|
92
|
+
observedScore: number | null;
|
|
83
93
|
expectedCapabilityScore?: number;
|
|
84
|
-
|
|
94
|
+
/**
|
|
95
|
+
* `null` where the reading is inconclusive, which is the same rule this product
|
|
96
|
+
* applies everywhere else: a number that was not measured is not reported. The
|
|
97
|
+
* reasons in `inconclusiveReasons` are the answer in its place.
|
|
98
|
+
*/
|
|
99
|
+
overallScore: number | null;
|
|
85
100
|
maturityLevel: MaturityLevel;
|
|
86
101
|
inconclusive: boolean;
|
|
87
102
|
inconclusiveReasons: string[];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@produtype/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.46.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": {
|