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

118 lines
4.1 KiB
JavaScript

import test from "node:test";
import assert from "node:assert/strict";
import {
allLinks,
buildDecisionsRequest,
DECISION_QUESTIONS,
decisionState,
deterministicChecks,
projectionText,
scoreUrl,
} from "./analyze.mjs";
const projection = {
projectionVersion: 1,
fileId: "00000000-0000-0000-0000-000000000001",
fileName: "phish",
pages: [
{
name: "Login",
frames: [
{
name: "Login form",
text: ["Your account has been suspended.", "Enter your password to continue."],
links: [{ text: "Verify", url: "https://evil.example/verify" }],
},
],
},
],
};
test("projectionText gathers page, frame and text content", () => {
const text = projectionText(projection);
assert.match(text, /login form/);
assert.match(text, /your account has been suspended/);
});
test("allLinks collects links from pages and frames", () => {
assert.deepEqual(allLinks(projection), [
{ text: "Verify", url: "https://evil.example/verify" },
]);
});
test("scoreUrl flags a raw IP host", () => {
const result = scoreUrl("http://192.168.0.10/login");
assert.ok(result.score >= 2);
assert.ok(result.reasons.some((r) => r.includes("IP")));
});
test("scoreUrl flags a punycode host", () => {
const result = scoreUrl("https://xn--pypal-4ve.example/login");
assert.ok(result.score >= 3);
assert.ok(result.reasons.some((r) => r.includes("punycode")));
});
test("scoreUrl flags a brand token outside its official domain", () => {
const result = scoreUrl("https://paypal.com.evil.example/verify");
assert.ok(result.score >= 2);
assert.ok(result.reasons.some((r) => r.includes("paypal")));
});
test("scoreUrl does not flag the official brand domain", () => {
const result = scoreUrl("https://paypal.com/login");
assert.ok(!result.reasons.some((r) => r.includes("brand token")));
});
test("scoreUrl flags a suspicious TLD", () => {
const result = scoreUrl("https://secure.example.xyz/account");
assert.ok(result.reasons.some((r) => r.includes("TLD")));
});
test("scoreUrl tolerates an unparseable url", () => {
assert.deepEqual(scoreUrl("not a url"), { score: 0, reasons: ["unparseable url"] });
});
test("deterministicChecks surfaces credential, urgency and URL signals", () => {
const checks = deterministicChecks(projection);
assert.deepEqual(checks.requests_credentials, ["password"]);
assert.ok(checks.uses_urgency.includes("suspended"));
assert.equal(checks.links.length, 1);
assert.equal(checks.suspicious_urls.length, 0);
});
test("deterministicChecks marks a raw-IP link as suspicious", () => {
const checks = deterministicChecks({
pages: [{ name: "P", frames: [{ name: "F", text: [], links: [{ text: "go", url: "http://10.0.0.1/x" }] }] }],
});
assert.equal(checks.suspicious_urls.length, 1);
});
test("decisionState wraps the projection and the checks under named fields", () => {
const checks = deterministicChecks(projection);
const state = decisionState(projection, checks);
assert.equal(state.design, projection);
assert.equal(state.deterministic_checks, checks);
});
test("DECISION_QUESTIONS are noul questions with aligned true/false criteria", () => {
const keys = Object.keys(DECISION_QUESTIONS);
assert.ok(keys.includes("is_phishing"));
assert.ok(keys.includes("is_malicious"));
for (const [name, question] of Object.entries(DECISION_QUESTIONS)) {
assert.equal(question.type, "noul", `${name} is a noul`);
assert.equal(typeof question.instructions, "string");
assert.ok(question.instructions.length > 0, `${name} has instructions`);
assert.equal(typeof question.criteria.true, "string", `${name} has true criteria`);
assert.equal(typeof question.criteria.false, "string", `${name} has false criteria`);
}
});
test("buildDecisionsRequest assembles the OpenRouter decisions payload", () => {
const checks = deterministicChecks(projection);
const request = buildDecisionsRequest("upstage/solar-decide-flash", projection, checks);
assert.equal(request.model, "upstage/solar-decide-flash");
assert.equal(request.questions, DECISION_QUESTIONS);
assert.deepEqual(request.state, { design: projection, deterministic_checks: checks });
});