Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/facett

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)
}