carveout 0.1.1 → 0.2.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/AGENTS.md +21 -3
- package/NOTICE.md +2 -1
- package/README.md +248 -473
- package/dist/src/account.d.ts +37 -0
- package/dist/src/account.js +275 -0
- package/dist/src/api.d.ts +162 -0
- package/dist/src/api.js +134 -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 +518 -0
- package/skills/carveout-gate/SKILL.md +98 -0
- package/SKILL.md +0 -423
|
@@ -0,0 +1,394 @@
|
|
|
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 const ALIAS_KINDS = ["identifier", "path", "text"];
|
|
21
|
+
export const ALIAS_PROBLEM_CODES = [
|
|
22
|
+
"empty",
|
|
23
|
+
"identity",
|
|
24
|
+
"invalid_identifier",
|
|
25
|
+
"invalid_path",
|
|
26
|
+
"duplicate_source",
|
|
27
|
+
"duplicate_target",
|
|
28
|
+
"chained",
|
|
29
|
+
"casing_variant",
|
|
30
|
+
"target_collision",
|
|
31
|
+
"irreversible",
|
|
32
|
+
"too_many",
|
|
33
|
+
"unsupported_kind",
|
|
34
|
+
];
|
|
35
|
+
export const ALIAS_RULES_MAX = 500;
|
|
36
|
+
const IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
37
|
+
// Any character a file name may hold but a NUL, a backslash or a quote:
|
|
38
|
+
// a framework's `app/[lunoxId]` or an `assets/Lunox Logo.png` is a path.
|
|
39
|
+
const PATH = /^[^\0\\"'`\n]+$/;
|
|
40
|
+
/** Whether the set is well formed and free of interactions. */
|
|
41
|
+
export function validateAliasRules(rules) {
|
|
42
|
+
const problems = [];
|
|
43
|
+
if (rules.length > ALIAS_RULES_MAX)
|
|
44
|
+
problems.push({
|
|
45
|
+
code: "too_many",
|
|
46
|
+
rule: null,
|
|
47
|
+
file: null,
|
|
48
|
+
detail: `${rules.length} rules exceed the limit of ${ALIAS_RULES_MAX}.`,
|
|
49
|
+
});
|
|
50
|
+
const sources = new Map();
|
|
51
|
+
const targets = new Map();
|
|
52
|
+
const folded = new Map();
|
|
53
|
+
rules.forEach((rule, index) => {
|
|
54
|
+
if (!ALIAS_KINDS.includes(rule.kind)) {
|
|
55
|
+
problems.push({
|
|
56
|
+
code: "unsupported_kind",
|
|
57
|
+
rule: index,
|
|
58
|
+
file: null,
|
|
59
|
+
detail: `Kind ${String(rule.kind)} is not supported.`,
|
|
60
|
+
});
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
if (rule.before === "" || rule.after === "") {
|
|
64
|
+
problems.push({
|
|
65
|
+
code: "empty",
|
|
66
|
+
rule: index,
|
|
67
|
+
file: null,
|
|
68
|
+
detail: "Both sides of a rule must be non-empty.",
|
|
69
|
+
});
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
if (rule.before === rule.after)
|
|
73
|
+
problems.push({
|
|
74
|
+
code: "identity",
|
|
75
|
+
rule: index,
|
|
76
|
+
file: null,
|
|
77
|
+
detail: `${rule.before} maps to itself.`,
|
|
78
|
+
});
|
|
79
|
+
if (rule.kind === "identifier" &&
|
|
80
|
+
(!IDENTIFIER.test(rule.before) || !IDENTIFIER.test(rule.after)))
|
|
81
|
+
problems.push({
|
|
82
|
+
code: "invalid_identifier",
|
|
83
|
+
rule: index,
|
|
84
|
+
file: null,
|
|
85
|
+
detail: `${rule.before} → ${rule.after} is not an identifier pair.`,
|
|
86
|
+
});
|
|
87
|
+
if (rule.kind === "path" &&
|
|
88
|
+
(!PATH.test(rule.before) ||
|
|
89
|
+
!PATH.test(rule.after) ||
|
|
90
|
+
rule.before.startsWith("/") ||
|
|
91
|
+
rule.after.startsWith("/") ||
|
|
92
|
+
rule.before.split("/").includes("..") ||
|
|
93
|
+
rule.after.split("/").includes("..")))
|
|
94
|
+
problems.push({
|
|
95
|
+
code: "invalid_path",
|
|
96
|
+
rule: index,
|
|
97
|
+
file: null,
|
|
98
|
+
detail: `${rule.before} → ${rule.after} is not a repository path pair.`,
|
|
99
|
+
});
|
|
100
|
+
const scope = rule.paths.length === 0 ? "" : [...rule.paths].sort().join("\n");
|
|
101
|
+
const sourceKey = `${rule.kind}\n${scope}\n${rule.before}`;
|
|
102
|
+
const targetKey = `${rule.kind}\n${scope}\n${rule.after}`;
|
|
103
|
+
const earlier = sources.get(sourceKey);
|
|
104
|
+
if (earlier !== undefined)
|
|
105
|
+
problems.push({
|
|
106
|
+
code: "duplicate_source",
|
|
107
|
+
rule: index,
|
|
108
|
+
file: null,
|
|
109
|
+
detail: `${rule.before} is already renamed by rule ${earlier}.`,
|
|
110
|
+
});
|
|
111
|
+
else
|
|
112
|
+
sources.set(sourceKey, index);
|
|
113
|
+
const shared = targets.get(targetKey);
|
|
114
|
+
if (shared !== undefined)
|
|
115
|
+
problems.push({
|
|
116
|
+
code: "duplicate_target",
|
|
117
|
+
rule: index,
|
|
118
|
+
file: null,
|
|
119
|
+
detail: `${rule.after} is already the target of rule ${shared}.`,
|
|
120
|
+
});
|
|
121
|
+
else
|
|
122
|
+
targets.set(targetKey, index);
|
|
123
|
+
// Identifier and path names that differ only by case are refused. Text
|
|
124
|
+
// is exempt: a text rule is applied with an exact, case-sensitive
|
|
125
|
+
// pattern and inverted exactly, so ` lunox ` → ` acme ` beside
|
|
126
|
+
// ` LUNOX ` → ` ACME ` is unambiguous both ways, and it is how a
|
|
127
|
+
// spelling no identifier rule can reach is covered.
|
|
128
|
+
if (rule.kind === "text")
|
|
129
|
+
return;
|
|
130
|
+
for (const [name, side] of [
|
|
131
|
+
[rule.before, "source"],
|
|
132
|
+
[rule.after, "target"],
|
|
133
|
+
]) {
|
|
134
|
+
const key = `${rule.kind}\n${name.toLowerCase()}`;
|
|
135
|
+
const seen = folded.get(key);
|
|
136
|
+
if (seen !== undefined && seen !== index) {
|
|
137
|
+
const other = rules[seen];
|
|
138
|
+
if (other !== undefined &&
|
|
139
|
+
other.before !== name &&
|
|
140
|
+
other.after !== name)
|
|
141
|
+
problems.push({
|
|
142
|
+
code: "casing_variant",
|
|
143
|
+
rule: index,
|
|
144
|
+
file: null,
|
|
145
|
+
detail: `${name} (${side}) differs only by case from a name in rule ${seen}.`,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
else
|
|
149
|
+
folded.set(key, index);
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
// A target that is another rule's source would be renamed again by a
|
|
153
|
+
// later rule, or would make the inverse table ambiguous.
|
|
154
|
+
rules.forEach((rule, index) => {
|
|
155
|
+
rules.forEach((other, otherIndex) => {
|
|
156
|
+
if (index !== otherIndex &&
|
|
157
|
+
rule.kind === other.kind &&
|
|
158
|
+
rule.after === other.before &&
|
|
159
|
+
scopesOverlap(rule.paths, other.paths))
|
|
160
|
+
problems.push({
|
|
161
|
+
code: "chained",
|
|
162
|
+
rule: index,
|
|
163
|
+
file: null,
|
|
164
|
+
detail: `${rule.after} is produced by rule ${index} and consumed by rule ${otherIndex}.`,
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
return { ok: problems.length === 0, problems: dedupe(problems) };
|
|
169
|
+
}
|
|
170
|
+
function dedupe(problems) {
|
|
171
|
+
const seen = new Set();
|
|
172
|
+
const result = [];
|
|
173
|
+
for (const problem of problems) {
|
|
174
|
+
const key = `${problem.code}\n${problem.rule ?? ""}\n${problem.file ?? ""}\n${problem.detail}`;
|
|
175
|
+
if (seen.has(key))
|
|
176
|
+
continue;
|
|
177
|
+
seen.add(key);
|
|
178
|
+
result.push(problem);
|
|
179
|
+
}
|
|
180
|
+
return result;
|
|
181
|
+
}
|
|
182
|
+
function inScope(path, scopes) {
|
|
183
|
+
if (scopes.length === 0)
|
|
184
|
+
return true;
|
|
185
|
+
return scopes.some((scope) => path === scope || path.startsWith(`${scope.replace(/\/$/, "")}/`));
|
|
186
|
+
}
|
|
187
|
+
function scopesOverlap(a, b) {
|
|
188
|
+
if (a.length === 0 || b.length === 0)
|
|
189
|
+
return true;
|
|
190
|
+
return a.some((x) => b.some((y) => inScope(x, [y]) || inScope(y, [x])));
|
|
191
|
+
}
|
|
192
|
+
const escape = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
193
|
+
const identifierPattern = (name) => new RegExp(`(?<![A-Za-z0-9_$])${escape(name)}(?![A-Za-z0-9_$])`, "g");
|
|
194
|
+
// A path's rule matches after a `/` too: `../../packages/lunox-money/x`
|
|
195
|
+
// is the same folder, from another file.
|
|
196
|
+
const pathPattern = (name) => new RegExp(`(?<![A-Za-z0-9_.\\-@])${escape(name)}(?![A-Za-z0-9_\\-@])`, "g");
|
|
197
|
+
const textPattern = (name) => new RegExp(escape(name), "g");
|
|
198
|
+
const patternFor = (rule, name) => rule.kind === "identifier"
|
|
199
|
+
? identifierPattern(name)
|
|
200
|
+
: rule.kind === "path"
|
|
201
|
+
? pathPattern(name)
|
|
202
|
+
: textPattern(name);
|
|
203
|
+
/**
|
|
204
|
+
* A path renamed by the table, renamed back: the path rules undone in
|
|
205
|
+
* reverse order, each scoped by the original path as the forward pass was.
|
|
206
|
+
*/
|
|
207
|
+
function invertPath(path, original, reversed) {
|
|
208
|
+
let back = path;
|
|
209
|
+
for (const rule of reversed)
|
|
210
|
+
if (rule.kind === "path" && inScope(original, rule.paths))
|
|
211
|
+
back = back.replace(patternFor(rule, rule.after), rule.before);
|
|
212
|
+
return back;
|
|
213
|
+
}
|
|
214
|
+
function rewrite(files, rules, direction, counts, problems) {
|
|
215
|
+
const renames = [];
|
|
216
|
+
const result = files.map((file) => {
|
|
217
|
+
let text = file.text;
|
|
218
|
+
let path = file.path;
|
|
219
|
+
rules.forEach((rule, index) => {
|
|
220
|
+
if (!inScope(file.path, rule.paths))
|
|
221
|
+
return;
|
|
222
|
+
const from = direction === "forward" ? rule.before : rule.after;
|
|
223
|
+
const to = direction === "forward" ? rule.after : rule.before;
|
|
224
|
+
if (problems !== null) {
|
|
225
|
+
// The target must not already occur in the file as it was, or the
|
|
226
|
+
// inverse table could not tell a renamed occurrence from a natural
|
|
227
|
+
// one. An occurrence an earlier rule made is not natural: two rules
|
|
228
|
+
// that give one term the same placeholder (a path and an
|
|
229
|
+
// identifier) make it on purpose, and the round trip below proves
|
|
230
|
+
// the table maps back.
|
|
231
|
+
const existing = file.text.match(patternFor(rule, to));
|
|
232
|
+
if (existing !== null && existing.length > 0)
|
|
233
|
+
problems.push({
|
|
234
|
+
code: "target_collision",
|
|
235
|
+
rule: index,
|
|
236
|
+
file: file.path,
|
|
237
|
+
detail: `${to} already occurs ${existing.length} time(s) in ${file.path}.`,
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
const pattern = patternFor(rule, from);
|
|
241
|
+
let made = 0;
|
|
242
|
+
text = text.replace(pattern, () => {
|
|
243
|
+
made += 1;
|
|
244
|
+
return to;
|
|
245
|
+
});
|
|
246
|
+
if (counts !== null)
|
|
247
|
+
counts[index] = (counts[index] ?? 0) + made;
|
|
248
|
+
if (rule.kind === "path") {
|
|
249
|
+
const renamed = path.replace(patternFor(rule, from), to);
|
|
250
|
+
if (renamed !== path) {
|
|
251
|
+
path = renamed;
|
|
252
|
+
if (counts !== null)
|
|
253
|
+
counts[index] = (counts[index] ?? 0) + 1;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
if (path !== file.path)
|
|
258
|
+
renames.push({ from: file.path, to: path });
|
|
259
|
+
return { path, text };
|
|
260
|
+
});
|
|
261
|
+
return { files: result, renames };
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Applies the rules to the files and proves the result maps back. Problems
|
|
265
|
+
* refuse the whole application: a partially aliased candidate is worse
|
|
266
|
+
* than none, because it looks finished.
|
|
267
|
+
*/
|
|
268
|
+
export function applyAliases(files, rules) {
|
|
269
|
+
const validation = validateAliasRules(rules);
|
|
270
|
+
if (!validation.ok)
|
|
271
|
+
return {
|
|
272
|
+
ok: false,
|
|
273
|
+
files: [],
|
|
274
|
+
renames: [],
|
|
275
|
+
problems: validation.problems,
|
|
276
|
+
applied: [],
|
|
277
|
+
};
|
|
278
|
+
const problems = [];
|
|
279
|
+
const counts = rules.map(() => 0);
|
|
280
|
+
const forward = rewrite(files, rules, "forward", counts, problems);
|
|
281
|
+
// The inverse walks the rules backwards, undoing the last rename first.
|
|
282
|
+
const reversed = [...rules].reverse();
|
|
283
|
+
const inverse = rewrite(forward.files.map((file, index) => ({
|
|
284
|
+
// Scope checks use the original path: the inverse must see what the
|
|
285
|
+
// forward pass saw.
|
|
286
|
+
path: files[index]?.path ?? file.path,
|
|
287
|
+
text: file.text,
|
|
288
|
+
})), reversed, "inverse", null, null);
|
|
289
|
+
files.forEach((file, index) => {
|
|
290
|
+
const back = inverse.files[index];
|
|
291
|
+
if (back === undefined || back.text !== file.text)
|
|
292
|
+
problems.push({
|
|
293
|
+
code: "irreversible",
|
|
294
|
+
rule: null,
|
|
295
|
+
file: file.path,
|
|
296
|
+
detail: `Applying the inverse table to ${file.path} does not give the original back.`,
|
|
297
|
+
});
|
|
298
|
+
// Paths too: with `src → lib` beside a `lib/` the repository already
|
|
299
|
+
// has, `lib/x.ts` would map back to `src/x.ts`, and the table alone
|
|
300
|
+
// could not say where a public file came from.
|
|
301
|
+
const aliased = forward.files[index]?.path ?? file.path;
|
|
302
|
+
if (invertPath(aliased, file.path, reversed) !== file.path)
|
|
303
|
+
problems.push({
|
|
304
|
+
code: "irreversible",
|
|
305
|
+
rule: null,
|
|
306
|
+
file: file.path,
|
|
307
|
+
detail: `${aliased} does not map back to ${file.path}.`,
|
|
308
|
+
});
|
|
309
|
+
});
|
|
310
|
+
const renamedPaths = new Set(forward.files.map((file) => file.path));
|
|
311
|
+
if (renamedPaths.size !== files.length)
|
|
312
|
+
problems.push({
|
|
313
|
+
code: "target_collision",
|
|
314
|
+
rule: null,
|
|
315
|
+
file: null,
|
|
316
|
+
detail: "Two files would share one aliased path.",
|
|
317
|
+
});
|
|
318
|
+
const unique = dedupe(problems);
|
|
319
|
+
return unique.length > 0
|
|
320
|
+
? { ok: false, files: [], renames: [], problems: unique, applied: counts }
|
|
321
|
+
: {
|
|
322
|
+
ok: true,
|
|
323
|
+
files: forward.files,
|
|
324
|
+
renames: forward.renames,
|
|
325
|
+
problems: [],
|
|
326
|
+
applied: counts,
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* A JSON value's string leaves, each as a file of its own, for aliasing a
|
|
331
|
+
* structure without touching its shape.
|
|
332
|
+
*
|
|
333
|
+
* Aliasing a serialized object as one text renames its keys too: a quote is
|
|
334
|
+
* a word boundary, so a rule `name → label` turned `"name":` into
|
|
335
|
+
* `"label":` and the object lost the field. Leaves alone keep every key,
|
|
336
|
+
* number and boolean as they were. Each is named `base#pointer`, and
|
|
337
|
+
* `withJsonLeaves` puts them back in the same order.
|
|
338
|
+
*/
|
|
339
|
+
export function jsonLeaves(base, value) {
|
|
340
|
+
const leaves = [];
|
|
341
|
+
const walk = (node, pointer) => {
|
|
342
|
+
if (typeof node === "string") {
|
|
343
|
+
leaves.push({ path: `${base}#${pointer}`, text: node });
|
|
344
|
+
}
|
|
345
|
+
else if (Array.isArray(node)) {
|
|
346
|
+
node.forEach((item, index) => walk(item, `${pointer}/${index}`));
|
|
347
|
+
}
|
|
348
|
+
else if (node !== null && typeof node === "object") {
|
|
349
|
+
for (const [key, item] of Object.entries(node))
|
|
350
|
+
walk(item, `${pointer}/${key.replace(/~/g, "~0").replace(/\//g, "~1")}`);
|
|
351
|
+
}
|
|
352
|
+
};
|
|
353
|
+
walk(value, "");
|
|
354
|
+
return leaves;
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* `value` with its string leaves replaced by `texts`, in `jsonLeaves`'s
|
|
358
|
+
* order; a leaf with no text left keeps its own. A fresh value: the one
|
|
359
|
+
* given is not changed.
|
|
360
|
+
*/
|
|
361
|
+
export function withJsonLeaves(value, texts) {
|
|
362
|
+
let at = 0;
|
|
363
|
+
const walk = (node) => {
|
|
364
|
+
if (typeof node === "string")
|
|
365
|
+
return texts[at++] ?? node;
|
|
366
|
+
if (Array.isArray(node))
|
|
367
|
+
return node.map(walk);
|
|
368
|
+
if (node !== null && typeof node === "object")
|
|
369
|
+
return Object.fromEntries(Object.entries(node).map(([key, item]) => [key, walk(item)]));
|
|
370
|
+
return node;
|
|
371
|
+
};
|
|
372
|
+
return walk(value);
|
|
373
|
+
}
|
|
374
|
+
/**
|
|
375
|
+
* The name a build gives its alias table among the private files it writes:
|
|
376
|
+
* `private/pseudonym.lunox`, beside the hidden tests.
|
|
377
|
+
*/
|
|
378
|
+
export const PSEUDONYMS_FILE = "pseudonym.lunox";
|
|
379
|
+
export const PSEUDONYMS_SCHEMA_VERSION = 1;
|
|
380
|
+
/**
|
|
381
|
+
* One file renamed by the rules, without the checks `applyAliases` makes:
|
|
382
|
+
* for reading back a file already proved to map back, such as a public
|
|
383
|
+
* file in its private names under the inverse table. `file.path` is its
|
|
384
|
+
* repository path, which scoped rules are matched against.
|
|
385
|
+
*/
|
|
386
|
+
export function renameFile(file, rules) {
|
|
387
|
+
return rewrite([file], rules, "forward", null, null).files[0] ?? file;
|
|
388
|
+
}
|
|
389
|
+
/** The inverse table, as a contribution's import applies it. */
|
|
390
|
+
export function invertAliasRules(rules) {
|
|
391
|
+
return [...rules]
|
|
392
|
+
.reverse()
|
|
393
|
+
.map((rule) => ({ ...rule, before: rule.after, after: rule.before }));
|
|
394
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pseudonyms drafted by rule: the name table a sliced version needs to keep
|
|
3
|
+
* the workspace's vocabulary out of its public files, written without a
|
|
4
|
+
* model wherever the vocabulary is where rules can find it.
|
|
5
|
+
*
|
|
6
|
+
* An import of one of the workspace's own packages (`@acme/db`) resolves to
|
|
7
|
+
* a module the slice copies or stubs, so it becomes the relative path the
|
|
8
|
+
* generator would give it, scoped to the files that import it: a renamed
|
|
9
|
+
* package would no longer resolve. Every other occurrence of a vocabulary
|
|
10
|
+
* term becomes a placeholder of the same case, one placeholder per term
|
|
11
|
+
* that no input already uses: a path segment by a path rule, an identifier
|
|
12
|
+
* that contains it renamed whole (`acmeOrders` becomes `globexOrders`), and
|
|
13
|
+
* anything else as text, held in place by the characters around it. One
|
|
14
|
+
* identifier rule covers each spelling up to case; a second spelling that
|
|
15
|
+
* differs from it only by case (`LUNOX` beside `Lunox`) is text too, and
|
|
16
|
+
* several casings may each get a text rule of their own, since a text rule
|
|
17
|
+
* matches and inverts its exact characters.
|
|
18
|
+
*
|
|
19
|
+
* The draft is a candidate, not a guarantee: the caller checks it as the
|
|
20
|
+
* build applies it, and one that does not hold is where the pseudonyms agent
|
|
21
|
+
* starts. Only the vocabulary is covered; a name that says whose code it is
|
|
22
|
+
* without being in it (a customer, a product's feature) is the agent's to
|
|
23
|
+
* find.
|
|
24
|
+
*/
|
|
25
|
+
import type { AliasRule, AliasedFile } from "./aliases.js";
|
|
26
|
+
/** An import the slice resolved: from a file, as written, to a module. */
|
|
27
|
+
export interface PseudonymImport {
|
|
28
|
+
readonly from: string;
|
|
29
|
+
readonly specifier: string;
|
|
30
|
+
readonly to: string;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Placeholders, in the order tried: a term takes the first that no input
|
|
34
|
+
* contains, in any case.
|
|
35
|
+
*/
|
|
36
|
+
export declare const PSEUDONYM_PLACEHOLDERS: readonly string[];
|
|
37
|
+
/** A term's placeholder as words: its base, and a tail word for each word more the term has. */
|
|
38
|
+
export declare function placeholderWords(term: string, base: string): string[];
|
|
39
|
+
/**
|
|
40
|
+
* `words` spelled as `surface` spells its own: each word in the case of the
|
|
41
|
+
* surface's word in its place, joined as they are (`contosoLabs` for
|
|
42
|
+
* `sandboxFactory`, `CONTOSO_LABS` for `SANDBOX_FACTORY`, `contoso-labs`
|
|
43
|
+
* for `sandbox-factory`). A surface whose words run together with no mark
|
|
44
|
+
* between them (`sandboxfactory`) gets the words run together too.
|
|
45
|
+
*/
|
|
46
|
+
export declare function restyle(words: readonly string[], surface: string): string;
|
|
47
|
+
export declare function draftPseudonyms(input: {
|
|
48
|
+
/** Everything the table is applied to, as `applyAliases` is given it. */
|
|
49
|
+
readonly files: readonly AliasedFile[];
|
|
50
|
+
readonly vocabulary: readonly string[];
|
|
51
|
+
/** The slice's resolved imports, inside it and across its cuts. */
|
|
52
|
+
readonly imports: readonly PseudonymImport[];
|
|
53
|
+
/**
|
|
54
|
+
* Filled with the placeholder each term was given: how a name the
|
|
55
|
+
* folder's holder writes in the placeholder's spelling maps back.
|
|
56
|
+
*/
|
|
57
|
+
readonly placeholders?: Map<string, string>;
|
|
58
|
+
/**
|
|
59
|
+
* The placeholder each term had in an earlier cut of the repository, by
|
|
60
|
+
* its base word (`zorvex` of `zorvex-labs`): kept where it still fits, so
|
|
61
|
+
* a folder cut again (wider, or later) reads as the one before did.
|
|
62
|
+
*/
|
|
63
|
+
readonly preferred?: ReadonlyMap<string, string>;
|
|
64
|
+
}): AliasRule[];
|