diff --git a/site/playwright.config.ts b/site/playwright.config.ts index 30fb0e9..bf9e668 100644 --- a/site/playwright.config.ts +++ b/site/playwright.config.ts @@ -1,6 +1,10 @@ import { defineConfig } from '@playwright/test'; -const baseURL = 'http://127.0.0.1:4329/Ziggurat/'; +const port = process.env.PORT ?? '4329'; +if (!/^\d+$/.test(port) || Number(port) < 1 || Number(port) > 65535) { + throw new Error('PORT must be a decimal integer between 1 and 65535.'); +} +const baseURL = `http://127.0.0.1:${Number(port)}/Ziggurat/`; const sizes = [ { name: 'desktop', viewport: { width: 1280, height: 900 } }, { name: 'tablet', viewport: { width: 834, height: 1112 } }, @@ -30,7 +34,7 @@ export default defineConfig({ webServer: { command: 'npm run build && node scripts/serve-dist.mjs', url: baseURL, - reuseExistingServer: !process.env.CI, + reuseExistingServer: false, timeout: 120_000, }, }); diff --git a/site/src/components/AdmissionFigure.astro b/site/src/components/AdmissionFigure.astro index 1cad70b..c3c2932 100644 --- a/site/src/components/AdmissionFigure.astro +++ b/site/src/components/AdmissionFigure.astro @@ -2,7 +2,7 @@ const stages = [ { actor: 'Model', title: 'Propose a draft', detail: 'Content and source ranges, without admission authority.' }, { actor: 'Host', title: 'Validate and stage Silver', detail: 'Resolve citations and check the live evidence.' }, - { actor: 'Reviewer', title: 'Author the page and sign externally', detail: 'The private key stays outside the model and host.', external: true }, + { actor: 'Reviewer', title: 'Author the page and sign externally', detail: 'Keep the private key separate from the model and host.', external: true }, { actor: 'Host', title: 'Admit eligible Gold', detail: 'Verify the receipt and every other eligibility condition.' }, ]; --- diff --git a/site/src/pages/index.astro b/site/src/pages/index.astro index 52fbc7c..c2598fa 100644 --- a/site/src/pages/index.astro +++ b/site/src/pages/index.astro @@ -129,7 +129,8 @@ const notGuaranteed = [ A model can read authorized content and draft a complete, evidence-backed candidate. It cannot admit that candidate to durable shared memory. Only the holder of an external Ed25519 private key that operator policy assigns to a reviewer - can, and that key is never reachable from any shipped code path. + can. Ziggurat ships no signer. Keeping the private key outside the vault and + inaccessible to the model and Ziggurat process is an operator responsibility. The receipt proves key control and exact-content authorization, not humanity or attention.

@@ -373,7 +374,7 @@ retrieval_eligible: true

Watch the boundary hold

The garden walkthrough runs the poisoned-memory scenario against fixture data and - stops at the human signing boundary by design. Clone the + stops at the human signing boundary by design. Clone the{' '} repository, then from the checkout root:

@@ -392,8 +393,8 @@ node scripts/run-garden-walkthrough.mjs

- Threat model overview, then the - attack-control mapping and the + Threat model overview, then the{' '} + attack-control mapping and the{' '} guarantees and residual risks.

diff --git a/site/tests/visual.spec.ts b/site/tests/visual.spec.ts index c2c818e..62a7e30 100644 --- a/site/tests/visual.spec.ts +++ b/site/tests/visual.spec.ts @@ -1,11 +1,8 @@ -import { mkdir } from 'node:fs/promises'; import { readdirSync } from 'node:fs'; -import { join } from 'node:path'; import { fileURLToPath } from 'node:url'; import AxeBuilder from '@axe-core/playwright'; import { expect, test } from '@playwright/test'; -const artifacts = fileURLToPath(new URL('../.artifacts/', import.meta.url)); const docsDirectory = fileURLToPath(new URL('../src/content/docs/', import.meta.url)); const docRoutes = readdirSync(docsDirectory, { recursive: true, encoding: 'utf8' }) .filter(path => /\.(md|mdx)$/.test(path)) @@ -13,6 +10,59 @@ const docRoutes = readdirSync(docsDirectory, { recursive: true, encoding: 'utf8' .map(path => `${path.replace(/\/$/, '')}/`); const routes = [...new Set(['/', ...docRoutes])].sort(); +test('homepage inline prose link boundaries', async ({ page }) => { + await page.goto('./'); + const boundaries = await page.locator('main p a, main li a, main dd a, main figcaption a, .site-footer p a') + .evaluateAll(links => links.map(link => { + const prose = link.closest('p, li, dd, figcaption')!; + const before = document.createRange(); + before.selectNodeContents(prose); + before.setEndBefore(link); + const after = document.createRange(); + after.selectNodeContents(prose); + after.setStartAfter(link); + // DOM ranges retain missing spaces that layout gaps and accessibility checks cannot detect. + return { + text: link.textContent, + before: before.toString().replace(/\s+/g, ' '), + after: after.toString().replace(/\s+/g, ' '), + }; + })); + + expect(boundaries).toEqual([ + { + text: 'provenance and authority', + before: expect.stringMatching(/; see $/), + after: '.', + }, + { + text: 'repository', + before: expect.stringMatching(/Clone the $/), + after: expect.stringMatching(/^, then from the checkout root:/), + }, + { + text: 'Threat model overview', + before: expect.stringMatching(/^\s*$/), + after: expect.stringMatching(/^, then the attack-control mapping and the guarantees and residual risks\.\s*$/), + }, + { + text: 'attack-control mapping', + before: expect.stringMatching(/Threat model overview, then the $/), + after: expect.stringMatching(/^ and the guarantees and residual risks\.\s*$/), + }, + { + text: 'guarantees and residual risks', + before: expect.stringMatching(/attack-control mapping and the $/), + after: expect.stringMatching(/^\.\s*$/), + }, + { + text: 'project status', + before: expect.stringMatching(/documented in $/), + after: expect.stringMatching(/^, alongside failures and remaining gaps\./), + }, + ]); +}); + for (const route of routes) { test(`${route} visual and accessibility`, async ({ page }, testInfo) => { // A leading slash would discard the /Ziggurat/ base path. @@ -27,6 +77,12 @@ for (const route of routes) { await expect(page.locator('h1')).toHaveCount(1); if (route === '/') { + await expect(page.locator('.thesis .prose')).toContainText( + 'Ziggurat ships no signer. Keeping the private key outside the vault and inaccessible to the model and Ziggurat process is an operator responsibility.', + ); + await expect(page.locator('.admission__external')).toContainText( + 'Keep the private key separate from the model and host.', + ); await expect(page.locator('.admission__flow > li')).toHaveCount(4); await expect(page.locator('#ascent [data-ascent-figure]')).toBeVisible(); await expect(page.locator('.walkthrough__steps > li')).toHaveCount(7); @@ -116,10 +172,8 @@ for (const route of routes) { expect(violations, `Axe violations on ${route} (${testInfo.project.name})`).toEqual([]); const slug = route === '/' ? 'home' : route.replace(/[^a-z0-9]+/gi, '-').replace(/^-|-$/g, ''); - const [project, scheme] = testInfo.project.name.split('-'); - await mkdir(artifacts, { recursive: true }); await page.screenshot({ - path: join(artifacts, `${slug}-${project}-${scheme}.png`), + path: testInfo.outputPath(`${slug}-${testInfo.project.name}.png`), fullPage: true, animations: 'disabled', });