carveout 0.1.2 → 0.3.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/NOTICE.md +44 -0
- package/dist/src/api.d.ts +147 -1
- package/dist/src/api.js +157 -1
- package/dist/src/carve.d.ts +55 -2
- package/dist/src/carve.js +31 -19
- package/dist/src/cut.d.ts +17 -0
- package/dist/src/cut.js +13 -7
- package/dist/src/extract.d.ts +31 -0
- package/dist/src/extract.js +31 -0
- package/dist/src/graph.d.ts +7 -2
- package/dist/src/graph.js +7 -4
- package/dist/src/project/mock-runtime.d.ts +9 -0
- package/dist/src/project/mock-runtime.js +81 -0
- package/dist/src/project/paths.d.ts +10 -0
- package/dist/src/project/paths.js +35 -0
- package/dist/src/project/project.d.ts +3 -6
- package/dist/src/project/project.js +3 -104
- package/dist/src/pseudonyms/carry.d.ts +161 -0
- package/dist/src/pseudonyms/carry.js +301 -0
- package/dist/src/pseudonyms/draft.js +1 -1
- package/dist/src/pseudonyms/folder.js +2 -40
- package/dist/src/pseudonyms/legacy/aliases-v1.d.ts +107 -0
- package/dist/src/pseudonyms/legacy/aliases-v1.js +391 -0
- package/dist/src/pseudonyms/legacy/vocabulary-v1.d.ts +107 -0
- package/dist/src/pseudonyms/legacy/vocabulary-v1.js +460 -0
- package/dist/src/pseudonyms/legacy.d.ts +34 -0
- package/dist/src/pseudonyms/legacy.js +46 -0
- package/dist/src/pseudonyms/vocabulary.d.ts +20 -0
- package/dist/src/pseudonyms/vocabulary.js +57 -0
- package/dist/src/pseudonyms.d.ts +20 -0
- package/dist/src/pseudonyms.js +20 -0
- package/package.json +9 -1
- package/skills/carveout/SKILL.md +15 -12
- package/skills/carveout-gate/SKILL.md +3 -3
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Carrying a change between a version's public names and the repository's:
|
|
3
|
+
* the reverse path merge-back needs (a submission, made in public names,
|
|
4
|
+
* as a patch on the repository at the pinned commit), and the forward path
|
|
5
|
+
* the CI judge needs (a pull request's files, in the repository's names,
|
|
6
|
+
* as a submission on the version).
|
|
7
|
+
*
|
|
8
|
+
* The build's transforms are token-for-token and never add or remove a
|
|
9
|
+
* line: the name table, import specifiers rewritten to the slice's layout,
|
|
10
|
+
* mocked packages and environment variables renamed. So a version's file
|
|
11
|
+
* and the repository's line up line by line, and a change carries by line:
|
|
12
|
+
*
|
|
13
|
+
* - A line the change kept is the other side's line at the same place,
|
|
14
|
+
* byte for byte. A change that touches nothing carries to the original.
|
|
15
|
+
* - A line the change added is renamed token by token, first by what the
|
|
16
|
+
* two originals' differing lines show the build renamed in this file
|
|
17
|
+
* (an import specifier, a package, an environment variable), then by the
|
|
18
|
+
* name table. It is refused unless renaming it back gives it again, so
|
|
19
|
+
* new text holding a name the side it was written on never shows (the
|
|
20
|
+
* repository's own names, in a contribution) cannot pass through.
|
|
21
|
+
* - A line the change added that imports the sandbox's harness (a mock, a
|
|
22
|
+
* stub), which the repository does not have, is refused.
|
|
23
|
+
*
|
|
24
|
+
* Pure and platform-neutral: the worker reads the files, the API writes
|
|
25
|
+
* the pull request.
|
|
26
|
+
*
|
|
27
|
+
* Moved here verbatim from sandbox-factory's packages/core/src/sandbox/carry.ts
|
|
28
|
+
* (commit f31d780), on carveout's alias implementation (`aliases.ts`). One
|
|
29
|
+
* addition: `aliases` on `ReverseInput`, `ForwardInput` and `projectPathOf`,
|
|
30
|
+
* so a version built with an older renaming carries back under it
|
|
31
|
+
* (`ALIASES_V1`, `legacy.ts`).
|
|
32
|
+
*/
|
|
33
|
+
import { invertAliasRules, renameFile } from "./aliases.js";
|
|
34
|
+
/** carveout's renaming (`aliases.ts`). */
|
|
35
|
+
export const CURRENT_ALIASES = { renameFile, invertAliasRules };
|
|
36
|
+
/** More than this many lines changed in one file, and added lines are not matched to kept ones. */
|
|
37
|
+
const MATCH_CELLS = 4_000_000;
|
|
38
|
+
/** Splits text into lines, each with its line ending. */
|
|
39
|
+
export function linesOf(text) {
|
|
40
|
+
return text === "" ? [] : (text.match(/[^\n]*\n|[^\n]+$/g) ?? []);
|
|
41
|
+
}
|
|
42
|
+
/** Where a string literal opened at `start` closes, past its quote; -1 for none on the line. */
|
|
43
|
+
function literalEnd(line, start) {
|
|
44
|
+
const quote = line[start];
|
|
45
|
+
for (let at = start + 1; at < line.length; at += 1) {
|
|
46
|
+
const char = line[at];
|
|
47
|
+
if (char === "\\")
|
|
48
|
+
at += 1;
|
|
49
|
+
else if (char === quote)
|
|
50
|
+
return at + 1;
|
|
51
|
+
else if (char === "\n" || (quote === "`" && char === "$"))
|
|
52
|
+
return -1;
|
|
53
|
+
}
|
|
54
|
+
return -1;
|
|
55
|
+
}
|
|
56
|
+
const IDENTIFIER_START = /[A-Za-z_$]/;
|
|
57
|
+
const IDENTIFIER_PART = /[A-Za-z0-9_$]/;
|
|
58
|
+
const SPACE = /\s/;
|
|
59
|
+
/**
|
|
60
|
+
* A line as tokens: string literals whole (a template literal only without
|
|
61
|
+
* `${`), identifiers, runs of space, single characters. One pass, linear in
|
|
62
|
+
* the line: a quote that does not close on it is a character of its own.
|
|
63
|
+
*/
|
|
64
|
+
export function tokensOf(line) {
|
|
65
|
+
const tokens = [];
|
|
66
|
+
// A quote that found no close cannot find one from further on: a later
|
|
67
|
+
// quote of its kind was either escaped in that scan, which then went on
|
|
68
|
+
// as a scan from it would, or would have closed it.
|
|
69
|
+
const unclosed = new Set();
|
|
70
|
+
let at = 0;
|
|
71
|
+
while (at < line.length) {
|
|
72
|
+
const char = line[at];
|
|
73
|
+
let end = at + 1;
|
|
74
|
+
if (char === '"' || char === "'" || char === "`") {
|
|
75
|
+
const close = unclosed.has(char) ? -1 : literalEnd(line, at);
|
|
76
|
+
if (close > 0)
|
|
77
|
+
end = close;
|
|
78
|
+
else
|
|
79
|
+
unclosed.add(char);
|
|
80
|
+
}
|
|
81
|
+
else if (IDENTIFIER_START.test(char))
|
|
82
|
+
while (end < line.length && IDENTIFIER_PART.test(line[end]))
|
|
83
|
+
end += 1;
|
|
84
|
+
else if (SPACE.test(char))
|
|
85
|
+
while (end < line.length && SPACE.test(line[end]))
|
|
86
|
+
end += 1;
|
|
87
|
+
tokens.push(line.slice(at, end));
|
|
88
|
+
at = end;
|
|
89
|
+
}
|
|
90
|
+
return tokens;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* What the two sides' originals show was renamed in this file: each token
|
|
94
|
+
* that differs where the lines otherwise line up, from one side to the
|
|
95
|
+
* other. A token renamed two ways is listed as ambiguous.
|
|
96
|
+
*/
|
|
97
|
+
export function learnedRenames(from, to) {
|
|
98
|
+
const renames = new Map();
|
|
99
|
+
const ambiguous = new Set();
|
|
100
|
+
from.forEach((line, index) => {
|
|
101
|
+
const other = to[index];
|
|
102
|
+
if (other === undefined || other === line)
|
|
103
|
+
return;
|
|
104
|
+
const a = tokensOf(line);
|
|
105
|
+
const b = tokensOf(other);
|
|
106
|
+
if (a.length !== b.length)
|
|
107
|
+
return;
|
|
108
|
+
a.forEach((token, at) => {
|
|
109
|
+
const target = b[at];
|
|
110
|
+
if (token === target)
|
|
111
|
+
return;
|
|
112
|
+
const known = renames.get(token);
|
|
113
|
+
if (known === undefined)
|
|
114
|
+
renames.set(token, target);
|
|
115
|
+
else if (known !== target)
|
|
116
|
+
ambiguous.add(token);
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
return { renames, ambiguous };
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* For each line of `b`, the line of `a` it keeps, or -1 for one the change
|
|
123
|
+
* added: a longest common subsequence, after the shared head and tail.
|
|
124
|
+
*/
|
|
125
|
+
export function keptLines(a, b) {
|
|
126
|
+
const kept = b.map(() => -1);
|
|
127
|
+
let head = 0;
|
|
128
|
+
while (head < a.length && head < b.length && a[head] === b[head]) {
|
|
129
|
+
kept[head] = head;
|
|
130
|
+
head += 1;
|
|
131
|
+
}
|
|
132
|
+
let tail = 0;
|
|
133
|
+
while (tail < a.length - head &&
|
|
134
|
+
tail < b.length - head &&
|
|
135
|
+
a[a.length - 1 - tail] === b[b.length - 1 - tail]) {
|
|
136
|
+
kept[b.length - 1 - tail] = a.length - 1 - tail;
|
|
137
|
+
tail += 1;
|
|
138
|
+
}
|
|
139
|
+
const n = a.length - head - tail;
|
|
140
|
+
const m = b.length - head - tail;
|
|
141
|
+
if (n === 0 || m === 0 || (n + 1) * (m + 1) > MATCH_CELLS)
|
|
142
|
+
return kept;
|
|
143
|
+
const width = m + 1;
|
|
144
|
+
const length = new Int32Array((n + 1) * width);
|
|
145
|
+
for (let i = n - 1; i >= 0; i -= 1)
|
|
146
|
+
for (let j = m - 1; j >= 0; j -= 1)
|
|
147
|
+
length[i * width + j] =
|
|
148
|
+
a[head + i] === b[head + j]
|
|
149
|
+
? length[(i + 1) * width + j + 1] + 1
|
|
150
|
+
: Math.max(length[(i + 1) * width + j], length[i * width + j + 1]);
|
|
151
|
+
let i = 0;
|
|
152
|
+
let j = 0;
|
|
153
|
+
while (i < n && j < m) {
|
|
154
|
+
if (a[head + i] === b[head + j]) {
|
|
155
|
+
kept[head + j] = head + i;
|
|
156
|
+
i += 1;
|
|
157
|
+
j += 1;
|
|
158
|
+
}
|
|
159
|
+
else if (length[(i + 1) * width + j] >=
|
|
160
|
+
length[i * width + j + 1])
|
|
161
|
+
i += 1;
|
|
162
|
+
else
|
|
163
|
+
j += 1;
|
|
164
|
+
}
|
|
165
|
+
return kept;
|
|
166
|
+
}
|
|
167
|
+
const IMPORT_SPECIFIER = /(?:\bfrom\s*|\bimport\s*\(?\s*|\brequire\s*\(\s*)(["'])(\.{1,2}\/[^"']*)\1/g;
|
|
168
|
+
/** A relative specifier resolved against the importing file's folder. */
|
|
169
|
+
export function resolveRelative(importer, specifier) {
|
|
170
|
+
const parts = importer.split("/").slice(0, -1);
|
|
171
|
+
for (const part of specifier.split("/")) {
|
|
172
|
+
if (part === "" || part === ".")
|
|
173
|
+
continue;
|
|
174
|
+
if (part === "..")
|
|
175
|
+
parts.pop();
|
|
176
|
+
else
|
|
177
|
+
parts.push(part);
|
|
178
|
+
}
|
|
179
|
+
return parts.join("/");
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* The change carried to the other side, file by file, with what cannot be
|
|
183
|
+
* carried refused. A refused file is left out of `files`.
|
|
184
|
+
*/
|
|
185
|
+
export function carryChange(files, renaming) {
|
|
186
|
+
const carried = [];
|
|
187
|
+
const refused = [];
|
|
188
|
+
for (const file of files) {
|
|
189
|
+
const refuse = (reason) => refused.push({ path: file.fromPath, reason });
|
|
190
|
+
if (file.edited === file.fromOriginal)
|
|
191
|
+
continue;
|
|
192
|
+
const from = linesOf(file.fromOriginal ?? "");
|
|
193
|
+
const to = linesOf(file.toOriginal ?? "");
|
|
194
|
+
if (from.length !== to.length) {
|
|
195
|
+
refuse("The version's copy of this file does not line up with the repository's, so the change cannot be carried.");
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
const { renames, ambiguous } = learnedRenames(from, to);
|
|
199
|
+
const back = new Map([...renames].map(([a, b]) => [b, a]));
|
|
200
|
+
const edited = linesOf(file.edited);
|
|
201
|
+
const kept = keptLines(from, edited);
|
|
202
|
+
const out = [];
|
|
203
|
+
const problems = new Set();
|
|
204
|
+
edited.forEach((line, index) => {
|
|
205
|
+
const keptAt = kept[index];
|
|
206
|
+
if (keptAt >= 0) {
|
|
207
|
+
out.push(to[keptAt]);
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
const tokens = tokensOf(line);
|
|
211
|
+
if (tokens.some((token) => ambiguous.has(token))) {
|
|
212
|
+
problems.add("An added line uses a name the build renamed two ways in this file.");
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
const renamed = renaming.to(tokens.map((token) => renames.get(token) ?? token).join(""), file);
|
|
216
|
+
const again = tokensOf(renaming.back(renamed, file))
|
|
217
|
+
.map((token) => back.get(token) ?? token)
|
|
218
|
+
.join("");
|
|
219
|
+
if (again !== line) {
|
|
220
|
+
problems.add("An added line holds a name the other side would read differently, so it does not map back.");
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
if (renaming.isHarness !== undefined)
|
|
224
|
+
for (const match of line.matchAll(IMPORT_SPECIFIER))
|
|
225
|
+
if (renaming.isHarness(resolveRelative(file.fromPath, match[2])))
|
|
226
|
+
problems.add(`An added line imports ${match[2]}, part of the sandbox's harness, which the repository does not have.`);
|
|
227
|
+
out.push(renamed);
|
|
228
|
+
});
|
|
229
|
+
if (problems.size > 0) {
|
|
230
|
+
for (const problem of [...problems].sort())
|
|
231
|
+
refuse(problem);
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
carried.push({ path: file.toPath, text: out.join("") });
|
|
235
|
+
}
|
|
236
|
+
return {
|
|
237
|
+
files: carried.sort((a, b) => (a.path < b.path ? -1 : 1)),
|
|
238
|
+
refused,
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
/** A file's path without its script extension, for matching an import to it. */
|
|
242
|
+
const stem = (path) => path.replace(/\.(?:[cm]?[jt]sx?|d\.ts)$/, "");
|
|
243
|
+
/**
|
|
244
|
+
* A submission, made in the version's public names, as whole files on the
|
|
245
|
+
* repository at the pinned commit: merge-back's reverse path.
|
|
246
|
+
*/
|
|
247
|
+
export function reverseSubmission(input) {
|
|
248
|
+
const { renameFile, invertAliasRules } = input.aliases ?? CURRENT_ALIASES;
|
|
249
|
+
const inverse = invertAliasRules(input.rules);
|
|
250
|
+
const repositoryPathOf = new Map(input.renames.map(({ from, to }) => [to, from]));
|
|
251
|
+
const harness = new Set(input.harness.map(stem));
|
|
252
|
+
const files = input.submitted.map((file) => {
|
|
253
|
+
const toPath = repositoryPathOf.get(file.path) ??
|
|
254
|
+
renameFile({ path: file.path, text: "" }, inverse).path;
|
|
255
|
+
return {
|
|
256
|
+
fromPath: file.path,
|
|
257
|
+
toPath,
|
|
258
|
+
fromOriginal: input.publicText(file.path),
|
|
259
|
+
toOriginal: input.repositoryText(toPath),
|
|
260
|
+
edited: file.text,
|
|
261
|
+
};
|
|
262
|
+
});
|
|
263
|
+
return carryChange(files, {
|
|
264
|
+
to: (text, file) => renameFile({ path: file.toPath, text }, inverse).text,
|
|
265
|
+
back: (text, file) => renameFile({ path: file.toPath, text }, input.rules).text,
|
|
266
|
+
isHarness: (path) => harness.has(stem(path)),
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Where a repository file is in the version's project: where the build
|
|
271
|
+
* moved it, else where the name table's path rules put it.
|
|
272
|
+
*/
|
|
273
|
+
export function projectPathOf(path, renames, rules, aliases = CURRENT_ALIASES) {
|
|
274
|
+
const { renameFile } = aliases;
|
|
275
|
+
return (renames.find((rename) => rename.from === path)?.to ??
|
|
276
|
+
renameFile({ path, text: "" }, rules).path);
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* A change made in the repository's names, as whole files on the version's
|
|
280
|
+
* project: the CI judge's forward path. Only files the version has (or its
|
|
281
|
+
* paths name) are carried; the caller keeps to the editable ones.
|
|
282
|
+
*/
|
|
283
|
+
export function forwardChange(input) {
|
|
284
|
+
const aliases = input.aliases ?? CURRENT_ALIASES;
|
|
285
|
+
const { renameFile } = aliases;
|
|
286
|
+
const inverse = aliases.invertAliasRules(input.rules);
|
|
287
|
+
const files = input.changed.map((file) => {
|
|
288
|
+
const toPath = projectPathOf(file.path, input.renames, input.rules, aliases);
|
|
289
|
+
return {
|
|
290
|
+
fromPath: file.path,
|
|
291
|
+
toPath,
|
|
292
|
+
fromOriginal: input.repositoryText(file.path),
|
|
293
|
+
toOriginal: input.publicText(toPath),
|
|
294
|
+
edited: file.text,
|
|
295
|
+
};
|
|
296
|
+
});
|
|
297
|
+
return carryChange(files, {
|
|
298
|
+
to: (text, file) => renameFile({ path: file.fromPath, text }, input.rules).text,
|
|
299
|
+
back: (text, file) => renameFile({ path: file.fromPath, text }, inverse).text,
|
|
300
|
+
});
|
|
301
|
+
}
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
* find.
|
|
24
24
|
*/
|
|
25
25
|
import { renameFile } from "./aliases.js";
|
|
26
|
-
import { relativeSpecifier } from "../project/
|
|
26
|
+
import { relativeSpecifier } from "../project/paths.js";
|
|
27
27
|
import { termMatches } from "./vocabulary.js";
|
|
28
28
|
/**
|
|
29
29
|
* Placeholders, in the order tried: a term takes the first that no input
|
|
@@ -26,7 +26,7 @@ import { existsSync, readFileSync } from "node:fs";
|
|
|
26
26
|
import { basename, join } from "node:path";
|
|
27
27
|
import { applyAliases, invertAliasRules, renameFile } from "./aliases.js";
|
|
28
28
|
import { draftPseudonyms, restyle } from "./draft.js";
|
|
29
|
-
import { emailNames, termMatches, termWords,
|
|
29
|
+
import { emailNames, scannedNames, termMatches, termWords, vocabularyLeaks, } from "./vocabulary.js";
|
|
30
30
|
/** A package.json's fields that name its owner, or null when unreadable. */
|
|
31
31
|
function manifestNames(path) {
|
|
32
32
|
if (!existsSync(path))
|
|
@@ -155,39 +155,6 @@ function hostedNames(url) {
|
|
|
155
155
|
names.push({ name: repo, kind: "name" });
|
|
156
156
|
return names;
|
|
157
157
|
}
|
|
158
|
-
/** Affixes a handle wraps a name in: `lunoxhq`, `getlunox`, `lunoxapp`. */
|
|
159
|
-
const HANDLE_PREFIXES = ["get", "try", "use", "join", "go", "hello", "meet"];
|
|
160
|
-
const HANDLE_SUFFIXES = ["hq", "app", "apps", "inc", "io", "dev", "devs", "labs", "lab", "team", "co", "ai", "hub", "official", "corp", "tech", "studio", "group"];
|
|
161
|
-
/**
|
|
162
|
-
* More spellings an organization's or a person's name goes by: the stem of
|
|
163
|
-
* a handle (`lunox` of `lunoxhq`), its words split at a digit (`web3 labs`
|
|
164
|
-
* of `web3labs`), and its letters without accents (`lunox` of `Lunöx`).
|
|
165
|
-
*/
|
|
166
|
-
function spellings(entry) {
|
|
167
|
-
const out = [entry];
|
|
168
|
-
const plain = entry.name.normalize("NFD").replace(/\p{M}/gu, "");
|
|
169
|
-
if (plain !== entry.name)
|
|
170
|
-
out.push({ ...entry, name: plain });
|
|
171
|
-
// Matched whole: its words alone (`web3`) are everyone's.
|
|
172
|
-
const split = plain.replace(/(\d)(\p{L})/gu, "$1 $2");
|
|
173
|
-
if (split !== plain)
|
|
174
|
-
out.push({ name: split, kind: "name" });
|
|
175
|
-
if (entry.kind === "organization") {
|
|
176
|
-
const word = plain.toLowerCase();
|
|
177
|
-
// A stem of letters alone, and not one everyone's code uses (`web3`).
|
|
178
|
-
const stem = (candidate) => {
|
|
179
|
-
if (candidate.length >= 4 && /^\p{L}+$/u.test(candidate) && !tooCommonAlone(candidate))
|
|
180
|
-
out.push({ name: candidate, kind: "organization" });
|
|
181
|
-
};
|
|
182
|
-
for (const prefix of HANDLE_PREFIXES)
|
|
183
|
-
if (word.startsWith(prefix))
|
|
184
|
-
stem(word.slice(prefix.length));
|
|
185
|
-
for (const suffix of HANDLE_SUFFIXES)
|
|
186
|
-
if (word.endsWith(suffix))
|
|
187
|
-
stem(word.slice(0, -suffix.length));
|
|
188
|
-
}
|
|
189
|
-
return out;
|
|
190
|
-
}
|
|
191
158
|
/** A Git setting of the checkout, or null. */
|
|
192
159
|
function gitValue(repo, args) {
|
|
193
160
|
try {
|
|
@@ -225,12 +192,7 @@ export function workspaceNames(repo, packageDirs, owner = []) {
|
|
|
225
192
|
const mail = gitValue(repo, ["config", "user.email"]);
|
|
226
193
|
if (mail !== null)
|
|
227
194
|
names.push(...emailNames(mail));
|
|
228
|
-
|
|
229
|
-
// scanned for alone; the owner's own names always are.
|
|
230
|
-
return [
|
|
231
|
-
...owner.map((name) => ({ name, kind: "organization" })),
|
|
232
|
-
...found.filter((entry) => !tooCommonAlone(entry.name)),
|
|
233
|
-
].flatMap(spellings);
|
|
195
|
+
return scannedNames(found, owner);
|
|
234
196
|
}
|
|
235
197
|
/**
|
|
236
198
|
* The names written after a name the repository goes by, the way its
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Alias rules: the private, bidirectional name table a sandbox version
|
|
3
|
+
* applies to copied source before it can leave the organization.
|
|
4
|
+
*
|
|
5
|
+
* A rule is an explicit pair with a kind and a scope. Rules are ordered,
|
|
6
|
+
* and applied in that order; they are validated as a set before any text
|
|
7
|
+
* is touched, because two rules that look harmless alone can interact (one
|
|
8
|
+
* rule's output is another's input, two rules share a target, two
|
|
9
|
+
* identifier or path names differ only by case). Application is
|
|
10
|
+
* deterministic and textual, bounded to identifier or path tokens so a name
|
|
11
|
+
* inside a longer identifier is never touched, and it is checked to be
|
|
12
|
+
* reversible: applying the inverse table to the output must give the input
|
|
13
|
+
* back byte for byte. A text rule matches its exact, case-sensitive
|
|
14
|
+
* characters and is inverted the same way, so two text rules that differ
|
|
15
|
+
* only by case are distinct and both hold. A
|
|
16
|
+
* transformation that cannot be mapped back is refused, never silently
|
|
17
|
+
* dropped. Redaction of implementation is not a rename and has no place
|
|
18
|
+
* here.
|
|
19
|
+
*/
|
|
20
|
+
export declare const ALIAS_KINDS: readonly ["identifier", "path", "text"];
|
|
21
|
+
export type AliasKind = (typeof ALIAS_KINDS)[number];
|
|
22
|
+
export interface AliasRule {
|
|
23
|
+
readonly before: string;
|
|
24
|
+
readonly after: string;
|
|
25
|
+
readonly kind: AliasKind;
|
|
26
|
+
/**
|
|
27
|
+
* Repository paths the rule applies to: a file, or a directory prefix.
|
|
28
|
+
* Empty means every file. Scoped rules let one name mean different
|
|
29
|
+
* things in different modules without a symbol table.
|
|
30
|
+
*/
|
|
31
|
+
readonly paths: readonly string[];
|
|
32
|
+
}
|
|
33
|
+
export declare const ALIAS_PROBLEM_CODES: readonly ["empty", "identity", "invalid_identifier", "invalid_path", "duplicate_source", "duplicate_target", "chained", "casing_variant", "target_collision", "irreversible", "too_many", "unsupported_kind"];
|
|
34
|
+
export type AliasProblemCode = (typeof ALIAS_PROBLEM_CODES)[number];
|
|
35
|
+
export interface AliasProblem {
|
|
36
|
+
readonly code: AliasProblemCode;
|
|
37
|
+
/** Index into the rule list; null for a problem of the whole set. */
|
|
38
|
+
readonly rule: number | null;
|
|
39
|
+
readonly file: string | null;
|
|
40
|
+
readonly detail: string;
|
|
41
|
+
}
|
|
42
|
+
export declare const ALIAS_RULES_MAX = 500;
|
|
43
|
+
export interface AliasValidation {
|
|
44
|
+
readonly ok: boolean;
|
|
45
|
+
readonly problems: readonly AliasProblem[];
|
|
46
|
+
}
|
|
47
|
+
/** Whether the set is well formed and free of interactions. */
|
|
48
|
+
export declare function validateAliasRules(rules: readonly AliasRule[]): AliasValidation;
|
|
49
|
+
export interface AliasedFile {
|
|
50
|
+
readonly path: string;
|
|
51
|
+
readonly text: string;
|
|
52
|
+
}
|
|
53
|
+
export interface AliasApplication {
|
|
54
|
+
readonly ok: boolean;
|
|
55
|
+
readonly files: readonly AliasedFile[];
|
|
56
|
+
/** Original path to aliased path, for every file whose path changed. */
|
|
57
|
+
readonly renames: readonly {
|
|
58
|
+
readonly from: string;
|
|
59
|
+
readonly to: string;
|
|
60
|
+
}[];
|
|
61
|
+
readonly problems: readonly AliasProblem[];
|
|
62
|
+
/** Rule index to the number of replacements it made, for the record. */
|
|
63
|
+
readonly applied: readonly number[];
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Applies the rules to the files and proves the result maps back. Problems
|
|
67
|
+
* refuse the whole application: a partially aliased candidate is worse
|
|
68
|
+
* than none, because it looks finished.
|
|
69
|
+
*/
|
|
70
|
+
export declare function applyAliases(files: readonly AliasedFile[], rules: readonly AliasRule[]): AliasApplication;
|
|
71
|
+
/**
|
|
72
|
+
* A JSON value's string leaves, each as a file of its own, for aliasing a
|
|
73
|
+
* structure without touching its shape.
|
|
74
|
+
*
|
|
75
|
+
* Aliasing a serialized object as one text renames its keys too: a quote is
|
|
76
|
+
* a word boundary, so a rule `name → label` turned `"name":` into
|
|
77
|
+
* `"label":` and the object lost the field. Leaves alone keep every key,
|
|
78
|
+
* number and boolean as they were. Each is named `base#pointer`, and
|
|
79
|
+
* `withJsonLeaves` puts them back in the same order.
|
|
80
|
+
*/
|
|
81
|
+
export declare function jsonLeaves(base: string, value: unknown): AliasedFile[];
|
|
82
|
+
/**
|
|
83
|
+
* `value` with its string leaves replaced by `texts`, in `jsonLeaves`'s
|
|
84
|
+
* order; a leaf with no text left keeps its own. A fresh value: the one
|
|
85
|
+
* given is not changed.
|
|
86
|
+
*/
|
|
87
|
+
export declare function withJsonLeaves<T>(value: T, texts: readonly string[]): T;
|
|
88
|
+
/**
|
|
89
|
+
* The name a build gives its alias table among the private files it writes:
|
|
90
|
+
* `private/pseudonym.lunox`, beside the hidden tests.
|
|
91
|
+
*/
|
|
92
|
+
export declare const PSEUDONYMS_FILE = "pseudonym.lunox";
|
|
93
|
+
export declare const PSEUDONYMS_SCHEMA_VERSION = 1;
|
|
94
|
+
/** `pseudonym.lunox`: the table a version's public files were renamed by. */
|
|
95
|
+
export interface PseudonymTable {
|
|
96
|
+
readonly schemaVersion: typeof PSEUDONYMS_SCHEMA_VERSION;
|
|
97
|
+
readonly rules: readonly AliasRule[];
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* One file renamed by the rules, without the checks `applyAliases` makes:
|
|
101
|
+
* for reading back a file already proved to map back, such as a public
|
|
102
|
+
* file in its private names under the inverse table. `file.path` is its
|
|
103
|
+
* repository path, which scoped rules are matched against.
|
|
104
|
+
*/
|
|
105
|
+
export declare function renameFile(file: AliasedFile, rules: readonly AliasRule[]): AliasedFile;
|
|
106
|
+
/** The inverse table, as a contribution's import applies it. */
|
|
107
|
+
export declare function invertAliasRules(rules: readonly AliasRule[]): AliasRule[];
|