The anchors.yaml
anchors.yaml is where a project declares its own rules. Anchors ships no
built-in standard for how your code should be organized — it ships the
mechanism, and this file says how to apply it here.
You don’t write this file from scratch: anchors init generates it through
questions and answers, reading the project’s own folders as layers and its language as dialect — it proposes no structure.
An unknown key is an ERROR, not silence. Anchors refuses to load an
anchors.yamlwith a key it doesn’t know, naming the key and the line.The reason came from a measurement: a block written with
guide:/tags:instead offrom:/governs:was silently discarded,map buildanswered “222 nodes, 0 edges”, and work went on for a day believing the project had no relations. An error with a line number costs a minute; an empty map with no explanation costs a session.
The skeleton
Section titled “The skeleton”version: 1
layers: # WHICH files Anchors governs, and what each one isderived: # how to find a spec's code/feature/testgoverns: # which guides govern which filesboundaries: # who may import whomgates: # what gets confronted, and what blocksworkflow: # where the work lives (local or GitHub)Only version and layers are required. The rest comes in as the project needs
it.
layers — what Anchors governs
Section titled “layers — what Anchors governs”The most important block, and the only one without which nothing works. It answers: which files in this repository are governed, and what is each one.
layers: spec: pattern: '**/*.spec.md' kind: spec tags: [spec] shared: pattern: 'packages/shared/**/*.ts' kind: code tags: [shared, contract] test: pattern: '**/*.test.*' kind: test| field | decides |
|---|---|
pattern |
the glob that recognizes the layer’s files |
kind |
spec · feature · test · code · doc · guide · plan |
tags |
free labels, used by governs to target groups |
exclude |
globs to exclude (derived files matching a broad glob) |
regime |
comportamental · declarativo · misto — what that layer is expected to be |
priority |
declared tie-break when two patterns match the same file |
code_prefix |
module prefix in the identity code |
A file outside every layer is invisible to Anchors. No gate confronts it, it
doesn’t enter the map, and the pipeline certifies work nobody verified. If you
create a new config file (a stryker.config.js, say) and check complains it
“isn’t governed”, this is the block that needs to learn about it.
priority, and when you’ll need it
Section titled “priority, and when you’ll need it”When two patterns match the same file, Anchors breaks the tie by pattern length — a heuristic that measures verbosity, not precision. It has already classified wrongly: a pattern with many alternatives beat one pointing at a subset of it.
Declare priority when it gets it wrong. check tells you where it decided on
its own.
support — files of a test layer that are not tests
Section titled “support — files of a test layer that are not tests”A test layer’s pattern often catches files that serve the tests without being tests:
sub-flows other flows call (runFlow), suite aggregators, helper files. List them:
layers: e2e-flow: pattern: "apps/mobile/.maestro/**/*.yaml" kind: test support: ["apps/mobile/.maestro/utils/**", "apps/mobile/.maestro/suites/**"]They stay in the map, so a change to a helper still reaches every test that uses it. The
gates that judge a test as a test (tests-pass, test-feature-match, test-traceable)
skip them, and they don’t count as tests that name a scenario.
derived — how to find a unit’s pieces
Section titled “derived — how to find a unit’s pieces”The doctrine says code, feature and test are born from the spec. This block says where to look for them.
derived: anchor: spec files: code: '{{dir}}/{{name}}.ts' feature: '{{dir}}/{{name}}.feature' test: '{{dir}}/{{name}}.test.ts'It’s by naming convention: the spec AreaStatus.spec.md looks for an
AreaStatus.ts beside it. That’s what lets the unit be checked without anyone
declaring edges by hand.
overrides — when the convention doesn’t fit
Section titled “overrides — when the convention doesn’t fit”Config files break the convention: a spec named TypeScriptConfig.spec.md
governs tsconfig.json, tsconfig.base.json and each package’s
tsconfig.json — none of which is named TypeScriptConfig.
derived: overrides: - code: TSCTY files: patterns: - 'tsconfig.base.json' - 'tsconfig.json' - 'packages/*/tsconfig.json'Without this, the spec stays forever “with no code linked” and the
unit-complete gate rightly fails — the file exists, but nothing connects
them.
governs — which guides govern which files
Section titled “governs — which guides govern which files”A guide is a cross-cutting rule document: the accessibility standard, the secrets policy, the naming convention. It doesn’t describe a unit — it cuts across many.
governs: - from: guides/security.md governs: [lambdas, infra]governs points at layer tags, not paths. That’s what lets you say “the
security rule applies to every lambda” without listing files one by one.
boundaries — who may import whom
Section titled “boundaries — who may import whom”boundaries: - from: shared forbid: [lambdas, infra] because: 'the contract does not know its consumers'because isn’t decoration: it’s the text shown when the layer-boundary gate
fails. A boundary with no written reason becomes “a rule someone added”, and the
first reaction of whoever hits it is to remove it.
gates — what gets confronted
Section titled “gates — what gets confronted”This block has its own page, being the longest. The minimal form:
gates: - name: unit-complete on: [spec] check: unit-complete blocking: true measures: 'the spec has the pieces of its unit: code, feature and test'A canonical gate needs only its name — - name: unit-complete — and inherits the rest. Fields a gate entry can also declare:
| Field | What it does |
|---|---|
when: [manual] |
the gate runs only under anchors check --phase manual; a check with no phase leaves it out |
severity: |
what each verdict level does (fail, divergence, pending → block, inform, ignore); with none, a blocking gate blocks only its failures. A top-level severity: is the project’s default |
presupposes: |
the configuration fields the gate takes as declared (derived.mock_detect); with one missing the gate is pending and asks nothing, and with it in dialect.opt_out it skips |
review: |
the gate’s targets are also marked to review, apart from its verdict (review: {ask: "..."}) — see judgment and review |
min_coverage: |
for line-coverage: the floor, 70 when undeclared |
coverage_floors: |
for line-coverage: a floor per glob, each with its why — {"cmd/**": {min: 40, why: "..."}} |
no_signal: |
targets with nothing to measure, each with its reason |
anchors doctor names every catalog gate that covers your layers and is not declared, and anchors check --all says how many in one line.
workflow — where the work lives
Section titled “workflow — where the work lives”workflow: mode: github repo: 'org/project' labels: [anchors] integration_branch: develop required_approvals: 1| field | decides |
|---|---|
mode |
local (tasks in .anchors/) or github (cards as issues) |
repo |
owner/name — required in github mode |
labels |
what marks an issue as Anchors work |
integration_branch |
the branch work goes to |
stale_pipeline_blocks |
an outdated pipeline BLOCKS CI instead of merely warning |
manual_ingest_blocks |
anchors ingest called by hand is REFUSED instead of merely warning |
repo is required and deliberately not inferred from the git remote: inferring
would make Anchors write to another repository when someone works on a fork, and
writing to the wrong place is the mistake a revert doesn’t undo.
enabled — the panic button
Section titled “enabled — the panic button”enabled: falsefreeze_reason: 'plan 0002 points at a spec that does not exist — see #42'Freezes the whole project. See Freezing the project.
An absent field means ENABLED. Only an explicit false freezes — otherwise
every project that never declared the field would be born stopped.
dialect.tests — how your tests are written
Section titled “dialect.tests — how your tests are written”The gates that read a test’s title (feature-test-match, test-traceable,
scenario-coverage, flag-covered) need to know how a test opens in your test library.
That is your project’s knowledge, not Anchors’, so you declare it — with a fixed pattern or
with a script, whichever fits:
dialect: family: ts tests: pattern: '\b(?:it|test)(?:\.only|\.skip)?\s*\(' # the call that opens a test # or script: "node scripts/anchors-tests.mjs" # a lister you provide-
pattern— the call that opens a test, up to its parenthesis. The title is the string literal right after it. -
script— a command Anchors runs at the project root, once per scan. It can ask your test library itself (jest --listTests,pytest --collect-only,go/ast), so when the library changes, the answer changes with it. It prints one JSON object:{"version": 1, "tests": [{"file": "src/a.test.ts", "line": 12, "title": "ABCDX-B01: …"}]}filerelative to the root,linefrom 1,titleas written. Anything else — another field, another version, a script that fails — is reported by the gates, naming the problem.
Declare one, never both. Without either, the go and ts families bring a default
(t.Run(; it/test/describe with .only, .skip, .each(table)). With no source at
all, the gates fall back to finding the code anywhere in the test file.
changelog — how anchors changelog --write writes
Section titled “changelog — how anchors changelog --write writes”changelog: mode: incremental # or per_version path: CHANGELOG.md # the file; per_version: the directory (default changelog/) template: doct/changelog.md.tmpl # optional: your own template for one releaseincremental(the default) — one file; the releases it does not hold yet go on its top, below a leading#title, each after a<!-- anchors:changelog vX -->marker. What is under a marker is yours to edit: it is never rewritten. The unreleased block is the one exception — it is replaced on each write, since it grows until the next tag.per_version— one file per release,<path>/<version>.md; a release’s file that exists is kept, andunreleased.mdis rewritten.template— a Gotext/templaterendering one release, like the docs templates. It receives.Version,.Dateand the lists.Breaking,.Features,.Bugs,.Fixes, each entry with.Type,.Scope,.Subject,.Hashand.Bug;{{t "changelog.features"}}and the otherchangelog.*keys translate the headings tolang.
This is the technical changelog; see anchors guide changelog for the product one.
Advanced blocks
Section titled “Advanced blocks”You probably won’t need these early on.
| block | for |
|---|---|
comments |
comment markers per language, when the project uses a dialect Anchors doesn’t know |
rule_types |
the identity-code letter vocabulary (B for rule, I for invariant…) |
code_lengths |
how many letters an identity code has (default: 5) |
obligations |
regulatory duties (GDPR, retention) and which nodes are subject to them |
contracts |
external contracts whose status must be declared |
regimes |
what each layer regime is expected to be |
tools |
external tools anchors verify runs per phase |