@produtype/core 0.2.2 → 0.3.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.
Files changed (45) hide show
  1. package/README.md +49 -134
  2. package/dist/analyzer/analyzeProject.js +183 -3
  3. package/dist/analyzer/catalogue.d.ts +65 -0
  4. package/dist/analyzer/catalogue.js +263 -0
  5. package/dist/analyzer/catalogueMarkdown.d.ts +8 -0
  6. package/dist/analyzer/catalogueMarkdown.js +36 -0
  7. package/dist/analyzer/detectAiSafety.js +21 -1
  8. package/dist/analyzer/detectAudit.d.ts +3 -0
  9. package/dist/analyzer/detectAudit.js +62 -0
  10. package/dist/analyzer/detectBackend.js +118 -6
  11. package/dist/analyzer/detectBilling.js +58 -3
  12. package/dist/analyzer/detectClientLogic.d.ts +3 -0
  13. package/dist/analyzer/detectClientLogic.js +88 -0
  14. package/dist/analyzer/detectContext.d.ts +34 -0
  15. package/dist/analyzer/detectContext.js +45 -0
  16. package/dist/analyzer/detectDatabase.d.ts +1 -0
  17. package/dist/analyzer/detectDatabase.js +169 -1
  18. package/dist/analyzer/detectEnv.js +25 -2
  19. package/dist/analyzer/detectErrorReporting.d.ts +3 -0
  20. package/dist/analyzer/detectErrorReporting.js +59 -0
  21. package/dist/analyzer/detectFrontend.js +9 -7
  22. package/dist/analyzer/detectGame.d.ts +3 -0
  23. package/dist/analyzer/detectGame.js +202 -0
  24. package/dist/analyzer/detectJobs.js +120 -13
  25. package/dist/analyzer/detectStack.d.ts +2 -0
  26. package/dist/analyzer/detectStack.js +2 -0
  27. package/dist/analyzer/types.d.ts +7 -0
  28. package/dist/api.d.ts +9 -0
  29. package/dist/api.js +11 -1
  30. package/dist/cli.js +15 -3
  31. package/dist/expectations/evaluateExpectations.js +18 -2
  32. package/dist/expectations/inferProductProfile.js +70 -33
  33. package/dist/expectations/productProfiles.d.ts +3 -0
  34. package/dist/expectations/productProfiles.js +110 -5
  35. package/dist/expectations/profileSignals.d.ts +64 -0
  36. package/dist/expectations/profileSignals.js +180 -0
  37. package/dist/expectations/types.d.ts +33 -3
  38. package/dist/report/buildReport.js +6 -19
  39. package/dist/report/executiveSummary.js +1 -0
  40. package/dist/report/markdownReport.js +2 -1
  41. package/dist/report/types.d.ts +11 -1
  42. package/dist/report/types.js +27 -0
  43. package/dist/rules/rules.js +26 -8
  44. package/dist/utils/fileScanner.js +16 -0
  45. package/package.json +2 -1
package/README.md CHANGED
@@ -4,7 +4,7 @@ ProdKit is a deterministic local CLI that analyzes an existing web application r
4
4
 
5
5
  It is designed for early-stage and AI-generated apps where architecture and security quality can vary significantly.
6
6
 
7
- ProdKit MVP is read-only: it never modifies the target repository.
7
+ ProdKit is read-only: it never modifies the target repository.
8
8
 
9
9
  ## What ProdKit does
10
10
 
@@ -99,7 +99,7 @@ Options:
99
99
  - `--format markdown|json` output format for the report or plan payload
100
100
  - `--summary` print summary only
101
101
  - `--output <path>` write output to file
102
- - `--profile <name>` evaluate expected product capabilities (`static-site`, `internal-tool`, `b2c-app`, `b2b-saas`, `ai-saas`, `marketplace`, `auto`, `observed-only`)
102
+ - `--profile <name>` evaluate expected product capabilities (`static-site`, `internal-tool`, `b2c-app`, `b2b-saas`, `ai-saas`, `marketplace`, `game`, `client-app`, `auto`, `observed-only`)
103
103
  - `--fail-under <score>` (analyze only) exit with code 1 if the overall score is below the threshold — useful as a CI quality gate
104
104
  - `--min-maturity <level>` (analyze only) exit with code 1 if maturity is below `prototype|early|partial|production_ready`
105
105
  - `--ai` (analyze only) add an AI stack/architecture insight — opt-in, advisory only, does not affect the score
@@ -110,11 +110,13 @@ Optional AI layer:
110
110
  - **A separate package.** The AI features live in `@produtype/ai`, which is commercial
111
111
  and is not a dependency of this package. An open source install runs the
112
112
  deterministic analysis only, and `--ai` reports that the package is not installed
113
- rather than failing.
113
+ rather than failing. That package brings its own model access; nothing needs to be
114
+ configured here to run the analysis.
114
115
  - ProdKit stays deterministic, offline, and read-only: nothing here makes a network
115
116
  call.
116
- - They require an `ANTHROPIC_API_KEY` and the optional `@anthropic-ai/sdk` package (`npm install @anthropic-ai/sdk`). Without them, the deterministic analysis is unaffected.
117
- - Repository content is redacted of secrets before anything is sent, and AI output is advisory only — it never changes the deterministic score.
117
+ - When `@produtype/ai` is installed, it redacts secrets from repository content before
118
+ sending anything, and its output stays advisory — it never changes the deterministic
119
+ score.
118
120
 
119
121
  Profile warning:
120
122
 
@@ -145,145 +147,58 @@ prodkit plan ../my-app --format json
145
147
  prodkit plan ../my-app --output prodkit-plan.md
146
148
  ```
147
149
 
148
- ## Supported stacks in MVP
150
+ ## Supported stacks
149
151
 
150
- - Backend: Express, NestJS, Fastify, Next.js (Node); Django, FastAPI, Flask (Python)
151
- - Frontend: React, Vue, Svelte, Angular, Nuxt, Vite
152
- - Generic unknown app fallback
152
+ <!-- stacks:start -->
153
153
 
154
- ## Acceptance commands
154
+ _Generated from the analyzer itself — run `npm run docs:stacks` after changing a detector._
155
155
 
156
- ```bash
157
- npm install
158
- npm run build
159
- npm test
160
- npm link
161
- prodkit analyze tests/fixtures/express-basic --format markdown
162
- prodkit analyze tests/fixtures/express-basic --format json
163
- prodkit analyze tests/fixtures/express-basic --output report.md
164
- prodkit plan tests/fixtures/express-basic
165
- prodkit plan tests/fixtures/express-basic --format json
166
- prodkit plan tests/fixtures/express-basic --output prodkit-plan.md
167
- ```
156
+ - **Backend:** Express, Next.js, NestJS, Fastify, Hono, Elysia, Koa, AdonisJS, SvelteKit, Remix, Nuxt, Nitro, Astro, Django, Flask, FastAPI, Litestar, Sanic, Tornado, aiohttp, Starlette, Gin, Echo, Fiber, chi, Gorilla, Beego, Go, Rails, Sinatra, Hanami, Roda, Grape, Ruby, Laravel, Symfony, Slim, CodeIgniter, CakePHP, Yii, PHP, ASP.NET Core, .NET
157
+ - **Frontend:** React, Vite, Vue, Nuxt, Svelte, Angular, Astro, Solid, Qwik, Preact, Remix, htmx, Tailwind CSS, Electron
158
+ - **Mobile:** Flutter
159
+ - **Databases:** Postgres, MySQL, SQLite, SQL Server, MongoDB, Redis, Firestore, DynamoDB, Convex
160
+ - **Hosted data platforms:** Supabase, Firebase, PlanetScale, Neon, Vercel Postgres, Turso, Upstash, DynamoDB, Convex
161
+ - **ORMs:** Prisma, Drizzle, TypeORM, Sequelize, Knex, MikroORM, Kysely, SQLAlchemy, Tortoise, Peewee
162
+
163
+ How some of these are decided:
164
+
165
+ - **Express** — the dependency, or an import in the source
166
+ - **Astro** — counted as a backend only when configured to serve requests
167
+ - **Django** — manage.py, settings.py and urls.py together
168
+ - **Flask** — the dependency, or an import in the source
169
+ - **FastAPI** — the dependency, or an import in the source
170
+ - **Go** — a go.mod with no framework in it — net/http is a real answer
171
+ - **Ruby** — a Gemfile with no web framework in it
172
+ - **PHP** — PHP sources with no framework in composer.json
173
+ - **ASP.NET Core** — the Microsoft.NET.Sdk.Web SDK attribute
174
+ - **.NET** — a .csproj with no web SDK
175
+ - **Flutter** — pubspec.yaml — classified as a client application, not a backend
176
+ - **Supabase** — recorded alongside the engine it is — Postgres
177
+ - **Firebase** — Firestore
178
+ - **PlanetScale** — MySQL
179
+ - **Neon** — Postgres
180
+ - **Vercel Postgres** — Postgres
181
+ - **Turso** — SQLite
182
+ - **Upstash** — Redis
183
+
184
+ <!-- stacks:end -->
185
+
186
+ A hosted platform is recorded separately from the engine underneath it, so rules
187
+ written about an engine keep working without knowing about the host, while "this data
188
+ lives on infrastructure someone else operates" stays a question the report can ask on
189
+ its own.
168
190
 
169
191
  ## Current limitations
170
192
 
171
- - Deterministic heuristics only, with no AI explanation layer yet
193
+ - Deterministic heuristics only: the AI layer is a separate package (see above)
172
194
  - Signal-based stack coverage across common Node/Python/JS frameworks + fallback
173
195
  - Signal-based detection can produce false positives/negatives
174
196
  - Plan output is deterministic and read-only only
175
- - No cloud dashboard/UI in MVP
197
+ - No cloud dashboard or UI: this package is the CLI and the library
176
198
 
177
199
  ## Roadmap
178
200
 
179
- 1. Deterministic analyzer
180
- 2. Deterministic remediation planning
181
- 3. AI explanation layer
182
- 4. Apply/generate PRs
183
- 5. Cloud GitHub integration
184
- 6. Framework adapters
185
- 7. Dashboard
186
-
187
- ## Project layout
188
-
189
- ```text
190
- prodkit/
191
- package.json
192
- tsconfig.json
193
- README.md
194
- src/
195
- index.ts
196
- cli.ts
197
- analyzer/
198
- planner/
199
- rules/
200
- report/
201
- utils/
202
- tests/
203
- fixtures/
204
- analyzer.test.ts
205
- report.test.ts
206
- planner.test.ts
207
- ```
208
-
209
- ## GitHub Action
210
-
211
- Gate a pull request on production readiness:
212
-
213
- ```yaml
214
- - uses: denisbilli/prodkit@main
215
- with:
216
- profile: b2b-saas
217
- fail-under: "65"
218
- ```
219
-
220
- Inputs: `path`, `profile`, `fail-under`, `min-maturity`, `comment-summary`.
221
- Outputs: `score`, `maturity`, `launch-ready`.
222
-
223
- The executive summary is written to the job summary even when the thresholds fail the
224
- job — a check that fails without saying why is worse than no check at all.
225
-
226
- Requires the package to be published; until then, point the action at a local
227
- checkout.
228
-
229
- ## MCP server
230
-
231
- ProdKit ships an MCP server so an agent can assess a repository without leaving the
232
- editor. It exposes the deterministic analysis only, makes no network calls, and the
233
- code being analysed never leaves the machine.
234
-
235
- Tools: `analyze_project`, `plan_remediation`, `compare_profiles`, `list_profiles`.
236
-
237
- ProdKit is not on npm yet — the name `prodkit` is taken by an unrelated package — so
238
- install it from a local checkout:
239
-
240
- ```bash
241
- npm install && npm run build && npm link
242
- ```
243
-
244
- Claude Code:
245
-
246
- ```bash
247
- claude mcp add prodkit -- prodkit-mcp
248
- ```
249
-
250
- Or, in a client that reads a JSON config:
251
-
252
- ```json
253
- {
254
- "mcpServers": {
255
- "prodkit": {
256
- "command": "prodkit-mcp"
257
- }
258
- }
259
- }
260
- ```
261
-
262
- `compare_profiles` is the one to reach for first: it scores the same repository
263
- against every product profile in a single call, which is the question ProdKit exists
264
- to answer.
265
-
266
- ## Releases
267
-
268
- Published from CI on a version tag, with
269
- [npm provenance](https://docs.npmjs.com/generating-provenance-statements): the tarball
270
- is cryptographically linked to the commit and the workflow run that produced it, so it
271
- can be verified rather than merely trusted. No publish token ever sits on a developer
272
- machine.
273
-
274
- To cut a release:
275
-
276
- ```bash
277
- npm version patch # or minor / major — commits and tags
278
- git push --follow-tags
279
- ```
280
-
281
- The workflow refuses to publish if the tag and `package.json` disagree, and runs a
282
- check that the commercial AI layer is absent from `dist` before anything leaves.
283
- Release notes are generated from the commits since the previous tag.
284
-
285
- ## License
286
-
287
-
201
+ Shipped: the deterministic analyzer, deterministic remediation planning, and the
202
+ optional AI layer as `@produtype/ai`.
288
203
 
289
- MIT
204
+ Next: applying fixes as generated pull requests, and a hosted GitHub integration.
@@ -42,6 +42,10 @@ const detectPackageManager_1 = require("./detectPackageManager");
42
42
  const detectFrontend_1 = require("./detectFrontend");
43
43
  const detectBackend_1 = require("./detectBackend");
44
44
  const detectDatabase_1 = require("./detectDatabase");
45
+ const detectAudit_1 = require("./detectAudit");
46
+ const detectGame_1 = require("./detectGame");
47
+ const detectClientLogic_1 = require("./detectClientLogic");
48
+ const detectErrorReporting_1 = require("./detectErrorReporting");
45
49
  const detectDocker_1 = require("./detectDocker");
46
50
  const detectEnv_1 = require("./detectEnv");
47
51
  const detectAuth_1 = require("./detectAuth");
@@ -134,8 +138,44 @@ function isTestOrExamplePath(file) {
134
138
  || /(^|\/)test[-_][^/]+\.(ts|tsx|js|jsx|mjs|cjs|py)$/i.test(file)
135
139
  || /\.(test|spec)\.(ts|tsx|js|jsx|mjs|cjs|py)$/i.test(file);
136
140
  }
141
+ /**
142
+ * Extensions the detectors can read.
143
+ *
144
+ * PHP, Go, Ruby, Java, C# and Rust are here because a detector that greps for a
145
+ * hardcoded secret, an open CORS policy or an unprotected upload does not care what
146
+ * language surrounds the line. Stack detection still leans on manifests, so a Laravel
147
+ * application is not fully understood by being readable — but being readable is the
148
+ * difference between a partial reading and none.
149
+ */
150
+ const SOURCE_EXTENSIONS = /\.(ts|tsx|js|jsx|mjs|cjs|py|php|go|rb|java|cs|rs|kt|swift|dart)$/;
137
151
  function pickSource(files) {
138
- return files.filter((f) => /\.(ts|tsx|js|jsx|mjs|cjs|py)$/.test(f) && !isTestOrExamplePath(f));
152
+ return files.filter((f) => SOURCE_EXTENSIONS.test(f) && !isTestOrExamplePath(f));
153
+ }
154
+ /**
155
+ * Languages present in the repository that nothing here can read.
156
+ *
157
+ * The failure this exists to stop: a published PHP application, 57 source files, was
158
+ * analysed from the single JavaScript file in it. The report named a backend of
159
+ * "unknown", raised a critical for missing authentication that is written in PHP, and
160
+ * said "Warnings: none" — confident, detailed, and built on two per cent of the code.
161
+ *
162
+ * Being wrong is recoverable. Being wrong while announcing no reservations is not.
163
+ */
164
+ const KNOWN_UNREADABLE = [
165
+ [/\.(ex|exs)$/, 'Elixir'],
166
+ [/\.(scala|sc)$/, 'Scala'],
167
+ [/\.(clj|cljs)$/, 'Clojure'],
168
+ [/\.(cpp|cc|hpp)$/, 'C++'],
169
+ [/\.(erl|hrl)$/, 'Erlang'],
170
+ [/\.(hs)$/, 'Haskell'],
171
+ [/\.(pl|pm)$/, 'Perl'],
172
+ [/\.(lua)$/, 'Lua'],
173
+ ];
174
+ function unreadableLanguages(files) {
175
+ return KNOWN_UNREADABLE
176
+ .map(([pattern, language]) => ({ language, files: files.filter((f) => pattern.test(f)).length }))
177
+ .filter((entry) => entry.files > 0)
178
+ .sort((a, b) => b.files - a.files);
139
179
  }
140
180
  function pickConfig(files) {
141
181
  return files.filter((f) => /(package\.json|tsconfig|vite\.config|docker|compose|requirements\.txt|pyproject\.toml|settings\.py|\.env)/i.test(f));
@@ -269,6 +309,119 @@ async function analyzeProject(projectPath) {
269
309
  });
270
310
  }
271
311
  const pythonDeps = unique(workspaces.flatMap((w) => [...w.requirementsDeps, ...w.pyprojectDeps]));
312
+ /**
313
+ * Composer requirements. A published PHP application in the corpus reported a
314
+ * backend of "unknown" because nothing read this file, while 57 source files sat
315
+ * beside it saying plainly what the project was.
316
+ */
317
+ /**
318
+ * go.mod and the Gemfile, read the same way and for the same reason as composer.json:
319
+ * a repository whose manifest nothing here parses is reported with a backend of
320
+ * "unknown" while its source sits in plain sight.
321
+ *
322
+ * Both are parsed with regular expressions rather than properly. A go.mod require
323
+ * block and a `gem 'name'` line are simple enough that a parser would be more code
324
+ * than it is worth, and a manifest this cannot read yields no dependencies rather
325
+ * than a wrong answer.
326
+ */
327
+ const goDeps = [];
328
+ for (const file of allFiles.filter((f) => /(^|\/)go\.mod$/.test(f))) {
329
+ const raw = (await (0, readTextFileSafe_1.readTextFileSafe)(root, file)) ?? '';
330
+ for (const line of raw.split('\n')) {
331
+ const match = /^\s*(?:require\s+)?([a-z0-9][\w.-]*(?:\.[a-z]{2,})?\/[\w./-]+)\s+v/i.exec(line);
332
+ if (match)
333
+ goDeps.push(match[1].toLowerCase());
334
+ }
335
+ }
336
+ /**
337
+ * pubspec.yaml, read for its two dependency blocks.
338
+ *
339
+ * YAML with a hand-written reader again, and the shape here is forgiving: the blocks
340
+ * are `dependencies:` and `dev_dependencies:`, and each entry is a name at one level
341
+ * of indentation. Nested constraints — a git source, an sdk pin — sit deeper and are
342
+ * skipped, which is right: what matters is which package is used, not where it comes
343
+ * from.
344
+ */
345
+ const dartDeps = [];
346
+ for (const file of allFiles.filter((f) => /(^|\/)pubspec\.yaml$/.test(f))) {
347
+ const raw = (await (0, readTextFileSafe_1.readTextFileSafe)(root, file)) ?? '';
348
+ let inDeps = false;
349
+ for (const line of raw.split('\n')) {
350
+ if (/^(dev_)?dependencies\s*:/.test(line)) {
351
+ inDeps = true;
352
+ continue;
353
+ }
354
+ // Any other top-level key ends the block. Without this, everything below
355
+ // `dependencies:` to the end of the file would be read as a package.
356
+ if (/^[a-z_]+\s*:/i.test(line)) {
357
+ inDeps = false;
358
+ continue;
359
+ }
360
+ if (!inDeps)
361
+ continue;
362
+ const match = /^\s{2}([a-z0-9_]+)\s*:/i.exec(line);
363
+ if (match)
364
+ dartDeps.push(match[1].toLowerCase());
365
+ }
366
+ }
367
+ /**
368
+ * .csproj, which is XML rather than JSON or one-entry-per-line.
369
+ *
370
+ * Parsed with regular expressions like the others, and here that decision needs more
371
+ * defending: XML has real nesting, and a regex cannot see it. What is being read is
372
+ * two flat things — the Sdk attribute on the root element and the Include attribute
373
+ * of each PackageReference — and neither depends on where it sits in the tree. A
374
+ * project file this cannot read yields nothing, which is the same failure as an
375
+ * unreadable package.json.
376
+ *
377
+ * The Sdk attribute carries more than any dependency: `Microsoft.NET.Sdk.Web` is
378
+ * what makes a project a web application, and it appears in no package list.
379
+ */
380
+ const dotnetDeps = [];
381
+ let dotnetWebSdk = false;
382
+ for (const file of allFiles.filter((f) => /\.(csproj|fsproj|vbproj)$/i.test(f))) {
383
+ const raw = (await (0, readTextFileSafe_1.readTextFileSafe)(root, file)) ?? '';
384
+ if (/Sdk\s*=\s*["']Microsoft\.NET\.Sdk\.Web["']/i.test(raw))
385
+ dotnetWebSdk = true;
386
+ for (const match of raw.matchAll(/<PackageReference\s+Include\s*=\s*["']([^"']+)["']/gi)) {
387
+ dotnetDeps.push(match[1].toLowerCase());
388
+ }
389
+ // A framework reference is how an application declares it needs the web runtime.
390
+ for (const match of raw.matchAll(/<FrameworkReference\s+Include\s*=\s*["']([^"']+)["']/gi)) {
391
+ dotnetDeps.push(match[1].toLowerCase());
392
+ }
393
+ }
394
+ const rubyDeps = [];
395
+ for (const file of allFiles.filter((f) => /(^|\/)Gemfile$/.test(f))) {
396
+ const raw = (await (0, readTextFileSafe_1.readTextFileSafe)(root, file)) ?? '';
397
+ for (const line of raw.split('\n')) {
398
+ // Skip commented-out gems, which are otherwise indistinguishable from real ones.
399
+ if (/^\s*#/.test(line))
400
+ continue;
401
+ const match = /^\s*gem\s+['"]([^'"]+)['"]/.exec(line);
402
+ if (match)
403
+ rubyDeps.push(match[1].toLowerCase());
404
+ }
405
+ }
406
+ const composerFiles = allFiles.filter((file) => /(^|\/)composer\.json$/.test(file));
407
+ const phpDeps = [];
408
+ for (const file of composerFiles) {
409
+ const raw = await (0, readTextFileSafe_1.readTextFileSafe)(root, file);
410
+ if (!raw)
411
+ continue;
412
+ try {
413
+ const parsed = JSON.parse(raw);
414
+ const require_ = parsed?.require ?? {};
415
+ const requireDev = parsed?.['require-dev'] ?? {};
416
+ for (const name of [...Object.keys(require_), ...Object.keys(requireDev)]) {
417
+ phpDeps.push(name.toLowerCase());
418
+ }
419
+ }
420
+ catch {
421
+ // A composer.json that does not parse tells us nothing; it is not an error worth
422
+ // failing an analysis over.
423
+ }
424
+ }
272
425
  const npmDeps = {};
273
426
  for (const workspace of workspaces) {
274
427
  mergeDeps(npmDeps, workspace.packageJson?.dependencies);
@@ -298,10 +451,16 @@ async function analyzeProject(projectPath) {
298
451
  files: { all: allFiles, source: sourceFiles, config: configFiles },
299
452
  packageJson,
300
453
  pythonDeps,
454
+ phpDeps: unique(phpDeps),
455
+ goDeps: unique(goDeps),
456
+ rubyDeps: unique(rubyDeps),
457
+ dotnetDeps: unique(dotnetDeps),
458
+ dotnetWebSdk,
459
+ dartDeps: unique(dartDeps),
301
460
  npmDeps,
302
461
  workspaces,
303
462
  };
304
- const [pm, frontend, backend, database, docker, env, auth, security, uploads, gdpr, billing, observability, jobs, marketplace, aiSafety, engagement, deployment] = await Promise.all([
463
+ const [pm, frontend, backend, database, docker, env, auth, security, uploads, gdpr, billing, observability, jobs, marketplace, aiSafety, engagement, deployment, audit, game, clientLogic, errorReporting] = await Promise.all([
305
464
  (0, detectPackageManager_1.detectPackageManager)(ctx),
306
465
  (0, detectFrontend_1.detectFrontend)(ctx),
307
466
  (0, detectBackend_1.detectBackend)(ctx),
@@ -319,12 +478,21 @@ async function analyzeProject(projectPath) {
319
478
  (0, detectAiSafety_1.detectAiSafety)(ctx),
320
479
  (0, detectNotifications_1.detectEngagement)(ctx),
321
480
  (0, detectDeployment_1.detectDeployment)(ctx),
481
+ (0, detectAudit_1.detectAudit)(ctx),
482
+ (0, detectGame_1.detectGame)(ctx),
483
+ (0, detectClientLogic_1.detectClientLogic)(ctx),
484
+ (0, detectErrorReporting_1.detectErrorReporting)(ctx),
322
485
  ]);
323
486
  const detectors = (0, detectStack_1.mergeDetectors)([
324
487
  pm.result,
325
488
  frontend.result,
326
489
  backend.result,
327
490
  database.result,
491
+ ...database.extra,
492
+ audit,
493
+ ...game,
494
+ clientLogic,
495
+ errorReporting,
328
496
  docker,
329
497
  ...env,
330
498
  ...auth,
@@ -346,9 +514,21 @@ async function analyzeProject(projectPath) {
346
514
  frontend: frontend.frameworks,
347
515
  backend: backend.frameworks,
348
516
  databases: database.databases,
517
+ dataPlatforms: database.extra.find((d) => d.key === 'stack.dataPlatform')?.details?.platforms ?? [],
518
+ orms: database.extra.find((d) => d.key === 'stack.orm')?.details?.orms ?? [],
349
519
  packageManager: pm.manager,
350
520
  packageManagerConfidence: pm.confidence,
351
- warnings: pm.warnings,
521
+ /**
522
+ * A reading built on part of the code says so.
523
+ *
524
+ * The report for a published PHP application said "Warnings: none" while it had
525
+ * seen one file out of 57. Being wrong is recoverable; being wrong while
526
+ * announcing no reservations is not.
527
+ */
528
+ warnings: [
529
+ ...pm.warnings,
530
+ ...unreadableLanguages(allFiles).map((entry) => `${entry.files} ${entry.language} files were not analysed: this reading covers only part of the repository`),
531
+ ],
352
532
  workspaces: workspaceStacks,
353
533
  files: allFiles,
354
534
  }),
@@ -0,0 +1,65 @@
1
+ /**
2
+ * What the analyzer recognises, in one place, as data.
3
+ *
4
+ * These tables used to live inside the detector functions, and the documentation of
5
+ * them lived in a README and on a website — three copies of the same list, two of them
6
+ * maintained by hand. They had already drifted apart. Anything that answers "does it
7
+ * work on my stack?" now reads this, and the detectors iterate the same arrays, so a
8
+ * framework cannot be recognised without being listed or listed without being
9
+ * recognised.
10
+ *
11
+ * Entries whose detection is not a dependency lookup are still listed here, with
12
+ * `detectedFrom` saying what actually settles it, because a catalogue that quietly
13
+ * omitted Express or Django would be answering the question wrongly in the direction
14
+ * that loses users.
15
+ */
16
+ /** Node backend frameworks, each conclusive from one dependency. */
17
+ export declare const NODE_BACKEND_FRAMEWORKS: Array<[string, string]>;
18
+ /** Go frameworks, read from go.mod. */
19
+ export declare const GO_BACKEND_FRAMEWORKS: Array<[string, string[]]>;
20
+ /** Ruby frameworks, read from the Gemfile. */
21
+ export declare const RUBY_BACKEND_FRAMEWORKS: Array<[string, string[]]>;
22
+ /** PHP frameworks, read from composer.json. */
23
+ export declare const PHP_BACKEND_FRAMEWORKS: Array<[string, string[]]>;
24
+ /**
25
+ * Python frameworks settled by a dependency alone. Django, Flask and FastAPI are not
26
+ * here: each also has a source-level fallback, so each has its own block in the
27
+ * detector and its own entry in the catalogue below.
28
+ */
29
+ export declare const PYTHON_BACKEND_FRAMEWORKS: Array<[string, string]>;
30
+ /** Frontend frameworks settled by their signature dependency. */
31
+ export declare const FRONTEND_FRAMEWORKS: Array<[string, string[]]>;
32
+ export interface CatalogueEntry {
33
+ /** The identifier the report uses. */
34
+ id: string;
35
+ /** What a person calls it. */
36
+ label: string;
37
+ /** What settles it, for the cases a dependency name does not. */
38
+ detectedFrom?: string;
39
+ }
40
+ export interface StackCatalogue {
41
+ backend: CatalogueEntry[];
42
+ frontend: CatalogueEntry[];
43
+ mobile: CatalogueEntry[];
44
+ databases: CatalogueEntry[];
45
+ dataPlatforms: CatalogueEntry[];
46
+ orms: CatalogueEntry[];
47
+ languages: CatalogueEntry[];
48
+ }
49
+ export declare function labelFor(id: string): string;
50
+ /**
51
+ * Whether this id has a name written for it, rather than falling back to itself.
52
+ *
53
+ * Exists for the test: several of these are genuinely lowercase — aiohttp, chi, htmx —
54
+ * so "the label looks like an id" cannot tell a missing name from a correct one. A new
55
+ * framework added to a table with no label is the drift worth catching, and this is
56
+ * what catches it.
57
+ */
58
+ export declare function hasLabel(id: string): boolean;
59
+ /**
60
+ * Everything the analyzer can name, as a catalogue meant to be read by a person.
61
+ *
62
+ * Built from the same arrays the detectors iterate, plus explicit entries for the
63
+ * frameworks whose detection is not a dependency lookup.
64
+ */
65
+ export declare function supportedStacks(): StackCatalogue;