@produtype/core 0.33.1 → 0.35.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.
@@ -160,7 +160,10 @@ function isTestOrExamplePath(file) {
160
160
  // `__mocks__` was missing, and a mock is the most misleading file in a repository:
161
161
  // `application_fee_percent: null` inside a Stripe fixture made an open-source CRM read
162
162
  // as a marketplace taking a cut. A field set to null is evidence of absence.
163
- return /(^|\/)(__tests__|__mocks__|mocks?|tests?|test-data|fixtures|frontend-example)(\/|$)/i.test(file)
163
+ // `fixtures` was listed and `fixture` was not, so `extra/fixture/authsources.php`
164
+ // made PHP one of the languages of an Elixir analytics product — and of cal.com,
165
+ // which is TypeScript.
166
+ return /(^|\/)(__tests__|__mocks__|mocks?|tests?|test-data|fixtures?|frontend-example)(\/|$)/i.test(file)
164
167
  || /(^|\/)test[-_][^/]+\.(ts|tsx|js|jsx|mjs|cjs|py)$/i.test(file)
165
168
  /**
166
169
  * The other half of the convention.
@@ -744,7 +747,16 @@ async function analyzeProject(projectPath) {
744
747
  : []),
745
748
  ],
746
749
  workspaces: workspaceStacks,
747
- files: allFiles,
750
+ /**
751
+ * The files this reading actually covers, not every file on disk.
752
+ *
753
+ * Built from `allFiles`, a single `extra/fixture/authsources.php` made PHP one of
754
+ * the languages of an Elixir analytics product and of cal.com, which is
755
+ * TypeScript. The languages a report names should be the ones it read; test
756
+ * fixtures and generated output are excluded from the reading and belong out of
757
+ * this list for the same reason.
758
+ */
759
+ files: sourceFiles,
748
760
  }),
749
761
  packageJson,
750
762
  pythonDeps,
@@ -230,8 +230,29 @@ async function detectAuth(ctx) {
230
230
  * `const teamId = 'W7LPPWA48L'` in its notarization script.
231
231
  */
232
232
  const STRONG_TENANCY = [/organizationId/i, /organization_id/i, /tenantId/i, /tenant_id/i];
233
- const WEAK_TENANCY = [/workspaceId/i, /workspace_id/i, /companyId/i];
234
- const strongOrganization = await (0, textSearch_1.searchInFiles)(ctx.root, sourceFiles, STRONG_TENANCY, 25);
233
+ const WEAK_TENANCY = [/workspaceId/i, /workspace_id/i, /companyId/i, /teamId/i, /team_id/i];
234
+ /**
235
+ * A team that has members is an account.
236
+ *
237
+ * "Team" is the third word products use for a tenant, after organization and
238
+ * workspace — and unlike those two it also means a team: a sports application has
239
+ * teams and no tenants. Documenso is the case that made it matter: `teamId` in three
240
+ * hundred and eighty-eight files, `TeamMember` in seventy-nine, `organizationId` in
241
+ * none, and no tenancy detected at all.
242
+ *
243
+ * The pairing is what disambiguates. A membership table beside the team turns a
244
+ * domain entity into an account boundary, which is the same reasoning the detector
245
+ * already applies to weak words: never on its own, always with something that means
246
+ * only one thing.
247
+ */
248
+ const teamMembership = await (0, textSearch_1.searchInFiles)(ctx.root, sourceFiles, [/teamMember/i, /team_member/i, /teamMembership/i], 10);
249
+ const teamAsTenant = teamMembership.length > 0
250
+ ? await (0, textSearch_1.searchInFiles)(ctx.root, sourceFiles, [/teamId/i, /team_id/i], 25)
251
+ : [];
252
+ const strongOrganization = [
253
+ ...(await (0, textSearch_1.searchInFiles)(ctx.root, sourceFiles, STRONG_TENANCY, 25)),
254
+ ...teamAsTenant,
255
+ ];
235
256
  const weakOrganization = await (0, textSearch_1.searchInFiles)(ctx.root, sourceFiles, WEAK_TENANCY, 25);
236
257
  // A weak word never stands on its own, however many files it appears in. Bruno says
237
258
  // `workspaceId` in three — it has workspaces, and they are local folders, not
@@ -248,7 +269,10 @@ async function detectAuth(ctx) {
248
269
  * already states the principle: a weak word never stands on its own, however many
249
270
  * files it appears in.
250
271
  */
251
- const strongMembership = await (0, textSearch_1.searchInFiles)(ctx.root, sourceFiles, STRONG_TENANCY, 25);
272
+ const strongMembership = [
273
+ ...(await (0, textSearch_1.searchInFiles)(ctx.root, sourceFiles, STRONG_TENANCY, 25)),
274
+ ...teamAsTenant,
275
+ ];
252
276
  const weakMembership = await (0, textSearch_1.searchInFiles)(ctx.root, sourceFiles, [/memberId/i, ...WEAK_TENANCY], 25);
253
277
  const membershipSignals = strongMembership.length > 0 ? [...strongMembership, ...weakMembership] : [];
254
278
  const b2bSignals = await (0, textSearch_1.searchInFiles)(ctx.root, sourceFiles, [
@@ -692,8 +692,25 @@ exports.productProfiles = {
692
692
  }),
693
693
  'ai-saas': defineProfile({
694
694
  id: 'ai-saas',
695
- title: 'AI SaaS',
696
- description: 'AI-powered SaaS with async workloads, upload safety, cost exposure and high observability needs.',
695
+ /**
696
+ * Named for what the evidence shows, not for what the product is about.
697
+ *
698
+ * The signal is a model SDK in the dependencies. That shows a product calls a model;
699
+ * it does not show that AI is what the product is. Supabase, Mattermost and PostHog
700
+ * all came back "AI SaaS" — a backend platform, a chat server and an analytics
701
+ * product, each with one assistant feature somewhere inside it.
702
+ *
703
+ * Counting was tried first and settles nothing: dify, a genuine AI platform, touches
704
+ * a model in 1.2% of its files and supabase in 1.2% too, while PostHog's 1.9% is
705
+ * higher than both. There is no proportion that separates them.
706
+ *
707
+ * What the profile *does* is right either way — it is B2B SaaS plus background
708
+ * jobs, cost control and prompt safety, and a product with one assistant feature
709
+ * owes all three for that feature. So the expectations stay and the title stops
710
+ * claiming more than the dependency proves.
711
+ */
712
+ title: 'SaaS that calls a model',
713
+ description: 'A hosted product that calls a language model somewhere in it: everything a B2B SaaS owes, plus the duties that come with sending somebody else\'s input to a model — a ceiling on what a request can spend, and treating what the model reads as untrusted.',
697
714
  importance: {
698
715
  'auth.baseline': 'required',
699
716
  'auth.mfa': 'recommended',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@produtype/core",
3
- "version": "0.33.1",
3
+ "version": "0.35.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": {