CodeBites: Spec-Driven Development

using specifications to guide AI-assisted code generation

"Write the spec first. The code follows from the spec, not the other way around."

1.0 - Introduction

Spec-driven development is a practice where a written specification is the primary artifact of a project. Code is generated from - and must remain consistent with - the spec. With AI coding tools this becomes practical at every scale: the AI reads the spec and generates code; the developer reviews and updates the spec when intent changes. The projects in Code/Projects/ - TextFinder and PageValidator - are concrete examples. Each has: This spec process was inspired by github.com/github/spec-kit but simplified for smallish projects where a lightweight set of Markdown documents is sufficient to guide AI-assisted development without the overhead of a full specification framework.

2.0 - Constitution.md — The Governing Document

Each project starts with a Constitution.md that is intentionally language-agnostic. It defines what the project does, how it decomposes into components, and the design principles that every implementation must follow regardless of programming language. For TextFinder the constitution specifies four components: For PageValidator the constitution specifies a linear pipeline:
  Tokenizer ← Lexer ← Validator ← EntryPoint
No component depends on anything to its right. Each component has one job and no knowledge of its callers.
Design Principles from the Constitutions

Principles shared by both projects

  1. Spec-First — implementation follows the spec; deviations require the spec to be updated first, not the other way around.
  2. Single Responsibility — each component does exactly one thing and exposes only what its callers need.
  3. Dependency Direction — EntryPoint depends on all libraries; libraries never depend on each other.
  4. Non-Failing Parse — parsers and navigators never abort; they forward problems to the caller (DirNav skips unreadable files; Tokenizer and Lexer emit error tokens rather than panicking).
  5. Complete Reporting — errors are collected, never short-circuited. PageValidator's Validator visits the entire file before returning a report.
  6. Caller-Defined Interfaces — components expose traits or callbacks (e.g., the DirEvent trait) so callers control behavior without modifying library code.

3.0 - Structure.md - The Implementation Layout

Where Constitution.md governs logical decomposition, Structure.md governs the physical implementation: directory trees, file names, build-system files, toolchain settings, build steps, and external dependencies. It is language-specific - each language implementation has its own Structure.md. The separation matters. Constitution.md never changes between Rust, C++, C#, and Python. Structure.md is completely different for each because the build systems and source-file conventions differ.
Structure.md - Rust TextFinder (excerpt)

Toolchain

  Language: Rust (edition 2018)
  Build:    Cargo - each crate builds independently (no workspace manifest)

Directory layout

  rs_textfinder/
  ├── Constitution.md
  ├── Structure.md
  ├── RustCmdLine/
  │   ├── Cargo.toml          ← library crate: rust_cmd_line
  │   └── src/cmd_line_lib.rs
  ├── RustDirNav/
  │   ├── Cargo.toml          ← library crate: rust_dir_nav
  │   └── src/dir_nav_lib.rs
  ├── RustTextFinder/
  │   ├── Cargo.toml          ← binary crate: rust_text_finder
  │   └── src/text_finder.rs
  └── RustTfVerify/
      ├── Cargo.toml          ← binary crate: tf_verify
      └── src/main.rs         ← integration verification harness

Dependency graph (physical)

  RustCmdLine   RustDirNav
       \            /
        \          /
      RustTextFinder

  RustTfVerify ──(subprocess)──► text_finder binary
Structure.md - C++ TextFinder (excerpt)

Toolchain

  Language: C++23 with named modules (.ixx files)
  Build:    CMake 3.28+ (FILE_SET CXX_MODULES support required)

Directory layout

  CppTextFinder/
  ├── CMakeLists.txt       ← top-level; sets standard, adds subdirectories
  ├── Constitution.md
  ├── Structure.md
  ├── CommandLine/
  │   ├── CMakeLists.txt
  │   └── src/
  │       ├── CmdLine.ixx  ← export module cmd_line;
  │       └── test.cpp
  ├── DirNav/
  │   └── src/DirNav.ixx   ← export module dir_nav;
  ├── Output/
  │   └── src/Output.ixx   ← export module output;
  └── EntryPoint/
      └── src/main.cpp     ← imports all three modules

Rule from Structure.md

Each part folder owns its own CMakeLists.txt. No part's build file references another part's source directory directly. Only the top-level CMakeLists.txt names the parts.

Structure.md - Rust PageValidator (excerpt)

Toolchain

  Language: Rust (edition 2021)
  Build:    Cargo workspace - each component is a separate crate

Directory layout

  rs_page_validator/
  ├── Cargo.toml       ← workspace root
  ├── Constitution.md
  ├── Structure.md
  ├── tokenizer/
  │   ├── Cargo.toml   ← library crate: tokenizer
  │   ├── Spec.md
  │   ├── Notes.md
  │   └── src/lib.rs
  ├── lexer/
  │   ├── Cargo.toml   ← library crate: lexer
  │   ├── Spec.md
  │   ├── Notes.md
  │   └── src/lib.rs
  ├── validator/
  │   ├── Cargo.toml   ← library crate: validator
  │   ├── Spec.md
  │   ├── Notes.md
  │   └── src/lib.rs
  └── entry_point/
      ├── Cargo.toml   ← binary crate: rs_page_validator
      ├── Spec.md
      ├── Notes.md
      └── src/main.rs

Workspace Cargo.toml

  [workspace]
  members = ["tokenizer", "lexer", "validator", "entry_point"]
  resolver = "2"

4.0 - Component Spec.md Files

Each component has its own Spec.md that specifies the public API in detail: struct names, method signatures, return types, algorithms, and invariants. The AI generates code from this spec; the spec is the source of truth. Example from RustDirNav_Spec.md (TextFinder):
DirNav Component Spec (excerpt)

DirEvent trait

  pub trait DirEvent {
    fn do_dir(&mut self, d: &str);
    fn do_file(&mut self, f: &str);
  }

DirNav<App: DirEvent> struct — methods

  DirNav::new() -> DirNav<App>
  recurse(p: bool) -> &mut Self   // default: true
  hide(p: bool) -> &mut Self      // hide empty dirs, default: true
  add_skip(s: &str) -> &mut Self  // add dir name to skip list
  add_pat(p: &str) -> &mut Self   // add file extension filter
  clear() -> &mut Self            // reset skip list and patterns
  visit(path: &str)               // execute depth-first walk
  get_dirs() -> usize             // count of visited directories
  get_files() -> usize            // count of visited files

Default skip list

  bin  obj  target  build  out  __pycache__
  .venv  venv  dist  .git  .vs  .idea  archive

Invariants

  • Never aborts on read errors — skips and continues
  • Fires do_dir before any do_file in that directory
  • Counts are cumulative across the full walk
Example from Validator_Spec.md (PageValidator) — eight validation rules:
PageValidator Rule Spec (excerpt)

ValidationError and Report types

  struct ValidationError {
    rule:    &'static str,
    message: String,
    line:    usize,
    col:     usize,
  }

  struct Report {
    file:   PathBuf,
    errors: Vec<ValidationError>,
    // is_valid() -> bool
  }

Eight structural rules

  doctype       document begins with <!DOCTYPE html>
  root-element  exactly one <html> wraps the document
  head-required <head> present, contains at least one <title>
  body-required <body> present as direct child of <html>
  tag-nesting   every open tag has a matching close in stack order
  void-elements void elements carry no close tag
  attr-quotes   all attribute values are quoted
  duplicate-id  id attribute values are unique within the document

Invariant

  • Never short-circuits — all errors collected before Report is returned

5.0 - Notes.md — The Development Record

Alongside each Spec.md sits a Notes.md that records the conversation history with the AI used to develop that component. It captures: Notes.md is the traceability record. If a future developer asks why DirNav silently skips unreadable files, the answer is in Notes.md: the spec required non-failing parse behavior and the AI implemented it that way from the first round.

6.0 - The Workflow

The full spec-driven workflow used in these projects:
1. Write Constitution.md - decompose the project into components, state design principles, define the CLI. This document is shared across all language implementations unchanged.
2. Write Structure.md - define the physical structure: directory tree, file names, build-system files (Cargo.toml, CMakeLists.txt, etc.), toolchain settings, and build steps. One Structure.md per language implementation.
3. Write Component Spec.md - for each component, specify the public API: struct names, method signatures, return types, error handling, and invariants. Language-agnostic. Precise enough for the AI to implement without clarification.
4. Submit Spec to AI - open a terminal in the component directory and run claude to start a Claude Code session. Paste or reference the Spec.md content as your prompt - for example: implement this component following Spec.md. Claude Code reads the spec, creates or edits the source file in place, and reports what it did. No copy-paste of generated code is needed; the file is written directly.
5. Review and Refine - read the generated code against the spec. If behavior is wrong or incomplete, update the spec first, then regenerate. The spec is always authoritative.
6. Record in Notes.md - save the conversation. This creates the development record: which prompts produced which code, and what was refined.
7. Repeat for each language - the same Spec.md generates Rust, C++, C#, and Python implementations. Language idioms differ; the design does not.

7.0 - One Spec, Four Languages

The most visible outcome of spec-driven development here is that the same Constitution.md and component Spec.md files produced four independent implementations that behave identically from the user's perspective. TextFinder code metrics across implementations: Performance varies (Python slowest, Rust/C++ fastest), but the CLI options, component boundaries, and output format are identical across all four. PageValidator code metrics: The eight validation rules and the output format are identical in every language.

8.0 - Regeneration from Specs

TextFinder includes generate_part.py scripts in each language variant. A command like:
  python generate_part.py CommandLine
reads the corresponding Spec.md, submits it to the Claude API, and writes the generated source file. This means:

9.0 - Takeaways

These projects demonstrate several lessons about spec-driven development with AI tools: