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
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',
});