directiva 0.1.0

A tiny, paste-friendly directive mini-language: ACTION:[<KIND>]NAME[@PATH][=NOTE]
Documentation
<div align="center">

# directiva

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

[![Rust 2021](https://img.shields.io/badge/rust-2021-orange.svg)](https://www.rust-lang.org/)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![grammar: context-free](https://img.shields.io/badge/grammar-context--free-blue.svg)](#the-grammar)
[![Types: pyright](https://img.shields.io/badge/types-pyright-blue)](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)
```

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

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

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

```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>