@produtype/core 0.52.0 → 0.53.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.
@@ -188,7 +188,30 @@ function isTestOrExamplePath(file) {
188
188
  * band — against a line written to be fake.
189
189
  */
190
190
  || /[-_]tests?\.(ts|tsx|js|jsx|mjs|cjs|py)$/i.test(file)
191
- || /\.(test|spec)\.(ts|tsx|js|jsx|mjs|cjs|py)$/i.test(file);
191
+ || /\.(test|spec)\.(ts|tsx|js|jsx|mjs|cjs|py)$/i.test(file)
192
+ /**
193
+ * Sample code, which is what a documentation repository is made of.
194
+ *
195
+ * Every one of ktor-documentation's 435 Kotlin files sits under
196
+ * `codeSnippets/snippets/`, and the report judged them as a product: nine essential
197
+ * capabilities missing at `high`, including email verification and GDPR consent,
198
+ * for a repository that is documentation.
199
+ *
200
+ * `examples` was already here in the sense that mattered to Node; `samples` and
201
+ * `snippets` are the same idea spelled the way the JVM and the Rust ecosystems
202
+ * spell it. A repository whose every source file is a sample now has no source
203
+ * files, and says so — which is the honest answer for one.
204
+ */
205
+ || /(^|\/)(samples?|snippets?|codesnippets)(\/|$)/i.test(file)
206
+ /**
207
+ * Somebody else's code, vendored in.
208
+ *
209
+ * meilisearch was reported as using Rocket. Rocket appears once in the repository,
210
+ * as a dev-dependency of `external-crates/reqwest-eventsource` — a third-party
211
+ * crate copied in whole. A manifest under a vendor directory is a statement about
212
+ * that library, not about this product.
213
+ */
214
+ || /(^|\/)(vendor|vendored|third[-_]?party|external[-_]crates)(\/|$)/i.test(file);
192
215
  }
193
216
  /**
194
217
  * Extensions the detectors can read.
@@ -367,6 +390,18 @@ async function analyzeProject(projectPath) {
367
390
  const configFiles = pickConfig(allFiles);
368
391
  const packageJsonRaw = await (0, readTextFileSafe_1.readJsonSafe)(root, 'package.json');
369
392
  const packageJson = packageJsonRaw ? packageJsonSchema.parse(packageJsonRaw) : null;
393
+ /**
394
+ * The manifests this project owns, without the ones it merely contains.
395
+ *
396
+ * A vendored crate, a sample application and a test harness each carry a manifest,
397
+ * and each says something about itself rather than about the product. meilisearch
398
+ * was reported as using Rocket on the strength of a dev-dependency inside
399
+ * `external-crates/reqwest-eventsource` — a third-party library copied in whole.
400
+ *
401
+ * The same rule the source list has always used, applied to the file list the
402
+ * manifest readers walk.
403
+ */
404
+ const ownManifests = allFiles.filter((f) => !isTestOrExamplePath(f));
370
405
  const packageJsonFiles = allFiles.filter((f) => f.endsWith('package.json'));
371
406
  const requirementsFiles = allFiles.filter((f) => /(^|\/)requirements\.txt$/i.test(f));
372
407
  const pyprojectFiles = allFiles.filter((f) => /(^|\/)pyproject\.toml$/i.test(f));
@@ -427,7 +462,7 @@ async function analyzeProject(projectPath) {
427
462
  * than a wrong answer.
428
463
  */
429
464
  const goDeps = [];
430
- for (const file of allFiles.filter((f) => /(^|\/)go\.mod$/.test(f))) {
465
+ for (const file of ownManifests.filter((f) => /(^|\/)go\.mod$/.test(f))) {
431
466
  const raw = (await (0, readTextFileSafe_1.readTextFileSafe)(root, file)) ?? '';
432
467
  for (const line of raw.split('\n')) {
433
468
  const match = /^\s*(?:require\s+)?([a-z0-9][\w.-]*(?:\.[a-z]{2,})?\/[\w./-]+)\s+v/i.exec(line);
@@ -449,27 +484,44 @@ async function analyzeProject(projectPath) {
449
484
  * read.
450
485
  */
451
486
  const rustDeps = [];
452
- for (const file of allFiles.filter((f) => /(^|\/)Cargo\.toml$/.test(f))) {
487
+ /**
488
+ * The crates the product ships with, without the ones it only builds and tests with.
489
+ *
490
+ * meilisearch was reported as using Rocket, from a `[dev-dependencies]` block. The
491
+ * same distinction `runtimeNpmDeps` draws for the reason axios taught: a web
492
+ * framework in dev-dependencies is a test server, and the question "what serves the
493
+ * requests" is answered by what ships.
494
+ */
495
+ const runtimeRustDeps = [];
496
+ for (const file of ownManifests.filter((f) => /(^|\/)Cargo\.toml$/.test(f))) {
453
497
  const raw = (await (0, readTextFileSafe_1.readTextFileSafe)(root, file)) ?? '';
454
498
  let inDeps = false;
499
+ let runtimeSection = false;
455
500
  for (const line of raw.split('\n')) {
456
501
  const heading = /^\s*\[([^\]]+)\]/.exec(line);
457
502
  if (heading) {
458
503
  const section = heading[1].trim();
459
504
  const nested = /^(?:[a-z-]+\.)?(?:dependencies|dev-dependencies|build-dependencies)\.(.+)$/.exec(section);
460
505
  if (nested) {
461
- rustDeps.push(nested[1].trim().toLowerCase());
506
+ const crate = nested[1].trim().toLowerCase();
507
+ rustDeps.push(crate);
508
+ if (/(^|\.)dependencies\./.test(section))
509
+ runtimeRustDeps.push(crate);
462
510
  inDeps = false;
463
511
  continue;
464
512
  }
465
513
  inDeps = /(^|\.)(dependencies|dev-dependencies|build-dependencies)$/.test(section);
514
+ runtimeSection = /(^|\.)dependencies$/.test(section);
466
515
  continue;
467
516
  }
468
517
  if (!inDeps)
469
518
  continue;
470
519
  const entry = /^\s*([A-Za-z0-9_-]+)\s*=/.exec(line);
471
- if (entry)
520
+ if (entry) {
472
521
  rustDeps.push(entry[1].toLowerCase());
522
+ if (runtimeSection)
523
+ runtimeRustDeps.push(entry[1].toLowerCase());
524
+ }
473
525
  }
474
526
  }
475
527
  /**
@@ -482,7 +534,7 @@ async function analyzeProject(projectPath) {
482
534
  * from.
483
535
  */
484
536
  const dartDeps = [];
485
- for (const file of allFiles.filter((f) => /(^|\/)pubspec\.yaml$/.test(f))) {
537
+ for (const file of ownManifests.filter((f) => /(^|\/)pubspec\.yaml$/.test(f))) {
486
538
  const raw = (await (0, readTextFileSafe_1.readTextFileSafe)(root, file)) ?? '';
487
539
  let inDeps = false;
488
540
  for (const line of raw.split('\n')) {
@@ -543,13 +595,13 @@ async function analyzeProject(projectPath) {
543
595
  * no dependencies rather than a wrong answer.
544
596
  */
545
597
  const gradleDeps = [];
546
- for (const file of allFiles.filter((f) => /(^|\/)pom\.xml$/.test(f))) {
598
+ for (const file of ownManifests.filter((f) => /(^|\/)pom\.xml$/.test(f))) {
547
599
  const raw = (await (0, readTextFileSafe_1.readTextFileSafe)(root, file)) ?? '';
548
600
  for (const match of raw.matchAll(/<groupId>\s*([^<\s]+)\s*<\/groupId>\s*<artifactId>\s*([^<\s]+)\s*<\/artifactId>/g)) {
549
601
  gradleDeps.push(`${match[1]}:${match[2]}`.toLowerCase());
550
602
  }
551
603
  }
552
- for (const file of allFiles.filter((f) => /(^|\/)build\.gradle(\.kts)?$/.test(f))) {
604
+ for (const file of ownManifests.filter((f) => /(^|\/)build\.gradle(\.kts)?$/.test(f))) {
553
605
  const raw = (await (0, readTextFileSafe_1.readTextFileSafe)(root, file)) ?? '';
554
606
  for (const match of raw.matchAll(/\b(?:implementation|api|compileOnly|runtimeOnly|kapt|ksp|annotationProcessor|testImplementation)\s*[( ]\s*["']([^"']+)["']/g)) {
555
607
  const [group, artifact] = match[1].split(':');
@@ -557,7 +609,7 @@ async function analyzeProject(projectPath) {
557
609
  gradleDeps.push(`${group}:${artifact}`.toLowerCase());
558
610
  }
559
611
  }
560
- for (const file of allFiles.filter((f) => /(^|\/)libs\.versions\.toml$/.test(f))) {
612
+ for (const file of ownManifests.filter((f) => /(^|\/)libs\.versions\.toml$/.test(f))) {
561
613
  const raw = (await (0, readTextFileSafe_1.readTextFileSafe)(root, file)) ?? '';
562
614
  // Two spellings, both common: group and name as separate keys, or one `module`
563
615
  // holding the coordinate. Order within the line varies, so each is matched on its
@@ -586,13 +638,13 @@ async function analyzeProject(projectPath) {
586
638
  * CocoaPods is simpler and still in wide use: `pod 'Alamofire'`.
587
639
  */
588
640
  const swiftDeps = [];
589
- for (const file of allFiles.filter((f) => /(^|\/)Package\.swift$/.test(f))) {
641
+ for (const file of ownManifests.filter((f) => /(^|\/)Package\.swift$/.test(f))) {
590
642
  const raw = (await (0, readTextFileSafe_1.readTextFileSafe)(root, file)) ?? '';
591
643
  for (const match of raw.matchAll(/\.package\s*\(\s*url:\s*["']https?:\/\/[^"']*?\/([^/"']+?)\/([^/"']+?)(?:\.git)?["']/g)) {
592
644
  swiftDeps.push(`${match[1]}/${match[2]}`.toLowerCase());
593
645
  }
594
646
  }
595
- for (const file of allFiles.filter((f) => /(^|\/)Podfile$/.test(f))) {
647
+ for (const file of ownManifests.filter((f) => /(^|\/)Podfile$/.test(f))) {
596
648
  const raw = (await (0, readTextFileSafe_1.readTextFileSafe)(root, file)) ?? '';
597
649
  for (const match of raw.matchAll(/^\s*pod\s+["']([^"'/]+)/gm)) {
598
650
  swiftDeps.push(match[1].toLowerCase());
@@ -613,7 +665,7 @@ async function analyzeProject(projectPath) {
613
665
  */
614
666
  const dotnetDeps = [];
615
667
  let dotnetWebSdk = false;
616
- for (const file of allFiles.filter((f) => /\.(csproj|fsproj|vbproj)$/i.test(f))) {
668
+ for (const file of ownManifests.filter((f) => /\.(csproj|fsproj|vbproj)$/i.test(f))) {
617
669
  const raw = (await (0, readTextFileSafe_1.readTextFileSafe)(root, file)) ?? '';
618
670
  if (/Sdk\s*=\s*["']Microsoft\.NET\.Sdk\.Web["']/i.test(raw))
619
671
  dotnetWebSdk = true;
@@ -626,7 +678,7 @@ async function analyzeProject(projectPath) {
626
678
  }
627
679
  }
628
680
  const rubyDeps = [];
629
- for (const file of allFiles.filter((f) => /(^|\/)Gemfile$/.test(f))) {
681
+ for (const file of ownManifests.filter((f) => /(^|\/)Gemfile$/.test(f))) {
630
682
  const raw = (await (0, readTextFileSafe_1.readTextFileSafe)(root, file)) ?? '';
631
683
  for (const line of raw.split('\n')) {
632
684
  // Skip commented-out gems, which are otherwise indistinguishable from real ones.
@@ -725,6 +777,7 @@ async function analyzeProject(projectPath) {
725
777
  phpDeps: unique(phpDeps),
726
778
  goDeps: unique(goDeps),
727
779
  rustDeps: unique(rustDeps),
780
+ runtimeRustDeps: unique(runtimeRustDeps),
728
781
  rubyDeps: unique(rubyDeps),
729
782
  dotnetDeps: unique(dotnetDeps),
730
783
  dotnetWebSdk,
@@ -111,7 +111,7 @@ async function detectBackend(ctx) {
111
111
  /** Rust, read from Cargo.toml. */
112
112
  let namedRustFramework = false;
113
113
  for (const [framework, deps] of catalogue_1.RUST_BACKEND_FRAMEWORKS) {
114
- const hits = (0, detectContext_1.hasAnyRustDep)(ctx, deps);
114
+ const hits = (0, detectContext_1.hasAnyRuntimeRustDep)(ctx, deps);
115
115
  if (!hits.length)
116
116
  continue;
117
117
  namedRustFramework = true;
@@ -28,6 +28,8 @@ export interface DetectContext {
28
28
  goDeps: string[];
29
29
  /** Crate names from Cargo.toml, lowercase. */
30
30
  rustDeps: string[];
31
+ /** Of those, the ones the product ships with rather than only builds and tests with. */
32
+ runtimeRustDeps: string[];
31
33
  /** Gem names from the Gemfile, lowercase. */
32
34
  rubyDeps: string[];
33
35
  /** PackageReference and FrameworkReference names from .csproj, lowercase. */
@@ -74,6 +76,8 @@ export declare function hasRuntimePyDep(ctx: DetectContext, name: string): boole
74
76
  export declare function hasAnyDep(ctx: DetectContext, names: string[]): string[];
75
77
  export declare function hasPyDep(ctx: DetectContext, name: string): boolean;
76
78
  export declare function hasAnyRustDep(ctx: DetectContext, names: string[]): string[];
79
+ /** The Rust counterpart of `hasRuntimeDep`: shipped, not merely present. */
80
+ export declare function hasAnyRuntimeRustDep(ctx: DetectContext, names: string[]): string[];
77
81
  export declare function hasAnyPyDep(ctx: DetectContext, names: string[]): string[];
78
82
  export declare function hasPhpDep(ctx: DetectContext, name: string): boolean;
79
83
  export declare function hasAnyPhpDep(ctx: DetectContext, names: string[]): string[];
@@ -6,6 +6,7 @@ exports.hasRuntimePyDep = hasRuntimePyDep;
6
6
  exports.hasAnyDep = hasAnyDep;
7
7
  exports.hasPyDep = hasPyDep;
8
8
  exports.hasAnyRustDep = hasAnyRustDep;
9
+ exports.hasAnyRuntimeRustDep = hasAnyRuntimeRustDep;
9
10
  exports.hasAnyPyDep = hasAnyPyDep;
10
11
  exports.hasPhpDep = hasPhpDep;
11
12
  exports.hasAnyPhpDep = hasAnyPhpDep;
@@ -40,6 +41,10 @@ function hasPyDep(ctx, name) {
40
41
  function hasAnyRustDep(ctx, names) {
41
42
  return names.filter((name) => ctx.rustDeps.includes(name.toLowerCase()));
42
43
  }
44
+ /** The Rust counterpart of `hasRuntimeDep`: shipped, not merely present. */
45
+ function hasAnyRuntimeRustDep(ctx, names) {
46
+ return names.filter((name) => ctx.runtimeRustDeps.includes(name.toLowerCase()));
47
+ }
43
48
  function hasAnyPyDep(ctx, names) {
44
49
  return names.filter((n) => hasPyDep(ctx, n));
45
50
  }
@@ -134,10 +134,23 @@ function buildVerdict(args) {
134
134
  launchReady: null,
135
135
  };
136
136
  }
137
- if (!args.profile) {
137
+ /**
138
+ * No profile applied is not a profile satisfied.
139
+ *
140
+ * An inconclusive inference produces a profile record with an empty gap — nothing
141
+ * required, so nothing missing — and the verdict read "This project covers
142
+ * everything expected of Auto (inconclusive). What remains is refinement, not
143
+ * blockers." meilisearch, a search engine somebody self-hosts, was told it was ready
144
+ * to launch because no expectations had been applied to it.
145
+ *
146
+ * It is the same mistake as every other one this run has found, in the strongest
147
+ * possible direction: absence read as satisfaction.
148
+ */
149
+ if (!args.profile || args.profile.selectedProfile === 'auto') {
138
150
  return {
139
151
  verdict: `Judged on its code alone, this project looks like ${MATURITY_LABEL[args.maturity]}. Choose a product profile to see what it would still need to launch.`,
140
- launchReady: args.maturity === 'production_ready',
152
+ // Launch readiness is a question about a profile. Without one it was not asked.
153
+ launchReady: args.profile ? null : args.maturity === 'production_ready',
141
154
  };
142
155
  }
143
156
  const { gap, profileTitle } = args.profile;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@produtype/core",
3
- "version": "0.52.0",
3
+ "version": "0.53.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": {