From 1d5a71681c049e99971e321f6cb91dc80e09b8ab Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 5 Nov 2026 08:41:12 +0000 Subject: [PATCH] Make the one-screen BQNF overview the Studio front page / now shows BQNF on one screen: four changes to a small sales table (id first), the stored SUM and MAX next to the table's real answer, and what a SQL Server indexed view, a Postgres materialized view and BQNF each do with the change. The full Studio moves to /studio/ (/studio redirects). The browser smoke test now opens /studio/ and also checks that the overview is served as committed. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_0194kAmZqD922d7DPkm5UTas --- scripts/browser_smoke.cjs | 4 +- tests/test_web_api.py | 4 +- web/app.py | 15 +- web/templates/index.html | 2 +- web/templates/overview.html | 302 ++++++++++++++++++++++++++++++++++++ web/test_api_smoke.py | 6 + 6 files changed, 326 insertions(+), 7 deletions(-) create mode 100644 web/templates/overview.html diff --git a/scripts/browser_smoke.cjs b/scripts/browser_smoke.cjs index 9ccde56..b26fa96 100644 --- a/scripts/browser_smoke.cjs +++ b/scripts/browser_smoke.cjs @@ -115,7 +115,7 @@ async function copy(id) { page.on('response', r => { if (r.status() >= 400) report.http_errors.push({ url: r.url(), status: r.status() }); }); page.on('dialog', async dialog => { report.page_errors.push(`Dialog: ${dialog.message()}`); await dialog.dismiss(); }); await step('Application identity, assets and initial rendering', async () => { - const r = await page.goto(base, { waitUntil: 'networkidle', timeout: 120000 }); + const r = await page.goto(base + '/studio/', { waitUntil: 'networkidle', timeout: 120000 }); assert.equal(r.status(), 200); assert.match(await page.title(), /Simplex Normal Forms Studio/); await rendered('ai-source-tokens', '62'); @@ -125,7 +125,7 @@ async function copy(id) { assert.equal(health.status(), 200); report.health = await health.json(); assert.equal(report.health.service, 'smplxnf-studio'); - for (const [urlPath, local] of [['/', 'web/templates/index.html'], ['/static/app.js', 'web/static/app.js'], ['/static/style.css', 'web/static/style.css']]) { + for (const [urlPath, local] of [['/studio/', 'web/templates/index.html'], ['/', 'web/templates/overview.html'], ['/static/app.js', 'web/static/app.js'], ['/static/style.css', 'web/static/style.css']]) { const response = await context.request.get(base + urlPath); assert.equal(response.status(), 200); const bytes = await response.body(); diff --git a/tests/test_web_api.py b/tests/test_web_api.py index c6952a6..f19d8d0 100644 --- a/tests/test_web_api.py +++ b/tests/test_web_api.py @@ -45,8 +45,8 @@ def test_healthz_endpoint(self) -> None: self.assertEqual(payload.get("version"), "0.1.0") def test_index_html_renders_all_tabs(self) -> None: - """Verifies GET / renders the landing SPA with all five workbench tabs.""" - response = self.client.get("/") + """Verifies GET /studio/ renders the studio SPA with all five workbench tabs.""" + response = self.client.get("/studio/") self.assertEqual(response.status_code, 200) self.assertIn("text/html", response.headers.get("content-type", "")) html = response.text diff --git a/web/app.py b/web/app.py index 7d73e93..0b766db 100644 --- a/web/app.py +++ b/web/app.py @@ -22,7 +22,7 @@ from fastapi import Body, FastAPI, HTTPException, Request from fastapi.middleware.cors import CORSMiddleware -from fastapi.responses import HTMLResponse, JSONResponse +from fastapi.responses import FileResponse, HTMLResponse, JSONResponse, RedirectResponse from fastapi.staticfiles import StaticFiles from fastapi.templating import Jinja2Templates from pydantic import BaseModel, Field @@ -942,6 +942,17 @@ def project_chunks(chunks: Sequence[Mapping[str, Any]]) -> List[Dict[str, Any]]: # Frontend SPA Route # ============================================================================= -@app.get("/", response_class=HTMLResponse) +@app.get("/", include_in_schema=False) +async def serve_overview() -> FileResponse: + """One-screen BQNF overview. Plain HTML, not rendered through Jinja.""" + return FileResponse(templates_dir / "overview.html", media_type="text/html") + + +@app.get("/studio", include_in_schema=False) +async def redirect_studio() -> RedirectResponse: + return RedirectResponse("/studio/", status_code=308) + + +@app.get("/studio/", response_class=HTMLResponse) async def serve_index(request: Request): return templates.TemplateResponse(request=request, name="index.html") diff --git a/web/templates/index.html b/web/templates/index.html index 7ec831d..a14d5fe 100644 --- a/web/templates/index.html +++ b/web/templates/index.html @@ -46,7 +46,7 @@ diff --git a/web/templates/overview.html b/web/templates/overview.html new file mode 100644 index 0000000..1b0a255 --- /dev/null +++ b/web/templates/overview.html @@ -0,0 +1,302 @@ + + + + + + + Simplex Normal Forms | smplxnf.anomalon.com + + + + + +
+ +
+
+
+

The normal forms you know keep stored rows clean. BQNF keeps stored answers correct.

+

Change one row of sales. Watch which parts of the stored answer can be fixed from the change alone, and which need extra state.

+
+ +
+
1NF to BCNFNo update, insert or delete anomalies in the rows you store.
+
BQNF, Boundary-Quotient Normal FormEvery change to the rows can be applied to the stored answer as a difference, with no rescan. Where an aggregate can't, it names the extra state that makes it possible.
+
+ +
+
KEEPThe change can't move this number.
+
FIX IN PLACEApply the difference, or read the extra state.
+
REREADNo difference and no extra state: rescan the group.
+
+ +
Stored answerSELECT region, SUM(amount), MAX(amount) FROM sales WHERE status = 'paid' GROUP BY region
+ +
+
+
Make a change
+ + + + + +
+ +
+
The table
+
+
sales
+ +
idregionamountstatus
+
extra state BQNF asks for: North's amounts, sorted
+
+
+
The stored answer
+
+ + + +
RegionSUM storedMAX storedDatabase says
+
+
What each one does with this change
+
+ + + + + + + +
SystemWhat it doesRows read
SQL Server indexed viewCan't hold this answer: an indexed view may not contain MAX.n/a
Postgres materialized viewKeeps the old answer until REFRESH reruns the whole query.0
BQNFPick a change on the left. Every answer is checked against the table.0
+
+
+
+ +

The rows and moves match the simplex-normal-forms BQNF simulator (financial ledger preset). The materialized view is counted as refreshed after each change. Rows read are for this demo only, not a benchmark. SmplCache is the cache engine built on BQNF.

+
+ + + + diff --git a/web/test_api_smoke.py b/web/test_api_smoke.py index 9e2e801..ae727b3 100644 --- a/web/test_api_smoke.py +++ b/web/test_api_smoke.py @@ -28,7 +28,13 @@ def test_healthz(): def test_frontend_serving(): r = client.get("/") assert r.status_code == 200 + assert "BQNF" in r.text + assert 'href="/studio/"' in r.text + + r = client.get("/studio/") + assert r.status_code == 200 assert "Simplex-NF Studio" in r.text + assert client.get("/studio", follow_redirects=False).status_code == 308 css = client.get("/static/style.css") assert css.status_code == 200