carveout 0.1.0 → 0.1.2
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/AGENTS.md +21 -3
- package/NOTICE.md +2 -1
- package/README.md +274 -460
- package/dist/src/account.d.ts +37 -0
- package/dist/src/account.js +275 -0
- package/dist/src/api.d.ts +148 -0
- package/dist/src/api.js +120 -0
- package/dist/src/carve.d.ts +788 -0
- package/dist/src/carve.js +1721 -125
- package/dist/src/cli.d.ts +10 -0
- package/dist/src/cli.js +674 -47
- package/dist/src/command.d.ts +10 -0
- package/dist/src/core.d.ts +11 -0
- package/dist/src/cut.d.ts +78 -0
- package/dist/src/cut.js +25 -7
- package/dist/src/extract/compiler-config.d.ts +49 -0
- package/dist/src/extract/fixture.d.ts +103 -0
- package/dist/src/extract/hash.d.ts +6 -0
- package/dist/src/extract/package-registry.d.ts +47 -0
- package/dist/src/extract/package-types.d.ts +52 -0
- package/dist/src/extract/surface.d.ts +44 -0
- package/dist/src/extract/typescript.d.ts +180 -0
- package/dist/src/gate.d.ts +55 -0
- package/dist/src/gate.js +1 -0
- package/dist/src/graph.d.ts +142 -0
- package/dist/src/graph.js +4 -3
- package/dist/src/impact.d.ts +113 -0
- package/dist/src/imports.d.ts +67 -0
- package/dist/src/mergeback.d.ts +182 -0
- package/dist/src/mergeback.js +580 -35
- package/dist/src/platform.d.ts +69 -0
- package/dist/src/platform.js +184 -0
- package/dist/src/policy.d.ts +181 -0
- package/dist/src/policy.js +716 -230
- package/dist/src/project/build.d.ts +23 -0
- package/dist/src/project/catalog.d.ts +25 -0
- package/dist/src/project/project.d.ts +287 -0
- package/dist/src/project/project.js +479 -52
- package/dist/src/project/provenance.d.ts +73 -0
- package/dist/src/project/provenance.js +12 -0
- package/dist/src/project/runner.d.ts +19 -0
- package/dist/src/project/secrets.d.ts +64 -0
- package/dist/src/project/vitest-config.d.ts +70 -0
- package/dist/src/project/vitest-config.js +291 -0
- package/dist/src/pseudonyms/aliases.d.ts +107 -0
- package/dist/src/pseudonyms/aliases.js +394 -0
- package/dist/src/pseudonyms/draft.d.ts +64 -0
- package/dist/src/pseudonyms/draft.js +284 -0
- package/dist/src/pseudonyms/folder.d.ts +172 -0
- package/dist/src/pseudonyms/folder.js +527 -0
- package/dist/src/pseudonyms/index.d.ts +11 -0
- package/dist/src/pseudonyms/index.js +11 -0
- package/dist/src/pseudonyms/vocabulary.d.ts +113 -0
- package/dist/src/pseudonyms/vocabulary.js +522 -0
- package/dist/src/readiness.d.ts +71 -0
- package/dist/src/readiness.js +131 -0
- package/dist/src/receipt.d.ts +154 -0
- package/dist/src/receipt.js +296 -0
- package/dist/src/sandbox.d.ts +53 -0
- package/dist/src/sandbox.js +154 -0
- package/dist/src/slice/analysis.d.ts +75 -0
- package/dist/src/slice/externals.d.ts +51 -0
- package/dist/src/slice/graph.d.ts +69 -0
- package/dist/src/slice/manifest.d.ts +262 -0
- package/dist/src/slice/manifest.js +4 -3
- package/dist/src/slice/reach.d.ts +204 -0
- package/dist/src/slice/reach.js +11 -5
- package/dist/src/slice/report.d.ts +49 -0
- package/dist/src/slice/report.js +5 -2
- package/dist/src/specifiers.d.ts +48 -0
- package/dist/src/status.d.ts +46 -0
- package/dist/src/status.js +3 -2
- package/dist/src/test-integrity.d.ts +54 -0
- package/dist/src/test-integrity.js +812 -0
- package/package.json +24 -5
- package/skills/carveout/SKILL.md +517 -0
- package/skills/carveout-gate/SKILL.md +98 -0
- package/SKILL.md +0 -423
|
@@ -0,0 +1,522 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workspace vocabulary: the names that say whose code a sandbox was cut
|
|
3
|
+
* from (the organization, its repository, its package scope, its author)
|
|
4
|
+
* and the check that none of them reaches a public file.
|
|
5
|
+
*
|
|
6
|
+
* Pseudonyms are what hide those names, but a table can be empty or miss
|
|
7
|
+
* one, and a sliced version is the workspace's own code. So a build scans
|
|
8
|
+
* what contributors would read, paths and text, for every name the
|
|
9
|
+
* workspace goes by, and a name still there blocks the build until the
|
|
10
|
+
* table renames it.
|
|
11
|
+
*
|
|
12
|
+
* A name is matched by its words, whatever joins them: `sandbox-factory`
|
|
13
|
+
* is found as `sandboxFactory`, `SANDBOX_FACTORY` and `sandbox factory`.
|
|
14
|
+
* A match must start and end on a word boundary, where a case change in an
|
|
15
|
+
* identifier counts as one: `acme` is found in `acmeOrders`, `MyAcme` and
|
|
16
|
+
* `ACMEOrders`, not in `acmes`. A name too short or too generic to say
|
|
17
|
+
* whose code it is (`api`, `web`, `monorepo`) is not scanned for.
|
|
18
|
+
*
|
|
19
|
+
* An organization's names are scanned word by word as well: `Acme Corp`
|
|
20
|
+
* writes its packages `@acme/ledger` and its code `acmeOrders`, never
|
|
21
|
+
* `acme-corp`. Only its distinctive words, though: one of at least
|
|
22
|
+
* `VOCABULARY_TERM_MIN` letters that is not a legal form or trade
|
|
23
|
+
* (`ORGANIZATION_WORDS`) nor an everyday word (`COMMON_WORDS`), whatever its
|
|
24
|
+
* length. `First Choice Logistics` is scanned whole, never as `first`, or
|
|
25
|
+
* every `firstName` would block; `acme` and `payflow` are scanned alone.
|
|
26
|
+
* Other names (a repository's, a package's) are matched only whole, since
|
|
27
|
+
* `billing-service` names one product but `billing` is everyone's word. A
|
|
28
|
+
* name or word without a letter (`12345`) names nothing and is dropped.
|
|
29
|
+
*
|
|
30
|
+
* A package the slice imports from the workspace itself or from a private
|
|
31
|
+
* registry is mocked under its own name unless the table renames it, so its
|
|
32
|
+
* scope is scanned as an organization's name (`@acme/ledger` gives `acme`)
|
|
33
|
+
* and an unscoped one by its whole name (`payflow-sdk`). A package's own
|
|
34
|
+
* one word (`ledger`, `logger`) is the code's vocabulary, not whose code it
|
|
35
|
+
* is, and a public package mocked by default (`lodash`) names no one.
|
|
36
|
+
*/
|
|
37
|
+
/** Shorter names, separators aside, match too much to block on. */
|
|
38
|
+
export const VOCABULARY_TERM_MIN = 4;
|
|
39
|
+
/** Files a leak's detail names before it counts the rest. */
|
|
40
|
+
const LEAK_FILES_SHOWN = 5;
|
|
41
|
+
const LETTER = /\p{L}/u;
|
|
42
|
+
const UPPER = /\p{Lu}/u;
|
|
43
|
+
const LOWER = /\p{Ll}/u;
|
|
44
|
+
const DIGIT = /\p{N}/u;
|
|
45
|
+
const is = (pattern, char) => char !== undefined && pattern.test(char);
|
|
46
|
+
/** Words any workspace might name itself with, which say nothing about whose. */
|
|
47
|
+
const GENERIC_WORDS = new Set([
|
|
48
|
+
"admin",
|
|
49
|
+
"android",
|
|
50
|
+
"api",
|
|
51
|
+
"app",
|
|
52
|
+
"apps",
|
|
53
|
+
"backend",
|
|
54
|
+
"client",
|
|
55
|
+
"code",
|
|
56
|
+
"common",
|
|
57
|
+
"company",
|
|
58
|
+
"core",
|
|
59
|
+
"corp",
|
|
60
|
+
"dashboard",
|
|
61
|
+
"demo",
|
|
62
|
+
"docs",
|
|
63
|
+
"example",
|
|
64
|
+
"frontend",
|
|
65
|
+
"internal",
|
|
66
|
+
"ios",
|
|
67
|
+
"lib",
|
|
68
|
+
"libs",
|
|
69
|
+
"local",
|
|
70
|
+
"main",
|
|
71
|
+
"mobile",
|
|
72
|
+
"mono",
|
|
73
|
+
"monorepo",
|
|
74
|
+
"packages",
|
|
75
|
+
"personal",
|
|
76
|
+
"platform",
|
|
77
|
+
"private",
|
|
78
|
+
"project",
|
|
79
|
+
"repo",
|
|
80
|
+
"root",
|
|
81
|
+
"server",
|
|
82
|
+
"service",
|
|
83
|
+
"services",
|
|
84
|
+
"shared",
|
|
85
|
+
"site",
|
|
86
|
+
"src",
|
|
87
|
+
"team",
|
|
88
|
+
"test",
|
|
89
|
+
"tests",
|
|
90
|
+
"utils",
|
|
91
|
+
"web",
|
|
92
|
+
"website",
|
|
93
|
+
"workspace",
|
|
94
|
+
"www",
|
|
95
|
+
]);
|
|
96
|
+
/**
|
|
97
|
+
* Words an organization's name is made of that say nothing about which
|
|
98
|
+
* organization: its legal form, its trade and the words code uses anyway.
|
|
99
|
+
* Matched whole, they are part of its name; alone, they are not scanned for.
|
|
100
|
+
*/
|
|
101
|
+
const ORGANIZATION_WORDS = new Set([
|
|
102
|
+
"academy",
|
|
103
|
+
"agency",
|
|
104
|
+
"airlines",
|
|
105
|
+
"analytics",
|
|
106
|
+
"bank",
|
|
107
|
+
"bhd",
|
|
108
|
+
"capital",
|
|
109
|
+
"cloud",
|
|
110
|
+
"co",
|
|
111
|
+
"communications",
|
|
112
|
+
"construction",
|
|
113
|
+
"consulting",
|
|
114
|
+
"corporation",
|
|
115
|
+
"data",
|
|
116
|
+
"design",
|
|
117
|
+
"digital",
|
|
118
|
+
"education",
|
|
119
|
+
"email",
|
|
120
|
+
"energy",
|
|
121
|
+
"engineering",
|
|
122
|
+
"enterprise",
|
|
123
|
+
"enterprises",
|
|
124
|
+
"finance",
|
|
125
|
+
"financial",
|
|
126
|
+
"foods",
|
|
127
|
+
"foundation",
|
|
128
|
+
"games",
|
|
129
|
+
"global",
|
|
130
|
+
"gmbh",
|
|
131
|
+
"group",
|
|
132
|
+
"health",
|
|
133
|
+
"holding",
|
|
134
|
+
"holdings",
|
|
135
|
+
"inc",
|
|
136
|
+
"incorporated",
|
|
137
|
+
"industries",
|
|
138
|
+
"info",
|
|
139
|
+
"insurance",
|
|
140
|
+
"interactive",
|
|
141
|
+
"international",
|
|
142
|
+
"labs",
|
|
143
|
+
"limited",
|
|
144
|
+
"llc",
|
|
145
|
+
"llp",
|
|
146
|
+
"logistics",
|
|
147
|
+
"ltd",
|
|
148
|
+
"mail",
|
|
149
|
+
"manufacturing",
|
|
150
|
+
"marketing",
|
|
151
|
+
"media",
|
|
152
|
+
"medical",
|
|
153
|
+
"motors",
|
|
154
|
+
"network",
|
|
155
|
+
"networks",
|
|
156
|
+
"official",
|
|
157
|
+
"online",
|
|
158
|
+
"open",
|
|
159
|
+
"partners",
|
|
160
|
+
"payments",
|
|
161
|
+
"pharma",
|
|
162
|
+
"plc",
|
|
163
|
+
"properties",
|
|
164
|
+
"pt",
|
|
165
|
+
"pte",
|
|
166
|
+
"pty",
|
|
167
|
+
"realty",
|
|
168
|
+
"retail",
|
|
169
|
+
"sdn",
|
|
170
|
+
"shop",
|
|
171
|
+
"software",
|
|
172
|
+
"solutions",
|
|
173
|
+
"source",
|
|
174
|
+
"store",
|
|
175
|
+
"studio",
|
|
176
|
+
"studios",
|
|
177
|
+
"supply",
|
|
178
|
+
"systems",
|
|
179
|
+
"tbk",
|
|
180
|
+
"tech",
|
|
181
|
+
"technologies",
|
|
182
|
+
"technology",
|
|
183
|
+
"telecom",
|
|
184
|
+
"trading",
|
|
185
|
+
"transport",
|
|
186
|
+
"ventures",
|
|
187
|
+
"work",
|
|
188
|
+
"works",
|
|
189
|
+
]);
|
|
190
|
+
/**
|
|
191
|
+
* Everyday English words an organization's name is often made of (`First
|
|
192
|
+
* Choice`, `Blue Harbor`, `Bright Path`) that code also uses for itself
|
|
193
|
+
* (`firstName`, `blueButton`, `pathname`). Matched whole, they are part of
|
|
194
|
+
* its name; alone, they are not scanned for. Brand-like words (`acme`,
|
|
195
|
+
* `payflow`) are deliberately absent.
|
|
196
|
+
*/
|
|
197
|
+
const COMMON_WORDS = new Set(("able about above access account action active actual advance after " +
|
|
198
|
+
"again agent alert align alpha amber anchor angle apex apple area " +
|
|
199
|
+
"army arrow asset atlas atom audio auto avenue away back badge " +
|
|
200
|
+
"balance band base basic beach beacon bear beat bell best beta better " +
|
|
201
|
+
"beyond bird black blank blaze block blue board body bold bolt bond " +
|
|
202
|
+
"book boost border boss bound brain branch brand brave bread break " +
|
|
203
|
+
"bridge bright broad brown budget build bull button cache call camp " +
|
|
204
|
+
"canvas card care cargo carbon case cash castle catch cedar cell " +
|
|
205
|
+
"center central chain change channel charge chart chase check chef " +
|
|
206
|
+
"cherry chief choice circle city civic class clean clear click climb " +
|
|
207
|
+
"clock close cloud coast coin color comet command compass connect " +
|
|
208
|
+
"contact content control coral count country county cove craft crane " +
|
|
209
|
+
"create credit crest cross crown crystal cube current custom cycle " +
|
|
210
|
+
"daily dash date dawn deal deep delta desk detail direct domain door " +
|
|
211
|
+
"double dove draft dragon dream drive drop eagle early earth east " +
|
|
212
|
+
"easy echo eden edge elite ember empire entry equal ever event every " +
|
|
213
|
+
"exact expert express fair faith falcon family farm fast field fire " +
|
|
214
|
+
"first fish flag flash fleet flex flight float flow focus force " +
|
|
215
|
+
"forest form forward fountain free fresh front frost fuel full fund " +
|
|
216
|
+
"fusion future galaxy game garden gate gear giant gold golden good " +
|
|
217
|
+
"grand graph great green grid ground grove guard guide happy harbor " +
|
|
218
|
+
"harbour harmony hawk head heart helix help hero high hill home honey " +
|
|
219
|
+
"hope horizon host house human idea image impact index inner input " +
|
|
220
|
+
"insight iron island item jade join journey jump just keep kind king " +
|
|
221
|
+
"kite label land large laser last launch layer leader leaf legacy " +
|
|
222
|
+
"level liberty life light lime line link lion list little live local " +
|
|
223
|
+
"lock logic long loop lotus love lucky lunar magic main major maple " +
|
|
224
|
+
"mark market master match matrix meadow mega member mercury merit " +
|
|
225
|
+
"meta metro middle mind mint mission mode model modern moment monster " +
|
|
226
|
+
"moon motion mountain move name nation national native nature navy " +
|
|
227
|
+
"near nest next noble node north nova number oasis ocean office omega " +
|
|
228
|
+
"orange orbit order origin output pacific page palm panda panel paper " +
|
|
229
|
+
"park path peak pearl people phoenix photo pilot pine pink pixel " +
|
|
230
|
+
"place plan planet plus pocket point polar portal post power premier " +
|
|
231
|
+
"press prime profile progress pulse pure purple quality quantum quest " +
|
|
232
|
+
"quick radar rain rainbow range rapid raven ready real record region " +
|
|
233
|
+
"reliable result ridge right ring rise river road rock rocket rose " +
|
|
234
|
+
"round royal ruby safe sage sail sales salt scale scope search secure " +
|
|
235
|
+
"select sense series shape share shield shift signal silver simple " +
|
|
236
|
+
"single smart snow social solar solid sound south space spark speed " +
|
|
237
|
+
"sphere spirit spring square stack standard star start state status " +
|
|
238
|
+
"steel step stone storm stream street strong summit sunrise super " +
|
|
239
|
+
"swift table talent target task team terra thunder tide tiger time " +
|
|
240
|
+
"title today token total touch tower track trade trail tree trend " +
|
|
241
|
+
"tribe trinity true trust truth turbo twin union unique united unity " +
|
|
242
|
+
"urban user valley value vector venture vertex view village vision " +
|
|
243
|
+
"vista vital voice wave west white wild wind wing winter wise wolf " +
|
|
244
|
+
"wood world year yellow young zero zone").split(" "));
|
|
245
|
+
/**
|
|
246
|
+
* Mail providers anyone may have an address at: an author's address there
|
|
247
|
+
* says who they are, not whom they work for.
|
|
248
|
+
*/
|
|
249
|
+
const PUBLIC_MAIL_DOMAINS = new Set([
|
|
250
|
+
"163.com",
|
|
251
|
+
"aol.com",
|
|
252
|
+
"fastmail.com",
|
|
253
|
+
"gmail.com",
|
|
254
|
+
"gmx.com",
|
|
255
|
+
"gmx.de",
|
|
256
|
+
"gmx.net",
|
|
257
|
+
"googlemail.com",
|
|
258
|
+
"hey.com",
|
|
259
|
+
"hotmail.com",
|
|
260
|
+
"icloud.com",
|
|
261
|
+
"live.com",
|
|
262
|
+
"mac.com",
|
|
263
|
+
"mail.com",
|
|
264
|
+
"me.com",
|
|
265
|
+
"msn.com",
|
|
266
|
+
"outlook.com",
|
|
267
|
+
"pm.me",
|
|
268
|
+
"proton.me",
|
|
269
|
+
"protonmail.com",
|
|
270
|
+
"qq.com",
|
|
271
|
+
"tutanota.com",
|
|
272
|
+
"users.noreply.github.com",
|
|
273
|
+
"yahoo.com",
|
|
274
|
+
"yandex.com",
|
|
275
|
+
"yandex.ru",
|
|
276
|
+
"ymail.com",
|
|
277
|
+
"zoho.com",
|
|
278
|
+
]);
|
|
279
|
+
/** Names that say who the organization is, to be scanned word by word too. */
|
|
280
|
+
export function organizationNames(names) {
|
|
281
|
+
return names.map((name) => ({ name, kind: "organization" }));
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* The names the slice's privately hosted packages give (see the module
|
|
285
|
+
* comment): one of the workspace's own, or one a lockfile or `.npmrc` says
|
|
286
|
+
* comes from another registry. A package nothing says is public is mocked
|
|
287
|
+
* too, but by default, so it is not one.
|
|
288
|
+
*/
|
|
289
|
+
export function privatePackageNames(packages) {
|
|
290
|
+
const names = [];
|
|
291
|
+
for (const item of packages) {
|
|
292
|
+
if (item.workspace !== true && item.registry?.source !== "private")
|
|
293
|
+
continue;
|
|
294
|
+
const slash = item.name.indexOf("/");
|
|
295
|
+
if (item.name.startsWith("@") && slash > 1)
|
|
296
|
+
names.push({ name: item.name.slice(1, slash), kind: "organization" });
|
|
297
|
+
else if (termWords(item.name).length > 1)
|
|
298
|
+
names.push({ name: item.name, kind: "name" });
|
|
299
|
+
}
|
|
300
|
+
return names;
|
|
301
|
+
}
|
|
302
|
+
/** Shared and automated mailboxes: they name a function, not a person. */
|
|
303
|
+
const ROLE_MAILBOXES = new Set([
|
|
304
|
+
"abuse",
|
|
305
|
+
"accounts",
|
|
306
|
+
"admin",
|
|
307
|
+
"billing",
|
|
308
|
+
"bot",
|
|
309
|
+
"careers",
|
|
310
|
+
"ci",
|
|
311
|
+
"contact",
|
|
312
|
+
"dev",
|
|
313
|
+
"developer",
|
|
314
|
+
"developers",
|
|
315
|
+
"devops",
|
|
316
|
+
"engineering",
|
|
317
|
+
"github",
|
|
318
|
+
"hello",
|
|
319
|
+
"help",
|
|
320
|
+
"hi",
|
|
321
|
+
"hostmaster",
|
|
322
|
+
"hr",
|
|
323
|
+
"info",
|
|
324
|
+
"it",
|
|
325
|
+
"jobs",
|
|
326
|
+
"legal",
|
|
327
|
+
"mail",
|
|
328
|
+
"maintainers",
|
|
329
|
+
"marketing",
|
|
330
|
+
"noreply",
|
|
331
|
+
"npm",
|
|
332
|
+
"office",
|
|
333
|
+
"ops",
|
|
334
|
+
"opensource",
|
|
335
|
+
"oss",
|
|
336
|
+
"postmaster",
|
|
337
|
+
"press",
|
|
338
|
+
"privacy",
|
|
339
|
+
"release",
|
|
340
|
+
"releases",
|
|
341
|
+
"root",
|
|
342
|
+
"sales",
|
|
343
|
+
"security",
|
|
344
|
+
"support",
|
|
345
|
+
"team",
|
|
346
|
+
"tech",
|
|
347
|
+
"webmaster",
|
|
348
|
+
]);
|
|
349
|
+
/**
|
|
350
|
+
* The names a package author's address gives: its local part unless a role
|
|
351
|
+
* mailbox's (`support@`) or without a letter, and its domain as the
|
|
352
|
+
* organization's unless it is a public mail provider's. A GitHub noreply
|
|
353
|
+
* address (`12345+octocat@users.noreply.github.com`) gives the login after
|
|
354
|
+
* the `+`; any other drops its `+tag`.
|
|
355
|
+
*/
|
|
356
|
+
export function emailNames(email) {
|
|
357
|
+
const at = email.lastIndexOf("@");
|
|
358
|
+
if (at <= 0)
|
|
359
|
+
return [];
|
|
360
|
+
const domain = email
|
|
361
|
+
.slice(at + 1)
|
|
362
|
+
.trim()
|
|
363
|
+
.toLowerCase();
|
|
364
|
+
const address = email.slice(0, at).trim();
|
|
365
|
+
const plus = address.indexOf("+");
|
|
366
|
+
const local = plus === -1
|
|
367
|
+
? address
|
|
368
|
+
: domain === "users.noreply.github.com"
|
|
369
|
+
? address.slice(plus + 1)
|
|
370
|
+
: address.slice(0, plus);
|
|
371
|
+
const names = [];
|
|
372
|
+
if (/\p{L}/u.test(local) && !ROLE_MAILBOXES.has(termWords(local).join("")))
|
|
373
|
+
names.push({ name: local, kind: "name" });
|
|
374
|
+
if (domain !== "" && !PUBLIC_MAIL_DOMAINS.has(domain))
|
|
375
|
+
names.push({ name: domain, kind: "organization" });
|
|
376
|
+
return names;
|
|
377
|
+
}
|
|
378
|
+
/**
|
|
379
|
+
* A name's words, lowercased: split at anything not a letter or digit, and
|
|
380
|
+
* at each case change an identifier marks (`acmeOrders`, `XMLParser`).
|
|
381
|
+
*/
|
|
382
|
+
export function termWords(name) {
|
|
383
|
+
return name
|
|
384
|
+
.replace(/(\p{Ll}|\p{N})(\p{Lu})/gu, "$1 $2")
|
|
385
|
+
.replace(/(\p{Lu})(\p{Lu}\p{Ll})/gu, "$1 $2")
|
|
386
|
+
.split(/[^\p{L}\p{N}]+/u)
|
|
387
|
+
.filter((word) => word.length > 0)
|
|
388
|
+
.map((word) => word.toLowerCase());
|
|
389
|
+
}
|
|
390
|
+
/**
|
|
391
|
+
* The names worth scanning for, each as its words joined by `-`, in the
|
|
392
|
+
* order first given; an organization's name is followed by each of its
|
|
393
|
+
* distinctive words (see the module comment). Repeats, names under
|
|
394
|
+
* `VOCABULARY_TERM_MIN`, names without a letter and names made only of
|
|
395
|
+
* generic words are dropped.
|
|
396
|
+
*/
|
|
397
|
+
export function workspaceVocabulary(names) {
|
|
398
|
+
const terms = [];
|
|
399
|
+
const add = (term) => {
|
|
400
|
+
if (!terms.includes(term))
|
|
401
|
+
terms.push(term);
|
|
402
|
+
};
|
|
403
|
+
for (const entry of names) {
|
|
404
|
+
const { name, kind } = typeof entry === "string" ? { name: entry, kind: "name" } : entry;
|
|
405
|
+
const words = termWords(name);
|
|
406
|
+
if (words.join("").length < VOCABULARY_TERM_MIN)
|
|
407
|
+
continue;
|
|
408
|
+
if (!LETTER.test(name))
|
|
409
|
+
continue;
|
|
410
|
+
if (words.every((word) => GENERIC_WORDS.has(word)))
|
|
411
|
+
continue;
|
|
412
|
+
add(words.join("-"));
|
|
413
|
+
if (kind !== "organization")
|
|
414
|
+
continue;
|
|
415
|
+
for (const word of words)
|
|
416
|
+
if (word.length >= VOCABULARY_TERM_MIN &&
|
|
417
|
+
LETTER.test(word) &&
|
|
418
|
+
!GENERIC_WORDS.has(word) &&
|
|
419
|
+
!ORGANIZATION_WORDS.has(word) &&
|
|
420
|
+
!COMMON_WORDS.has(word))
|
|
421
|
+
add(word);
|
|
422
|
+
}
|
|
423
|
+
return terms;
|
|
424
|
+
}
|
|
425
|
+
/**
|
|
426
|
+
* Names a codebase calls its own things by: the platform's globals and
|
|
427
|
+
* their members (`console`, `performance.mark`, `URLSearchParams`), and the
|
|
428
|
+
* words APIs are made of. A one-word name of the workspace's that is one of
|
|
429
|
+
* these (a repository called `search`, a mail address `mark@…`) would rename
|
|
430
|
+
* the code's own calls, so it is not scanned for alone.
|
|
431
|
+
*/
|
|
432
|
+
const CODE_WORDS = new Set(("console document window navigator location history screen performance " +
|
|
433
|
+
"process global globalthis crypto fetch request response headers event " +
|
|
434
|
+
"error date math json string number object array promise symbol proxy " +
|
|
435
|
+
"reflect intl buffer stream worker module require exports import " +
|
|
436
|
+
"search mark measure query params status result value values items " +
|
|
437
|
+
"node react vue next nuxt vite test tests spec mock mocks stub " +
|
|
438
|
+
"user users admin login logout session token auth account settings " +
|
|
439
|
+
"home index main page pages view views layout model models store " +
|
|
440
|
+
"data info list item name title label text type types kind state " +
|
|
441
|
+
"start stop open close read write send post update delete create").split(" "));
|
|
442
|
+
/**
|
|
443
|
+
* Whether a one-word name is too common to scan for alone: generic, an
|
|
444
|
+
* organization's trade or form, an everyday word, or a word code is made
|
|
445
|
+
* of. A name of several words is scanned whole whatever its words.
|
|
446
|
+
*/
|
|
447
|
+
export function tooCommonAlone(name) {
|
|
448
|
+
const words = termWords(name);
|
|
449
|
+
if (words.length !== 1)
|
|
450
|
+
return false;
|
|
451
|
+
const word = words[0] ?? "";
|
|
452
|
+
return GENERIC_WORDS.has(word) || ORGANIZATION_WORDS.has(word) || COMMON_WORDS.has(word) || CODE_WORDS.has(word);
|
|
453
|
+
}
|
|
454
|
+
/**
|
|
455
|
+
* Every match of `term` in `text` that starts and ends on a word boundary:
|
|
456
|
+
* not a letter, or a case change an identifier marks (`myAcme`, `acmeOrders`,
|
|
457
|
+
* and in a run of capitals the last one before a lowercase, `XMLAcme`,
|
|
458
|
+
* `ACMEOrders`), or a capital after a digit (`web3Labs`). A term that starts
|
|
459
|
+
* or ends on a digit (`web3`) needs a non-digit there too, so `web3` is not
|
|
460
|
+
* found in `web30`.
|
|
461
|
+
*/
|
|
462
|
+
export function termMatches(text, term) {
|
|
463
|
+
// A term's words join across a space, `_`, `-` or `.`, never a `/`: two
|
|
464
|
+
// folders that hold its words are two names, each renamed on its own.
|
|
465
|
+
const pattern = new RegExp(term.split("-").join("[\\s_\\-.]*"), "giu");
|
|
466
|
+
const matches = [];
|
|
467
|
+
for (const match of text.matchAll(pattern)) {
|
|
468
|
+
const start = match.index;
|
|
469
|
+
const end = start + match[0].length;
|
|
470
|
+
const before = text[start - 1];
|
|
471
|
+
const after = text[end];
|
|
472
|
+
if (is(DIGIT, text[start]) && is(DIGIT, before))
|
|
473
|
+
continue;
|
|
474
|
+
if (is(DIGIT, text[end - 1]) && is(DIGIT, after))
|
|
475
|
+
continue;
|
|
476
|
+
const opens = !is(LETTER, before) ||
|
|
477
|
+
(is(LOWER, before) && is(UPPER, text[start])) ||
|
|
478
|
+
(is(UPPER, before) &&
|
|
479
|
+
is(UPPER, text[start]) &&
|
|
480
|
+
is(LOWER, text[start + 1]));
|
|
481
|
+
const closes = !is(LETTER, after) ||
|
|
482
|
+
(is(UPPER, after) &&
|
|
483
|
+
(is(LOWER, text[end - 1]) || is(DIGIT, text[end - 1]))) ||
|
|
484
|
+
(is(UPPER, after) &&
|
|
485
|
+
is(UPPER, text[end - 1]) &&
|
|
486
|
+
is(LOWER, text[end + 1]));
|
|
487
|
+
if (opens && closes)
|
|
488
|
+
matches.push({ start, end });
|
|
489
|
+
}
|
|
490
|
+
return matches;
|
|
491
|
+
}
|
|
492
|
+
const countTerm = (text, term) => termMatches(text, term).length;
|
|
493
|
+
/** Each term found in the files' paths or text, in the vocabulary's order. */
|
|
494
|
+
export function vocabularyLeaks(files, vocabulary) {
|
|
495
|
+
const leaks = [];
|
|
496
|
+
for (const term of vocabulary) {
|
|
497
|
+
let occurrences = 0;
|
|
498
|
+
const found = [];
|
|
499
|
+
for (const file of files) {
|
|
500
|
+
const count = countTerm(file.path, term) + countTerm(file.text, term);
|
|
501
|
+
if (count === 0)
|
|
502
|
+
continue;
|
|
503
|
+
occurrences += count;
|
|
504
|
+
found.push(file.path);
|
|
505
|
+
}
|
|
506
|
+
if (occurrences > 0)
|
|
507
|
+
leaks.push({ term, occurrences, files: found });
|
|
508
|
+
}
|
|
509
|
+
return leaks;
|
|
510
|
+
}
|
|
511
|
+
/** A blocker for each leak, naming where it was found and what clears it. */
|
|
512
|
+
export function vocabularyBlockers(leaks) {
|
|
513
|
+
return leaks.map((leak) => {
|
|
514
|
+
const shown = leak.files.slice(0, LEAK_FILES_SHOWN).join(", ");
|
|
515
|
+
const rest = leak.files.length - LEAK_FILES_SHOWN;
|
|
516
|
+
return {
|
|
517
|
+
code: "vocabulary_leaked",
|
|
518
|
+
file: leak.files[0] ?? null,
|
|
519
|
+
detail: `"${leak.term}" names the workspace and appears ${leak.occurrences} time${leak.occurrences === 1 ? "" : "s"} in ${leak.files.length} public file${leak.files.length === 1 ? "" : "s"}: ${shown}${rest > 0 ? `, and ${rest} more` : ""}. Give it a pseudonym.`,
|
|
520
|
+
};
|
|
521
|
+
});
|
|
522
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether a folder is ready to hand to someone at a fixed scope: its tests
|
|
3
|
+
* must pass before any edit, or "make the tests pass" is not a task anyone
|
|
4
|
+
* can finish in it. A test fails at the folder's baseline when it reaches a
|
|
5
|
+
* mock for something the repository runs for real: pure code past the cut
|
|
6
|
+
* (a validator, a formatter), a database or queue client, or a folder the
|
|
7
|
+
* code reads by a name no import spells (drizzle's `drizzle/` migrations,
|
|
8
|
+
* Prisma's `prisma/`). The cut knows which of these the slice has, and
|
|
9
|
+
* names the flag that brings each: `--copy-pure`, `--live`, `--copy`.
|
|
10
|
+
*
|
|
11
|
+
* The advice is the maintainer's: a public cut prints it and keeps it out
|
|
12
|
+
* of the folder, since the paths it names may lie outside what the folder
|
|
13
|
+
* shows.
|
|
14
|
+
*/
|
|
15
|
+
/** What a cut can change so the folder's tests run on real code. */
|
|
16
|
+
export interface Readiness {
|
|
17
|
+
/** The tests that run the slice's code and came with the folder. */
|
|
18
|
+
readonly tests: number;
|
|
19
|
+
/**
|
|
20
|
+
* Modules the slice mocks that `--copy-pure` would copy (pure code its
|
|
21
|
+
* tests run), with their lines; null when the cut copies pure code
|
|
22
|
+
* already or carries no test.
|
|
23
|
+
*/
|
|
24
|
+
readonly copyPure: {
|
|
25
|
+
readonly files: readonly string[];
|
|
26
|
+
readonly lines: number;
|
|
27
|
+
/** The file budget the cut with it needs, when this cut's would leave files out. */
|
|
28
|
+
readonly needsMaxFiles?: number;
|
|
29
|
+
} | null;
|
|
30
|
+
/** Mocked packages of a database, cache or queue: `--live` keeps one real. */
|
|
31
|
+
readonly live: readonly {
|
|
32
|
+
readonly name: string;
|
|
33
|
+
readonly service: string;
|
|
34
|
+
}[];
|
|
35
|
+
/** Folders a slice package reads by a configured name: `--copy` brings one. */
|
|
36
|
+
readonly copy: readonly {
|
|
37
|
+
readonly path: string;
|
|
38
|
+
readonly files: number;
|
|
39
|
+
readonly why: string;
|
|
40
|
+
}[];
|
|
41
|
+
}
|
|
42
|
+
/** The mocked service packages `--live` could keep real, by name. */
|
|
43
|
+
export declare function liveCandidates(dependencies: readonly {
|
|
44
|
+
readonly name: string;
|
|
45
|
+
readonly kind: string;
|
|
46
|
+
readonly resolution: string;
|
|
47
|
+
}[], serviceOf: (name: string) => string | null): {
|
|
48
|
+
name: string;
|
|
49
|
+
service: string;
|
|
50
|
+
}[];
|
|
51
|
+
/**
|
|
52
|
+
* The folders the slice's packages read by a name their tools default to
|
|
53
|
+
* or their configuration spells, which the walk cannot follow: drizzle's
|
|
54
|
+
* migrations (`out` in `drizzle.config.*`, else `drizzle/` with its
|
|
55
|
+
* journal), Prisma's schema and migrations (`prisma/`), knex's
|
|
56
|
+
* (`directory` in a knexfile, else `migrations/`), and a package's own
|
|
57
|
+
* `migrations/` folder. Folders the cut copies already, or holds files of,
|
|
58
|
+
* are left out.
|
|
59
|
+
*/
|
|
60
|
+
export declare function configuredFolders(repo: string, packageDirs: readonly string[], held: (path: string) => boolean): {
|
|
61
|
+
path: string;
|
|
62
|
+
files: number;
|
|
63
|
+
why: string;
|
|
64
|
+
}[];
|
|
65
|
+
/** Whether the readiness names anything the maintainer could do. */
|
|
66
|
+
export declare function hasAdvice(readiness: Readiness): boolean;
|
|
67
|
+
/**
|
|
68
|
+
* The advice as lines for a terminal: each flag with what it brings and
|
|
69
|
+
* what it costs (more of the repository shown, or a service to run).
|
|
70
|
+
*/
|
|
71
|
+
export declare function renderReadiness(readiness: Readiness): string[];
|