commit bbc3e818b865c27b6f5c3f03e809d36b2d07597f
parent 2dc371aa8c81b65a2a87b987f2d1719e7873ba63
Author: James Eversole <james@eversole.co>
Date: Wed, 14 Oct 2026 08:42:01 -0500
Bring bundle downloads to the forefront
Diffstat:
9 files changed, 217 insertions(+), 81 deletions(-)
diff --git a/ext/demo-site/README.md b/ext/demo-site/README.md
@@ -181,7 +181,7 @@ pinned bundle and suggested inputs. Demo names appear only under Examples, not
again under Published packages. Examples with multiple releases show the version
picker and **Load package**; changing versions invalidates prior results, and
older releases load their exact bundle without borrowing current editable source.
-Single-release examples omit the version controls. The Published packages group contains
+Single-release examples also show an active version selector. The Published packages group contains
libraries and other non-demo packages, which reveal the version picker
and **Load package**. Hash loading reveals **Bundle hash (SHA-256)** and **Load
bundle**, accepting arbitrary browser-compatible bundles from this site's registry.
@@ -231,8 +231,12 @@ source is retained after cancellation or failure. No optional execution limits
are enabled by default. Compilation speed depends on the source and pinned
compiler; the notice does not prescribe an expected duration.
-**Download published .arboricx** returns the original, bounded, hash-verified bundle
-bytes, not a re-encoding, applied result or local draft. Textual exports likewise
+**Download .arboricx**, beside the primary **Load package** action in both
+Playground and Export, returns the original, bounded, hash-verified bundle
+bytes for the selected version or hash, not a re-encoding, applied result or local
+draft. Download verifies the bundle independently; it does not require Load,
+change the loaded executable, compile source or run a program. Narrow layouts
+place the download button on its own full-width toolbar row. Textual exports likewise
contain a selected stored definition. These actions and published sharing are
disabled while authoring a draft; restore the published source first. Draft bundle
publication/export is not part of this profile.
diff --git a/ext/demo-site/dist/app.js b/ext/demo-site/dist/app.js
@@ -56,7 +56,6 @@ let loaded = false, worker, pending;
let automaticRun = false;
let savedRun;
let preferredExport;
-let downloadAfterLoad = false;
const traceRows = new Map();
function pauseClock() { deadline.pause(); }
function clock() { deadline.start(); }
@@ -91,7 +90,7 @@ function controls(working) {
? 'Source is shown for reference only. This package does not support editing or local compilation.'
: 'This is a compiled package. Editable source is not included; you can run or export its stored definitions.';
restoreButton.disabled = busy || originalSource === undefined || !draft;
- downloadButton.disabled = busy || draft || !loaded;
+ downloadButton.disabled = busy || draft || downloadHash() === undefined;
el('authorship').textContent = draft ? 'Local draft · your authored code (not published)' : loaded ? 'Published package · verified executable' : 'Published selection · not yet verified';
el('authorship').classList.toggle('draft', draft);
sourceEditor.classList.toggle('draft', draft);
@@ -150,6 +149,10 @@ function finish(message, error = false, reason = 'failure') {
el('package-info').textContent = `Package not loaded. ${message}`;
if (treeExport.pending || (activePane() === 'export-definition' && !loaded))
treeExport.fail(message);
+ if (operation === 'downloading') {
+ el('bundle-download-status').textContent = message;
+ el('bundle-download-status').classList.toggle('error', error);
+ }
dispose();
el('status').textContent = error ? 'Stopped' : 'Finished';
el('result').textContent = message;
@@ -210,24 +213,6 @@ function newWorker(runAfterLoad, preserveInputs = false, exportAfterLoad) {
lineInput.focus();
}
break;
- case 'downloaded': {
- if (data.hash !== selection?.hash || draft)
- break;
- pauseClock();
- operation = undefined;
- controls(false);
- el('status').textContent = 'Ready';
- const url = URL.createObjectURL(new Blob([new Uint8Array(data.bytes)], { type: 'application/vnd.arboricx.bundle' }));
- const anchor = document.createElement('a');
- anchor.href = url;
- anchor.download = `${data.hash}.arboricx`;
- document.body.append(anchor);
- anchor.click();
- anchor.remove();
- setTimeout(() => URL.revokeObjectURL(url), 1000);
- el('result').textContent = `Downloaded original verified bundle ${data.hash}. No draft or applied result included.`;
- break;
- }
case 'loaded': {
currentVerified = true;
loaded = true;
@@ -244,7 +229,7 @@ function newWorker(runAfterLoad, preserveInputs = false, exportAfterLoad) {
preferredExport = undefined;
treeExport.selection(exportsInput);
el('package-info').textContent = `SHA-256 verified · ${data.nodes.toLocaleString()} stored DAG nodes · ${data.exports.length} export(s) · ${data.hash}`;
- if (!preserveInputs && !automaticRun && !exportAfterLoad && !downloadAfterLoad) {
+ if (!preserveInputs && !automaticRun && !exportAfterLoad) {
originalSource = selected?.environment !== undefined && data.source !== null ? data.source : undefined;
sourceEditor.value = originalSource ?? '';
sourceEditor.hidden = originalSource === undefined;
@@ -253,15 +238,7 @@ function newWorker(runAfterLoad, preserveInputs = false, exportAfterLoad) {
sourceAvailable = data.source !== null;
el('source-label').textContent = data.source === null ? 'Package metadata' : 'Curated tricu / stable';
el('source').textContent = data.source ?? `Verified package metadata\n${JSON.stringify(data.metadata, null, 2)}\n\nChoose an export, its arguments and a result interpretation to apply this tree.`;
- if (downloadAfterLoad) {
- downloadAfterLoad = false;
- pauseClock();
- operation = 'downloading';
- deadline.reset(PACKAGE_ACTIVE_MS);
- clock();
- send({ type: 'download' });
- }
- else if (exportAfterLoad) {
+ if (exportAfterLoad) {
pauseClock();
startExport(exportAfterLoad, send);
}
@@ -354,7 +331,6 @@ function select(entry, release) {
draft = false;
scratch = false;
originalSource = undefined;
- downloadAfterLoad = false;
dispose();
clearOutput();
clearTreeExport();
@@ -374,12 +350,12 @@ function select(entry, release) {
newWorker(false);
}
function invalidateProgram() {
+ el('bundle-download-status').textContent = '';
editingSource = false;
sourceAvailable = false;
draft = false;
scratch = false;
originalSource = undefined;
- downloadAfterLoad = false;
dispose();
clearOutput();
clearTreeExport();
@@ -512,9 +488,12 @@ function releaseProgram(release) {
return entry ? `demo:${entry.id}` : `package:${release.name}`;
}
function chooseProgram(value, version) {
+ el('bundle-download-status').textContent = '';
programPicker.value = value;
const name = programPackage(value);
- el('program-release').hidden = name === undefined || (value.startsWith('demo:') && releases.filter(r => r.name === name).length < 2);
+ el('program-release').hidden = name === undefined;
+ el('package-toolbar').hidden = name === undefined && value !== 'hash';
+ el('bundle-hash-help').hidden = value !== 'hash';
el('program-hash').hidden = value !== 'hash';
if (name && versionPackage !== name) {
versionPackage = name;
@@ -590,20 +569,72 @@ el('load-release').onclick = () => {
else
loadBundle(release);
};
+function downloadHash() {
+ const name = programPackage(programPicker.value);
+ if (name)
+ return releases.find(r => r.name === name && r.version === versionPicker.value)?.hash;
+ if (programPicker.value === 'hash') {
+ try {
+ return bundleHash(hashInput.value);
+ }
+ catch {
+ return undefined;
+ }
+ }
+ return undefined;
+}
downloadButton.onclick = () => {
- if (draft || !loaded || !selection)
+ const hash = downloadHash();
+ if (busy || draft || !hash)
return;
- if (worker) {
- operation = 'downloading';
- controls(true);
- deadline.reset(PACKAGE_ACTIVE_MS);
- clock();
- worker.postMessage({ type: 'download' });
- }
- else {
- downloadAfterLoad = true;
- newWorker(false, true);
- }
+ // Download has its own verified target, not the loaded executable selection.
+ // It never changes source, definitions, arguments or the Run/Export target.
+ dispose();
+ operation = 'downloading';
+ const current = new Worker(new URL('./worker.js', import.meta.url), { type: 'module' });
+ worker = current;
+ controls(true);
+ el('status').textContent = 'Downloading bundle';
+ el('bundle-download-status').textContent = 'Verifying published bundle…';
+ el('bundle-download-status').classList.remove('error');
+ deadline.reset(PACKAGE_ACTIVE_MS);
+ clock();
+ current.onmessage = ({ data }) => {
+ if (worker !== current)
+ return;
+ if (data.type === 'loaded') {
+ if (data.hash !== hash) {
+ finish('Bundle download target mismatch', true);
+ return;
+ }
+ current.postMessage({ type: 'download' });
+ }
+ else if (data.type === 'downloaded') {
+ if (data.hash !== hash) {
+ finish('Bundle download target mismatch', true);
+ return;
+ }
+ const url = URL.createObjectURL(new Blob([new Uint8Array(data.bytes)], { type: 'application/vnd.arboricx.bundle' }));
+ const anchor = document.createElement('a');
+ anchor.href = url;
+ anchor.download = `${hash}.arboricx`;
+ document.body.append(anchor);
+ anchor.click();
+ anchor.remove();
+ setTimeout(() => URL.revokeObjectURL(url), 1000);
+ const message = `Downloaded original verified bundle ${hash}. No draft or applied result included.`;
+ dispose();
+ el('status').textContent = loaded ? 'Ready' : 'Load package';
+ el('bundle-download-status').textContent = message;
+ el('result').textContent = message;
+ el('result').className = '';
+ }
+ else if (data.type === 'error')
+ finish(data.message, true);
+ };
+ current.onerror = event => { if (worker === current)
+ finish(event.message || 'Worker error', true); };
+ current.postMessage({ type: 'load', hash, uncapped: true });
};
el('edit-source').onclick = () => {
editingSource = true;
diff --git a/ext/demo-site/index.html b/ext/demo-site/index.html
@@ -34,8 +34,13 @@
<div data-package-slot><div id="package-selection">
<div class="program-choice"><label for="program-picker">Program</label><select id="program-picker" disabled><option value="">Loading programs…</option></select></div>
- <div id="program-release" class="program-input" hidden><label for="package-version">Package version</label><div class="input-row"><select id="package-version" aria-label="Exact package version"></select><button id="load-release" type="button">Load package</button></div></div>
- <form id="program-hash" class="program-input" hidden><label for="bundle-hash">Bundle hash (SHA-256)</label><div class="input-row"><input id="bundle-hash" maxlength="64" spellcheck="false" autocomplete="off" aria-describedby="bundle-hash-help"><button id="load" type="submit">Load bundle</button></div><p id="bundle-hash-help" class="hint">Paste the 64-character hash of an Arboricx bundle to load it from this site's registry.</p></form>
+ <div id="package-toolbar" class="package-toolbar" hidden>
+ <div id="program-release" class="release-choice" hidden><label for="package-version">Package version</label><div class="input-row"><select id="package-version" aria-label="Exact package version"></select><button id="load-release" type="button" class="primary">Load package</button></div></div>
+ <form id="program-hash" class="hash-choice" hidden><label for="bundle-hash">Bundle hash (SHA-256)</label><div class="input-row"><input id="bundle-hash" maxlength="64" spellcheck="false" autocomplete="off" aria-describedby="bundle-hash-help"><button id="load" type="submit" class="primary">Load bundle</button></div></form>
+ <button type="button" id="download-bundle" disabled>↓ Download .arboricx</button>
+ </div>
+ <p id="bundle-hash-help" class="hint" hidden>Paste the 64-character hash of an Arboricx bundle to load or download it from this site's registry.</p>
+ <p id="bundle-download-status" class="hint" role="status"></p>
<section class="bundle-details" aria-label="Bundle details"><p id="release-info" class="hint">No named release selected.</p><p id="package-info" class="hint">Loading program catalogue…</p></section>
</div></div>
<div class="program-workspace">
@@ -73,7 +78,7 @@
<p class="hint">Use [] for its arguments in raw JSON when a definition needs no inputs. Raw types: string, number (decimal string), bool, bytes, tree (ternary), and list.</p>
</details>
<div class="result-codec"><div><label for="output-type">Result interpretation</label><select id="output-type"><option value="tree">Tree graph</option><option value="string">String</option><option value="number">Natural number</option><option value="bool">Boolean</option><option value="bytes">Bytes</option><option value="list">List of trees</option><option value="record">Scalar record</option></select></div></div>
- <div><label for="mode">Execution mode</label><select id="mode"><option value="pure">Pure value</option><option value="io">Text / line IO</option></select></div><div class="controls"><button type="button" id="reset">Reset</button><button type="button" id="share" disabled>Copy published selection link</button><button type="button" id="download-bundle" disabled>Download published .arboricx</button></div> <div class="step-controls"><label><input type="checkbox" id="step-mode"> Pause before each IO action</label><button type="button" id="next" disabled>Next action →</button></div>
+ <div><label for="mode">Execution mode</label><select id="mode"><option value="pure">Pure value</option><option value="io">Text / line IO</option></select></div><div class="controls"><button type="button" id="reset">Reset</button><button type="button" id="share" disabled>Copy published selection link</button></div> <div class="step-controls"><label><input type="checkbox" id="step-mode"> Pause before each IO action</label><button type="button" id="next" disabled>Next action →</button></div>
<details id="package-settings" class="execution-limits"><summary id="package-limits-summary">Optional resource limits</summary><div class="limits-content"><div id="package-resource-choice" class="resource-choice">
<p>Optional limits: <strong id="package-resource-mode">Off</strong></p>
<label><input id="uncapped" type="checkbox" checked aria-describedby="package-resource-warning"> Run without optional limits</label>
diff --git a/ext/demo-site/src/app.ts b/ext/demo-site/src/app.ts
@@ -51,7 +51,6 @@ let loaded = false, worker: Worker | undefined, pending: { kind: 'input' | 'step
let automaticRun = false;
let savedRun: Extract<ClientMessage, { type: 'run' }> | undefined;
let preferredExport: string | undefined;
-let downloadAfterLoad = false;
const traceRows = new Map<number, HTMLLIElement>();
function pauseClock(): void { deadline.pause(); }
function clock(): void { deadline.start(); }
@@ -82,7 +81,7 @@ function controls(working: boolean): void {
? 'Source is shown for reference only. This package does not support editing or local compilation.'
: 'This is a compiled package. Editable source is not included; you can run or export its stored definitions.';
restoreButton.disabled = busy || originalSource === undefined || !draft;
- downloadButton.disabled = busy || draft || !loaded;
+ downloadButton.disabled = busy || draft || downloadHash() === undefined;
el('authorship').textContent = draft ? 'Local draft · your authored code (not published)' : loaded ? 'Published package · verified executable' : 'Published selection · not yet verified';
el('authorship').classList.toggle('draft', draft);
sourceEditor.classList.toggle('draft', draft);
@@ -117,6 +116,7 @@ function finish(message: string, error = false, reason: 'failure' | 'timeout' |
if (error && reason === 'failure') message = operationFailure(operation, message);
if (!draft && !loaded) el('package-info').textContent = `Package not loaded. ${message}`;
if (treeExport.pending || (activePane() === 'export-definition' && !loaded)) treeExport.fail(message);
+ if (operation === 'downloading') { el('bundle-download-status').textContent = message; el('bundle-download-status').classList.toggle('error', error); }
dispose(); el('status').textContent = error ? 'Stopped' : 'Finished';
el('result').textContent = message; el('result').className = error ? 'error' : '';
for (const row of traceRows.values()) row.classList.remove('pending');
@@ -149,16 +149,6 @@ function newWorker(runAfterLoad: boolean, preserveInputs = false, exportAfterLoa
if (data.state === 'input') lineInput.focus();
}
break;
- case 'downloaded': {
- if (data.hash !== selection?.hash || draft) break;
- pauseClock(); operation = undefined; controls(false); el('status').textContent = 'Ready';
- const url = URL.createObjectURL(new Blob([new Uint8Array(data.bytes)], { type: 'application/vnd.arboricx.bundle' }));
- const anchor = document.createElement('a'); anchor.href = url; anchor.download = `${data.hash}.arboricx`;
- document.body.append(anchor); anchor.click(); anchor.remove();
- setTimeout(() => URL.revokeObjectURL(url), 1000);
- el('result').textContent = `Downloaded original verified bundle ${data.hash}. No draft or applied result included.`;
- break;
- }
case 'loaded': {
currentVerified = true; loaded = true;
const oldExport = exportAfterLoad?.export ?? (automaticRun ? savedRun?.export : preferredExport ?? selected?.export);
@@ -169,7 +159,7 @@ function newWorker(runAfterLoad: boolean, preserveInputs = false, exportAfterLoa
if (oldExport && data.exports.some(e => e.name === oldExport)) exportsInput.value = oldExport;
preferredExport = undefined; treeExport.selection(exportsInput);
el('package-info').textContent = `SHA-256 verified · ${data.nodes.toLocaleString()} stored DAG nodes · ${data.exports.length} export(s) · ${data.hash}`;
- if (!preserveInputs && !automaticRun && !exportAfterLoad && !downloadAfterLoad) {
+ if (!preserveInputs && !automaticRun && !exportAfterLoad) {
originalSource = selected?.environment !== undefined && data.source !== null ? data.source : undefined;
sourceEditor.value = originalSource ?? ''; sourceEditor.hidden = originalSource === undefined;
el('source-notes').hidden = originalSource !== undefined;
@@ -177,8 +167,7 @@ function newWorker(runAfterLoad: boolean, preserveInputs = false, exportAfterLoa
sourceAvailable = data.source !== null;
el('source-label').textContent = data.source === null ? 'Package metadata' : 'Curated tricu / stable';
el('source').textContent = data.source ?? `Verified package metadata\n${JSON.stringify(data.metadata, null, 2)}\n\nChoose an export, its arguments and a result interpretation to apply this tree.`;
- if (downloadAfterLoad) { downloadAfterLoad = false; pauseClock(); operation = 'downloading'; deadline.reset(PACKAGE_ACTIVE_MS); clock(); send({ type: 'download' }); }
- else if (exportAfterLoad) { pauseClock(); startExport(exportAfterLoad, send); }
+ if (exportAfterLoad) { pauseClock(); startExport(exportAfterLoad, send); }
else if (automaticRun && savedRun) { pauseClock(); runClock(); automaticRun = false; send(savedRun); }
else { if (!preserveInputs && selected?.presets?.[exportsInput.value]) setPreset(selected, exportsInput.value); pauseClock(); operation = undefined; controls(false); el('status').textContent = 'Ready'; }
break;
@@ -219,7 +208,7 @@ function newWorker(runAfterLoad: boolean, preserveInputs = false, exportAfterLoa
}
function select(entry: CatalogueEntry, release?: Release): void {
editingSource = false; sourceAvailable = false;
- draft = false; scratch = false; originalSource = undefined; downloadAfterLoad = false;
+ draft = false; scratch = false; originalSource = undefined;
dispose(); clearOutput(); clearTreeExport(); selected = entry; loaded = false;
el<HTMLDetailsElement>('source-section').open = true;
el<HTMLDetailsElement>('try-program').open = true;
@@ -232,8 +221,9 @@ function select(entry: CatalogueEntry, release?: Release): void {
newWorker(false);
}
function invalidateProgram(): void {
+ el('bundle-download-status').textContent = '';
editingSource = false; sourceAvailable = false;
- draft = false; scratch = false; originalSource = undefined; downloadAfterLoad = false;
+ draft = false; scratch = false; originalSource = undefined;
dispose(); clearOutput(); clearTreeExport(); loaded = false; selection = undefined; selected = undefined; preferredExport = undefined;
exportsInput.replaceChildren(); treeExport.selection(exportsInput);
sourceEditor.hidden = true; el('source-notes').hidden = false;
@@ -306,9 +296,12 @@ function releaseProgram(release: Release): string {
return entry ? `demo:${entry.id}` : `package:${release.name}`;
}
function chooseProgram(value: string, version?: string): void {
+ el('bundle-download-status').textContent = '';
programPicker.value = value;
const name = programPackage(value);
- el('program-release').hidden = name === undefined || (value.startsWith('demo:') && releases.filter(r => r.name === name).length < 2);
+ el('program-release').hidden = name === undefined;
+ el('package-toolbar').hidden = name === undefined && value !== 'hash';
+ el('bundle-hash-help').hidden = value !== 'hash';
el('program-hash').hidden = value !== 'hash';
if (name && versionPackage !== name) {
versionPackage = name;
@@ -351,10 +344,40 @@ el<HTMLButtonElement>('load-release').onclick = () => {
if (demo) select(demo, release);
else loadBundle(release);
};
+function downloadHash(): BundleHash | undefined {
+ const name = programPackage(programPicker.value);
+ if (name) return releases.find(r => r.name === name && r.version === versionPicker.value)?.hash;
+ if (programPicker.value === 'hash') {
+ try { return bundleHash(hashInput.value); } catch { return undefined; }
+ }
+ return undefined;
+}
downloadButton.onclick = () => {
- if (draft || !loaded || !selection) return;
- if (worker) { operation = 'downloading'; controls(true); deadline.reset(PACKAGE_ACTIVE_MS); clock(); worker.postMessage({ type: 'download' }); }
- else { downloadAfterLoad = true; newWorker(false, true); }
+ const hash = downloadHash(); if (busy || draft || !hash) return;
+ // Download has its own verified target, not the loaded executable selection.
+ // It never changes source, definitions, arguments or the Run/Export target.
+ dispose(); operation = 'downloading';
+ const current = new Worker(new URL('./worker.js', import.meta.url), { type: 'module' }); worker = current;
+ controls(true); el('status').textContent = 'Downloading bundle';
+ el('bundle-download-status').textContent = 'Verifying published bundle…'; el('bundle-download-status').classList.remove('error');
+ deadline.reset(PACKAGE_ACTIVE_MS); clock();
+ current.onmessage = ({ data }: MessageEvent<WorkerMessage>): void => {
+ if (worker !== current) return;
+ if (data.type === 'loaded') {
+ if (data.hash !== hash) { finish('Bundle download target mismatch', true); return; }
+ current.postMessage({ type: 'download' });
+ } else if (data.type === 'downloaded') {
+ if (data.hash !== hash) { finish('Bundle download target mismatch', true); return; }
+ const url = URL.createObjectURL(new Blob([new Uint8Array(data.bytes)], { type: 'application/vnd.arboricx.bundle' }));
+ const anchor = document.createElement('a'); anchor.href = url; anchor.download = `${hash}.arboricx`;
+ document.body.append(anchor); anchor.click(); anchor.remove(); setTimeout(() => URL.revokeObjectURL(url), 1000);
+ const message = `Downloaded original verified bundle ${hash}. No draft or applied result included.`;
+ dispose(); el('status').textContent = loaded ? 'Ready' : 'Load package';
+ el('bundle-download-status').textContent = message; el('result').textContent = message; el('result').className = '';
+ } else if (data.type === 'error') finish(data.message, true);
+ };
+ current.onerror = event => { if (worker === current) finish(event.message || 'Worker error', true); };
+ current.postMessage({ type: 'load', hash, uncapped: true });
};
el<HTMLButtonElement>('edit-source').onclick = () => {
editingSource = true; el<HTMLDetailsElement>('source-section').open = true; controls(false); sourceEditor.focus();
diff --git a/ext/demo-site/style.css b/ext/demo-site/style.css
@@ -249,3 +249,20 @@ button.primary.draft { background:#8b581b; }
.execution-summary #status { font-size:12px; font-weight:400; color:var(--muted); }
.execution-panel .trace-panel { margin:18px 20px; }
@media(max-width:740px) { .execution-disclosure > summary { padding:14px 16px; } }
+
+/* Keep version/load/download together and promote Load without hiding Download. */
+.package-toolbar { display:flex; align-items:flex-end; flex-wrap:wrap; gap:12px; padding:16px; border:1px solid var(--line); border-radius:8px; background:var(--paper); }
+.package-toolbar label { font-size:14px; font-weight:650; margin:0 0 6px; }
+.package-toolbar .input-row { gap:10px; }
+.package-toolbar button { min-height:44px; white-space:nowrap; }
+.release-choice select { width:9rem; flex:none; }
+.hash-choice { flex:1 1 320px; min-width:0; }
+.hash-choice input { min-width:0; }
+#download-bundle { font-weight:650; }
+#bundle-download-status:empty { display:none; }
+@media(max-width:740px) {
+ .package-toolbar { padding:12px; }
+ .release-choice,.hash-choice { width:100%; }
+ .release-choice select { flex:1; width:auto; }
+ #download-bundle { width:100%; }
+}
diff --git a/ext/demo-site/test/browser.mjs b/ext/demo-site/test/browser.mjs
@@ -175,7 +175,7 @@ try {
assert.deepEqual(await evaluate("[...document.querySelectorAll('#program-picker optgroup')].map(g=>g.label)"),['Examples','Published packages']);
assert.equal(await evaluate("document.querySelector('#program-picker option[value=hash]').textContent"),'Load by bundle hash…');
assert.deepEqual(await evaluate("Array.from([...document.getElementById('program-picker').querySelectorAll('optgroup')].find(g=>g.label==='Published packages').querySelectorAll('option'),o=>o.value)"),packageIndex.releases.filter(r=>!r.demo).map(r=>'package:'+r.name).filter((v,i,a)=>a.indexOf(v)===i));
- assert.equal(await evaluate("document.getElementById('program-release').hidden"),true,'single-release example needs no version controls');
+ assert.equal(await evaluate("document.getElementById('program-release').hidden"),false,'single-release examples keep an active version toolbar');
await evaluate("document.getElementById('program-picker').value='demo:rgb';document.getElementById('program-picker').dispatchEvent(new Event('change'))");await wait(status('Ready'));
assert.equal(await evaluate("document.getElementById('program-release').hidden"),false);
assert.deepEqual(await evaluate("[...document.getElementById('package-version').options].map(o=>o.value)"),['0.1.0','0.1.1']);
@@ -363,11 +363,11 @@ try {
await evaluate("document.getElementById('export').value='add';document.getElementById('export').dispatchEvent(new Event('change'));document.getElementById('arguments').value=JSON.stringify([{type:'number',value:'123456789'},{type:'number',value:'11'}]);document.getElementById('output-type').value='number'");
await click('run');await wait(status('Finished'));
assert.equal(await evaluate("document.getElementById('result').textContent"),'Returned number: "123456800"');
- await evaluate("document.getElementById('program-picker').value='package:tricu-compiler';document.getElementById('program-picker').dispatchEvent(new Event('change'))");
+ await evaluate("document.getElementById('program-picker').value='package:tricu-compiler';document.getElementById('program-picker').dispatchEvent(new Event('change'));document.getElementById('package-version').value='0.1.1';document.getElementById('package-version').dispatchEvent(new Event('change'))");
await click('load-release');await wait(status('Ready'));
assert.equal(await evaluate("document.getElementById('source-summary').textContent"),'Package details');
assert.equal(await evaluate("document.getElementById('edit-source').hidden"),true);
- assert.equal(await evaluate("document.getElementById('export').options.length"),1169);
+ assert.equal(await evaluate("document.getElementById('export').options.length"),1165);
assert.match(await evaluate("document.getElementById('package-info').textContent"),new RegExp(compilerDescriptor.hash));
const indexedCompilerRequests=requests.filter(p=>p.endsWith('/dist/compiler-worker.js')).length;
await click('tab-export-definition');
@@ -379,7 +379,7 @@ try {
await click('tab-playground');
// Hash loading/sharing must recognize the indexed compiler too, not only its name.
await evaluate(`document.getElementById('program-picker').value='hash';document.getElementById('program-picker').dispatchEvent(new Event('change'));document.getElementById('bundle-hash').value=${JSON.stringify(compilerDescriptor.hash)};document.getElementById('program-hash').dispatchEvent(new Event('submit',{cancelable:true}))`);await wait(status('Ready'));
- assert.equal(await evaluate("document.getElementById('export').options.length"),1169);
+ assert.equal(await evaluate("document.getElementById('export').options.length"),1165);
await evaluate("document.getElementById('program-picker').value='package:tricu-source-builder';document.getElementById('program-picker').dispatchEvent(new Event('change'))");
await click('load-release');await wait(status('Ready'));
assert.deepEqual(await evaluate("[...document.getElementById('export').options].map(o=>o.value)"),['sourceBuildGraph']);
@@ -649,6 +649,36 @@ try {
const downloaded=Buffer.from(await evaluate('window.__bundleBytes'));
assert.equal(pin(downloaded),catalogue.find(e=>e.id==='intro').hash);
assert.equal(await evaluate('window.__bundleName'),pin(downloaded)+'.arboricx');
+ for(const pane of ['tab-playground','tab-export-definition']){
+ await click(pane);
+ await evaluate("document.getElementById('program-picker').value='demo:rgb';document.getElementById('program-picker').dispatchEvent(new Event('change'))");await wait(status('Ready'));
+ await evaluate("document.getElementById('package-version').value='0.1.0';document.getElementById('package-version').dispatchEvent(new Event('change'))");
+ assert.equal(await evaluate("document.getElementById('download-bundle').disabled"),false,'pending exact version downloads without Load');
+ assert.equal(await evaluate("document.getElementById('download-bundle').closest('[role=tabpanel]').id"),pane==='tab-playground'?'playground':'export-definition');
+ assert.equal(await evaluate("document.getElementById('download-bundle').closest('details')"),null);
+ const beforeCompile=requests.filter(p=>p.endsWith('/compiler.json')).length;
+ await click('download-bundle');await wait("document.getElementById('bundle-download-status').textContent.startsWith('Downloaded original verified bundle')");
+ const expected=packageIndex.releases.find(r=>r.name==='rgb'&&r.version==='0.1.0').hash;
+ assert.equal(pin(Buffer.from(await evaluate('window.__bundleBytes'))),expected);
+ assert.equal(await evaluate("document.getElementById('run').disabled"),true,'download does not load an executable');
+ assert.equal(await evaluate("document.getElementById('export-tree').disabled"),true);
+ assert.equal(requests.filter(p=>p.endsWith('/compiler.json')).length,beforeCompile);
+ }
+ await click('tab-playground');
+ await evaluate(`document.getElementById('program-picker').value='hash';document.getElementById('program-picker').dispatchEvent(new Event('change'));document.getElementById('bundle-hash').value=${JSON.stringify(published.id)};document.getElementById('bundle-hash').dispatchEvent(new Event('input'))`);
+ assert.equal(await evaluate("document.getElementById('program-release').hidden"),true);
+ assert.equal(await evaluate("document.getElementById('download-bundle').disabled"),false);
+ await click('download-bundle');await wait("document.getElementById('bundle-download-status').textContent.startsWith('Downloaded original verified bundle')");
+ assert.equal(pin(Buffer.from(await evaluate('window.__bundleBytes'))),published.id);
+ assert.equal(await evaluate("document.getElementById('run').disabled"),true);
+ for(const id of ['parser','lexer']){
+ await evaluate(`document.getElementById('program-picker').value='demo:${id}';document.getElementById('program-picker').dispatchEvent(new Event('change'))`);await wait(status('Ready'));
+ assert.equal(await evaluate("document.getElementById('program-release').hidden"),false);
+ assert.equal(await evaluate("document.getElementById('package-version').disabled"),false);
+ }
+ // Return to the original editable selection for the draft-isolation controls.
+ await evaluate("document.getElementById('program-picker').value='demo:intro';document.getElementById('program-picker').dispatchEvent(new Event('change'))");await wait(status('Ready'));
+ console.log('PASS prominent shared download toolbar / all demo versions visible / selected version and direct hash download without Load / no implicit execution');
await evaluate("URL.createObjectURL=window.__createURL;document.removeEventListener('click',window.__bundleClick)");
await click('edit-source');
assert.equal(await evaluate("document.getElementById('source-editor').readOnly"),false);
@@ -985,6 +1015,11 @@ try {
for(const id of ['tab-playground','tab-export-definition','tab-how']) {
await click(id);
assert.equal(await evaluate('document.documentElement.scrollWidth<=innerWidth'),true,`${id} at ${width}px`);
+ if(id!=='tab-how'){
+ const button=await evaluate("(()=>{const b=document.getElementById('download-bundle'),r=b.getBoundingClientRect();return {width:r.width,height:r.height,left:r.left,right:r.right,hidden:!!b.closest('[hidden]'),details:!!b.closest('details')};})()");
+ assert.ok(button.width>0&&button.height>=44&&!button.hidden&&!button.details,'download is outside disclosures');
+ assert.ok(button.left>=0&&button.right<=width,'download CTA fits the viewport');
+ }
}
}
for(const width of [320,390,1440]) {
diff --git a/ext/demo-site/test/bundle.test.mjs b/ext/demo-site/test/bundle.test.mjs
@@ -5,9 +5,9 @@ import {createHash} from 'node:crypto';
import {Graph} from '../dist/graph.js';
import {COMPILER_BUNDLE_LIMIT,decodeBundle,loadBundle,bundleHash,fetchBytes,fetchBundle,BUNDLE_LIMIT} from '../dist/bundle.js';
import {makeBundle,sizedBundle} from './bundle-fixture.mjs';
-// Audited publication: 1152 prior exports + 5 IO transport helpers,
-// sourceObjectFlatTreeDomain, and 11 pattern-dispatch helpers; no removals.
-const COMPILER_EXPORT_COUNT=1169;
+// Current authenticated compiler closure excludes the four patterns exports;
+// patterns now belongs to support. Older indexed compiler releases are retained.
+const COMPILER_EXPORT_COUNT=1165;
const data=JSON.parse(await readFile(new URL('../demos.json',import.meta.url)));
test('stable packaged sources/pins and real compiler bundle decode',async()=>{
for(const [name,demo] of Object.entries(data.demos)){
diff --git a/ext/demo-site/test/packages.test.mjs b/ext/demo-site/test/packages.test.mjs
@@ -104,9 +104,11 @@ test('both indexed compiler packages retain their exact independently authentica
['tricu-compiler','test/fixtures/compiler-library.arboricx','source_unit.compileStableModuleDefinitions'],
['tricu-source-builder','bootstrap/build-stable.arboricx','sourceBuildGraph'],
]){
- const release=releases.find(r=>r.name===name);assert.ok(release);assert.equal(release.demo,undefined);
+ const reference=await readFile(new URL('../../../'+path,import.meta.url));
+ const referenceHash=createHash('sha256').update(reference).digest('hex');
+ const release=releases.find(r=>r.name===name&&r.hash===referenceHash);assert.ok(release);assert.equal(release.demo,undefined);
const bytes=await read('bundles/'+release.hash+'.arboricx');
- assert.deepEqual(bytes,await readFile(new URL('../../../'+path,import.meta.url)));
+ assert.ok(bytes.equals(reference),'indexed current artifact matches authenticated reference');
assert.equal(createHash('sha256').update(bytes).digest('hex'),release.hash);
const {COMPILER_BUNDLE_LIMIT}=await import('../dist/bundle.js');
const bundle=decodeBundle(bytes,COMPILER_BUNDLE_LIMIT);assert.ok(bundle.nodes.length<=100000);
diff --git a/ext/demo-site/test/site.test.mjs b/ext/demo-site/test/site.test.mjs
@@ -152,3 +152,22 @@ test('source-first workspace has full-width source and explicit compilation',asy
assert.match(css,/\.wrap\s*\{[^}]*max-width:1120px;[^}]*padding-left:28px;[^}]*padding-right:28px;/);
assert.doesNotMatch(css,/\.workspace\s*\{[^}]*(?:max-width|width:|padding-inline|padding-left|padding-right)/);
});
+
+test('bundle download stays beside primary Load controls, never inside Advanced',async()=>{
+ const html=await readFile(new URL('index.html',here),'utf8');
+ const toolbar=html.slice(html.indexOf('id="package-toolbar"'),html.indexOf('<section class="bundle-details"'));
+ for(const id of ['program-release','package-version','load-release','program-hash','load','download-bundle'])assert.match(toolbar,new RegExp(`id="${id}"`));
+ assert.equal((html.match(/id="download-bundle"/g)||[]).length,1);
+ assert.match(toolbar,/id="load-release" type="button" class="primary"/);
+ assert.match(toolbar,/id="load" type="submit" class="primary"/);
+ assert.doesNotMatch(toolbar,/id="download-bundle"[^>]*class="primary"|bundle-download-help/);
+ assert.match(toolbar,/Download \.arboricx/);
+ const advanced=html.slice(html.indexOf('<details class="run-tools">'),html.indexOf('<details class="trace-panel"'));
+ assert.doesNotMatch(advanced,/download-bundle/);
+ const app=await readFile(new URL('src/app.ts',here),'utf8');
+ assert.match(app,/el\('program-release'\)\.hidden = name === undefined;/);
+ assert.match(app,/r\.name === name && r\.version === versionPicker\.value/);
+ const css=await readFile(new URL('style.css',here),'utf8');
+ assert.match(css,/\.release-choice select \{ width:9rem;/);
+ assert.match(css,/@media\(max-width:740px\)[\s\S]*#download-bundle \{ width:100%;/);
+});