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.
Files changed (78) hide show
  1. package/AGENTS.md +21 -3
  2. package/NOTICE.md +2 -1
  3. package/README.md +274 -460
  4. package/dist/src/account.d.ts +37 -0
  5. package/dist/src/account.js +275 -0
  6. package/dist/src/api.d.ts +148 -0
  7. package/dist/src/api.js +120 -0
  8. package/dist/src/carve.d.ts +788 -0
  9. package/dist/src/carve.js +1721 -125
  10. package/dist/src/cli.d.ts +10 -0
  11. package/dist/src/cli.js +674 -47
  12. package/dist/src/command.d.ts +10 -0
  13. package/dist/src/core.d.ts +11 -0
  14. package/dist/src/cut.d.ts +78 -0
  15. package/dist/src/cut.js +25 -7
  16. package/dist/src/extract/compiler-config.d.ts +49 -0
  17. package/dist/src/extract/fixture.d.ts +103 -0
  18. package/dist/src/extract/hash.d.ts +6 -0
  19. package/dist/src/extract/package-registry.d.ts +47 -0
  20. package/dist/src/extract/package-types.d.ts +52 -0
  21. package/dist/src/extract/surface.d.ts +44 -0
  22. package/dist/src/extract/typescript.d.ts +180 -0
  23. package/dist/src/gate.d.ts +55 -0
  24. package/dist/src/gate.js +1 -0
  25. package/dist/src/graph.d.ts +142 -0
  26. package/dist/src/graph.js +4 -3
  27. package/dist/src/impact.d.ts +113 -0
  28. package/dist/src/imports.d.ts +67 -0
  29. package/dist/src/mergeback.d.ts +182 -0
  30. package/dist/src/mergeback.js +580 -35
  31. package/dist/src/platform.d.ts +69 -0
  32. package/dist/src/platform.js +184 -0
  33. package/dist/src/policy.d.ts +181 -0
  34. package/dist/src/policy.js +716 -230
  35. package/dist/src/project/build.d.ts +23 -0
  36. package/dist/src/project/catalog.d.ts +25 -0
  37. package/dist/src/project/project.d.ts +287 -0
  38. package/dist/src/project/project.js +479 -52
  39. package/dist/src/project/provenance.d.ts +73 -0
  40. package/dist/src/project/provenance.js +12 -0
  41. package/dist/src/project/runner.d.ts +19 -0
  42. package/dist/src/project/secrets.d.ts +64 -0
  43. package/dist/src/project/vitest-config.d.ts +70 -0
  44. package/dist/src/project/vitest-config.js +291 -0
  45. package/dist/src/pseudonyms/aliases.d.ts +107 -0
  46. package/dist/src/pseudonyms/aliases.js +394 -0
  47. package/dist/src/pseudonyms/draft.d.ts +64 -0
  48. package/dist/src/pseudonyms/draft.js +284 -0
  49. package/dist/src/pseudonyms/folder.d.ts +172 -0
  50. package/dist/src/pseudonyms/folder.js +527 -0
  51. package/dist/src/pseudonyms/index.d.ts +11 -0
  52. package/dist/src/pseudonyms/index.js +11 -0
  53. package/dist/src/pseudonyms/vocabulary.d.ts +113 -0
  54. package/dist/src/pseudonyms/vocabulary.js +522 -0
  55. package/dist/src/readiness.d.ts +71 -0
  56. package/dist/src/readiness.js +131 -0
  57. package/dist/src/receipt.d.ts +154 -0
  58. package/dist/src/receipt.js +296 -0
  59. package/dist/src/sandbox.d.ts +53 -0
  60. package/dist/src/sandbox.js +154 -0
  61. package/dist/src/slice/analysis.d.ts +75 -0
  62. package/dist/src/slice/externals.d.ts +51 -0
  63. package/dist/src/slice/graph.d.ts +69 -0
  64. package/dist/src/slice/manifest.d.ts +262 -0
  65. package/dist/src/slice/manifest.js +4 -3
  66. package/dist/src/slice/reach.d.ts +204 -0
  67. package/dist/src/slice/reach.js +11 -5
  68. package/dist/src/slice/report.d.ts +49 -0
  69. package/dist/src/slice/report.js +5 -2
  70. package/dist/src/specifiers.d.ts +48 -0
  71. package/dist/src/status.d.ts +46 -0
  72. package/dist/src/status.js +3 -2
  73. package/dist/src/test-integrity.d.ts +54 -0
  74. package/dist/src/test-integrity.js +812 -0
  75. package/package.json +24 -5
  76. package/skills/carveout/SKILL.md +517 -0
  77. package/skills/carveout-gate/SKILL.md +98 -0
  78. 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[];