@produtype/core 0.20.0 → 0.21.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.
@@ -652,8 +652,16 @@ async function analyzeProject(projectPath) {
652
652
  // Said out loud, because a dependency nobody declared is a weaker fact than one
653
653
  // that is pinned in a lockfile, and the reader is entitled to know which of the
654
654
  // two this reading rests on.
655
+ /**
656
+ * Which manifest was missing, not "a manifest".
657
+ *
658
+ * "No dependency manifest was found: dependencies were read from Python imports"
659
+ * was printed for a Flutter application with a `pubspec.yaml` in its root — the
660
+ * same file the analyzer had just read to identify Flutter. The sentence is about
661
+ * the Python and browser dependency lists specifically, so it says so.
662
+ */
655
663
  ...(inferredDependencySources.length > 0
656
- ? [`No dependency manifest was found: dependencies were read from ${inferredDependencySources.join(' and ')}, so versions are unknown.`]
664
+ ? [`Some dependencies were read from ${inferredDependencySources.join(' and ')} rather than from a manifest, so their versions are unknown.`]
657
665
  : []),
658
666
  ],
659
667
  workspaces: workspaceStacks,
@@ -1,6 +1,33 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.detectPackageManager = detectPackageManager;
4
+ /**
5
+ * Manifests outside Node and Python, each one a file this analyzer already parses.
6
+ *
7
+ * Order matters only where two can coexist: a Gradle build with a `pom.xml` beside it is
8
+ * built by Gradle, and a `.csproj` next to a `packages.config` is NuGet either way.
9
+ *
10
+ * Read from the file list rather than from the workspace record, because the workspace
11
+ * record was built for Node and Python and knows nothing about these — which is the
12
+ * reason a Flutter project came out as `unknown` in the first place.
13
+ */
14
+ const OTHER_MANIFESTS = [
15
+ { manager: 'pub', pattern: /(^|\/)pubspec\.yaml$/ },
16
+ { manager: 'composer', pattern: /(^|\/)composer\.json$/ },
17
+ { manager: 'go modules', pattern: /(^|\/)go\.mod$/ },
18
+ { manager: 'bundler', pattern: /(^|\/)Gemfile$/ },
19
+ { manager: 'gradle', pattern: /(^|\/)build\.gradle(\.kts)?$/ },
20
+ { manager: 'maven', pattern: /(^|\/)pom\.xml$/ },
21
+ { manager: 'nuget', pattern: /\.(csproj|fsproj|vbproj)$/i },
22
+ ];
23
+ function resolveFromOtherManifests(files) {
24
+ for (const { manager, pattern } of OTHER_MANIFESTS) {
25
+ const match = files.find((file) => pattern.test(file));
26
+ if (match)
27
+ return { manager, confidence: 'manifest', warnings: [], evidence: [match] };
28
+ }
29
+ return null;
30
+ }
4
31
  function resolveWorkspaceManager(workspace) {
5
32
  const warnings = [];
6
33
  if (workspace.lockfiles.includes('pnpm-lock.yaml')) {
@@ -39,7 +66,12 @@ async function detectPackageManager(ctx) {
39
66
  };
40
67
  });
41
68
  const rootWorkspace = workspaceManagers.find((w) => w.root === '.');
42
- const selected = rootWorkspace ?? workspaceManagers.find((w) => w.manager !== 'unknown');
69
+ const fromWorkspaces = rootWorkspace ?? workspaceManagers.find((w) => w.manager !== 'unknown');
70
+ // Node and Python first, because the workspace record is built from their manifests and
71
+ // knows which of several it found. Anything else is resolved from the file list.
72
+ const selected = fromWorkspaces && fromWorkspaces.manager !== 'unknown'
73
+ ? fromWorkspaces
74
+ : resolveFromOtherManifests(ctx.files.all) ?? fromWorkspaces;
43
75
  if (selected) {
44
76
  return {
45
77
  manager: selected.manager,
@@ -1,4 +1,16 @@
1
- export type PackageManager = 'npm' | 'pnpm' | 'yarn' | 'pip' | 'poetry' | 'python' | 'unknown';
1
+ /**
2
+ * The names this tool can put to how a project declares its dependencies.
3
+ *
4
+ * It held seven, all of them Node or Python, so a Flutter application with a
5
+ * `pubspec.yaml` in its root reported "Package manager: unknown (unknown)" — in the
6
+ * same summary that had just read that file to identify Flutter. Five of the
7
+ * seventy-eight repositories in the verification corpus were in that position, across
8
+ * Dart, PHP, Kotlin and C#.
9
+ *
10
+ * Only managers whose manifest this analyzer actually reads are named here. A name it
11
+ * cannot back with a parsed file would be a guess from a filename.
12
+ */
13
+ export type PackageManager = 'npm' | 'pnpm' | 'yarn' | 'pip' | 'poetry' | 'python' | 'pub' | 'composer' | 'go modules' | 'bundler' | 'gradle' | 'maven' | 'nuget' | 'unknown';
2
14
  export type PackageManagerConfidence = 'lockfile' | 'manifest' | 'inferred' | 'unknown';
3
15
  export interface WorkspaceStack {
4
16
  root: string;
package/dist/cli.js CHANGED
@@ -42,10 +42,20 @@ const buildReport_1 = require("./report/buildReport");
42
42
  const markdownReport_1 = require("./report/markdownReport");
43
43
  const jsonReport_1 = require("./report/jsonReport");
44
44
  const buildPlan_1 = require("./planner/buildPlan");
45
+ const productProfiles_1 = require("./expectations/productProfiles");
45
46
  const markdownPlan_1 = require("./planner/markdownPlan");
46
47
  const jsonPlan_1 = require("./planner/jsonPlan");
47
48
  const pathUtils_1 = require("./utils/pathUtils");
48
49
  const version_1 = require("./version");
50
+ /**
51
+ * The profiles this tool actually has, read from the same table the analyzer uses.
52
+ *
53
+ * The list was typed out by hand and named eight of them: `library`, `game`,
54
+ * `client-app` and `mobile-app` had been added since, so a user asking `--help` which
55
+ * profiles exist was told four fewer than they could pass — including every profile that
56
+ * covers something other than a web application.
57
+ */
58
+ const PROFILE_OPTION_LIST = (0, productProfiles_1.productProfileChoices)().map((choice) => choice.id).join('|');
49
59
  const allowedProfiles = ['observed-only', 'static-site', 'internal-tool', 'b2c-app', 'b2b-saas', 'ai-saas', 'marketplace', 'game', 'client-app', 'mobile-app', 'library', 'auto'];
50
60
  const maturityOrder = ['prototype', 'early', 'partial', 'production_ready'];
51
61
  function normalizeProfile(profile) {
@@ -128,7 +138,15 @@ function summarize(report) {
128
138
  report.detectedStack.warnings.length > 0 ? `Warnings: ${report.detectedStack.warnings.join('; ')}` : 'Warnings: none',
129
139
  `Workspaces: ${workspaceSummary}`,
130
140
  `Score: ${report.overallScore}/100`,
131
- `Maturity: ${report.maturityLevel}${report.inconclusive ? ' (INCONCLUSIVE: project not recognized, score capped)' : ''}`,
141
+ /**
142
+ * The reasons, not a verdict about the project.
143
+ *
144
+ * This said "project not recognized" whatever the cause, two lines under
145
+ * "Detected frontend: flutter" — the same summary naming the stack it had just
146
+ * failed to recognise. The report has carried the reasons since the flag existed.
147
+ */
148
+ `Maturity: ${report.maturityLevel}${report.inconclusive ? ' (INCONCLUSIVE, score capped)' : ''}`,
149
+ ...(report.inconclusive ? report.inconclusiveReasons.map((reason) => ` - ${reason}`) : []),
132
150
  // Printed next to the score it is not allowed to change, so a reader who knows what
133
151
  // their project is can act on it in one step.
134
152
  ...(report.productProfile?.profileSuggestion
@@ -211,9 +229,10 @@ async function runCli(argv = process.argv) {
211
229
  .version(version_1.PRODKit_VERSION);
212
230
  program
213
231
  .command('analyze')
232
+ .description('Read a repository and report what production would demand of it.')
214
233
  .argument('<path-to-project>', 'Path to target repository')
215
234
  .option('--format <format>', 'Output format: markdown|json')
216
- .option('--profile <profile>', 'Product profile: observed-only|static-site|internal-tool|b2c-app|b2b-saas|ai-saas|marketplace|auto')
235
+ .option('--profile <profile>', `Product profile: ${PROFILE_OPTION_LIST}`)
217
236
  .option('--summary', 'Print summary only')
218
237
  .option('--output <path>', 'Output file path (optional)')
219
238
  .option('--fail-under <score>', 'Exit with an error if overall score is below this threshold (0-100)')
@@ -247,9 +266,10 @@ async function runCli(argv = process.argv) {
247
266
  });
248
267
  program
249
268
  .command('plan')
269
+ .description('Turn a report into an ordered list of remediation tasks.')
250
270
  .argument('<path-to-project>', 'Path to target repository')
251
271
  .option('--format <format>', 'Output format: markdown|json')
252
- .option('--profile <profile>', 'Product profile: observed-only|static-site|internal-tool|b2c-app|b2b-saas|ai-saas|marketplace|auto')
272
+ .option('--profile <profile>', `Product profile: ${PROFILE_OPTION_LIST}`)
253
273
  .option('--summary', 'Print summary only')
254
274
  .option('--output <path>', 'Output file path (optional)')
255
275
  .action(async (targetPath, options) => {
@@ -154,7 +154,21 @@ function buildReport(analysis, options) {
154
154
  * verdicts or fewer and thirty reach ten or more; not one lands in between. Four
155
155
  * verdicts cannot characterise a product, however many files it has.
156
156
  */
157
- const tooLittleAssessed = assessedChecks < score_1.MIN_ASSESSED_FOR_A_READING;
157
+ /**
158
+ * Only where the analysis was asked for a full reading.
159
+ *
160
+ * In observed-only mode nothing but the rules runs, and a small project reaches three
161
+ * or four verdicts because the expectations that would produce the rest were never
162
+ * requested — not because it is unreadable. The command-line tool defaults to that
163
+ * mode, and this rule, written against the profile path, turned twenty-three of its
164
+ * reports into "project not recognized": a Flutter application, a Vue application, a
165
+ * .NET game, each of them named on the line above by the same summary.
166
+ *
167
+ * Found by running the tool the way somebody who installed it would, rather than by
168
+ * calling the API the way the tests do.
169
+ */
170
+ const tooLittleAssessed = requestedProfile !== 'observed-only'
171
+ && assessedChecks < score_1.MIN_ASSESSED_FOR_A_READING;
158
172
  const inconclusive = nothingIdentified || tooLittleAssessed;
159
173
  // Each reason says which of the two it was, because they call for different things:
160
174
  // one is a repository this analyzer cannot read, the other is one there is barely
@@ -102,6 +102,16 @@ exports.DEFAULT_IGNORE = [
102
102
  'Intermediate/**',
103
103
  'DerivedData/**',
104
104
  '**/Pods/**',
105
+ /**
106
+ * Flutter's own generated tooling, and the word it chose for it.
107
+ *
108
+ * `ios/Flutter/ephemeral/` holds scripts Flutter writes and rewrites — one of them a
109
+ * Python helper for lldb. Its imports were being read as the project's dependencies,
110
+ * so a Dart application's report said some of its dependencies came from Python
111
+ * imports. The directory says what it is in its name.
112
+ */
113
+ '**/Flutter/ephemeral/**',
114
+ '**/.dart_tool/**',
105
115
  '**/Carthage/Build/**',
106
116
  // Composer, Go modules and Bundler all install into `vendor/`. The name means the
107
117
  // same thing in each: not ours.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@produtype/core",
3
- "version": "0.20.0",
3
+ "version": "0.21.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": {