Skip to main content

directiva

Write the rule on one line. Match anything. Keep the reason.

Rust 2024 MSRV 1.85 License: MIT grammar: context-free Types: pyright

A tiny, paste-friendly directive mini-language — one rule per line, the kind every linter, build tool and policy gate keeps reinventing:

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.

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_*", …
mytool ./src \
  -D 'suppress:<function>__repr__@*=dataclass boilerplate' \
  -D 'escalate:<class>*Service@*/core/*=core services must be unique' \
  -D @ci/directives.txt
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)
field required matched against sigil
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, …):

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:

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:

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:

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

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:

# 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 : @ =).

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.

module role
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:

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 (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 and install it by URL:

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

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:

cargo test                 # 64 tests: core + source + lint + property + integration + doctest
cargo clippy --all-targets

The parser and glob engine also carry cargo-fuzz harnesses — totality and the brace-bomb cap are asserted as fuzz invariants:

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

One line. Any tool. The reason, attached.

Made with ⚡ by @prostomarkeloff

Metadata

Release files for directiva 0.2.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for directiva 0.2.0
File Size Uploaded Provenance Actions
directiva-0.2.0.tar.gz 44.0 kB Without provenance Details for directiva-0.2.0.tar.gz

Built distributions (wheels)

Table of built distributions (wheels) for directiva 0.2.0
File Actions
directiva-0.2.0-cp39-abi3-win_amd64.whl CPython 3.9 abi3 Windows x86-64 Details for directiva-0.2.0-cp39-abi3-win_amd64.whl
directiva-0.2.0-cp39-abi3-musllinux_1_2_x86_64.whl CPython 3.9 abi3 Linux musl 1.2+ x86-64 Details for directiva-0.2.0-cp39-abi3-musllinux_1_2_x86_64.whl
directiva-0.2.0-cp39-abi3-musllinux_1_2_aarch64.whl CPython 3.9 abi3 Linux musl 1.2+ ARM64 Details for directiva-0.2.0-cp39-abi3-musllinux_1_2_aarch64.whl
directiva-0.2.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl CPython 3.9 abi3 Linux glibc 2.17+ x86-64 Details for directiva-0.2.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
directiva-0.2.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl CPython 3.9 abi3 Linux glibc 2.17+ ARM64 Details for directiva-0.2.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
directiva-0.2.0-cp39-abi3-macosx_11_0_arm64.whl CPython 3.9 abi3 macOS 11.0+ ARM64 Details for directiva-0.2.0-cp39-abi3-macosx_11_0_arm64.whl
directiva-0.2.0-cp39-abi3-macosx_10_12_x86_64.whl CPython 3.9 abi3 macOS 10.12+ x86-64 Details for directiva-0.2.0-cp39-abi3-macosx_10_12_x86_64.whl

Total release size: 2.6 MB

Release files / directiva-0.2.0.tar.gz

Download URL directiva-0.2.0.tar.gz
Size 44.0 kB
Tags Source
SHA-256 checksum
How to use checksums
cf7f61c0648f40af980f0f2d34d305d84c3ab2a10f87e258494feb2d1a41d28c
BLAKE2b-256 checksum
How to use checksums
c4f7e58df9114e7d53985c9d8675f4c9a4fd9c508a0863273e9f65aed30ef863
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.8.1

Release files / directiva-0.2.0-cp39-abi3-win_amd64.whl

Download URL directiva-0.2.0-cp39-abi3-win_amd64.whl
Size 194.7 kB
Tags CPython 3.9 Windows x86-64 abi3
SHA-256 checksum
How to use checksums
bd4a500c638f11f50770b5e81229674aeb5ce941acae976acc23127462daf861
BLAKE2b-256 checksum
How to use checksums
d3a4f992aa67f12ccd7458c95a5a2faddecebd1a725e84b74db951f872abba7d
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.8.1

Release files / directiva-0.2.0-cp39-abi3-musllinux_1_2_x86_64.whl

Download URL directiva-0.2.0-cp39-abi3-musllinux_1_2_x86_64.whl
Size 544.1 kB
Tags CPython 3.9 Linux musl 1.2+ x86-64 abi3
SHA-256 checksum
How to use checksums
066f085a428a18874ccdadfffcdbbfd551c6bafedbd7d7d0ba67c1f71bef5f16
BLAKE2b-256 checksum
How to use checksums
5702f6b9457e5c12f76a9941c0ca4220165401e2d71f66b42a3d4c5bf86ac443
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.8.1

Release files / directiva-0.2.0-cp39-abi3-musllinux_1_2_aarch64.whl

Download URL directiva-0.2.0-cp39-abi3-musllinux_1_2_aarch64.whl
Size 512.1 kB
Tags CPython 3.9 Linux musl 1.2+ ARM64 abi3
SHA-256 checksum
How to use checksums
96643bcafd8636d7951e0e058c5d8e765f4067f3cfa29e2fc162679519928707
BLAKE2b-256 checksum
How to use checksums
0c6faeba2887171054f1d8ca3b34d15020f54fb88b10296366e021b3d030a1fd
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.8.1

Release files / directiva-0.2.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl

Download URL directiva-0.2.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Size 339.4 kB
Tags CPython 3.9 Linux glibc 2.17+ x86-64 abi3
SHA-256 checksum
How to use checksums
edf91c0711b802bf1306caaff216d296027967d53cc1cb68ef1ba60babcadbbf
BLAKE2b-256 checksum
How to use checksums
f21a263915a5b03cfcb9cbc4c68517b72e6b297b90ba6409a6ce95946d1d8c0d
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.8.1

Release files / directiva-0.2.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl

Download URL directiva-0.2.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Size 337.0 kB
Tags CPython 3.9 Linux glibc 2.17+ ARM64 abi3
SHA-256 checksum
How to use checksums
ad3e9e41becf063d519b176f854c18dcd459d8a7282a54b04803c760806538ab
BLAKE2b-256 checksum
How to use checksums
6560ae84d4ccfaf146bcbeb6108fac645d957b27d0b9aa94301974cb14843c94
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.8.1

Release files / directiva-0.2.0-cp39-abi3-macosx_11_0_arm64.whl

Download URL directiva-0.2.0-cp39-abi3-macosx_11_0_arm64.whl
Size 300.7 kB
Tags CPython 3.9 abi3 macOS 11.0+ ARM64
SHA-256 checksum
How to use checksums
dbe702bcafc81c64f9ee949ddeffdf075a111310f20162beabf4becd2522c366
BLAKE2b-256 checksum
How to use checksums
2d769b489b5b22f52d64ac95445be45b6d6be991093f80e8961c61146b6d774a
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.8.1

Release files / directiva-0.2.0-cp39-abi3-macosx_10_12_x86_64.whl

Download URL directiva-0.2.0-cp39-abi3-macosx_10_12_x86_64.whl
Size 305.4 kB
Tags CPython 3.9 abi3 macOS 10.12+ x86-64
SHA-256 checksum
How to use checksums
136cb8276f30d0e8d62bd6b28b9061b16f0bc1690d753588abf40f6603a13604
BLAKE2b-256 checksum
How to use checksums
d3c3e0f176c2fed6fbcd21e290a81e4f055b1ab8dcb1c8ec2c32f4a9844cd855
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.8.1

Security for directiva 0.2.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

0.2.0 This release

8 release files No provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page