@produtype/core 0.44.0 → 0.45.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 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 < failUnder) {
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}${report.inconclusive ? ' (INCONCLUSIVE, score capped)' : ''}`,
149
- ...(report.inconclusive ? report.inconclusiveReasons.map((reason) => ` - ${reason}`) : []),
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
@@ -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,
@@ -29,7 +29,8 @@ export interface RemediationPhase {
29
29
  export interface RemediationPlan {
30
30
  projectPath: string;
31
31
  generatedAt: string;
32
- score: number;
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
- // An unrecognized project has almost no applicable detectors, so the absence
250
- // of findings must not be rewarded with a high score: cap it at prototype.
251
- const scoreAfterInconclusive = inconclusive ? Math.min(combinedScore, 39) : combinedScore;
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,14 @@ 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 = (0, score_1.mayClaimTopBand)(coverage, openCriticals)
273
- ? scoreAfterInconclusive
274
- : Math.min(scoreAfterInconclusive, score_1.TOP_BAND_CEILING);
275
- const maturityLevel = (0, score_1.computeMaturity)(overallScore, coverage, openCriticals);
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);
276
290
  const findingsByCategory = Object.fromEntries(types_1.CATEGORIES.map((c) => [c, []]));
277
291
  for (const f of findings)
278
292
  findingsByCategory[f.category].push(f);
@@ -21,7 +21,7 @@ export declare function buildExecutiveSummary(args: {
21
21
  profile: ProductExpectationResult | undefined;
22
22
  maturity: MaturityLevel;
23
23
  observedScore: number;
24
- overallScore: number;
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',
@@ -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.profile
196
- ? `${args.observedScore} of 100 on what the code does today, ${args.overallScore} of 100 once measured against what ${args.profile.profileTitle} normally requires.`
197
- : `${args.observedScore} of 100 on what the code does today, with no product expectations applied.`;
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,
@@ -42,7 +42,7 @@ function profileSummary(report) {
42
42
  `- Description: ${profile.profileDescription}`,
43
43
  `- Observed score: ${report.observedScore}/100`,
44
44
  `- Expected capability score: ${report.expectedCapabilityScore}/100`,
45
- `- Final score: ${report.overallScore}/100`,
45
+ report.overallScore === null ? '- Final score: not scored' : `- Final score: ${report.overallScore}/100`,
46
46
  '- Capability summary:',
47
47
  `- Required: ${requiredPresent} present / ${requiredMissing} missing / ${requiredPartial} partial`,
48
48
  `- Recommended: ${recommendedPresent} present / ${recommendedMissing} missing / ${recommendedPartial} partial`,
@@ -221,7 +221,7 @@ function renderMarkdown(report) {
221
221
  '',
222
222
  '## Score',
223
223
  '',
224
- `- Overall score: ${report.overallScore}/100`,
224
+ report.overallScore === null ? '- Overall score: not scored' : `- Overall score: ${report.overallScore}/100`,
225
225
  `- Observed score: ${report.observedScore}/100`,
226
226
  ...(report.expectedCapabilityScore !== undefined ? [`- Expected capability score: ${report.expectedCapabilityScore}/100`] : []),
227
227
  `- Maturity level: ${report.maturityLevel}${report.inconclusive ? ' (inconclusive)' : ''}`,
@@ -233,7 +233,7 @@ function renderMarkdown(report) {
233
233
  * one of them rested on two verified checks while the other rested on twelve.
234
234
  */
235
235
  `- Verified: ${report.diagnostics.verifiedChecks} of ${report.diagnostics.assessedChecks} checks that reached a verdict`,
236
- ...(report.overallScore > 84 && report.maturityLevel !== 'production_ready'
236
+ ...(report.overallScore !== null && report.overallScore > 84 && report.maturityLevel !== 'production_ready'
237
237
  ? ['- 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
238
  : []),
239
239
  ...(report.inconclusive
@@ -45,7 +45,16 @@ export interface Finding {
45
45
  */
46
46
  businessImpact?: string;
47
47
  }
48
- export type MaturityLevel = 'prototype' | 'early' | 'partial' | 'production_ready';
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 {
@@ -81,7 +90,12 @@ export interface ProductionReadinessReport {
81
90
  generatedAt: string;
82
91
  observedScore: number;
83
92
  expectedCapabilityScore?: number;
84
- overallScore: number;
93
+ /**
94
+ * `null` where the reading is inconclusive, which is the same rule this product
95
+ * applies everywhere else: a number that was not measured is not reported. The
96
+ * reasons in `inconclusiveReasons` are the answer in its place.
97
+ */
98
+ overallScore: number | null;
85
99
  maturityLevel: MaturityLevel;
86
100
  inconclusive: boolean;
87
101
  inconclusiveReasons: string[];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@produtype/core",
3
- "version": "0.44.0",
3
+ "version": "0.45.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": {