diffNamespaces
tcmts.cli.diff.diffNamespaces · #5t5wcggz · term · runs anywhere
import { substRefs } from "#codebase/tcmts/cli/diff";
import type { Change, NamespaceDiff, NameState } from "#codebase/tcmts/cli/diff";
import type { IR } from "#codebase/tcmts/core/hash";
export function diffNamespaces(
before: NameState,
after: NameState,
irOf: (hash: string) => string,
/** the text versions each side's definitions show, to tell which read differently */
texts?: { before: ReadonlyMap<string, string>; after: ReadonlyMap<string, string> },
): NamespaceDiff {
const d: NamespaceDiff = {
edited: [],
propagated: [],
added: [],
removed: [],
renamed: [],
aliased: [],
conflicted: [],
reworded: [],
};
const changed: Change[] = [];
for (const path of [...new Set([...before.keys(), ...after.keys()])].sort()) {
const o = before.get(path) ?? new Set<string>();
const n = after.get(path) ?? new Set<string>();
if (o.size === n.size && [...o].every((h) => n.has(h))) {
const [h] = n;
const [was, now] = [texts?.before.get(h!), texts?.after.get(h!)];
if (n.size === 1 && was !== now)
d.reworded.push({
path,
hash: h!,
...(was ? { old: was } : {}),
...(now ? { new: now } : {}),
});
continue;
}
if (n.size > 1) d.conflicted.push({ path, hashes: [...n] });
else if (o.size === 0) d.added.push({ path, hash: [...n][0]! });
else if (n.size === 0) for (const h of o) d.removed.push({ path, hash: h });
else for (const h of o) changed.push({ path, old: h, new: [...n][0]! });
}
// a hash that left one name and arrived at another was renamed
// (a copy: the loop takes from it)
for (const a of d.added.slice()) {
const r = d.removed.find((x) => x.hash === a.hash);
if (!r) continue;
d.renamed.push({ from: r.path, to: a.path, hash: a.hash });
d.added.splice(d.added.indexOf(a), 1);
d.removed.splice(d.removed.indexOf(r), 1);
}
// a hash that gained a name while keeping an old one was aliased
const pathsBefore = new Map<string, string[]>();
for (const [p, hs] of before)
for (const h of hs) pathsBefore.set(h, [...(pathsBefore.get(h) ?? []), p]);
// (a copy: the loop takes from it)
for (const a of d.added.slice()) {
const existing = (pathsBefore.get(a.hash) ?? []).filter((p) => after.get(p)?.has(a.hash));
if (!existing.length) continue;
d.aliased.push({ ...a, existing });
d.added.splice(d.added.indexOf(a), 1);
}
// A definition is a pure propagation if rewriting its references through this diff's own
// old → new pairs reproduces the new definition exactly.
const remap = new Map(changed.map((c) => [c.old, c.new]));
for (const c of changed) {
const hit = new Set<string>();
const rewritten = JSON.stringify(substRefs(JSON.parse(irOf(c.old)) as IR, remap, hit));
if (hit.size && rewritten === irOf(c.new))
d.propagated.push({ ...c, because: [...hit].map((h) => remap.get(h)!) });
else d.edited.push(c);
}
return d;
}
uses
used by