<div align="center">
# directiva
**Write the rule on one line. Match anything. Keep the reason.**
[](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.1"
```
---
## 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).
---
## 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.1.0/directiva-0.1.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:
```bash
cargo test # 56 tests: core + source + lint + integration + doctest
cargo clippy --all-targets
```
---
<div align="center">
**One line. Any tool. The reason, attached.**
Made with ⚡ by [@prostomarkeloff](https://github.com/prostomarkeloff)
</div>