mirror of
https://github.com/morluto/rea.git
synced 2026-11-15 19:23:24 +00:00
fix(guards): refuse a counted-baseline file that is not in its written order (#11150)
* fix(guards): refuse a counted-baseline file that is not in its written order * test(guards): decide the baseline file's rewrite or refusal in its owner and cover it
This commit is contained in:
1 parent
5e625e3139
commit
2c7c59e14e
4 files changed
+108
-14
No files matched your search
@@ -5,6 +5,7 @@ import { recordPredicateCounts } from "./record-predicate-guard.mjs";
|
||||
import {
|
||||
compareCountedBaseline,
|
||||
recordLoweredBaseline,
|
||||
reconcileBaselineFile,
|
||||
} from "./counted-baseline.mjs";
|
||||
import { localCompletenessEnumCount } from "./completeness-enum-guard.mjs";
|
||||
import { countKeyedFirstMatches } from "./keyed-first-match.mjs";
|
||||
@@ -269,7 +270,8 @@ const baselinePath = join(
|
||||
root,
|
||||
"scripts/verify/architecture-guard-baselines.json",
|
||||
);
|
||||
const baselines = JSON.parse(await readFile(baselinePath, "utf8"));
|
||||
const baselineText = await readFile(baselinePath, "utf8");
|
||||
const baselines = JSON.parse(baselineText);
|
||||
const shrinkBaselines = process.argv.includes("--shrink-baselines");
|
||||
let baselinesShrunk = false;
|
||||
const checkCounted = (label, key, selected, pattern, owners) => {
|
||||
@@ -671,20 +673,22 @@ checkCounted(
|
||||
[],
|
||||
);
|
||||
|
||||
if (baselinesShrunk) {
|
||||
const sorted = Object.fromEntries(
|
||||
Object.entries(baselines).map(([key, counts]) => [
|
||||
key,
|
||||
Object.fromEntries(
|
||||
Object.entries(counts).sort(([left], [right]) =>
|
||||
left < right ? -1 : left > right ? 1 : 0,
|
||||
),
|
||||
),
|
||||
]),
|
||||
const baselineFile = reconcileBaselineFile(
|
||||
baselineText,
|
||||
baselines,
|
||||
shrinkBaselines,
|
||||
);
|
||||
if (baselineFile.action === "write") {
|
||||
await writeFile(baselinePath, baselineFile.text);
|
||||
console.log(
|
||||
baselinesShrunk
|
||||
? `Lowered counted baselines in ${relative(root, baselinePath)}.`
|
||||
: `Rewrote ${relative(root, baselinePath)} in its sorted form.`,
|
||||
);
|
||||
} else if (baselineFile.action === "reject")
|
||||
failures.push(
|
||||
`${relative(root, baselinePath)} is not in its written form (each guard's paths sorted by code unit); rewrite it with \`node scripts/verify/architecture-guards.mjs --shrink-baselines\`.`,
|
||||
);
|
||||
await writeFile(baselinePath, `${JSON.stringify(sorted, null, 2)}\n`);
|
||||
console.log(`Lowered counted baselines in ${relative(root, baselinePath)}.`);
|
||||
}
|
||||
|
||||
if (failures.length > 0) {
|
||||
console.error(`Architecture guards failed:\n\n${failures.join("\n\n")}\n`);
|
||||
|
||||
@@ -27,3 +27,17 @@ export function recordLoweredBaseline(
|
||||
readonly lowered: boolean;
|
||||
},
|
||||
): boolean;
|
||||
|
||||
/** The baseline file text: guard order kept, each guard's paths in code-unit order. */
|
||||
export function serializeCountedBaselines(
|
||||
baselines: Readonly<Record<string, Readonly<Record<string, number>>>>,
|
||||
): string;
|
||||
|
||||
/** Leave, rewrite or reject the baseline file read as `text`. */
|
||||
export function reconcileBaselineFile(
|
||||
text: string,
|
||||
baselines: Readonly<Record<string, Readonly<Record<string, number>>>>,
|
||||
shrink: boolean,
|
||||
):
|
||||
| { readonly action: "none" | "reject" }
|
||||
| { readonly action: "write"; readonly text: string };
|
||||
@@ -36,3 +36,36 @@ export const recordLoweredBaseline = (baselines, key, compared) => {
|
||||
else baselines[key] = compared.baseline;
|
||||
return true;
|
||||
};
|
||||
|
||||
const byCodeUnit = ([left], [right]) =>
|
||||
left < right ? -1 : left > right ? 1 : 0;
|
||||
|
||||
/**
|
||||
* The baseline file's one written form: guard keys keep their order and each
|
||||
* guard's paths are sorted by code unit. The guard rejects a file in any other
|
||||
* form, so a hand edit or merge cannot leave an order the next
|
||||
* `--shrink-baselines` would silently rewrite in an unrelated change.
|
||||
*/
|
||||
export const serializeCountedBaselines = (baselines) =>
|
||||
`${JSON.stringify(
|
||||
Object.fromEntries(
|
||||
Object.entries(baselines).map(([key, counts]) => [
|
||||
key,
|
||||
Object.fromEntries(Object.entries(counts).sort(byCodeUnit)),
|
||||
]),
|
||||
),
|
||||
null,
|
||||
2,
|
||||
)}\n`;
|
||||
|
||||
/**
|
||||
* What the guard does with the baseline file it read as `text`, given the
|
||||
* baselines after comparison: nothing when the file is already in its written
|
||||
* form; otherwise `write` that form when shrinking (even if no count was
|
||||
* lowered), or `reject` the file so a hand edit is fixed in its own change.
|
||||
*/
|
||||
export const reconcileBaselineFile = (text, baselines, shrink) => {
|
||||
const written = serializeCountedBaselines(baselines);
|
||||
if (written === text) return { action: "none" };
|
||||
return shrink ? { action: "write", text: written } : { action: "reject" };
|
||||
};
|
||||
@@ -2,7 +2,9 @@ import { expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
compareCountedBaseline,
|
||||
reconcileBaselineFile,
|
||||
recordLoweredBaseline,
|
||||
serializeCountedBaselines,
|
||||
} from "../../../scripts/verify/counted-baseline.mjs";
|
||||
|
||||
it("rejects a file that gains copies or is missing from the baseline", () => {
|
||||
@@ -90,3 +92,44 @@ it("removes a key when shrinking removes its last recorded count", () => {
|
||||
expect(recordLoweredBaseline(baselines, "gone", compared)).toBe(true);
|
||||
expect(baselines).toEqual({ kept: { "src/b.ts": 2 } });
|
||||
});
|
||||
|
||||
it("writes each guard's paths in code-unit order and keeps guard order", () => {
|
||||
const baselines = {
|
||||
later: { "src/b.ts": 1, "src/Upper.ts": 2, "src/a.ts": 3 },
|
||||
earlier: {},
|
||||
};
|
||||
expect(serializeCountedBaselines(baselines)).toBe(
|
||||
`${JSON.stringify(
|
||||
{
|
||||
later: { "src/Upper.ts": 2, "src/a.ts": 3, "src/b.ts": 1 },
|
||||
earlier: {},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
)}\n`,
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects an unsorted baseline file unless shrinking, which rewrites it", () => {
|
||||
const baselines = { guard: { "src/a.ts": 1, "src/b.ts": 2 } };
|
||||
const written = serializeCountedBaselines(baselines);
|
||||
const unsorted = `${JSON.stringify(
|
||||
{ guard: { "src/b.ts": 2, "src/a.ts": 1 } },
|
||||
null,
|
||||
2,
|
||||
)}\n`;
|
||||
expect(reconcileBaselineFile(written, baselines, false)).toEqual({
|
||||
action: "none",
|
||||
});
|
||||
expect(reconcileBaselineFile(written, baselines, true)).toEqual({
|
||||
action: "none",
|
||||
});
|
||||
expect(reconcileBaselineFile(unsorted, baselines, false)).toEqual({
|
||||
action: "reject",
|
||||
});
|
||||
// No count was lowered; shrinking still writes the sorted form.
|
||||
expect(reconcileBaselineFile(unsorted, baselines, true)).toEqual({
|
||||
action: "write",
|
||||
text: written,
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user