# Check it headless

> Contract 2.0.0 · package sha256 `a857eca95b35921109faf997094bb4192f12527d9d0fcd4be7e97f06425bad6e` · generated from `CHECK_HEADLESS` by `services/onsite-scripter/scripts/build-contract-docs.mjs`; edit the source, not this file.

How an author with no human confirms, before hand-off, that the product is on screen, lit and framed. The fake (`/testing`) checks your calls but draws no pixels, so this runs the real runtime in a headless browser: Playwright's bundled Chromium draws WebGL 2 in software (SwiftShader) on any machine, with or without a GPU.

## 1. The page

Save it as `check.html` beside `scene.json`, the scene model you hand off (what your server's read returns, llms.txt's "Your server's read of the stored configurator"). It boots, sets the scene model and the start view your page sets (llms.txt's boot pages: the stored `defaultView`, else a fit), waits for `idle()`, captures the view at a pixel ratio of 1 over a transparent background, so a product pixel is an opaque one whatever colour the runtime draws, and runs the three checks in the page. `window.holoCheck` is the result, a Promise.

```html
<!doctype html>
<meta charset="utf-8">
<div id="viewer" style="width: 800px; height: 600px"></div>
<script type="module">
  const RUNTIME_URL = 'https://onsite.holostep.io/serve/runtime/v2-canary/configurator-runtime.js';
  const ACCOUNT_ID = 164; // your Holostep account id
  const SCENE_URL = './scene.json'; // the scene model you hand off

  // Your page's start view, as llms.txt's boot pages set it after the first model.
  // A stored view converts as the package's cameraViewFromStored does (degrees; a stored
  // `position` wins over the angles; points copied as { x, y, z }); none frames the product.
  function startView(session, view) {
    if (!view) return session.camera.fit(undefined, { durationMs: 0 });
    const xyz = (p) => ({ x: p.x, y: p.y, z: p.z });
    const t = xyz(view.target);
    const p = view.position && xyz(view.position);
    const d = p && { x: p.x - t.x, y: p.y - t.y, z: p.z - t.z };
    const deg = 180 / Math.PI;
    const camera = d
      ? {
          azimuthDeg: Math.atan2(d.x, d.z) * deg,
          polarDeg: Math.atan2(Math.hypot(d.x, d.z), d.y) * deg,
          distance: Math.hypot(d.x, d.y, d.z),
        }
      : { azimuthDeg: view.azimuth, polarDeg: view.polar, distance: view.radius };
    const focal = view.focalOffset ? { focalOffset: xyz(view.focalOffset) } : {};
    return session.camera.set({ target: t, ...camera, ...focal }, { durationMs: 0 });
  }

  /** The product's world bounds as a screen rectangle, CSS px; null when a corner is behind the camera. */
  function projectedRect(session) {
    const box = session.find({ role: 'product' }).bounds();
    if (!box) return null;
    const corners = [];
    for (const x of [box.min.x, box.max.x])
      for (const y of [box.min.y, box.max.y])
        for (const z of [box.min.z, box.max.z]) corners.push({ x, y, z });
    const points = session.camera.project(corners);
    if (points.some((p) => p === null)) return null;
    const xs = points.map((p) => p.x);
    const ys = points.map((p) => p.y);
    return { left: Math.min(...xs), top: Math.min(...ys), right: Math.max(...xs), bottom: Math.max(...ys) };
  }

  async function check() {
    const runtime = await import(RUNTIME_URL);
    const el = document.getElementById('viewer');
    const session = await runtime.boot({ el, accountId: ACCOUNT_ID, contract: '2.0' });
    const model = await (await fetch(SCENE_URL)).json();
    const report = await session.setSceneModel(model);
    // the view your page starts on: its stored view, else a fit (no scene write moves the camera)
    await startView(session, model.defaultView);
    await session.idle();
    // Transparent: no colour stands for 'no product', so a pixel is the product's when it is
    // opaque (a ground shadow or a see-through part never is).
    const shot = await session.capture({ pixelRatio: 1, background: 'transparent' });
    const { width, height } = shot.snapshot.size; // CSS px, the image's pixels at ratio 1
    const image = await createImageBitmap(shot.image);
    const canvas = Object.assign(document.createElement('canvas'), { width, height });
    const context = canvas.getContext('2d');
    context.drawImage(image, 0, 0);
    const { data } = context.getImageData(0, 0, width, height);

    // 1. Pixels over the projected bounds: framed, drawn and lit.
    const rect = projectedRect(session);
    const area = (r) => Math.max(0, r.right - r.left) * Math.max(0, r.bottom - r.top);
    const seen = rect && {
      left: Math.max(0, Math.floor(rect.left)), top: Math.max(0, Math.floor(rect.top)),
      right: Math.min(width, Math.ceil(rect.right)), bottom: Math.min(height, Math.ceil(rect.bottom)),
    };
    let inside = 0, drawn = 0, brightness = 0;
    for (let y = seen?.top ?? 0; y < (seen?.bottom ?? 0); y++) {
      for (let x = seen.left; x < seen.right; x++) {
        const i = (y * width + x) * 4;
        inside++;
        // a product pixel: opaque in the transparent capture
        if (data[i + 3] >= 250) {
          drawn++;
          brightness += Math.max(data[i], data[i + 1], data[i + 2]) / 255;
        }
      }
    }
    const problems = session.info().problems.map((p) => `${p.code}: ${p.message}`);
    const checks = {
      // the whole product is in the view (90 % of its rectangle) and fills 5 % of it at least
      framed: rect !== null && area(seen) >= 0.9 * area(rect) && area(rect) >= 0.05 * width * height,
      // a tenth of its rectangle at least is product pixels
      drawn: inside > 0 && drawn / inside >= 0.1,
      // its pixels are not black: a light is on it
      lit: drawn > 0 && brightness / drawn >= 0.15,
      // 2. no Problem recorded, and the scene model committed
      problems: problems.length === 0 && report.outcome === 'committed',
      // 3. nothing was still moving or loading in the image
      settled: shot.snapshot.settled && shot.completed,
    };
    const ok = Object.values(checks).every(Boolean);
    return { ok, checks, problems, rect, drawnShare: inside ? drawn / inside : 0, brightness: drawn ? brightness / drawn : 0 };
  }

  window.holoCheck = check().catch((error) => ({ ok: false, error: String(error?.stack ?? error) }));
</script>
```

## 2. The script

Save it as `check.mjs`. It needs Playwright and its Chromium once: `npm i -D playwright && npx playwright install chromium`. Serve the folder over http (`npx http-server . -p 8080`, or `python3 -m http.server 8080`): a `file://` page can't import the runtime module from another origin. Then `node check.mjs http://localhost:8080/check.html`: it prints the result and exits 0 when every check passes.

```js
import { chromium } from 'playwright';

const url = process.argv[2];
// WebGL 2 in software, so it draws on any machine, GPU or not.
const browser = await chromium.launch({
  args: ['--use-gl=angle', '--use-angle=swiftshader', '--enable-unsafe-swiftshader', '--ignore-gpu-blocklist'],
});
try {
  const page = await browser.newPage({ viewport: { width: 1000, height: 800 } });
  page.on('console', (m) => ['warning', 'error'].includes(m.type()) && console.error(m.text()));
  await page.goto(url);
  const result = await page.evaluate(() => window.holoCheck);
  console.log(JSON.stringify(result, null, 2));
  process.exitCode = result.ok ? 0 : 1;
} finally {
  await browser.close();
}
```

## 3. The three checks

1. **Pixels over the projected bounds.** `find({ role: 'product' }).bounds()` projected with `camera.project` gives the product's rectangle on screen. `framed`: the whole rectangle is in the view (90 % of it at least) and fills 5 % of the view or more, from your page's start view (`startView`, as the boot pages set it: no scene write moves the camera; a page with another start view writes its own there); a product off to one side, behind the camera or a speck fails it. `drawn`: a tenth of the rectangle at least is opaque in the transparent capture; a model that failed to load, is hidden or is drawn transparent fails it, and so does a glass product (lower the bar for one). `lit`: the opaque pixels average 15 % brightness or more; a black frame (no light, a stage whose lights the page document turned off) fails it, and a product authored black reads dark, so lower the bar for one. An opaque floor or backdrop in the page document counts as product inside the rectangle: check such a stage with a page document without them.
2. **`info().problems` is empty** and the scene model committed (`Report.outcome`). Each Problem prints with its code and message: its page under `problems/` says what to do. A page with a `configuratorId` but no stored viewer-document layer records 'tenant-document-absent' by design; drop that code from the check for such a page.
3. **`snapshot.settled`** (and `completed`): no clip, tween, fade, accumulation or late texture was in flight when the image was taken, so the pixels are the product at rest. An `autoRotate` or a looping clip keeps it false: stop it for the check.

Check the image itself too: `shot.image` is a PNG; send it to your own reviewer or save it (`await page.evaluate(...)` can return it as a data URL). The fake answers the same calls without pixels, so keep your unit tests on `createFakeSession` and run this check once per scene model you hand off.
