craftar 0.0.6 → 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/README.md +4 -3
- package/dist/cli.js +303 -12
- package/dist/cli.js.map +1 -1
- package/dist/core/diff.js +121 -4
- package/dist/core/diff.js.map +1 -1
- package/dist/core/forge.js +85 -0
- package/dist/core/forge.js.map +1 -1
- package/dist/core/unify.js +472 -0
- package/dist/core/unify.js.map +1 -0
- package/dist/core/variants.js +29 -18
- package/dist/core/variants.js.map +1 -1
- package/dist/schema/index.js +27 -0
- package/dist/schema/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,472 @@
|
|
|
1
|
+
import { promises as fs } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { isDeepStrictEqual } from "node:util";
|
|
4
|
+
import YAML from "yaml";
|
|
5
|
+
import { fingerprintDir } from "./fingerprint.js";
|
|
6
|
+
import { exists, listFiles, loadForge, readIngredientText } from "./forge.js";
|
|
7
|
+
import { splitLines } from "./diff.js";
|
|
8
|
+
import { detectEol, withEol } from "./text.js";
|
|
9
|
+
// Spelled out rather than embedded as a raw character: in the one function whose job is byte
|
|
10
|
+
// fidelity, correctness should not hinge on a glyph no diff viewer, editor or re-encoding shows.
|
|
11
|
+
const BOM = String.fromCharCode(0xfeff);
|
|
12
|
+
/** Where a hunk sits, worded exactly as `forge diff` prints it. */
|
|
13
|
+
export function hunkAt(h) {
|
|
14
|
+
return h.a.lines.length ? `lines ${h.a.start}–${h.a.start + h.a.lines.length - 1}` : `after line ${h.a.start - 1}`;
|
|
15
|
+
}
|
|
16
|
+
const strictUtf8 = new TextDecoder("utf-8", { fatal: true });
|
|
17
|
+
/** Valid UTF-8, BOM included (a BOM is a valid UTF-8 sequence). */
|
|
18
|
+
function isUtf8(bytes) {
|
|
19
|
+
try {
|
|
20
|
+
strictUtf8.decode(bytes);
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
/** Every file of an ingredient but `ingredient.yaml`, as the raw bytes on disk. */
|
|
28
|
+
async function rawFilesOf(ing) {
|
|
29
|
+
const out = new Map();
|
|
30
|
+
for (const rel of await listFiles(ing.dir)) {
|
|
31
|
+
if (rel === "ingredient.yaml")
|
|
32
|
+
continue;
|
|
33
|
+
out.set(rel, await fs.readFile(path.join(ing.dir, rel)));
|
|
34
|
+
}
|
|
35
|
+
return out;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Ruling 31: the merge engine is text-only, and `diffIngredients` decodes every file as UTF-8 —
|
|
39
|
+
* which folds each invalid byte to U+FFFD, so two different non-UTF-8 files can look identical to
|
|
40
|
+
* it (and to every decision built on it). Compare the raw bytes of every file present on both
|
|
41
|
+
* sides instead; when they differ and either side is not valid UTF-8, refuse by name rather than
|
|
42
|
+
* merge or drop what the text view cannot see. `diffIngredients` itself is left alone: it also
|
|
43
|
+
* serves `forge diff` and `forge variants`.
|
|
44
|
+
*/
|
|
45
|
+
export async function assertTextMergeable(base, variant) {
|
|
46
|
+
const a = await rawFilesOf(base);
|
|
47
|
+
const b = await rawFilesOf(variant);
|
|
48
|
+
for (const [rel, bytes] of a) {
|
|
49
|
+
const other = b.get(rel);
|
|
50
|
+
if (other === undefined || bytes.equals(other))
|
|
51
|
+
continue;
|
|
52
|
+
const bad = [!isUtf8(bytes) && "base", !isUtf8(other) && "variant"].filter(Boolean).join(" and ");
|
|
53
|
+
if (bad) {
|
|
54
|
+
throw new Error(`unify: "${rel}" differs between the base and the variant and is not valid UTF-8 in the ${bad} — ` +
|
|
55
|
+
`the merge engine is text-only and cannot merge it; resolve it by hand.`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/** A plan with every decision deferred. The human edits it; nothing is assumed. */
|
|
60
|
+
export async function planFrom(base, variant, diff, profile) {
|
|
61
|
+
await assertTextMergeable(base, variant);
|
|
62
|
+
const files = [];
|
|
63
|
+
for (const f of diff.files) {
|
|
64
|
+
files.push({ file: f.file, hunks: f.hunks.map((h, i) => ({ hunk: i + 1, at: hunkAt(h), take: "keep" })) });
|
|
65
|
+
}
|
|
66
|
+
for (const file of diff.onlyInBase)
|
|
67
|
+
files.push({ file, onlyIn: "base", take: "keep" });
|
|
68
|
+
for (const file of diff.onlyInVariant)
|
|
69
|
+
files.push({ file, onlyIn: "variant", take: "keep" });
|
|
70
|
+
return {
|
|
71
|
+
schema: 1,
|
|
72
|
+
base: base.ref,
|
|
73
|
+
profile,
|
|
74
|
+
variant: variant.ref,
|
|
75
|
+
baseFingerprint: await fingerprintDir(base.dir),
|
|
76
|
+
variantFingerprint: await fingerprintDir(variant.dir),
|
|
77
|
+
files,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
/** Metadata the two sides may legitimately disagree on: identity and provenance, not behaviour. */
|
|
81
|
+
function comparableMeta(meta) {
|
|
82
|
+
const rest = { ...meta };
|
|
83
|
+
delete rest.name;
|
|
84
|
+
delete rest.as;
|
|
85
|
+
delete rest.origin;
|
|
86
|
+
return rest;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Ruling 28: the top-level `ingredient.yaml` fields on which base and variant differ, sorted.
|
|
90
|
+
* `diffIngredients` leaves `ingredient.yaml` out, so without this a variant that differs only in
|
|
91
|
+
* its metadata (an MCP `server`, an agent's `model` or `tools`) would resolve with zero decisions
|
|
92
|
+
* and be deleted. Compared on the zod-validated `meta` with a real deep equality — never through
|
|
93
|
+
* `fingerprintOf` or `JSON.stringify(x, keys)`, whose key-array replacer filters at every depth
|
|
94
|
+
* and would see two different nested `server` objects as the same `{}`.
|
|
95
|
+
*/
|
|
96
|
+
export function metaDifferences(base, variant) {
|
|
97
|
+
const a = comparableMeta(base.meta);
|
|
98
|
+
const b = comparableMeta(variant.meta);
|
|
99
|
+
return [...new Set([...Object.keys(a), ...Object.keys(b)])].filter((k) => !isDeepStrictEqual(a[k], b[k])).sort();
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Rebuild one file by walking the base's lines and swapping in the variant's where chosen.
|
|
103
|
+
*
|
|
104
|
+
* Two things the base decides no matter what wins a hunk: its BOM and its line endings — the
|
|
105
|
+
* base is what survives, so both are read off `baseText` and restored around the merge, the same
|
|
106
|
+
* way `src/emitters/shared.ts` re-prepends a BOM onto a file it replaces.
|
|
107
|
+
*
|
|
108
|
+
* The final newline is the other subtlety: it comes from whichever side won the *last* hunk, and
|
|
109
|
+
* only when that hunk actually reaches the end of the file — i.e. no unchanged base line follows
|
|
110
|
+
* it. That "reaches the end" check has to be taken right after applying the last hunk, before the
|
|
111
|
+
* catch-up loop below copies any remaining base lines: that loop always drives `i` up to
|
|
112
|
+
* `A.lines.length`, so testing position after it would call every last hunk "at the tail" even
|
|
113
|
+
* when a common suffix follows it untouched — silently forcing (or dropping) a trailing newline
|
|
114
|
+
* a hunk in the middle of the file never decided.
|
|
115
|
+
*
|
|
116
|
+
* The decision itself is read off the whole file each side came from (`A.eofNewline` /
|
|
117
|
+
* `B.eofNewline`), not reconstructed from the winning hunk's own `noEofNewline` flag — that flag
|
|
118
|
+
* has nothing to say when the winning side contributes no lines (a pure removal taken as the
|
|
119
|
+
* winner), so `variantText` is passed in alongside the hunks for exactly this reason.
|
|
120
|
+
*/
|
|
121
|
+
function mergeFile(baseText, variantText, hunks, takes) {
|
|
122
|
+
const bom = baseText.charCodeAt(0) === BOM.charCodeAt(0);
|
|
123
|
+
const A = splitLines(baseText);
|
|
124
|
+
const B = splitLines(variantText);
|
|
125
|
+
const out = [];
|
|
126
|
+
let i = 0; // 0-based index into A.lines
|
|
127
|
+
let iAfterLastHunk = 0;
|
|
128
|
+
let lastWinnerIsVariant = false;
|
|
129
|
+
hunks.forEach((h, k) => {
|
|
130
|
+
const start = h.a.start - 1; // a.start is 1-based and marks where the hunk applies
|
|
131
|
+
while (i < start)
|
|
132
|
+
out.push(A.lines[i++]);
|
|
133
|
+
const takeVariant = takes[k] === "variant";
|
|
134
|
+
const side = takeVariant ? h.b : h.a;
|
|
135
|
+
out.push(...side.lines);
|
|
136
|
+
i += h.a.lines.length; // the base's lines for this hunk are consumed either way
|
|
137
|
+
if (k === hunks.length - 1) {
|
|
138
|
+
iAfterLastHunk = i;
|
|
139
|
+
lastWinnerIsVariant = takeVariant;
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
while (i < A.lines.length)
|
|
143
|
+
out.push(A.lines[i++]);
|
|
144
|
+
// The tail decides the final newline only when the last hunk reaches the end of the file.
|
|
145
|
+
const endsAtTail = hunks.length > 0 && iAfterLastHunk >= A.lines.length;
|
|
146
|
+
const eofNewline = endsAtTail ? (lastWinnerIsVariant ? B.eofNewline : A.eofNewline) : A.eofNewline;
|
|
147
|
+
// An empty result is the empty string, not a lone newline — `out.join("\n")` on an empty array
|
|
148
|
+
// is already "", but the `+ eofNewline` term must not turn that into a false "\n" on its own.
|
|
149
|
+
const body = out.length ? out.join("\n") + (eofNewline ? "\n" : "") : "";
|
|
150
|
+
return (bom ? BOM : "") + withEol(body, detectEol(baseText));
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Turn a plan's per-hunk, per-file decisions into the bytes to write. Only paths whose merged
|
|
154
|
+
* content differs from the base's current bytes appear in `write` — a plan of all-`base`
|
|
155
|
+
* decisions writes nothing, so `unify` never rewrites a file it did not change.
|
|
156
|
+
*/
|
|
157
|
+
export async function applyPlan(base, variant, diff, plan, opts = {}) {
|
|
158
|
+
await assertTextMergeable(base, variant);
|
|
159
|
+
const write = {};
|
|
160
|
+
const remove = [];
|
|
161
|
+
let unresolved = 0;
|
|
162
|
+
const hunksByFile = new Map(diff.files.map((f) => [f.file, f.hunks]));
|
|
163
|
+
const onlyInBase = new Set(diff.onlyInBase);
|
|
164
|
+
const onlyInVariant = new Set(diff.onlyInVariant);
|
|
165
|
+
// Ruling 29 (spec §8 refusal 7, in both directions): the plan must cover every file the diff
|
|
166
|
+
// has — paired, base-only and variant-only — each exactly once. The walk below only visits the
|
|
167
|
+
// plan's own entries, so without this a plan stripped of an entry would apply as "resolved" and
|
|
168
|
+
// the caller would delete the variant together with the one copy of whatever was left out; two
|
|
169
|
+
// entries for the same file would let the last one silently win. Checked before any decision is
|
|
170
|
+
// read, so a contradiction never gets half-applied.
|
|
171
|
+
const planned = new Set();
|
|
172
|
+
for (const pf of plan.files) {
|
|
173
|
+
if (pf.hunks && pf.onlyIn) {
|
|
174
|
+
throw new Error(`unify plan: "${pf.file}" carries both hunk decisions and a side ("onlyIn: ${pf.onlyIn}") — the plan no longer matches this diff.`);
|
|
175
|
+
}
|
|
176
|
+
if (planned.has(pf.file)) {
|
|
177
|
+
throw new Error(`unify plan: "${pf.file}" has more than one entry — each file in the diff must be decided exactly once.`);
|
|
178
|
+
}
|
|
179
|
+
planned.add(pf.file);
|
|
180
|
+
}
|
|
181
|
+
for (const file of [...hunksByFile.keys(), ...onlyInBase, ...onlyInVariant]) {
|
|
182
|
+
if (!planned.has(file)) {
|
|
183
|
+
throw new Error(`unify plan: "${file}" differs between the base and the variant but has no entry in the plan — the plan no longer matches this diff.`);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
for (const pf of plan.files) {
|
|
187
|
+
if (pf.hunks) {
|
|
188
|
+
const hunks = hunksByFile.get(pf.file);
|
|
189
|
+
if (!hunks)
|
|
190
|
+
throw new Error(`unify plan: no diff hunks recorded for paired file "${pf.file}"`);
|
|
191
|
+
// A plan is hand-edited YAML: bind each decision by its own `hunk` field, never by array
|
|
192
|
+
// position, so a reordered entry still lands on the hunk it names. A length mismatch, or a
|
|
193
|
+
// `hunk` value outside 1..n or repeated, means the plan no longer describes this diff —
|
|
194
|
+
// refuse rather than silently misapplying a decision to the wrong hunk or defaulting a gap
|
|
195
|
+
// to "base".
|
|
196
|
+
if (pf.hunks.length !== hunks.length) {
|
|
197
|
+
throw new Error(`unify plan: "${pf.file}" carries ${pf.hunks.length} hunk decision(s), but the diff has ${hunks.length} — the plan no longer matches this diff.`);
|
|
198
|
+
}
|
|
199
|
+
const takes = new Array(hunks.length);
|
|
200
|
+
const seen = new Set();
|
|
201
|
+
for (const ph of pf.hunks) {
|
|
202
|
+
if (ph.hunk < 1 || ph.hunk > hunks.length || seen.has(ph.hunk)) {
|
|
203
|
+
throw new Error(`unify plan: "${pf.file}" hunk index ${ph.hunk} is out of range or duplicated — expected each of 1..${hunks.length} exactly once.`);
|
|
204
|
+
}
|
|
205
|
+
seen.add(ph.hunk);
|
|
206
|
+
takes[ph.hunk - 1] = ph.take;
|
|
207
|
+
}
|
|
208
|
+
unresolved += takes.filter((t) => t === "keep").length;
|
|
209
|
+
// A plan with no `variant` decision cannot change this file's bytes — skip the merge
|
|
210
|
+
// entirely rather than round-tripping the base through `splitLines`/`withEol` for nothing,
|
|
211
|
+
// which would re-terminate a base with mixed line endings even though no decision moved it.
|
|
212
|
+
if (takes.includes("variant")) {
|
|
213
|
+
const baseText = await readIngredientText(base, pf.file);
|
|
214
|
+
const variantText = await readIngredientText(variant, pf.file);
|
|
215
|
+
const merged = mergeFile(baseText, variantText, hunks, takes);
|
|
216
|
+
if (merged !== baseText)
|
|
217
|
+
write[pf.file] = merged;
|
|
218
|
+
}
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
// A plan entry with neither `hunks` nor `onlyIn` describes nothing to apply — the schema
|
|
222
|
+
// leaves both optional (a hand-edited plan can drop either), so the engine is where this
|
|
223
|
+
// contradiction is caught, per spec §6: "it throws on contradiction."
|
|
224
|
+
if (!pf.onlyIn) {
|
|
225
|
+
throw new Error(`unify plan: "${pf.file}" has neither hunk decisions nor a side ("onlyIn") — the plan no longer matches this diff.`);
|
|
226
|
+
}
|
|
227
|
+
// One-sided file: present only in the base or only in the variant. Checked against the diff
|
|
228
|
+
// the same way a paired file's hunk count is checked above (spec §8 refusal 7: "names a file
|
|
229
|
+
// ... the diff does not have") — a hand-edited plan can name a file that is shared and
|
|
230
|
+
// identical (so the diff never lists it as one-sided at all, on either side) or has simply
|
|
231
|
+
// moved sides since the plan was saved. Left unchecked, `take: "variant"` on such an entry
|
|
232
|
+
// would fall through to `writeUnified`'s `fs.rm(..., { force: true })`, which deletes or
|
|
233
|
+
// silently no-ops on a path the engine never proposed touching — a Forge with no lock cannot
|
|
234
|
+
// afford that.
|
|
235
|
+
const onlySet = pf.onlyIn === "base" ? onlyInBase : onlyInVariant;
|
|
236
|
+
if (!onlySet.has(pf.file)) {
|
|
237
|
+
throw new Error(`unify plan: "${pf.file}" is recorded as present only in the ${pf.onlyIn}, but the diff does not have it there — the plan no longer matches this diff.`);
|
|
238
|
+
}
|
|
239
|
+
const take = pf.take ?? "keep";
|
|
240
|
+
if (take === "keep") {
|
|
241
|
+
unresolved += 1;
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
if (pf.onlyIn === "base") {
|
|
245
|
+
// take === "base" already matches the base as it stands; take === "variant" means the
|
|
246
|
+
// variant's absence of the file wins, so it comes out of the base.
|
|
247
|
+
if (take === "variant")
|
|
248
|
+
remove.push(pf.file);
|
|
249
|
+
}
|
|
250
|
+
else if (pf.onlyIn === "variant") {
|
|
251
|
+
// take === "base" already matches the base's absence of the file; take === "variant"
|
|
252
|
+
// brings the variant's copy in — read without an encoding, so the bytes written are the
|
|
253
|
+
// bytes on disk (Ruling 31). Valid UTF-8 round-trips exactly through a string, BOM
|
|
254
|
+
// included, so it is carried as text; anything else stays a Buffer, which a UTF-8
|
|
255
|
+
// decode/encode would corrupt.
|
|
256
|
+
if (take === "variant") {
|
|
257
|
+
const bytes = await fs.readFile(path.join(variant.dir, pf.file));
|
|
258
|
+
write[pf.file] = isUtf8(bytes) ? bytes.toString("utf8") : bytes;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
const metaDiffers = opts.discardVariantMeta ? [] : metaDifferences(base, variant);
|
|
263
|
+
return { write, remove, resolved: unresolved === 0 && metaDiffers.length === 0, unresolved, metaDiffers };
|
|
264
|
+
}
|
|
265
|
+
async function note(journal, abs) {
|
|
266
|
+
if (journal)
|
|
267
|
+
journal.push({ abs, created: !(await exists(abs)) });
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Put a `UnifyResult` on disk: write the merged files, delete the ones the plan resolved away.
|
|
271
|
+
* `ingredient.yaml` can never appear in either list — `diffIngredients` excludes it from both
|
|
272
|
+
* sides of the diff a plan is built from — so this function never touches an ingredient's
|
|
273
|
+
* metadata. Every path is noted in `journal`, when given, before it is touched.
|
|
274
|
+
*
|
|
275
|
+
* Returns every base-relative path touched, written or removed, sorted — what the CLI reports as
|
|
276
|
+
* "what changed."
|
|
277
|
+
*/
|
|
278
|
+
export async function writeUnified(base, result, journal) {
|
|
279
|
+
for (const [rel, content] of Object.entries(result.write)) {
|
|
280
|
+
const abs = path.join(base.dir, rel);
|
|
281
|
+
await note(journal, abs);
|
|
282
|
+
await fs.mkdir(path.dirname(abs), { recursive: true });
|
|
283
|
+
await fs.writeFile(abs, content);
|
|
284
|
+
}
|
|
285
|
+
for (const rel of result.remove) {
|
|
286
|
+
const abs = path.join(base.dir, rel);
|
|
287
|
+
await note(journal, abs);
|
|
288
|
+
await fs.rm(abs, { force: true });
|
|
289
|
+
}
|
|
290
|
+
return [...Object.keys(result.write), ...result.remove].sort();
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Find the recipe file under `recipes/` whose `name` field matches, by scanning rather than
|
|
294
|
+
* assuming `<name>.yaml` — `loadForge` keys `forge.recipes` by the `name` field *inside* the
|
|
295
|
+
* YAML, never by filename, so a recipe can legally live in a file named after something else.
|
|
296
|
+
* When two files declare the same `name`, the *last* one `readdir` returns wins — matching
|
|
297
|
+
* `loadForge`'s `recipes.set(r.name, r)`, which overwrites on every later match in the same
|
|
298
|
+
* directory order. There is no duplicate-name refusal for recipes the way there is for
|
|
299
|
+
* ingredients, so agreeing with `loadForge` is the only consistency this can offer.
|
|
300
|
+
*/
|
|
301
|
+
async function findRecipeFile(recipesDir, name) {
|
|
302
|
+
let entries = [];
|
|
303
|
+
try {
|
|
304
|
+
entries = await fs.readdir(recipesDir);
|
|
305
|
+
}
|
|
306
|
+
catch {
|
|
307
|
+
entries = [];
|
|
308
|
+
}
|
|
309
|
+
let found;
|
|
310
|
+
for (const f of entries) {
|
|
311
|
+
if (!/\.ya?ml$/.test(f))
|
|
312
|
+
continue;
|
|
313
|
+
const abs = path.join(recipesDir, f);
|
|
314
|
+
let parsed;
|
|
315
|
+
try {
|
|
316
|
+
parsed = YAML.parse(await fs.readFile(abs, "utf8"));
|
|
317
|
+
}
|
|
318
|
+
catch {
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
321
|
+
if (parsed && typeof parsed === "object" && parsed.name === name)
|
|
322
|
+
found = abs;
|
|
323
|
+
}
|
|
324
|
+
if (!found)
|
|
325
|
+
throw new Error(`recipe "${name}" not found under ${recipesDir}`);
|
|
326
|
+
return found;
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* Read a YAML file for an in-place edit, capturing the EOL and BOM of the bytes on disk so the
|
|
330
|
+
* edit can restore them — `YAML.parseDocument` / `doc.toString()` always emit LF without a BOM,
|
|
331
|
+
* so a one-token change would otherwise turn into a whole-file diff in the git repo the user
|
|
332
|
+
* reviews. The BOM check mirrors the one `mergeFile` already does on `baseText` in this file:
|
|
333
|
+
* `charCodeAt(0)` against the BOM code point, rather than reading the file again as a `Buffer`
|
|
334
|
+
* for `hasBom`.
|
|
335
|
+
*/
|
|
336
|
+
async function readYamlEdit(file) {
|
|
337
|
+
const raw = await fs.readFile(file, "utf8");
|
|
338
|
+
return { doc: YAML.parseDocument(raw), eol: detectEol(raw), bom: raw.charCodeAt(0) === BOM.charCodeAt(0) };
|
|
339
|
+
}
|
|
340
|
+
/** Serialize an edited document back with the original file's EOL and BOM, flow collections unpadded. */
|
|
341
|
+
function serializeYamlEdit(doc, eol, bom) {
|
|
342
|
+
return (bom ? BOM : "") + withEol(doc.toString({ flowCollectionPadding: false }), eol);
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* Replace every scalar entry equal to `from` with `to` inside the sequence at `key`, matched by
|
|
346
|
+
* value — never by rebuilding the sequence, so untouched entries, comments and formatting survive
|
|
347
|
+
* the round trip. Returns how many entries were replaced, so a caller that expected at least one
|
|
348
|
+
* (because the schema-parsed value it read off `loadForge` already contained `from`) can tell a
|
|
349
|
+
* no-op apart from a real edit — e.g. `key` resolving to an alias/anchor node rather than a plain
|
|
350
|
+
* `YAMLSeq`, which this function cannot rewrite safely and reports as zero.
|
|
351
|
+
*/
|
|
352
|
+
function replaceSeqEntry(doc, key, from, to) {
|
|
353
|
+
const seq = doc.get(key, true);
|
|
354
|
+
if (!(seq instanceof YAML.YAMLSeq))
|
|
355
|
+
return 0;
|
|
356
|
+
let count = 0;
|
|
357
|
+
seq.items.forEach((item, i) => {
|
|
358
|
+
const value = item instanceof YAML.Scalar ? item.value : item;
|
|
359
|
+
if (value === from) {
|
|
360
|
+
seq.set(i, to);
|
|
361
|
+
count++;
|
|
362
|
+
}
|
|
363
|
+
});
|
|
364
|
+
return count;
|
|
365
|
+
}
|
|
366
|
+
/**
|
|
367
|
+
* Render an edit without writing it. `loadForge` already said the recipe's `ingredients` holds
|
|
368
|
+
* `from`; if the sequence edit still replaces nothing (an alias/anchor, for instance), that is a
|
|
369
|
+
* file unify cannot safely rewrite — throw rather than record a rewrite that did not happen
|
|
370
|
+
* (Ruling 14).
|
|
371
|
+
*/
|
|
372
|
+
async function renderIngredientEdit(e) {
|
|
373
|
+
const { doc, eol, bom } = await readYamlEdit(e.file);
|
|
374
|
+
if (replaceSeqEntry(doc, "ingredients", e.from, e.to) === 0) {
|
|
375
|
+
throw new Error(`unify: recipe "${e.name}" (${e.file}) is recorded as naming ${e.from} in its "ingredients", ` +
|
|
376
|
+
`but no matching entry was found to rewrite (it may reach "ingredients" through a YAML alias/anchor) — refusing to report it as rewritten.`);
|
|
377
|
+
}
|
|
378
|
+
return serializeYamlEdit(doc, eol, bom);
|
|
379
|
+
}
|
|
380
|
+
/** Every recipe whose `ingredients` holds the variant, found by its `name` field (Ruling 9). */
|
|
381
|
+
async function ingredientEdits(forge, baseRef, variantRef) {
|
|
382
|
+
const recipesDir = path.join(forge.root, "recipes");
|
|
383
|
+
const out = [];
|
|
384
|
+
for (const [name, r] of forge.recipes) {
|
|
385
|
+
if (!r.ingredients.includes(variantRef))
|
|
386
|
+
continue;
|
|
387
|
+
out.push({ name, file: await findRecipeFile(recipesDir, name), from: variantRef, to: baseRef });
|
|
388
|
+
}
|
|
389
|
+
return out;
|
|
390
|
+
}
|
|
391
|
+
/**
|
|
392
|
+
* Ruling 42: among the rewritten recipes, each `<r>--<profile>` that is now identical to its
|
|
393
|
+
* unsuffixed sibling `<r>` — compared as `ingredients` (sorted), `extends`, `slot` and `params`,
|
|
394
|
+
* never `description`, which is prose. It is only reported: deleting it and repointing the lists
|
|
395
|
+
* that name it is safe only if `<r>` takes part in no other resolution — a transitive `extends`, a
|
|
396
|
+
* workspace's `recipes.add` — and param defaults are last-wins while recipes apply at their first
|
|
397
|
+
* occurrence, so either could shift. That cannot be proven from inside the Forge.
|
|
398
|
+
*/
|
|
399
|
+
function identicalToSibling(recipes, rewritten, profile) {
|
|
400
|
+
const suffix = `--${profile}`;
|
|
401
|
+
const out = [];
|
|
402
|
+
for (const rn of rewritten) {
|
|
403
|
+
if (!rn.endsWith(suffix))
|
|
404
|
+
continue;
|
|
405
|
+
const suffixed = recipes.get(rn);
|
|
406
|
+
const sibling = recipes.get(rn.slice(0, -suffix.length));
|
|
407
|
+
if (!suffixed || !sibling)
|
|
408
|
+
continue;
|
|
409
|
+
const same = isDeepStrictEqual([...suffixed.ingredients].sort(), [...sibling.ingredients].sort()) &&
|
|
410
|
+
isDeepStrictEqual(suffixed.extends, sibling.extends) &&
|
|
411
|
+
suffixed.slot === sibling.slot &&
|
|
412
|
+
isDeepStrictEqual(suffixed.params, sibling.params);
|
|
413
|
+
if (same)
|
|
414
|
+
out.push(rn);
|
|
415
|
+
}
|
|
416
|
+
return out;
|
|
417
|
+
}
|
|
418
|
+
/**
|
|
419
|
+
* The cascade's dry pass (Ruling 33): render every `ingredients` rewrite `rewriteRecipes` will
|
|
420
|
+
* make and check that each one lands — alias checks included — writing nothing. A caller runs it
|
|
421
|
+
* before the first write of the whole operation, so a refusal found here leaves the Forge exactly
|
|
422
|
+
* as it was.
|
|
423
|
+
*
|
|
424
|
+
* Returns the recipe files the cascade will rewrite, absolute — the set the caller checks git
|
|
425
|
+
* actually holds before writing (Ruling 37). Since Ruling 42 that is all the cascade ever touches:
|
|
426
|
+
* it deletes no recipe and edits no profile or `extends`.
|
|
427
|
+
*/
|
|
428
|
+
export async function checkRecipeCascade(forge, baseRef, variantRef) {
|
|
429
|
+
const files = [];
|
|
430
|
+
for (const edit of await ingredientEdits(forge, baseRef, variantRef)) {
|
|
431
|
+
await renderIngredientEdit(edit);
|
|
432
|
+
files.push(edit.file);
|
|
433
|
+
}
|
|
434
|
+
return files;
|
|
435
|
+
}
|
|
436
|
+
/**
|
|
437
|
+
* Follow the recipe cascade a resolved variant causes. Since Ruling 42 (a product decision) it is
|
|
438
|
+
* one pass: every recipe whose `ingredients` holds `variantRef` is rewritten to hold `baseRef`,
|
|
439
|
+
* which is required because the variant ingredient is about to be removed. It never deletes a
|
|
440
|
+
* recipe and never edits a profile or an `extends` list.
|
|
441
|
+
*
|
|
442
|
+
* It starts with `checkRecipeCascade`, the dry pass, so a rewrite that would not land throws with
|
|
443
|
+
* nothing written (Ruling 33). A caller that writes before calling this — the merged base, in
|
|
444
|
+
* `forge unify` — runs the dry pass itself first, for the same guarantee over the whole
|
|
445
|
+
* operation. What a dry pass cannot foresee (an I/O error, a file locked on Windows) can still
|
|
446
|
+
* fail midway; every path is noted in `journal`, when given, before it is touched.
|
|
447
|
+
*
|
|
448
|
+
* Each file is written back through `YAML.parseDocument` so only that one sequence entry changes
|
|
449
|
+
* (Ruling 10) — `RecipeSchema` materializes defaults a re-serialized schema object would write
|
|
450
|
+
* into a file that may have had far fewer keys, dropping comments too — with its EOL and BOM kept
|
|
451
|
+
* (Ruling 13). The caller orders this cascade *before* removing the variant's directory, so a late
|
|
452
|
+
* failure leaves the variant in place rather than a recipe naming a directory that no longer
|
|
453
|
+
* exists.
|
|
454
|
+
*
|
|
455
|
+
* Afterwards, against a reload of the Forge, it reports each rewritten `<r>--<profile>` now
|
|
456
|
+
* identical to its sibling `<r>` (see `identicalToSibling`) for a human to remove.
|
|
457
|
+
*/
|
|
458
|
+
export async function rewriteRecipes(forge, baseRef, variantRef, profile, journal) {
|
|
459
|
+
await checkRecipeCascade(forge, baseRef, variantRef);
|
|
460
|
+
const rewritten = [];
|
|
461
|
+
for (const edit of await ingredientEdits(forge, baseRef, variantRef)) {
|
|
462
|
+
const content = await renderIngredientEdit(edit);
|
|
463
|
+
await note(journal, edit.file);
|
|
464
|
+
await fs.writeFile(edit.file, content);
|
|
465
|
+
rewritten.push(edit.name);
|
|
466
|
+
}
|
|
467
|
+
if (rewritten.length === 0)
|
|
468
|
+
return { rewritten, identicalToSibling: [] };
|
|
469
|
+
const reloaded = await loadForge(forge.root);
|
|
470
|
+
return { rewritten, identicalToSibling: identicalToSibling(reloaded.recipes, rewritten, profile) };
|
|
471
|
+
}
|
|
472
|
+
//# sourceMappingURL=unify.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"unify.js","sourceRoot":"","sources":["../../src/core/unify.ts"],"names":[],"mappings":"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"}
|
package/dist/core/variants.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { promises as fs } from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { diffLines } from "./diff.js";
|
|
3
|
+
import { diffLines, splitLines } from "./diff.js";
|
|
4
4
|
import { fingerprintDir } from "./fingerprint.js";
|
|
5
5
|
import { listFiles } from "./forge.js";
|
|
6
6
|
/**
|
|
@@ -48,44 +48,55 @@ async function distanceOf(base, variant) {
|
|
|
48
48
|
const d = await diffIngredients(base, variant);
|
|
49
49
|
const baseFiles = await filesOf(base);
|
|
50
50
|
const variantFiles = await filesOf(variant);
|
|
51
|
-
// A file present on one side only counts as one hunk carrying all of its lines
|
|
52
|
-
//
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
51
|
+
// A file present on one side only counts as one hunk carrying all of its lines (spec 04,
|
|
52
|
+
// Ruling 9). Counted directly: diffing against "" mismatches a file with no final newline.
|
|
53
|
+
const oneSidedLines = d.onlyInBase.reduce((n, f) => n + splitLines(baseFiles.get(f)).lines.length, 0) +
|
|
54
|
+
d.onlyInVariant.reduce((n, f) => n + splitLines(variantFiles.get(f)).lines.length, 0);
|
|
55
|
+
// A hunk whose two sides carry the same lines exists only to carry the final-newline fact
|
|
56
|
+
// (diff.ts' forceTrailingHunk): no line content changed, so it costs 0 lines. It still counts
|
|
57
|
+
// as one hunk — it is a real difference — which keeps a newline-only variant nearer than any
|
|
58
|
+
// variant with a changed line instead of tied with it.
|
|
59
|
+
const sameLines = (h) => h.a.lines.length === h.b.lines.length && h.a.lines.every((line, k) => line === h.b.lines[k]);
|
|
60
|
+
const lineCount = (hunks) => hunks.reduce((k, h) => (sameLines(h) ? k : k + h.a.lines.length + h.b.lines.length), 0);
|
|
58
61
|
const hunks = d.files.reduce((n, f) => n + f.hunks.length, 0) + d.onlyInBase.length + d.onlyInVariant.length;
|
|
59
|
-
const lines = d.files.reduce((n, f) => n + lineCount(f.hunks), 0) +
|
|
62
|
+
const lines = d.files.reduce((n, f) => n + lineCount(f.hunks), 0) + oneSidedLines;
|
|
60
63
|
const bodyDiffers = hunks > 0;
|
|
61
64
|
const sameFingerprint = (await fingerprintDir(base.dir)) === (await fingerprintDir(variant.dir));
|
|
62
65
|
return {
|
|
63
66
|
lines,
|
|
64
67
|
hunks,
|
|
65
|
-
|
|
68
|
+
sameBodyDifferentMeta: !bodyDiffers && !sameFingerprint,
|
|
66
69
|
identicalAfterNormalization: !bodyDiffers && sameFingerprint,
|
|
67
70
|
};
|
|
68
71
|
}
|
|
69
72
|
const nearest = (x, y) => x.lines - y.lines || x.hunks - y.hunks;
|
|
70
|
-
/** Bases that have at least one variant, nearest first. */
|
|
73
|
+
/** Bases that have at least one variant, nearest first, plus variants whose base is missing. */
|
|
71
74
|
export async function listVariants(forge) {
|
|
72
75
|
const groups = new Map();
|
|
76
|
+
const orphans = [];
|
|
73
77
|
for (const ing of forge.ingredients.values()) {
|
|
74
78
|
const profile = profileOf(ing.meta);
|
|
75
79
|
if (!profile || !ing.meta.as)
|
|
76
80
|
continue;
|
|
77
81
|
const baseRef = `${ing.meta.type}/${ing.meta.as}`;
|
|
78
82
|
const base = forge.ingredients.get(baseRef);
|
|
79
|
-
if (!base)
|
|
83
|
+
if (!base) {
|
|
84
|
+
// A recipe including this still emits it under the base name, so silence would hide a
|
|
85
|
+
// broken Forge — the listing is where that state becomes visible.
|
|
86
|
+
orphans.push({ ref: ing.ref, profile, missingBase: baseRef });
|
|
80
87
|
continue;
|
|
88
|
+
}
|
|
81
89
|
const entry = { ref: ing.ref, profile, distance: await distanceOf(base, ing) };
|
|
82
90
|
groups.set(baseRef, [...(groups.get(baseRef) ?? []), entry]);
|
|
83
91
|
}
|
|
84
|
-
return
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
92
|
+
return {
|
|
93
|
+
groups: [...groups]
|
|
94
|
+
.map(([base, variants]) => ({
|
|
95
|
+
base,
|
|
96
|
+
variants: variants.sort((x, y) => nearest(x.distance, y.distance) || x.ref.localeCompare(y.ref)),
|
|
97
|
+
}))
|
|
98
|
+
.sort((x, y) => nearest(x.variants[0].distance, y.variants[0].distance) || x.base.localeCompare(y.base)),
|
|
99
|
+
orphans: orphans.sort((x, y) => x.ref.localeCompare(y.ref)),
|
|
100
|
+
};
|
|
90
101
|
}
|
|
91
102
|
//# sourceMappingURL=variants.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"variants.js","sourceRoot":"","sources":["../../src/core/variants.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,IAAI,EAAE,EAAE,MAAM,SAAS,CAAC;AACzC,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,SAAS,EAAa,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"variants.js","sourceRoot":"","sources":["../../src/core/variants.ts"],"names":[],"mappings":"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"}
|