Skip to main content

antigen_macros/
lib.rs

1//! Procedural macros for the antigen crate.
2//!
3//! This crate provides the five attribute macros that constitute the antigen
4//! API surface:
5//!
6//! - [`#[antigen(...)]`](macro@antigen) — declare a named failure-class with a
7//!   structural fingerprint (ADR-001, ADR-010)
8//! - [`#[presents(...)]`](macro@presents) — mark code as exhibiting an antigen's
9//!   structural pattern (vulnerability declaration); carries optional
10//!   `requires = <predicate>` (substrate-tier) / `proof = <expr>` (phantom-tier)
11//!   site-attached evidence (ADR-029)
12//! - [`#[defended_by(...)]`](macro@defended_by) — register a test/proptest as the
13//!   *observed* code-tier witness for a failure-class (ADR-029)
14//! - [`#[descended_from(...)]`](macro@descended_from) — propagate antigen markers
15//!   through an inheritance chain (ADR-013, ADR-018 §propagation)
16//! - [`#[antigen_tolerance(...)]`](macro@antigen_tolerance) — document an
17//!   intentional opt-out with required rationale (ADR-011)
18//!
19//! ### Deferred-Defense Family (ADR-023)
20//!
21//! - [`#[anergy(...)]`](macro@anergy) — deferred-but-muted posture; `until`
22//!   REQUIRED; aging escalation; loudness-as-discipline
23//! - [`#[immunosuppress(...)]`](macro@immunosuppress) — surgical silencing
24//!   with hard duration cap enforced at parse time
25//! - [`#[poxparty(...)]`](macro@poxparty) — intentional exposure with
26//!   structural compile-time isolation via `antigen-poxparty` feature flag
27//! - [`#[orient(...)]`](macro@orient) — see-also context without antigen
28//!   claim; lightest-weight deferred-defense primitive
29//!
30//! Users typically import these via the [`antigen`](https://docs.rs/antigen)
31//! crate (`use antigen::{antigen, presents, defended_by, descended_from,
32//! antigen_tolerance};`) rather than depending on `antigen-macros` directly.
33//!
34//! ## Design philosophy (v1)
35//!
36//! The macros are **mostly identity transformations**. Their job is to validate the
37//! attribute syntax at compile time and pass the input through unchanged. The
38//! semantic work — scanning the codebase, matching presentations against
39//! immunities, validating witnesses — lives in the `cargo-antigen` tooling, which
40//! parses source AST independently via `syn`.
41//!
42//! This keeps runtime overhead at zero (the macros generate no code beyond the
43//! original input) and means antigen declarations don't affect compilation speed,
44//! linker behavior, or binary size. The cost lives entirely at `cargo antigen scan`
45//! time, which runs out-of-band.
46//!
47//! See ADR-010 (fingerprint grammar v1) and the project's documentation
48//! for the design rationale.
49//!
50//! ## Known v1 limitations
51//!
52//! 1. **Macros are pure pass-through**: the proc-macros parse + validate the
53//!    attribute syntax and emit the original item unchanged (ADR-001 identity
54//!    transform). Cross-crate antigen discovery works via source-walking the
55//!    `.cargo/registry` tree. A future ADR amendment may add metadata-emitting
56//!    transforms (e.g., `<!-- antigen:metadata:v1 {...} -->` doc-comment
57//!    markers, or `#[cfg(doc)] pub static __ANTIGEN_META_*`) for the
58//!    no-source-access case — verified-viable but as-yet-undecided ADR territory.
59//!
60//! Span-aware error pointing and trybuild fixtures both shipped in v0.1.0-rc.1.
61
62use proc_macro::TokenStream;
63use quote::quote;
64use syn::parse_macro_input;
65
66mod parse;
67
68/// Declare a named failure-class with a structural fingerprint.
69///
70/// # Arguments
71///
72/// - `name = "..."` (required) — kebab-case identifier for the failure-class
73/// - `fingerprint = "..."` (optional; required for scan-locatable antigens) —
74///   structural pattern (see ADR-010). Omit for verify-only antigens whose
75///   detection-model is external-substrate (supply-chain / VCS-info-loss), per
76///   ADR-009 Amendment 1 — they have no syn-scannable source surface.
77/// - `family = "..."` (optional) — parent class, typically one of the 8
78///   first-principles failure classes
79/// - `summary = "..."` (optional) — human-readable description
80/// - `references = [...]` (optional) — open-vocabulary list of references
81///   (URLs, ADR/DEC IDs, CVE numbers, RFC numbers, etc.)
82/// - `category = AntigenCategory::X` (optional) — `SubstrateAlignment` or
83///   `FunctionalCorrectness` (ADR-028). **Do not import `AntigenCategory`** —
84///   the macro reads this as a token path, so `use antigen::AntigenCategory;`
85///   triggers `unused_imports` under `-D warnings`. Write the path directly
86///   without importing.
87/// - `provenance = Provenance::X` (optional) — the authored claim of *how we
88///   know this failure-class exists* (ADR-039 §C). One of `Encountered` (seen
89///   in real code — highest), `Constructable` (a minimal case can be built that
90///   verifiably exhibits the failure), `Heuristic` (a scannable tell that
91///   correlates without a constructable demo), or `Imagined` (articulated from
92///   shape — a tell but no demo yet — lowest). **Omitting it defaults to
93///   `Imagined`**: an unlabeled antigen is honestly the weakest claim. This is
94///   the honest-labeling on-ramp — admission is permissive, so the *label* is
95///   what stays truthful. Provenance is the evidence basis, NOT the confidence
96///   tier (suspected/named): that tier is the dial-derived audit-time calibration
97///   (the confidence-dial wave), and provenance sets the floor it may graduate from.
98/// - `presentation = Presentation::X` (optional) — `Passive` (tooling/scan-side;
99///   the default for low-provenance classes — no user-macro burden) or `Active`
100///   (user-facing, chosen by whoever encounters the failure). **Omitting it
101///   defaults to `Passive`** (ADR-039 passive-by-default rule). Like `category`,
102///   `provenance` and `presentation` are read as token paths — **do not import
103///   `Provenance`/`Presentation`** (a `use` of them trips `unused_imports` under
104///   `-D warnings`); write the path directly.
105///
106/// # Examples
107///
108/// Layer 1 (minimum viable — just `name` and `fingerprint`):
109///
110/// ```ignore
111/// use antigen::antigen;
112///
113/// #[antigen(
114///     name = "panicking-in-drop",
115///     fingerprint = "impl Drop with unwrap/expect/panic in body",
116/// )]
117/// pub struct PanickingInDrop;
118/// ```
119///
120/// Layer 2 (enriched — adds `family`, `summary`, `references`):
121///
122/// ```ignore
123/// #[antigen(
124///     name = "panicking-in-drop",
125///     family = "boundary-violation",
126///     fingerprint = "impl Drop with unwrap/expect/panic in body",
127///     summary = "Drop impls must not panic; double-panic causes process abort.",
128///     references = ["https://doc.rust-lang.org/std/ops/trait.Drop.html#panics"],
129/// )]
130/// pub struct PanickingInDrop;
131/// ```
132///
133/// Layer 3 (the honest-labeling fields — `category`, `provenance`,
134/// `presentation`). Note the paths are written *without* a `use` import:
135///
136/// ```ignore
137/// #[antigen(
138///     name = "panicking-in-drop",
139///     fingerprint = "impl Drop with unwrap/expect/panic in body",
140///     category = AntigenCategory::FunctionalCorrectness,
141///     provenance = Provenance::Constructable,
142///     presentation = Presentation::Passive,
143/// )]
144/// pub struct PanickingInDrop;
145/// ```
146///
147/// The struct must be declared as a unit struct (no fields). The macro validates
148/// the attribute arguments and passes the struct through unchanged.
149#[proc_macro_attribute]
150pub fn antigen(args: TokenStream, input: TokenStream) -> TokenStream {
151    let args = parse_macro_input!(args as parse::AntigenArgs);
152    let input = parse_macro_input!(input as syn::ItemStruct);
153
154    if let Err(e) = args.validate() {
155        return e.to_compile_error().into();
156    }
157
158    if !matches!(input.fields, syn::Fields::Unit) {
159        return syn::Error::new_spanned(
160            &input,
161            "#[antigen] must be applied to a unit struct (e.g., `pub struct Name;`)",
162        )
163        .to_compile_error()
164        .into();
165    }
166
167    // An antigen marker is a failure-class identity token: it carries no data
168    // and no type-level parameterization. A generic marker (`struct Foo<T>;`)
169    // is semantically meaningless — which failure-class does `Foo<T>` name? —
170    // and would break the dead_code use-token below (a bare `let _x: Foo;`
171    // reference needs type arguments, producing a cryptic E0107 pointing at the
172    // declaration). Reject it here with a clear, on-point error instead.
173    if !input.generics.params.is_empty() {
174        return syn::Error::new_spanned(
175            &input.generics,
176            "#[antigen] must be applied to a non-generic unit struct; a \
177             failure-class marker carries no type parameters",
178        )
179        .to_compile_error()
180        .into();
181    }
182
183    let name_string = &args.name;
184    let attr_doc = format!(
185        " antigen `{name_string}` — declares a named failure-class.\n\n Use \
186         `cargo antigen scan` to find sites presenting this antigen; \
187         `cargo antigen audit` to validate witness coverage."
188    );
189
190    // DX finding 1: in a *binary* crate, `#[antigen] pub struct Foo;` trips
191    // `dead_code` because `pub` does not exempt items with no external API
192    // surface, and antigen uses the marker type as a declaration token, never
193    // constructs it. Rather than `#[allow(dead_code)]` (which would also mask
194    // legitimate dead-code on the item), emit a zero-cost use-token that makes
195    // the type genuinely "used" from the compiler's view:
196    //
197    //   const _: fn() = || { let _x: Foo; };
198    //
199    // The `const _` is anonymous (no namespace pollution), is always compiled
200    // (not `#[cfg(test)]`-gated, so it works under any conditional compilation),
201    // and the closure body is never invoked — the binding only references the
202    // type. Zero runtime cost; honours lib.rs's "pure pass-through with zero
203    // overhead" contract at runtime while satisfying the dead_code analysis.
204    let marker_ident = &input.ident;
205    // The use-token references the type by bare name (`let _x: Foo;`). That is
206    // safe here precisely because the generics check above already rejected any
207    // parameterized marker — so `#marker_ident` always names a concrete,
208    // arg-free type. (use-tokens-under-generics; the guard,
209    // not an assumption about E0392, is what makes this sound.)
210    let expanded = quote! {
211        #[doc = #attr_doc]
212        #input
213        const _: fn() = || {
214            let _antigen_use_token: #marker_ident;
215        };
216    };
217
218    expanded.into()
219}
220
221/// Mark code as exhibiting a known antigen's structural pattern (vulnerability
222/// declaration).
223///
224/// # Arguments
225///
226/// Single positional argument: the antigen type name.
227///
228/// # Example
229///
230/// ```ignore
231/// use antigen::presents;
232///
233/// #[presents(PanickingInDrop)]
234/// impl Drop for MyType {
235///     fn drop(&mut self) { /* might panic */ }
236/// }
237/// ```
238///
239/// `cargo antigen scan` flags every `#[presents]` site that lacks a corresponding
240/// `#[immune]` declaration. This declaration is the *vulnerability surface* — it
241/// says "this code exhibits the structural pattern."
242///
243/// To express both vulnerability AND verified immunity, apply both attributes to
244/// the same item:
245///
246/// ```ignore
247/// #[presents(PanickingInDrop)]
248/// #[immune(PanickingInDrop, witness = no_panic_test)]
249/// impl Drop for SafeType { ... }
250/// ```
251#[proc_macro_attribute]
252pub fn presents(args: TokenStream, input: TokenStream) -> TokenStream {
253    let args = parse_macro_input!(args as parse::PresentsArgs);
254    let input = proc_macro2::TokenStream::from(input);
255
256    if let Err(e) = args.validate() {
257        return e.to_compile_error().into();
258    }
259
260    // ADR-029 R5: a `requires = <predicate>` folded onto `#[presents]` emits the
261    // same `antigen:requires:v1:<json>` doc marker `#[immune(requires=...)]`
262    // does, so `cargo antigen scan` discovers the substrate-witness predicate at
263    // the presents-site (the new substrate-tier carrier). A `proof = <expr>` is
264    // recognized structurally by the audit from the written source (phantom-tier),
265    // so it needs no marker.
266    args.requires_json().map_or_else(
267        || quote! { #input }.into(),
268        |json| {
269            let marker = format!(" antigen:requires:v1:{json}");
270            quote! {
271                #[doc = #marker]
272                #input
273            }
274            .into()
275        },
276    )
277}
278
279/// Propagate antigen markers from a parent function/type/method to a derived one.
280///
281/// # Arguments
282///
283/// Single positional argument: a path to the parent item.
284///
285/// # Example
286///
287/// ```ignore
288/// use antigen::descended_from;
289///
290/// #[descended_from(crate::other_module::parent_function)]
291/// fn refined_function(...) { ... }
292/// ```
293///
294/// **v0.1 status**: `#[descended_from]` is recognized and parsed but
295/// propagation is not yet implemented. In v0.1, this attribute compiles
296/// cleanly and is recorded by `cargo antigen scan` for future use.
297/// Chain-walking and marker propagation (`#[presents]` / `#[immune]`
298/// inheritance with witness re-validation) are not yet implemented.
299#[proc_macro_attribute]
300pub fn descended_from(args: TokenStream, input: TokenStream) -> TokenStream {
301    let _args = parse_macro_input!(args as parse::DescendedFromArgs);
302    let input = proc_macro2::TokenStream::from(input);
303
304    quote! { #input }.into()
305}
306
307/// Register a code-tier witness: declare that this test/proptest function
308/// defends against a known failure-class (ADR-029).
309///
310/// # Arguments
311///
312/// Single positional argument: the antigen type the witness defends.
313///
314/// # Example
315///
316/// ```ignore
317/// use antigen::defended_by;
318///
319/// #[test]
320/// #[defended_by(ParallelStateTrackersDiverge)]
321/// fn bijection_audit_hints_const_matches_enum() {
322///     // exercises both sides of the parallel state
323/// }
324/// ```
325///
326/// # Immunity is observed, not declared
327///
328/// `#[defended_by(X)]` is a *registration of evidence*, not a verdict. The test
329/// declares **what it defends**; `cargo antigen audit` determines **whether it
330/// defends it** — by cross-referencing the registered witness to the
331/// `#[presents(X)]` sites it covers and grading the witness tier. No code site
332/// ever claims "I am immune to X"; the audit tool is the single authoritative
333/// voice that reports `defended` / `undefended` / `substrate-gap`. This is the
334/// migration target for the code-tier (`witness = fn`) channel of the deprecated
335/// `#[immune]` macro.
336///
337/// # Scope
338///
339/// `#[defended_by]` is for **code-tier witnesses only** — `#[test]` functions
340/// and proptest properties. Site-attached evidence (a substrate predicate or a
341/// phantom-type proof) folds into `#[presents]` via `requires =` / `proof =`,
342/// not here (ADR-029 R5 discriminator: evidence belongs where it is).
343///
344/// Like the other antigen markers this is a pure identity transform plus a
345/// discoverable `#[doc = " antigen:defended_by:v1:<antigen>"]` marker that
346/// `cargo antigen scan` reads to register the witness.
347#[proc_macro_attribute]
348pub fn defended_by(args: TokenStream, input: TokenStream) -> TokenStream {
349    let parsed = parse_macro_input!(args as parse::DefendedByArgs);
350    let input = proc_macro2::TokenStream::from(input);
351
352    // Emit a discoverable doc marker carrying the bare antigen type name, so
353    // `cargo antigen scan` can register the witness via source-walking without
354    // a binary link — the same channel `#[immune(requires=...)]` uses for its
355    // predicate JSON (ADR-019 §P3b), here carrying just the failure-class name.
356    let antigen_name = parsed
357        .antigen
358        .segments
359        .last()
360        .map_or_else(String::new, |s| s.ident.to_string());
361    let marker = format!(" antigen:defended_by:v1:{antigen_name}");
362    quote! {
363        #[doc = #marker]
364        #input
365    }
366    .into()
367}
368
369/// Mark a site as a deliberate, non-vulnerable match against an antigen's
370/// fingerprint. Per ADR-011.
371///
372/// # Arguments
373///
374/// - The antigen type name (positional)
375/// - `rationale = "..."` (required) — human-readable justification; empty
376///   string is rejected
377/// - `until = "..."` (optional) — expiry tag (e.g., `"v1.0"`); empty string
378///   is rejected (per the reciprocal Phase 1-8 rule)
379/// - `see = [...]` (optional) — open-vocabulary string array of references
380///
381/// # Example
382///
383/// ```ignore
384/// use antigen::antigen_tolerance;
385///
386/// #[antigen_tolerance(
387///     PolarityInvertedClassMeet,
388///     rationale = "This test fixture deliberately constructs the failure \
389///                  pattern to verify the witness catches it.",
390///     until = "v1.0",
391///     see = ["GAP-BIT-EXACT-1"],
392/// )]
393/// fn test_polarity_inversion_caught() { /* ... */ }
394/// ```
395///
396/// `cargo antigen scan` recognizes tolerance markers as explicit
397/// acknowledgments of fingerprint matches; tolerated sites are reported in
398/// a separate category from unaddressed presentations.
399#[proc_macro_attribute]
400pub fn antigen_tolerance(args: TokenStream, input: TokenStream) -> TokenStream {
401    let args = parse_macro_input!(args as parse::ToleranceArgs);
402    let input = proc_macro2::TokenStream::from(input);
403
404    if let Err(e) = args.validate() {
405        return e.to_compile_error().into();
406    }
407
408    args.requires_json().map_or_else(
409        || quote! { #input }.into(),
410        |json| {
411            let marker = format!(" antigen:requires:v1:{json}");
412            quote! {
413                #[doc = #marker]
414                #input
415            }
416            .into()
417        },
418    )
419}
420
421/// Declare that a proc-macro / `macro_rules` emits code presenting an antigen.
422/// Per ADR-014 — the fifth core macro.
423///
424/// `cargo antigen scan` parses the source-level AST: it sees a `#[derive(Foo)]`
425/// invocation but NOT the code the `Foo` derive generates. Failure-classes that
426/// manifest only in macro-generated code are invisible to the scan. The fix
427/// lives at the macro author's side — they know what their macro emits, so they
428/// declare it. The scan then connects this declaration (on the macro
429/// DEFINITION) to every macro INVOCATION and surfaces a synthetic presentation
430/// at the invocation site.
431///
432/// # Arguments
433///
434/// - antigen type name (positional, required) — the failure-class the expansion presents
435/// - `rationale = "..."` (required, non-empty) — why the expansion presents this
436///   class + what the user should verify (mirrors ADR-011 tolerance)
437/// - `witness_template = "..."` (optional, v2) — path hint for a witness skeleton
438/// - `if_attr_present = "..."` (optional, v2) — conditional-generation guard
439///
440/// # Example
441///
442/// ```ignore
443/// use antigen::antigen_generates;
444///
445/// #[antigen_generates(
446///     PanickingInDrop,
447///     rationale = "This derive emits a Drop impl that may panic if the inner \
448///                  type's destructor panics; users should verify their inner \
449///                  types are panic-safe in Drop.",
450/// )]
451/// #[proc_macro_derive(SomeDerive)]
452/// pub fn some_derive(input: TokenStream) -> TokenStream { /* ... */ }
453/// ```
454///
455/// Like the other markers this is a pure identity transform plus a discoverable
456/// `#[doc = " antigen:generates:v1:<antigen>"]` marker that `cargo antigen scan`
457/// reads to register the macro as a generator of the named failure-class.
458#[proc_macro_attribute]
459pub fn antigen_generates(args: TokenStream, input: TokenStream) -> TokenStream {
460    let parsed = parse_macro_input!(args as parse::GeneratesArgs);
461    let input = proc_macro2::TokenStream::from(input);
462
463    if let Err(e) = parsed.validate() {
464        return e.to_compile_error().into();
465    }
466
467    // Emit a discoverable doc marker carrying the bare antigen type name. The
468    // scan's source-walk reads this on the macro DEFINITION and connects it to
469    // invocation sites (same no-binary-link channel as the other markers). The
470    // `rationale` is validated here (enforcing the ADR-014 discipline that a
471    // generation claim must be justified) but is not needed downstream by the
472    // synthesis pass, which only needs the antigen type to emit the presentation.
473    let marker = format!(" antigen:generates:v1:{}", parsed.antigen_name());
474    quote! {
475        #[doc = #marker]
476        #input
477    }
478    .into()
479}
480
481// ============================================================================
482// Marked-Unknown Plane (ADR-041) — #[aura] / #[dread] / #[red_flag]
483//
484// Three declarable ⊥ markers on the magnitude × existence-certainty plane (OFF
485// the dial's classification axis). Each FIXES its plane corner; the author
486// supplies only the REQUIRED `trigger` (guard 3). Like the other markers, each
487// is a pure identity transform plus a discoverable `#[doc = " antigen:marked-
488// unknown:v1:<json>"]` marker the scan reads (the no-binary-link channel) and
489// emits into the SCAN-TIME half of ADR-039's Finding.
490// ============================================================================
491
492/// Render the shared marked-unknown doc-marker for one corner + trigger.
493///
494/// `magnitude` ∈ {smell, aura, dread}; `existence_certainty` ∈ {unsure, sure}
495/// (the kebab forms `Magnitude`/`ExistenceCertainty` serialize to). The trigger
496/// is JSON-escaped so a quote/backslash in the felt-note can't corrupt the
497/// marker the scanner re-parses.
498fn marked_unknown_marker(marker: &str, magnitude: &str, certainty: &str, trigger: &str) -> String {
499    // JSON-escape the trigger string (the only free-text field). Per RFC 8259 a
500    // string MUST escape `"`, `\`, and every control char U+0000–U+001F — the
501    // short forms (\n/\t/\r/\b/\f) where they exist, else the `\u00XX` form. The
502    // earlier hand-rolled version passed un-short-formed control chars through
503    // raw, producing INVALID JSON the scanner's re-parse would reject — a silent
504    // producer-correctness bug (antigen's own class), fixed here. (The macro crate
505    // carries no serde dep, so this is the dependency-free equivalent of
506    // `serde_json::to_string`'s string escaping.)
507    let mut escaped = String::with_capacity(trigger.len());
508    for c in trigger.chars() {
509        match c {
510            '"' => escaped.push_str("\\\""),
511            '\\' => escaped.push_str("\\\\"),
512            '\n' => escaped.push_str("\\n"),
513            '\t' => escaped.push_str("\\t"),
514            '\r' => escaped.push_str("\\r"),
515            '\u{08}' => escaped.push_str("\\b"),
516            '\u{0c}' => escaped.push_str("\\f"),
517            // Remaining control chars (U+0000–U+001F) have no short form → \u00XX.
518            // Build it without `format!` (a String already; push the digits).
519            c if (c as u32) < 0x20 => {
520                const HEX: &[u8; 16] = b"0123456789abcdef";
521                let b = c as u8;
522                escaped.push_str("\\u00");
523                escaped.push(HEX[(b >> 4) as usize] as char);
524                escaped.push(HEX[(b & 0x0f) as usize] as char);
525            },
526            other => escaped.push(other),
527        }
528    }
529    format!(
530        " antigen:marked-unknown:v1:{{\"marker\":\"{marker}\",\"magnitude\":\"{magnitude}\",\"existence_certainty\":\"{certainty}\",\"trigger\":\"{escaped}\"}}"
531    )
532}
533
534/// Expand one marker macro: parse → stamp name → validate (required trigger) →
535/// emit `input` unchanged + the discoverable doc-marker for the fixed corner.
536fn expand_marker(
537    args: TokenStream,
538    input: TokenStream,
539    marker: &'static str,
540    magnitude: &'static str,
541    certainty: &'static str,
542) -> TokenStream {
543    let parsed = parse_macro_input!(args as parse::MarkerArgs).with_marker(marker);
544    let input = proc_macro2::TokenStream::from(input);
545    if let Err(e) = parsed.validate() {
546        return e.to_compile_error().into();
547    }
548    let doc = marked_unknown_marker(marker, magnitude, certainty, parsed.trigger_str());
549    quote! {
550        #[doc = #doc]
551        #input
552    }
553    .into()
554}
555
556/// `#[aura(trigger = "...")]` — the **light** marked-unknown (low magnitude):
557/// "something *may* be off here, can't name it, check later." (ADR-041.)
558///
559/// Surfaces at the dial's non-gating floor; never gates, never nags; an untouched
560/// `#[aura]` is a *mild* substrate-smell. The `trigger` is **required** (guard 3).
561///
562/// # Example
563///
564/// ```ignore
565/// use antigen::aura;
566///
567/// #[aura(trigger = "this retry loop has no jitter; under load it might thundering-herd")]
568/// fn retry_request() { /* ... */ }
569/// ```
570#[proc_macro_attribute]
571pub fn aura(args: TokenStream, input: TokenStream) -> TokenStream {
572    expand_marker(args, input, "aura", "aura", "unsure")
573}
574
575/// `#[dread(trigger = "...")]` — high magnitude, **low** existence-certainty
576/// (the *angor animi* corner): "something *is* wrong here, I can't name it, look
577/// now." Scared-but-unsure. (ADR-041.)
578///
579/// Surfaces at the dial's non-gating floor; never gates, never nags. The
580/// `trigger` is **required** (guard 3) — a triggerless `#[dread]` is rejected.
581///
582/// # Example
583///
584/// ```ignore
585/// use antigen::dread;
586///
587/// #[dread(trigger = "the teardown drops the guard before the flush; \
588///                    I can't prove a leak but the ordering feels wrong")]
589/// impl Drop for Connection { /* ... */ }
590/// ```
591#[proc_macro_attribute]
592pub fn dread(args: TokenStream, input: TokenStream) -> TokenStream {
593    expand_marker(args, input, "dread", "dread", "unsure")
594}
595
596/// `#[red_flag(trigger = "...")]` — **high** existence-certainty, unnameable.
597///
598/// The clinical sense-of-alarm corner: "I'm *sure* something is wrong here, I
599/// can't name it, act now." The sure-but-unnameable corner; **auto-escalates on
600/// first match** (its whole point). (ADR-041.)
601///
602/// The one marker that escalates rather than surfacing as a mild smell — because
603/// its defining axis is high certainty-that-something-is-wrong. The `trigger` is
604/// **required** (guard 3).
605///
606/// # Example
607///
608/// ```ignore
609/// use antigen::red_flag;
610///
611/// #[red_flag(trigger = "this auth check can be reached with an empty token in \
612///                       the cache-hit path; I'm sure this is exploitable")]
613/// fn authorize(token: &Token) -> bool { /* ... */ }
614/// ```
615#[proc_macro_attribute]
616pub fn red_flag(args: TokenStream, input: TokenStream) -> TokenStream {
617    expand_marker(args, input, "red-flag", "dread", "sure")
618}
619
620// ============================================================================
621// Deferred-Defense Family (ADR-023)
622// ============================================================================
623
624/// Declare an anergy posture: deferred-but-muted, with required time-bound
625/// and aging escalation.
626///
627/// # Arguments
628///
629/// - Antigen type name (optional positional)
630/// - `reason = "..."` (required) — minimum 20 characters
631/// - `until = "YYYY-MM-DD"` (required) — expiry date; `until` is not
632///   optional; anergy without time-bound degrades to silent tolerance
633/// - `expected_co_stimulation = "..."` (optional) — advisory-only; names the
634///   condition that would re-engage immune response; NOT machine-verified
635/// - `signed_by = "..."` (optional)
636///
637/// # Audit hints emitted by `cargo antigen audit`
638///
639/// - `anergy-active` — until has not passed
640/// - `anergy-co-stimulation-not-arrived` — past `until` date; awaiting trigger
641/// - `anergy-stale` — past `until` + grace period; escalates to warn/error
642///
643/// # Example
644///
645/// ```ignore
646/// use antigen::anergy;
647///
648/// #[anergy(
649///     MyFailureClass,
650///     reason = "Upstream dependency ships v2 in Q4; immunity blocked on that upgrade.",
651///     until = "2026-12-31",
652///     expected_co_stimulation = "upstream-v2-upgrade-complete",
653/// )]
654/// pub fn depends_on_upstream() { /* ... */ }
655/// ```
656#[proc_macro_attribute]
657pub fn anergy(args: TokenStream, input: TokenStream) -> TokenStream {
658    let args = parse_macro_input!(args as parse::AnergyArgs);
659    let input = proc_macro2::TokenStream::from(input);
660
661    if let Err(e) = args.validate() {
662        return e.to_compile_error().into();
663    }
664
665    quote! { #input }.into()
666}
667
668/// Declare surgical immunosuppression with a hard duration cap enforced at
669/// parse time.
670///
671/// # Arguments
672///
673/// - Antigen type name (optional positional)
674/// - `rationale = "..."` (required) — minimum 20 characters
675/// - `until = "YYYY-MM-DD"` (required) — suppression deadline
676/// - `since = "YYYY-MM-DD"` (optional) — suppression start; defaults to today
677///   for cap calculation
678/// - `duration_cap = N` (optional) — override cap in days; workspace default
679///   is 90 days (ADR-023 `immunosuppress_duration_cap`)
680/// - `signed_by = "..."` (optional)
681///
682/// # Parse-time enforcement
683///
684/// A COMPILE ERROR is emitted if `until - since > duration_cap`. This closes
685/// the audit-only gap — the cap cannot be bypassed by suppressing the audit.
686///
687/// # Audit hints
688///
689/// - `immunosuppress-active` — suppression current
690/// - `immunosuppress-expired` — past `until`
691/// - `immunosuppress-duration-cap-exceeded` — (should not occur post-compile;
692///   retained for audit re-evaluation of pre-cap-enforcement code)
693///
694/// # Example
695///
696/// ```ignore
697/// use antigen::immunosuppress;
698///
699/// #[immunosuppress(
700///     MyFailureClass,
701///     rationale = "CI matrix cannot run the verification harness until infra migration completes.",
702///     until = "2026-09-01",
703/// )]
704/// pub fn ci_constrained_path() { /* ... */ }
705/// ```
706#[proc_macro_attribute]
707pub fn immunosuppress(args: TokenStream, input: TokenStream) -> TokenStream {
708    let args = parse_macro_input!(args as parse::ImmunosuppressArgs);
709    let input = proc_macro2::TokenStream::from(input);
710
711    if let Err(e) = args.validate() {
712        return e.to_compile_error().into();
713    }
714
715    quote! { #input }.into()
716}
717
718/// Declare an intentional exposure exercise with structural isolation.
719///
720/// # Structural isolation (two-layer approach)
721///
722/// Primary isolation: wrap `#[poxparty]` sites inside a
723/// `#[cfg(feature = "antigen-poxparty")]` module or item. When the feature
724/// is inactive, `rustc` strips the block before proc-macro expansion runs,
725/// so `#[poxparty]` never fires in production builds.
726///
727/// Secondary (best-effort): the macro checks `CARGO_FEATURE_ANTIGEN_POXPARTY`
728/// at expansion time. This check is authoritative when Cargo propagates the
729/// variable (some CI configurations and future Cargo versions). When not
730/// propagated, the cfg gate provides the structural guarantee.
731///
732/// The `antigen-poxparty` feature MUST NOT be in the crate's default feature
733/// set. `cargo antigen scan` emits `poxparty-outside-isolation` for any
734/// `#[poxparty]` site found outside a cfg-gated context at audit time.
735///
736/// # Arguments
737///
738/// - Antigen type name (optional positional)
739/// - `exercise_type = "..."` (required) — minimum 20 characters; describes
740///   the controlled exposure exercise
741/// - `until = "YYYY-MM-DD"` (required) — exercise deadline
742/// - `name = "..."` (optional) — descriptive exercise name
743/// - `rationale = "..."` (optional) — additional context
744/// - `signed_by = "..."` (optional)
745///
746/// # Audit hints
747///
748/// - `poxparty-active` — exercise in progress
749/// - `poxparty-outcome-pending` — past `until`; outcome not yet recorded
750/// - `poxparty-outcome-recorded` — outcome attestation present
751/// - `poxparty-outside-isolation` — site found outside cfg-gated scope
752///   (should not occur if the compile-time check holds)
753///
754/// # Example
755///
756/// ```ignore
757/// // In a module gated by #[cfg(feature = "antigen-poxparty")]:
758/// use antigen::poxparty;
759///
760/// #[poxparty(
761///     MyFailureClass,
762///     exercise_type = "Fault injection: saturate the retry buffer to verify backpressure handling.",
763///     until = "2026-10-01",
764/// )]
765/// pub fn chaos_test_retry_saturation() { /* ... */ }
766/// ```
767#[proc_macro_attribute]
768pub fn poxparty(args: TokenStream, input: TokenStream) -> TokenStream {
769    // Structural isolation — two-layer approach:
770    //
771    // Layer 1 (primary): `#[cfg(feature = "antigen-poxparty")]` on the
772    // containing module/item. When the feature is inactive, `rustc` strips
773    // the entire block before proc-macro expansion — `#[poxparty]` never
774    // runs. This is the primary structural isolation mechanism. Callers
775    // MUST wrap poxparty sites in a cfg gate.
776    //
777    // Layer 2 (env-var check, best-effort): Cargo sets CARGO_FEATURE_*
778    // for build scripts but not reliably for proc-macro expansion in all
779    // versions/configurations. We check the var as a secondary guard — it
780    // fires when callers invoke the macro outside a cfg gate AND the var
781    // happens to be absent. When Cargo IS propagating the var (e.g., some
782    // CI configurations, or when the caller sets it explicitly), this
783    // check is authoritative. When it isn't propagated, the cfg gate is
784    // the load-bearing isolation.
785    //
786    // Per ADR-023 §Known limitations: "env-var propagation to proc-macro
787    // expansion environment is Cargo-version-dependent; cfg gate is the
788    // primary structural isolation."
789    if std::env::var("CARGO_FEATURE_ANTIGEN_POXPARTY").is_err() {
790        // Best-effort: emit a warning-level doc comment rather than a hard
791        // compile error, since the env var may simply not be propagated.
792        // The cfg gate is the primary structural check.
793        // In environments where the var IS set (e.g., future Cargo versions
794        // that propagate it, or explicit CI configuration), this would be a
795        // compile error. For now, the scan-side `poxparty-outside-isolation`
796        // hint provides the audit-time enforcement.
797        //
798        // INTENTIONAL: no compile error here — see above.
799    }
800
801    let args = parse_macro_input!(args as parse::PoxpartyArgs);
802    let input = proc_macro2::TokenStream::from(input);
803
804    if let Err(e) = args.validate() {
805        return e.to_compile_error().into();
806    }
807
808    quote! { #input }.into()
809}
810
811// ============================================================================
812// Convergent-Evidence Family (ADR-024)
813// ============================================================================
814
815/// Declare convergent multi-modality evidence backing a defense claim.
816///
817/// `#[diagnostic(modalities = [...], min_independent = N)]` asserts that
818/// at least `N` distinct [`WitnessClass`](https://docs.rs/antigen) categories
819/// converge on this defense. Per ADR-024 §Decision, the
820/// count is over distinct CLASSES, not raw witness count — running the
821/// same kind of test in triplicate doesn't add evidence.
822///
823/// # Biology grounding
824///
825/// `#[diagnostic]` is grounded in **clinical medicine**, not immunology
826/// proper. The metaphor is the diagnostic workup pattern from
827/// differential-diagnosis literature: a clinician confirms a diagnosis
828/// when independent modalities (history, physical, imaging, labs)
829/// converge on the same finding. A single modality is suggestive; the
830/// convergence is what carries clinical confidence. Per ADR-024 §Biology
831/// grounding — dual-axis honesty, `#[diagnostic]` sits on the
832/// clinical-medicine axis alongside `#[panel]`, `#[ddx]`, `#[rx]`,
833/// `#[triage]`, `#[refer]`, `#[biopsy]`, `#[culture]`, `#[quarantine]`,
834/// and `#[recurrence_anchor]`. The convergent-evidence family draws on
835/// both immunology (clonal expansion, `IgG` class-switching) and clinical
836/// medicine (diagnostic workup) — the dual axis is acknowledged
837/// explicitly rather than collapsed.
838///
839/// # Arguments
840///
841/// - `modalities = [WitnessClass::X, ...]` (required) — non-empty list
842/// - `min_independent = N` (required, > 0) — distinct-class floor; the
843///   parser rejects `min_independent` exceeding the number of distinct
844///   classes (vacuously unsatisfiable claim).
845///
846/// # Audit hints
847///
848/// - `diagnostic-modality-insufficient` — fewer modalities than the floor
849/// - `diagnostic-modalities-class-collapsed` — all witnesses share one class
850/// - `diagnostic-modalities-empty` — empty modalities list
851///
852/// # Example
853///
854/// ```ignore
855/// use antigen::{antigen, diagnostic, WitnessClass};
856///
857/// #[diagnostic(
858///     modalities = [WitnessClass::PropertyTest, WitnessClass::FormalVerification],
859///     min_independent = 2,
860/// )]
861/// pub fn checked_arithmetic_sum(a: i64, b: i64) -> Option<i64> {
862///     a.checked_add(b)
863/// }
864/// ```
865#[proc_macro_attribute]
866pub fn diagnostic(args: TokenStream, input: TokenStream) -> TokenStream {
867    let args = parse_macro_input!(args as parse::DiagnosticArgs);
868    let input = proc_macro2::TokenStream::from(input);
869    if let Err(e) = args.validate() {
870        return e.to_compile_error().into();
871    }
872    quote! { #input }.into()
873}
874
875/// Declare iterated witness evaluation (B-cell clonal expansion analog).
876///
877/// `#[clonal(witness = ..., iterations = N, seed = SeedKind::...)]`
878/// asserts that a witness is run with many independent iterations.
879/// Per ADR-024 §Decision, `seed = SeedKind::Fixed(_)`
880/// is a COMPILE ERROR — a fixed seed makes "independent iterations" a
881/// contradiction.
882///
883/// # Arguments
884///
885/// - `witness = <ident>` (required) — per-iteration witness function
886/// - `iterations = N` (required, > 0)
887/// - `seed = SeedKind::X` (optional; default `Random`) — non-deterministic
888///   variants accepted: `Random`, `EntropyFromCi`, `TimestampSeeded`.
889///   `SeedKind::Fixed(_)` rejected at parse time.
890///
891/// # Audit hints
892///
893/// - `clonal-fixed-seed-detected` — parse-time error (above)
894/// - `clonal-iterations-below-threshold` — N below workspace floor
895///
896/// # Example
897///
898/// ```ignore
899/// use antigen::{clonal, SeedKind};
900///
901/// #[clonal(witness = sum_property, iterations = 10_000, seed = SeedKind::Random)]
902/// pub fn checked_arithmetic_sum(a: i64, b: i64) -> Option<i64> {
903///     a.checked_add(b)
904/// }
905/// ```
906#[proc_macro_attribute]
907pub fn clonal(args: TokenStream, input: TokenStream) -> TokenStream {
908    let args = parse_macro_input!(args as parse::ClonalArgs);
909    let input = proc_macro2::TokenStream::from(input);
910    if let Err(e) = args.validate() {
911        return e.to_compile_error().into();
912    }
913    quote! { #input }.into()
914}
915
916/// Declare IgG-class affinity-matured evidence (re-attestation history).
917///
918/// `#[igg(witnesses = [...], historical_span = N, min_reattestations = N)]`
919/// asserts that the defense has been re-attested across a time span.
920/// Per ADR-024 §Decision, source-independence is
921/// NOMINAL only — different signer identity strings are not structural
922/// proof of independent sources.
923///
924/// # Arguments
925///
926/// - `witnesses = [...]` (required non-empty)
927/// - `historical_span = N` (required, > 0; days)
928/// - `min_reattestations = N` (required, > 0)
929///
930/// # Audit hints
931///
932/// - `igg-identity-collapse-warning` — same signer across reattestations
933/// - `igg-span-too-short` — historical span below floor
934/// - `igg-reattestations-insufficient` — fewer reattestations than floor
935#[proc_macro_attribute]
936pub fn igg(args: TokenStream, input: TokenStream) -> TokenStream {
937    let args = parse_macro_input!(args as parse::IggArgs);
938    let input = proc_macro2::TokenStream::from(input);
939    if let Err(e) = args.validate() {
940        return e.to_compile_error().into();
941    }
942    quote! { #input }.into()
943}
944
945/// Declare crossreactive coverage — one defense covers related antigens.
946///
947/// `#[crossreactive(fingerprints = [...])]` asserts that the annotated
948/// item's defense applies to multiple antigen fingerprints simultaneously
949/// (analogous to a crossreactive antibody binding related epitopes).
950///
951/// # Arguments
952///
953/// - `fingerprints = [...]` (required non-empty list of strings)
954///
955/// # Audit hints
956///
957/// - `crossreactive-fingerprint-unresolved` — fingerprint doesn't match
958///   any known antigen
959#[proc_macro_attribute]
960pub fn crossreactive(args: TokenStream, input: TokenStream) -> TokenStream {
961    let args = parse_macro_input!(args as parse::CrossreactiveArgs);
962    let input = proc_macro2::TokenStream::from(input);
963    if let Err(e) = args.validate() {
964        return e.to_compile_error().into();
965    }
966    quote! { #input }.into()
967}
968
969/// Declare polyclonal evidence — many independent lineages converge.
970///
971/// `#[polyclonal]` is a marker primitive (no required args) declaring
972/// that the defense rests on multiple independent witness lineages.
973/// Distinct from `#[diagnostic]`: polyclonal emphasizes LINEAGE diversity
974/// (different witness derivations) rather than MODALITY diversity
975/// (different witness classes).
976///
977/// # Audit hints (planned — not yet emitted)
978///
979/// - `polyclonal-insufficient-lineages` — fewer lineages than a configured
980///   floor. **Not implemented at v0.2**: `#[polyclonal]` is a pure
981///   documentation marker today; the `PolyclonalInsufficientLineages`
982///   `AuditHint` variant exists but is never produced (no lineage-counting
983///   audit pass yet). This hint is a forward-plan, not current behavior — do
984///   not rely on it firing.
985#[proc_macro_attribute]
986pub fn polyclonal(args: TokenStream, input: TokenStream) -> TokenStream {
987    let args = parse_macro_input!(args as parse::PolyclonalArgs);
988    let input = proc_macro2::TokenStream::from(input);
989    if let Err(e) = args.validate() {
990        return e.to_compile_error().into();
991    }
992    quote! { #input }.into()
993}
994
995/// Declare monoclonal evidence — single independent lineage.
996///
997/// `#[monoclonal]` is the structural contrast to `#[polyclonal]`. The
998/// monoclonal posture is honest about resting on a single lineage; the
999/// audit treats it as a documentary acknowledgement rather than a
1000/// failing.
1001#[proc_macro_attribute]
1002pub fn monoclonal(args: TokenStream, input: TokenStream) -> TokenStream {
1003    let args = parse_macro_input!(args as parse::MonoclonalArgs);
1004    let input = proc_macro2::TokenStream::from(input);
1005    if let Err(e) = args.validate() {
1006        return e.to_compile_error().into();
1007    }
1008    quote! { #input }.into()
1009}
1010
1011/// Declare ADCC (antibody-dependent cellular cytotoxicity) — multi-
1012/// mechanism convergent defense.
1013///
1014/// `#[adcc]` asserts that the defense combines antibody-style witness
1015/// (declaration + check) AND cellular-effector witness (runtime
1016/// behavioral check) via different mechanisms. The marker primitive
1017/// surfaces the structural commitment to multi-mechanism defense.
1018///
1019/// # Audit hints (planned — not yet emitted)
1020///
1021/// - `adcc-single-mechanism-only` — only one of the two mechanisms
1022///   detectable on the site. **Not implemented at v0.2**: `#[adcc]` is a pure
1023///   documentation marker today; the `AdccSingleMechanismOnly` `AuditHint`
1024///   variant exists but is never produced (no mechanism-detection audit pass
1025///   yet). This hint is a forward-plan, not current behavior — do not rely on
1026///   it firing.
1027#[proc_macro_attribute]
1028pub fn adcc(args: TokenStream, input: TokenStream) -> TokenStream {
1029    let args = parse_macro_input!(args as parse::AdccArgs);
1030    let input = proc_macro2::TokenStream::from(input);
1031    if let Err(e) = args.validate() {
1032        return e.to_compile_error().into();
1033    }
1034    quote! { #input }.into()
1035}
1036
1037// ============================================================================
1038// Recurrent-Emergence Family (ADR-024)
1039//
1040// Six present-looking primitives: #[itch], #[recurrence_anchor],
1041// #[crystallize], #[chronic], #[saturate], #[strand]. Cognitive-organizational
1042// grounding for itch/saturate/crystallize/strand; immunology-proper for
1043// chronic; clinical-medicine for recurrence_anchor.
1044// ============================================================================
1045
1046/// Declare a below-threshold noticing of a pattern (ADR-024 recurrent family).
1047///
1048/// `#[itch(name, antigen?, description, threshold?)]` marks a
1049/// cognitive-organizational observation: a pattern has been noticed but
1050/// has not yet crossed the threshold into a formal antigen declaration.
1051/// Per ADR-024 §Disambiguation: distinct from `#[anergy]` (ADR-023,
1052/// intentional non-defense while waiting) — itch is pre-commitment
1053/// noticing, anergy is deliberate defer.
1054///
1055/// # Biology grounding
1056///
1057/// **Cognitive-organizational** axis per ADR-024 §Biology grounding —
1058/// dual-axis honesty. Not an immunology-proper cognate; the metaphor is
1059/// drawn from how teams notice patterns before formalizing them.
1060///
1061/// # Arguments
1062///
1063/// - `name = "<slug>"` (required) — kebab-case identifier
1064/// - `antigen = <Path>` (optional) — failure-class path, if known
1065/// - `description = "..."` (required, ≥10 chars) — what is being noticed
1066/// - `threshold = "..."` (optional) — what would cause crystallize-promotion
1067///
1068/// # Audit hints
1069///
1070/// - `itch-noticed-not-anchored` — no antigen path; unlinked observation
1071#[proc_macro_attribute]
1072pub fn itch(args: TokenStream, input: TokenStream) -> TokenStream {
1073    let args = parse_macro_input!(args as parse::ItchArgs);
1074    let input = proc_macro2::TokenStream::from(input);
1075    if let Err(e) = args.validate() {
1076        return e.to_compile_error().into();
1077    }
1078    quote! { #input }.into()
1079}
1080
1081/// Declare formal recognition of a cross-substrate recurrent failure-class
1082/// (ADR-024 recurrent family).
1083///
1084/// `#[recurrence_anchor(antigen, instances, since, rationale)]` commits to
1085/// formal recognition of a pattern that has crossed the substrate-evidence
1086/// threshold. Per ADR-024 §Disambiguation: distinct from `#[chronic]`
1087/// (low-level persistent, NOT cross-substrate) — `recurrence_anchor` is
1088/// cross-substrate-threshold-reached.
1089///
1090/// # Biology grounding
1091///
1092/// **Clinical-medicine** axis per ADR-024 §Biology grounding. Analogous
1093/// to a clinical diagnosis after recurrent symptoms cross the threshold
1094/// for formal recognition.
1095///
1096/// # Arguments
1097///
1098/// All four fields REQUIRED:
1099/// - `antigen = <Path>` — failure-class path being anchored
1100/// - `instances = N` (positive `u32`) — how many recurrences observed
1101/// - `since = "<date-or-version>"` — first detected instance anchor
1102/// - `rationale = "..."` (≥20 chars) — clinical-diagnosis-grade rationale
1103///
1104/// # Audit hints
1105///
1106/// - `recurrence-threshold-reached-no-action` — anchor declared but no
1107///   downstream `#[immune]`/`#[presents]` registered
1108#[proc_macro_attribute]
1109pub fn recurrence_anchor(args: TokenStream, input: TokenStream) -> TokenStream {
1110    let args = parse_macro_input!(args as parse::RecurrenceAnchorArgs);
1111    let input = proc_macro2::TokenStream::from(input);
1112    if let Err(e) = args.validate() {
1113        return e.to_compile_error().into();
1114    }
1115    quote! { #input }.into()
1116}
1117
1118/// Declare the promotion event from itch-cluster to formal failure-class
1119/// (ADR-024 recurrent family).
1120///
1121/// `#[crystallize(name, from_itches?, antigen?, summary)]` records the
1122/// moment a pattern of noticings crystallizes into a formal antigen.
1123/// Captures the transition from informal noticing to formal recognition.
1124///
1125/// # Biology grounding
1126///
1127/// **Cognitive-organizational** axis per ADR-024 §Biology grounding.
1128///
1129/// # Arguments
1130///
1131/// - `name = "<slug>"` (required)
1132/// - `from_itches = [Ident, ...]` (optional) — `#[itch]` idents this
1133///   crystallizes from
1134/// - `antigen = <Path>` (optional) — the formal antigen this crystallizes
1135///   into
1136/// - `summary = "..."` (required, ≥10 chars)
1137///
1138/// # Audit hints
1139///
1140/// - `crystallize-without-antigen` — crystallized but no formal antigen
1141///   path registered yet
1142#[proc_macro_attribute]
1143pub fn crystallize(args: TokenStream, input: TokenStream) -> TokenStream {
1144    let args = parse_macro_input!(args as parse::CrystallizeArgs);
1145    let input = proc_macro2::TokenStream::from(input);
1146    if let Err(e) = args.validate() {
1147        return e.to_compile_error().into();
1148    }
1149    quote! { #input }.into()
1150}
1151
1152/// Declare a low-level persistent failure-class signal (ADR-024 recurrent
1153/// family).
1154///
1155/// `#[chronic(antigen, since, status?, managed_by?)]` marks a sustained
1156/// signal that has NOT crossed the cross-substrate-recurrence threshold
1157/// per ADR-024 §Disambiguation. Distinct from `#[recurrence_anchor]`.
1158///
1159/// # Biology grounding
1160///
1161/// **Immunology-proper** axis per ADR-024 §Biology grounding — chronic
1162/// inflammation is the biology cognate: sustained low-level immune
1163/// activity without acute recurrence.
1164///
1165/// # Arguments
1166///
1167/// - `antigen = <Path>` (required) — failure-class being marked chronic
1168/// - `since = "<date-or-version>"` (required) — when first observed
1169/// - `status = "..."` (optional) — current status description
1170/// - `managed_by = "..."` (optional) — team or role managing the state
1171///
1172/// # Audit hints
1173///
1174/// - `chronic-signal-unmanaged` — no `managed_by` after N versions
1175/// - `chronic-signal-past-review-date`
1176#[proc_macro_attribute]
1177pub fn chronic(args: TokenStream, input: TokenStream) -> TokenStream {
1178    let args = parse_macro_input!(args as parse::ChronicArgs);
1179    let input = proc_macro2::TokenStream::from(input);
1180    if let Err(e) = args.validate() {
1181        return e.to_compile_error().into();
1182    }
1183    quote! { #input }.into()
1184}
1185
1186/// Declare a saturation-evidence contribution toward a recurrence threshold
1187/// (ADR-024 recurrent family).
1188///
1189/// `#[saturate(antigen?, contributing_to?, description)]` accumulates
1190/// evidence toward an anchor or itch without (yet) committing to either.
1191///
1192/// # Biology grounding
1193///
1194/// **Cognitive-organizational** axis per ADR-024 §Biology grounding.
1195///
1196/// # Arguments
1197///
1198/// - `antigen = <Path>` (optional)
1199/// - `contributing_to = "<slug>"` (optional) — `#[recurrence_anchor]` or
1200///   `#[itch]` slug this contributes to
1201/// - `description = "..."` (required, ≥10 chars)
1202///
1203/// # Audit hints
1204///
1205/// - `saturate-no-anchor` — no `contributing_to` target named
1206#[proc_macro_attribute]
1207pub fn saturate(args: TokenStream, input: TokenStream) -> TokenStream {
1208    let args = parse_macro_input!(args as parse::SaturateArgs);
1209    let input = proc_macro2::TokenStream::from(input);
1210    if let Err(e) = args.validate() {
1211        return e.to_compile_error().into();
1212    }
1213    quote! { #input }.into()
1214}
1215
1216/// Declare a thread of related noticing across substrates (ADR-024
1217/// recurrent family).
1218///
1219/// `#[strand(name, anchored_by?, description)]` groups noticings that
1220/// share a structural rhyme but haven't yet crystallized. May spawn
1221/// `#[itch]` or `#[recurrence_anchor]` as the strand thickens.
1222///
1223/// # Biology grounding
1224///
1225/// **Cognitive-organizational** axis per ADR-024 §Biology grounding.
1226///
1227/// # Arguments
1228///
1229/// - `name = "<slug>"` (required)
1230/// - `anchored_by = [Ident, ...]` (optional) — `#[itch]` or
1231///   `#[recurrence_anchor]` idents this strand spans
1232/// - `description = "..."` (required, ≥10 chars)
1233///
1234/// # Audit hints
1235///
1236/// - `strand-no-anchors` — nothing anchors this strand
1237#[proc_macro_attribute]
1238pub fn strand(args: TokenStream, input: TokenStream) -> TokenStream {
1239    let args = parse_macro_input!(args as parse::StrandArgs);
1240    let input = proc_macro2::TokenStream::from(input);
1241    if let Err(e) = args.validate() {
1242        return e.to_compile_error().into();
1243    }
1244    quote! { #input }.into()
1245}
1246
1247// ============================================================================
1248// Mucosal Boundary Family (ADR-027 + Amendment 1)
1249//
1250// Three primitives: #[mucosal], #[mucosal_delegate], #[mucosal_tolerant].
1251// MucosalKind sealed 13-variant set. Biology grounds the tier-claim + 4
1252// functional disciplines (NOT per-variant tissue mapping per ADR-027
1253// NON-NEGOTIABLE). Three response states: active defense / active tolerance
1254// / undecided — parallel to ADR-016 immune/tolerance/undeclared triad.
1255// ============================================================================
1256
1257/// Declare a trust boundary is actively defended at this site (ADR-027).
1258///
1259/// `#[mucosal(kind = MucosalKind::X, rationale = "...")]` marks a function
1260/// as the defended boundary for a kind of data/control flow crossing the
1261/// trust surface.
1262///
1263/// # Biology grounding
1264///
1265/// Per ADR-027 §Biology grounding (NON-NEGOTIABLE): biology grounds the
1266/// TIER-CLAIM (mucosal surfaces are a distinct immune tier with selective
1267/// permeability) + the prevention-at-boundary discipline (secretory-IgA-style
1268/// exclusion). It does NOT ground per-variant tissue mapping — the
1269/// `MucosalKind` taxonomy is software-engineering scope-selection by
1270/// data-flow type, not anatomy.
1271///
1272/// # Arguments
1273///
1274/// - `kind = MucosalKind::X` (required) — the boundary type (one of 13
1275///   sealed-set variants)
1276/// - `rationale = "..."` (required, ≥20 chars) — why this boundary is
1277///   defended
1278///
1279/// # Audit hints
1280///
1281/// - `mucosal-boundary-undefended`, `mucosal-kind-mismatch`,
1282///   `mucosal-rationale-insufficient`
1283#[proc_macro_attribute]
1284pub fn mucosal(args: TokenStream, input: TokenStream) -> TokenStream {
1285    let args = parse_macro_input!(args as parse::MucosalArgs);
1286    let input = proc_macro2::TokenStream::from(input);
1287    if let Err(e) = args.validate() {
1288        return e.to_compile_error().into();
1289    }
1290    quote! { #input }.into()
1291}
1292
1293/// Declare boundary discipline is delegated to a named handler (ADR-027 +
1294/// Amendment 1).
1295///
1296/// `#[mucosal_delegate(boundary = MucosalKind::X, handled_by = path::to::fn,
1297/// rationale = "...")]` declares that the boundary defense is performed by a
1298/// callee. Per ADR-027 Amendment 1 Change 4 `handled_by` is a path
1299/// expression (not a string) so typos fail at parse-time. Per Change 5 the
1300/// handler MUST carry a matching `#[mucosal(kind = X)]` — enforced at
1301/// audit-time via the three-tier diagnosis.
1302///
1303/// # Arguments
1304///
1305/// - `boundary = MucosalKind::X` (required) — the delegated boundary kind
1306/// - `handled_by = <path>` (required) — path to the handler function
1307/// - `rationale = "..."` (required, ≥20 chars)
1308///
1309/// # Audit hints (three-tier diagnosis per Change 5)
1310///
1311/// - `mucosal-discipline-delegate-target-missing` — handler path doesn't
1312///   resolve
1313/// - `mucosal-discipline-delegate-target-not-mucosal` — handler has no
1314///   `#[mucosal]`
1315/// - `mucosal-discipline-delegate-target-kind-mismatch` — handler's
1316///   `#[mucosal(kind)]` set doesn't include `boundary`
1317#[proc_macro_attribute]
1318pub fn mucosal_delegate(args: TokenStream, input: TokenStream) -> TokenStream {
1319    let args = parse_macro_input!(args as parse::MucosalDelegateArgs);
1320    let input = proc_macro2::TokenStream::from(input);
1321    if let Err(e) = args.validate() {
1322        return e.to_compile_error().into();
1323    }
1324    quote! { #input }.into()
1325}
1326
1327/// Declare a boundary is INTENTIONALLY permitted — active tolerance, not
1328/// absence of defense (ADR-027 Amendment 1 Change 6).
1329///
1330/// `#[mucosal_tolerant(kind, rationale, accepts, reviewed_by?, until?)]`
1331/// declares that a boundary deliberately accepts input without the full
1332/// `#[mucosal]` defense discipline, and documents WHY that's acceptable.
1333/// Without this primitive, intentional-tolerance boundaries are
1334/// indistinguishable from undefended ones in `mucosal-map --undefended`.
1335///
1336/// # Biology grounding
1337///
1338/// Per ADR-027 Amendment 1: biology distinguishes THREE mucosal response
1339/// states — active defense (`#[mucosal]`), active tolerance
1340/// (`#[mucosal_tolerant]`), and undecided (no declaration). Active
1341/// tolerance is NOT absence of response — it is antigen-specific
1342/// Treg-mediated suppression with its own cellular machinery (oral
1343/// tolerance, fetal-maternal interface). Parallel to ADR-016
1344/// `#[antigen_tolerance]` but at the BOUNDARY tier rather than the
1345/// failure-class tier.
1346///
1347/// # Arguments
1348///
1349/// - `kind = MucosalKind::X` (required)
1350/// - `rationale = "..."` (required, **≥40 chars** — higher than
1351///   `#[mucosal]`'s ≥20; tolerance-errors are silent/latent — no acute
1352///   signal catches a bad tolerance decision, so the up-front declaration
1353///   must carry more justification to compensate for the detection asymmetry)
1354/// - `accepts = "..."` (required, non-empty) — what the boundary accepts
1355///   as legitimate input
1356/// - `reviewed_by = "..."` (optional v0.2; recommended v0.2.1+)
1357/// - `until = "<RFC-3339 date>"` (optional) — review deadline
1358///
1359/// # Audit hints
1360///
1361/// - `mucosal-tolerant-rationale-insufficient`, `mucosal-tolerant-accepts-empty`,
1362///   `mucosal-tolerant-past-review-date`, `mucosal-tolerant-without-reviewer`
1363///   (v0.2.1+ migration hint)
1364#[proc_macro_attribute]
1365pub fn mucosal_tolerant(args: TokenStream, input: TokenStream) -> TokenStream {
1366    let args = parse_macro_input!(args as parse::MucosalTolerantArgs);
1367    let input = proc_macro2::TokenStream::from(input);
1368    if let Err(e) = args.validate() {
1369        return e.to_compile_error().into();
1370    }
1371    quote! { #input }.into()
1372}
1373
1374/// Declare an orientation period — a time-bounded acknowledged absence of immunity.
1375///
1376/// An acknowledged, time-bounded absence of immunity with an explicit path
1377/// forward. A loud deferred-defense primitive (ADR-023) — *not* the
1378/// lightest-weight one; loudness is the discipline.
1379///
1380/// `learning_path` and `until` are **both REQUIRED** (ADR-023 §Decision +
1381/// the Option-A hard-break ruling). An orient without an explicit path-out and
1382/// a time-bound is silent deferred non-immunity — structurally identical to
1383/// tolerance, which this primitive exists to be loudly distinct from. A bare
1384/// `#[orient]` is a compile error.
1385///
1386/// # Arguments
1387///
1388/// - Antigen type name (optional positional)
1389/// - `learning_path = "..."` (REQUIRED, ≥ 20 chars) — the explicit path out of
1390///   the learning period
1391/// - `until = "YYYY-MM-DD"` (REQUIRED) — orientation horizon; UTC, within
1392///   `deferred_defense_max_horizon` (180d). A date beyond that is a compile error.
1393///
1394/// The pre-restoration drift fields (`see`, `adr`, `attestation_optional`) were
1395/// removed — they were never in the ADR-023 spec. Fold any see-also context
1396/// into the `learning_path` text or `references = [...]` on the antigen
1397/// declaration.
1398///
1399/// # Audit hints
1400///
1401/// - `orient-active` — orientation in progress
1402/// - `orient-pending-action-required` — orientation past its `until` horizon
1403///
1404/// # Example
1405///
1406/// ```ignore
1407/// use antigen::orient;
1408///
1409/// #[orient(
1410///     PanickingInDrop,
1411///     learning_path = "Audit every Drop impl for unwrap/panic before the v1 tag",
1412///     until = "2026-09-01",
1413/// )]
1414/// pub fn new_subsystem_under_construction() { /* ... */ }
1415/// ```
1416#[proc_macro_attribute]
1417pub fn orient(args: TokenStream, input: TokenStream) -> TokenStream {
1418    let args = parse_macro_input!(args as parse::OrientArgs);
1419    let input = proc_macro2::TokenStream::from(input);
1420
1421    if let Err(e) = args.validate() {
1422        return e.to_compile_error().into();
1423    }
1424
1425    quote! { #input }.into()
1426}
1427
1428/// Declare a rollback-as-triage commit: classify system state + commit to
1429/// rollback within a tight time-bound (ADR-026 §Rollback-as-triage).
1430///
1431/// Per the fixup-orient-dual-signature resolution, `#[triage_commit]` is a
1432/// SIBLING primitive to `#[orient]`, NOT
1433/// an extension. Orient names a failure-class with see-also context;
1434/// `triage_commit` names a triage decision + a rollback action. The two are
1435/// different speech acts in the deferred-defense family.
1436///
1437/// # Biology grounding — dual-axis honesty
1438///
1439/// The `#[triage_commit]` primitive carries DUAL-AXIS grounding per ADR-026
1440/// §Finding (NON-NEGOTIABLE); neither axis is decorative.
1441///
1442/// **Clinical-medicine axis grounds the OUTCOME**: triage as a discipline
1443/// comes from clinical emergency-response medicine — the practice of
1444/// classifying patients by acuity before deciding treatment order. The
1445/// 5-color taxonomy (Black/Red/Yellow/Green/White) rhymes with clinical
1446/// field-triage protocols (e.g., START — Simple Triage And Rapid
1447/// Treatment), but `#[triage_commit]` is not a clinical-medicine
1448/// implementation: the rollback-as-treatment use-case extends the
1449/// protocol's shape with software-specific cases (`White` for non-incident
1450/// triages, no clinical analog). Clinical-medicine grounds the
1451/// COMMIT-DECISION-BEFORE-ACTION discipline: informed consent + chart
1452/// documentation precede the procedure; structurally isomorphic to
1453/// triage-commit-before-rollback.
1454///
1455/// **Software-engineering axis grounds the PROCESS**:
1456/// rollback-as-mandated-by-triage-tag is software-engineering invention.
1457/// Immune biology has NO analog to "log rationale before acting" (per
1458/// ADR-026 §Finding). The `triaged_by` + `rationale` +
1459/// `rollback_due_within_minutes` fields operate at the
1460/// software-engineering tier; their structural enforcement (parse-time
1461/// validation, audit-time substrate-witness via git-trailer per ADR-019)
1462/// is software-engineering machinery composed under the clinical-medicine
1463/// outcome framing.
1464///
1465/// **What biology DOES ground (Class 1, outcome-level)**: the
1466/// `ForcePushErasingHistory` ↔ Immune Amnesia (measles) cognate (ADR-026
1467/// §Finding) is the central immune-biology grounding for the broader
1468/// VCS-info-loss family — catastrophic loss of memory-carrying substrates
1469/// with documented harm. `#[triage_commit]` is the prescribed defense at
1470/// the rollback boundary: biology predicts that memory-loss requires
1471/// structural defense; clinical-medicine prescribes the form
1472/// (triage-decision documented before action).
1473///
1474/// This is the same dual-axis honesty ADR-024 ratified for the
1475/// temporal-arc families — antigen draws from MULTIPLE grounding
1476/// disciplines, naming which axis grounds which property. Overclaim "this
1477/// is immune biology" would be dishonest; underclaim "this is decorative"
1478/// would lose the predictive power. Dual-axis is the right shape.
1479///
1480/// # Arguments
1481///
1482/// All five fields are REQUIRED per ADR-026 §Decision:
1483///
1484/// - `triage_decision = TriageDecision::X` — five-color triage classification
1485///   (one of `Black`, `Red`, `Yellow`, `Green`, `White`). See
1486///   [`antigen::vcs::TriageDecision`](https://docs.rs/antigen) for variant
1487///   semantics.
1488/// - `rollback_target = "<sha>"` — commit sha pointing to the last-known-good
1489///   state. Non-empty.
1490/// - `triaged_by = "<role|name>"` — informed-consent author identity (role
1491///   slug like `"reviewer"` or a personal name). Non-empty.
1492/// - `rationale = "..."` — chart-documentation; minimum 20 characters per
1493///   ADR-023 loudness-as-discipline applied to clinical-medicine
1494///   chart-documentation. Records WHY the rollback was decided before the
1495///   action commits.
1496/// - `rollback_due_within_minutes = N` — tight time-bound (positive `u32`).
1497///   Carries the discipline that triage-commits are followed by action in
1498///   bounded time; a zero deadline degrades the loudness pattern.
1499///
1500/// # Audit hints
1501///
1502/// - `vcs-rollback-without-triage-commit` — a rollback commit not preceded by
1503///   a `#[triage_commit]` declaration with `Triage-Decision: <sha>` trailer
1504/// - `vcs-rollback-due-window-exceeded` — `rollback_due_within_minutes`
1505///   elapsed without the rollback commit landing
1506///
1507/// # Example
1508///
1509/// ```ignore
1510/// use antigen::{triage_commit, TriageDecision};
1511///
1512/// #[triage_commit(
1513///     triage_decision = TriageDecision::Red,
1514///     rollback_target = "abc1234",
1515///     triaged_by = "reviewer",
1516///     rationale = "vital metric regression confirmed via #84; rolling back to last-known-good",
1517///     rollback_due_within_minutes = 30,
1518/// )]
1519/// fn _triage_marker_do_not_remove() {}
1520/// // Followed by rollback commit with trailer:
1521/// //   Triage-Decision: <sha-of-this-triage-commit-marker>
1522/// ```
1523#[proc_macro_attribute]
1524pub fn triage_commit(args: TokenStream, input: TokenStream) -> TokenStream {
1525    let args = parse_macro_input!(args as parse::TriageCommitArgs);
1526    let input = proc_macro2::TokenStream::from(input);
1527
1528    if let Err(e) = args.validate() {
1529        return e.to_compile_error().into();
1530    }
1531
1532    quote! { #input }.into()
1533}
1534
1535// ============================================================================
1536// Prescriptive Work-Orchestration Family (ADR-033, extends ADR-024)
1537//
1538// "The TODO comment becomes structure." Eight clinical-named work-need macros
1539// routing to FOUR structural shapes (ADR-033 §Decision 1):
1540//   S1 Role-workflow  — panel, rx, refer, biopsy  (ordered who-steps + frame)
1541//   S2 Elimination    — ddx                        (a set of closeable alternatives)
1542//   S3 Ordering       — triage                     (a re-validatable priority order)
1543//   S4 Frame-only     — culture, quarantine        (a temporal window + expiry)
1544//
1545// Each macro is a thin validating pass-through (like the recurrent family);
1546// `cargo antigen scan` reads the SOURCE attribute directly. The audit emits the
1547// four-valued WorkVerdict {Pending, Fulfilled, Overdue, OutOfFrame} — the board.
1548// Witness satisfaction REUSES the ADR-019/020 categorical spine (no new
1549// mechanism); only the S1 ORDERING is new content. `#[triage]` is intentionally
1550// NOT shipped in this commit — its arg-shape has a ratified-ADR-vs-test-corpus
1551// divergence; the other seven are
1552// unambiguous. `#[titer]` is NOT in this family (it is a titer-witness kind,
1553// ADR-019 Amendment 1).
1554// ============================================================================
1555
1556/// Declare a battery of work-needs to be filled + reviewed at this site
1557/// (ADR-033 S1 Role-workflow).
1558///
1559/// `#[panel(needs, filled_by?, reviewed_by?, ordered_by?, due?)]` marks a code
1560/// site as carrying an ordered diagnostic battery — a checklist the site's
1561/// reviewers must close. Biology: a clinical panel (a battery of tests ordered
1562/// together, closed by the reviewing clinician).
1563///
1564/// # Arguments
1565///
1566/// - `needs = ["...", ...]` (required) — the battery's checklist; non-empty
1567///   (empty = vacuous work-need; compile error)
1568/// - `filled_by = ["who", ...]` (optional) — ADR-020 who-refs that fill the needs
1569/// - `reviewed_by = ["who", ...]` (optional) — who-refs that review the fills
1570/// - `ordered_by = "who"` (optional) — who-ref that ordered the battery
1571/// - `due = "YYYY-MM-DD"` (optional) — ISO-8601 frame
1572///
1573/// Satisfaction (ADR-033 §Witness-binding) is collective coverage over the
1574/// need-set, attested per role-step at the current fingerprint — NOT a
1575/// positional `filled_by[i] ↔ needs[i]` pairing.
1576#[proc_macro_attribute]
1577pub fn panel(args: TokenStream, input: TokenStream) -> TokenStream {
1578    let args = parse_macro_input!(args as parse::PanelArgs);
1579    let input = proc_macro2::TokenStream::from(input);
1580    if let Err(e) = args.validate() {
1581        return e.to_compile_error().into();
1582    }
1583    quote! { #input }.into()
1584}
1585
1586/// Declare a prescribed treatment work-need at this site (ADR-033 S1
1587/// Role-workflow).
1588///
1589/// `#[rx(treatment, diagnosis?, filled_by?, reviewed_by?, due?)]` marks the
1590/// remedy a site must carry out. Biology: a prescription — a treatment ordered
1591/// for a diagnosis, filled and reviewed.
1592///
1593/// # Arguments
1594///
1595/// - `treatment = "..."` (required, non-empty) — what must be done
1596/// - `diagnosis = "..."` (optional) — opaque label (v0.3; backref to `ddx` not
1597///   resolved — VOID-4b)
1598/// - `filled_by = ["who", ...]` (optional) — ADR-020 who-refs
1599/// - `reviewed_by = ["who", ...]` (optional)
1600/// - `due = "YYYY-MM-DD"` (optional) — ISO-8601 frame
1601#[proc_macro_attribute]
1602pub fn rx(args: TokenStream, input: TokenStream) -> TokenStream {
1603    let args = parse_macro_input!(args as parse::RxArgs);
1604    let input = proc_macro2::TokenStream::from(input);
1605    if let Err(e) = args.validate() {
1606        return e.to_compile_error().into();
1607    }
1608    quote! { #input }.into()
1609}
1610
1611/// Declare a referral of work to an external owner (ADR-033 S1 Role-workflow).
1612///
1613/// `#[refer(to, response_due?)]` hands a work-need to an owner outside this
1614/// site's immediate responsibility. Biology: a specialist referral — the
1615/// referring clinician hands off and awaits a response.
1616///
1617/// # Arguments
1618///
1619/// - `to = "who"` (required) — ADR-020 who-ref (the external owner)
1620/// - `response_due = "YYYY-MM-DD"` (optional) — ISO-8601 frame for the response
1621#[proc_macro_attribute]
1622pub fn refer(args: TokenStream, input: TokenStream) -> TokenStream {
1623    let args = parse_macro_input!(args as parse::ReferArgs);
1624    let input = proc_macro2::TokenStream::from(input);
1625    if let Err(e) = args.validate() {
1626        return e.to_compile_error().into();
1627    }
1628    quote! { #input }.into()
1629}
1630
1631/// Declare a deep-investigation work-need at a sub-site (ADR-033 S1
1632/// Role-workflow).
1633///
1634/// `#[biopsy(location, request_text, deep_investigation_by?)]` marks a request
1635/// to investigate a specific sub-site in depth. Biology: a biopsy — sampling a
1636/// specific location for deep analysis.
1637///
1638/// # Arguments
1639///
1640/// - `location = "..."` (required) — sub-site pointer (opaque label v0.3)
1641/// - `request_text = "..."` (required, non-empty) — what to investigate
1642/// - `deep_investigation_by = "who"` (optional) — ADR-020 who-ref
1643#[proc_macro_attribute]
1644pub fn biopsy(args: TokenStream, input: TokenStream) -> TokenStream {
1645    let args = parse_macro_input!(args as parse::BiopsyArgs);
1646    let input = proc_macro2::TokenStream::from(input);
1647    if let Err(e) = args.validate() {
1648        return e.to_compile_error().into();
1649    }
1650    quote! { #input }.into()
1651}
1652
1653/// Declare a differential-diagnosis work-need: a set of alternatives to rule
1654/// out (ADR-033 S2 Elimination).
1655///
1656/// `#[ddx(symptom, rule_out, investigator?, reviewer?)]` marks a site where a
1657/// symptom has multiple candidate causes, each to be independently eliminated.
1658/// Biology: differential diagnosis — the list of conditions to rule out.
1659///
1660/// # Arguments
1661///
1662/// - `symptom = "..."` (required, non-empty) — the observed problem
1663/// - `rule_out = ["...", ...]` (required, non-empty) — the alternative-set; each
1664///   alternative is independently closeable (a rule-out carries a closing attestation)
1665/// - `investigator = "who"` (optional) — ADR-020 who-ref
1666/// - `reviewer = "who"` (optional) — ADR-020 who-ref
1667#[proc_macro_attribute]
1668pub fn ddx(args: TokenStream, input: TokenStream) -> TokenStream {
1669    let args = parse_macro_input!(args as parse::DdxArgs);
1670    let input = proc_macro2::TokenStream::from(input);
1671    if let Err(e) = args.validate() {
1672        return e.to_compile_error().into();
1673    }
1674    quote! { #input }.into()
1675}
1676
1677/// Declare a time-boxed test/observation work-need (ADR-033 S4 Frame-only).
1678///
1679/// `#[culture(test_kind, duration?, runs_until?)]` marks a site that must stay
1680/// green within a temporal window (a soak/observation). Biology: a culture —
1681/// incubate for a fixed period and read the result.
1682///
1683/// # Arguments
1684///
1685/// - `test_kind = "..."` (required, non-empty) — what is being cultured/observed
1686/// - `duration = "..."` (optional) — duration string
1687/// - `runs_until = "YYYY-MM-DD"` (optional) — ISO-8601 frame
1688#[proc_macro_attribute]
1689pub fn culture(args: TokenStream, input: TokenStream) -> TokenStream {
1690    let args = parse_macro_input!(args as parse::CultureArgs);
1691    let input = proc_macro2::TokenStream::from(input);
1692    if let Err(e) = args.validate() {
1693        return e.to_compile_error().into();
1694    }
1695    quote! { #input }.into()
1696}
1697
1698/// Declare an isolated region under a time-boxed hold (ADR-033 S4 Frame-only).
1699///
1700/// `#[quarantine(scope, until?, reason)]` marks a region deliberately isolated
1701/// until a frame passes. Biology: quarantine — isolate until cleared. The
1702/// `reason` is required per ADR-005 Amendment 2 (rationale-as-required for every
1703/// suppression-shaped primitive).
1704///
1705/// # Arguments
1706///
1707/// - `scope = "..."` (required) — the isolated-region pointer
1708/// - `until = "YYYY-MM-DD"` (optional) — ISO-8601 frame
1709/// - `reason = "..."` (required, non-empty) — why the hold (ADR-005 Amd2)
1710#[proc_macro_attribute]
1711pub fn quarantine(args: TokenStream, input: TokenStream) -> TokenStream {
1712    let args = parse_macro_input!(args as parse::QuarantineArgs);
1713    let input = proc_macro2::TokenStream::from(input);
1714    if let Err(e) = args.validate() {
1715        return e.to_compile_error().into();
1716    }
1717    quote! { #input }.into()
1718}
1719
1720/// Declare a re-validatable priority ordering over code sites (ADR-033 S3
1721/// Ordering).
1722///
1723/// `#[triage(priority_order, triaged_by?, re_triage_due?)]` marks a site that
1724/// carries an ordered priority over a set of code-site references. Biology:
1725/// triage — ranking by urgency, re-assessed each round. Distinct from
1726/// `#[triage_commit]` (ADR-026 VCS-rollback classification) — names rhyme,
1727/// surfaces are unrelated (ATK-PRES-10).
1728///
1729/// Per the 2026-06-01 post-ratification fixup, the `campsites` field was dropped:
1730/// `priority_order` entries are **code-site references** (file/item-path), not
1731/// external work-tracker units (anchor #3 — the audit resolves only
1732/// file/item-path references). They resolve at
1733/// audit-time (ADR-017 Amendment 1); an unresolvable entry is `out-of-frame`,
1734/// never silently satisfied.
1735///
1736/// # Arguments
1737///
1738/// - `priority_order = ["...", ...]` (required, non-empty) — code-site refs in
1739///   priority order
1740/// - `triaged_by = "who"` (optional) — ADR-020 who-ref that attested the order
1741/// - `re_triage_due = "YYYY-MM-DD"` (optional) — ISO-8601 staleness frame (not a
1742///   deadline; a standing ordering is re-earned each cycle)
1743#[proc_macro_attribute]
1744pub fn triage(args: TokenStream, input: TokenStream) -> TokenStream {
1745    let args = parse_macro_input!(args as parse::TriageArgs);
1746    let input = proc_macro2::TokenStream::from(input);
1747    if let Err(e) = args.validate() {
1748        return e.to_compile_error().into();
1749    }
1750    quote! { #input }.into()
1751}
1752
1753#[cfg(test)]
1754mod marker_emit_tests {
1755    use super::marked_unknown_marker;
1756
1757    /// The doc-marker's trigger field is JSON-escaped for EVERY control char
1758    /// (U+0000–U+001F), not just the five with short forms — the producer-
1759    /// correctness fix. A raw control char in the trigger would otherwise produce
1760    /// invalid JSON the scanner's re-parse rejects (antigen's own silent class).
1761    #[test]
1762    fn trigger_escapes_all_control_chars_to_valid_json() {
1763        // A backspace (→ \b short form), a form-feed (→ \f), a vertical-tab (→ the
1764        // long  form), and a SOH (→ ).
1765        let trigger = "a\u{08}b\u{0c}c\u{0b}d\u{01}e";
1766        let out = marked_unknown_marker("dread", "dread", "unsure", trigger);
1767        assert!(out.contains("\\b"), "backspace → \\b");
1768        assert!(out.contains("\\f"), "form-feed → \\f");
1769        assert!(out.contains("\\u000b"), "vertical-tab → \\u000b");
1770        assert!(out.contains("\\u0001"), "SOH → \\u0001");
1771        // No raw control byte survives into the (single-line) doc-marker output.
1772        assert!(
1773            !out.chars().any(|c| (c as u32) < 0x20),
1774            "no raw control char survives the escape"
1775        );
1776    }
1777
1778    #[test]
1779    fn trigger_escapes_quote_and_backslash() {
1780        // The original five short forms still hold (a quote/backslash in the
1781        // felt-note can't corrupt the marker the scanner re-parses).
1782        let out = marked_unknown_marker("aura", "aura", "unsure", r#"the "guard" path\here"#);
1783        assert!(out.contains(r#"\""#), "quote → escaped quote");
1784        assert!(out.contains(r"\\"), "backslash → escaped backslash");
1785    }
1786}