Skip to content

Development

Everything you need to clone and run specmap locally.

Prerequisites

Tool Purpose Install
Python 3.11+ Core library, MCP server, CLI, API server System package manager
uv Python package manager curl -LsSf https://astral.sh/uv/install.sh \| sh
Node.js 20+ React frontend System package manager
just Task runner cargo install just or system packages
git Version control System package manager

Clone and Install

git clone https://github.com/jdraines/specmap.git
cd specmap
just install         # Python deps (core library + MCP server + CLI + API server + test deps)
just web-install     # Node deps (React frontend)

Running the Web UI

The web UI needs a forge token to fetch repo data on your behalf. Specmap auto-detects the forge provider (GitHub or GitLab) from your git remote origin.

1. Set a personal access token

GitHub -- use any of these methods (checked in order):

  1. Set GITHUB_TOKEN (or GH_TOKEN) environment variable
  2. Have gh CLI authenticated (gh auth login) -- specmap falls back to gh auth token

The token needs repo scope for private repositories, or no scope for public-only.

GitLab -- use any of these methods (checked in order):

  1. Set GITLAB_TOKEN environment variable
  2. Have glab CLI authenticated -- specmap falls back to glab config get token

The token needs read_api and read_repository scopes.

2. Configure environment (optional)

cp .env.example .env

For PAT mode (the default), the only thing you might need to set is CORS_ORIGIN:

CORS_ORIGIN=http://localhost:5173

Session secrets are auto-generated if not provided.

OAuth mode (enterprise)

If your organization restricts PATs, you can configure OAuth instead:

GitHub -- Go to github.com/settings/developers > OAuth Apps > New OAuth App:

Field Value
Application name Specmap (dev)
Homepage URL http://localhost:8080
Authorization callback URL http://localhost:8080/api/v1/auth/callback/github

GitLab -- Go to your GitLab instance > Preferences > Applications > New Application:

Field Value
Name Specmap (dev)
Redirect URI http://localhost:8080/api/v1/auth/callback/gitlab
Scopes read_api, read_repository

Then set the client credentials in .env:

GITHUB_CLIENT_ID=<from step above>
GITHUB_CLIENT_SECRET=<from step above>
# or for GitLab:
GITLAB_CLIENT_ID=<from step above>
GITLAB_CLIENT_SECRET=<from step above>

3. Start services

just dev    # Runs API server + Vite dev server in one command

This starts both processes in parallel (API on :8080 with auto-reload, Vite on :5173). Open http://localhost:5173. In PAT mode with a valid token, the dashboard loads immediately. In OAuth mode, click the sign-in button.

You can also run them in separate terminals if you prefer:

# Terminal 1: Python API server
just serve         # or: just serve-dev (auto-reload)

# Terminal 2: React frontend
just web-dev

Project Structure

specmap/
├── src/specmap/               Python: core library, MCP server, CLI, API server
│   ├── indexer/               Diff analysis, annotation engine, diff optimizer
│   ├── state/                 Models, specmap file I/O
│   ├── llm/                   LLM client, prompts, agents, schemas
│   │   ├── client.py          litellm wrapper with retry & token tracking
│   │   ├── prompts.py         Annotation generation prompts
│   │   ├── schemas.py         Annotation response schemas
│   │   ├── chat_agent.py      Pydantic AI chat agent with tools
│   │   ├── code_review_agent.py  Three-phase code review pipeline
│   │   ├── walkthrough_*.py   Walkthrough prompts and schemas
│   │   └── code_review_*.py   Code review prompts and schemas
│   ├── tools/                 MCP tool implementations
│   ├── mcp/                   MCP server entrypoint
│   ├── cli/                   Typer CLI entrypoint + commands
│   ├── server/                FastAPI server (auth, forge API, generation, SQLite)
│   ├── config.py              Two-layer TOML config system
│   └── _static/               Bundled frontend (populated by build)
├── web/                       React frontend (Vite + Tailwind)
│   ├── src/
│   │   ├── api/               TypeScript API client + SSE streaming
│   │   ├── stores/            Zustand state management
│   │   ├── hooks/             Custom React hooks
│   │   ├── components/        React components (diff, review, walkthrough, etc.)
│   │   └── pages/             Route pages (Login, Dashboard, PRReview)
│   └── vite.config.ts
├── tests/                     Test suite
│   ├── unit/                  Unit tests (pytest)
│   └── scenarios/             Functional end-to-end test scenarios
├── docs/                      MkDocs documentation
├── justfile                   Task runner
└── mkdocs.yml

Running Tests

Unit Tests

Focused on individual components -- annotation engine, diff optimizer, models, file I/O, code analyzer.

just test

Run with coverage:

just test-cov

Functional Tests

End-to-end scenarios that exercise real spec-driven workflows: annotating code with spec references, verifying annotations, and validating via CLI -- all with deterministic LLM mocks.

just functional-test           # All scenarios (~5s)
just functional-test-fast      # Skip @slow tests
just functional-test -v        # Verbose per-test output
just functional-test -x        # Stop on first failure

The functional tests create temporary git repos, mock litellm.acompletion, call MCP tools, and run the CLI -- verifying the full pipeline end-to-end.

All Tests

just test-all   # Unit + functional tests

Linting and Formatting

just lint-py       # ruff check
just fmt           # ruff format
just web-typecheck # tsc --noEmit
just lint          # All lints (ruff + tsc)

Documentation

Docs use MkDocs Material with mike for versioning.

just docs-serve     # Live-reload dev server at localhost:8000
just docs-build     # Build static site (strict mode)

Functional Test Architecture

Multi-Layer Harness

tests/
├── conftest.py              # Session fixtures: CLI runner, LLM mock, temp repos
├── harness/
│   ├── repo.py              # GitRepo: temp repos, file ops, git ops
│   ├── llm_mock.py          # Mock litellm.acompletion + response builders
│   ├── cli.py               # Run Python CLI via subprocess
│   ├── assertions.py        # Domain-specific assertion helpers
│   ├── spec_content.py      # Reusable spec markdown constants
│   └── code_content.py      # Reusable code file constants
└── scenarios/
    ├── test_greenfield.py       # New repo: spec + code -> annotate -> validate
    ├── test_iterative.py        # Edit code -> re-annotate with incremental diff
    ├── test_cross_component.py  # Cross-component annotation scenarios
    ├── test_branch.py           # Feature branches, cumulative diffs
    ├── test_config.py           # Custom patterns, env vars, ignore rules
    └── test_errors.py           # Empty repos, missing files, unicode, deep headings

LLM Mock Strategy

Tests mock litellm.acompletion -- not LLMClient.complete -- so the full LLM client stack is exercised: retry logic, JSON parsing, Pydantic validation, and token tracking.

The LLMMockRegistry dispatches responses by call type using matcher functions:

# Register a mock response for annotation calls
annotation_resp = build_annotation_response(
    file="auth/session.go",
    start_line=15, end_line=42,
    description="Implements JWT session tokens [1].",
    refs=[build_spec_ref(1, "docs/auth-spec.md", "Authentication > Token Storage", 5)],
)
llm_mock.on_annotate(AnnotationResponse(annotations=[annotation_resp]))

Scenario Repo Setup

Each test gets a fresh temporary git repo with main and feature/test branches. Spec files are committed to main and merged into the feature branch so they exist in the working tree but don't appear in git diff main...HEAD:

def setup_spec_on_main(repo, spec_path, content):
    repo.git_checkout("main")
    repo.write_file(spec_path, content)
    repo.git_add(spec_path)
    repo.git_commit(f"Add {spec_path}")
    repo.git_checkout("feature/test")
    repo.git_merge("main")

Troubleshooting

"CORS error in browser console" -- Check that CORS_ORIGIN in .env matches the Vite dev server URL exactly (http://localhost:5173).

"Empty annotations on PR page" -- The .specmap/{branch}.json file must be committed and pushed to the PR branch. The API fetches it from the forge at the PR's head SHA.

"OAuth callback error" -- Verify the callback URL in your OAuth App settings matches BASE_URL + /api/v1/auth/callback/{provider} exactly.

"No token found" -- In PAT mode, ensure your token is set via env var or CLI tool. You can also enter it manually in the web UI login page.