<div align="center">
# directiva
**Write the rule on one line. Match anything. Keep the reason.**
[](https://www.rust-lang.org/)
[](https://www.rust-lang.org/)
[](https://opensource.org/licenses/MIT)
[](#the-grammar)
[](https://github.com/microsoft/pyright)
A tiny, paste-friendly **directive mini-language** — one rule per line, the kind every linter, build
tool and policy gate keeps reinventing:
```text
ACTION : [<KIND>] NAME [@PATH] [=NOTE]
```
*Verb the things kinded `<KIND>`, named `NAME`, located `@PATH` — because `NOTE`.* It fits in a
shell flag, a CI config, or a comment. The crate knows nothing about your domain: you bring the
verbs and the things they act on; `directiva` brings the parser, the glob matcher, the matching
rules, and a severity ladder.
</div>
```rust
use directiva::{parse, Pattern, Target};
let d = parse("de-escalate:<method>get_*@*/tests/*=test fixtures").unwrap();
// d.action == "de-escalate", d.kind == Some("method"), d.name == "get_*", …
```
```bash
mytool ./src \
-D 'suppress:<function>__repr__@*=dataclass boilerplate' \
-D 'escalate:<class>*Service@*/core/*=core services must be unique' \
-D @ci/directives.txt
```
```toml
directiva = "0.2" # edition 2024, MSRV 1.85
```
---
## The idea
The shape *selector → verb → reason* has been reinvented a hundred times — `.gitattributes`
(`glob attr=value`), CODEOWNERS (`glob @team`), `.dockerignore`, Checkstyle's `suppressions.xml`,
rsync's `--filter='- *.tmp'`, Snort rules (`alert … (msg:…)`). Each is a **single-domain** version
welded into one tool. `directiva` is the domain-agnostic substrate they each rebuild — so you can
use *one* grammar, *one* parser, and *one* consistent UX across all of your tools.
Two things make the shape worth extracting:
- **The reason travels with the rule.** `=NOTE` is a first-class field. Most suppression systems
(`# noqa`, baseline files) throw the *why* away; here it ships next to the match, ready for the
next reviewer.
- **The verbs are yours.** The crate has no built-in actions. `suppress` / `de-escalate` are
defined by *your* code, exactly like a custom `quarantine` / `allow` / `promote`. The action token
is opaque; you map it to whatever you like.
---
## The shape
```
de-escalate : <method> get_* @ */tests/* = test fixtures
└────┬─────┘ └──┬───┘ └─┬──┘ └───┬────┘ └─────┬──────┘
ACTION KIND NAME PATH NOTE
(your verb, (exact (glob, (glob, any-of (free text;
opaque) filter; any-of scopes) the "why")
<*>=any) names)
```
| `ACTION` | yes | resolved by your `Action::from_token` (or kept as a `String`) | ends at first `:` |
| `<KIND>` | no | exact equality with the target's qualifier (`<*>`/omitted = any) | `<…>` |
| `NAME` | yes | **glob**, any of the target's names | — |
| `@PATH` | no | **glob**, any of the target's scopes | `@…` |
| `=NOTE` | no | not matched — surfaced beside the result | `=…` |
The grammar is **context-free** (the angle-bracketed `<KIND>` is what buys that — no vocabulary
needed to parse, and a NAME can freely contain `:`, so Rust paths like `Foo::bar` are just names).
---
## What it does (Rust)
You implement [`Target`] for whatever a directive should select. Matching is pushed *into* the
target, so it's allocation-free and you decide what counts as a "name" (aliases, a joined form, …):
```rust
use directiva::{parse, Pattern, Target};
struct Item { kind: &'static str, name: &'static str, file: &'static str }
impl Target for Item {
fn qualifier(&self) -> Option<&str> { Some(self.kind) }
fn matches_name(&self, p: &Pattern) -> bool { p.matches(self.name) }
fn matches_scope(&self, p: &Pattern) -> bool { p.matches(self.file) }
}
let d = parse("suppress:<function>get_*@*/tests/*").unwrap();
let hit = Item { kind: "function", name: "get_user", file: "src/tests/api.rs" };
let miss = Item { kind: "function", name: "get_user", file: "src/api.rs" };
assert!(d.matches(&hit));
assert!(!d.matches(&miss)); // @PATH glob doesn't match
```
Closed-set actions catch typos; the default `String` action is the open set:
```rust
use directiva::{parse_as, Action};
enum Verb { Suppress, Quarantine }
impl Action for Verb {
fn from_token(t: &str) -> Option<Self> {
match t { "suppress" => Some(Verb::Suppress), "quarantine" => Some(Verb::Quarantine), _ => None }
}
}
assert!(parse_as::<Verb>("supress:foo").is_err()); // typo → UnknownAction, not a silent no-op
```
### Severity is a decoupled monoid
[`Ladder<S>`] is a standalone, saturating, order-independent ladder over *any* ordered set — it
references neither directives nor actions. Linter severities are one use; a markup workflow is
another:
```rust
use directiva::Ladder;
let sev = Ladder::new(vec!["error", "warning", "info"]); // most-severe first
assert_eq!(sev.step(&"error", 2), "info"); // saturating
assert_eq!(sev.step(&"info", -5), "error"); // clamps
let flow = Ladder::new(vec!["draft", "review", "final"]);
assert_eq!(flow.step(&"draft", 1), "review"); // not just severities
```
---
## The CLI use case (`-D`)
The headline use is many `-D` flags. `directiva::source` ingests them with **no `clap`
dependency** — you wire one call into your own arg parser. One flag, two forms:
```bash
mytool -D 'suppress:<function>spawn@*/vendor/*=vendored copy' # an inline directive
mytool -D @rules.txt # a directive file (one per line)
mytool -D @- # …or read them from stdin
```
```rust
// for each -D value clap collected:
let directives = directiva::source::cli::expand_all::<directiva::lint::LintAction, _, _>(values)?;
```
Files get `#` comments, blank-line skipping, CRLF/BOM tolerance, and **line-numbered parse errors**.
Multiple `-D` (inline and/or `@file`) are pure concatenation — **no dedup**, so a duplicate honestly
compounds; that's the caller's call, not magic.
> ⚠️ **Quote your `-D` values.** `*`, `{…}`, `[…]` and `!` are all shell metacharacters — unquoted,
> the shell mangles your directive before the tool sees it. Always single-quote:
> `-D 'suppress:*@*/{test,tests}/*'`.
---
## The `lint` pack — batteries included
`directiva::lint` is a ready-made instantiation for the classic lint case: a concrete
`LintAction` vocabulary (`suppress` / `de-escalate` / `escalate` / `note` / `set`), a
`Severity` ladder, and a `fold` combine policy.
```rust
use directiva::lint::{self, LintAction, Severity};
use directiva::source::cli;
let dirs: Vec<_> = cli::expand_all::<LintAction, _, _>([
"de-escalate:<method>get_*@*/tests/*=test fixtures",
"suppress:<function>spawn@*/vendor/*=vendored copy",
"set:max-name-group=256",
])?.into_iter().map(|s| s.directive).collect();
let outcome = lint::fold(Severity::Error, &finding, &dirs);
// outcome.severity (stepped + clamped) outcome.dropped (suppressed?) outcome.notes
let settings = lint::extract_settings(&dirs); // [("max-name-group", "256")] — you apply them
```
Notes accumulate from every match; escalate/de-escalate steps sum then clamp; `suppress` drops;
`set` is config (skipped by the fold). It's *one* pack — a tool with a different vocabulary
(`allow/deny`, `draft<review<final`) defines its own beside it.
---
## Use it in any domain
Same grammar, your verbs — a few sketches:
```bash
# access control (KIND = resource type, PATH = tenant)
-D 'deny:<route>/admin/*@*=staff only' -D 'allow:<route>/public/*'
# CI / flaky tests
-D 'quarantine:test_checkout_*@*/e2e/*=JIRA-1234, flaky on CI'
# feature flags / rollout
-D 'canary:checkout-v2@*/eu/*=10% EU'
# alert routing (ladder: log < ticket < page)
-D 'silence:<alert>DiskWarn*@*/staging/*=staging noise' -D 'escalate:<alert>Oom*@*/prod/*'
# doc workflow (ladder: draft < review < final)
-D 'promote:chapter-*@drafts/q3/*=ready for review'
# data pipeline / PII
-D 'mask:<column>*_ssn@*/users/*=PII'
```
---
## The grammar
A clean context-free grammar; the angle-bracketed `<KIND>` removes the only ambiguity. NAME/PATH
are globs; `=NOTE` is whatever follows the first unescaped `=` (so it may contain `:` `@` `=`).
```ebnf
directive ::= action ":" body
body ::= [ "<" kind ">" ] name [ "@" path ] [ "=" note ]
```
**Glob:** `*` (any run, **including `/`** — not path-segment-aware), `?` (one byte), `{a,b}`
alternation (one level), `[a-z]`/`[!neg]` shell-style char-classes, and one universal `\` escape.
Matching is **full-anchor** (`foo` matches only `foo`, not `foobar` — use `*foo*`) and **linear-time**
(no catastrophic backtracking on untrusted patterns). Compilation is bounded too: brace groups
cross-multiply (`{a,b}{c,d}` → 4), so the expander caps the product at `MAX_BRACE_ALTS` (a public
`core::glob` const) — a short `{a,b}×30` "brace bomb" would otherwise be 2³⁰ sub-patterns; past the
cap the braces degrade to literals. No OOM on hostile input.
---
## Architecture
Three layers; everything but `core` is feature-gated but on by default.
| `directiva::core` | the engine — `glob` (`Pattern`), `parse`/`parse_as`, `Directive<A = String>` + the `Action` trait, the `Target` trait + matching, and the decoupled `Ladder<S>`. Domain-agnostic, always on. |
| `directiva::source` | ingestion — the `-D` overload (`cli::expand`) and the line-based file parser. No `clap` dependency. *(feature `source`)* |
| `directiva::lint` | the standard batteries — `LintAction`, `Severity`, `fold`, `extract_settings`. One instantiation, not part of the engine. *(feature `lint`)* |
A minimal embedder takes just `core` (`default-features = false`).
---
## Python package
The same grammar from Python — parse, match, glob, expand `-D` values, and the `lint` fold:
```python
import directiva
from directiva import lint
d = directiva.parse("de-escalate:<method>get_*@*/tests/*=test fixtures")
d.action, d.kind, d.name, d.note # ('de-escalate', 'method', 'get_*', 'test fixtures')
d.matches(["get_user"], qualifier="method", scopes=["src/tests/api.py"]) # True
directiva.glob_match("*/{test,tests}/*", "pkg/tests/x.py") # True
directiva.expand("@ci/rules.txt") # list[Directive]
lint.fold(lint.ERROR, [d], ["get_user"], qualifier="method", scopes=["src/tests/x.py"])
# Outcome(severity='warning', dropped=False, notes=['test fixtures'])
```
The package is **typed** (`py.typed` + stubs; `Severity` is a `Literal`), and gated behind the
`python` cargo feature so the pure-Rust crate keeps **zero** Python dependency by default. Built with
[maturin](https://github.com/PyO3/maturin) (abi3 — one wheel per platform works on **CPython ≥ 3.9**).
**No PyPI.** Two ways to install:
**1. Prebuilt wheel — no Rust toolchain.** Grab the wheel for your platform from the
[Releases page](https://github.com/prostomarkeloff/directiva/releases/latest) and install it by URL:
```bash
pip install https://github.com/prostomarkeloff/directiva/releases/download/v0.2.0/directiva-0.2.0-cp39-abi3-macosx_11_0_arm64.whl
```
**2. From source — needs a Rust toolchain** (pip drives maturin automatically):
```bash
pip install git+https://github.com/prostomarkeloff/directiva
```
Build locally into a venv with `maturin develop --features python`.
---
## Correctness
The grammar's corner cases (note eats `:`/`@`/`=`, the `::`-in-names case, char-class edges,
escaping across layers, severity clamping, file conventions) are pinned by the test suite, and the
engine's safety invariants (the parser never panics, glob compilation stays bounded, literals are
full-anchored) are exercised by `proptest`:
```bash
cargo test # 64 tests: core + source + lint + property + integration + doctest
cargo clippy --all-targets
```
The parser and glob engine also carry [`cargo-fuzz`](https://github.com/rust-fuzz/cargo-fuzz)
harnesses — totality and the brace-bomb cap are asserted as fuzz invariants:
```bash
cargo +nightly fuzz run parse # the parser is total: Result, never a panic
cargo +nightly fuzz run glob # compile stays ≤ MAX_BRACE_ALTS; matches always terminates
```
---
<div align="center">
**One line. Any tool. The reason, attached.**
Made with ⚡ by [@prostomarkeloff](https://github.com/prostomarkeloff)
</div>