Skip to content
EN · PT

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.yaml with 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 of from:/governs: was silently discarded, map build answered “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.

version: 1
layers: # WHICH files Anchors governs, and what each one is
derived: # how to find a spec's code/feature/test
governs: # which guides govern which files
boundaries: # who may import whom
gates: # what gets confronted, and what blocks
workflow: # where the work lives (local or GitHub)

Only version and layers are required. The rest comes in as the project needs it.


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.

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.


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:
- 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.


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:
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: false
freeze_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: …"}]}

    file relative to the root, line from 1, title as 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 release
  • incremental (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, and unreleased.md is rewritten.
  • template — a Go text/template rendering one release, like the docs templates. It receives .Version, .Date and the lists .Breaking, .Features, .Bugs, .Fixes, each entry with .Type, .Scope, .Subject, .Hash and .Bug; {{t "changelog.features"}} and the other changelog.* keys translate the headings to lang.

This is the technical changelog; see anchors guide changelog for the product one.


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