Skip to main content
Welcome. This site supports keyboard navigation and screen readers. Press ? at any time for keyboard shortcuts. Press [ to focus the sidebar, ] to focus the content. High-contrast themes are available via the toolbar.
serard@dev00:~/cv

Part II: Building the Foundation

You can't specify what you can't test. You can't test what you can't render deterministically. The foundation comes first.

Phase 1: The SPA — Features Emerge

This website started as a PDF resume and became a terminal-styled SPA in 72 hours. No framework — vanilla JavaScript, marked.js for markdown, Mermaid for diagrams, CSS custom properties for theming.

What matters for typed specifications isn't the technology. It's what emerged: real features worth specifying.

By the end of the SPA phase, the site had:

  • Navigation: TOC sidebar, deep linking with #path.md::heading-slug, back button support, bookmarkable URLs
  • Scroll spy: 4-level heading tracking, active item highlighting, smooth scrolling
  • Search: full-text with weighted scoring, keyboard navigation, highlight wrapping
  • Theming: dark/light modes, high contrast, 3 OS styles, 8 accent color palettes, localStorage persistence
  • Mermaid diagrams: fullscreen overlay with zoom and pan, dual-theme SVG rendering
  • Accessibility: skip-to-content, ARIA roles, keyboard-first design, screen reader support
  • Mobile: responsive sidebar, hamburger menu, touch-friendly buttons

That's 20 distinct features with 112 acceptance criteria. Not because anyone planned 112 ACs — because the complexity emerged organically from building something real.

The Lesson

Don't add typed specifications to a trivial project. The overhead of defining features, writing decorators, and running compliance scans isn't worth it for 3 features. The system pays off when you have enough complexity that a human can't hold the feature-to-test mapping in their head. For this site, that threshold was around 10 features.

Phase 2: Static Generation — Deterministic Output

The SPA worked great for humans. But Google's crawler saw an empty <article> — JavaScript had to execute before content appeared. The site needed static generation.

A Node.js build script pre-renders every page into static HTML. Same content, two rendering paths:

  • Dev path: app.js fetches markdown at runtime, renders with marked.js
  • Static path: app-static.js loads pre-rendered HTML from the build

This matters for typed specifications because of one principle: if you can't test it deterministically, you can't specify it.

What Static Generation Unlocked

Before static generation, testing meant launching a browser and hoping JavaScript executed the same way every time. After static generation:

  1. Build output is a file. You can read it, parse it, assert on it — no browser needed.
  2. Two rendering paths = two test targets. The same test suite runs against both dev (runtime markdown) and static (pre-rendered HTML) to catch discrepancies.
  3. The build pipeline itself became testable. The buildPage() function accepts an io interface — swap in a mock filesystem and test every step without touching disk.
// The io interface that makes the build pipeline testable
const io = {
  read: (path) => fs.readFileSync(path, 'utf8'),
  write: (path, content) => fs.writeFileSync(path, content),
  exists: (path) => fs.existsSync(path),
  warn: (msg) => console.warn(msg),
};

// In tests: inject mock io
const mockIo = {
  read: (path) => fixtures[path],
  write: (path, content) => { outputs[path] = content; },
  exists: (path) => path in fixtures,
  warn: (msg) => { warnings.push(msg); },
};

This is the dependency injection pattern that later enabled the BuildPipelineFeature — 13 acceptance criteria, all testable in milliseconds with mock I/O:

export abstract class BuildPipelineFeature extends Feature {
  readonly id = 'BUILD';
  readonly title = 'Static Build Pipeline';
  readonly priority = Priority.High;

  abstract escapeHtmlWorks(): ACResult;
  abstract rewriteLinksConverts(): ACResult;
  abstract assetPrefixCalculates(): ACResult;
  abstract collectMermaidBlocks(): ACResult;
  abstract replaceMermaidPlaceholders(): ACResult;
  abstract buildMetaTagsGenerates(): ACResult;
  // ... 7 more
}

Without static generation, most of these ACs would require a browser. With it, they're pure function tests.

The Lesson

Make your output deterministic before you try to specify it. If your build produces files, you can assert on files. If your app only exists at runtime, you need heavier test infrastructure (Playwright, Puppeteer) for every AC — which is slower, flakier, and harder to maintain.

The static build didn't just solve SEO. It created the testing surface that typed specifications need.

The Foundation Checklist

Before moving to testing (Part III) and typed specs (Parts IV-VI), verify you have:

  • Real features — enough complexity that you can't hold the feature-to-test mapping in your head (10+ features)
  • Deterministic output — a build step that produces files you can assert on, or a stable test server
  • Testable architecture — dependency injection or similar patterns that let you test components in isolation
  • Two rendering paths (if applicable) — dev and production paths that should produce identical behavior

If you're missing any of these, address them first. Typed specifications on a shaky foundation create busywork, not confidence.


Previous: Part I: Why Typed Specifications Next: Part III: Multi-Layer Quality — six test layers, each catching different classes of bugs.