mirror of
https://github.com/badlogic/pi-mono.git
synced 2026-11-05 23:37:25 +00:00
fix(coding-agent): normalize eval inputs before hashing
This commit is contained in:
parent
56e6a8a3fa
commit
6a3eae8e48
2 changed files with 22 additions and 18 deletions
|
|
@ -61,34 +61,35 @@ function createSeededRandom(seed: number): () => number {
|
|||
};
|
||||
}
|
||||
|
||||
function serializeString(value: string): string {
|
||||
const serialized = JSON.stringify(value);
|
||||
if (serialized === undefined) throw new TypeError("Eval input must be JSON-serializable.");
|
||||
return serialized;
|
||||
}
|
||||
|
||||
function canonicalJson(value: unknown, ancestors: WeakSet<object>): string {
|
||||
if (value === null) return "null";
|
||||
if (typeof value === "string") return serializeString(value);
|
||||
if (typeof value === "boolean") return String(value);
|
||||
function canonicalizeJson(value: unknown, ancestors: WeakSet<object>): JsonValue {
|
||||
if (value === null || typeof value === "string" || typeof value === "boolean") return value;
|
||||
if (typeof value === "number") {
|
||||
if (!Number.isFinite(value)) throw new TypeError("Eval input must contain only finite numbers.");
|
||||
return JSON.stringify(value);
|
||||
return value;
|
||||
}
|
||||
if (typeof value !== "object") throw new TypeError("Eval input must be JSON-serializable.");
|
||||
if (ancestors.has(value)) throw new TypeError("Eval input must not contain circular references.");
|
||||
|
||||
ancestors.add(value);
|
||||
try {
|
||||
if (Array.isArray(value)) return `[${Array.from(value, (item) => canonicalJson(item, ancestors)).join(",")}]`;
|
||||
if (Array.isArray(value)) {
|
||||
const result: JsonValue[] = [];
|
||||
for (let index = 0; index < value.length; index += 1) {
|
||||
if (!Object.hasOwn(value, index)) throw new TypeError("Eval input arrays must not be sparse.");
|
||||
result.push(canonicalizeJson(value[index], ancestors));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
const prototype = Object.getPrototypeOf(value);
|
||||
if (prototype !== Object.prototype && prototype !== null) {
|
||||
throw new TypeError("Eval input must contain only plain objects and arrays.");
|
||||
}
|
||||
return `{${Object.entries(value)
|
||||
.sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0))
|
||||
.map(([key, item]) => `${serializeString(key)}:${canonicalJson(item, ancestors)}`)
|
||||
.join(",")}}`;
|
||||
const entries: Array<[string, unknown]> = Object.entries(value);
|
||||
return Object.fromEntries(
|
||||
entries
|
||||
.sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0))
|
||||
.map(([key, item]): [string, JsonValue] => [key, canonicalizeJson(item, ancestors)]),
|
||||
);
|
||||
} finally {
|
||||
ancestors.delete(value);
|
||||
}
|
||||
|
|
@ -99,7 +100,9 @@ function deriveInputKey(input: unknown): string {
|
|||
const id = input.id;
|
||||
if (typeof id === "string" && id.trim()) return id.trim();
|
||||
}
|
||||
return createHash("sha256").update(canonicalJson(input, new WeakSet())).digest("hex");
|
||||
const canonicalInput = JSON.stringify(canonicalizeJson(input, new WeakSet()));
|
||||
if (canonicalInput === undefined) throw new TypeError("Eval input must be JSON-serializable.");
|
||||
return createHash("sha256").update(canonicalInput).digest("hex");
|
||||
}
|
||||
|
||||
export function deriveEvalGroupKey(input: unknown, repetition: number, seed: number): string {
|
||||
|
|
|
|||
|
|
@ -18,13 +18,14 @@ describe("deriveEvalGroupKey", () => {
|
|||
);
|
||||
expect(deriveEvalGroupKey({ first: 1 }, 1, 42)).not.toBe(deriveEvalGroupKey({ first: 2 }, 1, 42));
|
||||
expect(deriveEvalGroupKey({ first: 1 }, 1, 42)).not.toBe(deriveEvalGroupKey({ first: 1 }, 2, 42));
|
||||
expect(deriveEvalGroupKey(["first", "second"], 1, 42)).not.toBe(deriveEvalGroupKey(["second", "first"], 1, 42));
|
||||
});
|
||||
|
||||
it("rejects non-JSON and circular input", () => {
|
||||
const circular: { self?: unknown } = {};
|
||||
circular.self = circular;
|
||||
expect(() => deriveEvalGroupKey(new Date(0), 1, 42)).toThrow("only plain objects and arrays");
|
||||
expect(() => deriveEvalGroupKey(Array(1), 1, 42)).toThrow("must be JSON-serializable");
|
||||
expect(() => deriveEvalGroupKey(Array(1), 1, 42)).toThrow("must not be sparse");
|
||||
expect(() => deriveEvalGroupKey(circular, 1, 42)).toThrow("must not contain circular references");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue