@produtype/core 1.26.1 → 1.27.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.
- package/dist/analyzer/detectMarketplace.js +105 -12
- package/dist/report/types.d.ts +7 -1
- package/package.json +1 -1
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.detectMarketplace = detectMarketplace;
|
|
4
4
|
const detectContext_1 = require("./detectContext");
|
|
5
5
|
const textSearch_1 = require("../utils/textSearch");
|
|
6
|
+
const absenceEvidence_1 = require("./absenceEvidence");
|
|
6
7
|
/**
|
|
7
8
|
* Marketplace-specific signals.
|
|
8
9
|
*
|
|
@@ -21,8 +22,51 @@ const textSearch_1 = require("../utils/textSearch");
|
|
|
21
22
|
// concept would actually be modelled, and a term only qualifies if its presence
|
|
22
23
|
// genuinely implies a supply side — which rules out build vocabulary (`vendor`),
|
|
23
24
|
// common verbs (`listing`), and anything that ships inside a payment SDK (`customer`).
|
|
24
|
-
|
|
25
|
-
|
|
25
|
+
/**
|
|
26
|
+
* A connected account is a seller, in the payment platform's own words.
|
|
27
|
+
*
|
|
28
|
+
* `seller`, `merchant`, `storefront` and `supplier` are four English words, and a
|
|
29
|
+
* mercato whose table is `venditori` has none of them. What it does have, if it pays
|
|
30
|
+
* anybody, is Stripe: `stripe.accounts.create({ type: 'express' })` creates a
|
|
31
|
+
* connected account, and a connected account is the supply side by definition — the
|
|
32
|
+
* platform is the other one. The same is true of `transfers.create` with a
|
|
33
|
+
* `destination`, which is money leaving the platform for somebody else's account.
|
|
34
|
+
*
|
|
35
|
+
* It is the anchor the payout capability beside this one already uses, and the reason
|
|
36
|
+
* an Italian marketplace was reported as having a payout but no seller.
|
|
37
|
+
*/
|
|
38
|
+
const CONNECTED_ACCOUNT = [
|
|
39
|
+
/\baccounts\.create\s*\(/,
|
|
40
|
+
/\bstripe\.accounts\b/i,
|
|
41
|
+
/['"`]account\.updated['"`]|['"`]account\.application\./,
|
|
42
|
+
/\bdestination_account\b|destination:\s*\w/,
|
|
43
|
+
/\bconnected_?account/i,
|
|
44
|
+
];
|
|
45
|
+
/**
|
|
46
|
+
* Nobody writes `seller` on its own.
|
|
47
|
+
*
|
|
48
|
+
* `\bseller\b` is a word boundary, and code is not prose: the supply side of a real
|
|
49
|
+
* marketplace is `onboardSeller`, `sellerId`, `db.sellers` and `seller_account`, and a
|
|
50
|
+
* word boundary matches none of the first three. A fixture written the way an English
|
|
51
|
+
* marketplace is actually written came out with zero seller signals and zero buyer
|
|
52
|
+
* signals — and then, since 1.27.0 reads that as "this repository's words are not
|
|
53
|
+
* mine", the commission and dispute findings would have been withdrawn from a
|
|
54
|
+
* repository that spells both sides on every line.
|
|
55
|
+
*
|
|
56
|
+
* A trailing lowercase letter is still excluded, so `sellerships` or `buyerish` do not
|
|
57
|
+
* qualify; `reseller` does, and a reseller is a supply side.
|
|
58
|
+
*
|
|
59
|
+
* The lookahead is why these are not written with the `i` flag. `/buyers?(?![a-z])/i`
|
|
60
|
+
* applies the flag to the lookahead too, so `[a-z]` matches the `I` of `buyerId` and
|
|
61
|
+
* the pattern rejects the one spelling it was widened to accept — a case-insensitive
|
|
62
|
+
* negative lookahead asserts the opposite of what it reads like.
|
|
63
|
+
*/
|
|
64
|
+
function identifierWord(word) {
|
|
65
|
+
const initial = word[0];
|
|
66
|
+
return new RegExp(`(?:[${initial.toLowerCase()}${initial.toUpperCase()}]${word.slice(1)}|${word.toUpperCase()})s?(?![a-z])`);
|
|
67
|
+
}
|
|
68
|
+
const SELLER_TERMS = ['seller', 'merchant', 'storefront', 'supplier'].map(identifierWord);
|
|
69
|
+
const BUYER_TERMS = ['buyer', 'purchaser', 'shopper'].map(identifierWord);
|
|
26
70
|
/** Files where a domain concept is declared rather than merely mentioned. */
|
|
27
71
|
const DOMAIN_FILE = /(model|schema|entity|migration|prisma|domain|route|controller)/i;
|
|
28
72
|
function domainFiles(ctx) {
|
|
@@ -33,7 +77,20 @@ async function detectMultiRole(ctx) {
|
|
|
33
77
|
const scope = domainFiles(ctx);
|
|
34
78
|
const sellerHits = await (0, textSearch_1.searchInFiles)(ctx.root, scope, SELLER_TERMS, 20);
|
|
35
79
|
const buyerHits = await (0, textSearch_1.searchInFiles)(ctx.root, scope, BUYER_TERMS, 20);
|
|
36
|
-
|
|
80
|
+
/**
|
|
81
|
+
* The connected-account search is not scoped and not restricted to files whose name
|
|
82
|
+
* is an English word, because it is not searching for a word.
|
|
83
|
+
*
|
|
84
|
+
* `DOMAIN_FILE` keeps the vocabulary terms away from `vite.config.js` — a reasonable
|
|
85
|
+
* precaution for `vendor` and `listing`, and useless for a repository whose files
|
|
86
|
+
* are called `venditori.js` and `ordini.js`. It is the same filter twice over: an
|
|
87
|
+
* English word inside a file whose path is an English word.
|
|
88
|
+
*
|
|
89
|
+
* `stripe.accounts.create(` is neither. Nothing calls it by accident, so it can be
|
|
90
|
+
* looked for where the code actually is.
|
|
91
|
+
*/
|
|
92
|
+
const connectedHits = await (0, textSearch_1.searchInFiles)(ctx.root, ctx.files.source, CONNECTED_ACCOUNT, 20);
|
|
93
|
+
for (const hit of [...sellerHits, ...buyerHits, ...connectedHits]) {
|
|
37
94
|
evidence.push({ type: 'snippet', value: hit.snippet, file: hit.file, line: hit.line });
|
|
38
95
|
}
|
|
39
96
|
/**
|
|
@@ -63,14 +120,36 @@ async function detectMultiRole(ctx) {
|
|
|
63
120
|
// A file name on its own is not a role either. Twenty had four such names and no
|
|
64
121
|
// buyer or seller vocabulary anywhere in its code, and came out a marketplace:
|
|
65
122
|
// naming a file is cheaper than building a two-sided product.
|
|
66
|
-
|
|
123
|
+
//
|
|
124
|
+
// The two-file rule guards against one sentence naming both sides — a comment in
|
|
125
|
+
// documenso listing "Tenant", "Landlord", "Buyer", "Seller" as example labels. A
|
|
126
|
+
// connected account is not a sentence, so it does not need the guard: one call to
|
|
127
|
+
// `accounts.create` is a supply side whether or not the repository also spells one.
|
|
128
|
+
const oneSide = vocabularyFiles.size >= 2 || connectedHits.length > 0;
|
|
67
129
|
return {
|
|
68
130
|
key: 'marketplace.multiRole',
|
|
69
131
|
present: oneSide,
|
|
70
132
|
complete: bothSides,
|
|
71
133
|
evidence,
|
|
72
134
|
details: {
|
|
73
|
-
sellerSignals: sellerHits.length,
|
|
135
|
+
sellerSignals: sellerHits.length + connectedHits.length,
|
|
136
|
+
connectedAccountSignals: connectedHits.length,
|
|
137
|
+
/**
|
|
138
|
+
* This repository runs a marketplace in words this tool does not have.
|
|
139
|
+
*
|
|
140
|
+
* The payment platform named the supply side and the repository never did: a
|
|
141
|
+
* connected account is created, and `seller`, `merchant`, `buyer` and
|
|
142
|
+
* `shopper` appear nowhere. That combination is not a marketplace missing its
|
|
143
|
+
* vocabulary — it is a marketplace whose vocabulary is `venditori` and
|
|
144
|
+
* `acquirenti`, or any of the other several thousand languages this tool reads
|
|
145
|
+
* none of.
|
|
146
|
+
*
|
|
147
|
+
* Commission and dispute are searched for in English and in provider API names.
|
|
148
|
+
* Where the provider name is absent — a cut computed in arithmetic, a table
|
|
149
|
+
* called `contestazioni` — the search has nothing left to look with, and the
|
|
150
|
+
* finding downstream becomes "not assessed" rather than "not there".
|
|
151
|
+
*/
|
|
152
|
+
vocabularyUnread: connectedHits.length > 0 && sellerHits.length === 0 && buyerHits.length === 0,
|
|
74
153
|
buyerSignals: buyerHits.length,
|
|
75
154
|
roleFiles: roleFiles.length,
|
|
76
155
|
},
|
|
@@ -95,7 +174,17 @@ async function detectPayout(ctx) {
|
|
|
95
174
|
},
|
|
96
175
|
};
|
|
97
176
|
}
|
|
98
|
-
|
|
177
|
+
/**
|
|
178
|
+
* Nothing was found, and the search had nothing left to look with.
|
|
179
|
+
*
|
|
180
|
+
* Commission and dispute are searched for in English and in provider API names. Where
|
|
181
|
+
* the provider name is absent — a cut computed as arithmetic, a table called
|
|
182
|
+
* `contestazioni` — only the English half remains, and a repository that never spelled
|
|
183
|
+
* `seller` or `buyer` was never going to answer it. `unanswered` turns the finding into
|
|
184
|
+
* "not assessed" downstream, which is the one direction blindness is allowed to move a
|
|
185
|
+
* verdict.
|
|
186
|
+
*/
|
|
187
|
+
async function detectCommission(ctx, vocabularyUnread) {
|
|
99
188
|
const evidence = [];
|
|
100
189
|
const hits = await (0, textSearch_1.searchInFiles)(ctx.root, ctx.files.source, [
|
|
101
190
|
// "Commission" is also an institution. The privacy policy of an open-source CRM
|
|
@@ -114,11 +203,12 @@ async function detectCommission(ctx) {
|
|
|
114
203
|
return {
|
|
115
204
|
key: 'marketplace.commission',
|
|
116
205
|
present: hits.length > 0,
|
|
117
|
-
|
|
206
|
+
unanswered: hits.length === 0 && vocabularyUnread,
|
|
207
|
+
evidence: (0, absenceEvidence_1.evidenceOrSearch)(evidence, 'a cut taken on the way through', ['commission_rate', 'commissionRate', 'application_fee', 'platform_fee', 'take_rate', 'service_fee']),
|
|
118
208
|
details: { commissionSignals: hits.length },
|
|
119
209
|
};
|
|
120
210
|
}
|
|
121
|
-
async function detectDispute(ctx) {
|
|
211
|
+
async function detectDispute(ctx, vocabularyUnread) {
|
|
122
212
|
const evidence = [];
|
|
123
213
|
// `refund` alone is not evidence of dispute handling — it appears in any payment
|
|
124
214
|
// integration. `arbitration` was dropped after it matched a glossary entry in a
|
|
@@ -137,8 +227,9 @@ async function detectDispute(ctx) {
|
|
|
137
227
|
return {
|
|
138
228
|
key: 'marketplace.dispute',
|
|
139
229
|
present: strong || weakHits.length > 0,
|
|
230
|
+
unanswered: !strong && weakHits.length === 0 && vocabularyUnread,
|
|
140
231
|
complete: strong,
|
|
141
|
-
evidence,
|
|
232
|
+
evidence: (0, absenceEvidence_1.evidenceOrSearch)(evidence, 'a way to unwind a transaction', ['disputes.create', 'chargeback', 'escrow', 'refund', 'dispute']),
|
|
142
233
|
details: {
|
|
143
234
|
disputeSignals: strongHits.length,
|
|
144
235
|
refundOnlySignals: weakHits.length,
|
|
@@ -147,10 +238,12 @@ async function detectDispute(ctx) {
|
|
|
147
238
|
};
|
|
148
239
|
}
|
|
149
240
|
async function detectMarketplace(ctx) {
|
|
241
|
+
const multiRole = await detectMultiRole(ctx);
|
|
242
|
+
const vocabularyUnread = multiRole.details?.vocabularyUnread === true;
|
|
150
243
|
return Promise.all([
|
|
151
|
-
|
|
244
|
+
Promise.resolve(multiRole),
|
|
152
245
|
detectPayout(ctx),
|
|
153
|
-
detectCommission(ctx),
|
|
154
|
-
detectDispute(ctx),
|
|
246
|
+
detectCommission(ctx, vocabularyUnread),
|
|
247
|
+
detectDispute(ctx, vocabularyUnread),
|
|
155
248
|
]);
|
|
156
249
|
}
|
package/dist/report/types.d.ts
CHANGED
|
@@ -85,8 +85,14 @@ export interface ReportDiagnostics {
|
|
|
85
85
|
* Withdrawing the claim silently would be its own failure: the reader of a project
|
|
86
86
|
* whose routes are `/accedi` and `/recupero-password` would see four questions
|
|
87
87
|
* simply absent, with nothing saying why. This is what the report says instead.
|
|
88
|
+
*
|
|
89
|
+
* Optional because this interface is a published type and somebody else builds
|
|
90
|
+
* values of it. Adding a required field to it is a breaking change however small
|
|
91
|
+
* the field is, and 1.26.1 shipped one in a patch release: produtype.dev stopped
|
|
92
|
+
* compiling on the fixture it builds for its own tests. `buildReport` always sets
|
|
93
|
+
* it; the question mark is for everyone who does not.
|
|
88
94
|
*/
|
|
89
|
-
routeNamesUnread
|
|
95
|
+
routeNamesUnread?: boolean;
|
|
90
96
|
/**
|
|
91
97
|
* Whether the optional TypeScript compiler was loaded for this reading.
|
|
92
98
|
*
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@produtype/core",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.27.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": {
|