// RuleChef Live: replays data/replay.json (written by sim.py) and runs the rules in the browser (engine.js). (function () { "use strict"; const E = window.RCEngine; const $ = (s) => document.querySelector(s); const fmt = (n) => n.toLocaleString("en-US"); const pct = (a, b) => (b ? (100 * a / b).toFixed(1) + " %" : "–"); const esc = (s) => String(s).replace(/[&<>"]/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """ }[c])); const NAMES = { DATETIME: "date/time", CODE: "code", QUANTITY: "quantity" }; let R, REQ = {}, EV, VERS, GUARD, META, N, AT, S = {}, LAB = [], HOLD = []; let k = 0, timer = null; function cum(xs) { const out = [0]; let s = 0; for (const x of xs) { s += x; out.push(s); } return out; } function versionAt(i) { let lo = 0; for (let j = 0; j < AT.length; j++) if (AT[j] <= i) lo = j; return VERS[lo]; } function blockedAt(i) { return new Set(GUARD.filter((g) => g.at < i).map((g) => g.key)); } function activeRules(v, i) { const off = blockedAt(i); return v.rules.filter((r) => r.trusted && !off.has(r.key)); } function card(t, v, s) { return `
${t}
${v}
${s}
`; } function renderKpis() { const v = versionAt(k); const nr = S.rules[k], ns = S.spans[k], nc = S.check[k], na = S.agree[k], calls = S.call[k]; const lat = k ? S.lat[k] / k : 0, tlat = k ? S.tlat[k] / k : 0; const rm = EV.slice(0, k).filter((e) => e.by === "rules").map((e) => e.rule_ms).sort((a, b) => a - b); const med = rm.length ? rm[rm.length >> 1].toFixed(2) + " ms (median)" : "–"; $("#kpis").innerHTML = [ card("Requests", `${fmt(k)} / ${fmt(N)}`, `rule set v${v.v}`), card("Answered by rules", pct(nr, k), `${fmt(nr)}: ${fmt(ns)} with spans, ${fmt(nr - ns)} with nothing to extract`), card("Model calls", fmt(calls), `${fmt(k - calls)} saved (${pct(k - calls, k)}); spot checks included`), card("Mean latency", `${Math.round(lat)} ms`, `teacher on every request: ${Math.round(tlat)} ms; a rule answer: ${med}`), card("Spot-check agreement", pct(na, nc), `${na} of ${nc} checked rule answers equal the teacher's`), card("Rules answering", String(activeRules(v, k).length), `of ${v.rules.length} in v${v.v}; ${blockedAt(k).size} switched off by the guard`), ].join(""); } // ------------------------------------------------------------------ charts (SVG, two series each, crosshair tooltip) const W = 360, H = 190, P = { l: 40, r: 10, t: 8, b: 22 }; function lineChart(el, cfg) { const { series, yMax, yTicks, yFmt, markers } = cfg; const x = (v) => P.l + (W - P.l - P.r) * v / N, y = (v) => H - P.b - (H - P.t - P.b) * v / yMax; let g = ``; for (const t of yTicks) g += `${yFmt(t)}`; for (let t = 0; t <= N; t += 1000) g += `${fmt(t)}`; for (const m of markers || []) if (m.at <= k && m.at > 0) g += ``; g += ``; for (const s of series) { if (s.pts.length < 2) continue; g += ``; } g += ``; g += ``; g += `
${series.map((s) => `${esc(s.name)}`).join("")}
`; el.innerHTML = g; const svg = el.querySelector("svg"), xh = el.querySelector(".xh"), tip = $("#tip"); svg.addEventListener("mousemove", (ev) => { const b = svg.getBoundingClientRect(); const vx = (ev.clientX - b.left) / b.width * W; const req = Math.max(0, Math.min(k, Math.round((vx - P.l) / (W - P.l - P.r) * N))); const rows = series.map((s) => { let best = null; for (const p of s.pts) if (!best || Math.abs(p[0] - req) < Math.abs(best[0] - req)) best = p; return [s, best]; }).filter((r) => r[1]); if (!rows.length) return; xh.setAttribute("x1", x(rows[0][1][0])); xh.setAttribute("x2", x(rows[0][1][0])); xh.setAttribute("visibility", "visible"); tip.innerHTML = `request ${fmt(rows[0][1][0])}
` + rows.map(([s, p]) => `▬ ${esc(s.name)}: ${cfg.tipFmt(p[1])}`).join("
"); tip.hidden = false; tip.style.left = Math.min(ev.clientX + 12, window.innerWidth - 230) + "px"; tip.style.top = (ev.clientY + 12) + "px"; }); svg.addEventListener("mouseleave", () => { tip.hidden = true; xh.setAttribute("visibility", "hidden"); }); } function windowPts(arr, w = 200, step = 20) { const pts = []; for (let i = step; i <= k; i += step) { const lo = Math.max(0, i - w); pts.push([i, (arr[i] - arr[lo]) / (i - lo)]); } return pts; } const C1 = () => getComputedStyle(document.documentElement).getPropertyValue("--s1").trim(); const C2 = () => getComputedStyle(document.documentElement).getPropertyValue("--s2").trim(); function renderCharts() { const markers = VERS.slice(1); lineChart($("#c-cov"), { label: "Share of requests answered by rules", yMax: 1, yTicks: [0, 0.25, 0.5, 0.75, 1], yFmt: (t) => Math.round(t * 100) + "%", tipFmt: (v) => (100 * v).toFixed(1) + " %", markers, series: [{ name: "answered by rules", color: C1(), pts: windowPts(S.rules) }, { name: "rules found spans", color: C2(), pts: windowPts(S.spans) }] }); const step = 20, a = [], b = []; for (let i = 0; i <= k; i += step) { a.push([i, S.call[i]]); b.push([i, i]); } lineChart($("#c-calls"), { label: "Model calls", yMax: N, yTicks: [0, 1000, 2000, 3000], yFmt: (t) => fmt(t), tipFmt: (v) => fmt(v), markers, series: [{ name: "with rules", color: C1(), pts: a }, { name: "teacher on every request", color: C2(), pts: b }] }); lineChart($("#c-lat"), { label: "Latency per request", yMax: 500, yTicks: [0, 100, 200, 300, 400, 500], yFmt: (t) => String(t), tipFmt: (v) => Math.round(v) + " ms", markers, series: [{ name: "cascade", color: C1(), pts: windowPts(S.lat) }, { name: "teacher on every request", color: C2(), pts: windowPts(S.tlat) }] }); } // ------------------------------------------------------------------ tables function renderVersions() { const cur = versionAt(k).v; const head = `versionfrom requestlabelled examplesrules answering / learned synthesis sholdout answered by rulessame answer as teacher rule spans P / R vs teachercascade F1 vs teacherTAB annotation F1: teacher / cascade`; const rows = VERS.filter((v) => v.at <= k).map((v) => { const h = v.holdout, ra = h.rules_answer; return `v${v.v}${fmt(v.at)}${fmt(v.n_labelled)} ${v.rules.filter((r) => r.trusted).length} / ${v.rules.length}${v.synth_s.toFixed(1)} ${pct(ra, h.n)}${ra ? pct(h.exact_agree, ra) : "–"} ${ra ? h.routed_t.P.toFixed(2) + " / " + h.routed_t.R.toFixed(2) : "–"} ${h.casc_t.F1.toFixed(3)}${h.teach_g.F1.toFixed(3)} / ${h.casc_g.F1.toFixed(3)}`; }); $("#t-vers").innerHTML = `${head}${rows.join("")}`; } function renderRules() { const v = versionAt(k), off = blockedAt(k), used = {}; for (let i = 0; i < k; i++) if (EV[i].by === "rules") for (const key of EV[i].fired) used[key] = (used[key] || 0) + 1; const rows = v.rules.map((r) => { const n = r.tp + r.fp; const st = off.has(r.key) ? ["cut", "switched off"] : r.trusted ? ["on", "answering"] : ["off", "not trusted"]; return { r, st, n, used: used[r.key] || 0 }; }).sort((a, b) => (a.st[0] !== "on") - (b.st[0] !== "on") || b.used - a.used || b.n - a.n); $("#rules-help").textContent = v.v === 0 ? "No rule set yet: every request goes to the teacher until 50 examples are labelled." : `v${v.v}, learned from ${fmt(v.n_labelled)} labelled examples in ${v.synth_s.toFixed(1)} s. "Answering" rules matched at least ${META.trust_min} times on the labelled data with precision at least ${META.trust_prec}; the others are kept but do not answer. The last column counts requests this exact rule answered so far.`; const head = `statustyperegexoutputlabelled matches (precision)requests answered`; const tr = ({ r, st, n, used: u }) => `${st[1]}${r.type}${esc(r.regex)} ${esc(r.template)}${n ? `${n} (${(r.tp / n).toFixed(2)})` : "0"}${u}`; const on = rows.filter((x) => x.st[0] !== "off"), rest = rows.filter((x) => x.st[0] === "off"); $("#t-rules").innerHTML = head + "" + (on.map(tr).join("") || 'No answering rules.') + ""; const det = $("#rules-rest"); const open = det.open; det.innerHTML = rest.length ? `${rest.length} learned rules that do not answer (precision below ${META.trust_prec} or too few matches)
${head}${rest.map(tr).join("")}
` : ""; det.open = open; const g = v.gate; let html = ""; if (g) { html += `

Gate of v${v.v}. A request goes to the rules only if every trigger token is covered by an answering rule or sits in a skip context. Trigger tokens: anything with a digit, plus ${g.trigger_words.length} learned words: ${g.trigger_words.map((w) => `${esc(w)}`).join(", ")}.

Skip contexts (previous word + digit shape, 9 = digits) that the teacher labels at most ${Math.round(META.skip_ratio * 100)} % of the time: ${g.skip.map(([p, s]) => `${esc(p || "(start)")} ${esc(s)}`).join(", ") || "none"}.

`; } const gl = GUARD.filter((x) => x.at < k); html += gl.length ? `

Drift guard. ${gl.map((x) => `at request ${x.at}: ${esc(x.key.split("\t")[1])} switched off (spot checks ${x.tp} right, ${x.fp} wrong)`).join("; ")}.

` : `

Drift guard. No rule switched off so far. A rule is switched off when its spot-check precision drops below ${META.guard_prec} after ${META.guard_min} checked spans; in this run none did.

`; $("#gate").innerHTML = html; } function highlight(text, spans) { const ss = spans.slice().sort((a, b) => a.start - b.start || b.end - a.end); let out = "", pos = 0; for (const s of ss) { if (s.start < pos) continue; out += esc(text.slice(pos, s.start)) + `${esc(text.slice(s.start, s.end))}${NAMES[s.type] || s.type}`; pos = s.end; } return out + esc(text.slice(pos)); } function renderRecent() { const items = []; for (let i = k - 1; i >= Math.max(0, k - 6); i--) { const e = EV[i], q = REQ[e.id]; let spans, by; if (e.by === "rules") { spans = E.applyRules(activeRules(versionAt(i), i).map((r) => r.dict), q.text); by = `rules (v${e.v}, ${e.rule_ms.toFixed(2)} ms)` + (e.checked ? `, spot-checked: ${e.agree ? "same as the teacher" : "differs from the teacher"}` : ""); } else { spans = q.teacher; by = `teacher (${Math.round(e.teacher_ms)} ms), answer added to the labelled examples`; } items.push(`
  • #${fmt(i + 1)} answered by ${by}
    ${highlight(q.text, spans)}${spans.length ? "" : ' (nothing to extract)'}
  • `); } $("#recent").innerHTML = items.join("") || "
  • No requests yet. Press Play.
  • "; } function render() { $("#pos").value = k; $("#posout").textContent = `${fmt(k)} requests`; renderKpis(); renderCharts(); renderVersions(); renderRules(); renderRecent(); } // ------------------------------------------------------------------ playback function stop() { if (timer) clearTimeout(timer); timer = null; $("#play").textContent = "Play"; } function tick() { k = Math.min(N, k + parseInt($("#speed").value, 10)); render(); if (k >= N) { stop(); return; } timer = setTimeout(tick, 280); } $("#play").addEventListener("click", () => { if (timer) { stop(); return; } if (k >= N) k = 0; $("#play").textContent = "Pause"; tick(); }); $("#pos").addEventListener("input", (ev) => { stop(); k = parseInt(ev.target.value, 10); render(); }); // ------------------------------------------------------------------ try your own text function tryText(text, recorded) { text = (text || "").trim().slice(0, 2000); if (!text) { $("#status").textContent = "Type or paste a sentence."; return; } let v = versionAt(k); if (!v.gate) v = VERS[VERS.length - 1]; const rules = activeRules(v, Math.max(k, v.at)).map((r) => r.dict); const t0 = performance.now(); const spans = E.applyRules(rules, text); const un = E.unexplained(v.gate, text, spans); const ms = performance.now() - t0; const rec = recorded || HOLD.find((q) => q.text === text); if (!un.length) { $("#status").innerHTML = `Answered by rules (v${v.v}, ${rules.length} answering rules) in ${ms.toFixed(2)} ms in your browser. No model call.`; $("#out").innerHTML = `Answer (rules)${highlight(text, spans)}`; $("#out-teacher").innerHTML = rec ? `Teacher's recorded answer for this holdout sentence, for comparison${highlight(rec.text, rec.teacher)}` : ""; } else { $("#status").innerHTML = `The gate sends this request to the teacher: uncovered trigger tokens ${un.slice(0, 12).map((u) => `${esc(u)}`).join(", ")}. ` + `Rules took ${ms.toFixed(2)} ms. The teacher does not run on this static page` + (rec ? "; its recorded answer for this holdout sentence is shown." : "."); $("#out").innerHTML = rec ? `Answer (teacher, recorded offline)${highlight(rec.text, rec.teacher)}` : ""; $("#out-teacher").innerHTML = `What the rules found${highlight(text, spans)}`; } } $("#go").addEventListener("click", () => tryText($("#inp").value)); $("#pick").addEventListener("click", () => { const q = HOLD[Math.floor(Math.random() * HOLD.length)]; $("#inp").value = q.text; tryText(q.text, q); }); document.querySelectorAll("button.ex").forEach((b) => b.addEventListener("click", () => { $("#inp").value = b.dataset.ex; tryText(b.dataset.ex); })); window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", () => R && renderCharts()); // ------------------------------------------------------------------ load Promise.all([fetch("data/replay.json").then((r) => r.json()), fetch("data/requests.jsonl").then((r) => r.text())]).then(([rep, txt]) => { R = rep; META = R.meta; VERS = R.versions; EV = R.events; GUARD = R.guard; N = EV.length; AT = VERS.map((v) => v.at); for (const line of txt.trim().split("\n")) { const q = JSON.parse(line); REQ[q.id] = q; if (q.part === "holdout") HOLD.push(q); } S.rules = cum(EV.map((e) => +(e.by === "rules"))); S.spans = cum(EV.map((e) => +(e.by === "rules" && e.fired.length > 0))); S.check = cum(EV.map((e) => +!!e.checked)); S.agree = cum(EV.map((e) => +!!e.agree)); S.call = cum(EV.map((e) => +(e.by === "teacher" || !!e.checked))); S.lat = cum(EV.map((e) => (e.by === "rules" ? e.rule_ms : e.rule_ms + e.teacher_ms))); S.tlat = cum(EV.map((e) => e.teacher_ms)); document.querySelectorAll("[data-n-stream]").forEach((x) => (x.textContent = fmt(META.n_stream))); document.querySelectorAll("[data-n-holdout]").forEach((x) => (x.textContent = fmt(META.n_holdout))); const set = (sel, v) => document.querySelectorAll(sel).forEach((x) => (x.textContent = v)); set("[data-trust-min]", META.trust_min); set("[data-trust-prec]", META.trust_prec); set("[data-spot]", META.spot_every); set("[data-guard-prec]", META.guard_prec); set("[data-guard-min]", META.guard_min); $("#pos").max = N; k = N; render(); tryText($("#inp").value); }).catch((err) => { $("#kpis").textContent = "Could not load the replay data: " + err; }); })();