@produtype/core 0.6.0 → 0.7.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.
|
@@ -135,7 +135,10 @@ function parsePyproject(text) {
|
|
|
135
135
|
return deps;
|
|
136
136
|
}
|
|
137
137
|
function isTestOrExamplePath(file) {
|
|
138
|
-
|
|
138
|
+
// `__mocks__` was missing, and a mock is the most misleading file in a repository:
|
|
139
|
+
// `application_fee_percent: null` inside a Stripe fixture made an open-source CRM read
|
|
140
|
+
// as a marketplace taking a cut. A field set to null is evidence of absence.
|
|
141
|
+
return /(^|\/)(__tests__|__mocks__|mocks?|tests?|test-data|fixtures|frontend-example)(\/|$)/i.test(file)
|
|
139
142
|
|| /(^|\/)test[-_][^/]+\.(ts|tsx|js|jsx|mjs|cjs|py)$/i.test(file)
|
|
140
143
|
|| /\.(test|spec)\.(ts|tsx|js|jsx|mjs|cjs|py)$/i.test(file);
|
|
141
144
|
}
|
|
@@ -83,19 +83,32 @@ async function detectAuth(ctx) {
|
|
|
83
83
|
/organizationId/i,
|
|
84
84
|
/tenantId/i,
|
|
85
85
|
/workspaceId/i,
|
|
86
|
-
/teamId/i,
|
|
87
86
|
], 20);
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
87
|
+
/**
|
|
88
|
+
* Words that only mean tenancy, and words that usually mean something else.
|
|
89
|
+
*
|
|
90
|
+
* `organizationId` and `tenantId` are not written by accident. `workspaceId` and
|
|
91
|
+
* `companyId` are common in SaaS and also in code that talks *about* somebody else's
|
|
92
|
+
* product, so they are corroborated across files before they count.
|
|
93
|
+
*
|
|
94
|
+
* `teamId` was a signal and is gone. In the JavaScript ecosystem it is overwhelmingly
|
|
95
|
+
* Apple's Developer Team ID: `usebruno/bruno`, a desktop API client with no accounts
|
|
96
|
+
* of any kind, was classified as a B2B SaaS with high confidence on the strength of
|
|
97
|
+
* `const teamId = 'W7LPPWA48L'` in its notarization script.
|
|
98
|
+
*/
|
|
99
|
+
const STRONG_TENANCY = [/organizationId/i, /organization_id/i, /tenantId/i, /tenant_id/i];
|
|
100
|
+
const WEAK_TENANCY = [/workspaceId/i, /workspace_id/i, /companyId/i];
|
|
101
|
+
const strongOrganization = await (0, textSearch_1.searchInFiles)(ctx.root, sourceFiles, STRONG_TENANCY, 25);
|
|
102
|
+
const weakOrganization = await (0, textSearch_1.searchInFiles)(ctx.root, sourceFiles, WEAK_TENANCY, 25);
|
|
103
|
+
// A weak word never stands on its own, however many files it appears in. Bruno says
|
|
104
|
+
// `workspaceId` in three — it has workspaces, and they are local folders, not
|
|
105
|
+
// customers. A tenant boundary is named somewhere by a word that means only that.
|
|
106
|
+
const organizationSignals = strongOrganization.length > 0
|
|
107
|
+
? [...strongOrganization, ...weakOrganization]
|
|
108
|
+
: [];
|
|
109
|
+
const strongMembership = await (0, textSearch_1.searchInFiles)(ctx.root, sourceFiles, [/memberId/i, ...STRONG_TENANCY], 25);
|
|
110
|
+
const weakMembership = await (0, textSearch_1.searchInFiles)(ctx.root, sourceFiles, WEAK_TENANCY, 25);
|
|
111
|
+
const membershipSignals = strongMembership.length > 0 ? [...strongMembership, ...weakMembership] : [];
|
|
99
112
|
const b2bSignals = await (0, textSearch_1.searchInFiles)(ctx.root, sourceFiles, [
|
|
100
113
|
/stripe/i,
|
|
101
114
|
/subscription/i,
|
|
@@ -12,7 +12,18 @@ async function detectBilling(ctx) {
|
|
|
12
12
|
if (hasStripeDep)
|
|
13
13
|
evidence.push({ type: 'dependency', value: 'stripe' });
|
|
14
14
|
const stripeContextHits = await (0, textSearch_1.searchInFiles)(ctx.root, ctx.files.source, [
|
|
15
|
-
|
|
15
|
+
/**
|
|
16
|
+
* The bare word is not here, on purpose.
|
|
17
|
+
*
|
|
18
|
+
* `\bstripe\b` matched `'table-stripe': '#f3f3f3'`, a CSS colour token for
|
|
19
|
+
* zebra-striped tables, and `{['Stripe API', 'GitHub REST', …]}`, a list of API
|
|
20
|
+
* names in a design-system demo. Both are in `usebruno/bruno`, a desktop API
|
|
21
|
+
* client that takes no payments and was reported as a B2B SaaS.
|
|
22
|
+
*
|
|
23
|
+
* A project that actually charges people has the dependency, a STRIPE_ variable,
|
|
24
|
+
* a customer or subscription id, or a webhook path. One that only ever writes
|
|
25
|
+
* "Stripe" in prose is talking about Stripe, not billing through it.
|
|
26
|
+
*/
|
|
16
27
|
/STRIPE_[A-Z0-9_]+/,
|
|
17
28
|
/stripeCustomerId/i,
|
|
18
29
|
/stripeSubscriptionId/i,
|
|
@@ -36,15 +36,34 @@ async function detectMultiRole(ctx) {
|
|
|
36
36
|
for (const hit of [...sellerHits, ...buyerHits]) {
|
|
37
37
|
evidence.push({ type: 'snippet', value: hit.snippet, file: hit.file, line: hit.line });
|
|
38
38
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
39
|
+
/**
|
|
40
|
+
* File names that suggest a supply side — read from the analysed sources, not from
|
|
41
|
+
* every path in the repository.
|
|
42
|
+
*
|
|
43
|
+
* `twentyhq/twenty`, an open-source CRM, ships an example real-estate app whose
|
|
44
|
+
* `roles/seller.role.ts` matched here. Scanning `files.all` meant any path anywhere
|
|
45
|
+
* counted, including bundled demos and vendored code.
|
|
46
|
+
*/
|
|
47
|
+
const roleFiles = scope.filter((file) => /(seller|merchant|storefront)/i.test(file)).slice(0, 20);
|
|
42
48
|
for (const file of roleFiles)
|
|
43
49
|
evidence.push({ type: 'file', value: file });
|
|
44
50
|
// Both sides must appear for a confident signal: a supply-side vocabulary on its own
|
|
45
51
|
// also fits a plain catalogue or CMS, so it downgrades to partial rather than present.
|
|
46
|
-
|
|
47
|
-
|
|
52
|
+
/**
|
|
53
|
+
* Two sides are not one sentence.
|
|
54
|
+
*
|
|
55
|
+
* `documenso`, an open-source document-signing product, came out a marketplace with
|
|
56
|
+
* high confidence on a single line: a comment in a field-detection schema listing
|
|
57
|
+
* `"Tenant", "Landlord", "Buyer", "Seller"` as examples of labels found in the
|
|
58
|
+
* documents its users sign. One line matched both sides, and one line is one
|
|
59
|
+
* mention.
|
|
60
|
+
*/
|
|
61
|
+
const vocabularyFiles = new Set([...sellerHits, ...buyerHits].map((hit) => hit.file));
|
|
62
|
+
const bothSides = sellerHits.length > 0 && buyerHits.length > 0 && vocabularyFiles.size >= 2;
|
|
63
|
+
// A file name on its own is not a role either. Twenty had four such names and no
|
|
64
|
+
// buyer or seller vocabulary anywhere in its code, and came out a marketplace:
|
|
65
|
+
// naming a file is cheaper than building a two-sided product.
|
|
66
|
+
const oneSide = vocabularyFiles.size >= 2;
|
|
48
67
|
return {
|
|
49
68
|
key: 'marketplace.multiRole',
|
|
50
69
|
present: oneSide,
|
|
@@ -78,7 +97,18 @@ async function detectPayout(ctx) {
|
|
|
78
97
|
}
|
|
79
98
|
async function detectCommission(ctx) {
|
|
80
99
|
const evidence = [];
|
|
81
|
-
const hits = await (0, textSearch_1.searchInFiles)(ctx.root, ctx.files.source, [
|
|
100
|
+
const hits = await (0, textSearch_1.searchInFiles)(ctx.root, ctx.files.source, [
|
|
101
|
+
// "Commission" is also an institution. The privacy policy of an open-source CRM
|
|
102
|
+
// said "European Commission, relying on an adequacy decision" and was read as a
|
|
103
|
+
// platform taking a cut, sixteen times over.
|
|
104
|
+
/commission[_\s]?(rate|fee|percent|amount|bps)/i,
|
|
105
|
+
/(rate|fee|percent|amount)[_\s]?commission/i,
|
|
106
|
+
/\bcommission[A-Z]/,
|
|
107
|
+
/application_fee/i,
|
|
108
|
+
/\bplatform_?fee/i,
|
|
109
|
+
/\btake_?rate/i,
|
|
110
|
+
/\bservice_?fee/i,
|
|
111
|
+
], 20);
|
|
82
112
|
for (const hit of hits)
|
|
83
113
|
evidence.push({ type: 'snippet', value: hit.snippet, file: hit.file, line: hit.line });
|
|
84
114
|
return {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@produtype/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.1",
|
|
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": {
|