Spec Lens
VSCode extension for spec-driven development on any repo (brownfield-first):
- Reverse Spec — generate specs from existing code
- Draft Spec — author new specs with LLM assist
- App Map — interactive app launch (named with the file it starts in, e.g.
lib/main.dart) → app init → router → screens → navigation → API-call map
See SPEC.md in the repository for the full implementation plan. Work is phased;
current status is tracked in CHANGELOG.md. docs/FAQ.md answers the common
questions — when the LLM is called, what lives in .speclens/, and how the
cache works.
Supported frameworks
The analyzer picks the first matching framework adapter (all analysis is
static and local — zero LLM calls):
| Framework |
Detected by |
Graph contents |
| Flutter/Dart |
pubspec.yaml with flutter: |
screens (GoRouter/Navigator), navigations, Dio/Retrofit/http calls, models, state deps, main() startup steps |
| Angular |
angular.json or @angular/core dep |
routed @Components, routerLink/router.navigate edges, HttpClient calls, main.ts startup steps |
| React / Next.js |
react/next in package.json |
App & Pages Router pages, React Router routes, <Link>/router.push/navigate edges, fetch/axios calls, Next API routes as server endpoints, entry-module startup steps |
| Go |
go.mod |
server endpoints (net/http incl. 1.22 patterns, gin, echo, chi, gorilla/mux, fiber) with request/response structs, outgoing HTTP calls |
| Java |
pom.xml / build.gradle |
Spring MVC & JAX-RS endpoints with @RequestBody/return-type models, RestTemplate/WebClient/Feign outgoing calls |
Backend workspaces (Go, Java) have no screens — the App Map is empty but the
API list and API specs work on the endpoint surface.
Monorepos: detection scans subdirectories (two levels deep) too, so a repo
with web/ (React) + server/ (Go) analyzes both and merges them into one
graph. A frontend call and the backend endpoint that implements it become one
node.
LLM providers
Configured via specLens.llm.provider:
| Provider |
Setup |
vscode-lm (default) |
None — uses your Copilot models via the VSCode Language Model API |
anthropic |
Run Spec Lens: Set API Key |
ollama |
Local Ollama; set specLens.ollama.baseUrl if not on :11434 |
custom |
Any OpenAI-compatible endpoint: specLens.custom.baseUrl (+ headers / API key) |
Pick the model with Spec Lens: Select LLM Model — it lists what is
actually available on this machine (live Copilot models for vscode-lm,
installed Ollama tags) rather than the static dropdown in Settings, and writes
the choice to user settings.
Semantic search embeddings (specLens.embeddings.provider) support ollama
and custom only; with none search is lexical-only.
All LLM responses are cached by content hash under .speclens/cache/ in the
target repo — identical inputs never call the LLM twice.
Using it
Run Spec Lens: Get Started — it initializes .speclens/, analyzes the
workspace and opens the App Map in one go. (Opening a supported repo that has
never been initialized offers this automatically, and a 5-step walkthrough
lives on VSCode's Welcome page.) After that you mostly do not touch the
command palette — the extension comes to the code you have open:
- CodeLens on screen classes — each screen shows its linked spec and that
spec's status, with
Show in Map next to it, or Generate spec when the
screen has none. Lenses read specs from disk, so scrolling never costs an LLM
call.
- CodeLens on the widgets a screen is built from — the
_OrderSummary
three hundred lines into a screen file gets its own Generate component spec,
right on the declaration.
- Problems panel — a static lint runs as you open or save a spec:
requirements that name no trigger or result, umbrella words that hide the
conditions they stand for, missing acceptance criteria, approved specs with
open questions still in them.
Commands
- Spec Lens: Get Started — initialize + analyze + open the App Map, in one command
- Spec Lens: Initialize — create
.speclens/ (specs/, cache/, README) in the workspace
- Spec Lens: Analyze Workspace — build the app graph (screens, navigation, API calls)
- Spec Lens: Open App Map — the interactive map
- Spec Lens: Open Component List — the widgets a screen is built from, and their specs
- Spec Lens: Search — QuickPick over screens, endpoints, specs, files
- Spec Lens: New Spec — author a spec for code that does not exist yet
- Spec Lens: New Component Spec — design a widget first: its props, states and mockup
- Spec Lens: Generate Specs for All Screens — bulk reverse-spec (confirms the call count first)
- Spec Lens: Approve Spec — mark the open spec approved
- Spec Lens: Set API Key — store the key in VSCode SecretStorage
- Spec Lens: Select LLM Model — pick
specLens.llm.model from the models this machine actually has
- Spec Lens: Test LLM Provider — one-shot round-trip against the configured provider
What a spec looks like
Specs are markdown + frontmatter under .speclens/specs/. A spec makes a claim
or records that nobody decided, and the section says which: ## Requirements is
what the code (or the author) settles, ## Open Questions is what nobody has
decided yet. Requirements read WHEN <trigger> THE SYSTEM SHALL <result> and are
numbered on write (R1, AC-1), so a review can name a line instead of
quoting it. They sit in a | ID | Requirement | Component | table, split into
feature blocks the spec names itself — ### F1 — Load products,
### F2 — Cart — so "does the cart work?" is one block to read rather than a
whole list to scan, with the fetch, the taps and the errors of that one feature
together instead of scattered.
A screen spec draws the screen as well as describing it: ## Component Tree is
the widgets nested as the build method nests them (each with the C<n> handle
the tables point at), ## Navigation is one row per way out, and
## Implementation Notes says where in the source each behavior lives.
## Acceptance Criteria is a table too, and its Covers column names the
requirements each scenario proves — a requirement nothing covers is one nobody
has said how to verify. ## Non-Goals keeps generation inside the fence;
## Technical Requirements holds the how — the library, endpoint or budget this
feature is fixed to, plus the non-functional bar — and is what "Generate code"
reads and cannot guess.
There are four kinds, each in its own folder under .speclens/:
| Kind |
Folder |
Subject |
REQ- |
specs/ |
one screen |
API- |
apis/ |
one endpoint |
LIB- |
modules/ |
one shared helper/util file (or the app's startup) |
UI- |
components/ |
one widget/component a screen is built from |
A component spec keys on the file and the declaration in it, because one
file routinely holds a widget and the smaller ones it composes. It can be
generated from a declaration already on disk, or designed before one exists —
New Component Spec takes a path, a name, a brief and/or a mockup, plus the
props and states you have already decided. It is written for the programmer
placing the widget, not for a product owner: ## Props is a table
(Prop | Type | Required | Default | Notes, callbacks included) and
## States is a table of every state, variant and mode with the condition that
selects each one — loading, empty, error, disabled, the variants a screen spec
flattens away — with the count in its heading. ## Behavior is
<trigger> → <result> lines grouped into Rendering / Interaction / Edge Cases,
and ## Composition, ## Design Tokens and ## Accessibility are written only
when there is something real for them. The list offers the widgets that own a
file but were never extracted for sharing. A widget two or more screens place is
design-system code and is left out; so is one written inside somebody else's
file — a screen's, or the exported component the file is named for — because
that is part of its composition, and its spec documents it. Extract a
sub-widget into a file and it gets a row.
Repo-wide rules no single spec should repeat — architecture, state management,
error handling, testing — live in .speclens/constitution.md and ride along
with every "Generate code" prompt.
Development
npm install
npm run typecheck
npm test # vitest: cache + provider tests against mock servers
npm run build # esbuild bundle → dist/extension.js
Launch with F5 (Extension Development Host) after npm run build.