open-standards/wai/quantum-studio/build-views.mjs
Transaction Science b894f1bbc5 wai-quantum contract over the course and the docs: the course's schemas in schemas/, its checks named, every page run, the translations in step, and the views in English
The course's documents join the version contract. The course as kept and as
served (wai.quantum.course/1) and each exercise (wai.quantum.exercise/1) move
from course/schema/ to the crate's schemas/, named and versioned as the rest;
a check, a lesson as served and a hint are wai.quantum.course-check/1, and a
check now names its engine beside its schema; the command line's progress
file is wai.quantum.learn-progress/1. The engine's test holds the course,
every exercise, every lesson in each locale and a check and hint of some to
them; the learn test holds the progress file; the contract workflow requires
both, and the eval and lease tests, by name, and runs when the answer and
eval sets its tests read change.

The docs runner reads every page the branches added: the error-correction
page, the version policy, the deprecations page, the service's self-hosting,
scheduler and provenance pages, and the views' README. Each block the other
branches wrote either runs or says why it cannot (a sweep fixture added), and
the job service it starts keeps its store in its scratch. The crate's README
names wai-quantum 0.7. The Chinese READMEs hold the English pages' code again:
the sections for error correction, sets and sweeps, the service's dry runs,
the scheduler, scoring a model, the answer layer, views and versions are
translated, and the compile pages name the gate set basis. The command
line's Chinese words cover task, qec, eval and version, and submit as it
reads now. The documentation index links the policy, the deprecations, the
error-correction page and the service's pages in both languages.

The policy lists every schema file and says the answer layer is a draft
outside its promise. The 0.7.0 heading names what the four branches bring.

The views page bundles the Studio components that now read their words
through the locale module; build-views.mjs gives it the English words alone,
so no other language's words reach a host. The result parts the Studio and
the views share read their words through it too, so the Studio's result pane
stays translated, its English unchanged.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-28 08:43:06 -04:00

124 lines
7 KiB
JavaScript

// Build the views an agent host draws (src/views/) into one self-contained
// page: the view's script (the Studio's components, bundled), the Studio's
// styles, and the engine inside the view (wai/wai-quantum-views) twice, as
// WebAssembly (base64) and as the same build translated to JavaScript. The page
// loads nothing and connects nowhere. The servers serve it for each of the six
// views, naming the view in its <meta name="wai-view">.
//
// node build-views.mjs --wasm VIEWS.wasm --js VIEWS.mjs --out DIR
//
// writes DIR/wai-quantum-views.html and DIR/manifest.json (each part's size and
// SHA-256, and every source the page is built from with its SHA-256). Run it
// through wai/wai-quantum-views/build.sh, which builds the two engines first.
//
// node build-views.mjs --check
//
// says whether the committed page (wai/wai-quantum/views/) is built from the
// sources as they are: every source it names unchanged, and no source added.
import { build, transform } from 'esbuild';
import { createHash } from 'node:crypto';
import { mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
import { dirname, join, relative } from 'node:path';
import { fileURLToPath } from 'node:url';
const here = dirname(fileURLToPath(import.meta.url));
const repo = join(here, '..', '..');
const arg = (k) => { const i = process.argv.indexOf(k); return i > 0 ? process.argv[i + 1] : null; };
const [wasmPath, jsPath, out] = [arg('--wasm'), arg('--js'), arg('--out')];
const checking = process.argv.includes('--check');
if (!checking && (!wasmPath || !jsPath || !out)) {
console.error('usage: node build-views.mjs --wasm VIEWS.wasm --js VIEWS.mjs --out DIR');
process.exit(2);
}
// The most a view may be: the servers' tests hold every view to it.
export const LIMIT = 1 << 20;
// The name the servers replace with the view's.
export const PLACEHOLDER = '__WAI_QUANTUM_VIEW__';
const sha = (b) => createHash('sha256').update(b).digest('hex');
const bytes = (s) => Buffer.byteLength(s, 'utf8');
// The views are in English: the Studio's components they share read their
// words through src/i18n.js, and here its locales are English alone, so the
// page carries no other language's words.
const englishOnly = {
name: 'english-only',
setup(b) {
b.onResolve({ filter: /\/locales\/index\.js$/ }, () => ({ path: 'english-only', namespace: 'wai-views-locales' }));
b.onLoad({ filter: /.*/, namespace: 'wai-views-locales' }, () => {
const dir = join(here, 'locales', 'en');
const areas = readdirSync(dir).filter((f) => f.endsWith('.json')).sort();
const names = areas.map((f, i) => `en${i}`);
return {
resolveDir: dir,
loader: 'js',
contents: `${areas.map((f, i) => `import ${names[i]} from './${f}';`).join('\n')}\nexport const AREAS = ${JSON.stringify(areas.map((f) => f.replace(/\.json$/, '')))};\nexport default { en: Object.assign({}, ${names.join(', ')}) };\n`,
};
});
},
};
// The view's script, and every module it bundles.
const app = await build({ absWorkingDir: here, entryPoints: [join(here, 'src/views/main.js')], bundle: true, format: 'esm', target: ['es2022'], minify: true, legalComments: 'none', write: false, metafile: true, logLevel: 'warning', plugins: [englishOnly] });
const script = app.outputFiles[0].text;
const styleFiles = ['src/tokens.css', 'src/studio.css', 'src/views/views.css'];
// Every source, relative to the repository.
const sourcesOf = (inputs) => [
...Object.keys(inputs).filter((p) => !p.startsWith('wai-views-locales:')).map((p) => join(here, p)),
...styleFiles.map((f) => join(here, f)),
join(here, 'build-views.mjs'),
...['src/lib.rs', 'Cargo.toml', 'Cargo.lock', 'build.sh'].map((f) => join(repo, 'wai/wai-quantum-views', f)),
].map((p) => relative(repo, p).split('\\').join('/')).sort();
if (checking) {
const committed = JSON.parse(readFileSync(join(repo, 'wai/wai-quantum/views/manifest.json'), 'utf8'));
const now = sourcesOf(app.metafile.inputs);
const stale = now.filter((p) => committed.sources[p] !== sha(readFileSync(join(repo, p))));
const gone = Object.keys(committed.sources).filter((p) => !now.includes(p));
if (stale.length || gone.length) {
console.error(`the committed views page is not built from these sources: ${[...stale.map((p) => `${p} changed`), ...gone.map((p) => `${p} no longer used`)].join('; ')}. Rebuild it: bash wai/wai-quantum-views/build.sh`);
process.exit(1);
}
console.log(`the committed views page is built from these ${now.length} sources`);
process.exit(0);
}
// The engine as JavaScript, made a factory: it runs only when a host's policy refuses the WebAssembly.
const engineJs = (await transform(readFileSync(jsPath, 'utf8'), { format: 'iife', globalName: '__wqEngine', minify: true, legalComments: 'none', target: 'es2022' })).code;
const factory = `globalThis.wqEngineJs=function(){${engineJs};return __wqEngine};`;
const wasm = readFileSync(wasmPath);
const css = (await transform(styleFiles.map((f) => readFileSync(join(here, f), 'utf8')).join('\n'), { loader: 'css', minify: true, legalComments: 'none' })).code;
// Text inside a <script> or <style> must not end it early.
const inert = (s, tag) => s.replace(new RegExp(`</${tag}`, 'gi'), `<\\/${tag}`);
const html = `<!doctype html>
<html lang="en" data-theme="auto">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="wai-view" content="${PLACEHOLDER}">
<meta name="generator" content="wai-quantum-views">
<title>WAI Quantum</title>
<style>${inert(css, 'style')}</style>
</head>
<body>
<main id="wq" aria-live="polite"></main>
<script>globalThis.WQ_ENGINE_WASM="${wasm.toString('base64')}";${inert(factory, 'script')}</script>
<script type="module">${inert(script, 'script')}</script>
</body>
</html>
`;
if (html.split(PLACEHOLDER).length !== 2) throw new Error('the page must name its view in one place');
if (bytes(html) > LIMIT) throw new Error(`the page is ${bytes(html)} bytes, past the limit of ${LIMIT}`);
const sources = sourcesOf(app.metafile.inputs);
const manifest = {
format: 'wai.quantum.views-bundle/1',
about: 'The page every view of WAI Quantum is served from, the view named in its <meta name="wai-view">. Built by wai/wai-quantum-views/build.sh; do not edit it by hand.',
placeholder: PLACEHOLDER,
limit_bytes: LIMIT,
bytes: { page: bytes(html), script: bytes(script), styles: bytes(css), engine_wasm: wasm.length, engine_wasm_base64: Math.ceil(wasm.length / 3) * 4, engine_js: bytes(factory) },
sha256: { page: sha(html), script: sha(script), styles: sha(css), engine_wasm: sha(wasm), engine_js: sha(factory) },
sources: Object.fromEntries(sources.map((p) => [p, sha(readFileSync(join(repo, p)))])),
};
mkdirSync(out, { recursive: true });
writeFileSync(join(out, 'wai-quantum-views.html'), html);
writeFileSync(join(out, 'manifest.json'), JSON.stringify(manifest, null, 2) + '\n');
console.log(`wrote ${join(out, 'wai-quantum-views.html')}: ${bytes(html)} bytes (script ${bytes(script)}, styles ${bytes(css)}, engine ${wasm.length} as WebAssembly and ${bytes(factory)} as JavaScript)`);