@sema-agent/core
Version:
Stateless, task-oriented AI agent core
121 lines (120 loc) • 5.31 kB
JavaScript
function canon(v, seen = new WeakSet()) {
if (v === null)
return "null";
if (v === undefined)
return "undefined";
const t = typeof v;
if (t === "number")
return Object.is(v, -0) ? "-0" : String(v);
if (t === "bigint")
return `${v}n`;
if (t === "string")
return JSON.stringify(v);
if (t === "boolean")
return String(v);
if (t === "function" || t === "symbol")
return `[${t}]`;
if (Array.isArray(v)) {
if (seen.has(v))
return "[Circular]";
seen.add(v);
const s = `[${v.map((e) => canon(e, seen)).join(",")}]`;
seen.delete(v);
return s;
}
const o = v;
if (seen.has(o))
return "[Circular]";
seen.add(o);
const s = `{${Object.keys(o).sort().map((k) => `${JSON.stringify(k)}:${canon(o[k], seen)}`).join(",")}}`;
seen.delete(o);
return s;
}
const structEq = (a, b) => canon(a) === canon(b);
const multisetEq = (a, b) => structEq(a.map((x) => canon(x)).sort(), b.map((x) => canon(x)).sort());
function multisetSubset(out, inp) {
const c = new Map();
for (const x of inp) {
const k = canon(x);
c.set(k, (c.get(k) ?? 0) + 1);
}
for (const x of out) {
const k = canon(x);
const n = (c.get(k) ?? 0) - 1;
if (n < 0)
return false;
c.set(k, n);
}
return true;
}
const lenOf = (x) => (typeof x === "string" || Array.isArray(x) ? x.length : 0);
export function deriveInvariants(contract) {
const inv = [];
const call = (fn, x) => fn(x);
const coll = (x) => (contract.inputCollection ? contract.inputCollection(x) : x);
const add = (kind, description, check) => inv.push({ kind, description, check });
if (contract.permutation)
add("permutation", "output is a permutation of the input collection", (f, x) => multisetEq(call(f, x), coll(x)));
if (contract.lengthPreserving)
add("lengthPreserving", "|output| === |input collection|", (f, x) => call(f, x).length === coll(x).length);
if (contract.subset)
add("subset", "output is a sub-multiset of the input collection", (f, x) => multisetSubset(call(f, x), coll(x)));
if (contract.distinctOutput)
add("distinctOutput", "output has no duplicate elements", (f, x) => { const o = call(f, x); return new Set(o.map((e) => canon(e))).size === o.length; });
if (contract.idempotent)
add("idempotent", "f(f(x)) === f(x)", (f, x) => { const once = call(f, x); return structEq(call(f, once), once); });
if (contract.involution)
add("involution", "f(f(x)) === x", (f, x) => structEq(call(f, call(f, x)), x));
if (contract.outputPredicate)
add("outputPredicate", "every output element satisfies the contract predicate", (f, x) => call(f, x).every((e) => contract.outputPredicate(e)));
if (contract.rangeBound)
add("rangeBound", "output is within the contract range", (f, x) => { const o = call(f, x); const [lo, hi] = contract.rangeBound(x); return typeof o === "number" && o >= lo && o <= hi; });
if (contract.nonNegative)
add("nonNegative", "output >= 0", (f, x) => call(f, x) >= 0);
if (contract.nonEmptyPreserving)
add("nonEmptyPreserving", "non-empty input collection => non-empty output", (f, x) => (lenOf(coll(x)) > 0 ? lenOf(call(f, x)) > 0 : true));
if (contract.concatPreserving)
add("concatPreserving", "concatenation of output groups === input collection", (f, x) => structEq([].concat(...call(f, x)), coll(x)));
if (contract.chunkMaxLen)
add("chunkMaxLen", "every output group has length in (0, n]", (f, x) => { const n = contract.chunkMaxLen(x); return call(f, x).every((g) => g.length > 0 && g.length <= n); });
if (contract.lengthEq)
add("lengthEq", "|output| === the contract length", (f, x) => call(f, x).length === contract.lengthEq(x));
if (contract.roundtrip)
add("roundtrip", "decode(encode(x)) === x", (pair, x) => { const p = pair; return structEq(p.decode(p.encode(x)), x); });
if (contract.totalNoThrow)
add("totalNoThrow", "must not throw on a valid input", (f, x) => { try {
if (typeof f === "function")
f(x);
return true;
}
catch {
return false;
} });
return inv;
}
export function checkInvariants(fn, genInput, invariants, opts = {}) {
const trials = Math.max(1, Math.floor(opts.trials ?? 200));
const found = new Map();
for (let i = 0; i < trials; i++) {
const input = genInput(i);
for (const iv of invariants) {
if (found.has(iv.kind))
continue;
try {
if (!iv.check(fn, input))
found.set(iv.kind, { kind: iv.kind, description: iv.description, input });
}
catch {
try {
if (typeof fn === "function")
fn(input);
found.set(iv.kind, { kind: iv.kind, description: iv.description, input, evaluationThrew: true });
}
catch {
}
}
}
}
const violations = [...found.values()];
return { ok: violations.length === 0, violations, trials };
}