rulechef-live / engine.js
adaamko's picture
Restore the static page (no cpu-basic quota for Gradio)
27e064c verified
Raw History Blame Contribute Delete
3.33 kB
// Browser port of cascade.py: RuleChef's executor semantics for regex NER rules, and the gate.
// Checked against the Python executor on every request and every rule set of this demo (see verify_engine.js).
(function (root) {
"use strict";
const cache = new Map();
function compile(src) {
if (!cache.has(src)) {
let re = null;
try { re = new RegExp(src, "g"); } catch (e) { re = null; }
cache.set(src, re);
}
return cache.get(src);
}
function sortKey(a, b) {
const pa = a.priority ?? 5, pb = b.priority ?? 5;
if (pa !== pb) return pb - pa;
const qa = a.validated_precision ?? a.confidence ?? 0.5, qb = b.validated_precision ?? b.confidence ?? 0.5;
if (qa !== qb) return qb - qa;
const na = (a.name || "").toLowerCase(), nb = (b.name || "").toLowerCase();
if (na !== nb) return na < nb ? -1 : 1;
const ia = a.id || "", ib = b.id || "";
return ia < ib ? -1 : ia > ib ? 1 : 0;
}
// rules: RuleChef rule dicts (content, priority, output_template {text, type}, ...)
function applyRules(rules, text) {
const out = [];
for (const r of rules.slice().sort(sortKey)) {
if ((r.format || "regex") !== "regex") continue;
const re = compile(r.content);
if (!re) continue;
const tpl = r.output_template || {};
const t = tpl.text || "$0";
re.lastIndex = 0;
for (const m of text.matchAll(re)) {
const mt = m[0];
let s = m.index, e = m.index + mt.length, val = mt;
if (/^\$\d+$/.test(t)) {
const g = parseInt(t.slice(1), 10);
val = g === 0 ? mt : (m[g] ?? "");
}
if (val && val !== mt) {
const off = mt.toLowerCase().indexOf(val.toLowerCase());
if (off >= 0) { s = s + off; e = s + val.length; }
}
if (out.some((x) => x.start === s && x.end === e)) continue;
out.push({ start: s, end: e, type: tpl.type, text: val, rule_id: r.id });
}
}
const uniq = [];
for (const it of out) {
const dup = uniq.some((ex) => {
const inter = Math.max(0, Math.min(it.end, ex.end) - Math.max(it.start, ex.start));
const uni = (it.end - it.start) + (ex.end - ex.start) - inter;
return uni > 0 && inter / uni > 0.7 && it.type === ex.type;
});
if (!dup) uniq.push(it);
}
return uniq;
}
const TOK = /§|[A-Za-z]+|\d+(?:[.,\/:-]\d+)*/g;
function tokens(text) {
const out = []; let prev = "";
for (const m of text.matchAll(TOK)) { out.push([m.index, m.index + m[0].length, m[0], prev]); prev = m[0].toLowerCase(); }
return out;
}
const shape = (tok) => tok.replace(/\d+/g, "9");
// gate: {trigger_words: [...], skip: [[prev, shape], ...]}
function unexplained(gate, text, spans) {
const words = new Set(gate.trigger_words), skip = new Set(gate.skip.map((k) => k[0] + "\u0000" + k[1]));
const out = [];
for (const [s, e, tok, prev] of tokens(text)) {
const digit = /^\d/.test(tok);
if (!(digit || words.has(tok))) continue;
if (spans.some((sp) => sp.start < e && s < sp.end)) continue;
if (digit && skip.has(prev + "\u0000" + shape(tok))) continue;
out.push(tok);
}
return out;
}
root.RCEngine = { applyRules, unexplained, compile };
})(typeof window !== "undefined" ? window : globalThis);