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}