feat: Lecture page (/lecture, 5 movements) — TDD

Replaces the WorkshopStub for /lecture with a real content screen:
5 movement sections (agency, perception loop, ZeroClaw, failure modes,
edge vs cloud), mirroring the Landing visual idiom. No PhaseStrip — the
lecture is not a workshop phase. 6 new tests; suite 29/29 green.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
This commit is contained in:
Omar Sobh
2026-06-16 19:01:53 -07:00
co-authored by Claude Opus 4.8
parent e64bce81ab
commit 8163dd7829
3 changed files with 279 additions and 1 deletions
+2 -1
View File
@@ -1,6 +1,7 @@
import { BrowserRouter, Routes, Route } from 'react-router-dom' import { BrowserRouter, Routes, Route } from 'react-router-dom'
import { Landing } from '@/pages/Landing' import { Landing } from '@/pages/Landing'
import { TeamRegistration } from '@/pages/TeamRegistration' import { TeamRegistration } from '@/pages/TeamRegistration'
import { Lecture } from '@/pages/Lecture'
import { PhaseStrip } from '@/components/PhaseStrip' import { PhaseStrip } from '@/components/PhaseStrip'
import type { PhaseKey } from '@/store/session' import type { PhaseKey } from '@/store/session'
@@ -28,7 +29,7 @@ export default function App() {
<Route path="/workshop/module1" element={<WorkshopStub title="Module 1 · Sense → Reason" phase="m1" />} /> <Route path="/workshop/module1" element={<WorkshopStub title="Module 1 · Sense → Reason" phase="m1" />} />
<Route path="/workshop/module2" element={<WorkshopStub title="Module 2 · Harness engineering" phase="m2" />} /> <Route path="/workshop/module2" element={<WorkshopStub title="Module 2 · Harness engineering" phase="m2" />} />
<Route path="/workshop/add" element={<WorkshopStub title="ADD builder & submit" phase="add" />} /> <Route path="/workshop/add" element={<WorkshopStub title="ADD builder & submit" phase="add" />} />
<Route path="/lecture" element={<WorkshopStub title="Lecture · 5 movements" phase="reg" />} /> <Route path="/lecture" element={<Lecture />} />
<Route path="/admin" element={<WorkshopStub title="Instructor dashboard" phase="reg" />} /> <Route path="/admin" element={<WorkshopStub title="Instructor dashboard" phase="reg" />} />
<Route path="/judge" element={<WorkshopStub title="Judge review" phase="reg" />} /> <Route path="/judge" element={<WorkshopStub title="Judge review" phase="reg" />} />
</Routes> </Routes>
+57
View File
@@ -0,0 +1,57 @@
import { describe, it, expect } from 'vitest'
import { render, screen } from '@testing-library/react'
import { MemoryRouter } from 'react-router-dom'
import { Lecture } from './Lecture'
function renderLecture() {
return render(
<MemoryRouter>
<Lecture />
</MemoryRouter>,
)
}
describe('Lecture', () => {
it('renders a lecture headline mentioning the five movements', () => {
renderLecture()
expect(screen.getByRole('heading', { level: 1 })).toHaveTextContent(/five movements/i)
})
it('renders exactly five movement sections', () => {
renderLecture()
expect(screen.getAllByTestId('movement')).toHaveLength(5)
})
it('renders the five movement titles as headings', () => {
renderLecture()
const titles = screen
.getAllByRole('heading', { level: 2 })
.map((h) => h.textContent)
expect(titles).toEqual([
'Agency',
'The perception loop',
'ZeroClaw',
'Failure modes',
'Edge vs cloud',
])
})
it('orders the movements 1 through 5 by slug', () => {
renderLecture()
const slugs = screen.getAllByTestId('movement').map((el) => el.getAttribute('data-movement'))
expect(slugs).toEqual(['agency', 'perception', 'zeroclaw', 'failure-modes', 'edge-vs-cloud'])
})
it('links back to landing and into the workshop', () => {
renderLecture()
expect(screen.getByRole('link', { name: /back to landing/i })).toHaveAttribute('href', '/')
const enter = screen.getAllByRole('link', { name: /enter workshop/i })
expect(enter.length).toBeGreaterThan(0)
enter.forEach((l) => expect(l).toHaveAttribute('href', '/workshop'))
})
it('does not render the workshop phase strip', () => {
renderLecture()
expect(screen.queryByTestId('phase-strip')).toBeNull()
})
})
+220
View File
@@ -0,0 +1,220 @@
import { Link } from 'react-router-dom'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
interface Movement {
n: number
id: string
title: string
thesis: string
body: string[]
takeaways: string[]
tags?: string[]
}
const MOVEMENTS: Movement[] = [
{
n: 1,
id: 'agency',
title: 'Agency',
thesis: 'An agent is a system that closes the loop between sensing the world and acting on it — without a human in the middle.',
body: [
'Most embedded software is reactive plumbing: read a sensor, threshold it, toggle a pin. An agent is different in kind, not degree — it holds a goal, forms a belief about its environment, and chooses an action it expects to advance that goal.',
'Today you build the smallest honest version of that: a board that decides, on its own, whether a structure is behaving nominally, anomalously, or critically — and is accountable for the call it makes.',
],
takeaways: [
'Agency = goal + perception + decision + action, closed in a loop',
'The interesting engineering is in the decision, not the wiring',
'Autonomy is a spectrum; pick the least you need to be useful',
],
tags: ['concept'],
},
{
n: 2,
id: 'perception',
title: 'The perception loop',
thesis: 'Sense → reason → act, repeated fast enough that the world has not changed underneath you.',
body: [
'The loop is the heartbeat of the agent. Sense produces a frame — here, IMU acceleration and an acoustic level. Reason maps that frame to a verdict against your tuned thresholds. Act emits the verdict and, where wired, drives an output.',
'Loop rate is a design parameter, not an afterthought. Too slow and you miss the event; too fast and you drown the reasoner in noise and burn your call budget. You will feel this tension directly when you tune the harness.',
],
takeaways: [
'A frame is the unit of perception — keep it small and typed',
'Latency budget = sense + reason + act must beat the event',
'Rate-limit reasoning deliberately; more calls is not more intelligence',
],
tags: ['concept', 'build'],
},
{
n: 3,
id: 'zeroclaw',
title: 'ZeroClaw',
thesis: 'A Rust agent runtime small enough to live on a Cortex-M4 and route reasoning off-device when it must.',
body: [
'ZeroClaw compiles to a ~6 MB binary for the Renesas RA4M1. It owns the perception loop on-device, classifies frames locally against your harness, and escalates only the ambiguous cases over UART → ESP32-S3 → a rate-limited cloud proxy.',
'This is the edge-agent pattern in miniature: cheap, fast, private decisions stay local; expensive judgement is borrowed sparingly. The harness you tune today is the contract between those two worlds.',
],
takeaways: [
'On-device first: classify locally, escalate the unsure',
'The harness is the local/remote contract',
'Small binaries are a feature — they fit where the structure is',
],
tags: ['hardware'],
},
{
n: 4,
id: 'failure-modes',
title: 'Failure modes',
thesis: 'An autonomous system is defined by how it fails, not how it succeeds on a good day.',
body: [
'Sensors drift, links drop, models hallucinate, and budgets run dry mid-event. A serious agent has a defined behaviour for each: a stale frame is not a calm frame, a dropped link is not a clean bill of health, an exhausted call budget falls back to the local verdict rather than going silent.',
'Layer 4 of your Agent Design Document is exactly this exercise — name the failure, then design the degradation. The grade is in the honesty of that analysis.',
],
takeaways: [
'Absence of signal is information — never read it as "fine"',
'Every dependency needs a defined degradation path',
'Design the fallback before the happy path ships',
],
tags: ['concept'],
},
{
n: 5,
id: 'edge-vs-cloud',
title: 'Edge vs cloud',
thesis: 'The question is never edge or cloud — it is which decision belongs where.',
body: [
'Latency, privacy, cost, and availability pull the boundary in different directions. A structural-safety call that must survive a network outage belongs on the edge; a once-an-hour summary that benefits from a large model belongs in the cloud.',
'The AI-native redesign in Layer 5 asks you to redraw that boundary on purpose, justified by the failure modes you just named. That is the whole craft: placing intelligence where it is accountable.',
],
takeaways: [
'Place each decision by its latency, privacy, cost, and availability',
'Survive-the-outage decisions live at the edge',
'A good architecture is a defensible boundary, not a default',
],
tags: ['concept', 'build'],
},
]
export function Lecture() {
return (
<main className="min-h-screen flex flex-col bg-background">
<header className="px-8 py-5 border-b border-border flex items-center justify-between sticky top-0 bg-background/90 backdrop-blur z-50">
<div className="font-mono text-xs tracking-widest uppercase">
APESS <span className="text-primary font-bold">2026</span>
<span className="text-muted-foreground"> · Lecture</span>
</div>
<div className="flex items-center gap-3">
<Link to="/" className="font-mono text-[11px] text-muted-foreground hover:text-foreground tracking-widest uppercase">
← Back to landing
</Link>
<Button asChild size="sm">
<Link to="/workshop">Enter workshop →</Link>
</Button>
</div>
</header>
<section className="border-b border-border px-8 py-20">
<div className="max-w-3xl mx-auto text-center space-y-6">
<Badge variant="secondary" className="font-mono text-[10px] tracking-widest uppercase">
Lecture · ~60 min
</Badge>
<h1 className="text-4xl md:text-5xl font-bold tracking-tight leading-[1.05]">
On-device agency
<br />
<span className="text-primary">in five movements</span>
</h1>
<p className="text-base md:text-lg text-muted-foreground leading-relaxed max-w-2xl mx-auto">
The conceptual spine of the workshop — from what an agent is, through the perception loop and the
ZeroClaw runtime, to how these systems fail and where their intelligence should live.
</p>
</div>
</section>
<section className="px-8 py-16">
<div className="max-w-4xl mx-auto space-y-10">
{MOVEMENTS.map((m) => (
<article
key={m.id}
data-testid="movement"
data-movement={m.id}
className="scroll-mt-24"
id={m.id}
>
<div className="grid md:grid-cols-[120px_1fr] gap-6">
<div className="md:text-right">
<div className="font-mono text-[10px] tracking-widest uppercase text-primary">
Movement {m.n}
</div>
<div className="font-mono text-3xl font-bold text-muted-foreground/40 mt-1">
{String(m.n).padStart(2, '0')}
</div>
</div>
<div className="space-y-4">
<div>
<h2 className="text-2xl md:text-3xl font-bold tracking-tight">{m.title}</h2>
<p className="text-sm text-primary/90 mt-2 font-medium leading-relaxed">{m.thesis}</p>
</div>
<div className="space-y-3">
{m.body.map((p, i) => (
<p key={i} className="text-sm text-muted-foreground leading-relaxed">{p}</p>
))}
</div>
<Card className="bg-secondary/30">
<CardHeader className="pb-2">
<CardTitle className="font-mono text-[10px] tracking-widest uppercase text-muted-foreground">
Takeaways
</CardTitle>
</CardHeader>
<CardContent>
<ul className="space-y-2">
{m.takeaways.map((t) => (
<li key={t} className="flex gap-2 text-sm leading-relaxed">
<span className="text-primary font-bold shrink-0">·</span>
<span>{t}</span>
</li>
))}
</ul>
</CardContent>
</Card>
{m.tags && (
<div className="flex gap-1.5 flex-wrap">
{m.tags.map((t) => (
<Badge key={t} variant="outline" className="font-mono text-[9px] uppercase tracking-wider">
{t}
</Badge>
))}
</div>
)}
</div>
</div>
</article>
))}
</div>
</section>
<section className="px-8 py-16 border-t border-border">
<div className="max-w-4xl mx-auto">
<Card className="bg-primary text-primary-foreground border-primary">
<CardContent className="p-10 flex flex-col md:flex-row items-center justify-between gap-6">
<div>
<div className="text-xl md:text-2xl font-bold tracking-tight">Theory's done. Go build the agent.</div>
<div className="font-mono text-[10px] uppercase tracking-widest opacity-70 mt-2">
Five movements · one Agent Design Document
</div>
</div>
<Button asChild size="lg" variant="secondary">
<Link to="/workshop">Enter workshop →</Link>
</Button>
</CardContent>
</Card>
</div>
</section>
<footer className="px-8 py-6 border-t border-border font-mono text-[10px] text-muted-foreground flex justify-between items-center">
<span>RedClaw Systems LLC · Los Gatos, CA</span>
<span>apess.redclaw.dev</span>
</footer>
</main>
)
}