Config layering
Your root .alint.yml is rarely the whole config. Drop-ins layer over it, per-directory nested configs add subtree-scoped rules, and three interpolation timings fill in values at three different moments. Knowing which layer resolves when is the difference between a config that behaves and one that surprises you.
Drop-ins: .alint.d/
Section titled “Drop-ins: .alint.d/”When a .alint.d/ directory sits next to your root .alint.yml, alint discovers every *.yml (or *.yaml) inside it and merges them in alphabetical order, last wins on a field-level conflict. It is the /etc/*.d/ pattern applied to config: ops layer 50-policy.yml through provisioning, a developer gitignores 99-local.yml. Each drop-in is a complete config (its own version: 1) and can add rules, override existing ones by id, add extends:, or layer more facts: and vars:.
Drop-ins are trust-equivalent to your root config: they live in the same workspace under your control, so they may declare spawning rules and custom: facts, unlike anything reached through extends:. Only the root config gets .alint.d/ discovery; a config reached via extends: does not carry its own drop-ins. Reserve numeric prefixes (00-base.yml, 50-team.yml, 99-local.yml) to control the order, the one merge knob. A drop-in overrides an existing rule field-by-field by id, but nested structures (a fix: block, a paths: include/exclude pair) replace wholesale rather than merging into the base.
Nested configs
Section titled “Nested configs”Opt in with nested_configs: true in the root config, and alint walks the tree (respecting .gitignore and ignore:) and picks up a .alint.yml in any subdirectory. A nested config’s rules are added, not overridden, and each rule’s path-like scope is auto-prefixed with that subtree, so a rule in packages/web/.alint.yml only ever looks at packages/web/.
The guardrails keep nesting predictable: of a nested config’s fields, only version: and rules: carry weight; the dangerous ones (extends:, facts:, baseline:, allow_out_of_root:, nested_configs:, and spawning rules) are rejected at load, while other top-level knobs such as respect_gitignore: are silently ignored rather than honored. Every nested rule needs at least one scope field; absolute and ..-escaping paths are rejected; and a duplicate rule id anywhere is a load error, never a silent override. Nesting is untrusted in the same way an extends:’d ruleset is (no spawning rules), and only the top-level config may turn it on; a nested config cannot enable its own nested discovery.
Three interpolation timings
Section titled “Three interpolation timings”The same {{...}} syntax resolves at three distinct times, and each layer only ever sees its own inputs:
{{env.X}}resolves at config load, from the process environment, in local config files only. A{{env.X | default('...')}}filter supplies a fallback.{{vars.X}}resolves when atemplates:body expands into its instances. A plain, non-template rule does not expand{{vars.X}}in its fields; a barewhen: vars.Xreads a top-level var directly.{{ctx.X}}resolves per violation, inside themessageof a rule that populates it. Apairrule fills{{ctx.primary}}and{{ctx.partner}}, for example; not every rule interpolates its message, and one likefile_existsemits itsmessageverbatim.
Because they resolve at different moments, they never cross: {{ctx.primary}} is meaningless at load, and {{env.X}} is long since resolved by the time a violation is formatted.
In practice
Section titled “In practice”One config threads all three timings in order: an env value feeds a template var, the var scopes which files the rule pairs, and each violation names the file and its missing partner:
version: 1templates: - id: sources-need-headers kind: pair primary: "{{vars.dir}}/**/*.c" # {{vars}} fills at template expansion partner: "{dir}/{stem}.h" message: "{{ctx.primary}} has no header at {{ctx.partner}}" # {{ctx}} fills per violation level: errorrules: - extends_template: sources-need-headers id: lib-headers vars: { dir: "{{env.SRC_ROOT | default('src')}}" } # {{env}} fills at loadWith SRC_ROOT unset and src/app.c missing its header, the default resolves at load, the template expands to pair src/**/*.c files, and the pair rule fills the message per violation:
error lib-headers src/app.c has no header at src/app.hGoing deeper
Section titled “Going deeper”- The config model is the whole assembly picture these layers feed into.
- Composition and trust covers
extends:, the other way configs combine. - Variable interpolation and Rule templates are the field-level references for the timings above.