Skip to content

Repository files navigation

📐 dspec

Claude Code, anchored

Build with Claude. Your product model keeps up — as markdown in the repo, bound to real symbols in your code, and measured against them.

License: MIT Node Dependencies Claude Code


ds sync reconciles both ways: a spec whose code moved on without it, and code the model never described

ds sync reconciles both directions — a spec its code has moved past, and code nothing in the model describes.
In a Claude Code session, the same thing is /ds:sync.


> add a discount to checkout

Claude edits src/billing/legacy.ts — a file deleted three weeks ago. CLAUDE.md still describes it, so Claude believed it. You find out in review, or you don't.

CLAUDE.md is what carries knowledge across an empty context window, and nothing checks that it is still true. dspec makes that checkable — in both directions:

/ds:sync

- [spec older than code] Checkout / Apply discount
    the code changed after this spec was written

Code the model does not describe — 2 symbols in 1 area:
  billing  2/3
    stackCoupons, expireCoupon

The first half is the one every drift tool has. The second half is the one that matters after you have built something: not "which spec has gone stale", but "what did we just ship that nothing describes". Without it, a model can only ever rot politely.

That is also why you do not write the spec first. Claude builds; /ds:sync asks what changed and what is new; you decide which of it is real product behaviour worth writing down. .ds/ ends up describing what the product is, never what somebody once proposed.


What the model looks like

One markdown file per behaviour, in .ds/. /ds:sync and /ds:bootstrap are what write these — you read and correct them, which is why the format is small enough to review at a glance:

---
name: Cancel order
kind: write                       # write = changes state · read = query only
actors: [Customer]
codeRef:
  file: src/order/cancel.ts       # ← the binding that makes drift measurable
  symbol: cancelOrder
  verifiedBy: [test/cancel.spec.ts]
---

Rules
- Only a pending @Order may be cancelled
- Cancelling is final — a cancelled @Order never returns to pending

Errors
- ALREADY_SHIPPED when the order has left the warehouse

Five labels and nothing else — Rules · Input · Errors · Effects · Visibility. @Order links to an entity; those links are the only edges, and the map between contexts is derived from them.

The filter for what belongs here:

If one read of the file codeRef points at would tell you, it is not worth a line. Write what that read would not tell you.

codeRef is what turns this from documentation into a measurement. dspec fingerprints the symbol it names, so "has this gone stale?" is answered by reading your checkout — not by anyone remembering.


What Claude gets

Every session opens knowing where the model and the code stand. Before you type anything:

This repository is modelled in DSpec at `.ds/`, and the model is the source of truth
for domain names, business rules and flows. `CLAUDE.md` is generated from it.

Outstanding right now:
- 2 spec files changed but not committed
- thin spec: Ordering / Cancel order

Do not invent behaviour the model does not describe — read the spec file for it, or ask.

Editing a file surfaces the spec that governs it. Claude is told, mid-session, which usecase binds to the file it just touched — and that leaving the model describing behaviour the code no longer has is a finding, not a detail.

Asking about a piece of work produces a scoped brief. ds pack "<what you are doing>" finds the elements involved and prints one ring of context around them, plus:

## Code Map
  Files bound to the entities in this task. Files not listed here
  are unaffected — do not modify them.
  - src/order/cancel.ts
  - src/order/order.entity.ts

One ring of context — what your usecase mentions and what mentions it — not the whole model, so the brief stays small as the model grows. It also carries a ⚠ block naming every spec in that scope that is thin, unproven, or older than its code: the places Claude must ask rather than infer.

Install

Two steps, in this order. Type both inside Claude Code:

/plugin marketplace add tuna781/dspec     # 1. register the marketplace
/plugin install ds@ds                      # 2. install the plugin from it

Important

Step 2 on its own fails with marketplace ds not found. ds@ds reads as <plugin>@<marketplace> — the second ds names the marketplace, which does not exist until step 1 registers it. Both halves really are called ds: the repository is tuna781/dspec, the marketplace it publishes is named ds.

Requires Claude Code, Node ≥ 20 and git. dspec is written in JavaScript, and Claude Code runs it by starting a process — so every slash command is really node "${CLAUDE_PLUGIN_ROOT}/bin/ds.js" …. You never type that; the command carries it. Node has to be there the same way git does.

dspec itself installs nothing. No npm package, no global binary, nothing of ours on your PATH. The CLI, the commands, the skill and the hooks all ship inside the plugin, so the hook can never be a different version from the CLI it calls — and taking a new release is /plugin update ds and nothing else.

Update: /plugin marketplace update, then /plugin update ds. Check it with /ds:doctor.


The loop

Build, then sync. A feature starts as a description, not as a spec file — and .ds/ is written once the code exists, so the model only ever describes what the product is.

1. /ds:bootstrap — set the repo up and draft a model from the code already here
2. /ds:spec {what you want} — a detailed statement of it, checked against the model, for you to approve
3. /ds:plan {what you want} — the implementation plan, then the implementation
4. /ds:sync — reconcile the model with what you just built

Step 2 is the one that earns its place. Describing a feature in chat gives you a description that may contradict a rule you already have, and you find out after the code is written. /ds:spec reads the model first, writes inside the rules that already exist, and says which ones your request conflicts with — while it is still a sentence, not a branch.

Important

Only /ds:sync and /ds:bootstrap write to .ds/. /ds:spec ships without file-writing tools at all, so it cannot leave the model describing something that does not exist yet.

You do not have to type any of it. The Stop hook notices when the code has moved past the specs bound to it and offers /ds:sync — so the loop closes even for a session that was ordinary chat from beginning to end.

/ds:bootstrap writes one thing outside .ds/, and you commit it:

// .claude/settings.json
{
  "extraKnownMarketplaces": { "ds": { "source": { "source": "github", "repo": "tuna781/dspec" } } },
  "enabledPlugins": { "ds@ds": true }
}

That is how a teammate gets all of this: they clone, trust the folder, and Claude Code enables the plugin with no prompting — no install steps for them at all.


Commands

/ds:bootstrap Set the repo up and draft a first model from the code already here
/ds:spec Describe in detail what you want to build, checked against the model
/ds:plan Plan the implementation, get it approved, build it
/ds:sync Reconcile the model with the code, and fix what is safe to fix
/ds:check Show where the model and the code disagree, changing nothing
/ds:doctor Is this installation healthy?

ds sync reconciles both directions: specs whose code moved, changed or vanished — and code that nothing in the model describes. The second half is what makes a code-first loop possible; without it, nothing could ever notice what you had just built.

--write only re-measures. It re-fingerprints every bound symbol and re-renders the artifacts; it never rewrites a spec body to agree with the code, and never deletes an element. A spec the code has overtaken is the place where the code is the unreviewed party.

Three hooks run without being asked — on session start, after an edit, and on stop. They only add context and can never block a tool call.

Everything is local: no server, no token, no network call, no telemetry, and zero runtime dependencies.


Docs

Reference File syntax, codeRef, drift kinds, lint rules, troubleshooting
Design notes Why the model has this shape, and what dspec refuses to guess
Complex domains When to split a context, shared entities vs. one drawn twice, worked at scale
Contributing Snapshots, the committed dist-plugin/, releasing
Security What it reads, what it writes, what it never does

License

MIT — see LICENSE.

About

Spec-driven development for Claude Code. Your product model lives as markdown in the repo, binds to real symbols in your code, and ds check fails when the two disagree. No server, no token, no network.

Topics

Resources

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages