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