@produtype/core 0.25.1 → 0.26.1

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.
@@ -596,7 +596,7 @@ async function analyzeProject(projectPath) {
596
596
  || w.packageManager !== 'unknown');
597
597
  const ctx = {
598
598
  root,
599
- files: { all: allFiles, source: sourceFiles, config: configFiles },
599
+ files: { all: allFiles, source: sourceFiles, config: configFiles, unreadable: unreadableLanguages(allFiles) },
600
600
  packageJson,
601
601
  pythonDeps,
602
602
  phpDeps: unique(phpDeps),
@@ -704,6 +704,7 @@ async function analyzeProject(projectPath) {
704
704
  pythonDeps,
705
705
  workspaceStacks,
706
706
  files: {
707
+ unreadable: unreadableLanguages(allFiles),
707
708
  all: allFiles,
708
709
  source: sourceFiles,
709
710
  config: configFiles,
@@ -90,7 +90,20 @@ async function detectAuth(ctx) {
90
90
  /createServerClient/,
91
91
  /\[\.\.\.nextauth\]/i,
92
92
  ], 30);
93
- const twoFaSignals = await (0, textSearch_1.searchInFiles)(ctx.root, sourceFiles, [/two[_-]?factor/i, /otp/i, /totp/i], 20);
93
+ /**
94
+ * `otp` as a word, not as three letters inside another one.
95
+ *
96
+ * `/otp/i` matched `VarError::NotPresent` and `PandasUseOfDotPivotOrUpdate` — N-**otp**-resent
97
+ * and D-**otp**-ivot — so a Rust linter was credited with two-factor authentication.
98
+ * Two of the three signals behind that reading were substring collisions of this kind.
99
+ *
100
+ * Word boundaries in the languages people actually write: delimited by a non-letter
101
+ * (`otp_secret`, `verify(otp)`), or the capital that starts a camelCase word
102
+ * (`verifyOtp`, `otpCode`). `pyotp` no longer matches here and does not need to: it is
103
+ * a dependency, and dependencies are read from the manifest above.
104
+ */
105
+ const OTP_AS_A_WORD = /(?:^|[^a-z])t?otp(?:[^a-z]|$)|[a-z_](?:Otp|OTP|Totp|TOTP)/;
106
+ const twoFaSignals = await (0, textSearch_1.searchInFiles)(ctx.root, sourceFiles, [/two[_-]?factor/i, OTP_AS_A_WORD], 20);
94
107
  const apiKeySignals = await (0, textSearch_1.searchInFiles)(ctx.root, sourceFiles, [
95
108
  /**
96
109
  * A key this project checks, not a key it holds.
@@ -206,8 +219,17 @@ async function detectAuth(ctx) {
206
219
  const organizationSignals = strongOrganization.length > 0
207
220
  ? [...strongOrganization, ...weakOrganization]
208
221
  : [];
209
- const strongMembership = await (0, textSearch_1.searchInFiles)(ctx.root, sourceFiles, [/memberId/i, ...STRONG_TENANCY], 25);
210
- const weakMembership = await (0, textSearch_1.searchInFiles)(ctx.root, sourceFiles, WEAK_TENANCY, 25);
222
+ /**
223
+ * `memberId` is a weak word, by this detector's own rule.
224
+ *
225
+ * It was strong enough to stand alone, and `ScopedMemberId` — a symbol table in a
226
+ * compiler — was read as a tenant membership. "Member" means a struct field in most
227
+ * languages and a person in a few; only a tenant word says which. The comment above
228
+ * already states the principle: a weak word never stands on its own, however many
229
+ * files it appears in.
230
+ */
231
+ const strongMembership = await (0, textSearch_1.searchInFiles)(ctx.root, sourceFiles, STRONG_TENANCY, 25);
232
+ const weakMembership = await (0, textSearch_1.searchInFiles)(ctx.root, sourceFiles, [/memberId/i, ...WEAK_TENANCY], 25);
211
233
  const membershipSignals = strongMembership.length > 0 ? [...strongMembership, ...weakMembership] : [];
212
234
  const b2bSignals = await (0, textSearch_1.searchInFiles)(ctx.root, sourceFiles, [
213
235
  /stripe/i,
@@ -86,6 +86,19 @@ export interface ProjectFiles {
86
86
  source: string[];
87
87
  /** Config files. */
88
88
  config: string[];
89
+ /**
90
+ * Files in languages nothing here can read, counted by language.
91
+ *
92
+ * The warning built from this has existed for a while and said the right thing —
93
+ * "1257 Elixir files were not analysed: this reading covers only part of the
94
+ * repository" — while the same report named a profile with high confidence and scored
95
+ * it 85. Exposing the count rather than only the sentence lets the reading take its
96
+ * own warning into account instead of printing it beside a verdict that ignores it.
97
+ */
98
+ unreadable: Array<{
99
+ language: string;
100
+ files: number;
101
+ }>;
89
102
  }
90
103
  export interface PackageJson {
91
104
  name?: string;
@@ -169,7 +169,26 @@ function buildReport(analysis, options) {
169
169
  */
170
170
  const tooLittleAssessed = requestedProfile !== 'observed-only'
171
171
  && assessedChecks < score_1.MIN_ASSESSED_FOR_A_READING;
172
- const inconclusive = nothingIdentified || tooLittleAssessed;
172
+ /**
173
+ * The report reads its own warning.
174
+ *
175
+ * Plausible is a Phoenix application: 1257 Elixir files and 215 of JavaScript around
176
+ * them. The analyzer said so — "1257 Elixir files were not analysed: this reading
177
+ * covers only part of the repository" — and in the same report called it a client
178
+ * application, with high confidence, scored 85 and not inconclusive. A warning printed
179
+ * beside a verdict that ignores it is not a warning.
180
+ *
181
+ * The test is which part is bigger. Where the languages nothing here can read
182
+ * outnumber the files it did read, the reading rests on a minority of the repository
183
+ * and cannot characterise it — whatever those remaining files happen to look like. A
184
+ * game with a handful of C++ files beside its TypeScript is unaffected, which is the
185
+ * shape this must not break: not one of the seventy-eight repositories on the machine
186
+ * this was written on crosses the line, and the first public repository that did was
187
+ * the sixth one tried.
188
+ */
189
+ const unreadableFiles = analysis.files.unreadable.reduce((total, entry) => total + entry.files, 0);
190
+ const mostlyUnreadable = unreadableFiles > analysis.files.source.length;
191
+ const inconclusive = nothingIdentified || tooLittleAssessed || mostlyUnreadable;
173
192
  // Each reason says which of the two it was, because they call for different things:
174
193
  // one is a repository this analyzer cannot read, the other is one there is barely
175
194
  // anything to read.
@@ -181,6 +200,10 @@ function buildReport(analysis, options) {
181
200
  inconclusiveReasons.push('No recognizable source files were found.');
182
201
  }
183
202
  }
203
+ if (mostlyUnreadable) {
204
+ const [largest] = [...analysis.files.unreadable].sort((left, right) => right.files - left.files);
205
+ inconclusiveReasons.push(`Most of this repository is written in ${largest.language}, which this analyzer does not read: ${unreadableFiles} of its files were skipped and ${analysis.files.source.length} were read.`);
206
+ }
184
207
  if (tooLittleAssessed) {
185
208
  inconclusiveReasons.push(`Only ${assessedChecks} check${assessedChecks === 1 ? '' : 's'} reached a verdict, which is too few to characterise this project.`);
186
209
  }
@@ -261,6 +284,7 @@ function buildReport(analysis, options) {
261
284
  observedScore,
262
285
  overallScore,
263
286
  inconclusive,
287
+ inconclusiveReasons,
264
288
  });
265
289
  const expectationMode = determineExpectationMode(requestedProfile, productProfile, expectationScore);
266
290
  const diagnostics = {
@@ -23,4 +23,6 @@ export declare function buildExecutiveSummary(args: {
23
23
  observedScore: number;
24
24
  overallScore: number;
25
25
  inconclusive: boolean;
26
+ /** Why, when the report could not form a reading. Named in the verdict. */
27
+ inconclusiveReasons?: string[];
26
28
  }): ExecutiveSummary;
@@ -109,8 +109,19 @@ function estimateEffort(findings, profile) {
109
109
  }
110
110
  function buildVerdict(args) {
111
111
  if (args.inconclusive) {
112
+ /**
113
+ * The reason, where there is one worth giving.
114
+ *
115
+ * "Check that the path points at application source" is right for a directory with
116
+ * nothing in it and wrong for a Phoenix application: the path was fine, the language
117
+ * is one this analyzer does not read, and telling its author to check their path
118
+ * sends them looking for a mistake they did not make.
119
+ */
120
+ const [reason] = args.inconclusiveReasons;
112
121
  return {
113
- verdict: 'ProdKit could not recognise this project well enough to judge it. Check that the path points at application source.',
122
+ verdict: reason
123
+ ? `ProdKit could not judge this project. ${reason}`
124
+ : 'ProdKit could not recognise this project well enough to judge it. Check that the path points at application source.',
114
125
  launchReady: false,
115
126
  };
116
127
  }
@@ -172,6 +183,7 @@ function buildExecutiveSummary(args) {
172
183
  profile: args.profile,
173
184
  maturity: args.maturity,
174
185
  inconclusive: args.inconclusive,
186
+ inconclusiveReasons: args.inconclusiveReasons ?? [],
175
187
  });
176
188
  const topRisks = (0, categoryScores_1.weakestCategories)(args.categoryScores).map((entry) => {
177
189
  const label = CATEGORY_LABEL[entry.category];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@produtype/core",
3
- "version": "0.25.1",
3
+ "version": "0.26.1",
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": {