facett-ops/src/jsguard.rs
//! **The no-JavaScript-graphics guard, enforced on the bytes the wheel ships.**
//!
//! Rickard, 2026-09-05, verbatim: *"it must NOT use javascript to do graphics, it
//! must REUSE the RUST components, packaged in wasm. We had a real bad performance a
//! few months ago when Claude reimplemented components in JavaScript."* This module
//! is that rule made mechanical: [`scan`] reads the loader page and reports every
//! construct that would DRAW, and [`stage_package`] refuses to build a wheel around
//! a page that has any.
//!
//! [`stage_package`]: crate::pip::stage_package
//!
//! ## What is banned, and what deliberately is NOT
//!
//! The naive version of this guard bans the substring `getContext` — and it would be
//! WRONG here, in a way worth writing down. `assets/index.html` calls `getContext`
//! three times, inside one function:
//!
//! ```js
//! window.facet_canvas_api = () => {
//! try { if (c.getContext("webgpu")) return "webgpu"; } catch (_) {}
//! try { if (c.getContext("webgl2")) return "webgl2"; } catch (_) {}
//! ...
//! ```
//!
//! That is not a renderer, it is the READ-BACK that PROVES the renderer is WebGPU —
//! the hook every robot test in this lane asserts on (`facet_canvas_api() ==
//! "webgpu"`). A substring ban would delete the evidence that the law is being kept,
//! which is the worst possible trade: the guard would be green and the proof gone.
//!
//! So the rule is shaped like the actual defect instead:
//!
//! - **Drawing verbs are banned outright** — `fillRect`, `beginPath`, `lineTo`,
//! `fillText`, `drawImage`, `putImageData` and friends. There is no innocent
//! reason for any of them on a page whose pixels come from wasm.
//! - **SVG construction is banned** — `createElementNS`, a literal `<svg`. This is
//! the exact shape of the defect the audit found in `knut-pipelines/src/web.rs`:
//! a hand-written SVG DAG renderer standing in for the `pipeline` component.
//! - **Charting libraries are banned by name** — plotly, d3, chart.js, echarts,
//! vega. Importing one is reimplementing a facett component by proxy.
//! - **`getContext` is allowed ONLY inside the `facet_canvas_api` probe.** Anywhere
//! else it is a renderer acquiring a surface, and it is refused with its line
//! number.
//!
//! The point is that the guard names the DEFECT, not a keyword. A guard that cannot
//! tell a probe from a painter would be worked around within a week.
/// One reason a page was refused, with enough to find it.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Finding {
/// 1-based line number in the scanned file.
pub line: usize,
/// The banned construct, as the scanner names it.
pub construct: String,
/// The offending line, trimmed.
pub text: String,
/// Why it is banned — printed to whoever hits it, so the rule travels with the
/// refusal instead of living only in this file.
pub why: &'static str,
}
impl std::fmt::Display for Finding {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "line {}: `{}` — {}\n {}", self.line, self.construct, self.why, self.text)
}
}
/// Drawing calls. Any one of these means JavaScript is painting.
const DRAWING_VERBS: &[&str] = &[
"fillRect",
"strokeRect",
"clearRect",
"beginPath",
"closePath",
"moveTo",
"lineTo",
"bezierCurveTo",
"quadraticCurveTo",
"arcTo",
"fillText",
"strokeText",
"drawImage",
"putImageData",
"createLinearGradient",
"createRadialGradient",
];
/// DOM-drawing construction — the SVG road, which is how the knut DAG viewer draws.
const SVG_CONSTRUCTS: &[&str] = &["createElementNS", "<svg", "<SVG"];
/// Charting libraries. Reaching for one is reimplementing a facett component.
const CHART_LIBS: &[&str] =
&["plotly", "Plotly", "d3.min.js", "d3.v7", "chart.js", "Chart.js", "echarts", "vega-lite"];
/// The ONE function `getContext` may appear inside: the read-back probe that proves
/// which graphics API actually drew. Its opening line is the anchor.
const PROBE_OPEN: &str = "window.facet_canvas_api";
/// Read a page and report every construct that would draw.
///
/// Returns an empty vec for a page that only loads wasm, hands it data and reports
/// its size — which is all `assets/index.html` is allowed to be.
pub fn scan(html: &str) -> Vec<Finding> {
let mut out = Vec::new();
// Track whether we are inside the sanctioned `facet_canvas_api` probe. The probe
// is an arrow function assigned in one statement, so it ends at the first line
// whose trimmed text is `};` at the same nesting — good enough, and DELIBERATELY
// narrow: widen the allowed region and the exception becomes the rule.
let mut in_probe = false;
for (i, raw) in html.lines().enumerate() {
let line = raw.trim();
let n = i + 1;
if line.contains(PROBE_OPEN) {
in_probe = true;
} else if in_probe && (line == "};" || line == "}") {
in_probe = false;
}
for verb in DRAWING_VERBS {
if line.contains(verb) {
out.push(Finding {
line: n,
construct: (*verb).to_string(),
text: line.to_string(),
why: "JavaScript must not paint — every pixel comes from the wasm component",
});
}
}
for c in SVG_CONSTRUCTS {
if line.contains(c) {
out.push(Finding {
line: n,
construct: (*c).to_string(),
text: line.to_string(),
why: "hand-built SVG is a reimplemented component (the knut DAG viewer's defect)",
});
}
}
for lib in CHART_LIBS {
if line.contains(lib) {
out.push(Finding {
line: n,
construct: (*lib).to_string(),
text: line.to_string(),
why: "a charting library replaces a facett component instead of reusing it",
});
}
}
if line.contains("getContext") && !in_probe {
out.push(Finding {
line: n,
construct: "getContext".to_string(),
text: line.to_string(),
why: "a graphics context outside the facet_canvas_api read-back probe is a \
JavaScript renderer acquiring a surface",
});
}
}
out
}
/// [`scan`], as a refusal with every finding named.
pub fn check(label: &str, html: &str) -> Result<(), String> {
let findings = scan(html);
if findings.is_empty() {
return Ok(());
}
let mut msg = format!(
"{label} draws in JavaScript, which this package exists to not do \
({} finding(s)):\n",
findings.len()
);
for f in &findings {
msg.push_str(&format!(" {f}\n"));
}
Err(msg)
}