Type to search pages and rules.

Browse pages

How LiteSpec Works

LiteSpec connects what a team writes down about a product with the code that implements it, and checks that the two still agree. This page shows that connection on Claimwick, the example product in examples/claimwick/ where employees submit work expenses with receipts, following the Submit expense use case from its written record to the code that implements it and the check that confirms they match.

Markdown

The Parts#

LiteSpec has four parts. Each one answers a different question.

PartWhat it isThe question it answers
The standardThe pages of one LiteSpec release, carried by the installed toolHow do we build?
RecordsShort YAML record files in your project, each with an optional Markdown explanation beside itWhat do we build, and why?
The modelWhat litespec model check and litespec model query compute from your records and the marks in code and testsDoes what we built match what we wrote?
The toolsThe litespec command lineIs everything consistent, and what is missing?

Your project installs one LiteSpec release as a local .NET tool. The installed release never changes under you. When you want a newer one, you move the version.

The diagram below shows how the four parts connect, with each layer written from the layer above it and the checks sending their findings back to the records.

The loop of standard, records, code, and model Four boxes run left to right: Standard (rules, pinned), Records (intent, owned), Code and tests (handlers, screens), and Model (in memory). The standard governs the records, the records are implemented by code, and the build reads code into the model. A Checks box below (consumer, model check, CI) takes gaps from the model and sends findings back to the records. Standard Records Code and tests Model Checks rules, pinned intent, owned handlers, screens in memory consumer, model check, CI governs implements build reads gaps findings An arrow is the next step of the loop; the checks send their findings back to the records.
The standard governs the records, the records are implemented by code and tests that point up to them, the build joins records to code in the model, and the checks send findings back to the records.

A Use Case from Record to Code#

Claimwick lets employees submit work expenses with receipts. In LiteSpec, that behavior is a use case: one command or query with its access, results, and scenarios. Reactions that answer events, jobs that run on a schedule or by hand, person-facing messages, and tunables are their own record types beside it.

The Written Record#

The record file lives in the project at .litespec/behavior/modules/spending/expenses/submit-expense.yaml. It states what the use case is and how it connects to other records:

YAML
type: use-case
id: use-case/spending.submit-expense
title: Submit expense
kind: command
summary: An employee submits a work expense.
serves:
  - journey/submit-and-reimburse#s1
fulfills:
  - requirement/approval-reminders.receipt-over-threshold
access:
  grant: permission/submit-expense
  tenant: own
reachedThrough:
  - entry-point/claimwick-app
changes:
  - aggregate/spending.expense
reads:
  - aggregate/spending.policy
consistency:
  mode: atomic
raises:
  - event/expense.submitted

Read it as a set of facts: submitting an expense is a command, a message that changes business state; it serves the first step of the submit-and-reimburse journey and fulfills the requirement that an expense above the receipt threshold carries a receipt; the grant names the submit-expense permission from the shared list, and the tenant scope stays inside one tenant; employees reach it through the expenses app, whose door declares session authentication; it changes the expense aggregate, reads the policy store for the threshold, and commits atomically; and it raises an expense.submitted event, one statement that the submission completed. The summary field states what the use case is in one paragraph: "An employee submits a work expense."

Each result is an entry in records: one way the use case ends. Refusals are listed in precedence order, first match wins, and each refusal names the stable code the API returns:

YAML
type: result
id: result/submit-expense.receipt-missing
outcome: refusal
code: spending.receipt-missing
meaning: The expense is above the receipt threshold and carries no receipt.

Two generic refusals are derived and never written: a use case whose grant is a permission derives a forbidden result, and a use case with rules on its input derives an invalid-input result. Their codes come from the refusals block in .litespec/project.json.

A record in git is decided. It carries no status, no owner, and no dates of its own history: git shows who changed what and when, and the pull request that changes a record is its review. A record no longer wanted is deleted, and git keeps its history.

The use case also carries a scenario: one worked example that leads from a given situation through an action to one named result.

YAML
type: scenario
id: scenario/submit-expense.submitting-with-a-receipt-succeeds
given: An employee paid for a train ticket and kept the receipt.
when: The employee submits the expense with the receipt.
result: result/submit-expense.submitted
then: The expense is submitted.

Read it as one proven path: given the situation, when the employee acts, the use case reaches the submitted result, and the expense is submitted. Each scenario names exactly one result, so a later test knows which ending it proves.

The Implementation#

The standard states where the code goes and what shape it takes. A use case is implemented by one command handler in the modules project, in a folder ordered by module, then aggregate, then UseCases/, then use case:

Text
apps/api/src/Claimwick.Modules/Spending/Expenses/UseCases/SubmitExpense/SubmitExpenseCommandHandler.cs

The use case folder is flat: the command, the result, the handler, the validator, the endpoint, and the request and response models sit beside each other. Each code element carries a mark naming the record it serves. The handler refuses an expense above the threshold that carries no receipt with the refusal code the result entry names. The folder mirrors the record, so any developer or agent knows where to look, and the mark is the link the model builder follows to find the handler.

Run the check from the project root:

Shell
dotnet litespec model check

It reads every record and follows the marks in the code and tests, the frontend routes, and the test runs. Then ask about one record:

Shell
dotnet litespec model query use-case/spending.submit-expense

Alongside the record's own fields, the answer holds what the builder computed from the marks:

JSON
"computed": {
  "implemented": true,
  "verified": false,
  "implements": [
    "apps/api/src/Claimwick.Modules/Spending/Expenses/UseCases/SubmitExpense/SubmitExpenseCommandHandler.cs"
  ],
  "calls": [],
  "proves": [
    {
      "scenario": "Submitting_with_a_receipt_succeedsAsync",
      "claim": "scenario/submit-expense.submitting-with-a-receipt-succeeds",
      "confirmed": true,
      "passing": null
    },
    {
      "scenario": "An_expense_is_submitted_through_a_helper_of_the_worldAsync",
      "claim": "scenario/submit-expense.submitting-with-a-receipt-succeeds",
      "confirmed": true,
      "passing": null
    }
  ]
}

The use case is implemented: the builder found its handler through its mark. Two tests claim its scenario: the test Submitting_with_a_receipt_succeedsAsync, whose when step belongs to the SubmitExpenseSteps class, and an integration test through a helper. Both claims check out, so each entry reads confirmed. The use case is still not verified: no test run exists yet, so passing is null. Nobody maintains those links by hand, and they cannot go stale, because the builder computes them again on every run. Nothing flows the other way: no record above Implementation is ever computed from code.

The Reported Gaps#

dotnet litespec model check turns gaps into findings with a level beside each one: level G checks read records only and always block, level S checks read code through a stack reader and block where that reader is qualified, and level A checks advise without blocking. A use case with no handler, a result the code never returns, a link to a record that does not exist, a journey step nothing serves, or a requirement nothing fulfills each becomes a finding. The check runs in CI, so a gap shows up in the pull request that introduced it.

For example, if the submit-expense record named no success result, dotnet litespec model check reports [G use-case-success-missing] against .litespec/behavior/modules/spending/expenses/submit-expense.yaml, and the pull request fails until the record names one.

To catch breaking changes against an older commit, such as a removed result or a changed integration event, run dotnet litespec model check --since <base>.

The Provision Identifier#

Every rule in the standard is a provision: one identified block with one assertion. Its ID joins its page and its heading. For example, the rule that every closed set of domain values is modeled without enums is litespec/backend/dotnet/domain#model-every-closed-set-of-domain-values-without-enums, on the domain page. A review comment, an agent's report, or a project exception can cite that ID, and everyone knows exactly which rule is meant.

Each provision also states its strength: rule, default, or advice. A rule binds every project it applies to. A default binds unless the project records its own choice. Advice is experience to apply with judgement, and it records nothing. Each page states the scope its provisions apply to, and a provision narrower than its page names it. Before editing a file, an agent asks which provisions apply to it:

Shell
dotnet litespec rules apps/api/src/Claimwick.Modules/Spending/Expenses/UseCases/SubmitExpense/SubmitExpenseCommandHandler.cs

The command lists the provisions grouped by strength, as one-line statements, with the project's own conventions and departures merged in.

The Specification Is Closed#

An agent builds what the specification states and nothing more. Something that seems missing becomes a question to the owner or an item on the backlog board outside the repository, never code. Ideas live on that board; what is in git is what the product wants.

Next#

Read the standard explains how the standard is organized and how to find the rules that apply to your project.