Skip to content

feat: unify docs and runtime control plane - #963

Merged
blove merged 9 commits into
mainfrom
blove/control-plane-polish-runtime-targets
Sep 2, 2026
Merged

feat: unify docs and runtime control plane#963
blove merged 9 commits into
mainfrom
blove/control-plane-polish-runtime-targets

Conversation

@blove

@blove blove commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Summary

  • make the docs sidebar the single control plane for Docs, Run, Code, API, Activity, Settings, and runtime status
  • retire the standalone Cockpit surface and legacy Examples header link while preserving redirect compatibility
  • add memory-only custom AG-UI and LangSmith targets with exact-origin bridge authorization, redacted failures, and generation-safe remounting
  • generate child/CSP origin policy from one source and promote Website previews only after examples and policy smoke checks

Verification

  • seven core test targets
  • 12 custom-runtime browser flows through real AG-UI, LangGraph, and Threads applications
  • lint and production builds for affected projects
  • all 41 Cockpit Angular application builds
  • generated API docs and public agent context
  • secret-path, assembled-policy, and diff checks

Known external verification

  • immutable preview policy smoke runs in CI before promotion
  • the opt-in real BFCache browser probe remains environment-limited; lifecycle unit coverage plus reload and top-level navigation browser coverage pass

@vercel

vercel Bot commented Sep 2, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
threadplane Ready Ready Preview Sep 2, 2026 11:07pm UTC

Request Review

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated approval: this PR received an intelligent (AI) code review. See the review comments on this PR.

@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Claude finished @blove's task in 0s —— View job


I'll analyze this and get back to you.

Comment thread apps/website/e2e/fixtures/custom-runtime-server.ts Fixed
@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Claude finished @blove's task in 0s —— View job


I'll analyze this and get back to you.

@blove
blove force-pushed the blove/control-plane-polish-runtime-targets branch from 2b4549e to 4200908 Compare September 2, 2026 21:04
@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Claude finished @blove's task in 0s —— View job


I'll analyze this and get back to you.

@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Claude finished @blove's task in 0s —— View job


I'll analyze this and get back to you.

@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Claude finished @blove's task in 0s —— View job


I'll analyze this and get back to you.

workspace-react is the library this branch puts nearly all of its new logic in
— the runtime target provider, memory-only credential session, origin
authorization and diagnostics redaction — and nothing in CI ever ran its tests.
It appears zero times in ci.yml.

Adding it to the `library` job's LIBS list would not have worked: its scope
tags are `scope:cockpit` and `scope:shared`, and `shared` is not a SCOPE_KEY,
so a workspace-react change flips `cockpit` and never flips `library`.

Name it in the Cockpit job instead, alongside cockpit-docs and cockpit-registry,
which are there for exactly this reason — `nx test` does not walk `^test`.

Verified: lint clean, 26 spec files / 387 tests pass, production build clean.
Mutation-probed the wiring by breaking a spec — run-many fails as it should.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Claude finished @blove's task in 0s —— View job


I'll analyze this and get back to you.

@blove
blove merged commit 738b2ed into main Sep 2, 2026
75 checks passed
blove added a commit that referenced this pull request Sep 2, 2026
Merging #963 brought two collisions:

- its playwright config rewrite dropped the growth form-policy switch from the
  local web server, restored here on the rewritten entry;
- its Cockpit-retirement scan used emails/newsletter-welcome.ts and
  lib/resend.ts as canaries proving the scan descends past the top level, and
  both were removed with the legacy email pipeline. Replaced with two nested
  files that still exist.

Its new SiteFooter retirement case also needs the real Footer, so the policy
forwarding is now asserted through the rendered disclosure rather than a
module mock.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
blove added a commit that referenced this pull request Sep 3, 2026
…e-export (#970)

#967 re-exported cockpit/runtimes/{aws-strands,microsoft-agent-framework}
requirements.txt so their headers record the --no-dev they were actually built
with. deployments/ag-ui-dev/deps/ is generated from those files, so its copies
still carried the old header and the Railway deploy's drift check failed on
main.

The Deploy AG-UI Railway workflow only runs on push to main, so neither #967
nor #963 exercised this check before merge.

Content is unchanged — two header lines, no package added or removed.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
blove added a commit that referenced this pull request Sep 3, 2026
The spec added in #963 never loaded. Playwright transpiles specs to CJS
(`var _test = require("@playwright/test")`), so `import.meta.url` compiles to a
`require` the loaded module cannot resolve:

    ReferenceError: require is not defined in ES module scope
      at platform-production-smoke.spec.ts:1
    Error: No tests found.

Production smoke has therefore been reporting "no tests" rather than running,
so the job has verified nothing since it landed. Resolve the repo-root policy
file from __dirname, which is what the emitted CJS module actually has.

The spec is testIgnore'd outside PRODUCTION_SMOKE mode, so nothing on a PR ever
loaded it — the failure only surfaced post-merge on main. Add a Website e2e step
that collects it with --list, which loads every spec without touching
production, so this cannot recur.

Verified: the spec now collects 104 tests, and running it against production
gives 98 passed / 5 failed / 1 skipped. The 5 failures are real production
findings (legacy Cockpit redirects still answer 200 instead of 308, and the
assembled frame-ancestors policy does not validate) and are reported separately
— they are not caused by this change.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
blove added a commit that referenced this pull request Sep 3, 2026
* docs: specify growth lifecycle hard cutover

* docs: plan growth lifecycle hard cutover

* fix: exclude imported contacts from lifecycle campaign

* feat: mark imported lifecycle contacts

* feat: reconcile scheduled lifecycle mail

* feat: add durable website growth boundary

* feat: persist acquisition forms in neon

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat: submit growth approval envelopes

Every rendered acquisition form now sends the immutable growth envelope —
submission UUID, acquisition session, and policy version — and shows the
server-owned disclosure beside its submit control. A 409 stops the flow with a
refresh instruction rather than reporting success.

Also carries the switch the pages now read at render time into the unit suite,
the Playwright web server, and the CI website job, and fixes the module-boundary
lint on the routes landed in the previous commit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat: expose lifecycle stop and dispatch routes

Lands the signed stop, Google reply, Resend webhook, and lifecycle cron
adapters, and registers the single every-minute Vercel cron.

The unsubscribe route no longer reports success when the durable write fails:
a legacy raw-email GET and a signed one-click POST both answer a stop failure
or a database close failure with the same retryable 503, while token and body
failures keep their closed 400. Known and unknown addresses still share one
success shape, so the link reveals nothing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* refactor: remove legacy website email pipeline

Deletes the drip scheduler, Loops client, legacy Resend transport, the email
preview route, and every legacy template, and drops the now-dead isolation
mocks from the acquisition route specs. The boundary spec fails if any of them
returns.

The Resend SDK stays a website dependency: the verified-webhook route uses it
to check Svix signatures, and the boundary spec pins that to the one importer.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test(ci): expect the fourth operator CLI suite

The cancellation operator landed with its spec wired into the dedicated
operator-CLI target, but the CI scope meta-test still pinned exactly three
suites and would have failed the required check.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs: mark the hard-cutover plan through repository verification

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(security): escape the signed token echoed into stop confirmation forms

CodeQL flagged reflected XSS on both confirmation pages: the token from the
query string was interpolated straight into a hidden input value. A valid HMAC
is still required to reach that branch, so exploitability was low, but the
interpolation was unescaped.

Both routes now escape the value, and each spec mocks only the verifier so a
hostile token reaches the form — the guard fails if the escaping is removed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix: reconcile the hard cutover with the unified control plane

Merging #963 brought two collisions:

- its playwright config rewrite dropped the growth form-policy switch from the
  local web server, restored here on the rewritten entry;
- its Cockpit-retirement scan used emails/newsletter-welcome.ts and
  lib/resend.ts as canaries proving the scan descends past the top level, and
  both were removed with the legacy email pipeline. Replaced with two nested
  files that still exist.

Its new SiteFooter retirement case also needs the real Footer, so the policy
forwarding is now asserted through the rendered disclosure rather than a
module mock.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs: record the verified cutover firewall rule and correct its response shape

The rollback control was installed against production, verified, and removed.
The runbook now carries the exact rule, the install/remove API calls, and the
observed probe results so Task 11 does not have to rediscover them.

It also corrects a promise the platform cannot keep: the plan and design said
the block returns a retryable maintenance response, but Vercel's WAF only
offers log, challenge, deny, bypass, and rate_limit. Deny returns 403, which
a well-behaved client will not retry.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
blove added a commit that referenced this pull request Sep 3, 2026
…974)

The "Verify Website preview runtime embedding policy" step added in #963 cannot
pass. Vercel deployment protection answers every preview path with
302 -> vercel.com/sso-api, so Playwright drives the browser to an SSO page and
times out waiting for the app:

    locator.click: Test timeout of 30000ms exceeded.
      waiting for [data-cockpit-desktop-navigation] button "Settings"

It has never succeeded: skipped on 738b2ed as stale, failed on its first real
execution. Because the step sits mid-deploy, its failure skips Website
promotion, the cockpit redirect build and cockpit promotion — production has
not advanced since the arc landed, which is why cockpit.threadplane.ai still
answers 200 where the smoke suite expects 308.

Send the project's automation bypass when CI supplies it. The header is added
only when VERCEL_AUTOMATION_BYPASS_SECRET is set, so local and production runs
are unchanged, and every URL this suite visits is a first-party origin. The
step now fails with an actionable message rather than a silent 90s timeout when
the secret is missing.

Requires the repository secret VERCEL_AUTOMATION_BYPASS_SECRET, from
"Protection Bypass for Automation" on the Vercel website project.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
blove added a commit that referenced this pull request Sep 3, 2026
Merges arrive faster than CI completes, so the deploy job's staleness gate
defers to a newer commit that then defers itself. Tonight that ran five rounds
and production never advanced past the pre-#963 build. A merge queue serialises
main so each candidate gets a clean window.

GitHub runs queue candidates on the merge_group event. Three things are needed
before the queue can be turned on — enabling it first would hang every merge on
a required check that never runs:

- trigger CI on merge_group;
- scope a candidate from the merge group's base/head range, so queued runs stay
  as narrow as pull requests instead of running the full matrix;
- report `CI — required` on merge_group. It is the only required status
  context, and the queue waits on it per candidate.

The 18 verification jobs need no change: they already fall through to the
ci-scope outputs whenever the event is not `push`. The deploy jobs need none
either — they additionally require github.ref == 'refs/heads/main', and a
candidate builds on gh-readonly-queue/*, so a queued candidate cannot promote.
Both properties are now pinned by tests.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
blove added a commit that referenced this pull request Sep 3, 2026
)

Production promotion fails on main:

    Error: Deployment doesn't belong to current team brian-loves-projects-c1a217a2

`vercel build`, `deploy` and `pull` read the team from .vercel/project.json,
but `promote` takes a bare deployment URL and cannot — it falls back to the
token's default team, which is a personal one, not cacheplane. Both promote
calls were introduced without --scope in #963, so neither the Website nor the
cockpit redirect service has ever promoted since.

refs/deploy/last-promoted still points at 8687258 (#945, 2026-09-01), so
everything merged in the last two days is built and verified but never live.

Verified both --scope forms against the API: the team slug and the team id in
VERCEL_ORG_ID each resolve to cacheplane. Use the existing secret rather than
hardcoding a slug, and guard it so an unscoped promote cannot return.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
blove added a commit that referenced this pull request Sep 8, 2026
DocsControlPlane.spec.tsx guards 'rounded sidebar states without a left
marker' from #963. The step would have overridden that deliberate decision to
achieve something the retheme had already achieved: --color-accent-surface is
now a yellow tint and --color-accent is scope navy, so the active item is
already a soft yellow pill with navy ink.

A plan written days earlier does not get to overrule a design decision the
codebase deliberately guards.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
blove added a commit that referenced this pull request Sep 8, 2026
…e navy (#1058)

* docs: spec — ATC theme spike (aviation yellow) for the website

Reskins the website to the visual language of ATC — Live Air Traffic Radio:
aviation yellow #FFAF00, Archivo Black display type, and instrument-panel
data strips. Built in libs/design-tokens rather than as a website override,
so the spike measures what adopting the theme actually costs.

The central finding is that the accent role has to split: #FFAF00 on white
is 1.84:1, so yellow is a fill colour only and the interactive ink moves to
the app's scope navy #15253E (15.37:1, higher than the #004090 it replaces).
The focus ring deliberately stays navy on light surfaces.

Records the load-bearing risk: the homepage architecture diagram e2e added
by #1048 measures text runs against their cards after document.fonts.ready,
and Archivo's metrics differ from Inter's.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs: spec — diagrams keep Inter via a --font-diagram token

Diagrams do not take the brand face. They stay clean and minimal on Inter
through a new --font-diagram token, for two reasons: a heavy grotesk fights
a 16-node drawing, and the diagram geometry is tuned to Inter's metrics.

That second reason is the load-bearing one. EnterpriseArchitecture pins every
rectangle to an 8px grid and home-architecture.spec.ts measures each rendered
text run against its card, so swapping diagrams to Archivo would force a
geometry rework that could exceed the retheme itself. Keeping Inter makes it
zero.

The carve-out is three declarations — .arch-figure text in landing.css, and
the two sans rules in the docs diagram kit — which the mechanical
--font-inter rename must send to --font-diagram rather than --font-sans.
Section 8 now treats the overflow e2e as the gate on that carve-out holding.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs: implementation plan for the ATC theme spike

Eleven tasks, TDD where there is something to assert: the three value-pinning
token specs are updated before the values move, so each colour change has a
red test first.

Notable ordering constraints baked into the plan:

- dark.ts needs no edit to `accent` (it derives from accentLight) but does
  need lines 29-33, which hardcode the old blue. Task 2 makes that a step of
  its own with a grep that proves no blue survives.
- The font-var rename and the --font-diagram carve-out land in ONE commit.
  Split apart, a blind find-and-replace sends the diagram rules to Archivo and
  the overflow e2e can fail at 390px only.
- The site is visibly broken between Task 4 and Task 5, which the plan says
  out loud so nobody stops to debug it.

Selectors verified against the source rather than guessed: the logo mark takes
the signal via `color` (PlaneMark fills with currentColor), and the docs active
item is [data-docs-navlink][data-active], already at docs.css:917.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(design-tokens): ATC palette — the accent role splits into navy ink and yellow signal

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs: card COLOURS are in scope; only its bundled font is not

A guard that compares values at test time cannot be satisfied by declaring
the divergence intentional in prose. card.spec.ts reads theme.css and asserts
card/tokens.ts still matches it, so the earlier scope boundary just left
nx test website red for the rest of the arc.

Colours are hex literals and cost nothing to move; the font genuinely needs a
bundled TTF. Splitting those.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(design-tokens): card colours follow the retheme; guard the dark accent tints

The Satori social card hand-copies the light palette as hex literals, because
Satori cannot read CSS variables, and card.spec.ts reads theme.css at test
time and asserts the copy still matches. So the retheme left two guards red:
ink and accent had moved in the tokens and not in the card. A spec that
compares values at test time cannot be satisfied by declaring the divergence
intentional in prose — the only way to make it green is to move the card, so
the card's ink, accent, accent-surface and accent-border now follow the ATC
palette. Only the card's bundled font stays out of scope.

Also adds the guard the retheme was missing: darkOverrides.accent derives from
brand.accentLight and flips on a repoint, while the four accent tints beside it
are hardcoded rgba() and do not. The new dark-theme test derives the expected
RGB triple from the token rather than pinning a second literal, so it moves
with the palette; it was mutation-checked by reverting accentGlow to the old
blue and confirming it goes red.

The rest is honesty upkeep on comments the retheme falsified: a dark test named
for a blue accent it no longer asserts, an accent-family comment, and the
generator docstring that still called the cockpit fallback divergence "minor
drift" when those fallbacks are now a different hue entirely. base.ts hoists
AVIATION_YELLOW and SCOPE_NAVY so signal cannot drift from accentLight, nor
scope from accent. No emitted value changes except #ffc233 -> #FFC233.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs: add a sweep task for the 21 hardcoded brand colours the plan missed

Task 2's review found the plan named only 3 of 24 hardcoded old-brand sites in
apps/website/src. The rest would have survived the retheme as blue islands.

Includes the one site that must NOT take the yellow: ChatLandingCodeShowcase's
--chat-user-bg sits directly above --chat-user-color: #ffffff, so white on
aviation yellow would be 1.84:1. It takes scope navy instead.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(design-tokens): emit signal, signal-strong, scope, alert and ink

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test(design-tokens): guard cssVars brand emission; explain the deliberate aliases

A reviewer proved by mutation that deleting all five brand names
(--ds-signal, --ds-signal-strong, --ds-scope, --ds-alert, --ds-ink) from
css-vars.ts left the full design-tokens suite green: the existing
ds-var-sources-agree.spec.ts only checks that tokens.css is a superset of
cssVars(), which is correct for the type scale but wrong for brand colors,
which have no reason to exist in one emitter and not the other.

- Add a scoped reverse check to ds-var-sources-agree.spec.ts asserting
  cssVars('light') emits all five brand names, without making the whole
  comparison bidirectional (which would break on the legitimate type-scale
  superset).
- Fix the ds-var-contract.spec.ts docstring, which claimed the list is
  derived purely from a grep of consumer usage — false for the five new
  brand names (zero references outside libs/design-tokens/ today) and a
  trap that would lead someone to "clean up" and delete the guard.
- Document the deliberate --color-signal/accent-light and
  --color-scope/accent duplication in theme.css, and add a Brand
  sub-header to the --ds-* block in tokens.css / tokens-dark.css so the
  generated output explains itself to a website/cockpit author.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(design-tokens): Archivo display and text faces, Inter retained for diagrams

Renames fontSerif (EB Garamond) to fontDisplay (Archivo Black) and
repoints fontSans at Archivo instead of Inter. Adds a new fontDiagram
token that keeps Inter: EnterpriseArchitecture.tsx pins every rectangle
to an 8px grid and home-architecture.spec.ts measures each rendered
text run against its card after document.fonts.ready, so retyping
diagram text to Archivo would silently overflow the cards and force a
geometry rework larger than the retheme itself. Diagram surfaces read
as information, not brand, and deliberately stay off the brand face.

Regenerates theme.css/tokens.css/tokens-dark.css from the new
typography values and updates the token<->CSS parity and --ds-* var
contract specs accordingly. apps/website and libs/workspace-react
still reference the retired --font-garamond/--font-inter names; that
rename is a separate follow-up task and is expected to leave the
website visually broken until it lands.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs: record the next/font constraint that makes --font-diagram fragile

theme.css emits --font-diagram as a raw 'Inter, ...' stack, which next/font's
hashed family never matches. If layout.tsx stops supplying the var, diagrams
silently fall back to system-ui and every glyph width shifts — defeating the
carve-out the token exists for. Only the architecture e2e would notice.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* refactor(website): --font-display/--font-sans, with diagrams carved out onto --font-diagram

The design-tokens generator no longer emits --font-garamond or --font-inter,
so every consumer reference in apps/website and libs/workspace-react moves to
the new names. layout.tsx's next/font `variable` fields move with them; the
faces themselves are swapped in a later task.

The diagram carve-out lands here rather than as a follow-up because
--font-inter was a single variable doing two jobs: ~100 body-text rules and
the diagram text. Renaming it alone would retype the architecture figure and
the docs diagram kit from Inter to Archivo, changing every glyph width.
EnterpriseArchitecture.tsx pins its rectangles to an 8px grid and
e2e/home-architecture.spec.ts measures each rendered text run against its
card, so that would land as a red e2e — possibly only at the 390px viewport.
Splitting the two commits would mean deliberately committing a broken
geometry, so the three diagram declarations move to --font-diagram (Inter,
retained for exactly this reason) in the same change:

  .arch-figure text                                    (landing.css)
  .tp-diagram-node[data-title="sans"] .tp-diagram-title (docs.css)
  .tp-diagram-meta                                      (docs.css)

Diagram rules already on --font-mono are untouched; mono was never Inter.

ui.css's FONTS note gains the constraint that makes --font-diagram fragile:
theme.css emits it as a raw `Inter, ...` stack, which next/font's hashed
family never matches, so the var has to keep being supplied by next/font in
layout.tsx or diagrams fall back to system-ui silently.

nx test website and nx test workspace-react pass, but these are jsdom suites
that do not resolve CSS custom properties — they cannot detect a wrong font
var. The architecture e2e is the real check.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs(design-tokens): note the next/font constraint on --font-diagram

Two documentation fixes from the Task 4 review, both about the diagram
carve-out that landed in the previous commit.

The fontDiagram docblock explained why diagrams keep Inter but not what keeps
them on it: on apps/website the var has to be supplied by next/font in
layout.tsx, because the raw `Inter` literal theme.css emits never matches
next/font's hashed family name. Cross-referenced to the FONTS note in
apps/website/src/styles/ui.css, which now carries the same warning.

The ds-var-contract docstring called `--ds-font-serif` "consumer-referenced"
in a paragraph that otherwise says the greps for these names come up empty,
which read as a contradiction. Reworded to what it actually is: a name this
list previously guarded and the retheme retires.

Note the review's stated premise was not quite right — `--ds-font-serif` is
not reference-free. libs/workspace-react/src/styles/workspace.css still names
it as the fallback behind var(--font-display), so the docstring now says so
explicitly instead of dropping the fact, and scopes the zero-reference claim
to the reserved names it was always about (verified: 0 hits each for
--ds-signal, --ds-signal-strong, --ds-scope, --ds-alert, --ds-ink,
--ds-font-display, --ds-font-diagram outside libs/design-tokens/).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(workspace-react): repair the dangling --ds-font-serif reference

Task 4 retired --ds-font-serif from the token sources; Task 5's rename swept
--font-garamond in the same declaration but left the --ds-* fallback pointing
at a name that no longer exists.

The failure mode is silent and worse than it looks: an undefined custom
property inside a font-family list is invalid at computed-value time, so the
whole declaration is dropped and .workspace-prose headings lose their font
entirely. There is no partial fallback to the first entry in the list.

ds-var-contract.spec.ts guarded only the other direction — that a name
consumers use does not vanish. Adds the inverse check, which fails if any
consumer references a --ds-* the generator does not define. Mutation-verified:
restoring the bug turns it red with the exact name.

--ds-surface-subtle is listed as a known pre-existing dangling reference
(already broken at 218ac1c) so it stays visible rather than being hidden by
the new guard.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(website): load Archivo Black and Archivo, drop EB Garamond

layout.tsx now loads Archivo_Black for --font-display and Archivo for
--font-sans via next/font/google, matching the ATC-derived token
library. EB_Garamond is gone.

Inter stays loaded here too, feeding --font-diagram. It cannot be
supplied by theme.css's raw `Inter, system-ui, sans-serif` literal:
next/font registers the family under a hashed name, so that literal
never matches it and would silently fall back to system-ui, changing
every diagram glyph width. Diagram geometry in
apps/website/e2e/home-architecture.spec.ts is pinned to Inter's
metrics on an 8px grid, so Inter must keep coming from next/font.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(website): signal surface scope for the ATC hero block

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(website): yellow hero block closed by the scope-navy frequency strip

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(website): the marker highlight is a yellow wash everywhere but the hero

The Task 8 rule replaced the wash with an ink underline globally, but
.marker-highlight is not hero-only — /langgraph, /chat, /ag-ui and /render all
use it on white canvas, where they would have picked up an accent-coloured
underline nobody asked for.

A yellow wash is the better answer anyway: it is literally what a highlighter
does, and it is the correct use of aviation yellow as a fill under near-black
ink rather than as a text colour. The ink underline is now scoped to the
signal block, where a yellow wash would be yellow on yellow.

Also records in the plan that nx test website does not type-check — a step
written as "watch the test fail" on a union change passes vacuously, because
vitest skips types and React forwards unknown props to the DOM.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(website): yellow primary button and nav mark, scope-navy dark band

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(website): sweep the last hardcoded navy out of the light and dark surfaces

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs: withdraw the docs left-rule step — the tokens already did it

DocsControlPlane.spec.tsx guards 'rounded sidebar states without a left
marker' from #963. The step would have overridden that deliberate decision to
achieve something the retheme had already achieved: --color-accent-surface is
now a yellow tint and --color-accent is scope navy, so the active item is
already a soft yellow pill with navy ink.

A plan written days earlier does not get to overrule a design decision the
codebase deliberately guards.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs: withdraw the plan's docs left-rule step to match the spec correction

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(website): Archivo Black is single-weight — stop synthesizing bold and italic

`--font-display` is Archivo Black, loaded by next/font at `weight: '400'`.
Google Fonts ships it in exactly one weight and no italic, so every rule that
asked for anything else was making the browser synthesize the face — smearing
an already-black glyph, or shearing it into a fake oblique. jsdom cannot
measure either, so nothing caught it.

- Removed 48 `font-weight` declarations (500–800) from rules whose own block
  sets `font-family: var(--font-display)`: pages.css 21, landing.css 15,
  marketing.css 5, docs.css 4, ui.css 2, chrome.css 1. The face has one weight,
  so declaring it is noise. `.hero-heading` keeps its explicit `400` and the
  comment documenting the constraint at the most-read site. Rules on
  `--font-sans` and `--font-mono` are untouched: Archivo and JetBrains Mono are
  genuinely multi-weight.
- Removed `font-style: italic` from the four display-face rules in landing.css
  (`.wp-cover-title`, `.final-cta-heading`, `.final-cta-mark`,
  `.proof-strip-watermark`). The faux oblique was most visible on the two that
  set it at 210px and up to 300px.

The logo mark was aviation yellow on the white nav and footer — 1.84:1, below
even the 3:1 floor for meaningful non-text graphics, and the exact thing this
theme's own thesis forbids. It is now the ATC app icon in miniature: a
signal-yellow squircle with an ink glyph, 10.73:1, inverting to an ink tile
inside the signal scope so it does not vanish on the yellow block.

The tile uses `box-sizing: border-box` rather than `content-box`. The logo sets
the nav's content-row height at the two smaller breakpoints, so padding on a
content-box icon inflates the whole nav — measured 58/66/81 to 64/72/81 against
a `--nav-h` that e2e/nav-height.spec.ts pins to the rendered nav within 1px.
Border-box insets the glyph instead, leaving every `--nav-h` offset untouched;
all 8 nav-height e2e tests pass. The stale comment above the rule (the glyph
"inherits the wordmark's color"; "the wordmark is Garamond") is rewritten.

Also in libs/design-tokens:

- The dangling-`--ds-*` guard only walked four hand-listed directories, missing
  libs/chat, libs/example-layouts, libs/design-tokens, apps/website/public and
  apps/website/e2e. Widened to the whole repo with node_modules/.next/dist/
  coverage excluded. This surfaced 13 pre-existing dangling names, all from the
  committed generator artifact apps/website/public/whitepapers/chat-preview.html;
  they are listed in KNOWN_PRE_EXISTING and attributed in the comment so they
  stay visible rather than hidden. The guard also had to stop grepping its own
  docblocks, which quote retired names as prose.
- `--ds-font-serif`'s comment claimed a surviving reference in workspace.css; a
  later commit repaired that, so it now has none.
- Aviation yellow on the dark ground is 10.24:1 on `canvas` and 9.24:1 on
  `surface`, not 8.33:1 — that figure is the ratio against the website's scope
  navy, a different surface. Corrected, and the deliberate website/dark.ts
  surface divergence is now documented from both sides.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(website): rebase repairs — #1057's strip used the retired font vars

The open-source strip landed on main after this branch renamed the font
custom properties, so it referenced --font-garamond and --font-inter, which no
longer exist. An undefined custom property in a font-family declaration is
invalid at computed-value time, so both rules would have shipped as silent
system-font fallbacks that look almost right.

.open-source-strip-line also puts an italic <em> inside what would have been
the display face, and Archivo Black has no italic. It is body-scale editorial
copy rather than display type, so it moves to --font-sans, where the italic is
real: Archivo now loads style ['normal','italic']. That also retires the faux
italics the sans face had been synthesizing all along, since Inter was loaded
normal-only too.

Adds font-vars.spec.ts, which fails if any stylesheet references a --font-*
var layout.tsx does not supply. This exact bug happened twice in one day — the
rename, and then a PR that landed in between — and nothing caught it: jsdom
does not resolve custom properties and the Next build does not type-check CSS.
Mutation-verified.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants