From 58be95381d78a3e7697d913a8eb97757506c2f08 Mon Sep 17 00:00:00 2001 From: Daniel Tyack Date: Wed, 4 Nov 2026 19:29:35 -0800 Subject: [PATCH 1/4] fix(ci): preserve visual reports across tiers --- .github/workflows/e2e.yml | 9 ++++++--- .../rows/CI-VISUAL-ARTIFACT-RETENTION.json | 18 ++++++++++++++++++ 2 files changed, 24 insertions(+), 3 deletions(-) create mode 100644 docs/ledger/rows/CI-VISUAL-ARTIFACT-RETENTION.json diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index f32208edea..fe05aec379 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -701,25 +701,27 @@ jobs: if: ${{ always() && steps.preview.outputs.ready == 'true' }} env: PW_BASE_URL: ${{ steps.preview.outputs.url }} + PLAYWRIGHT_HTML_OUTPUT_DIR: playwright-report/visual-public VERCEL_AUTOMATION_BYPASS_SECRET: ${{ secrets.VERCEL_AUTOMATION_BYPASS_SECRET }} PW_STORAGE_STATE: ${{ steps.session.outputs.path }} PW_ROOM_PATH: ${{ vars.PW_ROOM_PATH }} PW_SPACE_SLUG: ${{ vars.PW_SPACE_SLUG }} PW_REQUIRE_SHELL: ${{ vars.PW_REQUIRE_SHELL || secrets.PW_REQUIRE_SHELL }} PW_REQUIRE_OPERATOR: ${{ vars.PW_REQUIRE_OPERATOR || secrets.PW_REQUIRE_OPERATOR }} - run: pnpm test:e2e:visual:stable + run: pnpm test:e2e:visual:stable --output=test-results/visual-public - name: Visual compare -- member shell + operator if: ${{ always() && steps.preview.outputs.ready == 'true' }} env: PW_BASE_URL: ${{ steps.preview.outputs.url }} + PLAYWRIGHT_HTML_OUTPUT_DIR: playwright-report/visual-shell VERCEL_AUTOMATION_BYPASS_SECRET: ${{ secrets.VERCEL_AUTOMATION_BYPASS_SECRET }} PW_STORAGE_STATE: ${{ steps.session.outputs.path }} PW_ROOM_PATH: ${{ vars.PW_ROOM_PATH }} PW_SPACE_SLUG: ${{ vars.PW_SPACE_SLUG }} PW_REQUIRE_SHELL: ${{ vars.PW_REQUIRE_SHELL || secrets.PW_REQUIRE_SHELL }} PW_REQUIRE_OPERATOR: ${{ vars.PW_REQUIRE_OPERATOR || secrets.PW_REQUIRE_OPERATOR }} - run: pnpm test:e2e:visual:shell + run: pnpm test:e2e:visual:shell --output=test-results/visual-shell # LIVE-313 / ADR-1449: the member-shell step above is a real gate. /discover stays in # its own advisory step (LIVE-373) because its HEIGHT is production Circles, events and @@ -741,13 +743,14 @@ jobs: if: ${{ always() && steps.preview.outputs.ready == 'true' }} env: PW_BASE_URL: ${{ steps.preview.outputs.url }} + PLAYWRIGHT_HTML_OUTPUT_DIR: playwright-report/visual-advisory VERCEL_AUTOMATION_BYPASS_SECRET: ${{ secrets.VERCEL_AUTOMATION_BYPASS_SECRET }} PW_STORAGE_STATE: ${{ steps.session.outputs.path }} PW_ROOM_PATH: ${{ vars.PW_ROOM_PATH }} PW_SPACE_SLUG: ${{ vars.PW_SPACE_SLUG }} PW_REQUIRE_SHELL: ${{ vars.PW_REQUIRE_SHELL || secrets.PW_REQUIRE_SHELL }} PW_REQUIRE_OPERATOR: ${{ vars.PW_REQUIRE_OPERATOR || secrets.PW_REQUIRE_OPERATOR }} - run: pnpm test:e2e:visual:advisory + run: pnpm test:e2e:visual:advisory --output=test-results/visual-advisory # An advisory tier that fails silently is the thing it was built to avoid, so say it in # the summary where a reviewer already looks. EVERY surface on the tier is named here, with diff --git a/docs/ledger/rows/CI-VISUAL-ARTIFACT-RETENTION.json b/docs/ledger/rows/CI-VISUAL-ARTIFACT-RETENTION.json new file mode 100644 index 0000000000..bd8f03ccf6 --- /dev/null +++ b/docs/ledger/rows/CI-VISUAL-ARTIFACT-RETENTION.json @@ -0,0 +1,18 @@ +{ + "id": "CI-VISUAL-ARTIFACT-RETENTION", + "title": "Preserve every visual tier failure artifact through the final CI upload", + "status": "open", + "priority": "P2", + "lane": "hygiene", + "size": "S", + "source": { + "file": ".github/workflows/e2e.yml", + "ref": "Visual compare public/member/advisory steps and final Upload report + diffs" + }, + "detail": "Read-only audit of exact control run41904712802 found112 blocking public/member visual failures in logs, but its final official artifact13361658194 contains only6 final advisory Discover cases:4failed and2skipped. Independent Playwright invocations reuse default test-results and playwright-report, so later tiers erase earlier screenshots/reports before the single final upload. Isolate both output and HTML report directories per tier without changing selectors, thresholds, baselines, timeouts, secrets or blocking/advisory semantics. Preserve initial smoke/root evidence too. Related broader audit SCAN-1032 remains open. This repair preserves evidence only; it does not resolve visual differences, demonstrate a visual/performance pass or authorize baseline recapture.", + "verify": { + "kind": "manual", + "evidence": "OPEN. Daniel approved exact workflow patch SHA256 05b29b302606bea428c260a5e22b617a28c4dbd95767ee04d72eaaea9aa0c7f4 on 2026-11-04. Applied against main bbdfba1b: exactly six YAML leaves isolate public/member/advisory results and HTML directories; upload roots, selectors, secrets, gates, thresholds, baselines and blocking/advisory semantics preserved. Installed pnpm/Playwright non-browser controls reproduce pristine advisory-only retention and preserve candidate smoke plus all three visual tiers; intentional public/advisory exit1 and reporter append controls pass. Exact-head CI and inspection of actual retained artifacts remain pending. Local fixtures do not establish a visual or performance pass; do not close until CI artifact retention is verified.", + "checked": "2026-11-04" + } +} From 15fde7947c97d4705ffa32890c1f27ed5b184e81 Mon Sep 17 00:00:00 2001 From: Daniel Tyack Date: Wed, 4 Nov 2026 19:54:21 -0800 Subject: [PATCH 2/4] fix(ledger): place visual retention finding on W7 --- docs/ledger/rows/CI-VISUAL-ARTIFACT-RETENTION.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/ledger/rows/CI-VISUAL-ARTIFACT-RETENTION.json b/docs/ledger/rows/CI-VISUAL-ARTIFACT-RETENTION.json index bd8f03ccf6..98fccee253 100644 --- a/docs/ledger/rows/CI-VISUAL-ARTIFACT-RETENTION.json +++ b/docs/ledger/rows/CI-VISUAL-ARTIFACT-RETENTION.json @@ -14,5 +14,6 @@ "kind": "manual", "evidence": "OPEN. Daniel approved exact workflow patch SHA256 05b29b302606bea428c260a5e22b617a28c4dbd95767ee04d72eaaea9aa0c7f4 on 2026-11-04. Applied against main bbdfba1b: exactly six YAML leaves isolate public/member/advisory results and HTML directories; upload roots, selectors, secrets, gates, thresholds, baselines and blocking/advisory semantics preserved. Installed pnpm/Playwright non-browser controls reproduce pristine advisory-only retention and preserve candidate smoke plus all three visual tiers; intentional public/advisory exit1 and reporter append controls pass. Exact-head CI and inspection of actual retained artifacts remain pending. Local fixtures do not establish a visual or performance pass; do not close until CI artifact retention is verified.", "checked": "2026-11-04" - } + }, + "wave": "W7" } From 03eae7ea80ceaf9ef761485374d6a12770bc25f8 Mon Sep 17 00:00:00 2001 From: Daniel Tyack Date: Wed, 4 Nov 2026 22:19:03 -0800 Subject: [PATCH 3/4] Fix preview status lookup and preserve smoke reports --- .github/workflows/e2e.yml | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index fe05aec379..9d4b22ea51 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -474,10 +474,10 @@ jobs: rm -f "${refused}" states=$( for dep in $(read_api "repos/${REPO}/deployments?sha=${SHA}&per_page=100" '.[].id'); do - read_api "repos/${REPO}/deployments/${dep}/statuses?per_page=100" '.[].state' + read_api "repos/${REPO}/deployments/${dep}/statuses?per_page=100" 'max_by([.created_at, .id]) | .state // empty' done read_api "repos/${REPO}/commits/${SHA}/statuses?per_page=100" \ - '.[] | select(.context | ascii_downcase | test("vercel")) | .state' + 'group_by(.context)[] | max_by([.created_at, .id]) | select(.context | ascii_downcase | test("vercel")) | .state' # A refused read prints nothing and is recorded in ${refused}, read just below, so the # subshell's own exit status is not the signal and must not trip `set -e`. true @@ -654,9 +654,10 @@ jobs: - name: Public surface -- production contracts if: ${{ always() && steps.preview.outputs.ready == 'true' }} env: + PLAYWRIGHT_HTML_OUTPUT_DIR: playwright-report/public-contracts PW_BASE_URL: https://frequencylocal.com PW_PROBE_CRON: '0' - run: pnpm exec playwright test public-surface.spec.ts --project mobile + run: pnpm exec playwright test public-surface.spec.ts --project mobile --output=test-results/public-contracts # ── THE VISUAL COMPARE, IN TWO TIERS (2026-09-14) ────────────────────────────────── # @@ -1109,10 +1110,10 @@ jobs: rm -f "${refused}" states=$( for dep in $(read_api "repos/${REPO}/deployments?sha=${SHA}&per_page=100" '.[].id'); do - read_api "repos/${REPO}/deployments/${dep}/statuses?per_page=100" '.[].state' + read_api "repos/${REPO}/deployments/${dep}/statuses?per_page=100" 'max_by([.created_at, .id]) | .state // empty' done read_api "repos/${REPO}/commits/${SHA}/statuses?per_page=100" \ - '.[] | select(.context | ascii_downcase | test("vercel")) | .state' + 'group_by(.context)[] | max_by([.created_at, .id]) | select(.context | ascii_downcase | test("vercel")) | .state' # A refused read prints nothing and is recorded in ${refused}, read just below, so the # subshell's own exit status is not the signal and must not trip `set -e`. true From 13e3373c6e4a51d5521ca61ba6c5cfeef489ea40 Mon Sep 17 00:00:00 2001 From: Daniel Tyack Date: Thu, 5 Nov 2026 05:16:29 -0800 Subject: [PATCH 4/4] Fix homepage layout thrashing and verify status caching at the edge --- app/api/status/route.test.ts | 5 +++++ components/frequency/site/pages/home/weave.tsx | 13 ++++++++++--- test/e2e/public-surface.spec.ts | 10 +++++++++- 3 files changed, 24 insertions(+), 4 deletions(-) diff --git a/app/api/status/route.test.ts b/app/api/status/route.test.ts index f352c8e196..3ac6c00b4f 100644 --- a/app/api/status/route.test.ts +++ b/app/api/status/route.test.ts @@ -71,6 +71,11 @@ describe('the build identity a deploy check reads', () => { }) describe('the index contract it already had is unchanged', () => { + it('publishes the one-hour browser and shared-cache policy at the origin', () => { + const directives = GET().headers.get('cache-control')?.split(',').map((directive) => directive.trim()) + expect(directives).toEqual(['public', 'max-age=3600', 's-maxage=3600']) + }) + it('still lists the endpoint family and the SLO summary', async () => { const json = await body() expect(json.service).toBe('frequency') diff --git a/components/frequency/site/pages/home/weave.tsx b/components/frequency/site/pages/home/weave.tsx index 5cd28e50e1..36b944b9fd 100644 --- a/components/frequency/site/pages/home/weave.tsx +++ b/components/frequency/site/pages/home/weave.tsx @@ -67,11 +67,17 @@ export function Weave({ stages }: { stages: WeaveStage[] }) { const s = svg.current if (!host || !s) return const R = host.getBoundingClientRect() + // Measure each card once, and keep path writes detached until every read is complete. + const boxes = new Map() const box = (el: Element) => { + const cached = boxes.get(el) + if (cached) return cached const r = el.getBoundingClientRect() - return { x: r.left - R.left, y: r.top - R.top, w: r.width, h: r.height, cx: r.left - R.left + r.width / 2, cy: r.top - R.top + r.height / 2 } + const result = { x: r.left - R.left, y: r.top - R.top, w: r.width, h: r.height, cx: r.left - R.left + r.width / 2, cy: r.top - R.top + r.height / 2 } + boxes.set(el, result) + return result } - s.replaceChildren() + const paths = document.createDocumentFragment() const add = (d: string, colour: string, cols: number[], lane: Row, cls: string[]) => { const p = document.createElementNS(SVG_NS, 'path') p.setAttribute('d', d) @@ -79,7 +85,7 @@ export function Weave({ stages }: { stages: WeaveStage[] }) { cls.forEach((c) => p.classList.add(c)) p.dataset.cols = cols.join(',') p.dataset.lane = lane - s.appendChild(p) + paths.appendChild(p) } const link = (a: Element | null, b: Element | null, colour: string, cols: number[], flow: boolean, lane: Row) => { if (!a || !b) return @@ -123,6 +129,7 @@ export function Weave({ stages }: { stages: WeaveStage[] }) { setStageBoxes([]) setLoopTops(null) } + s.replaceChildren(paths) }, [mobile, stages]) // Draw after layout, on resize (debounced), and once the web fonts settle the boxes. diff --git a/test/e2e/public-surface.spec.ts b/test/e2e/public-surface.spec.ts index c9aacf64fb..e12822d2d8 100644 --- a/test/e2e/public-surface.spec.ts +++ b/test/e2e/public-surface.spec.ts @@ -88,11 +88,19 @@ test.describe('public surface (platform host)', { tag: '@public-surface' }, () = const res = await request.get('/api/status') expect(res.status()).toBe(200) expect(res.headers()['content-type']).toContain('application/json') - expect(res.headers()['cache-control']).toMatch(/s-maxage=\d+/) + // Vercel consumes s-maxage at its edge. Check the client policy and actual cache reuse. + const cacheControl = res.headers()['cache-control'].split(',').map((directive) => directive.trim()) + expect(cacheControl).toEqual(expect.arrayContaining(['public', 'max-age=3600'])) + expect(cacheControl).not.toContain('private') + expect(cacheControl).not.toContain('no-store') expect(res.headers()['set-cookie'], 'a cacheable machine endpoint sets no cookie').toBeUndefined() const json = await res.json() expect(json.service).toBe('frequency') expect(json.build.env).toBe('production') + await expect.poll(async () => { + const cached = await request.get('/api/status') + return cached.ok() ? cached.headers()['x-vercel-cache'] : cached.status() + }, { message: '/api/status is served from the Vercel cache after warming' }).toBe('HIT') }) test('unknown API paths and anonymous private APIs never 5xx and answer JSON-or-empty', async ({ request }) => {