@extenshi/cli 0.11.0 → 0.13.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/cli.js +4 -0
- package/dist/cli.js.map +1 -1
- package/dist/cws-policies.d.ts +65 -0
- package/dist/cws-policies.d.ts.map +1 -0
- package/dist/cws-policies.js +113 -0
- package/dist/cws-policies.js.map +1 -0
- package/dist/listing-checks.d.ts +91 -0
- package/dist/listing-checks.d.ts.map +1 -0
- package/dist/listing-checks.js +547 -0
- package/dist/listing-checks.js.map +1 -0
- package/dist/listing.d.ts +91 -0
- package/dist/listing.d.ts.map +1 -0
- package/dist/listing.js +246 -0
- package/dist/listing.js.map +1 -0
- package/dist/render.d.ts +48 -0
- package/dist/render.d.ts.map +1 -1
- package/dist/render.js +113 -0
- package/dist/render.js.map +1 -1
- package/dist/report.d.ts.map +1 -1
- package/dist/report.js +87 -1
- package/dist/report.js.map +1 -1
- package/dist/review-checks.d.ts +26 -1
- package/dist/review-checks.d.ts.map +1 -1
- package/dist/review-checks.js +157 -3
- package/dist/review-checks.js.map +1 -1
- package/dist/review-render.js +4 -0
- package/dist/review-render.js.map +1 -1
- package/dist/review-risk.d.ts +6 -0
- package/dist/review-risk.d.ts.map +1 -1
- package/dist/review-risk.js +65 -1
- package/dist/review-risk.js.map +1 -1
- package/dist/telemetry.d.ts +11 -3
- package/dist/telemetry.d.ts.map +1 -1
- package/dist/telemetry.js +59 -6
- package/dist/telemetry.js.map +1 -1
- package/package.json +1 -1
- package/scripts/postinstall.cjs +21 -3
|
@@ -0,0 +1,547 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* review-risk — deterministic store-listing checks.
|
|
3
|
+
*
|
|
4
|
+
* Moderation reads the listing as closely as the code, and two of its rules are
|
|
5
|
+
* written concretely enough to check without a model:
|
|
6
|
+
*
|
|
7
|
+
* Listing Requirements — a blank description is rejected outright; keyword
|
|
8
|
+
* spam is defined by example as "lists of sites/brands/keywords without
|
|
9
|
+
* substantial added value", "lists of regional locations", and "unnatural
|
|
10
|
+
* repetition of the same keyword more than 5 times"; unattributed user
|
|
11
|
+
* testimonials are not allowed.
|
|
12
|
+
* Misleading or Unexpected Behavior — the description must state the actual
|
|
13
|
+
* functionality, and must not claim functionality that is not possible to
|
|
14
|
+
* implement (their example: "Who has viewed your social media account").
|
|
15
|
+
*
|
|
16
|
+
* Every rule here lands as SLOW (advisory), never REJECTED — including the ones
|
|
17
|
+
* whose policy text says "will be rejected". A false block costs the developer
|
|
18
|
+
* a shipped release, a false advisory costs them a paragraph of reading, and
|
|
19
|
+
* these rules have not been measured against the corpus yet (roadmap iteration
|
|
20
|
+
* 2 T9). They get promoted by data, not by how confident the wording sounds.
|
|
21
|
+
*
|
|
22
|
+
* ALL LOCALES are checked, not just the default one: moderation reads every
|
|
23
|
+
* locale you publish, and a clean English listing over a stuffed Russian one is
|
|
24
|
+
* exactly the case that gets removed after it ships.
|
|
25
|
+
*
|
|
26
|
+
* This module is PURE (no I/O) so the rules are unit-testable against fixtures.
|
|
27
|
+
*/
|
|
28
|
+
/**
|
|
29
|
+
* Version of the dictionaries and thresholds below.
|
|
30
|
+
*
|
|
31
|
+
* Carried in every finding's evidence so corpus validation can scope its
|
|
32
|
+
* false-positive rate to ONE version of the rules. A rate averaged over two
|
|
33
|
+
* dictionaries measures nothing — the same listing is judged by different words
|
|
34
|
+
* before and after a bump. Same discipline as `ANALYSIS_VERSION` on the
|
|
35
|
+
* permission-usage rows. Bump on any change to a dictionary or a threshold.
|
|
36
|
+
*/
|
|
37
|
+
export const LISTING_DICTIONARY_VERSION = '1';
|
|
38
|
+
const DOC_LISTING = 'https://developer.chrome.com/docs/webstore/program-policies/listing-requirements';
|
|
39
|
+
const DOC_UNEXPECTED = 'https://developer.chrome.com/docs/webstore/program-policies/unexpected-behavior';
|
|
40
|
+
/**
|
|
41
|
+
* Google's own threshold, quoted: "Unnatural repetition of the same keyword
|
|
42
|
+
* more than 5 times". Strictly more than 5, so 6 fires and 5 does not.
|
|
43
|
+
*/
|
|
44
|
+
const MAX_KEYWORD_REPEATS = 5;
|
|
45
|
+
/** A word shorter than this is noise even when repeated (an, the, для). */
|
|
46
|
+
const MIN_KEYWORD_LENGTH = 4;
|
|
47
|
+
/**
|
|
48
|
+
* Function words that repeat naturally in prose. Filtering them is what keeps
|
|
49
|
+
* the repetition rule pointed at keyword spam rather than at English and
|
|
50
|
+
* Russian grammar. EN + RU because both appear in listings we check.
|
|
51
|
+
*/
|
|
52
|
+
const FUNCTION_WORDS = new Set([
|
|
53
|
+
// English
|
|
54
|
+
'that',
|
|
55
|
+
'this',
|
|
56
|
+
'with',
|
|
57
|
+
'from',
|
|
58
|
+
'your',
|
|
59
|
+
'you',
|
|
60
|
+
'the',
|
|
61
|
+
'and',
|
|
62
|
+
'for',
|
|
63
|
+
'are',
|
|
64
|
+
'can',
|
|
65
|
+
'will',
|
|
66
|
+
'have',
|
|
67
|
+
'has',
|
|
68
|
+
'not',
|
|
69
|
+
'but',
|
|
70
|
+
'all',
|
|
71
|
+
'any',
|
|
72
|
+
'when',
|
|
73
|
+
'what',
|
|
74
|
+
'which',
|
|
75
|
+
'their',
|
|
76
|
+
'them',
|
|
77
|
+
'they',
|
|
78
|
+
'into',
|
|
79
|
+
'over',
|
|
80
|
+
'only',
|
|
81
|
+
'also',
|
|
82
|
+
'just',
|
|
83
|
+
'more',
|
|
84
|
+
'most',
|
|
85
|
+
'than',
|
|
86
|
+
'then',
|
|
87
|
+
'there',
|
|
88
|
+
'these',
|
|
89
|
+
'those',
|
|
90
|
+
'other',
|
|
91
|
+
'each',
|
|
92
|
+
'every',
|
|
93
|
+
'some',
|
|
94
|
+
'such',
|
|
95
|
+
'been',
|
|
96
|
+
'was',
|
|
97
|
+
'were',
|
|
98
|
+
'its',
|
|
99
|
+
'our',
|
|
100
|
+
'about',
|
|
101
|
+
'after',
|
|
102
|
+
'before',
|
|
103
|
+
'without',
|
|
104
|
+
'between',
|
|
105
|
+
'through',
|
|
106
|
+
'while',
|
|
107
|
+
'where',
|
|
108
|
+
'would',
|
|
109
|
+
'could',
|
|
110
|
+
'should',
|
|
111
|
+
'want',
|
|
112
|
+
'need',
|
|
113
|
+
'like',
|
|
114
|
+
'make',
|
|
115
|
+
'made',
|
|
116
|
+
'does',
|
|
117
|
+
'doing',
|
|
118
|
+
'done',
|
|
119
|
+
// Russian
|
|
120
|
+
'для',
|
|
121
|
+
'что',
|
|
122
|
+
'это',
|
|
123
|
+
'как',
|
|
124
|
+
'или',
|
|
125
|
+
'все',
|
|
126
|
+
'вам',
|
|
127
|
+
'вас',
|
|
128
|
+
'ваш',
|
|
129
|
+
'ваши',
|
|
130
|
+
'вашего',
|
|
131
|
+
'если',
|
|
132
|
+
'может',
|
|
133
|
+
'можно',
|
|
134
|
+
'быть',
|
|
135
|
+
'есть',
|
|
136
|
+
'при',
|
|
137
|
+
'так',
|
|
138
|
+
'уже',
|
|
139
|
+
'его',
|
|
140
|
+
'ещё',
|
|
141
|
+
'еще',
|
|
142
|
+
'они',
|
|
143
|
+
'том',
|
|
144
|
+
'этот',
|
|
145
|
+
'этой',
|
|
146
|
+
'этом',
|
|
147
|
+
'него',
|
|
148
|
+
'нужно',
|
|
149
|
+
'после',
|
|
150
|
+
'также',
|
|
151
|
+
'чтобы',
|
|
152
|
+
'один',
|
|
153
|
+
'одна',
|
|
154
|
+
]);
|
|
155
|
+
/**
|
|
156
|
+
* Claims the policy names as not implementable, plus the near-identical
|
|
157
|
+
* variants of the same promises. Every entry is a claim about OTHER people's
|
|
158
|
+
* private activity or about defeating a platform's access controls — the class
|
|
159
|
+
* the policy calls out, not merely a word we dislike.
|
|
160
|
+
*
|
|
161
|
+
* Matched case-insensitively against the whole field. Multi-word phrases only:
|
|
162
|
+
* single words like "hack" appear legitimately ("hack your productivity") and
|
|
163
|
+
* cost more in false positives than they return.
|
|
164
|
+
*/
|
|
165
|
+
const IMPOSSIBLE_CLAIM_PATTERNS = [
|
|
166
|
+
{ re: /\bwho (?:has )?view(?:ed|s)? your\b/i, what: 'who viewed your profile' },
|
|
167
|
+
{ re: /\bwho (?:has )?visit(?:ed|s)? your\b/i, what: 'who visited your profile' },
|
|
168
|
+
{ re: /\bwho (?:has )?(?:un)?follow(?:ed|s)? you\b/i, what: 'who unfollowed you' },
|
|
169
|
+
{ re: /\bprofile (?:viewers|visitors|stalkers)\b/i, what: 'profile viewers' },
|
|
170
|
+
// One optional word between the adjective and the noun so a brand name
|
|
171
|
+
// ("private Instagram accounts") does not slip the phrase past the rule.
|
|
172
|
+
{
|
|
173
|
+
re: /\b(?:see|view|read) (?:private|hidden|secret) (?:\p{L}+ )?(?:profile|account|photo|message)/iu,
|
|
174
|
+
what: 'see private content',
|
|
175
|
+
},
|
|
176
|
+
{ re: /\b(?:read|recover|see) deleted (?:message|post|comment)/i, what: 'read deleted messages' },
|
|
177
|
+
{ re: /\b(?:free|unlimited) (?:premium|pro subscription)\b/i, what: 'free premium access' },
|
|
178
|
+
{ re: /\bbypass (?:paywall|subscription|login|password)/i, what: 'bypass a paywall or login' },
|
|
179
|
+
{ re: /\b(?:spy|snoop) on\b/i, what: 'spy on someone' },
|
|
180
|
+
// No \b around the Cyrillic phrases: \b is defined on ASCII word characters,
|
|
181
|
+
// so it never matches at a space→к boundary and would silently disable the
|
|
182
|
+
// rule for every Russian listing. The phrase is specific enough unanchored.
|
|
183
|
+
{ re: /кто (?:смотрел|заходил|посещал|просматривал)/i, what: 'кто смотрел ваш профиль' },
|
|
184
|
+
];
|
|
185
|
+
/**
|
|
186
|
+
* Ranking and guarantee claims a reviewer cannot verify. Deliberately narrow:
|
|
187
|
+
* "best" on its own is ordinary marketing and firing on it would bury the rule
|
|
188
|
+
* in noise. These are the fixed phrasings that assert a checkable-sounding fact
|
|
189
|
+
* with nothing behind it.
|
|
190
|
+
*/
|
|
191
|
+
const UNVERIFIABLE_CLAIM_PATTERNS = [
|
|
192
|
+
{ re: /#\s?1\b|\bnumber (?:one|1)\b/i, what: 'a #1 ranking claim' },
|
|
193
|
+
{ re: /\b100\s?% (?:safe|secure|guaranteed|accurate|free of)\b/i, what: 'a 100% guarantee' },
|
|
194
|
+
{ re: /\b(?:world'?s|the) best extension\b/i, what: '"the best extension"' },
|
|
195
|
+
{ re: /\bguaranteed? (?:results|to work)\b/i, what: 'a guaranteed-results claim' },
|
|
196
|
+
// Unanchored for the same reason as the Cyrillic claim pattern above.
|
|
197
|
+
{ re: /лучш(?:ее|ий|ая) расширение/i, what: '«лучшее расширение»' },
|
|
198
|
+
];
|
|
199
|
+
/** Three or more rating glyphs in a row — a pasted testimonial or ratings block. */
|
|
200
|
+
const TESTIMONIAL_GLYPH_RE = /[★⭐✩✪✫✬✭✮✯🌟]{3,}/u;
|
|
201
|
+
/** Separators a keyword list is built out of. */
|
|
202
|
+
const LIST_SEPARATOR_RE = /[,|/·•;]/;
|
|
203
|
+
/** A short description below this is "non-descriptive" metadata by any reading. */
|
|
204
|
+
const MIN_SHORT_DESCRIPTION_LENGTH = 25;
|
|
205
|
+
/** Beyond this a name has stopped being a name (the store's own cap is 75). */
|
|
206
|
+
const NAME_STUFFING_LENGTH = 45;
|
|
207
|
+
/** Comma-ish segments in a name past which it reads as a keyword list. */
|
|
208
|
+
const NAME_MAX_SEGMENTS = 3;
|
|
209
|
+
/** Segments in a run before it counts as a keyword list rather than a sentence. */
|
|
210
|
+
const MIN_LIST_SEGMENTS = 8;
|
|
211
|
+
/** Words per segment above which the run is prose, not a list. */
|
|
212
|
+
const MAX_WORDS_PER_LIST_SEGMENT = 3;
|
|
213
|
+
/** The listing fields a rule can point at, in the order a reviewer reads them. */
|
|
214
|
+
const FIELDS = [
|
|
215
|
+
{ key: 'name', label: 'name' },
|
|
216
|
+
{ key: 'shortDescription', label: 'short description' },
|
|
217
|
+
{ key: 'description', label: 'description' },
|
|
218
|
+
];
|
|
219
|
+
function fieldValue(locale, key) {
|
|
220
|
+
return locale[key];
|
|
221
|
+
}
|
|
222
|
+
/** `[locale] field` — the prefix every piece of evidence carries. */
|
|
223
|
+
function where(locale, label) {
|
|
224
|
+
return `[${locale.locale}] ${label}`;
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Every finding carries the dictionary version so a corpus run can group by it.
|
|
228
|
+
* Appended to evidence rather than added as a field: `ReviewFinding` is a
|
|
229
|
+
* published payload shape, and an extra key would need every consumer to know
|
|
230
|
+
* about it before it could be read.
|
|
231
|
+
*/
|
|
232
|
+
function stamp(evidence) {
|
|
233
|
+
return `${evidence} (listing rules v${LISTING_DICTIONARY_VERSION})`;
|
|
234
|
+
}
|
|
235
|
+
function advisory(f) {
|
|
236
|
+
return { verdict: 'SLOW', ...f };
|
|
237
|
+
}
|
|
238
|
+
// ── Check: empty / non-descriptive fields ────────────────────────────────────
|
|
239
|
+
/**
|
|
240
|
+
* "If your product has a blank description field … it will be rejected."
|
|
241
|
+
*
|
|
242
|
+
* A missing field is only reported where the SOURCE was in a position to carry
|
|
243
|
+
* it, otherwise the rule reports on our own input rather than on the
|
|
244
|
+
* developer's listing:
|
|
245
|
+
*
|
|
246
|
+
* artifact — `manifest.json` always has a `name` and a `description` slot, so
|
|
247
|
+
* an empty one is a real hole. (The full description is never in
|
|
248
|
+
* the package, and is not checked for absence at all.)
|
|
249
|
+
* draft — a `--listing` file may legitimately carry only the field the
|
|
250
|
+
* developer wanted checked. So absence is only an accusation when
|
|
251
|
+
* ANOTHER locale in the same file has that field — that is
|
|
252
|
+
* incomplete localization, which the store does read.
|
|
253
|
+
*/
|
|
254
|
+
export function checkListingEmptyFields(listing) {
|
|
255
|
+
const out = [];
|
|
256
|
+
const localizedElsewhere = (key, self) => listing.locales.some((l) => l !== self && Boolean(fieldValue(l, key)?.trim()));
|
|
257
|
+
// The manifest always carries both slots; a draft only proves a field is
|
|
258
|
+
// expected when a sibling locale supplies it.
|
|
259
|
+
const expected = (key, self) => listing.source === 'artifact' || localizedElsewhere(key, self);
|
|
260
|
+
for (const locale of listing.locales) {
|
|
261
|
+
const name = locale.name?.trim();
|
|
262
|
+
if (!name && expected('name', locale)) {
|
|
263
|
+
out.push(advisory({
|
|
264
|
+
check: 'listing-empty-field',
|
|
265
|
+
title: 'The listing has no name in this locale',
|
|
266
|
+
detail: 'A locale that resolves to an empty name publishes with a blank title. The store rejects listings ' +
|
|
267
|
+
'with missing or non-descriptive metadata, and this one is missing entirely.',
|
|
268
|
+
evidence: stamp(where(locale, 'name is empty')),
|
|
269
|
+
recommendation: 'Add the name for this locale to its _locales/<lang>/messages.json, or drop the locale if you do not ' +
|
|
270
|
+
'translate the listing into it.',
|
|
271
|
+
citation: `Chrome Web Store: Listing Requirements — ${DOC_LISTING}`,
|
|
272
|
+
}));
|
|
273
|
+
}
|
|
274
|
+
const short = locale.shortDescription?.trim();
|
|
275
|
+
if (!short && expected('shortDescription', locale)) {
|
|
276
|
+
out.push(advisory({
|
|
277
|
+
check: 'listing-empty-field',
|
|
278
|
+
title: 'The listing has no short description in this locale',
|
|
279
|
+
detail: 'A blank description field is called out in Listing Requirements as an outright rejection. The short ' +
|
|
280
|
+
'description is the manifest `description` field, and it is what a user reads before installing.',
|
|
281
|
+
evidence: stamp(where(locale, 'short description is empty')),
|
|
282
|
+
recommendation: 'Write a one-line description that states what the extension does, and localize it for every locale you ship.',
|
|
283
|
+
citation: `Chrome Web Store: Listing Requirements — ${DOC_LISTING}`,
|
|
284
|
+
}));
|
|
285
|
+
}
|
|
286
|
+
else if (short && short.length < MIN_SHORT_DESCRIPTION_LENGTH) {
|
|
287
|
+
out.push(advisory({
|
|
288
|
+
check: 'listing-empty-field',
|
|
289
|
+
title: 'The short description is too short to describe the extension',
|
|
290
|
+
detail: `At ${short.length} characters this does not state what the extension does. "Non-descriptive metadata" ` +
|
|
291
|
+
'is listed alongside missing metadata as grounds for rejection, and a reviewer who cannot tell what ' +
|
|
292
|
+
'the extension is for has no reason to approve it.',
|
|
293
|
+
evidence: stamp(`${where(locale, 'short description')}: "${short}"`),
|
|
294
|
+
recommendation: 'State the actual functionality in one sentence — what it does and to what. The description must let a ' +
|
|
295
|
+
'user understand the product before installing it.',
|
|
296
|
+
citation: `Chrome Web Store: Listing Requirements — ${DOC_LISTING}`,
|
|
297
|
+
}));
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
return out;
|
|
301
|
+
}
|
|
302
|
+
// ── Check: keyword repetition ────────────────────────────────────────────────
|
|
303
|
+
/** Lowercased word tokens, Unicode-aware so Cyrillic and accented text tokenize. */
|
|
304
|
+
function words(text) {
|
|
305
|
+
return text.toLowerCase().match(/[\p{L}\p{N}]+/gu) ?? [];
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* "Unnatural repetition of the same keyword more than 5 times."
|
|
309
|
+
*
|
|
310
|
+
* Counts content words only. Reports the worst offender per field rather than
|
|
311
|
+
* every word over the line: a stuffed description repeats a whole cluster of
|
|
312
|
+
* terms, and listing all of them turns one problem into fifteen findings.
|
|
313
|
+
*/
|
|
314
|
+
export function checkKeywordRepetition(listing) {
|
|
315
|
+
const out = [];
|
|
316
|
+
for (const locale of listing.locales) {
|
|
317
|
+
for (const field of FIELDS) {
|
|
318
|
+
const value = fieldValue(locale, field.key);
|
|
319
|
+
if (!value)
|
|
320
|
+
continue;
|
|
321
|
+
const counts = new Map();
|
|
322
|
+
for (const w of words(value)) {
|
|
323
|
+
if (w.length < MIN_KEYWORD_LENGTH || FUNCTION_WORDS.has(w))
|
|
324
|
+
continue;
|
|
325
|
+
counts.set(w, (counts.get(w) ?? 0) + 1);
|
|
326
|
+
}
|
|
327
|
+
const over = [...counts.entries()]
|
|
328
|
+
.filter(([, n]) => n > MAX_KEYWORD_REPEATS)
|
|
329
|
+
.sort((a, b) => b[1] - a[1]);
|
|
330
|
+
if (over.length === 0)
|
|
331
|
+
continue;
|
|
332
|
+
const [worstWord, worstCount] = over[0];
|
|
333
|
+
const others = over.slice(1, 5).map(([w, n]) => `${w} ×${n}`);
|
|
334
|
+
out.push(advisory({
|
|
335
|
+
check: 'listing-keyword-repetition',
|
|
336
|
+
title: `"${worstWord}" is repeated ${worstCount} times in the ${field.label}`,
|
|
337
|
+
detail: 'Listing Requirements defines keyword spam by example, and one of the examples is "unnatural repetition ' +
|
|
338
|
+
`of the same keyword more than ${MAX_KEYWORD_REPEATS} times". Repetition at this level reads as ranking ` +
|
|
339
|
+
'manipulation rather than as description, and it is enforced after publishing as well as before.',
|
|
340
|
+
evidence: stamp(`${where(locale, field.label)}: ${worstWord} ×${worstCount}` +
|
|
341
|
+
(others.length ? `, also ${others.join(', ')}` : '')),
|
|
342
|
+
recommendation: 'Say it once and spend the space on what the extension actually does. If the term genuinely belongs in ' +
|
|
343
|
+
'several sentences, vary the phrasing rather than repeating the exact keyword.',
|
|
344
|
+
citation: `Chrome Web Store: Listing Requirements — ${DOC_LISTING}`,
|
|
345
|
+
}));
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
return out;
|
|
349
|
+
}
|
|
350
|
+
// ── Check: keyword / brand / location lists ──────────────────────────────────
|
|
351
|
+
/**
|
|
352
|
+
* "Lists of sites/brands/keywords without substantial added value" and "lists
|
|
353
|
+
* of regional locations".
|
|
354
|
+
*
|
|
355
|
+
* A run of many short separator-delimited segments is what those look like
|
|
356
|
+
* structurally, and it is the part that can be measured without judging whether
|
|
357
|
+
* the value added is "substantial".
|
|
358
|
+
*/
|
|
359
|
+
export function checkKeywordList(listing) {
|
|
360
|
+
const out = [];
|
|
361
|
+
for (const locale of listing.locales) {
|
|
362
|
+
for (const field of FIELDS) {
|
|
363
|
+
const value = fieldValue(locale, field.key);
|
|
364
|
+
if (!value)
|
|
365
|
+
continue;
|
|
366
|
+
// Longest run of consecutive short segments anywhere in the field: a
|
|
367
|
+
// stuffed listing is usually one prose paragraph followed by a dump.
|
|
368
|
+
const segments = value.split(LIST_SEPARATOR_RE);
|
|
369
|
+
let best = 0;
|
|
370
|
+
let bestStart = 0;
|
|
371
|
+
let run = 0;
|
|
372
|
+
let runStart = 0;
|
|
373
|
+
for (let i = 0; i < segments.length; i++) {
|
|
374
|
+
const wordCount = words(segments[i]).length;
|
|
375
|
+
const isListItem = wordCount > 0 && wordCount <= MAX_WORDS_PER_LIST_SEGMENT;
|
|
376
|
+
if (isListItem) {
|
|
377
|
+
if (run === 0)
|
|
378
|
+
runStart = i;
|
|
379
|
+
run++;
|
|
380
|
+
if (run > best) {
|
|
381
|
+
best = run;
|
|
382
|
+
bestStart = runStart;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
else {
|
|
386
|
+
run = 0;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
if (best < MIN_LIST_SEGMENTS)
|
|
390
|
+
continue;
|
|
391
|
+
const sample = segments
|
|
392
|
+
.slice(bestStart, bestStart + 6)
|
|
393
|
+
.map((s) => s.trim())
|
|
394
|
+
.filter(Boolean)
|
|
395
|
+
.join(', ');
|
|
396
|
+
out.push(advisory({
|
|
397
|
+
check: 'listing-keyword-list',
|
|
398
|
+
title: `The ${field.label} contains a ${best}-item keyword list`,
|
|
399
|
+
detail: 'Listing Requirements names "lists of sites/brands/keywords without substantial added value" and "lists ' +
|
|
400
|
+
'of regional locations" as keyword spam. A long run of comma-separated terms is that pattern, whether ' +
|
|
401
|
+
'the terms are site names, brands, or cities.',
|
|
402
|
+
evidence: stamp(`${where(locale, field.label)}: ${best} items — ${sample}…`),
|
|
403
|
+
recommendation: 'Replace the list with a sentence saying what the extension does with those things. If the supported ' +
|
|
404
|
+
'sites matter to users, put them on your own site and link to it.',
|
|
405
|
+
citation: `Chrome Web Store: Listing Requirements — ${DOC_LISTING}`,
|
|
406
|
+
}));
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
return out;
|
|
410
|
+
}
|
|
411
|
+
// ── Check: name stuffing ─────────────────────────────────────────────────────
|
|
412
|
+
/**
|
|
413
|
+
* A title that is a keyword string rather than a name. Requires BOTH excessive
|
|
414
|
+
* length and separator structure, because a long descriptive title is fine and
|
|
415
|
+
* a short hyphenated one ("Tab Manager - Pro") is fine; it is the combination
|
|
416
|
+
* that reads as a search-bait title.
|
|
417
|
+
*/
|
|
418
|
+
export function checkNameStuffing(listing) {
|
|
419
|
+
const out = [];
|
|
420
|
+
for (const locale of listing.locales) {
|
|
421
|
+
const name = locale.name?.trim();
|
|
422
|
+
if (!name)
|
|
423
|
+
continue;
|
|
424
|
+
const segments = name
|
|
425
|
+
.split(/[,|/·•;–—-]/)
|
|
426
|
+
.map((s) => s.trim())
|
|
427
|
+
.filter(Boolean);
|
|
428
|
+
const tooManySegments = segments.length > NAME_MAX_SEGMENTS;
|
|
429
|
+
const tooLong = name.length > NAME_STUFFING_LENGTH;
|
|
430
|
+
if (!(tooLong && tooManySegments))
|
|
431
|
+
continue;
|
|
432
|
+
out.push(advisory({
|
|
433
|
+
check: 'listing-name-stuffing',
|
|
434
|
+
title: 'The extension name reads as a keyword list, not a name',
|
|
435
|
+
detail: `The title is ${name.length} characters across ${segments.length} separator-delimited segments. Titles ` +
|
|
436
|
+
'shaped this way are treated as keyword spam and as "improperly formatted" metadata; the name is also the ' +
|
|
437
|
+
'first thing a reviewer sees.',
|
|
438
|
+
evidence: stamp(`${where(locale, 'name')}: "${name}"`),
|
|
439
|
+
recommendation: 'Use a short product name and move the keywords into a description that explains what the extension does.',
|
|
440
|
+
citation: `Chrome Web Store: Listing Requirements — ${DOC_LISTING}`,
|
|
441
|
+
}));
|
|
442
|
+
}
|
|
443
|
+
return out;
|
|
444
|
+
}
|
|
445
|
+
// ── Check: testimonial blocks ────────────────────────────────────────────────
|
|
446
|
+
/** "We don't allow unattributed or anonymous user testimonials in the product's description." */
|
|
447
|
+
export function checkTestimonialBlock(listing) {
|
|
448
|
+
const out = [];
|
|
449
|
+
for (const locale of listing.locales) {
|
|
450
|
+
for (const field of FIELDS) {
|
|
451
|
+
const value = fieldValue(locale, field.key);
|
|
452
|
+
if (!value)
|
|
453
|
+
continue;
|
|
454
|
+
const m = TESTIMONIAL_GLYPH_RE.exec(value);
|
|
455
|
+
if (!m)
|
|
456
|
+
continue;
|
|
457
|
+
out.push(advisory({
|
|
458
|
+
check: 'listing-testimonial-block',
|
|
459
|
+
title: `The ${field.label} contains a pasted ratings or testimonial block`,
|
|
460
|
+
detail: 'A run of star glyphs is nearly always a quoted review. Unattributed or anonymous user testimonials are ' +
|
|
461
|
+
'not allowed in the description, and pasted star ratings also read as an attempt to display a rating ' +
|
|
462
|
+
'the store did not compute.',
|
|
463
|
+
evidence: stamp(`${where(locale, field.label)}: "${m[0]}"`),
|
|
464
|
+
recommendation: 'Remove the pasted ratings. Real reviews already appear on the listing page; a quote in the description ' +
|
|
465
|
+
'needs a named, attributable source.',
|
|
466
|
+
citation: `Chrome Web Store: Listing Requirements — ${DOC_LISTING}`,
|
|
467
|
+
}));
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
return out;
|
|
471
|
+
}
|
|
472
|
+
// ── Check: impossible and unverifiable claims ────────────────────────────────
|
|
473
|
+
/**
|
|
474
|
+
* "Claimed functionalities that are not possible to implement (e.g. 'Who has
|
|
475
|
+
* viewed your social media account')."
|
|
476
|
+
*/
|
|
477
|
+
export function checkImpossibleClaims(listing) {
|
|
478
|
+
const out = [];
|
|
479
|
+
for (const locale of listing.locales) {
|
|
480
|
+
for (const field of FIELDS) {
|
|
481
|
+
const value = fieldValue(locale, field.key);
|
|
482
|
+
if (!value)
|
|
483
|
+
continue;
|
|
484
|
+
for (const pattern of IMPOSSIBLE_CLAIM_PATTERNS) {
|
|
485
|
+
const m = pattern.re.exec(value);
|
|
486
|
+
if (!m)
|
|
487
|
+
continue;
|
|
488
|
+
out.push(advisory({
|
|
489
|
+
check: 'listing-impossible-claim',
|
|
490
|
+
title: `The ${field.label} claims functionality no extension can deliver`,
|
|
491
|
+
detail: 'Misleading or Unexpected Behavior forbids "claimed functionalities that are not possible to ' +
|
|
492
|
+
'implement", and gives "Who has viewed your social media account" as the example. Platforms do not ' +
|
|
493
|
+
"expose other people's private activity, so a listing promising it is misleading no matter what the " +
|
|
494
|
+
'code does — this is a removal reason after publishing, not only a rejection reason.',
|
|
495
|
+
evidence: stamp(`${where(locale, field.label)}: "${m[0]}" — ${pattern.what}`),
|
|
496
|
+
recommendation: 'Delete the claim. Describe what the extension actually reads and shows, using data the platform ' +
|
|
497
|
+
'genuinely exposes to the signed-in user.',
|
|
498
|
+
citation: `Chrome Web Store: Misleading or Unexpected Behavior — ${DOC_UNEXPECTED}`,
|
|
499
|
+
}));
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
return out;
|
|
504
|
+
}
|
|
505
|
+
/** Ranking / guarantee claims a reviewer cannot verify. */
|
|
506
|
+
export function checkUnverifiableClaims(listing) {
|
|
507
|
+
const out = [];
|
|
508
|
+
for (const locale of listing.locales) {
|
|
509
|
+
for (const field of FIELDS) {
|
|
510
|
+
const value = fieldValue(locale, field.key);
|
|
511
|
+
if (!value)
|
|
512
|
+
continue;
|
|
513
|
+
for (const pattern of UNVERIFIABLE_CLAIM_PATTERNS) {
|
|
514
|
+
const m = pattern.re.exec(value);
|
|
515
|
+
if (!m)
|
|
516
|
+
continue;
|
|
517
|
+
out.push(advisory({
|
|
518
|
+
check: 'listing-unverifiable-superlative',
|
|
519
|
+
title: `The ${field.label} makes a claim a reviewer cannot verify`,
|
|
520
|
+
detail: 'Content that "contains false or misleading information" may be removed, and a ranking or guarantee ' +
|
|
521
|
+
'claim has nothing behind it that a reviewer can check. It buys no ranking and it gives moderation a ' +
|
|
522
|
+
'reason to look harder at everything else.',
|
|
523
|
+
evidence: stamp(`${where(locale, field.label)}: "${m[0]}" — ${pattern.what}`),
|
|
524
|
+
recommendation: 'Replace the claim with a specific, checkable statement about what the extension does.',
|
|
525
|
+
citation: `Chrome Web Store: Misleading or Unexpected Behavior — ${DOC_UNEXPECTED}`,
|
|
526
|
+
}));
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
return out;
|
|
531
|
+
}
|
|
532
|
+
// ── Orchestration ────────────────────────────────────────────────────────────
|
|
533
|
+
/** Every listing rule, over every locale. Returns [] when there is no listing. */
|
|
534
|
+
export function runListingChecks(listing) {
|
|
535
|
+
if (!listing || listing.locales.length === 0)
|
|
536
|
+
return [];
|
|
537
|
+
return [
|
|
538
|
+
...checkListingEmptyFields(listing),
|
|
539
|
+
...checkKeywordRepetition(listing),
|
|
540
|
+
...checkKeywordList(listing),
|
|
541
|
+
...checkNameStuffing(listing),
|
|
542
|
+
...checkTestimonialBlock(listing),
|
|
543
|
+
...checkImpossibleClaims(listing),
|
|
544
|
+
...checkUnverifiableClaims(listing),
|
|
545
|
+
];
|
|
546
|
+
}
|
|
547
|
+
//# sourceMappingURL=listing-checks.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"listing-checks.js","sourceRoot":"","sources":["../src/listing-checks.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* review-risk — the store-listing input contract.
|
|
3
|
+
*
|
|
4
|
+
* Chrome Web Store moderation reads the LISTING (name, short description, full
|
|
5
|
+
* description) as well as the artifact, and it reads every locale you publish,
|
|
6
|
+
* not just the default one. So the listing checks need a locale-keyed input,
|
|
7
|
+
* and it has to be assemblable from more than one place, because the same
|
|
8
|
+
* developer is in a different situation before and after their first publish.
|
|
9
|
+
*
|
|
10
|
+
* Two sources, both offline:
|
|
11
|
+
*
|
|
12
|
+
* artifact — `manifest.json` name/description, with `__MSG_key__`
|
|
13
|
+
* placeholders resolved against every `_locales/<lang>/messages.json`
|
|
14
|
+
* in the package. This needs no flag and no network, so the
|
|
15
|
+
* listing checks run on every `review-risk` invocation. It cannot
|
|
16
|
+
* see the FULL description: that field lives in the developer
|
|
17
|
+
* dashboard, never in the package.
|
|
18
|
+
* draft — `--listing listing.json`, a local file. This is the only way to
|
|
19
|
+
* check the full description, and the only thing an unpublished
|
|
20
|
+
* extension has at all.
|
|
21
|
+
*
|
|
22
|
+
* Fetching the published listing out of the catalog by `--extension-id` is the
|
|
23
|
+
* third source; it needs a BFF read that does not exist yet, so it is not wired
|
|
24
|
+
* here. The contract below already carries `source`, so adding it later is
|
|
25
|
+
* additive for every consumer.
|
|
26
|
+
*/
|
|
27
|
+
import type { FileMap, Manifest } from './review-checks.js';
|
|
28
|
+
/** Where a listing's text came from — reported so a developer can see what was checked. */
|
|
29
|
+
export type ListingSource = 'artifact' | 'draft';
|
|
30
|
+
/** One locale's listing text. Every field is optional: sources differ in coverage. */
|
|
31
|
+
export interface ListingLocale {
|
|
32
|
+
/** BCP-47-ish locale tag as the store spells it (`en`, `en_GB`, `ru`). */
|
|
33
|
+
locale: string;
|
|
34
|
+
/** Store listing title. */
|
|
35
|
+
name?: string;
|
|
36
|
+
/** The one-line summary (manifest `description` / dashboard "short description"). */
|
|
37
|
+
shortDescription?: string;
|
|
38
|
+
/** The full dashboard description. Never present in an artifact-sourced listing. */
|
|
39
|
+
description?: string;
|
|
40
|
+
}
|
|
41
|
+
export interface Listing {
|
|
42
|
+
source: ListingSource;
|
|
43
|
+
/** The locale a store visitor sees by default. */
|
|
44
|
+
defaultLocale: string;
|
|
45
|
+
/** Every locale with at least one non-empty field. */
|
|
46
|
+
locales: ListingLocale[];
|
|
47
|
+
/**
|
|
48
|
+
* True when the text was written WITHOUT reference to any locale — the flat
|
|
49
|
+
* `{ name, description }` draft shape, which describes the extension rather
|
|
50
|
+
* than one of its translations.
|
|
51
|
+
*
|
|
52
|
+
* This is the difference between "one description for the whole listing" and
|
|
53
|
+
* "the English block of a per-locale file". Only the former may be applied to
|
|
54
|
+
* other locales: copying an explicitly-English block onto `ru` and then
|
|
55
|
+
* reporting findings against `[ru]` accuses the developer of text they never
|
|
56
|
+
* wrote.
|
|
57
|
+
*/
|
|
58
|
+
appliesToAllLocales?: boolean;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Build a listing from the package itself.
|
|
62
|
+
*
|
|
63
|
+
* Returns `null` when the manifest carries no listing text at all — there is
|
|
64
|
+
* nothing to check, and an empty listing would make the checks report on
|
|
65
|
+
* absence they cannot distinguish from "the developer keeps this in the
|
|
66
|
+
* dashboard".
|
|
67
|
+
*/
|
|
68
|
+
export declare function listingFromArtifact(manifest: Manifest, files: FileMap): Listing | null;
|
|
69
|
+
/**
|
|
70
|
+
* Parse a `--listing` draft file.
|
|
71
|
+
*
|
|
72
|
+
* Two shapes, because both are natural to write:
|
|
73
|
+
*
|
|
74
|
+
* { "name": "…", "shortDescription": "…", "description": "…" }
|
|
75
|
+
* { "defaultLocale": "en", "locales": { "en": { … }, "ru": { … } } }
|
|
76
|
+
*
|
|
77
|
+
* Throws on a file that is not an object or that yields no usable text — unlike
|
|
78
|
+
* the artifact path, the developer explicitly pointed at this file, so silence
|
|
79
|
+
* would look like a passing check.
|
|
80
|
+
*/
|
|
81
|
+
export declare function parseListingFile(raw: unknown): Listing;
|
|
82
|
+
/**
|
|
83
|
+
* Merge a draft over an artifact-derived listing.
|
|
84
|
+
*
|
|
85
|
+
* The draft wins field by field rather than wholesale, because the two sources
|
|
86
|
+
* cover different fields: the artifact knows the localized name and short
|
|
87
|
+
* description, the draft is the only place the full description exists. Taking
|
|
88
|
+
* the draft as a whole would silently drop every locale it does not mention.
|
|
89
|
+
*/
|
|
90
|
+
export declare function mergeListings(artifact: Listing | null, draft: Listing): Listing;
|
|
91
|
+
//# sourceMappingURL=listing.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"listing.d.ts","sourceRoot":"","sources":["../src/listing.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAA;AAE3D,2FAA2F;AAC3F,MAAM,MAAM,aAAa,GAAG,UAAU,GAAG,OAAO,CAAA;AAEhD,sFAAsF;AACtF,MAAM,WAAW,aAAa;IAC7B,0EAA0E;IAC1E,MAAM,EAAE,MAAM,CAAA;IACd,2BAA2B;IAC3B,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,qFAAqF;IACrF,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,oFAAoF;IACpF,WAAW,CAAC,EAAE,MAAM,CAAA;CACpB;AAED,MAAM,WAAW,OAAO;IACvB,MAAM,EAAE,aAAa,CAAA;IACrB,kDAAkD;IAClD,aAAa,EAAE,MAAM,CAAA;IACrB,sDAAsD;IACtD,OAAO,EAAE,aAAa,EAAE,CAAA;IACxB;;;;;;;;;;OAUG;IACH,mBAAmB,CAAC,EAAE,OAAO,CAAA;CAC7B;AAmED;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,QAAQ,EAAE,KAAK,EAAE,OAAO,GAAG,OAAO,GAAG,IAAI,CAgCtF;AAuBD;;;;;;;;;;;GAWG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CA8BtD;AAED;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,QAAQ,EAAE,OAAO,GAAG,IAAI,EAAE,KAAK,EAAE,OAAO,GAAG,OAAO,CAmD/E"}
|