Andrey Antukh a16d40b954 🎉 Add semantic file projection RPC and analysis prototype
Add a main RPC command, get-semantic-file, that turns a Penpot file
into a small, neutral projection: pages, frames, text, links and
interactions, with libraries, geometry and assets stripped. The
projection is what a decision model sees, so the extractor stays
model-agnostic and the deterministic checks live outside it.

Add experiments/security-projection, a Node prototype that consumes
the command: analyze.mjs (file -> projection -> checks -> decision
model), compare.mjs (one file, several models), evaluate.mjs with a
labeled set of projections, and seed.mjs, which builds real files to
validate the extractor. It talks to OpenRouter decision models.

Tests: backend_tests.rpc-semantic-test (3 tests, 21 assertions) and
24 Node tests across the prototype.

AI-assisted-by: deepseek-v4.1-flash
2026-10-08 17:40:22 +00:00

62 lines
1.6 KiB
JavaScript

import test from "node:test";
import assert from "node:assert/strict";
import { median, summarize } from "./evaluate.mjs";
test("median returns the middle value, not the mean", () => {
assert.equal(median([10, 30, 20]), 20);
assert.equal(median([]), null);
});
test("summarize counts TP/FP/FN/TN at the threshold", () => {
const rows = [
{ label: "phishing", prob: 0.9 }, // TP
{ label: "phishing", prob: 0.4 }, // FN
{ label: "benign", prob: 0.6 }, // FP
{ label: "benign", prob: 0.1 }, // TN
];
const m = summarize(rows, 0.5);
assert.deepEqual(
{ tp: m.tp, fp: m.fp, fn: m.fn, tn: m.tn },
{ tp: 1, fp: 1, fn: 1, tn: 1 }
);
assert.equal(m.precision, 0.5);
assert.equal(m.recall, 0.5);
assert.equal(m.f1, 0.5);
});
test("summarize is perfect when every case is on the right side", () => {
const m = summarize(
[
{ label: "phishing", prob: 0.98 },
{ label: "benign", prob: 0.03 },
],
0.5
);
assert.equal(m.precision, 1);
assert.equal(m.recall, 1);
assert.equal(m.f1, 1);
});
test("summarize reports null precision when nothing is flagged", () => {
const m = summarize(
[
{ label: "phishing", prob: 0.2 },
{ label: "benign", prob: 0.1 },
],
0.5
);
assert.equal(m.precision, null);
assert.equal(m.recall, 0);
});
test("summarize moves the confusion matrix with the threshold", () => {
const rows = [
{ label: "phishing", prob: 0.6 },
{ label: "benign", prob: 0.6 },
];
assert.equal(summarize(rows, 0.5).fp, 1);
assert.equal(summarize(rows, 0.7).fp, 0);
assert.equal(summarize(rows, 0.7).fn, 1);
});