From d686cea36927e78a5d2a4b218b31c3f1383c2b4f Mon Sep 17 00:00:00 2001 From: Brian Love Date: Fri, 11 Sep 2026 21:05:17 -0700 Subject: [PATCH 01/18] docs(specs): homepage trust band shrinks and a No-runtime band lands after the architecture diagram Co-Authored-By: Claude Fable 5.1 --- ...mepage-trust-band-and-no-runtime-design.md | 144 ++++++++++++++++++ 1 file changed, 144 insertions(+) create mode 100644 docs/superpowers/specs/2026-09-11-homepage-trust-band-and-no-runtime-design.md diff --git a/docs/superpowers/specs/2026-09-11-homepage-trust-band-and-no-runtime-design.md b/docs/superpowers/specs/2026-09-11-homepage-trust-band-and-no-runtime-design.md new file mode 100644 index 000000000..60079eca8 --- /dev/null +++ b/docs/superpowers/specs/2026-09-11-homepage-trust-band-and-no-runtime-design.md @@ -0,0 +1,144 @@ +# The trust band shrinks, and a No-runtime band lands after the architecture diagram + +**Date:** 2026-09-11 +**Status:** Approved in conversation, awaiting written review +**Branch:** `blove/homepage-trust-no-runtime` +**Supersedes:** the two-column checklist in `2026-09-08-preflight-checklist-design.md` §2 (the Airworthiness block survives; the Threadplane and Yours columns do not) + +## 1. Why + +Two homepage changes, argued together because they trade against each other. + +**The preflight checklist is too long.** Twenty-seven rows at 33px, roughly 1,100px of dark band on desktop, and the section's job was to communicate final-mile completeness and trust. The Threadplane column had become a feature list, and the Yours column, the part that made the section honest, only made sense beside it. Both come out. What stays is the only part of the band that is not self-reported: the third-party scores. + +**The site never says "no runtime."** Threadplane's adapters call your LangGraph or AG-UI server from the browser. There is no Threadplane server between the two, no key to obtain, no dev-only flag to reach for, and no licence tier to unlock production. Other agent UI kits in the same space ship a "runtime" of their own, and the research behind this spec (recorded in the session notes, not in the repo, because competitors are never named in tracked files) established the shape of the claim we can make honestly: + +- One widely used kit documents its runtime as "the backend layer that connects your frontend application to your AI agents" and the recommended way to use it. Its free path to a direct AG-UI connection is a prop whose name ends in `unsafe_dev_only`, documented as "intended for development and prototyping" and "not officially supported." Its production-sanctioned direct path is labelled part of a paid Enterprise tier. Without its runtime, its own comparison table lists auth as yours, its middleware as "Not available," routing as manual, and threads as unavailable. +- A newer kit's client now speaks AG-UI directly and can target any AG-UI server, but its agent loop, server tools, approval pause and resume, client-tool round trip, structured-output enforcement, persistence and resume, and MCP support all live in its server call. Its own migration guide says whether a foreign server invokes client tools "depends on its tool-calling logic." + +So the honest claim is **"no runtime, and nothing degrades,"** not "nobody else can do this." The band makes the first claim and stays silent on the second. + +**What the words mean here.** "Runtime" means a server component shipped by the UI vendor that sits between the browser and your agent. It does not mean your agent's own runtime, which the architecture diagram directly above already labels as yours. `@threadplane/middleware` is not a runtime in this sense: it runs inside your LangGraph graph, binding browser-declared tools onto your model and routing client-tool turns back to the browser. Nothing of ours handles the request in flight. + +## 2. Section 1: the trust band + +`components/landing/Reliability.tsx` keeps its id `#proof`, its `dark` surface, the masthead line, the watermark, the eyebrow **Climb performance**, and the heading **Audited, scored, published.** with id `proof-heading`. The spine e2e in `e2e/website.spec.ts` stays green without edits. + +### 2.1 What comes out + +- `PREFLIGHT_OURS` (11 rows) and `PREFLIGHT_YOURS` (8 rows), their column headings, the two-column grid, and the `.preflight-cols` / `.preflight-yours` CSS. +- The three self-reported Airworthiness rows: Cloud, Signup, VC board. The masthead 200px above already says "no account, no cloud," the FAQ says no hosted service, and the About page carries the rest. +- The spec assertion that pinned the Yours column length, and the checklist spec's "claims nothing in the Yours column" case. + +### 2.2 What stays + +A single list of five rows, one column, every row linked to its source, in this order: + +| Challenge | Response | Source | +| --- | --- | --- | +| Framework rank | #8 OF 119 | hvtracker.net categories | +| OpenSSF Scorecard | 8.2 / 10 | scorecard.dev | +| Supply-chain grade | live HVTrust badge | hvtracker.net/agents | +| Angular support | 20–22 CI-TESTED (derived) | npmjs.com | +| Release provenance | SIGNED · OIDC · SLSA | npmjs.com | + +Rank and score drift. Re-verify both against the live pages on touch, never round up. The badge stays a live `` with real alt text. The Angular range stays derived from `WEBSITE_SUPPORTED_ANGULAR_MAJORS`, and the mutation-style guard that proves it is derived stays. + +The data module `lib/preflight-checklist.ts` shrinks to the one export, `AIRWORTHINESS`, and its header comment is rewritten for the new shape. The renderer `PreflightChecklist.tsx` keeps the tick sequence, the IntersectionObserver trigger, and the reduced-motion path, now over five rows. The list keeps a visible label so it is named for assistive tech; the label text becomes **Airworthiness** as before, and the `.preflight-cols + .preflight-col-head` spacing rule is deleted with the columns. + +The aside beneath the heading is rewritten because Vx/Vy framed a two-column climb: **"Not self-reported. Every figure links to the body that published it."** + +Height on desktop drops from roughly 1,100px to roughly 450px. + +### 2.3 Guards + +- `preflight-checklist.spec.ts`: five rows, every row linked, human-readable sources only, live badge, derived Angular range. The length assertion stays, because a row added without a source is the failure mode this list exists to prevent. +- `PreflightChecklist.spec.tsx`: one row per data row, every row an ``, no form controls, one labelled list, badge alt text, safe external links. +- `Reliability.spec.tsx`: masthead first, id and surface, eyebrow and heading, no customer-claim vocabulary, and the new aside text. + +## 3. Section 2: the No-runtime band + +A new `components/landing/NoRuntimeBand.tsx`, inserted in `app/page.tsx` between `` and ``. Section id `no-runtime`, heading id `no-runtime-heading`, surface `dark`. + +### 3.1 Layout + +The split chosen in the visual companion. Two columns on desktop, copy left and diagram right, vertically centred against each other. + +**Left column** + +- Eyebrow, in the Fork-us eyebrow treatment (mono, 11px, 0.18em, signal yellow): **Cleared direct**. Controller phraseology for a clearance straight to a fix, skipping the intermediate ones. Texture, like "Squawk 1200": the headline carries the meaning. +- Headline, in the Fork-us display treatment (Archivo Black, clamp 56px to 116px, line-height 0.9): **No runtime.** +- One supporting sentence, 17px, max-width around 480px: **"Your users reach your LangGraph or AG-UI server from your Angular app. Nothing of ours in between: no cloud, no key, no dev-only flag."** +- One text link in signal yellow, no button: **How it is wired →** to `/docs/choosing-an-adapter`, the page that already states the adapters talk to your server directly. Tracked with a new `CtaId` member `home_no_runtime_docs`, `track: 'developer'`, `surface: 'home'`. + +No licence chip and no button, so the band stays lighter than Fork-us and the two dark bands do not read as one. + +**Right column: two vertical flows**, side by side, each under a mono column label: + +| The usual | Threadplane | +| --- | --- | +| Your users | Your users | +| ↓ grey | ↓ yellow, taller | +| Their runtime (dashed border, muted, struck through) | Your agent (yellow border) | +| ↓ grey | | +| Your agent (yellow border) | | + +Nodes are mono, uppercase, 12px, bordered boxes with a fixed min-width so the two columns align. "Your users" echoes the first column label of the architecture diagram directly above. The ghost node is the only dashed, struck-through element on the page, which is what makes it read as "not here." + +The diagram is plain HTML and CSS, not SVG: three boxes and two arrows do not need the diagram kit, and HTML keeps the text selectable and the nodes measurable with `getBoundingClientRect`. + +**The runway stripe** runs along the foot of the band, the same 46px-on-92px marking as Fork-us, so the two dark bands rhyme. + +**Under 820px** the columns stack: copy first, then the two flows side by side beneath it. **Under 480px** the flows themselves stack, "The usual" above "Threadplane." Node text never wraps. + +### 3.2 Accessibility + +- The section is labelled by its heading. The flow diagram is one `
` with a `
` that reads the same two flows in prose, visually hidden: **"The usual path runs from your users through the vendor's runtime to your agent. With Threadplane your users reach your agent directly."** The arrows and the strikethrough are decorative (`aria-hidden`). +- The struck-through node keeps the word visible; the strikethrough is not the only signal, the dashed border and muted colour carry it too. +- No motion. + +### 3.3 Copy lives in one place + +`lib/positioning.ts` gains `NO_RUNTIME_BAND` beside `OPEN_SOURCE_STRIP`: eyebrow, headline, body, link label, link href, the two column labels, the three node labels, and the figure caption. The component reads only from it. `positioning.spec.ts` pins the eyebrow, the headline, the 12-character headline budget from the Fork-us test, the link href, and that the body says "no cloud" and never says "no proxy" (the one wording this spec reversed on purpose). + +### 3.4 Copy rules honoured + +- No competitor is named anywhere: not in copy, not in comments, not in the spec, not in the plan. +- "No cloud" rather than "no proxy." Putting your agent behind your own proxy is something the docs tell you to do, so "no proxy" would be false. "No cloud" is true and matches the masthead. +- "No dev-only flag" and "no key" are literal descriptions of what Threadplane does not have. They are not claims about anyone else. +- None of the barred patterns in `lib/public-copy-contract.ts` appear. The source scan in `public-copy.spec.ts` covers the new strings automatically. + +### 3.5 Guards + +- `NoRuntimeBand.spec.tsx`: renders the eyebrow, headline and body from `NO_RUNTIME_BAND`; exactly one `` in the section; the section is `dark` with id `no-runtime`; the figure has a caption; the ghost node exists once and the "Threadplane" flow has no ghost; the runway is a sibling of the container, not inside it, mirroring the Fork-us guard. +- `e2e/website.spec.ts`: the spine array gains `no-runtime-heading` between `architecture-heading` and `open-source-heading`. +- A new `e2e/home-no-runtime.spec.ts`: at 1440px the two flow columns' node boxes share a left edge per column and no node text overflows its box; at 390px the flows stack and every node still fits its box. This follows the measure-not-eyeball rule the architecture e2e set. + +## 4. Section 3: one FAQ entry + +`HomeFAQ.tsx` gains a fifth item after "Does Threadplane require a hosted service or an account?": + +**Q:** Does Threadplane have a runtime I need to deploy? +**A:** No. The adapters call your LangGraph or AG-UI server from the browser. There is no Threadplane server in the request path, no key, and no production tier. Link: **How it is wired** → `/docs/choosing-an-adapter`. + +`HomeFAQ.spec.tsx` gains an assertion for the new question and, if it pins the item count, moves it to five. The wording stays inside the public-copy contract. + +## 5. Order of the spine after this change + +hero → proof (trust band) → compatibility → architecture → **no-runtime** → open-source → stage → teams → FAQ → articles. + +Two dark bands now sit back to back. Each dark section paints its own 1px yellow seam at its top edge, and the No-runtime band has no button while Fork-us has one, so the boundary reads. This is a deliberate choice, recorded here so it is not mistaken for an accident. If it reads as one tall block in the browser, the fallback is to drop the runway stripe from the No-runtime band and keep it on Fork-us only. + +## 6. Known, accepted + +- **The Yours column argument leaves the homepage.** The product boundary (server-generated thread IDs, keys never in the bundle, CORS is yours) is now stated only in the docs. The No-runtime band carries the sharper half of that argument. +- **The homepage loses its list of product capabilities in checklist form.** The Stage section's four beats and the architecture diagram's capability links remain the capability surface. +- **The comparison is implied, never drawn against a named product.** A reader who does not know other kits ship a runtime reads the band as a plain fact about Threadplane, which is fine: it is one. + +## 7. Verification + +- `npx nx test website`, `npx nx lint website`, `npx nx build website`. Lint and test do not typecheck; only the build does, so the build is not optional. +- `npx nx e2e website` with the spine test, the new No-runtime e2e, and the existing `#proof` assertions passing. +- Every `href` in both sections resolves, including the four off-site registry links. +- Measured, not eyeballed: the trust band's five boxes share one left edge and five responses share one right edge; the No-runtime nodes share edges per column at 1440px and stack cleanly at 390px. +- Rank and Scorecard figures re-read from the live pages on the day of the PR. From 0049ed6f15cab9407262bd71eddda077df6d2340 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Fri, 11 Sep 2026 21:13:13 -0700 Subject: [PATCH 02/18] docs(plans): homepage trust band and No-runtime band implementation plan Co-Authored-By: Claude Fable 5.1 --- ...9-11-homepage-trust-band-and-no-runtime.md | 1523 +++++++++++++++++ 1 file changed, 1523 insertions(+) create mode 100644 docs/superpowers/plans/2026-09-11-homepage-trust-band-and-no-runtime.md diff --git a/docs/superpowers/plans/2026-09-11-homepage-trust-band-and-no-runtime.md b/docs/superpowers/plans/2026-09-11-homepage-trust-band-and-no-runtime.md new file mode 100644 index 000000000..87404f3da --- /dev/null +++ b/docs/superpowers/plans/2026-09-11-homepage-trust-band-and-no-runtime.md @@ -0,0 +1,1523 @@ +# Homepage trust band + No-runtime band Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Shrink the homepage's dark proof band to the five third-party Airworthiness rows, and add a bold "No runtime." band (copy left, two vertical flow diagrams right) between the architecture diagram and the Fork-us band, plus one FAQ entry. + +**Architecture:** Section 1 is a deletion: the two checklist columns and three self-reported rows leave `lib/preflight-checklist.ts`, `PreflightChecklist.tsx`, and `landing.css`, and the guards shrink with them. Section 2 is a new client-free server component `NoRuntimeBand.tsx` that reads every string from a new `NO_RUNTIME_BAND` object in `lib/positioning.ts`, draws the flows in plain HTML/CSS, and is pinned by a unit spec, the spine e2e, and a new measuring e2e. Section 3 adds one FAQ item. + +**Tech Stack:** Next.js app in `apps/website` (React server components, `'use client'` only where hooks are used), Vitest + Testing Library (jsdom), Playwright e2e, plain CSS in `apps/website/src/styles/landing.css`, Nx targets. + +**Spec:** `docs/superpowers/specs/2026-09-11-homepage-trust-band-and-no-runtime-design.md` + +**Ground rules for every task** +- Never write a competitor's product name anywhere: not in code, comments, tests, commit messages, or this plan. Describe generically ("other agent UI kits"). +- Run commands from the worktree root `/Users/blove/repos/angular-agent-framework/.claude/worktrees/homepage-trust-no-runtime`. Dependencies are already installed there (`npm ci` has been run); do not run `npm install`. +- Unit tests: `npx nx test website -- `; the `--` passes the filter to vitest. Lint: `npx nx lint website`. Build (the only typecheck): `npx nx build website`. +- The website's lint reports warnings on many files already; only **errors** fail CI. Strip ANSI before grepping lint output if you pipe it. +- Commit after every task with the trailer `Co-Authored-By: Claude Fable 5.1 `. + +--- + +## File map + +| File | Change | Responsibility | +| --- | --- | --- | +| `apps/website/src/lib/preflight-checklist.ts` | Modify | Only `AIRWORTHINESS` (5 rows) and `ChecklistRow` remain | +| `apps/website/src/lib/preflight-checklist.spec.ts` | Modify | Guards the five rows | +| `apps/website/src/components/landing/PreflightChecklist.tsx` | Modify | Renders one labelled list of five ticking rows | +| `apps/website/src/components/landing/PreflightChecklist.spec.tsx` | Modify | One list, every row linked | +| `apps/website/src/components/landing/Reliability.tsx` | Modify | New aside text, comment updated | +| `apps/website/src/components/landing/Reliability.spec.tsx` | Modify | Pins the new aside | +| `apps/website/src/styles/landing.css` | Modify | Delete column CSS; add `.no-runtime-*` rules | +| `apps/website/src/lib/positioning.ts` | Modify | Add `NO_RUNTIME_BAND` | +| `apps/website/src/lib/positioning.spec.ts` | Modify | Pins `NO_RUNTIME_BAND` | +| `apps/website/src/lib/analytics/events.ts` | Modify | Add `'home_no_runtime_docs'` to `CtaId` | +| `apps/website/src/components/landing/NoRuntimeBand.tsx` | Create | The band | +| `apps/website/src/components/landing/NoRuntimeBand.spec.tsx` | Create | Unit guards | +| `apps/website/src/app/page.tsx` | Modify | Insert the band in the spine | +| `apps/website/e2e/website.spec.ts` | Modify | Spine order gains `no-runtime-heading` | +| `apps/website/e2e/home-no-runtime.spec.ts` | Create | Measures node alignment at 1440 and 390 | +| `apps/website/src/components/landing/HomeFAQ.tsx` | Modify | Fifth question | +| `apps/website/src/components/landing/HomeFAQ.spec.tsx` | Modify | Counts five | +| `docs/superpowers/specs/2026-09-08-preflight-checklist-design.md` | Modify | One-line "superseded" note at the top | + +--- + +## Task 1: Shrink the checklist data to the five Airworthiness rows + +**Files:** +- Modify: `apps/website/src/lib/preflight-checklist.ts` +- Test: `apps/website/src/lib/preflight-checklist.spec.ts` + +- [ ] **Step 1: Rewrite the spec to describe the five-row list** + +Replace the whole file `apps/website/src/lib/preflight-checklist.spec.ts` with: + +```ts +import { describe, it, expect, vi } from 'vitest'; +import { AIRWORTHINESS } from './preflight-checklist'; + +describe('airworthiness rows', () => { + it('lists exactly the five third-party figures', () => { + // A length pin, on purpose: the list is only as honest as its sources, + // and a row added without one is the failure mode this guard exists for. + expect(AIRWORTHINESS.map((r) => r.challenge)).toEqual([ + 'Framework rank', + 'OpenSSF Scorecard', + 'Supply-chain grade', + 'Angular support', + 'Release provenance', + ]); + }); + + it('proves every row it ticks', () => { + // "Not self-reported" is the section's own aside. A ticked row with no + // link is a claim with no source. + for (const row of AIRWORTHINESS) { + expect(row.href, row.challenge).toBeTruthy(); + } + }); + + it('links pages a human can read, never a raw API', () => { + for (const row of AIRWORTHINESS) { + const { hostname, pathname } = new URL(row.href, 'https://threadplane.ai'); + expect(hostname.startsWith('api.'), row.href).toBe(false); + expect(pathname.startsWith('/api/'), row.href).toBe(false); + } + }); + + it('keeps the HVTrust grade live rather than hardcoding it', () => { + // It has flipped grade several times in a month against an A-band floor + // of 80; a hardcoded number would be wrong on some days. + const grade = AIRWORTHINESS.find((r) => r.challenge === 'Supply-chain grade'); + expect(grade?.badgeSrc).toBe('https://hvtracker.net/badge/threadplane.svg'); + expect(grade?.response).toBe(''); + }); + + it('carries no self-reported rows', () => { + // Cloud, Signup and VC board were ours to say; the masthead and the FAQ + // say them. Only figures a third party published belong here. + for (const row of AIRWORTHINESS) { + expect(row.response, row.challenge).not.toBe('NONE'); + } + }); + + it('derives the Angular range rather than hardcoding it', async () => { + const { WEBSITE_SUPPORTED_ANGULAR_MAJORS } = await import( + '../components/pricing/angular-support.mjs' + ); + const row = AIRWORTHINESS.find((r) => r.challenge === 'Angular support'); + expect(row?.response).toContain(String(WEBSITE_SUPPORTED_ANGULAR_MAJORS[0])); + expect(row?.response).toContain(String(WEBSITE_SUPPORTED_ANGULAR_MAJORS.at(-1))); + + // The assertions above cannot tell a derived "20-22" from a typed one — + // they agree until someone bumps a major, and by then the homepage has + // been wrong for a release. So move the dependency and check the value + // follows it. A hardcoded string will not. + vi.resetModules(); + vi.doMock('../components/pricing/angular-support.mjs', () => ({ + WEBSITE_SUPPORTED_ANGULAR_MAJORS: Object.freeze([41, 42, 43]), + })); + const { AIRWORTHINESS: moved } = await import('./preflight-checklist'); + vi.doUnmock('../components/pricing/angular-support.mjs'); + vi.resetModules(); + + expect(moved.find((r) => r.challenge === 'Angular support')?.response).toBe('41–43'); + }); +}); +``` + +- [ ] **Step 2: Run it and watch it fail** + +Run: `npx nx test website -- src/lib/preflight-checklist.spec.ts` +Expected: FAIL. "lists exactly the five third-party figures" fails because the current array has eight entries, and "carries no self-reported rows" fails on the `NONE` rows. + +- [ ] **Step 3: Rewrite the data module** + +Replace the whole file `apps/website/src/lib/preflight-checklist.ts` with: + +```ts +import { WEBSITE_SUPPORTED_ANGULAR_MAJORS } from '../components/pricing/angular-support.mjs'; + +/** + * The homepage airworthiness list: the figures a third party published about + * this project, each linking to the body that published it. + * + * This used to be the tail of a 27-row preflight checklist with a Threadplane + * column and an unticked Yours column (spec 2026-09-08). Both columns came + * out on 2026-09-11: the section's job is trust, and the only half of it that + * was not self-reported is this one. The three self-reported rows that lived + * here (Cloud, Signup, VC board) went with them — the masthead above already + * says "no account, no cloud" and the FAQ says the rest. + * + * Before adding a row: find the page that proves it. If there is no page, it + * is not a row. + */ +export interface ChecklistRow { + /** Left side of the line. 1–3 words. */ + readonly challenge: string; + /** Right side. The figure itself, never a feature name. */ + readonly response: string; + /** The page that proves it. */ + readonly href: string; + /** Small trailing unit. */ + readonly unit?: string; + /** Live badge rendered instead of `response` text. */ + readonly badgeSrc?: string; +} + +/** + * Verified 2026-09-04 against live sources. The rank and score drift — re-verify + * on touch, and never "round up". + */ +export const AIRWORTHINESS: readonly ChecklistRow[] = [ + { challenge: 'Framework rank', response: '#8', unit: 'OF 119', href: 'https://hvtracker.net/categories/agent-frameworks/' }, + { challenge: 'OpenSSF Scorecard', response: '8.2', unit: '/ 10', href: 'https://scorecard.dev/viewer/?uri=github.com/cacheplane/angular-agent-framework' }, + { + challenge: 'Supply-chain grade', + // Deliberately empty: the badge is the response. It sits near an A-band + // floor of 80 and has flipped grade several times in a month, so a + // hardcoded number would be wrong on some days. + response: '', + badgeSrc: 'https://hvtracker.net/badge/threadplane.svg', + href: 'https://hvtracker.net/agents/threadplane/', + }, + { + challenge: 'Angular support', + // Derived, not typed: bumping a supported major must update the homepage + // without anyone remembering to edit this file. + response: `${WEBSITE_SUPPORTED_ANGULAR_MAJORS[0]}–${WEBSITE_SUPPORTED_ANGULAR_MAJORS.at(-1)}`, + unit: 'CI-TESTED', + href: 'https://www.npmjs.com/package/@threadplane/langgraph', + }, + { challenge: 'Release provenance', response: 'SIGNED', unit: 'OIDC · SLSA', href: 'https://www.npmjs.com/package/@threadplane/chat' }, +]; +``` + +Note `href` is now `string`, not `string | null`: nothing unlinked is left. + +- [ ] **Step 4: Run the data spec and see it pass** + +Run: `npx nx test website -- src/lib/preflight-checklist.spec.ts` +Expected: PASS, 6 tests. (Other specs that import `PREFLIGHT_OURS` will fail until Task 2; that is expected and is why this task does not run the full suite.) + +- [ ] **Step 5: Re-verify the two drifting figures against the live pages** + +Open `https://hvtracker.net/categories/agent-frameworks/` and `https://scorecard.dev/viewer/?uri=github.com/cacheplane/angular-agent-framework` in a browser. If the rank or score differs from `#8 OF 119` / `8.2`, change the row to the live value and note it in the commit message. Never round up. + +- [ ] **Step 6: Commit** + +```bash +git add apps/website/src/lib/preflight-checklist.ts apps/website/src/lib/preflight-checklist.spec.ts +git commit -m "refactor(website): the checklist data keeps only the five airworthiness rows + +Co-Authored-By: Claude Fable 5.1 " +``` + +--- + +## Task 2: Render one labelled list of five ticking rows + +**Files:** +- Modify: `apps/website/src/components/landing/PreflightChecklist.tsx` +- Test: `apps/website/src/components/landing/PreflightChecklist.spec.tsx` + +- [ ] **Step 1: Rewrite the component spec** + +Replace the whole file `apps/website/src/components/landing/PreflightChecklist.spec.tsx` with: + +```tsx +import { render } from '@testing-library/react'; +import { describe, it, expect } from 'vitest'; +import { PreflightChecklist } from './PreflightChecklist'; +import { AIRWORTHINESS } from '../../lib/preflight-checklist'; + +describe('PreflightChecklist', () => { + it('renders one row per data row and nothing else', () => { + const { container } = render(); + expect(container.querySelectorAll('.preflight-row')).toHaveLength(AIRWORTHINESS.length); + // The two-column checklist is gone; nothing may bring its grid back. + expect(container.querySelector('.preflight-cols')).toBeNull(); + expect(container.querySelector('.preflight-yours')).toBeNull(); + }); + + it('links every row', () => { + const { container } = render(); + expect(container.querySelectorAll('a.preflight-row')).toHaveLength(AIRWORTHINESS.length); + for (const a of Array.from(container.querySelectorAll('a.preflight-row'))) { + expect(a.getAttribute('href')).toBeTruthy(); + } + }); + + it('draws the boxes rather than using form controls', () => { + // Nothing here is interactive. An would tell a + // screen reader it can be toggled, which is a lie. + const { container } = render(); + expect(container.querySelectorAll('input')).toHaveLength(0); + expect(container.querySelectorAll('.preflight-box')).toHaveLength(AIRWORTHINESS.length); + }); + + it('names the list', () => { + const { container } = render(); + const lists = Array.from(container.querySelectorAll('ul[aria-labelledby]')); + expect(lists).toHaveLength(1); + const label = container.querySelector(`#${lists[0].getAttribute('aria-labelledby')}`); + expect(label?.textContent).toBe('Airworthiness'); + }); + + it('keeps the HVTrust grade a live badge with real alt text', () => { + const { container } = render(); + const badge = container.querySelector('img.preflight-badge'); + expect(badge?.getAttribute('src')).toBe('https://hvtracker.net/badge/threadplane.svg'); + // Not decorative: it carries the grade, so it needs a real description. + expect(badge?.getAttribute('alt')).toBeTruthy(); + expect(badge?.getAttribute('alt')).not.toBe(''); + }); + + it('opens off-site sources safely in a new tab', () => { + const { container } = render(); + for (const a of Array.from(container.querySelectorAll('a.preflight-row'))) { + const href = a.getAttribute('href')!; + if (!href.startsWith('http')) continue; + expect(a.getAttribute('target')).toBe('_blank'); + expect(a.getAttribute('rel')).toBe('noopener noreferrer'); + } + }); +}); +``` + +- [ ] **Step 2: Run it and watch it fail** + +Run: `npx nx test website -- src/components/landing/PreflightChecklist.spec.tsx` +Expected: FAIL at import time — the component still imports `PREFLIGHT_OURS` and `PREFLIGHT_YOURS`, which no longer exist. + +- [ ] **Step 3: Rewrite the component** + +Replace the whole file `apps/website/src/components/landing/PreflightChecklist.tsx` with: + +```tsx +'use client'; + +import { useEffect, useRef, useState } from 'react'; +import { AIRWORTHINESS, type ChecklistRow } from '../../lib/preflight-checklist'; + +/** Milliseconds between ticks. Fast enough not to read as a loading bar. */ +const TICK_MS = 180; + +function Box() { + return ( + + ); +} + +function Row({ row, done }: { row: ChecklistRow; done: boolean }) { + const external = row.href.startsWith('http'); + return ( +
  • + + + {row.challenge} + +
  • + ); +} + +/** + * The trust band's list: five third-party figures, each linking to the body + * that published it. The boxes tick in sequence on scroll-into-view. + * + * The tick sequence is decorative: `is-done` also changes the response colour, + * so a reader who never sees the animation still sees the state. Under + * reduced motion every row is done from the first paint. + * + * This is a client component only because of the IntersectionObserver; + * Reliability.tsx, which frames it, stays a server component. + */ +export function PreflightChecklist() { + const ref = useRef(null); + const [ticked, setTicked] = useState(0); + + useEffect(() => { + const el = ref.current; + if (!el || typeof IntersectionObserver === 'undefined') return; + const total = AIRWORTHINESS.length; + if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) { + setTicked(total); + return; + } + const timers: ReturnType[] = []; + const io = new IntersectionObserver( + (entries) => { + if (!entries.some((e) => e.isIntersecting)) return; + io.disconnect(); + for (let i = 1; i <= total; i += 1) { + timers.push(setTimeout(() => setTicked(i), TICK_MS * i)); + } + }, + { threshold: 0.25 }, + ); + io.observe(el); + return () => { + io.disconnect(); + timers.forEach(clearTimeout); + }; + }, []); + + return ( +
    +

    Airworthiness

    +
      + {AIRWORTHINESS.map((row, i) => ( + + ))} +
    +
    + ); +} +``` + +- [ ] **Step 4: Run the component spec and see it pass** + +Run: `npx nx test website -- src/components/landing/PreflightChecklist.spec.tsx` +Expected: PASS, 6 tests. + +- [ ] **Step 5: Delete the column CSS** + +In `apps/website/src/styles/landing.css`, inside the "Preflight checklist" block (it begins with the comment `/* Preflight checklist.`), delete these four rules exactly and nothing else: + +```css +.preflight-cols { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 0 48px; +} +``` + +```css +.preflight-col-head.is-yours { + color: var(--color-text-muted); +} +``` + +```css +/* The Airworthiness heading follows the two columns, so it — not its list — + * is what needs the separation. Putting the margin on the list instead left + * the heading butted flat against the last Threadplane row (measured: 0px) + * while pushing it away from the rows it labels. */ +.preflight-cols + .preflight-col-head { + margin-top: 38px; +} +``` + +```css +.preflight-yours .preflight-response { + font-weight: 400; +} +``` + +And replace the `@media (max-width: 820px)` block at the end of the preflight section: + +```css +@media (max-width: 820px) { + .preflight-cols { + grid-template-columns: 1fr; + gap: 24px; + } +} +``` + +with nothing (delete it; the single column needs no breakpoint). + +Also update the block's header comment. Replace: + +```css +/* Preflight checklist. + * + * Plain flex rows: every row starts at its column's left edge and its first + * child is a fixed 16px box, so the boxes share one edge by construction. Do + * NOT reach for `display: contents` here — it breaks the moment one row is an + *
  • rather than an , and it weakens the link's semantics. */ +``` + +with: + +```css +/* Airworthiness list — components/landing/PreflightChecklist.tsx. + * + * Plain flex rows: every row starts at the list's left edge and its first + * child is a fixed 16px box, so the boxes share one edge by construction. Do + * NOT reach for `display: contents` here — it weakens the link's semantics + * and, in the two-column version this replaced, broke alignment. */ +``` + +And in the `.preflight-box` rule, replace the comment lines: + +```css + /* Deliberately NOT var(--color-border-strong): that resolves to + * rgba(255,255,255,.2) in the dark scope, and the empty Yours boxes are the + * section's whole argument — they cannot be the faintest thing on the band. + * .35 is the value the approved prototype was reviewed at. */ +``` + +with: + +```css + /* Deliberately NOT var(--color-border-strong): that resolves to + * rgba(255,255,255,.2) in the dark scope and an unticked box would be the + * faintest thing on the band before the sequence reaches it. */ +``` + +- [ ] **Step 6: Confirm no other file references the deleted classes or exports** + +Run: `grep -rn "preflight-cols\|preflight-yours\|PREFLIGHT_OURS\|PREFLIGHT_YOURS\|is-yours" apps/website/src apps/website/e2e` +Expected: only the `PreflightChecklist.spec.tsx` assertions from Step 1 (which assert absence). If anything else appears, remove that reference. + +- [ ] **Step 7: Commit** + +```bash +git add apps/website/src/components/landing/PreflightChecklist.tsx apps/website/src/components/landing/PreflightChecklist.spec.tsx apps/website/src/styles/landing.css +git commit -m "feat(website): the trust band renders the five airworthiness rows alone + +Co-Authored-By: Claude Fable 5.1 " +``` + +--- + +## Task 3: Rewrite the trust band's aside and framing comment + +**Files:** +- Modify: `apps/website/src/components/landing/Reliability.tsx` +- Test: `apps/website/src/components/landing/Reliability.spec.tsx` + +- [ ] **Step 1: Add the aside assertion and drop the checklist-shape assertions** + +In `apps/website/src/components/landing/Reliability.spec.tsx`, replace the third test (`'carries the checklist and keeps the framing above it'`) with: + +```tsx + it('frames the list as third-party proof', () => { + const { container } = render(); + expect(screen.getByText('Climb performance')).toBeTruthy(); + expect( + screen.getByText('Not self-reported. Every figure links to the body that published it.'), + ).toBeTruthy(); + expect(container.querySelector('.preflight')).toBeTruthy(); + // The two-column checklist and its predecessors are gone. + expect(container.querySelector('.preflight-cols')).toBeNull(); + expect(container.querySelector('.proof-ladder')).toBeNull(); + expect(container.querySelector('.proof-strip-cells')).toBeNull(); + }); +``` + +- [ ] **Step 2: Run it and watch it fail** + +Run: `npx nx test website -- src/components/landing/Reliability.spec.tsx` +Expected: FAIL on `getByText('Not self-reported. …')` — the aside still reads "Vx clears today's obstacle…". + +- [ ] **Step 3: Update the component** + +In `apps/website/src/components/landing/Reliability.tsx`: + +Replace the `aside` prop value: + +```tsx + aside="Vx clears today’s obstacle; Vy gets you to altitude. Not self-reported — every number links to its source." +``` + +with: + +```tsx + aside="Not self-reported. Every figure links to the body that published it." +``` + +Replace the file's leading doc comment: + +```tsx +/** + * The reliability section (homepage design spec §3, block 2): the sourced + * proof band, now argued as a preflight checklist. + * + * Replaces ProofStrip and LogoRibbon, both deleted with the homepage + * restructure. The figure cards, the prose receipts and the pitch ladder came + * out in favour of the checklist: every row states a challenge, the response + * it gets, and links the page that proves it — and the unticked Yours column + * is the half that makes the section honest. + * +``` + +with: + +```tsx +/** + * The trust section (homepage design spec §3, block 2): the sourced proof + * band, reduced on 2026-09-11 to the five third-party figures. + * + * Replaces ProofStrip and LogoRibbon, both deleted with the homepage + * restructure, and the 27-row preflight checklist that followed them. The + * Threadplane and Yours columns of that checklist were self-reported; the + * airworthiness rows are not, and they are all that stays. The boundary + * argument the Yours column used to make now lives in the No-runtime band + * (NoRuntimeBand.tsx) and in the docs. + * +``` + +- [ ] **Step 4: Run the three trust-band specs and see them pass** + +Run: `npx nx test website -- src/components/landing/Reliability.spec.tsx src/components/landing/PreflightChecklist.spec.tsx src/lib/preflight-checklist.spec.ts` +Expected: PASS, all green. + +- [ ] **Step 5: Commit** + +```bash +git add apps/website/src/components/landing/Reliability.tsx apps/website/src/components/landing/Reliability.spec.tsx +git commit -m "feat(website): the trust band's aside frames third-party proof + +Co-Authored-By: Claude Fable 5.1 " +``` + +--- + +## Task 4: Add the No-runtime copy to positioning.ts and the CTA id + +**Files:** +- Modify: `apps/website/src/lib/positioning.ts` (after `OPEN_SOURCE_STRIP`) +- Modify: `apps/website/src/lib/analytics/events.ts` (the `CtaId` union, "Homepage sections" group) +- Test: `apps/website/src/lib/positioning.spec.ts` + +- [ ] **Step 1: Write the failing copy test** + +In `apps/website/src/lib/positioning.spec.ts`, directly after the `describe('homepage restructure copy (live-stage spec §3)', …)` block, add: + +```ts +describe('NO_RUNTIME_BAND (spec 2026-09-11)', () => { + it('argues in two words over an aviation eyebrow, like Fork us', async () => { + const { NO_RUNTIME_BAND } = await import('./positioning'); + expect(NO_RUNTIME_BAND.eyebrow).toBe('Cleared direct'); + expect(NO_RUNTIME_BAND.headline).toBe('No runtime.'); + // The same budgets as OPEN_SOURCE_STRIP: one line at 116px, one-line eyebrow. + expect(NO_RUNTIME_BAND.headline.length).toBeLessThanOrEqual(12); + expect(NO_RUNTIME_BAND.eyebrow.length).toBeLessThanOrEqual(14); + }); + + it('says no cloud, never no proxy', async () => { + // The docs tell readers to put their agent behind their own proxy, so + // "no proxy" would be false. "No cloud" is true and matches the masthead. + const { NO_RUNTIME_BAND } = await import('./positioning'); + expect(NO_RUNTIME_BAND.body).toMatch(/no cloud/i); + expect(NO_RUNTIME_BAND.body).not.toMatch(/proxy/i); + expect(NO_RUNTIME_BAND.figureCaption).not.toMatch(/proxy/i); + }); + + it('links the page that already states the adapters call your server directly', async () => { + const { NO_RUNTIME_BAND } = await import('./positioning'); + expect(NO_RUNTIME_BAND.link.href).toBe('/docs/choosing-an-adapter'); + expect(NO_RUNTIME_BAND.link.label).toBe('How it is wired'); + }); + + it('draws the usual path with one more hop than ours, both starting at your users', async () => { + const { NO_RUNTIME_BAND } = await import('./positioning'); + expect(NO_RUNTIME_BAND.flows.usual.nodes).toEqual(['Your users', 'Their runtime', 'Your agent']); + expect(NO_RUNTIME_BAND.flows.ours.nodes).toEqual(['Your users', 'Your agent']); + expect(NO_RUNTIME_BAND.flows.usual.ghost).toBe('Their runtime'); + }); +}); +``` + +- [ ] **Step 2: Run it and watch it fail** + +Run: `npx nx test website -- src/lib/positioning.spec.ts` +Expected: FAIL — `NO_RUNTIME_BAND` is undefined. + +- [ ] **Step 3: Add the copy object** + +In `apps/website/src/lib/positioning.ts`, directly after the `OPEN_SOURCE_STRIP` object's closing `} as const;`, add: + +```ts +// ── The No-runtime band (dark, between the architecture diagram and Fork us; +// spec 2026-09-11). Threadplane's adapters call your LangGraph or AG-UI server +// from the browser: there is no server of ours in the request path, no key, +// no dev-only flag, no production tier. Other agent UI kits ship a "runtime" +// that sits between the two; this band states our shape and names nobody. ── +export const NO_RUNTIME_BAND = { + /** + * Controller phraseology for a clearance straight to a fix, skipping the + * intermediate ones. Texture, like "Squawk 1200": the headline carries the + * meaning, so a reader who does not fly loses nothing. + */ + eyebrow: 'Cleared direct', + /** Two words at up to 116px. */ + headline: 'No runtime.', + /** + * "No cloud", not "no proxy": the docs tell you to put your agent behind + * your own proxy, so "no proxy" would be false. + */ + body: + 'Your users reach your LangGraph or AG-UI server from your Angular app. Nothing of ours in between: no cloud, no key, no dev-only flag.', + link: { + label: 'How it is wired', + href: '/docs/choosing-an-adapter', + }, + /** + * The two vertical flows. `ghost` is the node drawn dashed and struck + * through: the hop that is not there with Threadplane. "Your users" echoes + * the first column label of the architecture diagram above the band. + */ + flows: { + usual: { + label: 'The usual', + nodes: ['Your users', 'Their runtime', 'Your agent'], + ghost: 'Their runtime', + }, + ours: { + label: 'Threadplane', + nodes: ['Your users', 'Your agent'], + }, + }, + /** Read to assistive tech in place of the drawn flows. */ + figureCaption: + 'The usual path runs from your users through the vendor’s runtime to your agent. With Threadplane your users reach your agent directly.', +} as const; +``` + +- [ ] **Step 4: Add the CTA id** + +In `apps/website/src/lib/analytics/events.ts`, in the `CtaId` union under the `// Homepage sections` comment, add one member after `| 'home_coding_agent_link'`: + +```ts + | 'home_no_runtime_docs' +``` + +- [ ] **Step 5: Run the positioning spec and see it pass** + +Run: `npx nx test website -- src/lib/positioning.spec.ts` +Expected: PASS, including the four new tests. + +- [ ] **Step 6: Commit** + +```bash +git add apps/website/src/lib/positioning.ts apps/website/src/lib/positioning.spec.ts apps/website/src/lib/analytics/events.ts +git commit -m "feat(website): single-source the No-runtime band copy and register its CTA id + +Co-Authored-By: Claude Fable 5.1 " +``` + +--- + +## Task 5: Build the NoRuntimeBand component + +**Files:** +- Create: `apps/website/src/components/landing/NoRuntimeBand.tsx` +- Create: `apps/website/src/components/landing/NoRuntimeBand.spec.tsx` +- Modify: `apps/website/src/styles/landing.css` (append after the `.open-source-strip` block's `@media (max-width: 640px)` rule) + +- [ ] **Step 1: Write the failing component spec** + +Create `apps/website/src/components/landing/NoRuntimeBand.spec.tsx`: + +```tsx +// @vitest-environment jsdom +import { fireEvent, render, screen } from '@testing-library/react'; +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { NoRuntimeBand } from './NoRuntimeBand'; +import { NO_RUNTIME_BAND } from '../../lib/positioning'; + +const trackCtaClickMock = vi.hoisted(() => vi.fn()); +vi.mock('../../lib/analytics/client', () => ({ + track: vi.fn(), + trackCtaClick: trackCtaClickMock, + trackExternalLinkClick: vi.fn(), +})); + +beforeEach(() => trackCtaClickMock.mockClear()); + +describe('NoRuntimeBand', () => { + it('makes the two-word headline the section heading and names the section by it', () => { + const { container } = render(); + const heading = screen.getByRole('heading', { level: 2 }); + expect(heading.textContent).toBe(NO_RUNTIME_BAND.headline); + expect(heading.id).toBe('no-runtime-heading'); + const section = container.querySelector('[data-ui="section"]'); + expect(section?.getAttribute('aria-labelledby')).toBe('no-runtime-heading'); + expect(section?.getAttribute('id')).toBe('no-runtime'); + expect(container.querySelector('.no-runtime-eyebrow')?.textContent).toBe(NO_RUNTIME_BAND.eyebrow); + expect(screen.getByText(NO_RUNTIME_BAND.body)).toBeTruthy(); + }); + + it('sits on the dark surface at the full rhythm, like Fork us', () => { + const { container } = render(); + const section = container.querySelector('[data-ui="section"]'); + expect(section?.getAttribute('data-surface')).toBe('dark'); + expect(section?.getAttribute('data-tight')).toBeNull(); + expect(section?.classList.contains('no-runtime')).toBe(true); + }); + + it('offers one text link and no button, and reports it as a homepage CTA', () => { + const { container } = render(); + const links = container.querySelectorAll('a'); + expect(links).toHaveLength(1); + const link = screen.getByRole('link', { name: NO_RUNTIME_BAND.link.label }); + expect(link.getAttribute('href')).toBe(NO_RUNTIME_BAND.link.href); + expect(container.querySelector('[data-ui="button"]')).toBeNull(); + fireEvent.click(link); + expect(trackCtaClickMock).toHaveBeenCalledWith({ + cta_id: 'home_no_runtime_docs', + track: 'developer', + surface: 'home', + destination_url: NO_RUNTIME_BAND.link.href, + }); + }); + + it('draws both flows from the copy module, with the ghost hop only in the usual one', () => { + const { container } = render(); + const usual = container.querySelector('[data-flow="usual"]'); + const ours = container.querySelector('[data-flow="ours"]'); + expect( + Array.from(usual!.querySelectorAll('.no-runtime-node')).map((n) => n.textContent), + ).toEqual([...NO_RUNTIME_BAND.flows.usual.nodes]); + expect( + Array.from(ours!.querySelectorAll('.no-runtime-node')).map((n) => n.textContent), + ).toEqual([...NO_RUNTIME_BAND.flows.ours.nodes]); + expect(container.querySelectorAll('.no-runtime-node.is-ghost')).toHaveLength(1); + expect(usual!.querySelector('.no-runtime-node.is-ghost')?.textContent).toBe( + NO_RUNTIME_BAND.flows.usual.ghost, + ); + expect(ours!.querySelector('.is-ghost')).toBeNull(); + }); + + it('gives the flows a prose caption and hides the arrows from assistive tech', () => { + const { container } = render(); + const figure = container.querySelector('figure.no-runtime-figure'); + expect(figure).toBeTruthy(); + expect(figure?.querySelector('figcaption')?.textContent).toBe(NO_RUNTIME_BAND.figureCaption); + // The drawn flows duplicate the caption, so they are hidden as a unit. + expect(figure?.querySelector('.no-runtime-flows')?.getAttribute('aria-hidden')).toBe('true'); + }); + + it('marks the runway decorative and puts it outside the container', () => { + const { container } = render(); + const runway = container.querySelector('.no-runtime-runway'); + expect(runway?.getAttribute('aria-hidden')).toBe('true'); + expect(runway?.closest('[data-ui="container"]')).toBeNull(); + expect(runway?.parentElement?.getAttribute('data-ui')).toBe('section'); + }); +}); +``` + +- [ ] **Step 2: Run it and watch it fail** + +Run: `npx nx test website -- src/components/landing/NoRuntimeBand.spec.tsx` +Expected: FAIL — cannot resolve `./NoRuntimeBand`. + +- [ ] **Step 3: Write the component** + +Create `apps/website/src/components/landing/NoRuntimeBand.tsx`: + +```tsx +'use client'; + +import { Container } from '../ui/Container'; +import { Section } from '../ui/Section'; +import { trackCtaClick } from '../../lib/analytics/client'; +import { NO_RUNTIME_BAND } from '../../lib/positioning'; + +/** + * The No-runtime band (spec 2026-09-11): a dark band between the architecture + * diagram and Fork us, in the same loud register. Copy left, two vertical + * flows right — the usual path with a vendor runtime in the middle, and ours + * without it. + * + * Everything it says comes from NO_RUNTIME_BAND (positioning.ts). It names no + * other product: the band states Threadplane's shape and leaves the + * comparison to the reader. + * + * `'use client'` only for the analytics click handler; there is no state. + */ +export function NoRuntimeBand() { + const { eyebrow, headline, body, link, flows, figureCaption } = NO_RUNTIME_BAND; + + return ( +
    + +
    + + +
    + {/* The drawn flows and the caption say the same thing; the drawing + is hidden as a unit so a screen reader hears it once. */} + +
    {figureCaption}
    +
    +
    + + {/* Spans the section, not the container, like the Fork us runway. */} +
    + ); +} + +function Flow({ + id, + label, + nodes, + ghost, +}: { + id: 'usual' | 'ours'; + label: string; + nodes: readonly string[]; + ghost?: string; +}) { + return ( +
    +

    {label}

    + {nodes.map((node, i) => ( +
    + {i > 0 ? : null} + + {node} + +
    + ))} +
    + ); +} +``` + +- [ ] **Step 4: Run the spec and see it pass** + +Run: `npx nx test website -- src/components/landing/NoRuntimeBand.spec.tsx` +Expected: PASS, 6 tests. + +- [ ] **Step 5: Add the CSS** + +In `apps/website/src/styles/landing.css`, directly after the Fork-us block's closing `@media (max-width: 640px) { … }` rule (the one that sets `.open-source-strip-headline` to `clamp(44px, 13vw, 56px)`), append: + +```css +/* The No-runtime band — components/landing/NoRuntimeBand.tsx. + * Same register as Fork us (eyebrow, display headline, runway) at the full + * section rhythm, but with a text link instead of a button so two adjacent + * dark bands do not read as one. Copy left, two vertical flows right. */ +.no-runtime { + position: relative; +} +.no-runtime-grid { + display: grid; + grid-template-columns: minmax(0, 1.05fr) minmax(0, 1fr); + gap: 56px; + align-items: center; +} +.no-runtime-eyebrow { + font-family: var(--font-mono); + font-size: 11px; + font-weight: 700; + letter-spacing: 0.18em; + text-transform: uppercase; + line-height: 1.4; + color: var(--color-signal); + margin: 0 0 22px; +} +.no-runtime-headline { + font-family: var(--font-display); + font-weight: 400; + font-size: clamp(56px, 8.5vw, 116px); + line-height: 0.9; + letter-spacing: -0.04em; + color: var(--color-text-primary); + margin: 0; +} +.no-runtime-body { + font-family: var(--font-sans); + font-size: 17px; + line-height: 1.55; + color: var(--color-text-secondary); + max-width: 480px; + margin: 26px 0 0; +} +.no-runtime-link { + display: inline-block; + margin-top: 24px; + font-family: var(--font-sans); + font-size: 14px; + font-weight: 600; + color: var(--color-signal); + text-decoration: none; +} +.no-runtime-link:hover { + text-decoration: underline; +} + +/* The flows. Each is a column of bordered mono nodes joined by 1.5px arrows; + * the ours arrow is yellow and taller because it spans the hop that is not + * there. Nodes share a min-width so the two columns line up. */ +.no-runtime-figure { + margin: 0; +} +.no-runtime-flows { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 32px; +} +.no-runtime-flow { + display: flex; + flex-direction: column; + align-items: center; +} +.no-runtime-flow-label { + font-family: var(--font-mono); + font-size: 10px; + font-weight: 700; + letter-spacing: 0.16em; + text-transform: uppercase; + color: var(--color-text-muted); + margin: 0 0 14px; +} +.no-runtime-hop { + display: flex; + flex-direction: column; + align-items: center; +} +.no-runtime-node { + display: block; + box-sizing: border-box; + min-width: 150px; + padding: 13px 18px; + border: 1.5px solid var(--color-text-primary); + border-radius: 4px; + font-family: var(--font-mono); + font-size: 12px; + letter-spacing: 0.08em; + text-transform: uppercase; + text-align: center; + white-space: nowrap; + color: var(--color-text-primary); +} +.no-runtime-node.is-agent { + border-color: var(--color-signal); + color: var(--color-signal); +} +/* The hop that is not there with Threadplane. Dashed, muted and struck + * through — three signals, so none of them has to carry it alone. */ +.no-runtime-node.is-ghost { + border-style: dashed; + border-color: var(--color-text-muted); + color: var(--color-text-muted); + text-decoration: line-through; +} +.no-runtime-arrow { + position: relative; + width: 1.5px; + height: 36px; + background: var(--color-text-muted); +} +.no-runtime-arrow::after { + content: ''; + position: absolute; + bottom: -1px; + left: -4px; + border: 5px solid transparent; + border-top-color: var(--color-text-muted); +} +.no-runtime-arrow.is-direct { + height: 74px; + background: var(--color-signal); +} +.no-runtime-arrow.is-direct::after { + border-top-color: var(--color-signal); +} +/* Visually hidden, read by assistive tech in place of the drawing. Same + * recipe as .stage-skip. */ +.no-runtime-caption { + position: absolute; + width: 1px; + height: 1px; + overflow: hidden; + clip: rect(0 0 0 0); + clip-path: inset(50%); + white-space: nowrap; +} +/* The runway centreline, as on Fork us. */ +.no-runtime-runway { + pointer-events: none; + position: absolute; + left: 0; + right: 0; + bottom: 30px; + height: 6px; + background: repeating-linear-gradient( + 90deg, + rgba(255, 175, 0, 0.5) 0 46px, + transparent 46px 92px + ); +} +@media (max-width: 820px) { + .no-runtime-grid { + grid-template-columns: 1fr; + gap: 44px; + } +} +@media (max-width: 640px) { + .no-runtime-headline { + font-size: clamp(44px, 13vw, 56px); + } +} +@media (max-width: 480px) { + .no-runtime-flows { + grid-template-columns: 1fr; + gap: 36px; + } +} +``` + +- [ ] **Step 6: Lint the two new files** + +Run: `npx nx lint website 2>&1 | sed 's/\x1b\[[0-9;]*m//g' | grep -A3 "NoRuntimeBand" || echo "no NoRuntimeBand lint output"` +Expected: `no NoRuntimeBand lint output` (warnings elsewhere are pre-existing; only errors matter). + +- [ ] **Step 7: Commit** + +```bash +git add apps/website/src/components/landing/NoRuntimeBand.tsx apps/website/src/components/landing/NoRuntimeBand.spec.tsx apps/website/src/styles/landing.css +git commit -m "feat(website): the No-runtime band — two words over two flows + +Co-Authored-By: Claude Fable 5.1 " +``` + +--- + +## Task 6: Insert the band into the spine and pin the order + +**Files:** +- Modify: `apps/website/src/app/page.tsx` +- Modify: `apps/website/e2e/website.spec.ts` (the `ids` array in the spine test) + +- [ ] **Step 1: Add the id to the spine e2e** + +In `apps/website/e2e/website.spec.ts`, in the test `'landing page renders the spine in order (live-stage spec §3)'`, change the `ids` array to: + +```ts + const ids = [ + 'hero-heading', + 'proof-heading', + 'compatibility-heading', + 'architecture-heading', + 'no-runtime-heading', + 'open-source-heading', + 'stage-heading', + 'field-report-heading', + 'faq-heading', + ]; +``` + +- [ ] **Step 2: Insert the band in the page** + +In `apps/website/src/app/page.tsx`: + +Add the import after the `OpenSourceStrip` import: + +```tsx +import { NoRuntimeBand } from '../components/landing/NoRuntimeBand'; +``` + +Replace: + +```tsx + + + {/* The open-source full stop: a loud dark band with one fork CTA. Copy + lives in OPEN_SOURCE_STRIP (positioning.ts). */} + +``` + +with: + +```tsx + + + {/* No runtime: the diagram above shows Threadplane reaching your agent + directly; this band says so in two words. Copy lives in + NO_RUNTIME_BAND (positioning.ts). Deliberately dark-on-dark with the + band below; each dark section paints its own seam (spec §5). */} + + + {/* The open-source full stop: a loud dark band with one fork CTA. Copy + lives in OPEN_SOURCE_STRIP (positioning.ts). */} + +``` + +- [ ] **Step 3: Run the whole website unit suite** + +Run: `npx nx test website` +Expected: PASS, 0 failures. If `public-copy.spec.ts` fails, one of the new strings matched a barred pattern in `lib/public-copy-contract.ts`; reword the string in `positioning.ts` (the barred phrases are listed in `BANNED_CLAIMS` and `NARRATIVE_MENTIONS`), never the contract. + +- [ ] **Step 4: Build, because lint and test do not typecheck** + +Run: `rm -rf apps/website/.next && npx nx build website` +Expected: exits 0. A `CtaId` typo would surface only here. + +- [ ] **Step 5: Run the spine e2e** + +Run: `npx nx e2e website -- --grep "spine in order"` +Expected: 1 passed. If it times out on `page.goto`, a stale `apps/website/.next/dev` directory is the usual cause; `rm -rf apps/website/.next` and rerun. + +- [ ] **Step 6: Commit** + +```bash +git add apps/website/src/app/page.tsx apps/website/e2e/website.spec.ts +git commit -m "feat(website): the No-runtime band joins the homepage spine after the architecture diagram + +Co-Authored-By: Claude Fable 5.1 " +``` + +--- + +## Task 7: Measure the flows at 1440 and 390 + +**Files:** +- Create: `apps/website/e2e/home-no-runtime.spec.ts` + +- [ ] **Step 1: Write the measuring spec** + +Create `apps/website/e2e/home-no-runtime.spec.ts`: + +```ts +import { test, expect, type Page } from '@playwright/test'; + +const BAND = '#no-runtime'; + +/** + * The unit spec proves the flows are drawn from the copy module; this proves + * the drawing holds together in a real layout. Measured, not eyeballed: every + * node in a flow shares that flow's centre line, no node's text overflows its + * box, and on a phone the flows stack instead of squeezing. + */ +async function nodeReport(page: Page) { + return page.evaluate((sel) => { + const flows = Array.from(document.querySelectorAll(`${sel} [data-flow]`)); + return flows.map((flow) => { + const nodes = Array.from(flow.querySelectorAll('.no-runtime-node')); + return { + id: flow.dataset['flow'], + top: flow.getBoundingClientRect().top, + centres: nodes.map((n) => { + const r = n.getBoundingClientRect(); + return Math.round((r.left + r.right) / 2); + }), + overflowing: nodes.filter((n) => n.scrollWidth > n.clientWidth).map((n) => n.textContent), + }; + }); + }, BAND); +} + +test.describe('homepage No-runtime band', () => { + test('aligns every node on its flow centre line and clips no text at 1440', async ({ page }) => { + await page.setViewportSize({ width: 1440, height: 900 }); + await page.goto('/'); + await expect(page.locator('#no-runtime-heading')).toHaveText('No runtime.'); + await page.locator(BAND).scrollIntoViewIfNeeded(); + const flows = await nodeReport(page); + expect(flows.map((f) => f.id)).toEqual(['usual', 'ours']); + for (const f of flows) { + expect(f.overflowing, f.id).toEqual([]); + expect(new Set(f.centres).size, `${f.id} centres ${f.centres.join(',')}`).toBe(1); + } + // Side by side: the two flows share a top edge. + expect(Math.abs(flows[0].top - flows[1].top)).toBeLessThanOrEqual(1); + // The ghost hop exists exactly once and only in the usual flow. + await expect(page.locator(`${BAND} .no-runtime-node.is-ghost`)).toHaveCount(1); + await expect(page.locator(`${BAND} [data-flow="ours"] .is-ghost`)).toHaveCount(0); + }); + + test('stacks the flows and still fits every node on a phone', async ({ page }) => { + await page.setViewportSize({ width: 390, height: 844 }); + await page.goto('/'); + await page.locator(BAND).scrollIntoViewIfNeeded(); + const flows = await nodeReport(page); + for (const f of flows) { + expect(f.overflowing, f.id).toEqual([]); + expect(new Set(f.centres).size).toBe(1); + } + // Stacked: the ours flow starts below the usual one. + expect(flows[1].top).toBeGreaterThan(flows[0].top + 100); + // The page never scrolls sideways. + const overflow = await page.evaluate( + () => document.documentElement.scrollWidth - document.documentElement.clientWidth, + ); + expect(overflow).toBeLessThanOrEqual(0); + }); + + test('carries one link, to the adapter guide, and no button', async ({ page }) => { + await page.goto('/'); + const links = page.locator(`${BAND} a`); + await expect(links).toHaveCount(1); + await expect(links).toHaveAttribute('href', '/docs/choosing-an-adapter'); + await expect(page.locator(`${BAND} [data-ui="button"]`)).toHaveCount(0); + }); +}); +``` + +- [ ] **Step 2: Run it** + +Run: `npx nx e2e website -- home-no-runtime.spec.ts` +Expected: 3 passed. If the 390 test reports an overflowing node, the node text is wrapping or clipping: raise the `@media (max-width: 480px)` breakpoint in the CSS from Task 5 rather than shrinking the font below 12px. + +- [ ] **Step 3: Commit** + +```bash +git add apps/website/e2e/home-no-runtime.spec.ts +git commit -m "test(website): measure the No-runtime flows at desktop and phone widths + +Co-Authored-By: Claude Fable 5.1 " +``` + +--- + +## Task 8: The FAQ entry + +**Files:** +- Modify: `apps/website/src/components/landing/HomeFAQ.tsx` +- Test: `apps/website/src/components/landing/HomeFAQ.spec.tsx` + +- [ ] **Step 1: Extend the FAQ spec** + +Replace the body of the single test in `apps/website/src/components/landing/HomeFAQ.spec.tsx` so the file reads: + +```tsx +// @vitest-environment jsdom +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import { describe, it, expect } from 'vitest'; +import { HomeFAQ } from './HomeFAQ'; + +describe('HomeFAQ', () => { + it('asks only what the page above did not answer', () => { + const { container } = render(); + const questions = [ + 'Is Threadplane a backend agent framework?', + 'Can I use my existing Angular component library and design system?', + 'Does generated UI execute arbitrary code?', + 'Does Threadplane require a hosted service or an account?', + 'Does Threadplane have a runtime I need to deploy?', + ]; + for (const q of questions) expect(screen.getByText(q)).toBeTruthy(); + expect(screen.queryByText('Does Threadplane require LangGraph?')).toBeNull(); + expect(screen.queryByText(/raw streaming SDK/)).toBeNull(); + expect(container.querySelectorAll('summary')).toHaveLength(5); + expect(container.querySelectorAll('a')).toHaveLength(5); + }); + + it('answers the runtime question in the band’s own words', () => { + render(); + const answer = screen.getByText(/no Threadplane server in the request path/); + expect(answer.textContent).toMatch(/no key/); + expect(answer.textContent).toMatch(/no production tier/); + expect(answer.textContent).not.toMatch(/proxy/i); + expect(answer.querySelector('a')?.getAttribute('href')).toBe('/docs/choosing-an-adapter'); + }); +}); +``` + +- [ ] **Step 2: Run it and watch it fail** + +Run: `npx nx test website -- src/components/landing/HomeFAQ.spec.tsx` +Expected: FAIL — the fifth question is not rendered. + +- [ ] **Step 3: Add the item** + +In `apps/website/src/components/landing/HomeFAQ.tsx`, after the item whose `q` is `'Does Threadplane require a hosted service or an account?'` and before the closing `];`, add: + +```tsx + { + q: 'Does Threadplane have a runtime I need to deploy?', + a: ( + <> + No. The adapters call your LangGraph or AG-UI server from the browser. There is no + Threadplane server in the request path, no key, and no production tier.{' '} + How it is wired + + ), + }, +``` + +Also change the leading comment `// Four questions the page above does not answer (live-stage spec §3).` to `// Five questions the page above does not answer (live-stage spec §3; the runtime one from spec 2026-09-11).` + +- [ ] **Step 4: Run it and see it pass** + +Run: `npx nx test website -- src/components/landing/HomeFAQ.spec.tsx` +Expected: PASS, 2 tests. + +- [ ] **Step 5: Commit** + +```bash +git add apps/website/src/components/landing/HomeFAQ.tsx apps/website/src/components/landing/HomeFAQ.spec.tsx +git commit -m "feat(website): the homepage FAQ answers whether there is a runtime to deploy + +Co-Authored-By: Claude Fable 5.1 " +``` + +--- + +## Task 9: Mark the old spec superseded and verify the whole change + +**Files:** +- Modify: `docs/superpowers/specs/2026-09-08-preflight-checklist-design.md` (header only) + +- [ ] **Step 1: Add the supersession note** + +In `docs/superpowers/specs/2026-09-08-preflight-checklist-design.md`, directly after the line `**Supersedes:** the pitch-ladder device in …`, add: + +```markdown +**Superseded on 2026-09-11** by `2026-09-11-homepage-trust-band-and-no-runtime-design.md`: the Threadplane and Yours columns and the three self-reported Airworthiness rows were removed; the five third-party rows remain. +``` + +- [ ] **Step 2: Full unit suite, lint, build** + +Run: + +```bash +npx nx test website && npx nx lint website 2>&1 | sed 's/\x1b\[[0-9;]*m//g' | grep -E "^\s*[0-9]+:[0-9]+\s+error" ; rm -rf apps/website/.next && npx nx build website +``` + +Expected: tests pass with 0 failures; the grep prints no `error` lines (warnings are pre-existing); build exits 0. + +- [ ] **Step 3: Full website e2e** + +Run: `npx nx e2e website` +Expected: all passed. Before running, make sure no stray `next dev` or example serve is holding port 4308 (`lsof -i :4308`); a stale server serves the old bundle and the spine test will fail on the missing `no-runtime-heading`. + +- [ ] **Step 4: Look at it in a browser, and measure the two things the spec calls out** + +Start the site with the Browser pane (`preview_start` on the website dev server; do not use Bash for servers) and open `/`. Then: + +- Scroll to `#proof`. Confirm five rows, one column, and that the boxes tick in sequence. Run in the console and confirm one distinct value: + `new Set([...document.querySelectorAll('#proof .preflight-box')].map(b => Math.round(b.getBoundingClientRect().left))).size` +- Scroll to `#no-runtime`. Confirm the band reads as its own band above Fork us, not as one tall dark block. If it does not, apply the spec §5 fallback: delete the `.no-runtime-runway` rule and the `
    ` element plus its spec assertion, and note it in the commit. +- Resize to 390px wide and confirm the copy sits above the flows and the flows stack. +- Take a screenshot of each band for the PR description. + +- [ ] **Step 5: Confirm every href in both bands resolves** + +Run from the worktree root, with the dev server still up on 4308: + +```bash +for u in https://hvtracker.net/categories/agent-frameworks/ "https://scorecard.dev/viewer/?uri=github.com/cacheplane/angular-agent-framework" https://hvtracker.net/agents/threadplane/ https://www.npmjs.com/package/@threadplane/langgraph https://www.npmjs.com/package/@threadplane/chat http://127.0.0.1:4308/docs/choosing-an-adapter; do printf '%s ' "$u"; curl -s -o /dev/null -w '%{http_code}\n' -L "$u"; done +``` + +Expected: every line ends in `200`. + +- [ ] **Step 6: Commit** + +```bash +git add docs/superpowers/specs/2026-09-08-preflight-checklist-design.md +git commit -m "docs(specs): mark the preflight checklist spec superseded + +Co-Authored-By: Claude Fable 5.1 " +``` + +--- + +## Task 10: Open the pull request + +- [ ] **Step 1: Push and open the PR** + +```bash +git push -u origin blove/homepage-trust-no-runtime +gh pr create --title "feat(website): the trust band keeps only third-party proof, and a No-runtime band lands after the architecture diagram" --body "$(cat <<'EOF' +## What + +- The homepage's dark proof band drops the 27-row preflight checklist to the five third-party Airworthiness rows (framework rank, OpenSSF Scorecard, live supply-chain badge, Angular range, signed provenance). About 1,100px becomes about 450px on desktop. +- A new **No runtime.** band sits between the architecture diagram and Fork us: two words in the Fork-us register, one sentence, one link, and two vertical flows — the usual path with a vendor runtime in the middle, and ours without it. +- One FAQ entry: does Threadplane have a runtime to deploy? No. + +Spec: `docs/superpowers/specs/2026-09-11-homepage-trust-band-and-no-runtime-design.md`. + +## Why + +The checklist had become a feature list, and the only part of the band that was not self-reported was the scores. And the site never said the one thing the architecture diagram shows: nothing of ours sits between the browser and your agent. No cloud, no key, no dev-only flag. + +## Guards + +- Unit: the five rows, every row linked, the band copy pinned (including "no cloud, never no proxy"), the flows drawn from the copy module, one link and no button. +- e2e: spine order gains `no-runtime-heading`; a new spec measures node alignment at 1440 and stacking at 390. + +## Screenshots + +(attach the two band screenshots from Task 9) + +🤖 Generated with [Claude Code](https://claude.com/claude-code) +EOF +)" +``` + +- [ ] **Step 2: Watch CI, then verify on the preview** + +The required check is `CI — required`. When the Vercel preview is up, open it and repeat the Task 9 Step 4 checks against the preview URL, because the website's e2e is known to pass against `next dev` and fail against a production build in a few focus-related cases. + +--- + +## Self-review against the spec + +- §2.1 removals → Tasks 1, 2 (data, renderer, CSS, guards). ✔ +- §2.2 five rows, derived range, live badge, list label, new aside → Tasks 1, 2, 3. ✔ +- §2.3 guards → Tasks 1, 2, 3. ✔ +- §3.1 layout, eyebrow, headline, body, link with CTA id, flows, runway, breakpoints → Tasks 4, 5. ✔ +- §3.2 figure + hidden caption, arrows hidden → Task 5 (`aria-hidden` on `.no-runtime-flows`, `.no-runtime-caption`). ✔ +- §3.3 copy in positioning.ts with spec pins → Task 4. ✔ +- §3.4 no proxy, no competitor names, copy contract → Task 4 tests + Task 6 Step 3. ✔ +- §3.5 unit spec, spine e2e, measuring e2e → Tasks 5, 6, 7. ✔ +- §4 FAQ → Task 8. ✔ +- §5 dark-on-dark decision and fallback → Task 6 comment, Task 9 Step 4. ✔ +- §7 verification → Task 9. ✔ + +Type consistency: `NO_RUNTIME_BAND.flows.usual.nodes` / `.ghost`, `.flows.ours.nodes`, `.link.href` / `.link.label`, `.figureCaption` are used identically in Tasks 4, 5, and 7. `ChecklistRow.href` is `string` in Task 1 and treated as always present in Task 2. From a19c87e413e907f5f3ef6987b656b4eb3a747c30 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Fri, 11 Sep 2026 21:20:21 -0700 Subject: [PATCH 03/18] refactor(website): the checklist data keeps only the five airworthiness rows Re-verified against live sources on 2026-09-11: framework rank moved from #8 of 119 to #11 of 122, and the OpenSSF Scorecard moved from 8.2 to 8.3. Updated both rather than rounding up. Co-Authored-By: Claude Fable 5.1 --- .../src/lib/preflight-checklist.spec.ts | 53 ++++++------- apps/website/src/lib/preflight-checklist.ts | 78 ++++--------------- 2 files changed, 39 insertions(+), 92 deletions(-) diff --git a/apps/website/src/lib/preflight-checklist.spec.ts b/apps/website/src/lib/preflight-checklist.spec.ts index 6a3249349..15aac11a5 100644 --- a/apps/website/src/lib/preflight-checklist.spec.ts +++ b/apps/website/src/lib/preflight-checklist.spec.ts @@ -1,39 +1,32 @@ import { describe, it, expect, vi } from 'vitest'; -import { - PREFLIGHT_OURS, - PREFLIGHT_YOURS, - AIRWORTHINESS, -} from './preflight-checklist'; +import { AIRWORTHINESS } from './preflight-checklist'; -describe('preflight checklist data', () => { - it('has the shape the section argues for', () => { - // The unticked half is the argument, not an oversight — if Yours ever - // empties out, the section stops making its point. - expect(PREFLIGHT_OURS).toHaveLength(11); - expect(PREFLIGHT_YOURS).toHaveLength(8); - expect(AIRWORTHINESS).toHaveLength(8); +describe('airworthiness rows', () => { + it('lists exactly the five third-party figures', () => { + // A length pin, on purpose: the list is only as honest as its sources, + // and a row added without one is the failure mode this guard exists for. + expect(AIRWORTHINESS.map((r) => r.challenge)).toEqual([ + 'Framework rank', + 'OpenSSF Scorecard', + 'Supply-chain grade', + 'Angular support', + 'Release provenance', + ]); }); - it('proves every claim it ticks', () => { + it('proves every row it ticks', () => { // "Not self-reported" is the section's own aside. A ticked row with no // link is a claim with no source. - for (const row of [...PREFLIGHT_OURS, ...AIRWORTHINESS]) { + for (const row of AIRWORTHINESS) { expect(row.href, row.challenge).toBeTruthy(); } }); - it('claims nothing in the Yours column', () => { - // Nothing proves that YOU set a cost ceiling, so these carry no link. - for (const row of PREFLIGHT_YOURS) { - expect(row.href, row.challenge).toBeNull(); - } - }); - it('links pages a human can read, never a raw API', () => { - for (const row of [...PREFLIGHT_OURS, ...AIRWORTHINESS]) { - const { hostname, pathname } = new URL(row.href!, 'https://threadplane.ai'); - expect(hostname.startsWith('api.'), row.href!).toBe(false); - expect(pathname.startsWith('/api/'), row.href!).toBe(false); + for (const row of AIRWORTHINESS) { + const { hostname, pathname } = new URL(row.href, 'https://threadplane.ai'); + expect(hostname.startsWith('api.'), row.href).toBe(false); + expect(pathname.startsWith('/api/'), row.href).toBe(false); } }); @@ -45,11 +38,11 @@ describe('preflight checklist data', () => { expect(grade?.response).toBe(''); }); - it('gives every response an outcome, not a feature name', () => { - // Responses are states you could verify. Lowercase would mean someone - // wrote a sentence instead of a checklist response. - for (const row of [...PREFLIGHT_OURS, ...PREFLIGHT_YOURS]) { - expect(row.response, row.challenge).toBe(row.response.toUpperCase()); + it('carries no self-reported rows', () => { + // Cloud, Signup and VC board were ours to say; the masthead and the FAQ + // say them. Only figures a third party published belong here. + for (const row of AIRWORTHINESS) { + expect(row.response, row.challenge).not.toBe('NONE'); } }); diff --git a/apps/website/src/lib/preflight-checklist.ts b/apps/website/src/lib/preflight-checklist.ts index dbae5b766..3e55bbf5c 100644 --- a/apps/website/src/lib/preflight-checklist.ts +++ b/apps/website/src/lib/preflight-checklist.ts @@ -1,82 +1,39 @@ import { WEBSITE_SUPPORTED_ANGULAR_MAJORS } from '../components/pricing/angular-support.mjs'; /** - * The homepage preflight checklist. + * The homepage airworthiness list: the figures a third party published about + * this project, each linking to the body that published it. * - * Every row in OURS and AIRWORTHINESS was verified against the docs rather - * than written from memory, and that caught three errors worth remembering: - * - * - "Destructive actions — HELD FOR APPROVAL" was FALSE. `` does not - * render the interrupt panel; the docs say "Interrupt UI is not part of it - * — compose yourself." It is two YOURS rows now. - * - "Errors & retry" had been held back as unverified and is in fact the - * strongest line: zero-config retry across five classified error kinds. - * - Keyboard and screen reader stay OFF. Four components document a11y, - * there is no overview page and no audit, and reduced motion exists only - * in code with nothing to link. A tick would overclaim. + * This used to be the tail of a 27-row preflight checklist with a Threadplane + * column and an unticked Yours column (spec 2026-09-08). Both columns came + * out on 2026-09-11: the section's job is trust, and the only half of it that + * was not self-reported is this one. The three self-reported rows that lived + * here (Cloud, Signup, VC board) went with them — the masthead above already + * says "no account, no cloud" and the FAQ says the rest. * * Before adding a row: find the page that proves it. If there is no page, it - * is not a tick. + * is not a row. */ export interface ChecklistRow { /** Left side of the line. 1–3 words. */ readonly challenge: string; - /** Right side. An outcome you could verify, never a feature name. */ + /** Right side. The figure itself, never a feature name. */ readonly response: string; - /** The page that proves it. `null` only in YOURS. */ - readonly href: string | null; - /** Small trailing unit, AIRWORTHINESS only. */ + /** The page that proves it. */ + readonly href: string; + /** Small trailing unit. */ readonly unit?: string; /** Live badge rendered instead of `response` text. */ readonly badgeSrc?: string; } -export const PREFLIGHT_OURS: readonly ChecklistRow[] = [ - { challenge: 'A run fails', response: 'RETRY, BUILT IN', href: '/docs/chat/guides/error-handling' }, - { challenge: 'Tool calls', response: 'LIVE STATUS CARDS', href: '/docs/chat/components/chat-tool-call-card' }, - { challenge: 'Durable threads', response: 'SURVIVE RESTARTS', href: '/docs/langgraph/guides/persistence' }, - { challenge: 'Reader scrolls up', response: 'STREAM STAYS PUT', href: '/docs/chat/components/chat' }, - { challenge: 'Model output', response: 'SANITIZED, 26 NODES', href: '/docs/chat/guides/markdown' }, - { challenge: 'Shared link', response: 'URL IS THE TRUTH', href: '/docs/chat/guides/thread-routing' }, - { challenge: 'Your design system', response: 'CSS VARS, NO !IMPORTANT', href: '/docs/chat/guides/theming' }, - { challenge: 'Your tests', response: 'RUN WITHOUT A MODEL', href: '/docs/chat/getting-started/try-without-a-backend' }, - { challenge: 'Swapping backend', response: 'ONE IMPORT CHANGES', href: '/docs/choosing-an-adapter' }, - { - challenge: 'Model drift', - response: 'CHECKED WEEKLY, LIVE', - // No doc page for this one. The workflow IS the source: it runs the - // @drift e2e subset against the live provider every Monday and diffs - // fresh recordings against the committed fixtures. - href: 'https://github.com/cacheplane/angular-agent-framework/blob/main/.github/workflows/aimock-drift.yml', - }, - { challenge: 'Debug panel', response: 'TREE-SHAKEN OUT', href: '/docs/chat/components/chat-debug' }, -]; - -/** - * The product's own words, not ours. Every response here is a paraphrase of a - * sentence in the docs — "Never generate a thread ID client-side", "never - * expose a LangSmith API key in client-side code", "you must configure CORS". - * - * These boxes never fill. That is the argument. - */ -export const PREFLIGHT_YOURS: readonly ChecklistRow[] = [ - { challenge: 'Agent endpoint', response: 'BEHIND YOUR PROXY', href: null }, - { challenge: 'API keys', response: 'NEVER IN THE BUNDLE', href: null }, - { challenge: 'Thread IDs', response: 'SERVER-GENERATED', href: null }, - { challenge: 'CORS', response: 'YOURS TO CONFIGURE', href: null }, - { challenge: 'Interrupt panel', response: 'YOU COMPOSE IT', href: null }, - { challenge: 'Resume payload', response: 'YOURS TO CHOOSE', href: null }, - { challenge: 'Runs', response: 'TRACED', href: null }, - { challenge: 'Regressions', response: 'EVALUATED', href: null }, -]; - /** - * Verified 2026-09-04 against live sources. The rank and score drift — re-verify + * Verified 2026-09-11 against live sources. The rank and score drift — re-verify * on touch, and never "round up". */ export const AIRWORTHINESS: readonly ChecklistRow[] = [ - { challenge: 'Framework rank', response: '#8', unit: 'OF 119', href: 'https://hvtracker.net/categories/agent-frameworks/' }, - { challenge: 'OpenSSF Scorecard', response: '8.2', unit: '/ 10', href: 'https://scorecard.dev/viewer/?uri=github.com/cacheplane/angular-agent-framework' }, + { challenge: 'Framework rank', response: '#11', unit: 'OF 122', href: 'https://hvtracker.net/categories/agent-frameworks/' }, + { challenge: 'OpenSSF Scorecard', response: '8.3', unit: '/ 10', href: 'https://scorecard.dev/viewer/?uri=github.com/cacheplane/angular-agent-framework' }, { challenge: 'Supply-chain grade', // Deliberately empty: the badge is the response. It sits near an A-band @@ -95,7 +52,4 @@ export const AIRWORTHINESS: readonly ChecklistRow[] = [ href: 'https://www.npmjs.com/package/@threadplane/langgraph', }, { challenge: 'Release provenance', response: 'SIGNED', unit: 'OIDC · SLSA', href: 'https://www.npmjs.com/package/@threadplane/chat' }, - { challenge: 'Cloud', response: 'NONE', unit: 'SELF-HOSTED', href: '/privacy' }, - { challenge: 'Signup', response: 'NONE', unit: 'npm i', href: '/docs/chat/getting-started/installation' }, - { challenge: 'VC board', response: 'NONE', href: '/about' }, ]; From a786a39c116d6cb4b190f00623c1c334083852ee Mon Sep 17 00:00:00 2001 From: Brian Love Date: Fri, 11 Sep 2026 21:21:39 -0700 Subject: [PATCH 04/18] feat(website): the trust band renders the five airworthiness rows alone Co-Authored-By: Claude Fable 5.1 --- .../landing/PreflightChecklist.spec.tsx | 31 +++---- .../components/landing/PreflightChecklist.tsx | 90 ++++++------------- apps/website/src/styles/landing.css | 38 ++------ 3 files changed, 47 insertions(+), 112 deletions(-) diff --git a/apps/website/src/components/landing/PreflightChecklist.spec.tsx b/apps/website/src/components/landing/PreflightChecklist.spec.tsx index 4418563cc..8626d29a1 100644 --- a/apps/website/src/components/landing/PreflightChecklist.spec.tsx +++ b/apps/website/src/components/landing/PreflightChecklist.spec.tsx @@ -1,21 +1,20 @@ import { render } from '@testing-library/react'; import { describe, it, expect } from 'vitest'; import { PreflightChecklist } from './PreflightChecklist'; -import { PREFLIGHT_OURS, PREFLIGHT_YOURS, AIRWORTHINESS } from '../../lib/preflight-checklist'; +import { AIRWORTHINESS } from '../../lib/preflight-checklist'; describe('PreflightChecklist', () => { - it('renders one row per data row', () => { + it('renders one row per data row and nothing else', () => { const { container } = render(); - expect(container.querySelectorAll('.preflight-row')).toHaveLength( - PREFLIGHT_OURS.length + PREFLIGHT_YOURS.length + AIRWORTHINESS.length, - ); + expect(container.querySelectorAll('.preflight-row')).toHaveLength(AIRWORTHINESS.length); + // The two-column checklist is gone; nothing may bring its grid back. + expect(container.querySelector('.preflight-cols')).toBeNull(); + expect(container.querySelector('.preflight-yours')).toBeNull(); }); - it('links every ticked row and none of the Yours rows', () => { + it('links every row', () => { const { container } = render(); - expect(container.querySelectorAll('a.preflight-row')).toHaveLength( - PREFLIGHT_OURS.length + AIRWORTHINESS.length, - ); + expect(container.querySelectorAll('a.preflight-row')).toHaveLength(AIRWORTHINESS.length); for (const a of Array.from(container.querySelectorAll('a.preflight-row'))) { expect(a.getAttribute('href')).toBeTruthy(); } @@ -26,19 +25,15 @@ describe('PreflightChecklist', () => { // screen reader it can be toggled, which is a lie. const { container } = render(); expect(container.querySelectorAll('input')).toHaveLength(0); - expect(container.querySelectorAll('.preflight-box')).toHaveLength( - PREFLIGHT_OURS.length + PREFLIGHT_YOURS.length + AIRWORTHINESS.length, - ); + expect(container.querySelectorAll('.preflight-box')).toHaveLength(AIRWORTHINESS.length); }); - it('names each column list so two adjacent lists are distinguishable', () => { + it('names the list', () => { const { container } = render(); const lists = Array.from(container.querySelectorAll('ul[aria-labelledby]')); - expect(lists).toHaveLength(3); - for (const ul of lists) { - const label = container.querySelector(`#${ul.getAttribute('aria-labelledby')}`); - expect(label?.textContent).toBeTruthy(); - } + expect(lists).toHaveLength(1); + const label = container.querySelector(`#${lists[0].getAttribute('aria-labelledby')}`); + expect(label?.textContent).toBe('Airworthiness'); }); it('keeps the HVTrust grade a live badge with real alt text', () => { diff --git a/apps/website/src/components/landing/PreflightChecklist.tsx b/apps/website/src/components/landing/PreflightChecklist.tsx index 8833aba71..a5e361669 100644 --- a/apps/website/src/components/landing/PreflightChecklist.tsx +++ b/apps/website/src/components/landing/PreflightChecklist.tsx @@ -1,12 +1,7 @@ 'use client'; import { useEffect, useRef, useState } from 'react'; -import { - AIRWORTHINESS, - PREFLIGHT_OURS, - PREFLIGHT_YOURS, - type ChecklistRow, -} from '../../lib/preflight-checklist'; +import { AIRWORTHINESS, type ChecklistRow } from '../../lib/preflight-checklist'; /** Milliseconds between ticks. Fast enough not to read as a loading bar. */ const TICK_MS = 180; @@ -22,54 +17,48 @@ function Box() { } function Row({ row, done }: { row: ChecklistRow; done: boolean }) { - const body = ( - <> - - {row.challenge} -
  • {body}
  • ; - const external = row.href.startsWith('http'); return (
  • - {body} + + {row.challenge} +
  • ); } /** - * The band's argument in checklist form. The Yours boxes never fill — that is - * deliberate, and it is the half that makes the section honest. + * The trust band's list: five third-party figures, each linking to the body + * that published it. The boxes tick in sequence on scroll-into-view. * * The tick sequence is decorative: `is-done` also changes the response colour, * so a reader who never sees the animation still sees the state. Under * reduced motion every row is done from the first paint. + * + * This is a client component only because of the IntersectionObserver; + * Reliability.tsx, which frames it, stays a server component. */ export function PreflightChecklist() { const ref = useRef(null); @@ -78,7 +67,7 @@ export function PreflightChecklist() { useEffect(() => { const el = ref.current; if (!el || typeof IntersectionObserver === 'undefined') return; - const total = PREFLIGHT_OURS.length + AIRWORTHINESS.length; + const total = AIRWORTHINESS.length; if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) { setTicked(total); return; @@ -101,35 +90,12 @@ export function PreflightChecklist() { }; }, []); - // One counter across both ticked groups, so Airworthiness continues the - // sequence rather than restarting it. - const doneAt = (index: number) => index < ticked; - return (
    -
    -
    -

    Threadplane

    -
      - {PREFLIGHT_OURS.map((row, i) => ( - - ))} -
    -
    -
    -

    Yours

    -
      - {PREFLIGHT_YOURS.map((row) => ( - - ))} -
    -
    -
    -

    Airworthiness

      {AIRWORTHINESS.map((row, i) => ( - + ))}
    diff --git a/apps/website/src/styles/landing.css b/apps/website/src/styles/landing.css index f5329e90d..2a84752f8 100644 --- a/apps/website/src/styles/landing.css +++ b/apps/website/src/styles/landing.css @@ -2173,20 +2173,15 @@ object-fit: contain; } -/* Preflight checklist. +/* Airworthiness list — components/landing/PreflightChecklist.tsx. * - * Plain flex rows: every row starts at its column's left edge and its first + * Plain flex rows: every row starts at the list's left edge and its first * child is a fixed 16px box, so the boxes share one edge by construction. Do - * NOT reach for `display: contents` here — it breaks the moment one row is an - *
  • rather than an , and it weakens the link's semantics. */ + * NOT reach for `display: contents` here — it weakens the link's semantics + * and, in the two-column version this replaced, broke alignment. */ .preflight { margin-top: 26px; } -.preflight-cols { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 0 48px; -} .preflight-col-head { font-family: var(--font-mono); font-size: 10px; @@ -2199,21 +2194,11 @@ .preflight-col-head.is-ours { color: var(--color-signal); } -.preflight-col-head.is-yours { - color: var(--color-text-muted); -} .preflight-rows { list-style: none; margin: 0; padding: 0; } -/* The Airworthiness heading follows the two columns, so it — not its list — - * is what needs the separation. Putting the margin on the list instead left - * the heading butted flat against the last Threadplane row (measured: 0px) - * while pushing it away from the rows it labels. */ -.preflight-cols + .preflight-col-head { - margin-top: 38px; -} .preflight-row { display: flex; align-items: center; @@ -2226,9 +2211,8 @@ height: 16px; box-sizing: border-box; /* Deliberately NOT var(--color-border-strong): that resolves to - * rgba(255,255,255,.2) in the dark scope, and the empty Yours boxes are the - * section's whole argument — they cannot be the faintest thing on the band. - * .35 is the value the approved prototype was reviewed at. */ + * rgba(255,255,255,.2) in the dark scope and an unticked box would be the + * faintest thing on the band before the sequence reaches it. */ border: 1.5px solid rgba(255, 255, 255, 0.35); border-radius: 3px; display: grid; @@ -2278,9 +2262,6 @@ .preflight-row.is-done .preflight-response { color: var(--color-signal); } -.preflight-yours .preflight-response { - font-weight: 400; -} /* Airworthiness responses are figures, so they take the display face and a * larger size; the unit stays mono beside them. */ .preflight-air .preflight-response { @@ -2313,13 +2294,6 @@ a.preflight-row:hover .preflight-challenge { transition: none; } } -@media (max-width: 820px) { - .preflight-cols { - grid-template-columns: 1fr; - gap: 24px; - } -} - /* For teams — components/landing/TeamsBlock.tsx. * Ask left, briefing right. The cover is a fixed 400px so the form column * keeps a comfortable measure rather than stretching across the section. */ From 16318006a39183b5f6a15c0d763ec77125ee76f2 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Fri, 11 Sep 2026 21:22:20 -0700 Subject: [PATCH 05/18] feat(website): the trust band's aside frames third-party proof Co-Authored-By: Claude Fable 5.1 --- .../src/components/landing/Reliability.spec.tsx | 9 ++++++--- .../src/components/landing/Reliability.tsx | 15 ++++++++------- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/apps/website/src/components/landing/Reliability.spec.tsx b/apps/website/src/components/landing/Reliability.spec.tsx index b8392901b..4aa0406ca 100644 --- a/apps/website/src/components/landing/Reliability.spec.tsx +++ b/apps/website/src/components/landing/Reliability.spec.tsx @@ -32,12 +32,15 @@ describe('Reliability', () => { expect(container.textContent).not.toMatch(/trusted by|customers|our clients|powered by/i); }); - it('carries the checklist and keeps the framing above it', () => { + it('frames the list as third-party proof', () => { const { container } = render(); - // The eyebrow and aside still frame the band; the checklist is what - // replaced the figure cards beneath them. expect(screen.getByText('Climb performance')).toBeTruthy(); + expect( + screen.getByText('Not self-reported. Every figure links to the body that published it.'), + ).toBeTruthy(); expect(container.querySelector('.preflight')).toBeTruthy(); + // The two-column checklist and its predecessors are gone. + expect(container.querySelector('.preflight-cols')).toBeNull(); expect(container.querySelector('.proof-ladder')).toBeNull(); expect(container.querySelector('.proof-strip-cells')).toBeNull(); }); diff --git a/apps/website/src/components/landing/Reliability.tsx b/apps/website/src/components/landing/Reliability.tsx index 9a3afe547..6658bd82c 100644 --- a/apps/website/src/components/landing/Reliability.tsx +++ b/apps/website/src/components/landing/Reliability.tsx @@ -5,14 +5,15 @@ import { HERO_TRUST_LINE } from '../../lib/positioning'; import { PreflightChecklist } from './PreflightChecklist'; /** - * The reliability section (homepage design spec §3, block 2): the sourced - * proof band, now argued as a preflight checklist. + * The trust section (homepage design spec §3, block 2): the sourced proof + * band, reduced on 2026-09-11 to the five third-party figures. * * Replaces ProofStrip and LogoRibbon, both deleted with the homepage - * restructure. The figure cards, the prose receipts and the pitch ladder came - * out in favour of the checklist: every row states a challenge, the response - * it gets, and links the page that proves it — and the unticked Yours column - * is the half that makes the section honest. + * restructure, and the 27-row preflight checklist that followed them. The + * Threadplane and Yours columns of that checklist were self-reported; the + * airworthiness rows are not, and they are all that stays. The boundary + * argument the Yours column used to make now lives in the No-runtime band + * (NoRuntimeBand.tsx) and in the docs. * * The works-with line used to close this section. It now has its own LIGHT * section (Compatibility.tsx): the vendor marks are drawn for light grounds @@ -45,7 +46,7 @@ export function Reliability() { eyebrow="Climb performance" heading="Audited, scored, published." headingId="proof-heading" - aside="Vx clears today’s obstacle; Vy gets you to altitude. Not self-reported — every number links to its source." + aside="Not self-reported. Every figure links to the body that published it." /> From a80859ab1b7da8c2e14521c8dec7fd058ee70188 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Fri, 11 Sep 2026 21:28:47 -0700 Subject: [PATCH 06/18] docs(specs): record the live rank and score read on 2026-09-11 Co-Authored-By: Claude Fable 5.1 --- .../2026-09-11-homepage-trust-band-and-no-runtime-design.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/superpowers/specs/2026-09-11-homepage-trust-band-and-no-runtime-design.md b/docs/superpowers/specs/2026-09-11-homepage-trust-band-and-no-runtime-design.md index 60079eca8..faf48e093 100644 --- a/docs/superpowers/specs/2026-09-11-homepage-trust-band-and-no-runtime-design.md +++ b/docs/superpowers/specs/2026-09-11-homepage-trust-band-and-no-runtime-design.md @@ -36,8 +36,8 @@ A single list of five rows, one column, every row linked to its source, in this | Challenge | Response | Source | | --- | --- | --- | -| Framework rank | #8 OF 119 | hvtracker.net categories | -| OpenSSF Scorecard | 8.2 / 10 | scorecard.dev | +| Framework rank | #11 OF 122 (live value on 2026-09-11; drifts) | hvtracker.net categories | +| OpenSSF Scorecard | 8.3 / 10 (live value on 2026-09-11; drifts) | scorecard.dev | | Supply-chain grade | live HVTrust badge | hvtracker.net/agents | | Angular support | 20–22 CI-TESTED (derived) | npmjs.com | | Release provenance | SIGNED · OIDC · SLSA | npmjs.com | From cf5e4f850c90353490bbc63449c40c99403c5ec7 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Fri, 11 Sep 2026 21:30:44 -0700 Subject: [PATCH 07/18] refactor(website): the trust band CSS and guards keep only what the single list uses - landing.css: fold the .preflight-air response rules into the base .preflight-response rules (the base "done -> signal colour" rule was dead since every row is now inside .preflight-air), and fold is-ours into the base .preflight-col-head rule since it is the only heading left. Restore the blank line before the "For teams" block. - PreflightChecklist.tsx: drop the now-unused preflight-air/is-ours classes, check prefers-reduced-motion before the IntersectionObserver guard (guard matchMedia's existence too, since jsdom in this suite does not implement it), and soften the reduced-motion docstring line. - preflight-checklist.spec.ts: replace the "carries no self-reported rows" test with a property that actually distinguishes third-party rows (an absolute href whose host is not threadplane.ai), since the deleted Cloud/Signup/VC rows were self-reported by virtue of linking on-site pages. Co-Authored-By: Claude Fable 5.1 --- .../components/landing/PreflightChecklist.tsx | 15 +++++++------ .../src/lib/preflight-checklist.spec.ts | 9 +++++--- apps/website/src/styles/landing.css | 21 ++++--------------- 3 files changed, 19 insertions(+), 26 deletions(-) diff --git a/apps/website/src/components/landing/PreflightChecklist.tsx b/apps/website/src/components/landing/PreflightChecklist.tsx index a5e361669..aa5df3742 100644 --- a/apps/website/src/components/landing/PreflightChecklist.tsx +++ b/apps/website/src/components/landing/PreflightChecklist.tsx @@ -55,7 +55,7 @@ function Row({ row, done }: { row: ChecklistRow; done: boolean }) { * * The tick sequence is decorative: `is-done` also changes the response colour, * so a reader who never sees the animation still sees the state. Under - * reduced motion every row is done from the first paint. + * reduced motion every row is done as soon as the effect runs. * * This is a client component only because of the IntersectionObserver; * Reliability.tsx, which frames it, stays a server component. @@ -65,13 +65,16 @@ export function PreflightChecklist() { const [ticked, setTicked] = useState(0); useEffect(() => { - const el = ref.current; - if (!el || typeof IntersectionObserver === 'undefined') return; const total = AIRWORTHINESS.length; - if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) { + if ( + typeof window.matchMedia === 'function' && + window.matchMedia('(prefers-reduced-motion: reduce)').matches + ) { setTicked(total); return; } + const el = ref.current; + if (!el || typeof IntersectionObserver === 'undefined') return; const timers: ReturnType[] = []; const io = new IntersectionObserver( (entries) => { @@ -92,8 +95,8 @@ export function PreflightChecklist() { return (
    -

    Airworthiness

    -
  • - {/* Spans the section, not the container, like the Fork us runway. */} -