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:
co-authored by
Claude Opus 4.8
parent
e64bce81ab
commit
8163dd7829
+2
-1
@@ -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>
|
||||||
|
|||||||
@@ -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()
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user