AIBites: 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 reverses the usual order: you write what the software must do before you write any code, then hand those documents to an AI to drive implementation. The 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.
Why specs before prompts?
  1. A prompt without a spec is ambiguous. A spec makes the constraints explicit and auditable before the AI writes a line of code.
  2. The spec becomes the acceptance criterion: generated code that violates the spec is wrong by definition, regardless of whether it compiles.
  3. Separating “what must be true” (Constitution) from “how it is organized” (Structure) from “what each piece does” (Spec) keeps each document small and focused.
  4. Specs are reusable - the same Constitution can govern implementations in multiple languages.

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. Written in plain imperative sentences; one page maximum. Examples: 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. The AI reads Structure.md before generating any file so it knows where each piece belongs. A minimal example:
## Package Layout
src/
  lib.rs       -- public API re-exports only
  config.rs    -- configuration types; no I/O
  scanner.rs   -- directory traversal; depends on config only
  matcher.rs   -- regex matching; depends on config only
  reporter.rs  -- output formatting; depends on scanner and matcher
  main.rs      -- entry point; depends on all others

## Rules
- scanner.rs must not import from matcher.rs
- reporter.rs must not perform I/O beyond writing to a provided Writer
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
  ├── EntryPoint/
  │   ├── 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
       \            /
        \          /
      EntryPoint

  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 documents each public function or type: struct names, method signatures, return types, preconditions, postconditions, algorithms, and invariants. The AI generates code from this spec; the spec is the source of truth. A minimal example:
## fn scan(root: &Path, config: &Config) -> Result<Vec<Match>>
- root must exist and be a directory; returns Err otherwise
- traverses root recursively, following symlinks if config.follow_symlinks
- returns all Match records where config.pattern matches file content
- excludes files whose extension is not in config.extensions
- example: scan(Path::new("src"), &cfg) -> Ok(vec![Match{path: ..., line: 3}])
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:
1. Write Constitution.md - values and constraints, no code yet. Decompose the project into components, state design principles, define the CLI. This document is shared across all language implementations unchanged.
2. Write Structure.md - package layout and dependency rules. 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 - signatures and contracts for each public item. Specify 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 - start a CLI session or API call; load all three files as context. Ask the AI to implement one file at a time, citing the spec for each function. For example: open a terminal in the component directory, run claude, then: 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 - run tests after each file; feed failures back into the session. 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. After all files pass, ask the AI to verify the implementation against Constitution.md and report any violations.
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:

10.0 - Epilogue - Connecting to the SWDev Track

Spec-driven development is the Code Track's answer to the SWDev track's design chapter: Constitution maps to architectural constraints, Structure maps to package design, and Spec maps to specification. The difference is the AI is now the implementer. For a deeper treatment of the design concepts behind these documents, see the SWDev Story: Software Design chapter.

11.0 - References

ResourceDescription
github.com/github/spec-kit Inspiration for the lightweight spec approach used here.
AIBites: Spec-Driven Development Original CodeBites page with full workflow examples.
SWDev Story: Software Design The design chapter that underpins spec-driven development.
Anthropic Docs Full API reference, model guides, and prompt engineering tips.