@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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
696
|
-
|
|
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.
|
|
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": {
|