This walkthrough takes you from an empty directory to a validated project with coverage measured. Every command and output shown here was produced by the real tool.
xspec requires Node.js ≥ 22. It's published on npm as @modularcloud/xspec. Install it into your project as a development dependency under the name xspec:
npm install --save-dev xspec@npm:@modularcloud/xspec
npx xspec versionThe alias does two things. It installs the package as xspec, the module name that xspec.config.ts imports defineConfig from (see below), so your editor gets type support. It also puts the xspec command in node_modules/.bin.
Two other ways to run it:
- Globally:
npm install -g @modularcloud/xspec. - Once, without installing:
npx @modularcloud/xspec <command>.
The rest of the documentation writes xspec <command>. With a local install, run it as npx xspec <command> or from an npm script.
To work on xspec itself, build it from a checkout instead: clone https://github.com/modularcloud/xspec, then run npm ci && npm run build && npm link.
An xspec project is any directory with an xspec.config.ts at its root. The config names your spec files and, optionally, the code that uses them:
// xspec.config.ts
import { defineConfig } from "xspec"
export default defineConfig({
specs: {
product: ["specs/**/*.mdx"]
},
code: {
app: ["src/**/*.ts"],
tests: ["test/**/*.ts"]
},
markdown: { emit: true },
coverage: [
{
name: "tested",
target: "product",
boundary: "tests",
mode: "direct"
}
]
})Two things to know about this file:
- xspec parses it statically and never executes it, so it must be purely declarative (literals only). See Configuration for the full schema.
- The
import { defineConfig } from "xspec"exists for editor type support. xspec itself never resolves it, so the CLI works whether or not the import resolves. If your owntscrun includesxspec.config.ts, install the package under thexspecalias as above, or exclude the config file from your tsconfig.
Spec files are MDX documents in which requirement sections are wrapped in <S> tags. Each section carries a structural dot-path id. Create specs/AUTH.mdx:
<S id="auth">
Authentication.
<S id="auth.login">
Users sign in with an email address and a password.
<S id="auth.login.valid" tags="happy-path">
A user submitting valid credentials is signed in.
</S>
<S id="auth.login.invalid" tags="negative">
Invalid credentials are rejected with a generic error message.
</S>
</S>
<S id="auth.lockout" tags="negative temporal">
Five consecutive failed attempts lock the account for 15 minutes.
</S>
</S>The nesting is the structure: auth.login.valid must be a child of auth.login, which must be a child of auth. See Writing specs for the complete syntax.
$ xspec build
$ echo $?
0build validates the sources and generates everything (see Workspace files for details):
specs/AUTH.mdx ← your source
specs/AUTH.md ← pure Markdown (annotations stripped), because markdown.emit
specs/AUTH.xspec.ts ← generated typed module, runtime included
specs/AUTH.xspec.impl.d.ts ← companions of the generated module (types, go-to-definition)
specs/AUTH.xspec.impl.d.ts.map
.xspec/ ← graph data: the project graph
A failed build (invalid sources, exit 1) modifies nothing: derived files stay exactly as they were.
Code declares which requirements it implements or exercises by importing the generated module:
// src/auth.ts
import AUTH from "../specs/AUTH.xspec"
export function login(email: string, password: string): boolean {
AUTH.auth.login.valid // dependency marker → "references" edge
return email.includes("@") && password.length > 0
}// test/auth.test.ts
import AUTH, { text } from "../specs/AUTH.xspec"
export function testValidLogin(): string {
AUTH.auth.login.valid // marker: this test exercises that requirement
return text(AUTH.auth.login.valid) // the requirement text, as a string
}
export function testInvalidLogin(): void {
AUTH.auth.login.invalid
}A marker is an ordinary property read at runtime (harmless, no tooling required) and a type-checked reference at compile time. If the requirement is renamed or deleted, your build breaks instead of silently drifting. Setup details and rules are in Using specs from TypeScript.
Re-run xspec build after adding code references so the graph includes them.
List every requirement:
$ xspec ids --tree
specs/AUTH.mdx
auth
auth.login
auth.login.valid
auth.login.invalid
auth.lockoutShow one requirement, with its text, tags, hashes, and every edge in and out:
$ xspec show "specs/AUTH.mdx#auth.login.valid"
specs/AUTH.mdx#auth.login.valid
source range: bytes 104-202
tags: happy-path
coverage: required
hashes:
ownHash: b235263b…
subtreeHash: bc3ccfe3…
effectiveHash: 08b71bba…
metadataHash: d2d935c3…
edges:
incoming:
contains from specs/AUTH.mdx#auth.login
references from src/auth.ts#login
embeds from test/auth.test.ts#testValidLogin
references from test/auth.test.ts#testValidLogin
outgoing:
own text:
A user submitting valid credentials is signed in.
subtree text:
A user submitting valid credentials is signed in.Find every place that references it, with exact byte positions:
$ xspec occurrences --to "specs/AUTH.mdx#auth.login.valid"xspec query is the machine-facing equivalent of show and ids; see the CLI reference. occurrences, view, at, and inventory serve editors and other tools; see Machine interface. All of them print JSON.
The tested profile above asks: which product requirements are directly referenced by anything in the tests code group?
$ xspec coverage
profile tested
required: 3, covered: 2, uncovered: 1, ignored: 3
covered:
specs/AUTH.mdx#auth.login.valid
path: test/auth.test.ts#testValidLogin -> specs/AUTH.mdx#auth.login.valid
specs/AUTH.mdx#auth.login.invalid
path: test/auth.test.ts#testInvalidLogin -> specs/AUTH.mdx#auth.login.invalid
uncovered:
specs/AUTH.mdx#auth.lockout
ignored:
specs/AUTH.mdx: root node; non-leaf under targets: "leaves"
specs/AUTH.mdx#auth: non-leaf under targets: "leaves"
specs/AUTH.mdx#auth.login: non-leaf under targets: "leaves"auth.lockout has no test yet. While that's true, xspec coverage tested --check exits 1, so wire it into CI to keep specs and tests honest. Details are in Coverage.
$ xspec check
$ echo $?
0check runs every build validation without writing anything. That covers references, cycles, and the journal. It additionally verifies that generated files and graph data are up to date, policy rules hold, and review sessions are intact. Any finding exits 1. Each finding names its location and stable code, with an actionable message:
$ echo "// tampered" >> specs/AUTH.xspec.ts && xspec check
specs/AUTH.xspec.ts: stale-output: stale generated output: specs/AUTH.xspec.ts does not match what the current sources and configuration generate; run `xspec build` to regenerate every derived file (SPEC 14.10)
1 findingxspec build && xspec check is the everyday loop; check alone is the CI gate.
The other pages extend this project with one more spec file, specs/OVERVIEW.mdx, which depends on the auth section and is excluded from coverage:
import AUTH from "./AUTH.xspec"
<S id="overview" coverage="none" d={AUTH.auth}>
The product summary, for context.
</S>- Track what a spec edit affects: Impact analysis
- Turn changes into a reviewable checklist: Reviews
- Restructure specs without losing history, with a preview first: Renaming and moving
- Commit the right files: Workspace files
- Build an editor integration or other tooling: Machine interface