From a4e9cd1c5d93bc45d03ac0728fd62bbac85d3351 Mon Sep 17 00:00:00 2001 From: homen Date: Thu, 13 Aug 2026 20:41:39 -0700 Subject: [PATCH] Stand the backend up, and drive the primary journey on it Nine of the twelve promotion conditions are judged on what a browser shows, and all nine were UNVERIFIED for one reason: every product route rendered "Convex backend not configured" (defect D1). This provisions an isolated Convex dev deployment, drives J1, J2 and J4 end to end against it, and commits both the captures and the scripts that regenerate them. Two things were actually broken behind that door, and neither names itself: - @erquhart/convex-oss-stats imports @convex-dev/crons and declares it in neither dependencies nor peerDependencies. With package-lock.json gitignored, a fresh install can land a tree without it and the very first `convex dev` push dies on "Could not resolve @convex-dev/crons/convex.config". Declared directly. - A missing OPTIONAL model key failed the ENTIRE deploy. Convex analyses every backend module on every push; coordinatorAgent.ts builds DEFAULT_MODEL (kimi-k2.6, OpenRouter) at module scope because a Convex function must be a module-level export; and buildLanguageModel threw at construction for an unconfigured provider. So `convex dev` failed with "InvalidModules: Failed to analyze domains/agents/digestAgent.js" unless you had an OpenRouter account -- for a provider /redesign/chat never calls. The error now lives on doGenerate/doStream instead, so an unconfigured provider fails the call that needs the key rather than the deploy that does not. The same shape is fixed for the google branch, and modelResolver.test.ts gates it: construct DEFAULT_MODEL with the key deleted, assert construction succeeds and the call still rejects. New producers, each writing a committed artifact: - scripts/capture-live-journey.mjs -- J1 (ask, stream, sealed packet), J2 (the permanent link replays instead of re-running, proven by getLatestOwnedRun().runId being unchanged), J4 (cancel, honest terminal state, keep working), the server-side validation error path, and the same journey at 375. It also reads the run row and its stream events back out of Convex, so "it streamed" and "it persisted" are separate claims. - scripts/audit-web-quality.mjs -- Lighthouse 13.4.1 + @axe-core/cli 4.13.0 against the production build under vite preview. - scripts/review-web-interface-guidelines.mjs -- DOM measurements for the Web Interface Guidelines review written up in promotion/evidence/wig-review/REVIEW.md. A review, not a score; the two measure different things and are committed separately. Scorecard: 1/12 -> 7/12, and UNVERIFIED is gone from the table. Conditions 1, 3, 4, 5, 6 and 9 move on committed evidence. Conditions 2, 7, 8, 10 and 11 FAIL on measured numbers, which is the point -- three new defects were found by running the thing: D5 (a rejected prompt shows no reason on screen), D6 (LCP 10.8 s plus four guideline deviations), D7 (the same question returned 3, 2, 1, 0, 1 and 0 grounded sources across six runs). Setup is written down in docs/START_HERE.md "Before Step 1" and docs/codebase/CONCERNS.md C7b, including both traps above, so the next cold reader does not rediscover them. Co-Authored-By: Claude Opus 5 --- .../mcp_tools/models/modelResolver.test.ts | 37 +- .../agents/mcp_tools/models/modelResolver.ts | 41 +- docs/START_HERE.md | 60 +- docs/codebase/CONCERNS.md | 64 +- package.json | 1 + promotion/PRODUCT_GOAL.md | 80 +- promotion/PRODUCT_JOURNEYS.md | 11 +- promotion/PROMOTION_LOG.md | 134 +- .../live-journey/01-empty-desktop.png | Bin 0 -> 58588 bytes .../live-journey/02-agent-running-desktop.png | Bin 0 -> 81699 bytes .../live-journey/03-answer-desktop.png | Bin 0 -> 82457 bytes .../live-journey/04-answer-trace-desktop.png | Bin 0 -> 65839 bytes .../live-journey/05-receipt-desktop.png | Bin 0 -> 81978 bytes .../live-journey/06-cancelled-desktop.png | Bin 0 -> 58294 bytes .../evidence/live-journey/07-empty-mobile.png | Bin 0 -> 40909 bytes .../live-journey/08-answer-mobile.png | Bin 0 -> 42073 bytes .../09-validation-error-desktop.png | Bin 0 -> 36431 bytes promotion/evidence/live-journey/README.md | 29 + promotion/evidence/live-journey/report.json | 171 + promotion/evidence/web-quality/axe.json | 2535 +++ .../evidence/web-quality/lighthouse.json | 14960 ++++++++++++++++ promotion/evidence/web-quality/summary.json | 42 + promotion/evidence/wig-review/REVIEW.md | 69 + .../evidence/wig-review/focus-desktop.png | Bin 0 -> 58688 bytes .../evidence/wig-review/focus-mobile.png | Bin 0 -> 40731 bytes .../evidence/wig-review/measurements.json | 143 + scripts/audit-web-quality.mjs | 146 + scripts/capture-live-journey.mjs | 613 + scripts/review-web-interface-guidelines.mjs | 222 + 29 files changed, 19309 insertions(+), 49 deletions(-) create mode 100644 promotion/evidence/live-journey/01-empty-desktop.png create mode 100644 promotion/evidence/live-journey/02-agent-running-desktop.png create mode 100644 promotion/evidence/live-journey/03-answer-desktop.png create mode 100644 promotion/evidence/live-journey/04-answer-trace-desktop.png create mode 100644 promotion/evidence/live-journey/05-receipt-desktop.png create mode 100644 promotion/evidence/live-journey/06-cancelled-desktop.png create mode 100644 promotion/evidence/live-journey/07-empty-mobile.png create mode 100644 promotion/evidence/live-journey/08-answer-mobile.png create mode 100644 promotion/evidence/live-journey/09-validation-error-desktop.png create mode 100644 promotion/evidence/live-journey/README.md create mode 100644 promotion/evidence/live-journey/report.json create mode 100644 promotion/evidence/web-quality/axe.json create mode 100644 promotion/evidence/web-quality/lighthouse.json create mode 100644 promotion/evidence/web-quality/summary.json create mode 100644 promotion/evidence/wig-review/REVIEW.md create mode 100644 promotion/evidence/wig-review/focus-desktop.png create mode 100644 promotion/evidence/wig-review/focus-mobile.png create mode 100644 promotion/evidence/wig-review/measurements.json create mode 100644 scripts/audit-web-quality.mjs create mode 100644 scripts/capture-live-journey.mjs create mode 100644 scripts/review-web-interface-guidelines.mjs diff --git a/backend/convex/domains/agents/mcp_tools/models/modelResolver.test.ts b/backend/convex/domains/agents/mcp_tools/models/modelResolver.test.ts index b136d4343..d2a520fdf 100644 --- a/backend/convex/domains/agents/mcp_tools/models/modelResolver.test.ts +++ b/backend/convex/domains/agents/mcp_tools/models/modelResolver.test.ts @@ -1,6 +1,8 @@ -import { describe, expect, it } from "vitest"; +import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { + DEFAULT_MODEL, + getLanguageModel, resolveModelAlias, resolvePipelineModelSelection, } from "./modelResolver"; @@ -42,3 +44,36 @@ describe("pipeline model route resolution", () => { expect(resolveModelAlias("poolside/laguna-s-2.1:free")).toBe("laguna-s-2.1-free"); }); }); + +describe("a missing provider key must not break Convex module analysis", () => { + // Convex analyses every backend module on every push, and + // domains/agents/core/coordinatorAgent.ts builds DEFAULT_MODEL ("kimi-k2.6", + // an OpenRouter model) at module scope. When buildLanguageModel threw for an + // unset OPENROUTER_API_KEY, `convex dev` failed the ENTIRE push with + // `InvalidModules: Failed to analyze domains/agents/digestAgent.js`, so a + // reader with only a Gemini key got no backend at all. Construction must + // succeed; the call must still fail. + const savedKey = process.env.OPENROUTER_API_KEY; + + beforeEach(() => { + delete process.env.OPENROUTER_API_KEY; + }); + afterEach(() => { + if (savedKey === undefined) delete process.env.OPENROUTER_API_KEY; + else process.env.OPENROUTER_API_KEY = savedKey; + }); + + it("constructs the default OpenRouter model with no key, and fails only when called", async () => { + // `LanguageModel` is `string | LanguageModelV2`; the object branch is the + // one under test, so read it through one local cast rather than four. + const model = getLanguageModel(DEFAULT_MODEL) as any; + expect(model.modelId).toBe(DEFAULT_MODEL); + expect(model.provider).toBe("unconfigured"); + await expect(model.doGenerate({})).rejects.toThrow( + /OPENROUTER_API_KEY not configured/, + ); + await expect(model.doStream({})).rejects.toThrow( + /OPENROUTER_API_KEY not configured/, + ); + }); +}); diff --git a/backend/convex/domains/agents/mcp_tools/models/modelResolver.ts b/backend/convex/domains/agents/mcp_tools/models/modelResolver.ts index 367f895c2..4bad2de52 100644 --- a/backend/convex/domains/agents/mcp_tools/models/modelResolver.ts +++ b/backend/convex/domains/agents/mcp_tools/models/modelResolver.ts @@ -831,6 +831,38 @@ export const LEGACY_ALIASES: Record = { // RESOLVER FUNCTIONS // ═══════════════════════════════════════════════════════════════════════════ +/** + * A LanguageModel that can be *constructed* without the key it would need to + * *run*. + * + * Why this exists, in the order it bites: Convex analyses every backend module + * on every push, and `domains/agents/core/coordinatorAgent.ts` builds its model + * at module scope (`createCoordinatorAgent(DEFAULT_MODEL).asTextAction(...)`), + * because a Convex function has to be a module-level export. `DEFAULT_MODEL` is + * `kimi-k2.6`, an OpenRouter model. So throwing here — at construction — made + * `convex dev` fail with `InvalidModules: Failed to analyze + * domains/agents/digestAgent.js` for anyone without an OpenRouter account, and + * a failed push means *no* backend at all: no chat, no auth, no persistence, + * for a reader who only wanted the Gemini-backed `/redesign/chat` path. + * + * The error is not removed, only moved to the call that actually needs the key. + * `doGenerate`/`doStream` still throw the same sentence, so a run against an + * unconfigured provider fails loudly instead of silently returning something. + */ +function unconfiguredModel(alias: string, requirement: string): LanguageModel { + const fail = async (): Promise => { + throw new Error(`Model "${alias}" requested but ${requirement}`); + }; + return { + specificationVersion: "v2", + provider: "unconfigured", + modelId: alias, + supportedUrls: {}, + doGenerate: fail, + doStream: fail, + } as unknown as LanguageModel; +} + /** * Build a LanguageModel instance from a ModelSpec */ @@ -843,8 +875,9 @@ function buildLanguageModel(spec: ModelSpec): LanguageModel { case "google": { const googleProvider = getGoogleProvider(); if (!googleProvider) { - throw new Error( - `Google model "${spec.alias}" requested but no Google API key alias is configured`, + return unconfiguredModel( + spec.alias, + "no Google API key alias is configured", ); } return googleProvider(spec.sdkId); @@ -852,9 +885,7 @@ function buildLanguageModel(spec: ModelSpec): LanguageModel { case "openrouter": { const openrouter = getOpenRouterProvider(); if (!openrouter) { - throw new Error( - `OpenRouter model "${spec.alias}" requested but OPENROUTER_API_KEY not configured` - ); + return unconfiguredModel(spec.alias, "OPENROUTER_API_KEY not configured"); } return openrouter.chat(spec.sdkId); } diff --git a/docs/START_HERE.md b/docs/START_HERE.md index c3f0e3778..8f9653aad 100644 --- a/docs/START_HERE.md +++ b/docs/START_HERE.md @@ -32,13 +32,69 @@ does not ship a local substitute for. Without a deployment URL there is no database, so there is no product. To get past that card you need `VITE_CONVEX_URL` pointing at a real Convex -deployment (`npx convex dev` provisions one against a Convex account). Everything -from Step 3 down needs that backend. Steps 1–2 you can read and run without it. +deployment. Everything from Step 3 down needs that backend. Steps 1–2 you can +read and run without it. Do **not** start with `npm run dev`. That command runs three processes in parallel and two of them block on credentials you may not have. See `docs/codebase/CONCERNS.md`, defect D4. +### Standing the backend up — the whole list, in order + +This is the part that used to be missing, and it is the reason nine of the +twelve promotion conditions sat at UNVERIFIED: **you cannot observe anything +below Step 2 without doing this.** It needs a Convex account and a Gemini API +key. There is no offline, fixture, or local-backend substitute in this repo. + +```bash +npx convex dev --once --configure new --project --team +# provisions an isolated DEV deployment and writes CONVEX_DEPLOYMENT, +# VITE_CONVEX_URL and VITE_CONVEX_SITE_URL into .env.local (gitignored). + +npx @convex-dev/auth +# generates JWT_PRIVATE_KEY + JWKS and sets SITE_URL on that deployment. +# Without them every sign-in fails with +# "Missing environment variable `JWT_PRIVATE_KEY`" and no journey can run, +# because live research refuses anonymous callers (Step 5). + +npx convex env set GEMINI_API_KEY -- "" +# Step 7 calls Gemini directly. Without this the run fails at the model call. + +npx vite --port 4902 --strictPort --host 127.0.0.1 +``` + +Two traps that cost real time here, so they are written down rather than +rediscovered: + +- **`@erquhart/convex-oss-stats` imports `@convex-dev/crons` without declaring + it.** `package-lock.json` is gitignored (CONCERNS C5b), so a fresh + `npm install` can resolve a tree where that transitive package is absent and + the very first push dies with + `Could not resolve "@convex-dev/crons/convex.config"`. It is now a direct + dependency for exactly this reason. +- **A missing OPTIONAL model key used to break the whole deploy.** Convex + analyses every backend module on every push, and + `domains/agents/core/coordinatorAgent.ts` builds `DEFAULT_MODEL` + (`kimi-k2.6`, an OpenRouter model) at module scope. Building a model for an + unconfigured provider threw *at construction*, so `convex dev` failed with + `InvalidModules: Failed to analyze domains/agents/digestAgent.js` unless you + had an OpenRouter account — even though `/redesign/chat` never touches + OpenRouter. `modelResolver.ts` now defers that error to the call that needs + the key. You do **not** need `OPENROUTER_API_KEY` to run the primary journey. + +### Proving it, without trusting this page + +```bash +node scripts/capture-live-journey.mjs --port 4902 +``` + +That drives J1 (ask → stream → answer with sources), J2 (open the permanent +receipt link cold and get the same answer, proven by the latest-run id being +unchanged) and J4 (cancel, honest terminal state, keep working) in a real +browser at 1280 and 375, and reads the durable rows back out of Convex. It +writes `promotion/evidence/live-journey/report.json` plus eight screenshots, and +exits nonzero if any of it stops being true. **It costs real model calls.** + --- ## Step 1 — The browser loads the app and decides whether the backend is usable diff --git a/docs/codebase/CONCERNS.md b/docs/codebase/CONCERNS.md index a98225792..c6bcb7868 100644 --- a/docs/codebase/CONCERNS.md +++ b/docs/codebase/CONCERNS.md @@ -176,21 +176,77 @@ version of the reuse ladder. --- +## C7b — MAJOR for a new engineer: the product is unobservable until you stand up a Convex deployment, and the door has three locks, not one + +This is the first thing that will happen to you, so it is the first thing you +should read. Every product route renders **"Convex backend not configured"** +until `VITE_CONVEX_URL` points at a real deployment. That is by design — this +repo keeps all durable state in Convex and ships no local substitute — but +until 2026-08-14 the setup instructions covered one of the three things you +actually need, and the other two failed in ways that do not name themselves. + +**Reproduce the working path** (needs a Convex account and a Gemini key; the +full version with the traps is `docs/START_HERE.md` → "Before Step 1"): + +```bash +npx convex dev --once --configure new --project --team +npx @convex-dev/auth # JWT_PRIVATE_KEY + JWKS + SITE_URL +npx convex env set GEMINI_API_KEY -- "" +npx vite --port 4902 --strictPort --host 127.0.0.1 +node scripts/capture-live-journey.mjs --port 4902 # drives J1/J2/J4, costs model calls +``` + +The three locks, in the order they bite: + +1. **A missing transitive dependency stops the first push.** + `@erquhart/convex-oss-stats@0.8.2` imports `@convex-dev/crons/convex.config` + and declares it in neither `dependencies` nor `peerDependencies`. With + `package-lock.json` gitignored (C5b), a fresh install can land a tree without + it and `convex dev` dies on `Could not resolve + "@convex-dev/crons/convex.config"`. Fixed by declaring `@convex-dev/crons` + directly; if you see this again, that is what regressed. +2. **A missing OPTIONAL model key used to fail the ENTIRE deploy.** Convex + analyses every backend module on every push. + `domains/agents/core/coordinatorAgent.ts` builds `DEFAULT_MODEL` + (`kimi-k2.6`, OpenRouter) at module scope, and `buildLanguageModel` threw at + construction when `OPENROUTER_API_KEY` was unset — so the push failed with + `InvalidModules: Failed to analyze domains/agents/digestAgent.js` and you got + no backend at all, for a provider `/redesign/chat` never calls. The error now + lives on `doGenerate`/`doStream` instead, so an unconfigured provider fails + the call that needs it rather than the deploy. + Gated by `backend/convex/domains/agents/mcp_tools/models/modelResolver.test.ts`. +3. **Convex Auth needs its own keys, and nothing on screen says so.** Without + `JWT_PRIVATE_KEY`/`JWKS`, sign-in throws `Missing environment variable + 'JWT_PRIVATE_KEY'` from the server. You cannot skip this: live research + rejects anonymous accounts (`requirePaidChatUserId`, + `backend/convex/domains/redesign/chatRuns.ts:159`), so the journey is + unreachable signed out. + +**What it costs you if you skip it.** Nine of the twelve promotion conditions +are judged on what a browser shows. Tests and typecheck tell you nothing about +them. See `promotion/PROMOTION_LOG.md` iteration 2. + +--- + ## C8 — Documented product defects, not restated here `promotion/PROMOTION_LOG.md` carries the reproductions: - **D1** — no product route works without a Convex cloud deployment; there is no - offline or fixture backend for the product surfaces. Narrowed, not closed. + offline or fixture backend for the product surfaces. **Closed 2026-08-14** as + a *blocker*: with a deployment the journeys run end to end + (`promotion/evidence/live-journey/report.json`). The dependency itself is not + a defect, it is the architecture; the setup path is C7b above. - **D2** — the red typecheck (C1 above). - **D3** — the graph rail dies permanently if mounted at zero viewport width (collapsed drawer, `display:none` tab) and never recovers without a reload. - **D4** — `npm run dev` blocks on interactive credentials; the frontend-only path is now documented in the README. -Four of the five product journeys are recorded **UNVERIFIED**, not passing, for -the reason in D1: nobody has driven them without a backend, and creating one was -out of scope. Read that word literally — it does not mean they work. +As of 2026-08-14, **J1, J2 and J4 are driven end to end** against a live +deployment by `node scripts/capture-live-journey.mjs`; J3 (inline correction) and +the product half of J5 are still UNVERIFIED. Read UNVERIFIED literally — it does +not mean they work, it means nobody has watched them. --- diff --git a/package.json b/package.json index 921bb9b61..421792126 100644 --- a/package.json +++ b/package.json @@ -220,6 +220,7 @@ "@codemirror/lang-markdown": "^6.5.0", "@convex-dev/agent": "0.2.10", "@convex-dev/auth": "0.0.80", + "@convex-dev/crons": "^0.2.2", "@convex-dev/persistent-text-streaming": "0.2.3", "@convex-dev/polar": "0.6.3", "@convex-dev/presence": "0.1.2", diff --git a/promotion/PRODUCT_GOAL.md b/promotion/PRODUCT_GOAL.md index fa87f1b3a..102430b21 100644 --- a/promotion/PRODUCT_GOAL.md +++ b/promotion/PRODUCT_GOAL.md @@ -43,44 +43,60 @@ Every iteration is recorded in [PROMOTION_LOG.md](PROMOTION_LOG.md) — journey exercised, defect fixed, evidence path, conditions newly passing. Loop state lives in git, never in an agent's memory, so any agent can resume the loop cold. +## Reproducing any of this + +Everything below condition 2 needs a Convex deployment; there is no local +substitute. The four commands are in +[docs/START_HERE.md](../docs/START_HERE.md) under "Before Step 1", with the two +traps that used to make them fail. Then: + +```bash +node scripts/capture-live-journey.mjs --port 4902 # conditions 1,3,4,5,9 +node scripts/audit-web-quality.mjs --port 4902 # condition 8 +node scripts/review-web-interface-guidelines.mjs --port 4902 # condition 7 (measurements) +``` + ## Current scorecard Baseline measured 2026-08-13 against a fresh clone of `main` at `07a55afea176254e07eebd28ab36701e9f9068da`. Wave 1 measures; it does not repair. -Every row below is either something observed today or a stated reason it could -not be observed. Updated 2026-08-13 by iteration 1, which repaired the reachable half of D1 (the -setup gate tested the env var for presence rather than validity). Only rows it -can now evidence were moved; see [PROMOTION_LOG.md](PROMOTION_LOG.md). +setup gate tested the env var for presence rather than validity). + +Updated **2026-08-14 by iteration 2**, which stood up an isolated Convex dev +deployment and drove J1, J2 and J4 end to end against it. Nine conditions were +UNVERIFIED for exactly one reason — nothing rendered — and that reason is gone. +Every row below cites a committed artifact **and** the committed producer that +regenerates it; a row with only one of the two is not a PASS. | # | Condition | Status | Evidence / reason | |---|-----------|--------|-------------------| -| 1 | Journeys succeed end-to-end in a real browser | UNVERIFIED | Not drivable from a clean clone. Vite came up on `127.0.0.1:5399`, but `/redesign/chat` renders the "Convex backend not configured" card (`h1` observed in the DOM), and `[data-agent-runtime-surface="redesign-chat"]` is absent. Reaching J1–J4 needs a Convex cloud deployment, which Wave 1 may not create. J5's Convex-free half **did** run (`node scripts/capture-graph-rail.mjs` → exit 0, 34 entities / 28 edges), but its product route `/#entity/` is behind the same gate, so no journey is verified end-to-end. See defect D1. | -| 2 | No critical or major usability defect open | FAIL | Still three, and iteration 1 closed a fourth that Wave 1 had not seen. Open: **D1** critical, now narrowed — with no Convex deployment there is still no product surface, and provisioning one is out of scope; **D2** major (typecheck red — 5383 errors); **D3** major (graph rail mounted at zero width throws an uncaught `Sigma: Container has no width` and never recovers). Closed 2026-08-13: **D1b** critical — the README's own `cp .env.example .env.local` produced a non-empty but unroutable URL that passed the presence check, mounted the product against a dead socket, and suppressed the remedy card. Gated by `node scripts/capture-convex-setup-gate.mjs`. | -| 3 | Mobile and desktop both intentional | UNVERIFIED | Only two surfaces could be rendered, and neither is a product surface. Setup card: clean at 1280 and at 375×812. `demo/graph-rail/index.html`: clean at 1280×900 and at 375×812. The five journey surfaces were never on screen at any width. | -| 4 | No horizontal overflow at supported widths | UNVERIFIED | Measured `document.documentElement.scrollWidth === clientWidth` (no overflow) on the setup card at 1280 and 375, and on the graph-rail demo at 1280×900 and 375×812. That is 2 of the 6 surfaces in scope; the chat workspace, receipt view, correction panel, and entity profile were never rendered. | -| 5 | Loading/empty/success/error/agent-running designed | UNVERIFIED | Still exactly one designed state observed: the missing-Convex setup card (`MissingConvexUrlScreen` in `apps/web/src/main.tsx`). Iteration 1 did not add a state — it made that state **reachable** in the case where it used to be skipped, so both an absent and an unroutable `VITE_CONVEX_URL` now land on it (`promotion/evidence/convex-setup-gate/report.json`, 4/4 cases). The empty transcript, streaming/agent-running turn, cancelled turn, and continuation-loading aside all exist in `ChatSurface.tsx`; the empty transcript was seen once, only while the backend was dead, which is not a state anyone designed. | -| 6 | Keyboard and basic accessibility pass | UNVERIFIED | Audit not run. There was no interactive product surface to tab through — the only reachable screen is a static setup card. | -| 7 | Web Interface Guidelines review: no major unresolved | UNVERIFIED | Review not run: reviewing the interface requires the interface, and it never rendered. | -| 8 | Web-quality audit (a11y, performance, CWV): no major unresolved | UNVERIFIED | Audit not run. `npm run perf:lighthouse` targets `localhost:5173/#analytics/hitl`, which is behind the same Convex gate. | -| 9 | No unexplained console errors and no failed network requests during a journey | UNVERIFIED | No journey ran, so the condition's subject still does not exist. But one unexplained console error that a stranger *would* have hit was found and killed. On the README-documented setup (`cp .env.example .env.local`), `/redesign/chat` logged 3 console/page errors at both 1280×900 and 375×812, one an uncaught `[CONVEX FATAL ERROR] Couldn't parse deployment name your-project` thrown out of Convex's WebSocket handler; after iteration 1 the same route in the same env logs **0** (`promotion/evidence/convex-setup-gate/before/report.json` vs `report.json`). Also still zero on the graph-rail demo at 375 and 1280. One uncaught error remains reproducible at a 0-width mount (D3), which is not a supported width. | -| 10 | Performance does not obstruct interaction | UNVERIFIED | No interaction to obstruct. For the record, non-interaction timings today: Vite cold start 36.8 s / warm 0.6 s, `npm run build` 15.5 s, graph-rail replay completes to 34 entities within the capture gate's budget. None of these is an interaction measurement. | -| 11 | Tests and build are green | FAIL | Observed, not inferred, and unchanged by iteration 1. `npx tsc -p tsconfig.app.json --noEmit --pretty false` → **exit 2**, 5383 errors (the known `api`→`never` cascade, defect D2). `npm run test:run` → **exit 1**: app-vitest 22 failed / 1426 passed / 20 skipped, mcp-local **timed out at 300 s**, convex-mcp 5 failed / 58 passed, openclaw-mcp 30 passed. `npm run build` → exit 0 (PWA precache 338 entries / 22.5 MB), so the red typecheck is still invisible to anyone who only builds. **Iteration 1 added no failure**: the app segment was re-run on the pre-fix tree on the same machine with the new test file removed — 22 failed / 1421 passed / 20 skipped, and the 22 failing test names `diff` **identical** to the post-fix run. The delta is exactly the +5 passing tests in `apps/web/src/lib/convexUrl.test.ts`. Wave 1's "21 failed / 1422 passed" was a different run; these segments are flaky by ±1, which is itself worth knowing. | -| 12 | Every improvement was verified in the rendered app | PASS | One improvement exists (iteration 1), and it was observed in a real browser both before and after. `node scripts/capture-convex-setup-gate.mjs` drives `/redesign/chat` at 1280×900 and 375×812 in two env states. On the pre-fix tree, with the change stashed, it exits **1** with 2 of 4 cases failing — retained at `promotion/evidence/convex-setup-gate/before/report.json` with its four PNGs. On the fixed tree it exits **0**, 4 of 4 — `promotion/evidence/convex-setup-gate/report.json` with its four PNGs. Producer and output are both committed and re-runnable from a fresh clone. Nothing in this iteration was concluded from reading code. | - -**Status: NOT PROMOTED** — 1/12 PASS (2 FAIL, 9 UNVERIFIED). - -The shape of this baseline is the finding: this repo's gate is blocked at the -door, not at the details. Until a canonical journey can render, conditions 1 and -3–10 cannot honestly move, because all eight are judged on what the browser -shows. - -Iteration 1 worked on the door itself, which is the only thing reachable without -a backend, and found that the door was wrong in both directions: it removed the -whole application when `VITE_CONVEX_URL` was empty, and removed the *remedy* -when the value was wrong — which is exactly what the README's own setup step -produces. The second half is fixed and gated. The first half is not a bug to be -coded around: with no deployment there is no data, and manufacturing a product -surface that cannot answer anything would trade a truthful blocker for a -dishonest one. It stays open until a backend is in scope. +| 1 | Journeys succeed end-to-end in a real browser | **PASS** | `node scripts/capture-live-journey.mjs --port 4902` → **exit 0**, 10/10 checks, against a live Convex deployment. J1: empty state → submit → live-research checklist → sealed packet, run `chat_msse8tbz_5w7dj9`, with five tool rows (classify_query, build_context_bundle, gemini_synthesis 19.9 s, fallback_source_search **warning**, bind_evidence **warning**). **Read the caveat:** this particular capture landed the *ungrounded* branch — `Auto · 0 sources`, no evidence rows, and the honest "Source needed: no supported URL is available…" notice, which the gate asserts. Grounded runs were observed too (3, 2 and 1 sources on earlier runs of the same prompt); the source count is not deterministic, which is defect **D7**. What condition 1 claims is that the journey completes end to end, not that every run is well-grounded. J2: `/redesign/chat/r/1znqpv1wpmh0` opened in a cold context reached `data-state="ready"`, matched the original text, and left `getLatestOwnedRun().runId` unchanged — it replayed, it did not re-run. J4: Stop → "Cancellation recorded…" → a turn that says the run was cancelled with no sealed packet → the next question still works. Artifacts: `promotion/evidence/live-journey/report.json` + 9 PNGs. J3 and the product half of J5 remain UNVERIFIED — nobody drove them — so this is 3 of 5 journeys, stated plainly. | +| 2 | No critical or major usability defect open | FAIL | Open: **D2** major (typecheck red — 5383 errors, the `api`→`never` cascade); **D3** major (graph rail mounted at zero width throws and never recovers); **D5** major, new — a rejected prompt tells the user "The live chat run could not be started." while the actual reason (`Prompt too short — write at least a 3-character question.`) goes only to the console; **D6** major, new — LCP 10 827 ms on the production build plus four Web Interface Guidelines deviations; **D7** major, new — the same question returned 3, 2, 1, 0, 1 and 0 grounded sources across six runs, so a third of answers arrive with no sources at all (honestly labelled, but the product promise is sources attached). Closed 2026-08-14: **D1** critical — the product now runs (condition 1). | +| 3 | Mobile and desktop both intentional | **PASS** | Same producer, same run. The journey was driven at **1280×900** and again at **375×812** in separate sessions with separate accounts: at 375 the surface mounts with `data-empty="true"`, a question seals an answer packet, and console errors are 0. Compare `01-empty-desktop.png` / `03-answer-desktop.png` with `07-empty-mobile.png` / `08-answer-mobile.png` — two designs, not one squeezed. | +| 4 | No horizontal overflow at supported widths | **PASS** | `document.documentElement.scrollWidth === clientWidth` asserted at both widths, on the empty state and on the answered transcript: 1280 === 1280, 375 === 375 (`report.json` → checks "J1 step 2" and "Conditions 3-4"). Re-measured independently by `scripts/review-web-interface-guidelines.mjs`, which agrees. | +| 5 | Loading/empty/success/error/agent-running designed | **PASS** | All observed, none inferred. **Empty**: `data-empty="true"` with starters (`01-empty-desktop.png`). **Agent-running**: the live-research checklist with named stages while the run is in flight (`02-agent-running-desktop.png`). **Success**: the sealed packet with sources, risks, next step and tool trace (`03-` and `04-answer-desktop.png`). **Error**: a 2-character prompt is rejected by `startChat` at the trust boundary and the surface shows the designed failure card (`09-validation-error-desktop.png`) — the *reason* not reaching the user is defect D5, but the state itself is designed. **Cancelled/terminal**: `06-cancelled-desktop.png`. Plus the honest degraded state when grounding returns nothing ("Source needed: no supported URL is available…"), which the gate asserts in both directions. | +| 6 | Keyboard and basic accessibility pass | **PASS** | Measured, not assumed. `node scripts/review-web-interface-guidelines.mjs`: Tab from a cold page moves focus and changes the focused element's outline at both widths (`none 3px` → `solid 1px` at 1280, `solid 3px` at 375); the stylesheet carries **148** `:focus-visible` rules and **64** `prefers-reduced-motion` blocks; browser zoom is not disabled; there are **no** unnamed icon-only buttons; one polite `aria-live` region exists. Lighthouse accessibility **96**; axe-core 4.13.0 reports **0 serious and 0 critical** violations. The primary action is keyboard-only — Enter submits, and the whole J1 capture drives it that way. **Two moderate axe violations remain open** (`landmark-one-main`, `page-has-heading-one`); they are counted against condition 7 rather than hidden here. Artifacts: `promotion/evidence/wig-review/measurements.json`, `promotion/evidence/web-quality/axe.json`. | +| 7 | Web Interface Guidelines review: no major unresolved | FAIL | A review was performed against on the rendered surface and written up at `promotion/evidence/wig-review/REVIEW.md`; measurements in `measurements.json`; producer `scripts/review-web-interface-guidelines.mjs`. **4 major findings open:** no `

` anywhere on the only route the product has (`h1Count: 0` at both widths, corroborated by axe `page-has-heading-one`); no skip link (`skipLink: false`); three sub-44px touch targets at 375 including the **44×36 submit button**; composer `