).
python3 -B tools/site_asset_release.py # run after ANY change under docs/site or docs/public-math; re-keys ?site-release= URLs of changed assets and their importers
diff --git a/docs/site/atlas-controls.mjs b/docs/site/atlas-controls.mjs
new file mode 100644
index 000000000..64a741694
--- /dev/null
+++ b/docs/site/atlas-controls.mjs
@@ -0,0 +1,42 @@
+// Native controls only; no source decisions or mathematical joins belong here.
+export function enableAtlasView(select, view) {
+ // AT can retain the disabled state of the original option objects. Introduce
+ // the admitted, enabled control together; no handlers exist on the old one.
+ const enabled = select.cloneNode(false);
+ enabled.disabled = false;
+ enabled.replaceChildren(new Option('Graph', 'graph'), new Option('List', 'list'));
+ enabled.value = view === 'list' ? 'list' : 'graph';
+ select.replaceWith(enabled);
+}
+
+export function reconcileEquationOptions(select, entries, selectedId = select.value) {
+ const wanted = [{id: '', label: 'Choose a source equation excerpt'}, ...entries];
+ const existing = new Map([...select.options].map(option => [option.value, option]));
+ const ids = new Set(wanted.map(entry => entry.id));
+ for (const option of [...select.options]) if (!ids.has(option.value)) option.remove();
+ wanted.forEach((entry, index) => {
+ const option = existing.get(entry.id) || new Option(entry.label, entry.id);
+ if (option.text !== entry.label) option.text = entry.label;
+ if (select.options[index] !== option) select.insertBefore(option, select.options[index] || null);
+ });
+ // A removed selection must not silently become the first matching source.
+ select.value = ids.has(selectedId) ? selectedId : '';
+}
+
+export function createEquationAnnouncement(output, quietMs = 500) {
+ let timer, current = '';
+ const cancel = () => {
+ clearTimeout(timer);
+ if (output.textContent) output.textContent = '';
+ };
+ const publish = () => {
+ cancel();
+ if (document.hidden || !current) return;
+ timer = setTimeout(() => { if (!document.hidden) output.textContent = current; }, quietMs);
+ };
+ // Hide/page-cache transitions cannot leave an old query queued to speak.
+ document.addEventListener('visibilitychange', publish);
+ window.addEventListener('pagehide', cancel);
+ window.addEventListener('pageshow', publish);
+ return text => { current = text; publish(); };
+}
diff --git a/docs/site/atlas-layout.css b/docs/site/atlas-layout.css
index 2e7c8d249..e705faeb9 100644
--- a/docs/site/atlas-layout.css
+++ b/docs/site/atlas-layout.css
@@ -125,3 +125,4 @@
.atlas-page .atlas-snapshot-details li { margin-block: .5rem; }
.atlas-page .atlas-foundations { margin: 0 0 .5rem; padding: 0; border: 0; background: transparent; }
.atlas-page .atlas-foundations > summary { padding: .4rem 0; font-size: .875rem; }
+.atlas-announcement{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap;border:0}
diff --git a/docs/site/atlas.mjs b/docs/site/atlas.mjs
index 9f763dea9..fe9622175 100644
--- a/docs/site/atlas.mjs
+++ b/docs/site/atlas.mjs
@@ -2,6 +2,7 @@ import {currentSnapshot,element,anchor} from './current.mjs?site-release=fc7e8a9
import {hasNode,readState,neighborhood,searchGraph,downstreamReach,requiredDependencyPaths,REQUIRED_ROUTE_SEMANTICS,evidenceAxes,sourceURL,sourceForNode,staticNodeLinks,resumeProjection,RESUME_NOT_RECORDED,resumeFieldHref} from './current-model.mjs?site-release=a30d68465e3acda48eedce64854afebc7af5c84903a4e6b0c649e08007b08999';
import {createAtlasShell,viewForViewport} from './atlas-shell.mjs?site-release=b6853b9dc8f370daaa52ba7204b4880a191e50aa04719a4b3c8501cf7d8e36b0';
import {createClaimSummary} from './atlas-claim-summary.mjs?site-release=c3282397f25caaf0882d47f4433279c88bab7471df6eaf5b123f1e357da30c94';
+import {enableAtlasView,reconcileEquationOptions,createEquationAnnouncement} from './atlas-controls.mjs?site-release=58e7dd9a4fcfc326effc240080bbfac0cadd584908f84047b480fd9dcba522c7';
const byId=id=>document.getElementById(id);
const svgElement=(tag,attrs)=>{const n=document.createElementNS('http://www.w3.org/2000/svg',tag);for(const [k,v]of Object.entries(attrs))n.setAttribute(k,String(v));return n;};
// Mirrors the static node page block rendered from the same projection by tools/current_site_history.py.
@@ -31,7 +32,7 @@ async function start() {
byId('atlas-snapshot-pins').replaceChildren(...data.repositories.map(repo=>{const li=element('li');li.append(anchor(`${repo.repository} · ${repo.commit}`,`https://github.com/${repo.repository}/tree/${repo.commit}`));return li;}));
const graphSource=element('li');graphSource.append(anchor('Exact graph source at this revision',sourceURL(data.graph.identity)));byId('atlas-snapshot-pins').append(graphSource);
byId('atlas-coverage').textContent=`${Object.values(graph.nodes).filter(n=>n.source).length} source-bearing metadata records; 0 explicit graph-to-artifact bindings. Source metadata presence does not establish acquired proof content. ${data.equations.length} separately acquired equation excerpts from the named analytic sources. No logical joins from equations to graph nodes are recorded.`;
- for(const button of root.querySelectorAll('button'))button.disabled=false;query.disabled=false;byId('atlas-view').disabled=false;byId('equation-select').disabled=false;byId('equation-search').disabled=false;
+ for(const button of root.querySelectorAll('button'))button.disabled=false;query.disabled=false;enableAtlasView(byId('atlas-view'),view);byId('equation-select').disabled=false;byId('equation-search').disabled=false;
const shell=createAtlasShell({root,inspector:byId('atlas-inspector'),diagnostics:byId('atlas-diagnostics').parentElement,media:mobile,location,history,onChange({write=true}={}){if(!write)view=viewForViewport(viewPreference?'?atlas-view='+viewPreference:'',view,mobile.matches);render();if(write)urlState(true,true,!!state.error);}});
// A refused bookmark may serialize layout, but never current-source defaults.
// Only explicit selection clears state.error before adopting current pins.
@@ -86,7 +87,8 @@ async function start() {
const diagnostics=byId('atlas-diagnostics');diagnostics.replaceChildren(element('li','Diagnostic report unavailable in this source projection. Engineering generation checks are separate from mathematical evidence.'));
for(const [id,n]of Object.entries(graph.nodes)){const li=element('li');const b=element('button',`${id}: no explicit graph-to-artifact binding; roles/custody unknown`);b.type='button';b.addEventListener('click',()=>select(id));li.append(b);if(!n.source)li.append(element('span',' · source metadata not recorded'));diagnostics.append(li);}
const eq=byId('equation-select'),eqSearch=byId('equation-search');eqSearch.value=state.equationQuery||'';
- function equationOptions(selectedId=eq.value){const words=eqSearch.value.toLowerCase().trim().split(/\s+/).filter(Boolean);const matches=data.equations.filter(e=>words.every(w=>`${e.text} ${e.path} ${e.repository}`.toLowerCase().includes(w)));eq.replaceChildren(element('option','Choose a source equation excerpt'));eq.firstChild.value='';for(const e of matches){const option=element('option',`${e.path.split('/').slice(-2).join('/')} · lines ${e.line_start}–${e.line_end}`);option.value=e.id;eq.append(option);}if(selectedId&&matches.some(e=>e.id===selectedId))eq.value=selectedId;byId('equation-count').textContent=`${matches.length} matching excerpts of ${data.equations.length} acquired excerpts. Search covers exact text and public source paths; no equation-to-node logical join is inferred.`;}
+ const announceEquations=createEquationAnnouncement(byId('equation-announcement'));
+ function equationOptions(selectedId=eq.value){const words=eqSearch.value.toLowerCase().trim().split(/\s+/).filter(Boolean);const matches=data.equations.filter(e=>words.every(w=>`${e.text} ${e.path} ${e.repository}`.toLowerCase().includes(w)));reconcileEquationOptions(eq,matches.map(e=>({id:e.id,label:`${e.path.split('/').slice(-2).join('/')} · lines ${e.line_start}–${e.line_end}`})),selectedId);const countText=`${matches.length} matching excerpts of ${data.equations.length} acquired excerpts. Search covers exact text and public source paths; no equation-to-node logical join is inferred.`;const count=byId('equation-count');if(count.textContent!==countText)count.textContent=countText;announceEquations(state.error?'Source-bound bookmark refused; no current equation substituted.':`${matches.length} matching excerpts of ${data.equations.length} acquired excerpts.`);}
equationOptions(state.equation);
function renderEquation(){const record=data.equations.find(e=>e.id===eq.value),panel=byId('equation-content');panel.replaceChildren();byId('equation-next').disabled=!!state.error||eq.selectedIndex>=eq.options.length-1;byId('equation-previous').disabled=!!state.error||eq.selectedIndex<=1;if(state.error){panel.append(element('p','Source-bound bookmark refused; no current equation substituted.'));return;}if(!record){panel.append(element('p','Select an acquired excerpt. The equation collection has no recorded logical joins to dependency nodes.'));return;}panel.append(element('pre',record.text),anchor('Read exact source lines and surrounding hypotheses',`${sourceURL(record)}#L${record.line_start}-L${record.line_end}`),element('p',`Acquired excerpt · ${record.repository} ${record.commit} · SHA-256 ${record.sha256} · lines ${record.line_start}–${record.line_end}. The surrounding proof's hypotheses and scope govern this equation; excerpt extraction is not theorem acceptance.`));}
eq.addEventListener('change',()=>{renderEquation();urlState();});
diff --git a/docs/site/dependencies.html b/docs/site/dependencies.html
index 192878574..8967fd4cd 100644
--- a/docs/site/dependencies.html
+++ b/docs/site/dependencies.html
@@ -13,10 +13,10 @@
-
+
-
+
Skip to content
@@ -97,7 +97,8 @@ Read a claim in three steps
A separate reading collection
Read an equation in its source
Exact acquired excerpts retain their surrounding proof scope. No equation-to-node logical association is recorded.
-
+
+
diff --git a/docs/superpowers/plans/2026-10-26-atlas-live-controls.md b/docs/superpowers/plans/2026-10-26-atlas-live-controls.md
new file mode 100644
index 000000000..589cca77f
--- /dev/null
+++ b/docs/superpowers/plans/2026-10-26-atlas-live-controls.md
@@ -0,0 +1,47 @@
+# Atlas Live Controls Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** Make admitted View selection and equation-search results usable with assistive technology while preserving exact source state.
+
+**Architecture:** Keep the existing Atlas state model. A small DOM helper enables View without retaining disabled option objects, reconciles equation options by excerpt ID, and manages a final-result announcement separate from the immediate visible count.
+
+**Tech Stack:** Existing native DOM modules, Python Playwright1.62, Chromium154 and local Orca48.1; no added production dependency.
+
+**Spec:** `docs/superpowers/specs/2026-10-26-atlas-live-controls-design.md`
+
+## Global Constraints
+
+- Preserve source admission, refused bookmarks, mathematical/formal records, historical snapshots and other contributors' work.
+- Source originals must have independently verified private Drive custody before replacement.
+- The announcement quiet period is 500ms; actual speech is distinct from DOM assertions.
+- Current controls only; no historical renderer rewrite or proof-coverage inference.
+
+## Review Focus
+
+- A surviving selected excerpt keeps the same ID and exact source; removing it selects the placeholder.
+- Empty and rapid query changes never announce a stale result.
+- Nondefault mobile/URL View survives enabling and history.
+- A refused or unavailable snapshot never adopts current source pins through these controls.
+- No-JS and 2D/list/3D experiences preserve their existing boundaries.
+
+### Task 1: Source custody and baseline
+
+- [ ] Preserve original complete site at Maina15d2fb6 in private Drive; independently download and verify all members/whole bytes/CRC.
+- [ ] Run existing130 current/reader checks and the same real Orca journey before changes. Keep failed setup runs with their reason.
+- [ ] Add focused browser assertions for remaining option identity, immediate visible count, settled final announcements, selection/source and View/history/refusal. Run against baseline and retain expected failures.
+
+### Task 2: Live control repair
+
+**Files:** new `docs/site/atlas-controls.mjs`; modify `docs/site/atlas.mjs`, `docs/site/dependencies.html`, `docs/site/atlas-layout.css`; focused driver `tools/atlas_live_controls_browser_check.py`.
+
+- [ ] Implement the smallest helper and integration satisfying the failing assertions. Keep source decisions in the existing controller.
+- [ ] Run focused browser checks, actual same-driver Orca replays, existing reader/refusal/3D/formal regressions, visuals and performance comparison.
+- [ ] Stage new files, run asset release and check, run all prescribed site-generation/public-shop/formal contract checks. Commit exact implementation.
+
+### Task 3: Reviewable release
+
+- [ ] Add source/version-qualified evidence report and guide command; request actual separate whole-branch engineering review.
+- [ ] Address material findings with reproducing checks; preserve source/preimages before replacements.
+- [ ] Prepare Draft PR, exact-head preservation and separate integrator handoff. Integrate only through current protected process; verify actual deployed bytes, source observation and reader journeys.
+- [ ] Preserve final evidence privately, report limitations and release only own claim.
diff --git a/docs/superpowers/specs/2026-10-26-atlas-live-controls-design.md b/docs/superpowers/specs/2026-10-26-atlas-live-controls-design.md
new file mode 100644
index 000000000..eb33c6a32
--- /dev/null
+++ b/docs/superpowers/specs/2026-10-26-atlas-live-controls-design.md
@@ -0,0 +1,9 @@
+# Atlas live controls accessibility repair
+
+The existing verified Atlas, source inspector and native controls remain the interface. This bounded release repairs View's inherited disabled accessibility state and equation-search event churn. A reader must retain the URL/viewport View choice, select only exact matching excerpts, and hear the final result count after typing stops. Missing source mappings remain explicit.
+
+Approaches considered: wholesale adoption of Cursor's `aadd04e4` assist would also rewrite historical renderers and is outside this release; debounce alone leaves repeated option destruction and stale selection risks. The chosen approach replaces the initially disabled View with an enabled equivalent after source admission, reconciles equation options by stable excerpt ID, updates the visible count immediately, and publishes a separate concise atomic announcement after 500ms of quiet. This is an engineering decision under the user's continuing implementation authorization. Cursor's observation and assist are attributed; this is a new implementation, not acceptance of that branch.
+
+All count requests, including unchanged numeric counts, restart the announcement timer so the final query can announce after a key flush. Pending messages are replaced on input/history and suppressed while hidden; returning publishes only current state. Do not add motion or production dependencies. No source-loader, admission, graph, formal authority, historical renderer, mathematical source or security changes.
+
+Validate on the same source-pinned 49-node/55-edge/108-excerpt observation before and after: actual Orca48.1/Chromium154 spoken strings, nondefault View, fast typing, zero/clear, option identity, exact source selection, sharing/reload/history, source refusal and JS-free fallback. Include narrow/desktop light/dark screenshots, mutation counts and observed latency. This implementation-side replay is not Claude54's distinct Orca46.1 qualification or full WCAG conformance.
diff --git a/tests/test_atlas_controls.mjs b/tests/test_atlas_controls.mjs
new file mode 100644
index 000000000..f7dd1c19b
--- /dev/null
+++ b/tests/test_atlas_controls.mjs
@@ -0,0 +1,45 @@
+import test from 'node:test';
+import assert from 'node:assert/strict';
+import {readFile} from 'node:fs/promises';
+import {runInNewContext} from 'node:vm';
+
+// Execute the production helper with an explicit clock and browser lifecycle
+// boundary. Actual native option behavior and speech have separate replays.
+const source = (await readFile(new URL('../docs/site/atlas-controls.mjs', import.meta.url), 'utf8')).replace(/^export /gm, '');
+function fixture() {
+ const document = new EventTarget(), window = new EventTarget();
+ document.hidden = false;
+ let clock = 0, next = 0;
+ const timers = new Map(), output = {textContent: ''};
+ const request = runInNewContext(source + '\ncreateEquationAnnouncement(output);', {
+ document, window, output,
+ setTimeout(fn, ms) { const id = ++next; timers.set(id, {at: clock + ms, fn}); return id; },
+ clearTimeout(id) { timers.delete(id); }
+ });
+ return {document, window, output, request, tick(ms) {
+ clock += ms;
+ for (const [id, timer] of timers) if (timer.at <= clock) { timers.delete(id); timer.fn(); }
+ }};
+}
+test('only the current query is announced after 500ms of quiet, even if its count repeats', () => {
+ const f = fixture();
+ f.request('108 excerpts'); f.tick(450); f.request('23 excerpts'); f.tick(450);
+ assert.equal(f.output.textContent, '');
+ f.request('23 excerpts'); f.tick(499); assert.equal(f.output.textContent, '');
+ f.tick(1); assert.equal(f.output.textContent, '23 excerpts');
+ f.request('0 excerpts'); f.tick(100); f.request('108 excerpts');
+ f.tick(500); assert.equal(f.output.textContent, '108 excerpts');
+});
+test('hidden documents and page-cache transitions cancel pending speech and resume current results', () => {
+ const f = fixture();
+ f.request('23 excerpts'); f.tick(100);
+ f.document.hidden = true; f.document.dispatchEvent(new Event('visibilitychange'));
+ f.tick(1000); assert.equal(f.output.textContent, '');
+ f.request('0 excerpts'); f.document.hidden = false;
+ f.document.dispatchEvent(new Event('visibilitychange')); f.tick(500);
+ assert.equal(f.output.textContent, '0 excerpts');
+ f.request('108 excerpts'); f.window.dispatchEvent(new Event('pagehide'));
+ f.tick(1000); assert.equal(f.output.textContent, '');
+ f.window.dispatchEvent(new Event('pageshow')); f.tick(500);
+ assert.equal(f.output.textContent, '108 excerpts');
+});
diff --git a/tests/test_current_site.mjs b/tests/test_current_site.mjs
index 31d98035f..ef2f8f82f 100644
--- a/tests/test_current_site.mjs
+++ b/tests/test_current_site.mjs
@@ -144,6 +144,7 @@ test('atlas offset equation search and exact metadata filters survive URL state'
async function atlasController(search='',data=fixture()) {
const renderer=await import('../docs/site/current.mjs'),shell=await import('../docs/site/atlas-shell.mjs'),summary=await import('../docs/site/atlas-claim-summary.mjs');
const source=await readFile(new URL('../docs/site/atlas.mjs',import.meta.url),'utf8');
+ const controls=await readFile(new URL('../docs/site/atlas-controls.mjs',import.meta.url),'utf8');
class Element extends EventTarget {
constructor(tag,doc){super();this.tagName=tag.toUpperCase();this.ownerDocument=doc;this.children=[];this.attributes=new Map();this.style={};this.dataset={};this.hidden=false;this._text='';this._value='';}
set textContent(value){this._text=String(value);this.children=[];}
@@ -156,7 +157,10 @@ async function atlasController(search='',data=fixture()) {
remove(){if(this.parentNode){const siblings=this.parentNode.children;siblings.splice(siblings.indexOf(this),1);this.parentNode=null;}}
append(...children){for(const child of children){if(typeof child!=='string'){child.remove();child.parentNode=this;}this.children.push(child);}}
replaceChildren(...children){for(const child of this.children)if(typeof child!=='string')child.parentNode=null;this.children=[];this._text='';this.append(...children);}
- insertBefore(child,before){child.remove();child.parentNode=this;this.children.splice(this.children.indexOf(before),0,child);}
+ insertBefore(child,before){if(!before){this.append(child);return;}child.remove();child.parentNode=this;this.children.splice(this.children.indexOf(before),0,child);}
+ cloneNode(){const copy=new Element(this.tagName,this.ownerDocument);for(const [key,value]of this.attributes)copy.setAttribute(key,value);return copy;}
+ replaceWith(node){const parent=this.parentNode;parent.insertBefore(node,this);this.remove();nodes.set(node.id,node);}
+ get text(){return this.textContent;}set text(value){this.textContent=value;}
contains(child){return this===child||this.children.some(node=>typeof node!=='string'&&node.contains(child));}
querySelectorAll(selector){const matches=[];for(const child of this.children)if(typeof child!=='string'){if(selector==='a[href]'?child.tagName==='A'&&!!child.getAttribute('href'):child.tagName===selector.toUpperCase())matches.push(child);matches.push(...child.querySelectorAll(selector));}return matches;}
get firstChild(){return this.children[0];}
@@ -170,7 +174,7 @@ async function atlasController(search='',data=fixture()) {
scrollIntoView(){}
getClientRects(){return this.hidden?[]:[{}];}
}
- const doc={activeElement:null,createElement(tag){return new Element(tag,this);},createElementNS(ns,tag){return this.createElement(tag);}};
+ const doc=Object.assign(new EventTarget(),{activeElement:null,createElement(tag){return new Element(tag,this);},createElementNS(ns,tag){return this.createElement(tag);}});
const nodes=new Map();const add=(id,tag='div',parent)=>{const node=doc.createElement(tag);node.id=id;nodes.set(id,node);parent?.append(node);return node;};
doc.getElementById=id=>nodes.get(id);const root=add('current-atlas'),layout=add('atlas-layout','div',root),browse=add('atlas-browse','div',layout);
add('atlas-inspector','article',layout);add('atlas-diagnostics','ul',add('atlas-diagnostics-details','details',root));
@@ -181,13 +185,13 @@ async function atlasController(search='',data=fixture()) {
const location=new URL('https://example.test/main/site/dependencies.html'+search),entries=[location.href];let cursor=0;
const history={replaceState(state,title,url){location.href=new URL(url,location.href).href;entries[cursor]=location.href;},pushState(state,title,url){location.href=new URL(url,location.href).href;entries.splice(++cursor);entries.push(location.href);},back(){if(cursor>0){location.href=entries[--cursor];window.dispatchEvent(new Event('popstate'));}},forward(){if(cursor+1media,fetch:async()=>({ok:false})};
+ const bindings={...m,...shell,...summary,currentSnapshot:Promise.resolve(data),element:renderer.element,anchor:renderer.anchor,document:doc,window,history,URL,URLSearchParams,setTimeout,clearTimeout,Option:function(text,value){const option=doc.createElement('option');option.text=text;option.value=value;return option;},matchMedia:()=>media,fetch:async()=>({ok:false})};
Object.defineProperty(bindings,'location',{get:()=>location});
try{
// Imports use the actual modules above; awaiting the real start lifecycle
// avoids testing a copied render function or a source-text pattern.
const program=source.replace(/^import .*;\n/gm,'').replace(/^if\(typeof document[^\n]+\n?$/m,'');
- await runInNewContext(program+'\nstart();',bindings);
+ await runInNewContext(controls.replace(/^export /gm,'')+'\n'+program+'\nstart();',bindings);
const graphIDs=()=>nodes.get('atlas-canvas').querySelectorAll('button').map(node=>node.textContent.split(/candidate|axiom|predicate/)[0]);
const listIDs=()=>nodes.get('atlas-list').querySelectorAll('button').map(node=>node.textContent.split(/candidate|axiom|predicate/)[0]);
return {get:id=>nodes.get(id),graphIDs,listIDs,history,window,get url(){return location.href;},event(id,type){nodes.get(id).dispatchEvent(new Event(type));},async reload(){return atlasController(location.search,data);},close(){if(saved)Object.defineProperty(globalThis,'document',saved);else delete globalThis.document;}};
diff --git a/tools/atlas_live_controls_browser_check.py b/tools/atlas_live_controls_browser_check.py
new file mode 100644
index 000000000..e6daa8f1c
--- /dev/null
+++ b/tools/atlas_live_controls_browser_check.py
@@ -0,0 +1,119 @@
+#!/usr/bin/env python3
+"""Source-pinned live Atlas control regressions; --observe keeps baseline failures."""
+import argparse
+from functools import partial
+import hashlib
+from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer
+import json
+from pathlib import Path
+import threading
+import time
+from playwright.sync_api import expect, sync_playwright
+
+
+def main():
+ ap = argparse.ArgumentParser(description=__doc__)
+ ap.add_argument('--site', type=Path, required=True)
+ ap.add_argument('--output', type=Path, required=True)
+ ap.add_argument('--browser', default='/usr/bin/chromium')
+ ap.add_argument('--observe', action='store_true')
+ args = ap.parse_args()
+ args.output.mkdir(parents=True, exist_ok=True)
+ raw = (args.site / 'current.json').read_bytes()
+ data = json.loads(raw)
+ class Handler(SimpleHTTPRequestHandler):
+ def log_message(self, *unused): pass
+ server = ThreadingHTTPServer(('127.0.0.1', 0), partial(Handler, directory=str(args.site)))
+ threading.Thread(target=server.serve_forever, daemon=True).start()
+ origin = f'http://127.0.0.1:{server.server_port}'
+ report = {'driver_sha256': hashlib.sha256(Path(__file__).read_bytes()).hexdigest(),
+ 'payload_sha256': hashlib.sha256(raw).hexdigest(), 'repositories': data['repositories'],
+ 'dataset': {'nodes': len(data['graph']['data']['nodes']), 'edges': len(data['graph']['data']['edges']), 'excerpts': len(data['equations'])},
+ 'cases': []}
+ try:
+ with sync_playwright() as pw:
+ browser = pw.chromium.launch(executable_path=args.browser, chromium_sandbox=True,
+ args=['--enable-unsafe-swiftshader'])
+ report['browser'] = browser.version
+ for width, scheme in [(1200, 'light'), (1200, 'dark'), (390, 'light'), (390, 'dark')]:
+ row = {'case': f'{width}-{scheme}', 'failures': []}; report['cases'].append(row)
+ context = browser.new_context(viewport={'width': width, 'height': 900}, color_scheme=scheme, reduced_motion='reduce')
+ page = context.new_page()
+ def check(condition, name):
+ if not condition: row['failures'].append(name)
+ try:
+ t = time.perf_counter(); page.goto(origin + '/dependencies.html?atlas-view=list')
+ expect(page.locator('#atlas-view')).to_be_enabled()
+ row['ready_ms'] = (time.perf_counter() - t) * 1000
+ row['decoded_resource_bytes'] = page.evaluate('() => performance.getEntriesByType("resource").reduce((n,r)=>n+r.decodedBodySize,0)')
+ expect(page.locator('#atlas-view')).to_have_value('list')
+ page.locator('#atlas-view').focus(); page.keyboard.press('Home'); page.keyboard.press('Enter')
+ expect(page.locator('#atlas-view')).to_have_value('graph')
+ page.locator('#atlas-view').select_option('list')
+ page.locator('#equation-search').scroll_into_view_if_needed()
+ # Observation only: no replacement of app responses or state.
+ page.evaluate('''() => {
+ window.optionBefore = [...document.querySelector('#equation-select').options];
+ window.optionEvents = 0; window.countEvents = 0;
+ new MutationObserver(rs => { for (const r of rs) window.optionEvents += r.addedNodes.length + r.removedNodes.length; }).observe(document.querySelector('#equation-select'), {childList:true,subtree:true});
+ new MutationObserver(rs => window.countEvents += rs.length).observe(document.querySelector('#equation-count'), {childList:true,subtree:true,characterData:true});
+ }''')
+ t = time.perf_counter(); page.locator('#equation-search').press_sequentially('sigma', delay=45)
+ row['typing_ms'] = (time.perf_counter() - t) * 1000
+ matches = [e for e in data['equations'] if 'sigma' in f"{e['text']} {e['path']} {e['repository']}".lower()]
+ expect(page.locator('#equation-count')).to_contain_text(f'{len(matches)} matching excerpts')
+ expect(page.locator('#equation-select')).to_have_value('')
+ row['option_events'] = page.evaluate('window.optionEvents')
+ row['count_events'] = page.evaluate('window.countEvents')
+ check(page.evaluate('''() => [...document.querySelector('#equation-select').options].every(o=>window.optionBefore.includes(o))'''), 'remaining options retain their identity')
+ check(row['option_events'] <= len(data['equations']), 'option event volume bounded by removed excerpts')
+ announcement = page.locator('#equation-announcement')
+ check(announcement.count() == 1, 'separate settled announcement exists')
+ if announcement.count():
+ t = time.perf_counter()
+ expect(announcement).to_have_text(f'{len(matches)} matching excerpts of {len(data["equations"])} acquired excerpts.')
+ row['announcement_wait_ms'] = (time.perf_counter() - t) * 1000
+ page.locator('#equation-search').fill('not-a-source-xyz')
+ page.locator('#equation-search').fill('sigma')
+ expect(announcement).to_have_text(f'{len(matches)} matching excerpts of {len(data["equations"])} acquired excerpts.')
+ # Observe beyond the deadline: the abandoned zero result must never reappear.
+ page.wait_for_timeout(650)
+ check('0 matching' not in announcement.text_content(), 'abandoned zero-result announcement cancelled')
+ if matches:
+ chosen = matches[0]
+ page.locator('#equation-select').select_option(chosen['id'])
+ expect(page.locator('#equation-content pre')).to_have_text(chosen['text'])
+ href = page.locator('#equation-content a').get_attribute('href')
+ check(chosen['commit'] in href and f'#L{chosen["line_start"]}-L{chosen["line_end"]}' in href, 'exact source revision and lines')
+ page.locator('#equation-search').fill('sigm')
+ expect(page.locator('#equation-select')).to_have_value(chosen['id'])
+ share = page.locator('#atlas-share').get_attribute('href')
+ page.goto(share); expect(page.locator('#equation-select')).to_have_value(chosen['id'])
+ expect(page.locator('#equation-content pre')).to_have_text(chosen['text'])
+ page.locator('#equation-search').fill('not-a-source-xyz')
+ expect(page.locator('#equation-select')).to_have_value('')
+ expect(page.locator('#equation-content')).to_contain_text('Select an acquired excerpt')
+ page.locator('#equation-search').fill('sigma')
+ page.locator('.atlas-equation').screenshot(path=str(args.output / f'equations-{width}-{scheme}.png'))
+ check(page.evaluate('document.documentElement.scrollWidth <= innerWidth'), 'no horizontal page overflow')
+ page.goto(origin + '/dependencies.html?snapshot=' + '0'*40 + '&atlas-view=list')
+ expect(page.locator('#atlas-error')).to_be_visible()
+ page.locator('#atlas-view').select_option('graph')
+ check('snapshot='+'0'*40 in page.locator('#atlas-share').get_attribute('href'), 'View preserves refused snapshot')
+ expect(page.locator('#atlas-error')).to_be_visible()
+ if announcement.count():
+ expect(announcement).to_have_text('Source-bound bookmark refused; no current equation substituted.')
+ page.go_back(); expect(page.locator('#atlas-error')).to_be_hidden()
+ page.go_forward(); expect(page.locator('#atlas-error')).to_be_visible()
+ except Exception as exc:
+ row['failures'].append(str(exc))
+ context.close(); row['passed'] = not row['failures']
+ (args.output / 'controls-browser.json').write_text(json.dumps(report, indent=2)+'\n')
+ browser.close()
+ finally:
+ server.shutdown(); server.server_close()
+ print(json.dumps({'passed':sum(r['passed'] for r in report['cases']), 'total':len(report['cases']), 'report':str(args.output/'controls-browser.json')}))
+ if not args.observe and any(not r['passed'] for r in report['cases']): raise SystemExit(1)
+
+
+if __name__ == '__main__': main()