A requirement is a statement about how the software should behave.
Validators are how SLCR tests that statement. Each validator runs a tool,
such as a test runner, a linter, or a script, and reports green (pass) or red
(fail). slcr check runs every validator and combines the results through
the spec's links into a status for
each requirement. This is how SLCR detects drift: a failing validator points
to the specific requirement the code no longer meets.
The path from a requirement to a result has five parts:
- A requirement states what must hold.
- Its acceptance criteria describe what "done" looks like.
- Validators each name a requirement, optionally one of its criteria, and the tool that checks it.
- A tool descriptor describes how to run that tool.
slcr checkruns the tools and reports which requirements are satisfied.
Acceptance criteriaLink to this section
An acceptance criterion describes, for one requirement, something that could be checked to show the requirement holds:
acceptanceCriteria:
- kind: acceptanceCriterion
id: AC-001
title: Table schema is declared
body: >-
A table named `todo_list_item` is declared with a string column `title`
and a boolean column `done`. A structural match against the schema or
migration source is sufficient.Criteria are usually written before the code exists, so they describe behavior rather than naming files, functions, or code patterns. How the behavior is checked is left to validators. A criterion asserts nothing on its own: the requirement is the claim, and validators provide the evidence. Criteria are optional, but they make it clearer what a validator should check.
ValidatorsLink to this section
A validator is an entry in a validator file, which sits in the directory of the code it checks:
# src/views/items/.validators.slcr.toml
formatVersion = "1"
[[validator]]
requirement = "todo-api/REQ-002"
criterion = "AC-004"
tool = "ast-grep"
description = "POST handler is defined and registered with the router"
with = { rule = "rules/post-handler.yml" }
[[validator]]
requirement = "todo-api/REQ-002"
tool = "cargo-nextest"
with = { test = "items::post::creates_item" }Each validator names the requirement it checks (as a qualified ID),
optionally the criterion it tests, the tool that runs the check, and the
tool's settings under with. A directory holds at most one validator file.
Because the file sits beside the code it checks, it moves when that code
moves.
The dependency runs one wayLink to this section
Validators name requirements, and requirements never name validators. The spec states what must be true and stays readable for people who never look at the code. Implementation detail, such as test names, file paths, and tool settings, lives with the code. Tests can be rewritten without changing the spec, and the spec can be edited without knowing how it's tested.
Several validators per criterionLink to this section
Turning an acceptance criterion into validators is called materializing
it. One criterion may need several validators. For example, the same
authentication check might need to run against handlers in two directories.
A requirement's own validators combine with AND: every one must pass for
the requirement to be satisfied.
A validator only provides evidence if it can fail. This is why the usual workflow is red-green: write the validator, see it fail before the code exists, then implement until it passes.
Tools and descriptorsLink to this section
SLCR has no built-in tools. Each project declares the programs its
validators may run in SLCR.toml, and a tool descriptor describes how to
run each one. A descriptor is a small, declarative TOML file. It is never
executed itself. It specifies:
- a schema for the validator's
withsettings, so a mistake in a validator is caught before anything runs - the command to run, with placeholders filled from the validator
- the working directory, and optionally an environment and timeout
- which exit codes count as a pass
formatVersion = "1"
name = "cargo-nextest"
version = "1.0.0"
[validator.schema]
type = "object"
required = ["test"]
additionalProperties = false
properties.test = { type = "string", minLength = 1 }
[invoke]
argv = ["cargo", "nextest", "run", "--no-fail-fast", "--no-tests=fail",
"-E", "test(={with.test})"]
cwd = { nearest = "Cargo.toml" }
[results]
pass = [0]A descriptor can wrap any program, including a short script committed to the repository, so validators can check anything a program can check: tests, code structure, configuration, or generated files. In the current version, a validator's result comes only from the tool's exit code.
PinsLink to this section
When a tool is declared, SLCR.toml records where its descriptor came from
and a pin: a cryptographic hash (BLAKE3) of the descriptor's exact
contents. slcr refuses to use a descriptor whose contents don't match its
pin. This means the commands that run on any machine are exactly the ones
recorded, and reviewed, in the project's history.
Vet and checkLink to this section
vet and check answer two different questions:
slcr vetasks whether the project is well formed: specs, validator files, the manifest, and installed descriptors. It finds broken links, cycles, duplicate IDs, and validators whose settings don't match their tool's schema. It runs nothing.slcr checkasks whether the validators pass. It vets the project first and runs nothing if vetting fails. It then runs every validator and reports each requirement's status. In the current version, every run ofcheckreruns every validator.
Each validator produces one of three results:
| Result | Meaning |
|---|---|
| Green | The tool ran and passed. |
| Red | The tool ran and failed. Either the code or the validator needs to change. |
| Could not be run | The tool couldn't start, for example because it isn't installed. This proves nothing either way, so check fails. |
Running slcr check runs the commands a project's descriptors declare, the
same way running a test suite runs its tests. It carries the same trust
considerations as running any project's tests.