use proc_macro2::Span;
use syn::parse::{Parse, ParseStream};
use syn::punctuated::Punctuated;
use syn::{Expr, Ident, Lit, LitStr, Path, Token};
pub use antigen_attestation::parser::RequiresExpr;
#[cfg(test)]
use antigen_attestation::parser::LeafExpr;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum MacroAntigenCategory {
SubstrateAlignment,
FunctionalCorrectness,
}
impl MacroAntigenCategory {
fn from_path_str(s: &str) -> Option<Self> {
match s {
"SubstrateAlignment" | "AntigenCategory::SubstrateAlignment" => {
Some(Self::SubstrateAlignment)
}
"FunctionalCorrectness" | "AntigenCategory::FunctionalCorrectness" => {
Some(Self::FunctionalCorrectness)
}
_ => None,
}
}
#[allow(dead_code)]
pub const fn as_str(self) -> &'static str {
match self {
Self::SubstrateAlignment => "substrate-alignment",
Self::FunctionalCorrectness => "functional-correctness",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum MacroProvenance {
Encountered,
Constructable,
Heuristic,
Imagined,
}
impl MacroProvenance {
fn from_path_str(s: &str) -> Option<Self> {
match s {
"Encountered" | "Provenance::Encountered" => Some(Self::Encountered),
"Constructable" | "Provenance::Constructable" => Some(Self::Constructable),
"Heuristic" | "Provenance::Heuristic" => Some(Self::Heuristic),
"Imagined" | "Provenance::Imagined" => Some(Self::Imagined),
_ => None,
}
}
#[allow(dead_code)]
pub const fn as_str(self) -> &'static str {
match self {
Self::Encountered => "encountered",
Self::Constructable => "constructable",
Self::Heuristic => "heuristic",
Self::Imagined => "imagined",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum MacroPresentation {
Passive,
Active,
}
impl MacroPresentation {
fn from_path_str(s: &str) -> Option<Self> {
match s {
"Passive" | "Presentation::Passive" => Some(Self::Passive),
"Active" | "Presentation::Active" => Some(Self::Active),
_ => None,
}
}
#[allow(dead_code)]
pub const fn as_str(self) -> &'static str {
match self {
Self::Passive => "passive",
Self::Active => "active",
}
}
}
#[allow(dead_code)]
#[derive(Debug)]
pub struct AntigenArgs {
pub name: String,
pub fingerprint: Option<String>,
pub family: Option<String>,
pub summary: Option<String>,
pub references: Vec<String>,
pub category: Vec<MacroAntigenCategory>,
pub provenance: Option<MacroProvenance>,
pub presentation: Option<MacroPresentation>,
pub name_span: Option<Span>,
pub fingerprint_span: Option<Span>,
pub args_span: Span,
}
#[derive(Debug)]
pub struct PresentsArgs {
#[allow(dead_code)]
pub antigen: Path,
pub requires: Option<(RequiresExpr, Span)>,
#[allow(dead_code)]
pub proof: Option<Expr>,
}
pub struct ImmuneArgs {
pub antigen: Path,
pub witness: Option<Expr>,
pub requires: Option<(RequiresExpr, Span)>,
#[allow(dead_code)]
pub rationale: Option<String>,
}
pub struct DescendedFromArgs {
#[allow(dead_code)]
pub parent: Path,
}
#[derive(Debug)]
pub struct DefendedByArgs {
#[allow(dead_code)]
pub antigen: Path,
}
pub struct ToleranceArgs {
#[allow(dead_code)]
pub antigen: Path,
pub rationale: Option<String>,
pub rationale_span: Option<Span>,
pub until: Option<String>,
pub until_span: Option<Span>,
#[allow(dead_code)]
pub see: Vec<String>,
pub requires: Option<(RequiresExpr, Span)>,
pub args_span: Span,
}
impl Parse for AntigenArgs {
fn parse(input: ParseStream) -> syn::Result<Self> {
let args_span = input.span();
let mut name: Option<String> = None;
let mut name_span: Option<Span> = None;
let mut fingerprint: Option<String> = None;
let mut fingerprint_span: Option<Span> = None;
let mut family: Option<String> = None;
let mut summary: Option<String> = None;
let mut references: Vec<String> = Vec::new();
let mut category: Vec<MacroAntigenCategory> = Vec::new();
let mut provenance: Option<MacroProvenance> = None;
let mut presentation: Option<MacroPresentation> = None;
let pairs: Punctuated<MetaPair, Token![,]> =
input.parse_terminated(MetaPair::parse, Token![,])?;
for pair in pairs {
match pair.key.to_string().as_str() {
"name" => {
let (s, span) = pair.expect_string_spanned()?;
name = Some(s);
name_span = Some(span);
}
"fingerprint" => {
let (s, span) = pair.expect_string_spanned()?;
fingerprint = Some(s);
fingerprint_span = Some(span);
}
"family" => family = Some(pair.expect_string()?),
"summary" => summary = Some(pair.expect_string()?),
"references" => references = pair.expect_string_array()?,
"category" => category = pair.expect_antigen_category()?,
"provenance" => provenance = Some(pair.expect_provenance()?),
"presentation" => presentation = Some(pair.expect_presentation()?),
"tier" | "confidence" | "named" | "suspected" => {
return Err(syn::Error::new(
pair.key.span(),
"#[antigen] has no authored confidence tier — the tier \
(suspected/named) is dial-derived at audit (ADR-039 §B). \
Author `provenance = Provenance::{Encountered|Constructable|\
Heuristic|Imagined}` instead; provenance is the verified \
claim that sets the tier floor.",
))
}
other => {
return Err(syn::Error::new(
pair.key.span(),
format!(
"unknown #[antigen] field `{other}`; expected one of: \
name, fingerprint, family, summary, references, \
category, provenance, presentation"
),
))
}
}
}
let name =
name.ok_or_else(|| syn::Error::new(args_span, "#[antigen] requires `name = \"...\"`"))?;
Ok(Self {
name,
fingerprint,
family,
summary,
references,
category,
provenance,
presentation,
name_span,
fingerprint_span,
args_span,
})
}
}
impl AntigenArgs {
pub fn validate(&self) -> syn::Result<()> {
if self.name.is_empty() {
return Err(syn::Error::new(
self.name_span.unwrap_or(self.args_span),
"#[antigen] `name` cannot be empty",
));
}
if !is_kebab_case(&self.name) {
return Err(syn::Error::new(
self.name_span.unwrap_or(self.args_span),
format!(
"#[antigen] `name = \"{}\"` must be kebab-case (lowercase with hyphens)",
self.name
),
));
}
if let Some(fingerprint) = self.fingerprint.as_deref() {
if fingerprint.is_empty() {
return Err(syn::Error::new(
self.fingerprint_span.unwrap_or(self.args_span),
"#[antigen] `fingerprint` cannot be empty (omit the field entirely \
for a verify-only antigen, per ADR-009 Amendment 1)",
));
}
if let Err(parse_err) = antigen_fingerprint::Fingerprint::parse(fingerprint) {
let anchor = self.fingerprint_span.unwrap_or(self.args_span);
return Err(syn::Error::new(
anchor,
format!(
"#[antigen] `fingerprint` does not parse: {parse_err}\n\
(per ADR-010 Amendment 1 Path C — DSL syntax, not raw Rust expressions)"
),
));
}
}
{
let mut seen = std::collections::HashSet::new();
for cat in &self.category {
if !seen.insert(cat) {
return Err(syn::Error::new(
self.args_span,
"duplicate AntigenCategory variant in `category` array; \
each category variant may appear at most once",
));
}
}
}
Ok(())
}
}
impl Parse for PresentsArgs {
fn parse(input: ParseStream) -> syn::Result<Self> {
let antigen: Path = input.parse()?;
let mut requires: Option<(RequiresExpr, Span)> = None;
let mut proof: Option<Expr> = None;
while !input.is_empty() {
input.parse::<Token![,]>()?;
if input.is_empty() {
break;
}
let key: Ident = input.parse()?;
let key_span = key.span();
input.parse::<Token![=]>()?;
match key.to_string().as_str() {
"requires" => {
let pred: RequiresExpr = input.parse()?;
requires = Some((pred, key_span));
}
"proof" => {
proof = Some(input.parse()?);
}
"witness" => {
let _: Expr = input.parse()?; return Err(syn::Error::new(
key_span,
"`witness = ...` is not valid on `#[presents]`. \
Code-tier witnesses register via `#[defended_by(X)]` on the \
test/proptest function, not on the presents-site. \
For substrate-tier evidence use `requires = ...` here. \
For phantom-tier evidence use `proof = ...` here.",
));
}
other => {
return Err(syn::Error::new(
key.span(),
format!(
"unknown #[presents] field `{other}`; expected one of: \
requires, proof"
),
));
}
}
}
Ok(Self {
antigen,
requires,
proof,
})
}
}
impl PresentsArgs {
pub fn validate(&self) -> syn::Result<()> {
if let Some((pred, span)) = &self.requires {
pred.validate(*span)?;
}
if let Some(Expr::Lit(syn::ExprLit {
lit: Lit::Str(s), ..
})) = &self.proof
{
return Err(syn::Error::new_spanned(
s,
format!(
"#[presents] `proof` must be a type-system construction whose \
existence is the proof (e.g. `proof = NonPanickingProof::<T>::verified`), \
not a string literal. `proof = \"{}\"` would silently grade FormalProof \
(the strongest tier) at audit time with no actual phantom evidence. \
{}",
s.value(),
if s.value().trim().is_empty() {
"An empty/placeholder proof especially must not claim FormalProof."
} else {
"Drop the quotes and reference the phantom constructor directly."
}
),
));
}
Ok(())
}
pub fn requires_json(&self) -> Option<String> {
self.requires.as_ref().map(|(pred, _)| pred.to_json())
}
}
impl Parse for ImmuneArgs {
fn parse(input: ParseStream) -> syn::Result<Self> {
let antigen: Path = input.parse()?;
let mut witness: Option<Expr> = None;
let mut requires: Option<(RequiresExpr, Span)> = None;
let mut rationale: Option<String> = None;
while !input.is_empty() {
input.parse::<Token![,]>()?;
if input.is_empty() {
break;
}
let key: Ident = input.parse()?;
let key_span = key.span();
input.parse::<Token![=]>()?;
match key.to_string().as_str() {
"witness" => {
witness = Some(input.parse()?);
}
"requires" => {
let pred: RequiresExpr = input.parse()?;
requires = Some((pred, key_span));
}
"rationale" => {
let lit: LitStr = input.parse()?;
rationale = Some(lit.value());
}
other => {
return Err(syn::Error::new(
key.span(),
format!(
"unknown #[immune] field `{other}`; expected one of: \
witness, requires, rationale"
),
));
}
}
}
Ok(Self {
antigen,
witness,
requires,
rationale,
})
}
}
impl ImmuneArgs {
pub fn validate(&self) -> syn::Result<()> {
match (&self.witness, &self.requires) {
(None, None) => Err(syn::Error::new_spanned(
&self.antigen,
"#[immune] requires either `witness = ...` (code-tier: a test, proptest, \
lint reference, formal-verification proof, or phantom-type construction) \
or `requires = <predicate>` (substrate-witness predicate, ADR-019). \
A marker without proof is not a claim.",
)),
(Some(_), Some((_, span))) => Err(syn::Error::new(
*span,
"#[immune] accepts either `witness = ...` or `requires = ...`, not both. \
For compound evidence across code-tier and substrate-tier (e.g., \
hybrid antigens per ADR-028), stack two `#[immune]` attributes on \
the same item — one with `witness = ...`, one with `requires = ...`. \
Audit treats stacked `#[immune]` attributes as independent coverage \
entries.",
)),
(_, Some((pred, span))) => pred.validate(*span),
(Some(witness), None) => {
if let Expr::Lit(syn::ExprLit {
lit: syn::Lit::Str(s),
..
}) = witness
{
return Err(syn::Error::new_spanned(
witness,
format!(
"#[immune] `witness` must be a bare identifier or path to the \
verifying item (e.g. `witness = my_test`), not a string literal. \
`witness = \"{}\"` silently never resolves at audit time because \
the resolver matches function names against the token, and a \
string literal carries its quotes. Drop the quotes: \
`witness = {}`.",
s.value(),
s.value()
),
));
}
Ok(())
}
}
}
pub fn requires_json(&self) -> Option<String> {
self.requires.as_ref().map(|(pred, _)| pred.to_json())
}
}
impl Parse for DescendedFromArgs {
fn parse(input: ParseStream) -> syn::Result<Self> {
let parent: Path = input.parse()?;
Ok(Self { parent })
}
}
impl Parse for DefendedByArgs {
fn parse(input: ParseStream) -> syn::Result<Self> {
let antigen: Path = input.parse()?;
if !input.is_empty() {
return Err(syn::Error::new(
input.span(),
"#[defended_by] takes exactly one positional argument: the antigen \
type it defends, e.g. `#[defended_by(ParallelStateTrackersDiverge)]`. \
It registers a code-tier witness (a test/proptest); it does NOT carry \
`witness =`/`requires =`/`proof =`. Site-attached evidence folds into \
`#[presents]` (ADR-029).",
));
}
Ok(Self { antigen })
}
}
impl Parse for ToleranceArgs {
fn parse(input: ParseStream) -> syn::Result<Self> {
let args_span = input.span();
let antigen: Path = input.parse()?;
let mut rationale: Option<String> = None;
let mut rationale_span: Option<Span> = None;
let mut until: Option<String> = None;
let mut until_span: Option<Span> = None;
let mut see: Vec<String> = Vec::new();
let mut requires: Option<(RequiresExpr, Span)> = None;
while !input.is_empty() {
input.parse::<Token![,]>()?;
if input.is_empty() {
break;
}
let key: Ident = input.parse()?;
let key_span = key.span();
input.parse::<Token![=]>()?;
match key.to_string().as_str() {
"rationale" => {
let lit: LitStr = input.parse()?;
rationale_span = Some(lit.span());
rationale = Some(lit.value());
}
"until" => {
let lit: LitStr = input.parse()?;
until_span = Some(lit.span());
until = Some(lit.value());
}
"see" => {
let arr: syn::ExprArray = input.parse()?;
for elem in &arr.elems {
if let Expr::Lit(syn::ExprLit {
lit: Lit::Str(s), ..
}) = elem
{
see.push(s.value());
} else {
return Err(syn::Error::new_spanned(
elem,
"expected a string literal in `see` array",
));
}
}
}
"requires" => {
let pred: RequiresExpr = input.parse()?;
requires = Some((pred, key_span));
}
other => {
return Err(syn::Error::new(
key.span(),
format!(
"unknown #[antigen_tolerance] field `{other}`; expected one of: \
rationale, until, see, requires",
),
));
}
}
}
Ok(Self {
antigen,
rationale,
rationale_span,
until,
until_span,
see,
requires,
args_span,
})
}
}
impl ToleranceArgs {
pub fn validate(&self) -> syn::Result<()> {
let Some(rationale) = self.rationale.as_deref() else {
return Err(syn::Error::new_spanned(
&self.antigen,
"#[antigen_tolerance] requires `rationale = \"...\"`. \
A tolerance without rationale is not a claim — it's a silent suppression.",
));
};
if rationale.is_empty() {
return Err(syn::Error::new(
self.rationale_span.unwrap_or(self.args_span),
"#[antigen_tolerance] `rationale` must not be empty",
));
}
if let Some(until) = self.until.as_deref() {
if until.is_empty() {
return Err(syn::Error::new(
self.until_span.unwrap_or(self.args_span),
"#[antigen_tolerance] `until = \"\"` rejected — \
an empty expiry indicates user error. Use `until = \"v1.0\"` \
(or similar) or omit the field entirely for forever-tolerance.",
));
}
}
if let Some((pred, span)) = &self.requires {
pred.validate(*span)?;
}
Ok(())
}
pub fn requires_json(&self) -> Option<String> {
self.requires.as_ref().map(|(pred, _)| pred.to_json())
}
}
#[derive(Debug)]
pub struct MarkerArgs {
pub trigger: Option<String>,
pub trigger_span: Option<Span>,
pub args_span: Span,
pub marker_name: &'static str,
}
impl Parse for MarkerArgs {
fn parse(input: ParseStream) -> syn::Result<Self> {
let args_span = input.span();
let mut trigger: Option<String> = None;
let mut trigger_span: Option<Span> = None;
let pairs: Punctuated<MetaPair, Token![,]> =
input.parse_terminated(MetaPair::parse, Token![,])?;
for pair in pairs {
match pair.key.to_string().as_str() {
"trigger" => {
let (s, span) = pair.expect_string_spanned()?;
trigger = Some(s);
trigger_span = Some(span);
}
"magnitude" | "existence_certainty" | "certainty" => {
return Err(syn::Error::new(
pair.key.span(),
"a marked-unknown marker's plane corner (magnitude × \
existence-certainty) is FIXED by which marker you use \
(#[aura] / #[dread] / #[red_flag]), not authored. Only \
`trigger = \"...\"` is a marker field.",
))
}
other => {
return Err(syn::Error::new(
pair.key.span(),
format!(
"unknown marked-unknown field `{other}`; the only field \
is `trigger = \"...\"` (required)"
),
))
}
}
}
Ok(Self {
trigger,
trigger_span,
args_span,
marker_name: "marked-unknown",
})
}
}
impl MarkerArgs {
#[must_use]
pub const fn with_marker(mut self, marker_name: &'static str) -> Self {
self.marker_name = marker_name;
self
}
pub fn validate(&self) -> syn::Result<()> {
let Some(trigger) = self.trigger.as_deref() else {
return Err(syn::Error::new(
self.args_span,
format!(
"#[{}] requires `trigger = \"...\"` — what did you see that \
made you feel this? A marked-unknown with no stated trigger \
is not an asserted observation; it's the contentless \"this \
seems off\" graffiti the marker exists to prevent (ADR-041 \
guard 3).",
self.marker_name
),
));
};
if trigger.trim().is_empty() {
return Err(syn::Error::new(
self.trigger_span.unwrap_or(self.args_span),
format!(
"#[{}] `trigger` must not be empty — state what you saw.",
self.marker_name
),
));
}
Ok(())
}
#[must_use]
pub fn trigger_str(&self) -> &str {
self.trigger.as_deref().unwrap_or_default()
}
}
pub struct GeneratesArgs {
pub antigen: Path,
pub rationale: Option<String>,
pub rationale_span: Option<Span>,
#[allow(dead_code)]
pub witness_template: Option<String>,
pub witness_template_span: Option<Span>,
#[allow(dead_code)]
pub if_attr_present: Option<String>,
pub if_attr_present_span: Option<Span>,
pub args_span: Span,
}
impl Parse for GeneratesArgs {
fn parse(input: ParseStream) -> syn::Result<Self> {
let args_span = input.span();
let antigen: Path = input.parse()?;
let mut rationale: Option<String> = None;
let mut rationale_span: Option<Span> = None;
let mut witness_template: Option<String> = None;
let mut witness_template_span: Option<Span> = None;
let mut if_attr_present: Option<String> = None;
let mut if_attr_present_span: Option<Span> = None;
while !input.is_empty() {
input.parse::<Token![,]>()?;
if input.is_empty() {
break;
}
let key: Ident = input.parse()?;
input.parse::<Token![=]>()?;
match key.to_string().as_str() {
"rationale" => {
let lit: LitStr = input.parse()?;
rationale_span = Some(lit.span());
rationale = Some(lit.value());
}
"witness_template" => {
let lit: LitStr = input.parse()?;
witness_template_span = Some(lit.span());
witness_template = Some(lit.value());
}
"if_attr_present" => {
let lit: LitStr = input.parse()?;
if_attr_present_span = Some(lit.span());
if_attr_present = Some(lit.value());
}
other => {
return Err(syn::Error::new(
key.span(),
format!(
"unknown #[antigen_generates] field `{other}`; expected one of: \
rationale, witness_template, if_attr_present",
),
));
}
}
}
Ok(Self {
antigen,
rationale,
rationale_span,
witness_template,
witness_template_span,
if_attr_present,
if_attr_present_span,
args_span,
})
}
}
impl GeneratesArgs {
pub fn validate(&self) -> syn::Result<()> {
let Some(rationale) = self.rationale.as_deref() else {
return Err(syn::Error::new_spanned(
&self.antigen,
"#[antigen_generates] requires `rationale = \"...\"`. \
A generation declaration without rationale is not a claim — \
the macro author must justify what the expansion presents.",
));
};
if rationale.is_empty() {
return Err(syn::Error::new(
self.rationale_span.unwrap_or(self.args_span),
"#[antigen_generates] `rationale` must not be empty",
));
}
if matches!(self.witness_template.as_deref(), Some("")) {
return Err(syn::Error::new(
self.witness_template_span.unwrap_or(self.args_span),
"#[antigen_generates] `witness_template = \"\"` rejected — \
omit the field rather than supplying an empty path.",
));
}
if matches!(self.if_attr_present.as_deref(), Some("")) {
return Err(syn::Error::new(
self.if_attr_present_span.unwrap_or(self.args_span),
"#[antigen_generates] `if_attr_present = \"\"` rejected — \
omit the field rather than supplying an empty guard.",
));
}
Ok(())
}
pub fn antigen_name(&self) -> String {
self.antigen
.segments
.last()
.map_or_else(String::new, |s| s.ident.to_string())
}
}
#[derive(Debug)]
pub struct AnergyArgs {
#[allow(dead_code)]
pub antigen: Option<syn::Path>,
pub reason: Option<String>,
pub reason_span: Option<Span>,
pub until: Option<String>,
pub until_span: Option<Span>,
#[allow(dead_code)]
pub expected_co_stimulation: Option<String>,
#[allow(dead_code)]
pub signed_by: Option<String>,
pub args_span: Span,
}
impl Parse for AnergyArgs {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
let args_span = input.span();
let mut antigen: Option<syn::Path> = None;
let mut reason: Option<String> = None;
let mut reason_span: Option<Span> = None;
let mut until: Option<String> = None;
let mut until_span: Option<Span> = None;
let mut expected_co_stimulation: Option<String> = None;
let mut signed_by: Option<String> = None;
if !input.is_empty() && input.peek(Ident) && !input.peek2(Token![=]) {
antigen = Some(input.parse()?);
if !input.is_empty() {
input.parse::<Token![,]>()?;
}
}
while !input.is_empty() {
let key: Ident = input.parse()?;
input.parse::<Token![=]>()?;
match key.to_string().as_str() {
"reason" => {
let lit: LitStr = input.parse()?;
reason_span = Some(lit.span());
reason = Some(lit.value());
}
"until" => {
let lit: LitStr = input.parse()?;
until_span = Some(lit.span());
until = Some(lit.value());
}
"expected_co_stimulation" => {
let lit: LitStr = input.parse()?;
expected_co_stimulation = Some(lit.value());
}
"signed_by" => {
let lit: LitStr = input.parse()?;
signed_by = Some(lit.value());
}
other => {
return Err(syn::Error::new(
key.span(),
format!(
"unknown #[anergy] field `{other}`; expected one of: \
reason, until, expected_co_stimulation, signed_by"
),
));
}
}
if !input.is_empty() {
input.parse::<Token![,]>()?;
}
}
Ok(Self {
antigen,
reason,
reason_span,
until,
until_span,
expected_co_stimulation,
signed_by,
args_span,
})
}
}
impl AnergyArgs {
pub fn validate(&self) -> syn::Result<()> {
match self.reason.as_deref() {
None => {
return Err(syn::Error::new(
self.args_span,
"#[anergy] requires `reason = \"...\"`. \
Anergy without a stated reason is a silent suppression.",
));
}
Some(r) if r.len() < 20 => {
return Err(syn::Error::new(
self.reason_span.unwrap_or(self.args_span),
format!(
"#[anergy] `reason` must be at least 20 characters \
(got {}); per ADR-023 loudness-as-discipline.",
r.len()
),
));
}
Some(r) if r.trim().is_empty() => {
return Err(syn::Error::new(
self.reason_span.unwrap_or(self.args_span),
"#[anergy] `reason` must not be whitespace-only; \
a blank reason bypasses the loudness-as-discipline \
requirement (ADR-023).",
));
}
_ => {}
}
match self.until.as_deref() {
None => {
return Err(syn::Error::new(
self.args_span,
"#[anergy] requires `until = \"YYYY-MM-DD\"`. \
Anergy without a time-bound degrades to silent tolerance. \
Per ADR-023 A5: `until` is not optional.",
));
}
Some("") => {
return Err(syn::Error::new(
self.until_span.unwrap_or(self.args_span),
"#[anergy] `until` must not be empty. \
Use a date string, e.g. `until = \"2026-12-31\"`.",
));
}
_ => {}
}
if let Some(until_str) = self.until.as_deref() {
match parse_iso_date(until_str) {
Ok(until_date) => {
let horizon_days = (until_date - today_utc()).num_days();
if horizon_days < 0 {
return Err(syn::Error::new(
self.until_span.unwrap_or(self.args_span),
format!(
"#[anergy] `until` ({until_str}) is {} day(s) in the past — \
an already-expired anergy window is silent suppression with \
no accountability. Set a future `until` date or remove the \
#[anergy] marker.",
-horizon_days
),
));
}
}
Err(()) => {
return Err(syn::Error::new(
self.until_span.unwrap_or(self.args_span),
format!(
"#[anergy] `until` value {until_str:?} is not a valid ISO-8601 \
date. Use YYYY-MM-DD format, e.g. `until = \"2026-12-31\"`."
),
));
}
}
}
Ok(())
}
}
#[derive(Debug)]
pub struct ImmunosuppressArgs {
#[allow(dead_code)]
pub antigen: Option<syn::Path>,
pub rationale: Option<String>,
pub rationale_span: Option<Span>,
pub until: Option<String>,
pub until_span: Option<Span>,
#[allow(dead_code)]
pub since: Option<String>,
pub duration_cap: Option<u64>,
pub duration_cap_span: Option<Span>,
#[allow(dead_code)]
pub signed_by: Option<String>,
pub args_span: Span,
}
impl Parse for ImmunosuppressArgs {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
use syn::LitInt;
let args_span = input.span();
let mut antigen: Option<syn::Path> = None;
let mut rationale: Option<String> = None;
let mut rationale_span: Option<Span> = None;
let mut until: Option<String> = None;
let mut until_span: Option<Span> = None;
let mut since: Option<String> = None;
let mut duration_cap: Option<u64> = None;
let mut duration_cap_span: Option<Span> = None;
let mut signed_by: Option<String> = None;
if !input.is_empty() && input.peek(Ident) && !input.peek2(Token![=]) {
antigen = Some(input.parse()?);
if !input.is_empty() {
input.parse::<Token![,]>()?;
}
}
while !input.is_empty() {
let key: Ident = input.parse()?;
input.parse::<Token![=]>()?;
match key.to_string().as_str() {
"rationale" => {
let lit: LitStr = input.parse()?;
rationale_span = Some(lit.span());
rationale = Some(lit.value());
}
"until" => {
let lit: LitStr = input.parse()?;
until_span = Some(lit.span());
until = Some(lit.value());
}
"since" => {
let lit: LitStr = input.parse()?;
since = Some(lit.value());
}
"duration_cap" => {
let lit: LitInt = input.parse()?;
duration_cap_span = Some(lit.span());
duration_cap = Some(lit.base10_parse::<u64>()?);
}
"signed_by" => {
let lit: LitStr = input.parse()?;
signed_by = Some(lit.value());
}
other => {
return Err(syn::Error::new(
key.span(),
format!(
"unknown #[immunosuppress] field `{other}`; expected one of: \
rationale, until, since, duration_cap, signed_by"
),
));
}
}
if !input.is_empty() {
input.parse::<Token![,]>()?;
}
}
Ok(Self {
antigen,
rationale,
rationale_span,
until,
until_span,
since,
duration_cap,
duration_cap_span,
signed_by,
args_span,
})
}
}
pub const IMMUNOSUPPRESS_DEFAULT_CAP_DAYS: u64 = 90;
pub const ORIENT_DEFAULT_MAX_HORIZON_DAYS: i64 = 180;
impl ImmunosuppressArgs {
pub fn validate(&self) -> syn::Result<()> {
match self.rationale.as_deref() {
None => {
return Err(syn::Error::new(
self.args_span,
"#[immunosuppress] requires `rationale = \"...\"`. \
Immunosuppression without rationale is not a claim.",
));
}
Some(r) if r.len() < 20 => {
return Err(syn::Error::new(
self.rationale_span.unwrap_or(self.args_span),
format!(
"#[immunosuppress] `rationale` must be at least 20 characters \
(got {}); per ADR-023 loudness-as-discipline.",
r.len()
),
));
}
Some(r) if r.trim().is_empty() => {
return Err(syn::Error::new(
self.rationale_span.unwrap_or(self.args_span),
"#[immunosuppress] `rationale` must not be whitespace-only; \
a blank rationale bypasses the loudness-as-discipline \
requirement (ADR-023).",
));
}
_ => {}
}
match self.until.as_deref() {
None => {
return Err(syn::Error::new(
self.args_span,
"#[immunosuppress] requires `until = \"YYYY-MM-DD\"`. \
Suppression without a deadline is indefinite suppression.",
));
}
Some("") => {
return Err(syn::Error::new(
self.until_span.unwrap_or(self.args_span),
"#[immunosuppress] `until` must not be empty.",
));
}
_ => {}
}
let cap = self.duration_cap.unwrap_or(IMMUNOSUPPRESS_DEFAULT_CAP_DAYS);
let cap_i64 = i64::try_from(cap).unwrap_or(i64::MAX);
if let Some(until_str) = self.until.as_deref() {
match parse_iso_date(until_str) {
Ok(until_date) => {
let since_date = self
.since
.as_deref()
.and_then(|s| parse_iso_date(s).ok())
.unwrap_or_else(today_utc);
let duration_days = (until_date - since_date).num_days();
if duration_days < 0 {
return Err(syn::Error::new(
self.until_span.unwrap_or(self.args_span),
format!(
"#[immunosuppress] `until` ({}) is {} day(s) in the past — \
an expired suppression window is silent check-disabling with \
no accountability. Set a future `until` date or remove the \
#[immunosuppress] marker.",
until_str, -duration_days
),
));
}
if duration_days > cap_i64 {
return Err(syn::Error::new(
self.until_span
.unwrap_or_else(|| self.duration_cap_span.unwrap_or(self.args_span)),
format!(
"#[immunosuppress] duration {duration_days}d exceeds cap {cap_i64}d. \
Per ADR-023: duration cap enforced at parse-time. \
Reduce the `until` date or set `duration_cap = N` (workspace \
default is {IMMUNOSUPPRESS_DEFAULT_CAP_DAYS}d)."
),
));
}
}
Err(()) => {
return Err(syn::Error::new(
self.until_span.unwrap_or(self.args_span),
format!(
"#[immunosuppress] `until` value {until_str:?} is not a valid \
ISO-8601 date. Use YYYY-MM-DD format, e.g. `until = \"2026-12-31\"`."
),
));
}
}
}
Ok(())
}
}
#[derive(Debug)]
pub struct PoxpartyArgs {
#[allow(dead_code)]
pub antigen: Option<syn::Path>,
pub exercise_type: Option<String>,
pub exercise_type_span: Option<Span>,
pub until: Option<String>,
pub until_span: Option<Span>,
#[allow(dead_code)]
pub name: Option<String>,
#[allow(dead_code)]
pub rationale: Option<String>,
#[allow(dead_code)]
pub signed_by: Option<String>,
pub args_span: Span,
}
impl Parse for PoxpartyArgs {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
let args_span = input.span();
let mut antigen: Option<syn::Path> = None;
let mut exercise_type: Option<String> = None;
let mut exercise_type_span: Option<Span> = None;
let mut until: Option<String> = None;
let mut until_span: Option<Span> = None;
let mut name: Option<String> = None;
let mut rationale: Option<String> = None;
let mut signed_by: Option<String> = None;
if !input.is_empty() && input.peek(Ident) && !input.peek2(Token![=]) {
antigen = Some(input.parse()?);
if !input.is_empty() {
input.parse::<Token![,]>()?;
}
}
while !input.is_empty() {
let key: Ident = input.parse()?;
input.parse::<Token![=]>()?;
match key.to_string().as_str() {
"exercise_type" => {
let lit: LitStr = input.parse()?;
exercise_type_span = Some(lit.span());
exercise_type = Some(lit.value());
}
"until" => {
let lit: LitStr = input.parse()?;
until_span = Some(lit.span());
until = Some(lit.value());
}
"name" => {
let lit: LitStr = input.parse()?;
name = Some(lit.value());
}
"rationale" => {
let lit: LitStr = input.parse()?;
rationale = Some(lit.value());
}
"signed_by" => {
let lit: LitStr = input.parse()?;
signed_by = Some(lit.value());
}
other => {
return Err(syn::Error::new(
key.span(),
format!(
"unknown #[poxparty] field `{other}`; expected one of: \
exercise_type, until, name, rationale, signed_by"
),
));
}
}
if !input.is_empty() {
input.parse::<Token![,]>()?;
}
}
Ok(Self {
antigen,
exercise_type,
exercise_type_span,
until,
until_span,
name,
rationale,
signed_by,
args_span,
})
}
}
impl PoxpartyArgs {
pub fn validate(&self) -> syn::Result<()> {
match self.exercise_type.as_deref() {
None => {
return Err(syn::Error::new(
self.args_span,
"#[poxparty] requires `exercise_type = \"...\"`. \
Per ADR-023: describes the controlled exposure exercise.",
));
}
Some(et) if et.len() < 20 => {
return Err(syn::Error::new(
self.exercise_type_span.unwrap_or(self.args_span),
format!(
"#[poxparty] `exercise_type` must be at least 20 characters \
(got {}); per ADR-023 loudness-as-discipline.",
et.len()
),
));
}
Some(et) if et.trim().is_empty() => {
return Err(syn::Error::new(
self.exercise_type_span.unwrap_or(self.args_span),
"#[poxparty] `exercise_type` must not be whitespace-only; \
a blank exercise type bypasses the loudness-as-discipline \
requirement (ADR-023).",
));
}
_ => {}
}
match self.until.as_deref() {
None => {
return Err(syn::Error::new(
self.args_span,
"#[poxparty] requires `until = \"YYYY-MM-DD\"`. \
A pox party without a deadline runs indefinitely.",
));
}
Some("") => {
return Err(syn::Error::new(
self.until_span.unwrap_or(self.args_span),
"#[poxparty] `until` must not be empty.",
));
}
_ => {}
}
if let Some(until_str) = self.until.as_deref() {
match parse_iso_date(until_str) {
Ok(until_date) => {
let horizon_days = (until_date - today_utc()).num_days();
if horizon_days < 0 {
return Err(syn::Error::new(
self.until_span.unwrap_or(self.args_span),
format!(
"#[poxparty] `until` ({until_str}) is {} day(s) in the past — \
an expired pox-party exercise is stale controlled exposure with \
no accountability. Set a future `until` date or remove the \
#[poxparty] marker.",
-horizon_days
),
));
}
}
Err(()) => {
return Err(syn::Error::new(
self.until_span.unwrap_or(self.args_span),
format!(
"#[poxparty] `until` value {until_str:?} is not a valid ISO-8601 \
date. Use YYYY-MM-DD format, e.g. `until = \"2026-12-31\"`."
),
));
}
}
}
Ok(())
}
}
#[derive(Debug)]
pub struct OrientArgs {
#[allow(dead_code)]
pub antigen: Option<syn::Path>,
#[allow(dead_code)]
pub learning_path: Option<String>,
#[allow(dead_code)]
pub learning_path_span: Option<Span>,
#[allow(dead_code)]
pub until: Option<String>,
#[allow(dead_code)]
pub until_span: Option<Span>,
#[allow(dead_code)]
pub args_span: Span,
}
impl Parse for OrientArgs {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
let args_span = input.span();
let mut antigen: Option<syn::Path> = None;
let mut learning_path: Option<String> = None;
let mut learning_path_span: Option<Span> = None;
let mut until: Option<String> = None;
let mut until_span: Option<Span> = None;
if !input.is_empty() && input.peek(Ident) && !input.peek2(Token![=]) {
antigen = Some(input.parse()?);
if !input.is_empty() {
input.parse::<Token![,]>()?;
}
}
while !input.is_empty() {
let key: Ident = input.parse()?;
input.parse::<Token![=]>()?;
match key.to_string().as_str() {
"learning_path" => {
let lit: LitStr = input.parse()?;
learning_path_span = Some(lit.span());
learning_path = Some(lit.value());
}
"until" => {
let lit: LitStr = input.parse()?;
until_span = Some(lit.span());
until = Some(lit.value());
}
"see" | "adr" | "attestation_optional" => {
return Err(syn::Error::new(
key.span(),
format!(
"#[orient] field `{key}` was removed in the ADR-023 restoration \
(it was never in the spec). The orient spec is \
`#[orient(<antigen>, learning_path = \"...\", until = \"YYYY-MM-DD\")]`. \
Migrate `see`/`adr` context into the `learning_path` text (or \
`references = [...]` on the antigen declaration); \
`attestation_optional` inverted loudness-as-discipline and has no \
replacement."
),
));
}
other => {
return Err(syn::Error::new(
key.span(),
format!(
"unknown #[orient] field `{other}`; expected: learning_path, until"
),
));
}
}
if !input.is_empty() {
input.parse::<Token![,]>()?;
}
}
Ok(Self {
antigen,
learning_path,
learning_path_span,
until,
until_span,
args_span,
})
}
}
impl OrientArgs {
pub fn validate(&self) -> syn::Result<()> {
match self.learning_path.as_deref() {
None => {
return Err(syn::Error::new(
self.args_span,
"#[orient] requires `learning_path = \"...\"`. An orientation period \
without an explicit path forward is silent deferred non-immunity \
(= tolerance); orient exists to be loud about it (ADR-023).",
));
}
Some(p) if p.len() < 20 => {
return Err(syn::Error::new(
self.learning_path_span.unwrap_or(self.args_span),
format!(
"#[orient] `learning_path` must be at least 20 characters (got {}); \
per ADR-023 loudness-as-discipline (rationale-class field).",
p.len()
),
));
}
Some(p) if p.trim().is_empty() => {
return Err(syn::Error::new(
self.learning_path_span.unwrap_or(self.args_span),
"#[orient] `learning_path` must not be whitespace-only; \
a blank learning path bypasses the loudness-as-discipline \
requirement (ADR-023).",
));
}
_ => {}
}
let until_str = match self.until.as_deref() {
None => {
return Err(syn::Error::new(
self.args_span,
"#[orient] requires `until = \"YYYY-MM-DD\"`. An orientation period \
without a time-bound is indefinite; orient must escalate at a horizon.",
));
}
Some("") => {
return Err(syn::Error::new(
self.until_span.unwrap_or(self.args_span),
"#[orient] `until` must not be empty.",
));
}
Some(s) => s,
};
match parse_iso_date(until_str) {
Err(()) => {
return Err(syn::Error::new(
self.until_span.unwrap_or(self.args_span),
format!(
"#[orient] `until` must be an ISO-8601 date (YYYY-MM-DD); got `{until_str}`."
),
));
}
Ok(until_date) => {
let horizon_days = (until_date - today_utc()).num_days();
if horizon_days < 0 {
return Err(syn::Error::new(
self.until_span.unwrap_or(self.args_span),
format!(
"#[orient] `until` ({until_str}) is {} day(s) in the past — an \
already-expired orientation window. Set a future horizon, or if \
the period has genuinely lapsed, resolve the failure-class \
(declare immunity/tolerance) rather than leave an expired orient.",
-horizon_days
),
));
}
if horizon_days > ORIENT_DEFAULT_MAX_HORIZON_DAYS {
return Err(syn::Error::new(
self.until_span.unwrap_or(self.args_span),
format!(
"#[orient] `until` is {horizon_days}d out, beyond the \
{ORIENT_DEFAULT_MAX_HORIZON_DAYS}d orientation-period horizon \
(deferred_defense_max_horizon). An orientation period this long \
is not orientation — shorten `until` or address the failure-class."
),
));
}
}
}
Ok(())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MacroTriageDecision {
Black,
Red,
Yellow,
Green,
White,
}
impl MacroTriageDecision {
fn from_path_str(s: &str) -> Option<Self> {
match s {
"Black" | "TriageDecision::Black" | "black" => Some(Self::Black),
"Red" | "TriageDecision::Red" | "red" => Some(Self::Red),
"Yellow" | "TriageDecision::Yellow" | "yellow" => Some(Self::Yellow),
"Green" | "TriageDecision::Green" | "green" => Some(Self::Green),
"White" | "TriageDecision::White" | "white" => Some(Self::White),
_ => None,
}
}
#[allow(dead_code)]
pub const fn as_str(self) -> &'static str {
match self {
Self::Black => "black",
Self::Red => "red",
Self::Yellow => "yellow",
Self::Green => "green",
Self::White => "white",
}
}
}
#[derive(Debug)]
pub struct TriageCommitArgs {
pub triage_decision: Option<MacroTriageDecision>,
#[allow(dead_code)]
pub triage_decision_span: Option<Span>,
pub rollback_target: Option<String>,
pub rollback_target_span: Option<Span>,
pub triaged_by: Option<String>,
pub triaged_by_span: Option<Span>,
pub rationale: Option<String>,
pub rationale_span: Option<Span>,
pub rollback_due_within_minutes: Option<u32>,
pub rollback_due_within_minutes_span: Option<Span>,
pub args_span: Span,
}
fn parse_triage_decision_expr(
input: syn::parse::ParseStream,
expr: &Expr,
) -> syn::Result<(MacroTriageDecision, Span)> {
let expr_span = match expr {
Expr::Path(p) => p
.path
.segments
.first()
.map_or_else(|| input.span(), |s| s.ident.span()),
_ => input.span(),
};
let s = match expr {
Expr::Path(p) => {
let segs: Vec<String> = p
.path
.segments
.iter()
.map(|seg| seg.ident.to_string())
.collect();
segs.join("::")
}
_ => {
return Err(syn::Error::new_spanned(
expr,
"expected `TriageDecision::Black`, `TriageDecision::Red`, \
`TriageDecision::Yellow`, `TriageDecision::Green`, or \
`TriageDecision::White`",
));
}
};
let decision = MacroTriageDecision::from_path_str(&s).ok_or_else(|| {
syn::Error::new_spanned(
expr,
format!(
"unknown TriageDecision `{s}`; expected one of: Black, Red, \
Yellow, Green, White"
),
)
})?;
Ok((decision, expr_span))
}
impl Parse for TriageCommitArgs {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
let args_span = input.span();
let mut triage_decision: Option<MacroTriageDecision> = None;
let mut triage_decision_span: Option<Span> = None;
let mut rollback_target: Option<String> = None;
let mut rollback_target_span: Option<Span> = None;
let mut triaged_by: Option<String> = None;
let mut triaged_by_span: Option<Span> = None;
let mut rationale: Option<String> = None;
let mut rationale_span: Option<Span> = None;
let mut rollback_due_within_minutes: Option<u32> = None;
let mut rollback_due_within_minutes_span: Option<Span> = None;
while !input.is_empty() {
let key: Ident = input.parse()?;
input.parse::<Token![=]>()?;
match key.to_string().as_str() {
"triage_decision" => {
let expr: Expr = input.parse()?;
let (decision, span) = parse_triage_decision_expr(input, &expr)?;
triage_decision = Some(decision);
triage_decision_span = Some(span);
}
"rollback_target" => {
let lit: LitStr = input.parse()?;
rollback_target_span = Some(lit.span());
rollback_target = Some(lit.value());
}
"triaged_by" => {
let lit: LitStr = input.parse()?;
triaged_by_span = Some(lit.span());
triaged_by = Some(lit.value());
}
"rationale" => {
let lit: LitStr = input.parse()?;
rationale_span = Some(lit.span());
rationale = Some(lit.value());
}
"rollback_due_within_minutes" => {
let lit: syn::LitInt = input.parse()?;
rollback_due_within_minutes_span = Some(lit.span());
rollback_due_within_minutes = Some(lit.base10_parse::<u32>()?);
}
other => {
return Err(syn::Error::new(
key.span(),
format!(
"unknown #[triage_commit] field `{other}`; expected one of: \
triage_decision, rollback_target, triaged_by, rationale, \
rollback_due_within_minutes"
),
));
}
}
if !input.is_empty() {
input.parse::<Token![,]>()?;
}
}
Ok(Self {
triage_decision,
triage_decision_span,
rollback_target,
rollback_target_span,
triaged_by,
triaged_by_span,
rationale,
rationale_span,
rollback_due_within_minutes,
rollback_due_within_minutes_span,
args_span,
})
}
}
impl TriageCommitArgs {
pub fn validate(&self) -> syn::Result<()> {
if self.triage_decision.is_none() {
return Err(syn::Error::new(
self.args_span,
"#[triage_commit] requires `triage_decision = TriageDecision::X` \
(one of Black, Red, Yellow, Green, White). Per ADR-026 \
§Rollback-as-triage, the loud-acknowledgment IS the discipline.",
));
}
match self.rollback_target.as_deref() {
None => {
return Err(syn::Error::new(
self.args_span,
"#[triage_commit] requires `rollback_target = \"<sha>\"` \
(commit sha pointing to last-known-good state).",
));
}
Some(s) if s.trim().is_empty() => {
return Err(syn::Error::new(
self.rollback_target_span.unwrap_or(self.args_span),
"#[triage_commit] `rollback_target` cannot be empty or whitespace-only. \
A rollback without a target is not a rollback.",
));
}
Some(_) => {}
}
match self.triaged_by.as_deref() {
None => {
return Err(syn::Error::new(
self.args_span,
"#[triage_commit] requires `triaged_by = \"<role|name>\"`. \
Per ADR-026 §Rollback-as-triage clinical-medicine \
grounding, informed-consent requires an authoring identity.",
));
}
Some(s) if s.trim().is_empty() => {
return Err(syn::Error::new(
self.triaged_by_span.unwrap_or(self.args_span),
"#[triage_commit] `triaged_by` cannot be empty or whitespace-only.",
));
}
Some(_) => {}
}
match self.rationale.as_deref() {
None => {
return Err(syn::Error::new(
self.args_span,
"#[triage_commit] requires `rationale = \"...\"` \
(chart-documentation; minimum 20 characters). Per ADR-026 \
§Rollback-as-triage: rationale before action, not after.",
));
}
Some(s) if s.len() < 20 => {
return Err(syn::Error::new(
self.rationale_span.unwrap_or(self.args_span),
format!(
"#[triage_commit] `rationale` must be at least 20 \
characters (got {}); per ADR-023 loudness-as-discipline \
applied to clinical-medicine chart-documentation.",
s.len()
),
));
}
Some(s) if s.trim().is_empty() => {
return Err(syn::Error::new(
self.rationale_span.unwrap_or(self.args_span),
"#[triage_commit] `rationale` must not be whitespace-only; \
a blank rationale bypasses chart-documentation discipline \
(ADR-026 §Rollback-as-triage: rationale before action, not after).",
));
}
Some(_) => {}
}
match self.rollback_due_within_minutes {
None => {
return Err(syn::Error::new(
self.args_span,
"#[triage_commit] requires `rollback_due_within_minutes = N` \
(positive integer; e.g., 30 for a Red triage per ADR-026 example).",
));
}
Some(0) => {
return Err(syn::Error::new(
self.rollback_due_within_minutes_span
.unwrap_or(self.args_span),
"#[triage_commit] `rollback_due_within_minutes` must be > 0. \
A zero deadline degrades to no-deadline; per ADR-026 \
§Rollback-as-triage the time-bound carries discipline.",
));
}
Some(_) => {}
}
Ok(())
}
}
#[derive(Debug)]
pub struct DiagnosticArgs {
pub modality_classes: Vec<String>,
pub modality_span: Option<Span>,
pub min_independent: Option<u64>,
pub min_independent_span: Option<Span>,
pub args_span: Span,
}
impl Parse for DiagnosticArgs {
fn parse(input: ParseStream) -> syn::Result<Self> {
use syn::LitInt;
let args_span = input.span();
let mut modality_classes: Vec<String> = Vec::new();
let mut modality_span: Option<Span> = None;
let mut min_independent: Option<u64> = None;
let mut min_independent_span: Option<Span> = None;
while !input.is_empty() {
let key: Ident = input.parse()?;
input.parse::<Token![=]>()?;
match key.to_string().as_str() {
"modalities" => {
let arr: syn::ExprArray = input.parse()?;
modality_span = Some(arr.bracket_token.span.join());
for elem in &arr.elems {
if let Expr::Path(p) = elem {
if let Some(seg) = p.path.segments.last() {
modality_classes.push(seg.ident.to_string());
continue;
}
}
return Err(syn::Error::new_spanned(
elem,
"expected a `WitnessClass::*` path in `modalities` array",
));
}
}
"min_independent" => {
let lit: LitInt = input.parse()?;
min_independent_span = Some(lit.span());
min_independent = Some(lit.base10_parse::<u64>()?);
}
other => {
return Err(syn::Error::new(
key.span(),
format!(
"unknown #[diagnostic] field `{other}`; expected: modalities, min_independent"
),
));
}
}
if !input.is_empty() {
input.parse::<Token![,]>()?;
}
}
Ok(Self {
modality_classes,
modality_span,
min_independent,
min_independent_span,
args_span,
})
}
}
const KNOWN_WITNESS_CLASSES: &[&str] = &[
"StaticAnalysis",
"PropertyTest",
"FormalVerification",
"ManualReview",
"RuntimeFuzz",
"SubstrateWitness",
];
impl DiagnosticArgs {
pub fn validate(&self) -> syn::Result<()> {
if self.modality_classes.is_empty() {
return Err(syn::Error::new(
self.modality_span.unwrap_or(self.args_span),
"#[diagnostic] requires non-empty `modalities = [WitnessClass::X, ...]` \
(ADR-024 §Decision; an empty modality set has no evidence to converge).",
));
}
for class in &self.modality_classes {
if !KNOWN_WITNESS_CLASSES.contains(&class.as_str()) {
return Err(syn::Error::new(
self.modality_span.unwrap_or(self.args_span),
format!(
"#[diagnostic] `modalities` entry `{class}` is not a known \
WitnessClass variant; expected one of: {}. \
(A modality that names no real witness class is dead evidence — \
it can never converge.)",
KNOWN_WITNESS_CLASSES.join(", ")
),
));
}
}
let Some(min) = self.min_independent else {
return Err(syn::Error::new(
self.args_span,
"#[diagnostic] requires `min_independent = N` (ADR-024 §Decision; \
N counts distinct WitnessClass categories per adversarial C1).",
));
};
if min == 0 {
return Err(syn::Error::new(
self.min_independent_span.unwrap_or(self.args_span),
"#[diagnostic] `min_independent` must be at least 1 (a 0-floor is \
vacuously true and structurally meaningless).",
));
}
let mut distinct: Vec<&String> = Vec::new();
for c in &self.modality_classes {
if !distinct.contains(&c) {
distinct.push(c);
}
}
if u64::try_from(distinct.len()).unwrap_or(u64::MAX) < min {
return Err(syn::Error::new(
self.min_independent_span.unwrap_or(self.args_span),
format!(
"#[diagnostic] `min_independent = {min}` exceeds the number of \
distinct WitnessClass categories supplied ({}). Per ADR-024 \
adversarial C1, min_independent counts CLASSES not witnesses — \
duplicate classes don't add independence. Increase distinct \
modalities or lower the floor.",
distinct.len()
),
));
}
Ok(())
}
}
#[derive(Debug)]
pub struct ClonalArgs {
#[allow(dead_code)]
pub witness: Option<Expr>,
pub witness_span: Option<Span>,
pub iterations: Option<u64>,
pub iterations_span: Option<Span>,
#[allow(dead_code)]
pub seed_kind: Option<String>,
pub seed_span: Option<Span>,
pub seed_is_fixed: bool,
pub args_span: Span,
}
impl Parse for ClonalArgs {
fn parse(input: ParseStream) -> syn::Result<Self> {
use syn::LitInt;
let args_span = input.span();
let mut witness: Option<Expr> = None;
let mut witness_span: Option<Span> = None;
let mut iterations: Option<u64> = None;
let mut iterations_span: Option<Span> = None;
let mut seed_kind: Option<String> = None;
let mut seed_span: Option<Span> = None;
let mut seed_is_fixed = false;
while !input.is_empty() {
let key: Ident = input.parse()?;
input.parse::<Token![=]>()?;
match key.to_string().as_str() {
"witness" => {
let e: Expr = input.parse()?;
witness_span = Some(syn::spanned::Spanned::span(&e));
witness = Some(e);
}
"iterations" => {
let lit: LitInt = input.parse()?;
iterations_span = Some(lit.span());
iterations = Some(lit.base10_parse::<u64>()?);
}
"seed" => {
let e: Expr = input.parse()?;
seed_span = Some(syn::spanned::Spanned::span(&e));
if let Expr::Path(p) = &e {
if let Some(seg) = p.path.segments.last() {
let name = seg.ident.to_string();
seed_kind = Some(name);
}
} else if let Expr::Call(c) = &e {
if let Expr::Path(p) = &*c.func {
if let Some(seg) = p.path.segments.last() {
let name = seg.ident.to_string();
if name == "Fixed" {
seed_is_fixed = true;
}
seed_kind = Some(name);
}
}
}
}
other => {
return Err(syn::Error::new(
key.span(),
format!(
"unknown #[clonal] field `{other}`; expected: witness, iterations, seed"
),
));
}
}
if !input.is_empty() {
input.parse::<Token![,]>()?;
}
}
Ok(Self {
witness,
witness_span,
iterations,
iterations_span,
seed_kind,
seed_span,
seed_is_fixed,
args_span,
})
}
}
impl ClonalArgs {
pub fn validate(&self) -> syn::Result<()> {
if self.witness.is_none() {
return Err(syn::Error::new(
self.witness_span.unwrap_or(self.args_span),
"#[clonal] requires `witness = <identifier>` referencing the \
per-iteration function/test (ADR-024 §Decision).",
));
}
let Some(iters) = self.iterations else {
return Err(syn::Error::new(
self.args_span,
"#[clonal] requires `iterations = N` (ADR-024 §Decision; \
explicit iteration count is the structural memory of how \
much independence the witness is claiming).",
));
};
if iters == 0 {
return Err(syn::Error::new(
self.iterations_span.unwrap_or(self.args_span),
"#[clonal] `iterations` must be > 0 (zero iterations means \
no evidence).",
));
}
if self.seed_is_fixed {
return Err(syn::Error::new(
self.seed_span.unwrap_or(self.args_span),
"#[clonal] rejects `seed = SeedKind::Fixed(_)` — a fixed seed \
makes 'iterations' a misnomer (every iteration replays the same \
RNG state). Use SeedKind::Random, SeedKind::EntropyFromCi, or \
SeedKind::TimestampSeeded. \
Per ADR-024 adversarial C2: COMPILE-TIME enforcement.",
));
}
Ok(())
}
}
#[derive(Debug)]
pub struct IggArgs {
#[allow(dead_code)]
pub witnesses: Vec<Expr>,
pub witnesses_span: Option<Span>,
pub historical_span: Option<u64>,
pub historical_span_span: Option<Span>,
pub min_reattestations: Option<u64>,
pub min_reattestations_span: Option<Span>,
pub args_span: Span,
}
impl Parse for IggArgs {
fn parse(input: ParseStream) -> syn::Result<Self> {
use syn::LitInt;
let args_span = input.span();
let mut witnesses: Vec<Expr> = Vec::new();
let mut witnesses_span: Option<Span> = None;
let mut historical_span: Option<u64> = None;
let mut historical_span_span: Option<Span> = None;
let mut min_reattestations: Option<u64> = None;
let mut min_reattestations_span: Option<Span> = None;
while !input.is_empty() {
let key: Ident = input.parse()?;
input.parse::<Token![=]>()?;
match key.to_string().as_str() {
"witnesses" => {
let arr: syn::ExprArray = input.parse()?;
witnesses_span = Some(arr.bracket_token.span.join());
for elem in &arr.elems {
witnesses.push(elem.clone());
}
}
"historical_span" => {
let lit: LitInt = input.parse()?;
historical_span_span = Some(lit.span());
historical_span = Some(lit.base10_parse::<u64>()?);
}
"min_reattestations" => {
let lit: LitInt = input.parse()?;
min_reattestations_span = Some(lit.span());
min_reattestations = Some(lit.base10_parse::<u64>()?);
}
other => {
return Err(syn::Error::new(
key.span(),
format!(
"unknown #[igg] field `{other}`; expected: \
witnesses, historical_span, min_reattestations"
),
));
}
}
if !input.is_empty() {
input.parse::<Token![,]>()?;
}
}
Ok(Self {
witnesses,
witnesses_span,
historical_span,
historical_span_span,
min_reattestations,
min_reattestations_span,
args_span,
})
}
}
impl IggArgs {
pub fn validate(&self) -> syn::Result<()> {
if self.witnesses.is_empty() {
return Err(syn::Error::new(
self.witnesses_span.unwrap_or(self.args_span),
"#[igg] requires non-empty `witnesses = [...]` (ADR-024 §Decision).",
));
}
let Some(span) = self.historical_span else {
return Err(syn::Error::new(
self.args_span,
"#[igg] requires `historical_span = N` (days; ADR-024 §Decision).",
));
};
if span == 0 {
return Err(syn::Error::new(
self.historical_span_span.unwrap_or(self.args_span),
"#[igg] `historical_span` must be > 0 days.",
));
}
let Some(min) = self.min_reattestations else {
return Err(syn::Error::new(
self.args_span,
"#[igg] requires `min_reattestations = N` (ADR-024 §Decision).",
));
};
if min == 0 {
return Err(syn::Error::new(
self.min_reattestations_span.unwrap_or(self.args_span),
"#[igg] `min_reattestations` must be > 0.",
));
}
Ok(())
}
}
#[derive(Debug)]
pub struct CrossreactiveArgs {
#[allow(dead_code)]
pub fingerprints: Vec<String>,
pub fingerprints_span: Option<Span>,
pub args_span: Span,
}
impl Parse for CrossreactiveArgs {
fn parse(input: ParseStream) -> syn::Result<Self> {
let args_span = input.span();
let mut fingerprints: Vec<String> = Vec::new();
let mut fingerprints_span: Option<Span> = None;
while !input.is_empty() {
let key: Ident = input.parse()?;
input.parse::<Token![=]>()?;
match key.to_string().as_str() {
"fingerprints" => {
let arr: syn::ExprArray = input.parse()?;
fingerprints_span = Some(arr.bracket_token.span.join());
for elem in &arr.elems {
if let Expr::Lit(syn::ExprLit {
lit: Lit::Str(s), ..
}) = elem
{
fingerprints.push(s.value());
} else {
return Err(syn::Error::new_spanned(
elem,
"expected a string literal fingerprint in `fingerprints` array",
));
}
}
}
other => {
return Err(syn::Error::new(
key.span(),
format!("unknown #[crossreactive] field `{other}`; expected: fingerprints"),
));
}
}
if !input.is_empty() {
input.parse::<Token![,]>()?;
}
}
Ok(Self {
fingerprints,
fingerprints_span,
args_span,
})
}
}
impl CrossreactiveArgs {
pub fn validate(&self) -> syn::Result<()> {
if self.fingerprints.is_empty() {
return Err(syn::Error::new(
self.fingerprints_span.unwrap_or(self.args_span),
"#[crossreactive] requires non-empty `fingerprints = [...]` \
(a defense that covers no related antigens isn't crossreactive).",
));
}
for f in &self.fingerprints {
if f.is_empty() {
return Err(syn::Error::new(
self.fingerprints_span.unwrap_or(self.args_span),
"#[crossreactive] `fingerprints` entries must be non-empty strings.",
));
}
}
Ok(())
}
}
#[derive(Debug)]
pub struct PolyclonalArgs {
#[allow(dead_code)]
pub args_span: Span,
}
impl Parse for PolyclonalArgs {
fn parse(input: ParseStream) -> syn::Result<Self> {
let args_span = input.span();
while !input.is_empty() {
let _: proc_macro2::TokenTree = input.parse()?;
}
Ok(Self { args_span })
}
}
impl PolyclonalArgs {
#[allow(clippy::unnecessary_wraps, clippy::unused_self)]
pub const fn validate(&self) -> syn::Result<()> {
Ok(())
}
}
#[derive(Debug)]
pub struct MonoclonalArgs {
#[allow(dead_code)]
pub args_span: Span,
}
impl Parse for MonoclonalArgs {
fn parse(input: ParseStream) -> syn::Result<Self> {
let args_span = input.span();
while !input.is_empty() {
let _: proc_macro2::TokenTree = input.parse()?;
}
Ok(Self { args_span })
}
}
impl MonoclonalArgs {
#[allow(clippy::unnecessary_wraps, clippy::unused_self)]
pub const fn validate(&self) -> syn::Result<()> {
Ok(())
}
}
#[derive(Debug)]
pub struct AdccArgs {
#[allow(dead_code)]
pub args_span: Span,
}
impl Parse for AdccArgs {
fn parse(input: ParseStream) -> syn::Result<Self> {
let args_span = input.span();
while !input.is_empty() {
let _: proc_macro2::TokenTree = input.parse()?;
}
Ok(Self { args_span })
}
}
impl AdccArgs {
#[allow(clippy::unnecessary_wraps, clippy::unused_self)]
pub const fn validate(&self) -> syn::Result<()> {
Ok(())
}
}
#[derive(Debug)]
pub struct ItchArgs {
pub name: Option<String>,
pub name_span: Option<Span>,
#[allow(dead_code)]
pub antigen: Option<syn::Path>,
pub description: Option<String>,
pub description_span: Option<Span>,
#[allow(dead_code)]
pub threshold: Option<String>,
pub args_span: Span,
}
impl Parse for ItchArgs {
fn parse(input: ParseStream) -> syn::Result<Self> {
let args_span = input.span();
let mut name: Option<String> = None;
let mut name_span: Option<Span> = None;
let mut antigen: Option<syn::Path> = None;
let mut description: Option<String> = None;
let mut description_span: Option<Span> = None;
let mut threshold: Option<String> = None;
while !input.is_empty() {
let key: Ident = input.parse()?;
input.parse::<Token![=]>()?;
match key.to_string().as_str() {
"name" => {
let lit: LitStr = input.parse()?;
name_span = Some(lit.span());
name = Some(lit.value());
}
"antigen" => {
antigen = Some(input.parse()?);
}
"description" => {
let lit: LitStr = input.parse()?;
description_span = Some(lit.span());
description = Some(lit.value());
}
"threshold" => {
let lit: LitStr = input.parse()?;
threshold = Some(lit.value());
}
other => {
return Err(syn::Error::new(
key.span(),
format!(
"unknown #[itch] field `{other}`; expected one of: \
name, antigen, description, threshold"
),
));
}
}
if !input.is_empty() {
input.parse::<Token![,]>()?;
}
}
Ok(Self {
name,
name_span,
antigen,
description,
description_span,
threshold,
args_span,
})
}
}
impl ItchArgs {
pub fn validate(&self) -> syn::Result<()> {
match self.name.as_deref() {
None => {
return Err(syn::Error::new(
self.args_span,
"#[itch] requires `name = \"<slug>\"` (kebab-case identifier).",
));
}
Some(s) if s.trim().is_empty() => {
return Err(syn::Error::new(
self.name_span.unwrap_or(self.args_span),
"#[itch] `name` cannot be empty.",
));
}
Some(_) => {}
}
match self.description.as_deref() {
None => {
return Err(syn::Error::new(
self.args_span,
"#[itch] requires `description = \"...\"` (what is being noticed; \
minimum 10 characters).",
));
}
Some(s) if s.len() < 10 => {
return Err(syn::Error::new(
self.description_span.unwrap_or(self.args_span),
format!(
"#[itch] `description` must be at least 10 characters \
(got {}); per cognitive-organizational discipline a noticing \
needs enough text to be useful next-time.",
s.len()
),
));
}
Some(_) => {}
}
Ok(())
}
}
#[derive(Debug)]
pub struct RecurrenceAnchorArgs {
#[allow(dead_code)]
pub antigen: Option<syn::Path>,
pub instances: Option<u32>,
pub instances_span: Option<Span>,
pub since: Option<String>,
pub since_span: Option<Span>,
pub rationale: Option<String>,
pub rationale_span: Option<Span>,
pub args_span: Span,
}
impl Parse for RecurrenceAnchorArgs {
fn parse(input: ParseStream) -> syn::Result<Self> {
let args_span = input.span();
let mut antigen: Option<syn::Path> = None;
let mut instances: Option<u32> = None;
let mut instances_span: Option<Span> = None;
let mut since: Option<String> = None;
let mut since_span: Option<Span> = None;
let mut rationale: Option<String> = None;
let mut rationale_span: Option<Span> = None;
if !input.is_empty() && input.peek(Ident) && !input.peek2(Token![=]) {
antigen = Some(input.parse()?);
if !input.is_empty() {
input.parse::<Token![,]>()?;
}
}
while !input.is_empty() {
let key: Ident = input.parse()?;
input.parse::<Token![=]>()?;
match key.to_string().as_str() {
"antigen" => {
antigen = Some(input.parse()?);
}
"instances" => {
let lit: syn::LitInt = input.parse()?;
instances_span = Some(lit.span());
instances = Some(lit.base10_parse::<u32>()?);
}
"since" => {
let lit: LitStr = input.parse()?;
since_span = Some(lit.span());
since = Some(lit.value());
}
"rationale" => {
let lit: LitStr = input.parse()?;
rationale_span = Some(lit.span());
rationale = Some(lit.value());
}
other => {
return Err(syn::Error::new(
key.span(),
format!(
"unknown #[recurrence_anchor] field `{other}`; expected \
one of: antigen, instances, since, rationale"
),
));
}
}
if !input.is_empty() {
input.parse::<Token![,]>()?;
}
}
Ok(Self {
antigen,
instances,
instances_span,
since,
since_span,
rationale,
rationale_span,
args_span,
})
}
}
impl RecurrenceAnchorArgs {
pub fn validate(&self) -> syn::Result<()> {
if self.antigen.is_none() {
return Err(syn::Error::new(
self.args_span,
"#[recurrence_anchor] requires an `antigen` path (the failure-class \
being formally anchored as a cross-substrate recurrence).",
));
}
match self.instances {
None => {
return Err(syn::Error::new(
self.args_span,
"#[recurrence_anchor] requires `instances = N` (positive \
integer; how many recurrences have been observed).",
));
}
Some(0) => {
return Err(syn::Error::new(
self.instances_span.unwrap_or(self.args_span),
"#[recurrence_anchor] `instances` must be > 0; an anchor at \
zero observed instances is structurally premature.",
));
}
Some(_) => {}
}
match self.since.as_deref() {
None => {
return Err(syn::Error::new(
self.args_span,
"#[recurrence_anchor] requires `since = \"<date-or-version>\"` \
(first detected instance anchor).",
));
}
Some(s) if s.trim().is_empty() => {
return Err(syn::Error::new(
self.since_span.unwrap_or(self.args_span),
"#[recurrence_anchor] `since` cannot be empty.",
));
}
Some(_) => {}
}
match self.rationale.as_deref() {
None => {
return Err(syn::Error::new(
self.args_span,
"#[recurrence_anchor] requires `rationale = \"...\"` \
(≥20 characters; why this recurrence warrants action).",
));
}
Some(s) if s.len() < 20 => {
return Err(syn::Error::new(
self.rationale_span.unwrap_or(self.args_span),
format!(
"#[recurrence_anchor] `rationale` must be at least 20 \
characters (got {}); clinical-diagnosis-grade rationale \
per ADR-024 clinical-medicine grounding.",
s.len()
),
));
}
Some(_) => {}
}
Ok(())
}
}
#[derive(Debug)]
pub struct CrystallizeArgs {
pub name: Option<String>,
pub name_span: Option<Span>,
#[allow(dead_code)]
pub from_itches: Vec<syn::Path>,
#[allow(dead_code)]
pub antigen: Option<syn::Path>,
pub summary: Option<String>,
pub summary_span: Option<Span>,
pub args_span: Span,
}
impl Parse for CrystallizeArgs {
fn parse(input: ParseStream) -> syn::Result<Self> {
let args_span = input.span();
let mut name: Option<String> = None;
let mut name_span: Option<Span> = None;
let mut from_itches: Vec<syn::Path> = Vec::new();
let mut antigen: Option<syn::Path> = None;
let mut summary: Option<String> = None;
let mut summary_span: Option<Span> = None;
while !input.is_empty() {
let key: Ident = input.parse()?;
input.parse::<Token![=]>()?;
match key.to_string().as_str() {
"name" => {
let lit: LitStr = input.parse()?;
name_span = Some(lit.span());
name = Some(lit.value());
}
"from_itches" => {
let arr: syn::ExprArray = input.parse()?;
for elem in &arr.elems {
if let Expr::Path(p) = elem {
from_itches.push(p.path.clone());
} else {
return Err(syn::Error::new_spanned(
elem,
"expected a path expression in `from_itches` array",
));
}
}
}
"antigen" => {
antigen = Some(input.parse()?);
}
"summary" => {
let lit: LitStr = input.parse()?;
summary_span = Some(lit.span());
summary = Some(lit.value());
}
other => {
return Err(syn::Error::new(
key.span(),
format!(
"unknown #[crystallize] field `{other}`; expected \
one of: name, from_itches, antigen, summary"
),
));
}
}
if !input.is_empty() {
input.parse::<Token![,]>()?;
}
}
Ok(Self {
name,
name_span,
from_itches,
antigen,
summary,
summary_span,
args_span,
})
}
}
impl CrystallizeArgs {
pub fn validate(&self) -> syn::Result<()> {
match self.name.as_deref() {
None => {
return Err(syn::Error::new(
self.args_span,
"#[crystallize] requires `name = \"<slug>\"`.",
));
}
Some(s) if s.trim().is_empty() => {
return Err(syn::Error::new(
self.name_span.unwrap_or(self.args_span),
"#[crystallize] `name` cannot be empty.",
));
}
Some(_) => {}
}
match self.summary.as_deref() {
None => {
return Err(syn::Error::new(
self.args_span,
"#[crystallize] requires `summary = \"...\"` (≥10 characters).",
));
}
Some(s) if s.len() < 10 => {
return Err(syn::Error::new(
self.summary_span.unwrap_or(self.args_span),
format!(
"#[crystallize] `summary` must be at least 10 characters \
(got {}); per cognitive-organizational discipline a \
crystallization event needs enough text to be useful.",
s.len()
),
));
}
Some(_) => {}
}
Ok(())
}
}
#[derive(Debug)]
pub struct ChronicArgs {
#[allow(dead_code)]
pub antigen: Option<syn::Path>,
pub since: Option<String>,
pub since_span: Option<Span>,
#[allow(dead_code)]
pub status: Option<String>,
#[allow(dead_code)]
pub managed_by: Option<String>,
pub args_span: Span,
}
impl Parse for ChronicArgs {
fn parse(input: ParseStream) -> syn::Result<Self> {
let args_span = input.span();
let mut antigen: Option<syn::Path> = None;
let mut since: Option<String> = None;
let mut since_span: Option<Span> = None;
let mut status: Option<String> = None;
let mut managed_by: Option<String> = None;
if !input.is_empty() && input.peek(Ident) && !input.peek2(Token![=]) {
antigen = Some(input.parse()?);
if !input.is_empty() {
input.parse::<Token![,]>()?;
}
}
while !input.is_empty() {
let key: Ident = input.parse()?;
input.parse::<Token![=]>()?;
match key.to_string().as_str() {
"antigen" => {
antigen = Some(input.parse()?);
}
"since" => {
let lit: LitStr = input.parse()?;
since_span = Some(lit.span());
since = Some(lit.value());
}
"status" => {
let lit: LitStr = input.parse()?;
status = Some(lit.value());
}
"managed_by" => {
let lit: LitStr = input.parse()?;
managed_by = Some(lit.value());
}
other => {
return Err(syn::Error::new(
key.span(),
format!(
"unknown #[chronic] field `{other}`; expected one of: \
antigen, since, status, managed_by"
),
));
}
}
if !input.is_empty() {
input.parse::<Token![,]>()?;
}
}
Ok(Self {
antigen,
since,
since_span,
status,
managed_by,
args_span,
})
}
}
impl ChronicArgs {
pub fn validate(&self) -> syn::Result<()> {
if self.antigen.is_none() {
return Err(syn::Error::new(
self.args_span,
"#[chronic] requires an `antigen` path (the failure-class \
being marked chronic).",
));
}
match self.since.as_deref() {
None => {
return Err(syn::Error::new(
self.args_span,
"#[chronic] requires `since = \"<date-or-version>\"` (when \
the chronic signal was first observed).",
));
}
Some(s) if s.trim().is_empty() => {
return Err(syn::Error::new(
self.since_span.unwrap_or(self.args_span),
"#[chronic] `since` cannot be empty.",
));
}
Some(_) => {}
}
Ok(())
}
}
#[derive(Debug)]
pub struct SaturateArgs {
#[allow(dead_code)]
pub antigen: Option<syn::Path>,
#[allow(dead_code)]
pub contributing_to: Option<String>,
pub description: Option<String>,
pub description_span: Option<Span>,
pub args_span: Span,
}
impl Parse for SaturateArgs {
fn parse(input: ParseStream) -> syn::Result<Self> {
let args_span = input.span();
let mut antigen: Option<syn::Path> = None;
let mut contributing_to: Option<String> = None;
let mut description: Option<String> = None;
let mut description_span: Option<Span> = None;
while !input.is_empty() {
let key: Ident = input.parse()?;
input.parse::<Token![=]>()?;
match key.to_string().as_str() {
"antigen" => {
antigen = Some(input.parse()?);
}
"contributing_to" => {
let lit: LitStr = input.parse()?;
contributing_to = Some(lit.value());
}
"description" => {
let lit: LitStr = input.parse()?;
description_span = Some(lit.span());
description = Some(lit.value());
}
other => {
return Err(syn::Error::new(
key.span(),
format!(
"unknown #[saturate] field `{other}`; expected one of: \
antigen, contributing_to, description"
),
));
}
}
if !input.is_empty() {
input.parse::<Token![,]>()?;
}
}
Ok(Self {
antigen,
contributing_to,
description,
description_span,
args_span,
})
}
}
impl SaturateArgs {
pub fn validate(&self) -> syn::Result<()> {
match self.description.as_deref() {
None => {
return Err(syn::Error::new(
self.args_span,
"#[saturate] requires `description = \"...\"` (≥10 characters; \
what evidence is saturating).",
));
}
Some(s) if s.len() < 10 => {
return Err(syn::Error::new(
self.description_span.unwrap_or(self.args_span),
format!(
"#[saturate] `description` must be at least 10 characters \
(got {}).",
s.len()
),
));
}
Some(_) => {}
}
Ok(())
}
}
#[derive(Debug)]
pub struct StrandArgs {
pub name: Option<String>,
pub name_span: Option<Span>,
#[allow(dead_code)]
pub anchored_by: Vec<syn::Path>,
pub description: Option<String>,
pub description_span: Option<Span>,
pub args_span: Span,
}
impl Parse for StrandArgs {
fn parse(input: ParseStream) -> syn::Result<Self> {
let args_span = input.span();
let mut name: Option<String> = None;
let mut name_span: Option<Span> = None;
let mut anchored_by: Vec<syn::Path> = Vec::new();
let mut description: Option<String> = None;
let mut description_span: Option<Span> = None;
while !input.is_empty() {
let key: Ident = input.parse()?;
input.parse::<Token![=]>()?;
match key.to_string().as_str() {
"name" => {
let lit: LitStr = input.parse()?;
name_span = Some(lit.span());
name = Some(lit.value());
}
"anchored_by" => {
let arr: syn::ExprArray = input.parse()?;
for elem in &arr.elems {
if let Expr::Path(p) = elem {
anchored_by.push(p.path.clone());
} else {
return Err(syn::Error::new_spanned(
elem,
"expected a path expression in `anchored_by` array",
));
}
}
}
"description" => {
let lit: LitStr = input.parse()?;
description_span = Some(lit.span());
description = Some(lit.value());
}
other => {
return Err(syn::Error::new(
key.span(),
format!(
"unknown #[strand] field `{other}`; expected one of: \
name, anchored_by, description"
),
));
}
}
if !input.is_empty() {
input.parse::<Token![,]>()?;
}
}
Ok(Self {
name,
name_span,
anchored_by,
description,
description_span,
args_span,
})
}
}
impl StrandArgs {
pub fn validate(&self) -> syn::Result<()> {
match self.name.as_deref() {
None => {
return Err(syn::Error::new(
self.args_span,
"#[strand] requires `name = \"<slug>\"`.",
));
}
Some(s) if s.trim().is_empty() => {
return Err(syn::Error::new(
self.name_span.unwrap_or(self.args_span),
"#[strand] `name` cannot be empty.",
));
}
Some(_) => {}
}
match self.description.as_deref() {
None => {
return Err(syn::Error::new(
self.args_span,
"#[strand] requires `description = \"...\"` (≥10 characters; \
what threads of noticing this strand groups).",
));
}
Some(s) if s.len() < 10 => {
return Err(syn::Error::new(
self.description_span.unwrap_or(self.args_span),
format!(
"#[strand] `description` must be at least 10 characters \
(got {}).",
s.len()
),
));
}
Some(_) => {}
}
Ok(())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MacroMucosalKind {
ApiRequest,
ApiResponse,
McpInvocation,
ExternalLink,
Iframe,
DatabaseQuery,
CrossService,
SubprocessLaunch,
DependencyImport,
UserInput,
FilesystemPath,
EnvironmentVariable,
ShellArgument,
}
impl MacroMucosalKind {
fn from_path_str(s: &str) -> Option<Self> {
let bare = s.strip_prefix("MucosalKind::").unwrap_or(s);
match bare {
"ApiRequest" | "api-request" | "api_request" => Some(Self::ApiRequest),
"ApiResponse" | "api-response" | "api_response" => Some(Self::ApiResponse),
"McpInvocation" | "mcp-invocation" | "mcp_invocation" => Some(Self::McpInvocation),
"ExternalLink" | "external-link" | "external_link" => Some(Self::ExternalLink),
"Iframe" | "iframe" => Some(Self::Iframe),
"DatabaseQuery" | "database-query" | "database_query" => Some(Self::DatabaseQuery),
"CrossService" | "cross-service" | "cross_service" => Some(Self::CrossService),
"SubprocessLaunch" | "subprocess-launch" | "subprocess_launch" => {
Some(Self::SubprocessLaunch)
}
"DependencyImport" | "dependency-import" | "dependency_import" => {
Some(Self::DependencyImport)
}
"UserInput" | "user-input" | "user_input" => Some(Self::UserInput),
"FilesystemPath" | "filesystem-path" | "filesystem_path" => Some(Self::FilesystemPath),
"EnvironmentVariable" | "environment-variable" | "environment_variable" => {
Some(Self::EnvironmentVariable)
}
"ShellArgument" | "shell-argument" | "shell_argument" => Some(Self::ShellArgument),
_ => None,
}
}
}
fn parse_mucosal_kind_expr(expr: &Expr) -> syn::Result<MacroMucosalKind> {
let s = match expr {
Expr::Path(p) => p
.path
.segments
.iter()
.map(|seg| seg.ident.to_string())
.collect::<Vec<_>>()
.join("::"),
_ => {
return Err(syn::Error::new_spanned(
expr,
"expected a `MucosalKind::X` path expression (e.g., \
`MucosalKind::UserInput`), not a string literal",
));
}
};
MacroMucosalKind::from_path_str(&s).ok_or_else(|| {
syn::Error::new_spanned(
expr,
format!(
"unknown MucosalKind `{s}`; expected one of the 13 sealed-set \
variants (ApiRequest, ApiResponse, McpInvocation, ExternalLink, \
Iframe, DatabaseQuery, CrossService, SubprocessLaunch, \
DependencyImport, UserInput, FilesystemPath, EnvironmentVariable, \
ShellArgument). Note: PrBoundary + Import were removed in ADR-027 \
Amendment 1."
),
)
})
}
#[derive(Debug)]
pub struct MucosalArgs {
pub kind: Option<MacroMucosalKind>,
#[allow(dead_code)]
pub kind_span: Option<Span>,
pub rationale: Option<String>,
pub rationale_span: Option<Span>,
pub args_span: Span,
}
impl Parse for MucosalArgs {
fn parse(input: ParseStream) -> syn::Result<Self> {
let args_span = input.span();
let mut kind: Option<MacroMucosalKind> = None;
let mut kind_span: Option<Span> = None;
let mut rationale: Option<String> = None;
let mut rationale_span: Option<Span> = None;
while !input.is_empty() {
let key: Ident = input.parse()?;
input.parse::<Token![=]>()?;
match key.to_string().as_str() {
"kind" => {
let expr: Expr = input.parse()?;
kind_span = Some(input.span());
kind = Some(parse_mucosal_kind_expr(&expr)?);
}
"rationale" => {
let lit: LitStr = input.parse()?;
rationale_span = Some(lit.span());
rationale = Some(lit.value());
}
other => {
return Err(syn::Error::new(
key.span(),
format!(
"unknown #[mucosal] field `{other}`; expected one of: \
kind, rationale"
),
));
}
}
if !input.is_empty() {
input.parse::<Token![,]>()?;
}
}
Ok(Self {
kind,
kind_span,
rationale,
rationale_span,
args_span,
})
}
}
impl MucosalArgs {
pub fn validate(&self) -> syn::Result<()> {
if self.kind.is_none() {
return Err(syn::Error::new(
self.args_span,
"#[mucosal] requires `kind = MucosalKind::X` (the boundary type \
being defended).",
));
}
match self.rationale.as_deref() {
None => {
return Err(syn::Error::new(
self.args_span,
"#[mucosal] requires `rationale = \"...\"` (≥20 characters; \
why this boundary is defended).",
));
}
Some(s) if s.len() < 20 => {
return Err(syn::Error::new(
self.rationale_span.unwrap_or(self.args_span),
format!(
"#[mucosal] `rationale` must be at least 20 characters \
(got {}); per ADR-027 boundary-defense discipline.",
s.len()
),
));
}
Some(_) => {}
}
Ok(())
}
}
#[derive(Debug)]
pub struct MucosalDelegateArgs {
pub boundary: Option<MacroMucosalKind>,
#[allow(dead_code)]
pub boundary_span: Option<Span>,
#[allow(dead_code)]
pub handled_by: Option<syn::Path>,
pub rationale: Option<String>,
pub rationale_span: Option<Span>,
pub args_span: Span,
}
impl Parse for MucosalDelegateArgs {
fn parse(input: ParseStream) -> syn::Result<Self> {
let args_span = input.span();
let mut boundary: Option<MacroMucosalKind> = None;
let mut boundary_span: Option<Span> = None;
let mut handled_by: Option<syn::Path> = None;
let mut rationale: Option<String> = None;
let mut rationale_span: Option<Span> = None;
while !input.is_empty() {
let key: Ident = input.parse()?;
input.parse::<Token![=]>()?;
match key.to_string().as_str() {
"boundary" => {
let expr: Expr = input.parse()?;
boundary_span = Some(input.span());
boundary = Some(parse_mucosal_kind_expr(&expr)?);
}
"handled_by" => {
handled_by = Some(input.parse()?);
}
"rationale" => {
let lit: LitStr = input.parse()?;
rationale_span = Some(lit.span());
rationale = Some(lit.value());
}
other => {
return Err(syn::Error::new(
key.span(),
format!(
"unknown #[mucosal_delegate] field `{other}`; expected \
one of: boundary, handled_by, rationale"
),
));
}
}
if !input.is_empty() {
input.parse::<Token![,]>()?;
}
}
Ok(Self {
boundary,
boundary_span,
handled_by,
rationale,
rationale_span,
args_span,
})
}
}
impl MucosalDelegateArgs {
pub fn validate(&self) -> syn::Result<()> {
if self.boundary.is_none() {
return Err(syn::Error::new(
self.args_span,
"#[mucosal_delegate] requires `boundary = MucosalKind::X` (the \
boundary kind being delegated).",
));
}
if self.handled_by.is_none() {
return Err(syn::Error::new(
self.args_span,
"#[mucosal_delegate] requires `handled_by = <path>` (the handler \
function that carries the matching `#[mucosal(kind = X)]`). Per \
ADR-027 Amendment 1 this is a path expression, not a string.",
));
}
match self.rationale.as_deref() {
None => {
return Err(syn::Error::new(
self.args_span,
"#[mucosal_delegate] requires `rationale = \"...\"` (≥20 chars).",
));
}
Some(s) if s.len() < 20 => {
return Err(syn::Error::new(
self.rationale_span.unwrap_or(self.args_span),
format!(
"#[mucosal_delegate] `rationale` must be at least 20 \
characters (got {}).",
s.len()
),
));
}
Some(_) => {}
}
Ok(())
}
}
#[derive(Debug)]
pub struct MucosalTolerantArgs {
pub kind: Option<MacroMucosalKind>,
#[allow(dead_code)]
pub kind_span: Option<Span>,
pub rationale: Option<String>,
pub rationale_span: Option<Span>,
pub accepts: Option<String>,
pub accepts_span: Option<Span>,
#[allow(dead_code)]
pub reviewed_by: Option<String>,
#[allow(dead_code)]
pub until: Option<String>,
pub args_span: Span,
}
impl Parse for MucosalTolerantArgs {
fn parse(input: ParseStream) -> syn::Result<Self> {
let args_span = input.span();
let mut kind: Option<MacroMucosalKind> = None;
let mut kind_span: Option<Span> = None;
let mut rationale: Option<String> = None;
let mut rationale_span: Option<Span> = None;
let mut accepts: Option<String> = None;
let mut accepts_span: Option<Span> = None;
let mut reviewed_by: Option<String> = None;
let mut until: Option<String> = None;
while !input.is_empty() {
let key: Ident = input.parse()?;
input.parse::<Token![=]>()?;
match key.to_string().as_str() {
"kind" => {
let expr: Expr = input.parse()?;
kind_span = Some(input.span());
kind = Some(parse_mucosal_kind_expr(&expr)?);
}
"rationale" => {
let lit: LitStr = input.parse()?;
rationale_span = Some(lit.span());
rationale = Some(lit.value());
}
"accepts" => {
let lit: LitStr = input.parse()?;
accepts_span = Some(lit.span());
accepts = Some(lit.value());
}
"reviewed_by" => {
let lit: LitStr = input.parse()?;
reviewed_by = Some(lit.value());
}
"until" => {
let lit: LitStr = input.parse()?;
until = Some(lit.value());
}
other => {
return Err(syn::Error::new(
key.span(),
format!(
"unknown #[mucosal_tolerant] field `{other}`; expected \
one of: kind, rationale, accepts, reviewed_by, until. \
(For failure-class-tier tolerance see \
`#[antigen_tolerance]`; this is the BOUNDARY-tier \
tolerance primitive.)"
),
));
}
}
if !input.is_empty() {
input.parse::<Token![,]>()?;
}
}
Ok(Self {
kind,
kind_span,
rationale,
rationale_span,
accepts,
accepts_span,
reviewed_by,
until,
args_span,
})
}
}
impl MucosalTolerantArgs {
pub fn validate(&self) -> syn::Result<()> {
if self.kind.is_none() {
return Err(syn::Error::new(
self.args_span,
"#[mucosal_tolerant] requires `kind = MucosalKind::X`. (For \
failure-class-tier tolerance see `#[antigen_tolerance]`; this \
is the BOUNDARY-tier tolerance primitive.)",
));
}
match self.rationale.as_deref() {
None => {
return Err(syn::Error::new(
self.args_span,
"#[mucosal_tolerant] requires `rationale = \"...\"` (≥40 \
characters — tolerance errors are silent/latent, so the \
up-front declaration carries a higher loudness floor than \
#[mucosal]'s ≥20 to compensate for the detection asymmetry).",
));
}
Some(s) if s.len() < 40 => {
return Err(syn::Error::new(
self.rationale_span.unwrap_or(self.args_span),
format!(
"#[mucosal_tolerant] `rationale` must be at least 40 \
characters (got {}); per ADR-027 Amendment 1 the floor \
is higher than #[mucosal]'s ≥20 because tolerance errors \
are silent/latent — no acute signal catches a bad \
tolerance decision.",
s.len()
),
));
}
Some(_) => {}
}
match self.accepts.as_deref() {
None => {
return Err(syn::Error::new(
self.args_span,
"#[mucosal_tolerant] requires `accepts = \"...\"` (non-empty; \
description of what the boundary accepts as legitimate input).",
));
}
Some(s) if s.trim().is_empty() => {
return Err(syn::Error::new(
self.accepts_span.unwrap_or(self.args_span),
"#[mucosal_tolerant] `accepts` cannot be empty. Audit emits \
mucosal-tolerant-accepts-empty.",
));
}
Some(_) => {}
}
Ok(())
}
}
fn parse_iso_date(s: &str) -> Result<chrono::NaiveDate, ()> {
chrono::NaiveDate::parse_from_str(s, "%Y-%m-%d").map_err(|_| ())
}
fn today_utc() -> chrono::NaiveDate {
chrono::Utc::now().date_naive()
}
struct MetaPair {
key: Ident,
value: Expr,
}
impl Parse for MetaPair {
fn parse(input: ParseStream) -> syn::Result<Self> {
let key: Ident = input.parse()?;
input.parse::<Token![=]>()?;
let value: Expr = input.parse()?;
Ok(Self { key, value })
}
}
impl MetaPair {
fn expect_string(&self) -> syn::Result<String> {
if let Expr::Lit(syn::ExprLit {
lit: Lit::Str(s), ..
}) = &self.value
{
Ok(s.value())
} else {
Err(syn::Error::new_spanned(
&self.value,
format!("expected a string literal for `{}`", self.key),
))
}
}
fn expect_string_spanned(&self) -> syn::Result<(String, Span)> {
if let Expr::Lit(syn::ExprLit {
lit: Lit::Str(s), ..
}) = &self.value
{
Ok((s.value(), s.span()))
} else {
Err(syn::Error::new_spanned(
&self.value,
format!("expected a string literal for `{}`", self.key),
))
}
}
fn expect_string_array(&self) -> syn::Result<Vec<String>> {
if let Expr::Array(arr) = &self.value {
let mut out = Vec::new();
for elem in &arr.elems {
if let Expr::Lit(syn::ExprLit {
lit: Lit::Str(s), ..
}) = elem
{
out.push(s.value());
} else {
return Err(syn::Error::new_spanned(
elem,
"expected a string literal in references array",
));
}
}
Ok(out)
} else {
Err(syn::Error::new_spanned(
&self.value,
format!("expected a string array for `{}`", self.key),
))
}
}
fn expect_antigen_category(&self) -> syn::Result<Vec<MacroAntigenCategory>> {
fn parse_single(expr: &Expr) -> syn::Result<MacroAntigenCategory> {
let s = match expr {
Expr::Path(p) => {
let segments: Vec<String> = p
.path
.segments
.iter()
.map(|seg| seg.ident.to_string())
.collect();
segments.join("::")
}
_ => {
return Err(syn::Error::new_spanned(
expr,
"expected `AntigenCategory::SubstrateAlignment` or \
`AntigenCategory::FunctionalCorrectness`",
));
}
};
MacroAntigenCategory::from_path_str(&s).ok_or_else(|| {
syn::Error::new_spanned(
expr,
format!(
"unknown AntigenCategory `{s}`; expected \
`AntigenCategory::SubstrateAlignment` or \
`AntigenCategory::FunctionalCorrectness`"
),
)
})
}
match &self.value {
Expr::Array(arr) => {
let mut out = Vec::new();
for elem in &arr.elems {
out.push(parse_single(elem)?);
}
if out.is_empty() {
return Err(syn::Error::new_spanned(
&self.value,
"`category` array must not be empty",
));
}
Ok(out)
}
single => Ok(vec![parse_single(single)?]),
}
}
fn expect_provenance(&self) -> syn::Result<MacroProvenance> {
let s = path_expr_to_string(&self.value).ok_or_else(|| {
syn::Error::new_spanned(
&self.value,
"expected a provenance path like `Provenance::Heuristic` \
(Encountered | Constructable | Heuristic | Imagined)",
)
})?;
MacroProvenance::from_path_str(&s).ok_or_else(|| {
syn::Error::new_spanned(
&self.value,
format!(
"unknown Provenance `{s}`; expected one of \
`Provenance::Encountered`, `Provenance::Constructable`, \
`Provenance::Heuristic`, `Provenance::Imagined`"
),
)
})
}
fn expect_presentation(&self) -> syn::Result<MacroPresentation> {
let s = path_expr_to_string(&self.value).ok_or_else(|| {
syn::Error::new_spanned(
&self.value,
"expected a presentation path like `Presentation::Passive` \
(Passive | Active)",
)
})?;
MacroPresentation::from_path_str(&s).ok_or_else(|| {
syn::Error::new_spanned(
&self.value,
format!(
"unknown Presentation `{s}`; expected \
`Presentation::Passive` or `Presentation::Active`"
),
)
})
}
}
fn path_expr_to_string(expr: &Expr) -> Option<String> {
match expr {
Expr::Path(p) => Some(
p.path
.segments
.iter()
.map(|seg| seg.ident.to_string())
.collect::<Vec<_>>()
.join("::"),
),
_ => None,
}
}
fn is_kebab_case(s: &str) -> bool {
!s.is_empty()
&& s.chars()
.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-')
&& !s.starts_with('-')
&& !s.ends_with('-')
&& !s.contains("--")
}
#[derive(Debug)]
pub struct PanelArgs {
pub needs: Vec<String>,
pub needs_present: bool,
#[allow(dead_code)]
pub filled_by: Vec<String>,
#[allow(dead_code)]
pub reviewed_by: Vec<String>,
#[allow(dead_code)]
pub ordered_by: Option<String>,
#[allow(dead_code)]
pub due: Option<String>,
pub args_span: Span,
}
impl Parse for PanelArgs {
fn parse(input: ParseStream) -> syn::Result<Self> {
let args_span = input.span();
let mut needs: Vec<String> = Vec::new();
let mut needs_present = false;
let mut filled_by: Vec<String> = Vec::new();
let mut reviewed_by: Vec<String> = Vec::new();
let mut ordered_by: Option<String> = None;
let mut due: Option<String> = None;
let pairs: Punctuated<MetaPair, Token![,]> =
input.parse_terminated(MetaPair::parse, Token![,])?;
for pair in pairs {
match pair.key.to_string().as_str() {
"needs" => {
needs = pair.expect_string_array()?;
needs_present = true;
}
"filled_by" => filled_by = pair.expect_string_array()?,
"reviewed_by" => reviewed_by = pair.expect_string_array()?,
"ordered_by" => ordered_by = Some(pair.expect_string()?),
"due" => due = Some(pair.expect_string()?),
other => {
return Err(syn::Error::new(
pair.key.span(),
format!(
"unknown #[panel] field `{other}`; expected one of: \
needs, filled_by, reviewed_by, ordered_by, due"
),
));
}
}
}
Ok(Self {
needs,
needs_present,
filled_by,
reviewed_by,
ordered_by,
due,
args_span,
})
}
}
impl PanelArgs {
pub fn validate(&self) -> syn::Result<()> {
if !self.needs_present {
return Err(syn::Error::new(
self.args_span,
"#[panel] requires `needs = [\"...\", ...]` (the battery's checklist).",
));
}
if self.needs.iter().all(|n| n.trim().is_empty()) {
return Err(syn::Error::new(
self.args_span,
"#[panel] `needs` must be non-empty — an empty checklist is a vacuous \
work-need (it would always read as satisfied).",
));
}
Ok(())
}
}
#[derive(Debug)]
pub struct RxArgs {
pub treatment: Option<String>,
pub treatment_span: Option<Span>,
#[allow(dead_code)]
pub diagnosis: Option<String>,
#[allow(dead_code)]
pub filled_by: Vec<String>,
#[allow(dead_code)]
pub reviewed_by: Vec<String>,
#[allow(dead_code)]
pub due: Option<String>,
pub args_span: Span,
}
impl Parse for RxArgs {
fn parse(input: ParseStream) -> syn::Result<Self> {
let args_span = input.span();
let mut treatment: Option<String> = None;
let mut treatment_span: Option<Span> = None;
let mut diagnosis: Option<String> = None;
let mut filled_by: Vec<String> = Vec::new();
let mut reviewed_by: Vec<String> = Vec::new();
let mut due: Option<String> = None;
let pairs: Punctuated<MetaPair, Token![,]> =
input.parse_terminated(MetaPair::parse, Token![,])?;
for pair in pairs {
match pair.key.to_string().as_str() {
"treatment" => {
let (s, span) = pair.expect_string_spanned()?;
treatment = Some(s);
treatment_span = Some(span);
}
"diagnosis" => diagnosis = Some(pair.expect_string()?),
"filled_by" => filled_by = pair.expect_string_array()?,
"reviewed_by" => reviewed_by = pair.expect_string_array()?,
"due" => due = Some(pair.expect_string()?),
other => {
return Err(syn::Error::new(
pair.key.span(),
format!(
"unknown #[rx] field `{other}`; expected one of: \
treatment, diagnosis, filled_by, reviewed_by, due"
),
));
}
}
}
Ok(Self {
treatment,
treatment_span,
diagnosis,
filled_by,
reviewed_by,
due,
args_span,
})
}
}
impl RxArgs {
pub fn validate(&self) -> syn::Result<()> {
match self.treatment.as_deref() {
None => Err(syn::Error::new(
self.args_span,
"#[rx] requires `treatment = \"...\"` (what must be done).",
)),
Some(s) if s.trim().is_empty() => Err(syn::Error::new(
self.treatment_span.unwrap_or(self.args_span),
"#[rx] `treatment` cannot be empty.",
)),
Some(_) => Ok(()),
}
}
}
#[derive(Debug)]
pub struct ReferArgs {
pub to: Option<String>,
pub to_span: Option<Span>,
#[allow(dead_code)]
pub response_due: Option<String>,
pub args_span: Span,
}
impl Parse for ReferArgs {
fn parse(input: ParseStream) -> syn::Result<Self> {
let args_span = input.span();
let mut to: Option<String> = None;
let mut to_span: Option<Span> = None;
let mut response_due: Option<String> = None;
let pairs: Punctuated<MetaPair, Token![,]> =
input.parse_terminated(MetaPair::parse, Token![,])?;
for pair in pairs {
match pair.key.to_string().as_str() {
"to" => {
let (s, span) = pair.expect_string_spanned()?;
to = Some(s);
to_span = Some(span);
}
"response_due" => response_due = Some(pair.expect_string()?),
other => {
return Err(syn::Error::new(
pair.key.span(),
format!(
"unknown #[refer] field `{other}`; expected one of: \
to, response_due"
),
));
}
}
}
Ok(Self {
to,
to_span,
response_due,
args_span,
})
}
}
impl ReferArgs {
pub fn validate(&self) -> syn::Result<()> {
match self.to.as_deref() {
None => Err(syn::Error::new(
self.args_span,
"#[refer] requires `to = \"who\"` (the external owner this work is referred to).",
)),
Some(s) if s.trim().is_empty() => Err(syn::Error::new(
self.to_span.unwrap_or(self.args_span),
"#[refer] `to` cannot be empty.",
)),
Some(_) => Ok(()),
}
}
}
#[derive(Debug)]
pub struct BiopsyArgs {
pub location: Option<String>,
pub location_span: Option<Span>,
pub request_text: Option<String>,
pub request_text_span: Option<Span>,
#[allow(dead_code)]
pub deep_investigation_by: Option<String>,
pub args_span: Span,
}
impl Parse for BiopsyArgs {
fn parse(input: ParseStream) -> syn::Result<Self> {
let args_span = input.span();
let mut location: Option<String> = None;
let mut location_span: Option<Span> = None;
let mut request_text: Option<String> = None;
let mut request_text_span: Option<Span> = None;
let mut deep_investigation_by: Option<String> = None;
let pairs: Punctuated<MetaPair, Token![,]> =
input.parse_terminated(MetaPair::parse, Token![,])?;
for pair in pairs {
match pair.key.to_string().as_str() {
"location" => {
let (s, span) = pair.expect_string_spanned()?;
location = Some(s);
location_span = Some(span);
}
"request_text" => {
let (s, span) = pair.expect_string_spanned()?;
request_text = Some(s);
request_text_span = Some(span);
}
"deep_investigation_by" => deep_investigation_by = Some(pair.expect_string()?),
other => {
return Err(syn::Error::new(
pair.key.span(),
format!(
"unknown #[biopsy] field `{other}`; expected one of: \
location, request_text, deep_investigation_by"
),
));
}
}
}
Ok(Self {
location,
location_span,
request_text,
request_text_span,
deep_investigation_by,
args_span,
})
}
}
impl BiopsyArgs {
pub fn validate(&self) -> syn::Result<()> {
match self.location.as_deref() {
None => {
return Err(syn::Error::new(
self.args_span,
"#[biopsy] requires `location = \"...\"` (the sub-site to investigate).",
));
}
Some(s) if s.trim().is_empty() => {
return Err(syn::Error::new(
self.location_span.unwrap_or(self.args_span),
"#[biopsy] `location` cannot be empty.",
));
}
Some(_) => {}
}
match self.request_text.as_deref() {
None => Err(syn::Error::new(
self.args_span,
"#[biopsy] requires `request_text = \"...\"` (what to investigate).",
)),
Some(s) if s.trim().is_empty() => Err(syn::Error::new(
self.request_text_span.unwrap_or(self.args_span),
"#[biopsy] `request_text` cannot be empty.",
)),
Some(_) => Ok(()),
}
}
}
#[derive(Debug)]
pub struct DdxArgs {
pub symptom: Option<String>,
pub symptom_span: Option<Span>,
pub rule_out: Vec<String>,
pub rule_out_present: bool,
#[allow(dead_code)]
pub investigator: Option<String>,
#[allow(dead_code)]
pub reviewer: Option<String>,
pub args_span: Span,
}
impl Parse for DdxArgs {
fn parse(input: ParseStream) -> syn::Result<Self> {
let args_span = input.span();
let mut symptom: Option<String> = None;
let mut symptom_span: Option<Span> = None;
let mut rule_out: Vec<String> = Vec::new();
let mut rule_out_present = false;
let mut investigator: Option<String> = None;
let mut reviewer: Option<String> = None;
let pairs: Punctuated<MetaPair, Token![,]> =
input.parse_terminated(MetaPair::parse, Token![,])?;
for pair in pairs {
match pair.key.to_string().as_str() {
"symptom" => {
let (s, span) = pair.expect_string_spanned()?;
symptom = Some(s);
symptom_span = Some(span);
}
"rule_out" => {
rule_out = pair.expect_string_array()?;
rule_out_present = true;
}
"investigator" => investigator = Some(pair.expect_string()?),
"reviewer" => reviewer = Some(pair.expect_string()?),
other => {
return Err(syn::Error::new(
pair.key.span(),
format!(
"unknown #[ddx] field `{other}`; expected one of: \
symptom, rule_out, investigator, reviewer"
),
));
}
}
}
Ok(Self {
symptom,
symptom_span,
rule_out,
rule_out_present,
investigator,
reviewer,
args_span,
})
}
}
impl DdxArgs {
pub fn validate(&self) -> syn::Result<()> {
match self.symptom.as_deref() {
None => {
return Err(syn::Error::new(
self.args_span,
"#[ddx] requires `symptom = \"...\"` (the observed problem).",
));
}
Some(s) if s.trim().is_empty() => {
return Err(syn::Error::new(
self.symptom_span.unwrap_or(self.args_span),
"#[ddx] `symptom` cannot be empty.",
));
}
Some(_) => {}
}
if !self.rule_out_present {
return Err(syn::Error::new(
self.args_span,
"#[ddx] requires `rule_out = [\"...\", ...]` (the alternatives to eliminate).",
));
}
if self.rule_out.iter().all(|r| r.trim().is_empty()) {
return Err(syn::Error::new(
self.args_span,
"#[ddx] `rule_out` must be non-empty — an empty alternative-set is no \
differential diagnosis.",
));
}
Ok(())
}
}
#[derive(Debug)]
pub struct CultureArgs {
pub test_kind: Option<String>,
pub test_kind_span: Option<Span>,
#[allow(dead_code)]
pub duration: Option<String>,
#[allow(dead_code)]
pub runs_until: Option<String>,
pub args_span: Span,
}
impl Parse for CultureArgs {
fn parse(input: ParseStream) -> syn::Result<Self> {
let args_span = input.span();
let mut test_kind: Option<String> = None;
let mut test_kind_span: Option<Span> = None;
let mut duration: Option<String> = None;
let mut runs_until: Option<String> = None;
let pairs: Punctuated<MetaPair, Token![,]> =
input.parse_terminated(MetaPair::parse, Token![,])?;
for pair in pairs {
match pair.key.to_string().as_str() {
"test_kind" => {
let (s, span) = pair.expect_string_spanned()?;
test_kind = Some(s);
test_kind_span = Some(span);
}
"duration" => duration = Some(pair.expect_string()?),
"runs_until" => runs_until = Some(pair.expect_string()?),
other => {
return Err(syn::Error::new(
pair.key.span(),
format!(
"unknown #[culture] field `{other}`; expected one of: \
test_kind, duration, runs_until"
),
));
}
}
}
Ok(Self {
test_kind,
test_kind_span,
duration,
runs_until,
args_span,
})
}
}
impl CultureArgs {
pub fn validate(&self) -> syn::Result<()> {
match self.test_kind.as_deref() {
None => Err(syn::Error::new(
self.args_span,
"#[culture] requires `test_kind = \"...\"` (what is being cultured/observed).",
)),
Some(s) if s.trim().is_empty() => Err(syn::Error::new(
self.test_kind_span.unwrap_or(self.args_span),
"#[culture] `test_kind` cannot be empty.",
)),
Some(_) => Ok(()),
}
}
}
#[derive(Debug)]
pub struct QuarantineArgs {
pub scope: Option<String>,
pub scope_span: Option<Span>,
#[allow(dead_code)]
pub until: Option<String>,
pub reason: Option<String>,
pub reason_span: Option<Span>,
pub args_span: Span,
}
impl Parse for QuarantineArgs {
fn parse(input: ParseStream) -> syn::Result<Self> {
let args_span = input.span();
let mut scope: Option<String> = None;
let mut scope_span: Option<Span> = None;
let mut until: Option<String> = None;
let mut reason: Option<String> = None;
let mut reason_span: Option<Span> = None;
let pairs: Punctuated<MetaPair, Token![,]> =
input.parse_terminated(MetaPair::parse, Token![,])?;
for pair in pairs {
match pair.key.to_string().as_str() {
"scope" => {
let (s, span) = pair.expect_string_spanned()?;
scope = Some(s);
scope_span = Some(span);
}
"until" => until = Some(pair.expect_string()?),
"reason" => {
let (s, span) = pair.expect_string_spanned()?;
reason = Some(s);
reason_span = Some(span);
}
other => {
return Err(syn::Error::new(
pair.key.span(),
format!(
"unknown #[quarantine] field `{other}`; expected one of: \
scope, until, reason"
),
));
}
}
}
Ok(Self {
scope,
scope_span,
until,
reason,
reason_span,
args_span,
})
}
}
impl QuarantineArgs {
pub fn validate(&self) -> syn::Result<()> {
match self.scope.as_deref() {
None => {
return Err(syn::Error::new(
self.args_span,
"#[quarantine] requires `scope = \"...\"` (the isolated region).",
));
}
Some(s) if s.trim().is_empty() => {
return Err(syn::Error::new(
self.scope_span.unwrap_or(self.args_span),
"#[quarantine] `scope` cannot be empty.",
));
}
Some(_) => {}
}
match self.reason.as_deref() {
None => Err(syn::Error::new(
self.args_span,
"#[quarantine] requires `reason = \"...\"` (why the hold; ADR-005 Amendment 2 \
rationale-as-required).",
)),
Some(s) if s.trim().is_empty() => Err(syn::Error::new(
self.reason_span.unwrap_or(self.args_span),
"#[quarantine] `reason` cannot be empty (ADR-005 Amendment 2).",
)),
Some(_) => Ok(()),
}
}
}
#[derive(Debug)]
pub struct TriageArgs {
pub priority_order: Vec<String>,
pub priority_order_present: bool,
#[allow(dead_code)]
pub triaged_by: Option<String>,
#[allow(dead_code)]
pub re_triage_due: Option<String>,
pub args_span: Span,
}
impl Parse for TriageArgs {
fn parse(input: ParseStream) -> syn::Result<Self> {
let args_span = input.span();
let mut priority_order: Vec<String> = Vec::new();
let mut priority_order_present = false;
let mut triaged_by: Option<String> = None;
let mut re_triage_due: Option<String> = None;
let pairs: Punctuated<MetaPair, Token![,]> =
input.parse_terminated(MetaPair::parse, Token![,])?;
for pair in pairs {
match pair.key.to_string().as_str() {
"priority_order" => {
priority_order = pair.expect_string_array()?;
priority_order_present = true;
}
"triaged_by" => triaged_by = Some(pair.expect_string()?),
"re_triage_due" => re_triage_due = Some(pair.expect_string()?),
other => {
return Err(syn::Error::new(
pair.key.span(),
format!(
"unknown #[triage] field `{other}`; expected one of: \
priority_order, triaged_by, re_triage_due (note: `campsites` was \
dropped — `priority_order` entries are code-site references)"
),
));
}
}
}
Ok(Self {
priority_order,
priority_order_present,
triaged_by,
re_triage_due,
args_span,
})
}
}
impl TriageArgs {
pub fn validate(&self) -> syn::Result<()> {
if !self.priority_order_present {
return Err(syn::Error::new(
self.args_span,
"#[triage] requires `priority_order = [\"...\", ...]` (code-site references in \
priority order).",
));
}
if self.priority_order.iter().all(|p| p.trim().is_empty()) {
return Err(syn::Error::new(
self.args_span,
"#[triage] `priority_order` must be non-empty — an empty ordering is a vacuous \
work-need.",
));
}
Ok(())
}
}
#[cfg(test)]
type AntigenFixture = (
&'static str,
&'static str,
&'static str,
Option<&'static str>,
Option<&'static str>,
);
#[cfg(test)]
const ANTIGEN_PARSER_FIXTURES: &[AntigenFixture] = &[
(
r#"name = "panicking-in-drop", fingerprint = "impl Drop with panic""#,
"panicking-in-drop",
"impl Drop with panic",
None,
None,
),
(
r#"name = "frame-translation", fingerprint = "class enum + meet", family = "semantic-drift", summary = "Polarity inverts at the frame boundary""#,
"frame-translation",
"class enum + meet",
Some("semantic-drift"),
Some("Polarity inverts at the frame boundary"),
),
(
r#"name = "x", fingerprint = "item: enum, has_method(\"meet\", \"(Self, Self) -> Self\")""#,
"x",
r#"item: enum, has_method("meet", "(Self, Self) -> Self")"#,
None,
None,
),
(
r#"summary = "S", family = "F", fingerprint = "FP", name = "n""#,
"n",
"FP",
Some("F"),
Some("S"),
),
(
r#"name = "x", fingerprint = "y", references = ["GAP-1", "DEC-2"]"#,
"x",
"y",
None,
None,
),
(
"name = \"multi-line\",\n\tfingerprint = \"shape\",\n\tfamily = \"family\"",
"multi-line",
"shape",
Some("family"),
None,
),
];
#[cfg(test)]
mod tests {
use super::*;
use proc_macro2::TokenStream;
#[test]
fn antigen_parser_accepts_all_fixtures() {
for (input, exp_name, exp_fp, exp_family, exp_summary) in ANTIGEN_PARSER_FIXTURES {
let tokens: TokenStream = input
.parse()
.unwrap_or_else(|e| panic!("fixture failed to tokenize: {input:?}: {e}"));
let args = syn::parse2::<AntigenArgs>(tokens)
.unwrap_or_else(|e| panic!("macro parser rejected fixture {input:?}: {e}"));
assert_eq!(&args.name, exp_name, "name mismatch for fixture: {input:?}");
assert_eq!(
args.fingerprint.as_deref(),
Some(*exp_fp),
"fingerprint mismatch for fixture: {input:?}"
);
assert_eq!(
args.family.as_deref(),
*exp_family,
"family mismatch for fixture: {input:?}"
);
assert_eq!(
args.summary.as_deref(),
*exp_summary,
"summary mismatch for fixture: {input:?}"
);
}
}
#[test]
fn antigen_parser_rejects_missing_name() {
let tokens: TokenStream = r#"fingerprint = "x""#.parse().unwrap();
assert!(syn::parse2::<AntigenArgs>(tokens).is_err());
}
#[test]
fn antigen_parser_accepts_missing_fingerprint_as_verify_only() {
let tokens: TokenStream = r#"name = "verify-only-class""#.parse().unwrap();
let args = syn::parse2::<AntigenArgs>(tokens)
.expect("a fingerprint-less #[antigen] is a valid verify-only declaration");
assert!(
args.fingerprint.is_none(),
"absent fingerprint must parse as None, not a placeholder"
);
assert!(
args.validate().is_ok(),
"validate() must accept a verify-only antigen (no fingerprint to check)"
);
}
#[test]
fn antigen_parser_rejects_unknown_field() {
let tokens: TokenStream = r#"name = "x", fingerprint = "y", bogus = "z""#.parse().unwrap();
match syn::parse2::<AntigenArgs>(tokens) {
Ok(_) => panic!("expected parse to reject unknown field `bogus`"),
Err(e) => {
let msg = e.to_string();
assert!(
msg.contains("unknown") && msg.contains("bogus"),
"expected unknown-field error mentioning `bogus`, got: {msg}"
);
}
}
}
fn args_with(name: &str, fingerprint: &str) -> AntigenArgs {
AntigenArgs {
name: name.to_string(),
fingerprint: Some(fingerprint.to_string()),
family: None,
summary: None,
references: Vec::new(),
category: Vec::new(),
provenance: None,
presentation: None,
name_span: Some(proc_macro2::Span::call_site()),
fingerprint_span: Some(proc_macro2::Span::call_site()),
args_span: proc_macro2::Span::call_site(),
}
}
const VALID_DSL: &str = r#"name = matches("*")"#;
#[test]
fn validate_rejects_empty_name() {
assert!(args_with("", VALID_DSL).validate().is_err());
}
#[test]
fn validate_rejects_non_kebab_name() {
assert!(args_with("FooBar", VALID_DSL).validate().is_err());
}
#[test]
fn validate_accepts_kebab_name_with_digits() {
assert!(args_with("frame-2-translation", VALID_DSL)
.validate()
.is_ok());
}
#[test]
fn validate_rejects_name_with_double_hyphen() {
assert!(args_with("frame--translation", VALID_DSL)
.validate()
.is_err());
}
#[test]
fn validate_rejects_name_starting_with_hyphen() {
assert!(args_with("-frame", VALID_DSL).validate().is_err());
}
#[test]
fn validate_rejects_malformed_dsl_fingerprint() {
let args = args_with("ok-name", "this is not the dsl");
let err = args.validate().unwrap_err().to_string();
assert!(err.contains("fingerprint"), "got: {err}");
}
#[test]
fn immune_parser_requires_witness() {
let tokens: TokenStream = r"PanickingInDrop".parse().unwrap();
let args = syn::parse2::<ImmuneArgs>(tokens).unwrap();
assert!(args.validate().is_err());
}
#[test]
fn immune_parser_accepts_witness_path() {
let tokens: TokenStream = r"PanickingInDrop, witness = my::module::test_fn"
.parse()
.unwrap();
let args = syn::parse2::<ImmuneArgs>(tokens).unwrap();
assert!(args.witness.is_some());
assert!(args.validate().is_ok());
}
#[test]
fn immune_parser_accepts_rationale() {
let tokens: TokenStream = r#"X, witness = my_test, rationale = "checked manually""#
.parse()
.unwrap();
let args = syn::parse2::<ImmuneArgs>(tokens).unwrap();
assert_eq!(args.rationale.as_deref(), Some("checked manually"));
}
#[test]
fn diagnostic_validate_rejects_unknown_witness_class() {
let tokens: TokenStream = r"modalities = [WitnessClass::Nonsense], min_independent = 1"
.parse()
.unwrap();
let args = syn::parse2::<DiagnosticArgs>(tokens).unwrap();
let err = args
.validate()
.expect_err("an unknown WitnessClass variant must be rejected")
.to_string();
assert!(
err.contains("Nonsense") && err.contains("WitnessClass"),
"error must name the offending ident + the valid set: {err}"
);
}
#[test]
fn diagnostic_validate_rejects_non_witness_class_path() {
let tokens: TokenStream = r"modalities = [Foo::Bar], min_independent = 1"
.parse()
.unwrap();
let args = syn::parse2::<DiagnosticArgs>(tokens).unwrap();
assert!(
args.validate().is_err(),
"a non-WitnessClass path must be rejected, not counted as a modality"
);
}
#[test]
fn diagnostic_validate_accepts_all_known_witness_classes() {
let tokens: TokenStream = "modalities = [WitnessClass::StaticAnalysis, \
WitnessClass::PropertyTest, WitnessClass::FormalVerification, \
WitnessClass::ManualReview, WitnessClass::RuntimeFuzz, \
WitnessClass::SubstrateWitness], min_independent = 6"
.parse()
.unwrap();
let args = syn::parse2::<DiagnosticArgs>(tokens).unwrap();
args.validate()
.expect("all 6 ratified WitnessClass variants must validate");
}
#[test]
fn requires_expr_depth_guard_rejects_excessive_nesting() {
const MAX_DEPTH: usize = 64;
let leaf = RequiresExpr::Leaf(LeafExpr::FreshWithinDays { days: 90 });
let mut pred = leaf;
for _ in 0..=MAX_DEPTH {
pred = RequiresExpr::Not(Box::new(pred));
}
let err = pred
.validate(proc_macro2::Span::call_site())
.expect_err("depth exceeding MAX_DEPTH must be rejected by validate()");
assert!(
err.to_string().contains("depth") || err.to_string().contains("nesting"),
"error must mention depth/nesting: {err}"
);
}
#[test]
fn requires_expr_depth_guard_accepts_at_max_depth() {
const MAX_DEPTH: usize = 64;
let leaf = RequiresExpr::Leaf(LeafExpr::FreshWithinDays { days: 90 });
let mut pred = leaf;
for _ in 0..MAX_DEPTH {
pred = RequiresExpr::Not(Box::new(pred));
}
assert!(
pred.validate(proc_macro2::Span::call_site()).is_ok(),
"predicate at exactly MAX_DEPTH must be accepted"
);
}
#[test]
fn antigen_parser_accepts_category_single_substrate_alignment() {
let tokens: TokenStream =
r#"name = "x", fingerprint = "item = fn", category = AntigenCategory::SubstrateAlignment"#
.parse()
.unwrap();
let args = syn::parse2::<AntigenArgs>(tokens).unwrap();
assert_eq!(
args.category,
vec![MacroAntigenCategory::SubstrateAlignment]
);
}
#[test]
fn antigen_parser_accepts_category_single_functional_correctness() {
let tokens: TokenStream =
r#"name = "x", fingerprint = "item = fn", category = AntigenCategory::FunctionalCorrectness"#
.parse()
.unwrap();
let args = syn::parse2::<AntigenArgs>(tokens).unwrap();
assert_eq!(
args.category,
vec![MacroAntigenCategory::FunctionalCorrectness]
);
}
#[test]
fn antigen_parser_accepts_category_hybrid_array() {
let tokens: TokenStream = r#"name = "x", fingerprint = "item = fn", category = [AntigenCategory::SubstrateAlignment, AntigenCategory::FunctionalCorrectness]"#
.parse()
.unwrap();
let args = syn::parse2::<AntigenArgs>(tokens).unwrap();
assert_eq!(
args.category,
vec![
MacroAntigenCategory::SubstrateAlignment,
MacroAntigenCategory::FunctionalCorrectness
]
);
}
#[test]
fn antigen_parser_accepts_bare_path_without_type_prefix() {
let tokens: TokenStream =
r#"name = "x", fingerprint = "item = fn", category = SubstrateAlignment"#
.parse()
.unwrap();
let args = syn::parse2::<AntigenArgs>(tokens).unwrap();
assert_eq!(
args.category,
vec![MacroAntigenCategory::SubstrateAlignment]
);
}
#[test]
fn antigen_parser_accepts_absent_category_for_compat() {
let tokens: TokenStream = r#"name = "x", fingerprint = "item = fn""#.parse().unwrap();
let args = syn::parse2::<AntigenArgs>(tokens).unwrap();
assert!(
args.category.is_empty(),
"absent category should yield empty vec"
);
}
#[test]
fn antigen_parser_rejects_unknown_category_variant() {
let tokens: TokenStream =
r#"name = "x", fingerprint = "item = fn", category = AntigenCategory::Unknown"#
.parse()
.unwrap();
let result = syn::parse2::<AntigenArgs>(tokens);
assert!(
result.is_err(),
"unknown category variant should be rejected"
);
}
#[test]
fn antigen_parser_rejects_empty_category_array() {
let tokens: TokenStream = r#"name = "x", fingerprint = "item = fn", category = []"#
.parse()
.unwrap();
let result = syn::parse2::<AntigenArgs>(tokens);
assert!(result.is_err(), "empty category array should be rejected");
}
#[test]
fn antigen_parser_accepts_provenance_qualified_path() {
let tokens: TokenStream =
r#"name = "x", fingerprint = "item = fn", provenance = Provenance::Heuristic"#
.parse()
.unwrap();
let args = syn::parse2::<AntigenArgs>(tokens).unwrap();
assert_eq!(args.provenance, Some(MacroProvenance::Heuristic));
}
#[test]
fn antigen_parser_accepts_provenance_bare_pascal() {
let tokens: TokenStream =
r#"name = "x", fingerprint = "item = fn", provenance = Constructable"#
.parse()
.unwrap();
let args = syn::parse2::<AntigenArgs>(tokens).unwrap();
assert_eq!(args.provenance, Some(MacroProvenance::Constructable));
}
#[test]
fn antigen_parser_accepts_all_four_provenance_tiers() {
for (src, want) in [
("Provenance::Encountered", MacroProvenance::Encountered),
("Provenance::Constructable", MacroProvenance::Constructable),
("Provenance::Heuristic", MacroProvenance::Heuristic),
("Provenance::Imagined", MacroProvenance::Imagined),
] {
let tokens: TokenStream =
format!(r#"name = "x", fingerprint = "item = fn", provenance = {src}"#)
.parse()
.unwrap();
let args = syn::parse2::<AntigenArgs>(tokens).unwrap();
assert_eq!(args.provenance, Some(want), "provenance {src}");
}
}
#[test]
fn antigen_parser_accepts_presentation_passive_and_active() {
for (src, want) in [
("Presentation::Passive", MacroPresentation::Passive),
("Active", MacroPresentation::Active),
] {
let tokens: TokenStream =
format!(r#"name = "x", fingerprint = "item = fn", presentation = {src}"#)
.parse()
.unwrap();
let args = syn::parse2::<AntigenArgs>(tokens).unwrap();
assert_eq!(args.presentation, Some(want), "presentation {src}");
}
}
#[test]
fn antigen_parser_absent_provenance_and_presentation_are_none() {
let tokens: TokenStream = r#"name = "x", fingerprint = "item = fn""#.parse().unwrap();
let args = syn::parse2::<AntigenArgs>(tokens).unwrap();
assert_eq!(args.provenance, None, "absent provenance is None");
assert_eq!(args.presentation, None, "absent presentation is None");
}
#[test]
fn antigen_parser_rejects_unknown_provenance_variant() {
let tokens: TokenStream =
r#"name = "x", fingerprint = "item = fn", provenance = Provenance::Bogus"#
.parse()
.unwrap();
let result = syn::parse2::<AntigenArgs>(tokens);
assert!(
result.is_err(),
"an unknown Provenance variant must be rejected (the verified ladder is closed)"
);
}
#[test]
fn antigen_parser_rejects_authored_tier_with_helpful_error() {
for key in ["tier", "confidence", "named", "suspected"] {
let tokens: TokenStream =
format!(r#"name = "x", fingerprint = "item = fn", {key} = Named"#)
.parse()
.unwrap();
let result = syn::parse2::<AntigenArgs>(tokens);
let err = result.expect_err("an authored tier field must be rejected");
let msg = err.to_string();
assert!(
msg.contains("provenance") && msg.contains("dial-derived"),
"the `{key}` rejection must point the author at provenance + name dial-derivation; got: {msg}"
);
}
}
#[test]
fn macro_provenance_and_presentation_str_forms() {
assert_eq!(MacroProvenance::Encountered.as_str(), "encountered");
assert_eq!(MacroProvenance::Constructable.as_str(), "constructable");
assert_eq!(MacroProvenance::Heuristic.as_str(), "heuristic");
assert_eq!(MacroProvenance::Imagined.as_str(), "imagined");
assert_eq!(MacroPresentation::Passive.as_str(), "passive");
assert_eq!(MacroPresentation::Active.as_str(), "active");
assert_eq!(
MacroProvenance::from_path_str("Heuristic"),
Some(MacroProvenance::Heuristic)
);
assert_eq!(
MacroProvenance::from_path_str("Provenance::Heuristic"),
Some(MacroProvenance::Heuristic)
);
assert_eq!(MacroProvenance::from_path_str("heuristic"), None);
}
fn marker(src: &str) -> syn::Result<MarkerArgs> {
let tokens: TokenStream = src.parse().unwrap();
syn::parse2::<MarkerArgs>(tokens)
}
#[test]
fn marker_accepts_a_stated_trigger() {
let m = marker(r#"trigger = "the teardown drops the guard before the flush""#)
.expect("a marker with a stated trigger parses")
.with_marker("dread");
m.validate().expect("a stated non-empty trigger validates");
assert_eq!(
m.trigger_str(),
"the teardown drops the guard before the flush"
);
}
#[test]
fn marker_rejects_missing_trigger() {
let m = marker("").expect("empty args parse").with_marker("dread");
let err = m
.validate()
.expect_err("a triggerless #[dread] must be rejected (guard 3)");
let msg = err.to_string();
assert!(
msg.contains("dread") && msg.contains("trigger"),
"the rejection must name the marker + the required trigger; got: {msg}"
);
}
#[test]
fn marker_rejects_empty_trigger() {
for src in [r#"trigger = """#, r#"trigger = " ""#] {
let m = marker(src).expect("parses").with_marker("aura");
assert!(
m.validate().is_err(),
"an empty/whitespace trigger must be rejected: {src}"
);
}
}
#[test]
fn marker_rejects_authored_plane_corner() {
for key in ["magnitude", "existence_certainty", "certainty"] {
let result = marker(&format!(r#"{key} = "high", trigger = "x""#));
let err = result.expect_err("an authored plane-corner field must be rejected");
assert!(
err.to_string().contains("FIXED"),
"the `{key}` rejection must explain the corner is marker-fixed"
);
}
}
#[test]
fn marker_rejects_unknown_field() {
let result = marker(r#"trigger = "x", bogus = "y""#);
let err = result.expect_err("an unknown marker field must be rejected");
assert!(err.to_string().contains("trigger"));
}
#[test]
fn marker_takes_no_positional_antigen() {
let result = marker(r#"SomeAntigen, trigger = "x""#);
assert!(
result.is_err(),
"a marked-unknown must not accept a positional antigen (it names nothing)"
);
}
#[test]
fn macro_antigen_category_as_str() {
assert_eq!(
MacroAntigenCategory::SubstrateAlignment.as_str(),
"substrate-alignment"
);
assert_eq!(
MacroAntigenCategory::FunctionalCorrectness.as_str(),
"functional-correctness"
);
}
#[test]
fn macro_antigen_category_from_path_str_accepts_path_forms() {
for s in ["SubstrateAlignment", "AntigenCategory::SubstrateAlignment"] {
assert_eq!(
MacroAntigenCategory::from_path_str(s),
Some(MacroAntigenCategory::SubstrateAlignment),
"expected SubstrateAlignment from {s:?}"
);
}
for s in [
"FunctionalCorrectness",
"AntigenCategory::FunctionalCorrectness",
] {
assert_eq!(
MacroAntigenCategory::from_path_str(s),
Some(MacroAntigenCategory::FunctionalCorrectness),
"expected FunctionalCorrectness from {s:?}"
);
}
for s in [
"substrate-alignment",
"substrate_alignment",
"functional-correctness",
"functional_correctness",
] {
assert_eq!(
MacroAntigenCategory::from_path_str(s),
None,
"expected None (not a valid macro-input path token) for {s:?}"
);
}
}
#[test]
fn antigen_parser_duplicate_category_in_array_is_rejected() {
let tokens: TokenStream = r#"name = "x", fingerprint = "item = fn", category = [AntigenCategory::SubstrateAlignment, AntigenCategory::SubstrateAlignment]"#
.parse()
.unwrap();
let args = syn::parse2::<AntigenArgs>(tokens).unwrap();
let err = args.validate().unwrap_err().to_string();
assert!(
err.contains("duplicate") && err.contains("category"),
"validate() must reject duplicate category entries; got: {err:?}"
);
}
#[test]
fn antigen_parser_three_element_category_array_with_duplicate_is_rejected() {
let tokens: TokenStream = r#"name = "x", fingerprint = "item = fn", category = [AntigenCategory::SubstrateAlignment, AntigenCategory::FunctionalCorrectness, AntigenCategory::SubstrateAlignment]"#
.parse()
.unwrap();
let args = syn::parse2::<AntigenArgs>(tokens).unwrap();
let err = args.validate().unwrap_err().to_string();
assert!(
err.contains("duplicate") && err.contains("category"),
"validate() must reject duplicate category entries; got: {err:?}"
);
}
#[test]
fn antigen_parser_rejects_string_literal_as_category() {
let tokens: TokenStream =
r#"name = "x", fingerprint = "item = fn", category = "substrate-alignment""#
.parse()
.unwrap();
let result = syn::parse2::<AntigenArgs>(tokens);
assert!(
result.is_err(),
"string literal category should be rejected; got Ok"
);
}
#[test]
fn antigen_parser_rejects_integer_as_category() {
let tokens: TokenStream = r#"name = "x", fingerprint = "item = fn", category = 42"#
.parse()
.unwrap();
let result = syn::parse2::<AntigenArgs>(tokens);
assert!(
result.is_err(),
"integer category should be rejected; got Ok"
);
}
fn valid_triage_commit_tokens() -> TokenStream {
r#"triage_decision = TriageDecision::Red,
rollback_target = "abc1234",
triaged_by = "navigator",
rationale = "vital metric regression confirmed via #84; rolling back",
rollback_due_within_minutes = 30"#
.parse()
.unwrap()
}
#[test]
fn triage_commit_parser_accepts_canonical_form() {
let args = syn::parse2::<TriageCommitArgs>(valid_triage_commit_tokens()).unwrap();
assert_eq!(args.triage_decision, Some(MacroTriageDecision::Red));
assert_eq!(args.rollback_target.as_deref(), Some("abc1234"));
assert_eq!(args.triaged_by.as_deref(), Some("navigator"));
assert_eq!(args.rollback_due_within_minutes, Some(30));
args.validate().unwrap();
}
#[test]
fn triage_commit_parser_accepts_bare_variant_ident() {
let tokens: TokenStream = r#"triage_decision = Black,
rollback_target = "deadbeef",
triaged_by = "oncall",
rationale = "system-down: payment processor pod crashlooping",
rollback_due_within_minutes = 5"#
.parse()
.unwrap();
let args = syn::parse2::<TriageCommitArgs>(tokens).unwrap();
assert_eq!(args.triage_decision, Some(MacroTriageDecision::Black));
args.validate().unwrap();
}
#[test]
fn triage_commit_parser_accepts_all_five_variants() {
for (variant_text, expected) in [
("TriageDecision::Black", MacroTriageDecision::Black),
("TriageDecision::Red", MacroTriageDecision::Red),
("TriageDecision::Yellow", MacroTriageDecision::Yellow),
("TriageDecision::Green", MacroTriageDecision::Green),
("TriageDecision::White", MacroTriageDecision::White),
] {
let src = format!(
r#"triage_decision = {variant_text},
rollback_target = "abc1234",
triaged_by = "navigator",
rationale = "twenty-character-rationale-text-here",
rollback_due_within_minutes = 30"#
);
let tokens: TokenStream = src.parse().unwrap();
let args = syn::parse2::<TriageCommitArgs>(tokens).unwrap();
assert_eq!(args.triage_decision, Some(expected));
args.validate().unwrap();
}
}
#[test]
fn triage_commit_parser_rejects_unknown_variant() {
let tokens: TokenStream = r#"triage_decision = TriageDecision::Purple,
rollback_target = "abc1234",
triaged_by = "navigator",
rationale = "twenty-character-rationale-text-here",
rollback_due_within_minutes = 30"#
.parse()
.unwrap();
let result = syn::parse2::<TriageCommitArgs>(tokens);
assert!(result.is_err(), "unknown variant should be rejected");
}
#[test]
fn triage_commit_parser_rejects_string_literal_triage_decision() {
let tokens: TokenStream = r#"triage_decision = "red",
rollback_target = "abc1234",
triaged_by = "navigator",
rationale = "twenty-character-rationale-text-here",
rollback_due_within_minutes = 30"#
.parse()
.unwrap();
let result = syn::parse2::<TriageCommitArgs>(tokens);
assert!(
result.is_err(),
"string literal triage_decision should be rejected"
);
}
#[test]
fn triage_commit_validate_rejects_missing_triage_decision() {
let tokens: TokenStream = r#"rollback_target = "abc1234",
triaged_by = "navigator",
rationale = "twenty-character-rationale-text-here",
rollback_due_within_minutes = 30"#
.parse()
.unwrap();
let args = syn::parse2::<TriageCommitArgs>(tokens).unwrap();
assert!(args.validate().is_err());
}
#[test]
fn triage_commit_validate_rejects_empty_rollback_target() {
let tokens: TokenStream = r#"triage_decision = TriageDecision::Red,
rollback_target = "",
triaged_by = "navigator",
rationale = "twenty-character-rationale-text-here",
rollback_due_within_minutes = 30"#
.parse()
.unwrap();
let args = syn::parse2::<TriageCommitArgs>(tokens).unwrap();
assert!(args.validate().is_err());
}
#[test]
fn triage_commit_validate_rejects_empty_triaged_by() {
let tokens: TokenStream = r#"triage_decision = TriageDecision::Red,
rollback_target = "abc1234",
triaged_by = "",
rationale = "twenty-character-rationale-text-here",
rollback_due_within_minutes = 30"#
.parse()
.unwrap();
let args = syn::parse2::<TriageCommitArgs>(tokens).unwrap();
assert!(args.validate().is_err());
}
#[test]
fn triage_commit_validate_rejects_short_rationale() {
let tokens: TokenStream = r#"triage_decision = TriageDecision::Red,
rollback_target = "abc1234",
triaged_by = "navigator",
rationale = "too short",
rollback_due_within_minutes = 30"#
.parse()
.unwrap();
let args = syn::parse2::<TriageCommitArgs>(tokens).unwrap();
assert!(args.validate().is_err());
}
#[test]
fn triage_commit_validate_rejects_zero_minutes() {
let tokens: TokenStream = r#"triage_decision = TriageDecision::Red,
rollback_target = "abc1234",
triaged_by = "navigator",
rationale = "twenty-character-rationale-text-here",
rollback_due_within_minutes = 0"#
.parse()
.unwrap();
let args = syn::parse2::<TriageCommitArgs>(tokens).unwrap();
assert!(args.validate().is_err());
}
#[test]
fn triage_commit_parser_rejects_unknown_field() {
let tokens: TokenStream = r#"triage_decision = TriageDecision::Red,
rollback_target = "abc1234",
triaged_by = "navigator",
rationale = "twenty-character-rationale-text-here",
rollback_due_within_minutes = 30,
bogus = "value""#
.parse()
.unwrap();
let result = syn::parse2::<TriageCommitArgs>(tokens);
assert!(result.is_err(), "unknown field should be rejected");
}
#[test]
fn macro_triage_decision_as_str_roundtrip() {
for (variant, expected) in [
(MacroTriageDecision::Black, "black"),
(MacroTriageDecision::Red, "red"),
(MacroTriageDecision::Yellow, "yellow"),
(MacroTriageDecision::Green, "green"),
(MacroTriageDecision::White, "white"),
] {
assert_eq!(variant.as_str(), expected);
}
}
#[test]
fn triage_commit_validate_rationale_exactly_20_chars_passes() {
let tokens: TokenStream = r#"triage_decision = TriageDecision::Red,
rollback_target = "abc1234",
triaged_by = "navigator",
rationale = "20characterrationale",
rollback_due_within_minutes = 30"#
.parse()
.unwrap();
let args = syn::parse2::<TriageCommitArgs>(tokens).unwrap();
assert_eq!(
args.rationale.as_deref().map(str::len),
Some(20),
"fixture must be exactly 20 chars"
);
assert!(
args.validate().is_ok(),
"exactly 20 chars should pass rationale validation"
);
}
#[test]
fn triage_commit_validate_rationale_19_chars_fails() {
let tokens: TokenStream = r#"triage_decision = TriageDecision::Red,
rollback_target = "abc1234",
triaged_by = "navigator",
rationale = "19characterrational",
rollback_due_within_minutes = 30"#
.parse()
.unwrap();
let args = syn::parse2::<TriageCommitArgs>(tokens).unwrap();
assert_eq!(
args.rationale.as_deref().map(str::len),
Some(19),
"fixture must be exactly 19 chars"
);
assert!(
args.validate().is_err(),
"19-char rationale should fail validation"
);
}
#[test]
fn triage_commit_validate_absurdly_large_deadline_is_currently_accepted() {
let tokens: TokenStream = r#"triage_decision = TriageDecision::Yellow,
rollback_target = "abc1234",
triaged_by = "navigator",
rationale = "twenty-character-rationale-text",
rollback_due_within_minutes = 4294967295"#
.parse()
.unwrap();
let args = syn::parse2::<TriageCommitArgs>(tokens).unwrap();
assert_eq!(args.rollback_due_within_minutes, Some(4_294_967_295));
assert!(
args.validate().is_ok(),
"u32::MAX deadline currently accepted (no upper cap); this pins that behavior"
);
}
#[test]
fn triage_commit_validate_whitespace_only_rollback_target_is_rejected() {
let tokens: TokenStream = r#"triage_decision = TriageDecision::Red,
rollback_target = " ",
triaged_by = "navigator",
rationale = "twenty-character-rationale-text",
rollback_due_within_minutes = 30"#
.parse()
.unwrap();
let args = syn::parse2::<TriageCommitArgs>(tokens).unwrap();
assert_eq!(args.rollback_target.as_deref(), Some(" "));
let err = args.validate().unwrap_err();
assert!(
err.to_string().contains("rollback_target"),
"error should mention rollback_target field"
);
}
#[test]
fn triage_commit_validate_whitespace_only_rationale_of_20_chars_should_fail() {
let tokens: TokenStream = r#"triage_decision = TriageDecision::Red,
rollback_target = "abc1234",
triaged_by = "navigator",
rationale = " ",
rollback_due_within_minutes = 30"#
.parse()
.unwrap();
let args = syn::parse2::<TriageCommitArgs>(tokens).unwrap();
assert_eq!(
args.rationale.as_deref().map(str::len),
Some(20),
"fixture must be exactly 20 spaces"
);
assert!(
args.validate().is_err(),
"ATK-TRIAGE-WHITESPACE-RATIONALE: rationale of 20 spaces must fail validation. \
The 20-char length check (s.len() < 20) passes for ' ' because \
len==20. But trim().is_empty() reveals it contains no meaningful content. \
A blank rationale bypasses chart-documentation discipline. \
Fix: after the len < 20 check, add trim().is_empty() check — same pattern as \
rollback_target's whitespace guard."
);
}
#[test]
fn triage_commit_validate_non_sha_rollback_target_is_currently_accepted() {
let tokens: TokenStream = r#"triage_decision = TriageDecision::Red,
rollback_target = "not-a-sha-at-all-just-text",
triaged_by = "navigator",
rationale = "twenty-character-rationale-text",
rollback_due_within_minutes = 30"#
.parse()
.unwrap();
let args = syn::parse2::<TriageCommitArgs>(tokens).unwrap();
assert!(
args.validate().is_ok(),
"non-SHA rollback_target currently accepted; pins that no format validation exists"
);
}
#[test]
fn triage_commit_validate_green_triage_with_tight_deadline_is_accepted() {
let tokens: TokenStream = r#"triage_decision = TriageDecision::Green,
rollback_target = "abc1234",
triaged_by = "navigator",
rationale = "no regression detected in twenty chars",
rollback_due_within_minutes = 1"#
.parse()
.unwrap();
let args = syn::parse2::<TriageCommitArgs>(tokens).unwrap();
assert!(
args.validate().is_ok(),
"Green triage with tight deadline currently accepted; no semantic inconsistency check"
);
}
fn orient_until_within_horizon() -> String {
(today_utc() + chrono::Duration::days(90))
.format("%Y-%m-%d")
.to_string()
}
#[test]
fn orient_parser_bare_form_is_a_validate_error() {
let tokens: TokenStream = "".parse().unwrap();
let args = syn::parse2::<OrientArgs>(tokens).unwrap();
assert!(
args.validate().is_err(),
"bare #[orient] must fail validation (learning_path + until required)"
);
}
#[test]
fn orient_parser_accepts_canonical_adr023_form() {
let until = orient_until_within_horizon();
let tokens: TokenStream = format!(
r#"PanickingInDrop,
learning_path = "Audit Drop impls before alpha tag",
until = "{until}""#
)
.parse()
.unwrap();
let args = syn::parse2::<OrientArgs>(tokens).unwrap();
assert!(args.antigen.is_some());
assert_eq!(
args.learning_path.as_deref(),
Some("Audit Drop impls before alpha tag")
);
assert_eq!(args.until.as_deref(), Some(until.as_str()));
args.validate().unwrap();
}
#[test]
fn orient_parser_requires_learning_path() {
let until = orient_until_within_horizon();
let tokens: TokenStream = format!(r#"until = "{until}""#).parse().unwrap();
let args = syn::parse2::<OrientArgs>(tokens).unwrap();
assert!(
args.validate().is_err(),
"until without learning_path must fail validation"
);
}
#[test]
fn orient_parser_requires_until() {
let tokens: TokenStream = r#"learning_path = "Audit Drop impls before the alpha tag""#
.parse()
.unwrap();
let args = syn::parse2::<OrientArgs>(tokens).unwrap();
assert!(
args.validate().is_err(),
"learning_path without until must fail validation"
);
}
#[test]
fn orient_parser_learning_path_min_20_chars() {
let until = orient_until_within_horizon();
let tokens: TokenStream = format!(r#"learning_path = "too short", until = "{until}""#)
.parse()
.unwrap();
let args = syn::parse2::<OrientArgs>(tokens).unwrap();
assert!(
args.validate().is_err(),
"learning_path under 20 chars must fail (rationale-class minimum)"
);
}
#[test]
fn orient_parser_rejects_until_beyond_horizon() {
let far = (today_utc() + chrono::Duration::days(400))
.format("%Y-%m-%d")
.to_string();
let tokens: TokenStream =
format!(r#"learning_path = "Audit Drop impls before the alpha tag", until = "{far}""#)
.parse()
.unwrap();
let args = syn::parse2::<OrientArgs>(tokens).unwrap();
assert!(
args.validate().is_err(),
"until beyond the 180d orientation horizon must fail validation"
);
}
#[test]
fn orient_parser_rejects_until_in_the_past() {
let yesterday = (today_utc() - chrono::Duration::days(1))
.format("%Y-%m-%d")
.to_string();
let tokens: TokenStream = format!(
r#"learning_path = "Audit Drop impls before the alpha tag", until = "{yesterday}""#
)
.parse()
.unwrap();
let args = syn::parse2::<OrientArgs>(tokens).unwrap();
assert!(
args.validate().is_err(),
"ATK-ORIENT-PAST-DATE: until in the past must fail validation — \
an already-expired orientation period represents hidden unresolved \
deferred non-immunity with no accountability; got Ok(()) for until={yesterday}"
);
}
#[test]
fn orient_parser_drift_fields_are_parse_errors() {
for drift in [
r#"see = ["ADR-023"]"#,
r#"adr = "ADR-023""#,
"attestation_optional = true",
] {
let tokens: TokenStream = drift.parse().unwrap();
assert!(
syn::parse2::<OrientArgs>(tokens).is_err(),
"drift-form field must be a parse error: {drift}"
);
}
}
#[test]
fn orient_parser_rejects_unknown_field() {
let tokens: TokenStream = r#"bogus = "value""#.parse().unwrap();
let result = syn::parse2::<OrientArgs>(tokens);
assert!(result.is_err(), "unknown field should still be rejected");
}
#[test]
fn itch_parser_accepts_minimal() {
let tokens: TokenStream =
r#"name = "drop-panic-rhyme", description = "noticed Drop panics rhyming with unwrap-in-cleanup""#
.parse()
.unwrap();
let args = syn::parse2::<ItchArgs>(tokens).unwrap();
assert_eq!(args.name.as_deref(), Some("drop-panic-rhyme"));
args.validate().unwrap();
}
#[test]
fn itch_validate_rejects_missing_name() {
let tokens: TokenStream = r#"description = "noticed something worth ten chars""#
.parse()
.unwrap();
let args = syn::parse2::<ItchArgs>(tokens).unwrap();
assert!(args.validate().is_err());
}
#[test]
fn itch_validate_rejects_short_description() {
let tokens: TokenStream = r#"name = "x", description = "short""#.parse().unwrap();
let args = syn::parse2::<ItchArgs>(tokens).unwrap();
assert!(args.validate().is_err());
}
#[test]
fn itch_parser_rejects_unknown_field() {
let tokens: TokenStream = r#"name = "x", description = "long enough text", bogus = 1"#
.parse()
.unwrap();
assert!(syn::parse2::<ItchArgs>(tokens).is_err());
}
#[test]
fn recurrence_anchor_parser_accepts_canonical() {
let tokens: TokenStream = r#"MsrvCreep, instances = 3, since = "v0.1.0", rationale = "MSRV crept three times across major bumps""#
.parse()
.unwrap();
let args = syn::parse2::<RecurrenceAnchorArgs>(tokens).unwrap();
assert!(args.antigen.is_some());
assert_eq!(args.instances, Some(3));
args.validate().unwrap();
}
#[test]
fn recurrence_anchor_validate_rejects_zero_instances() {
let tokens: TokenStream =
r#"X, instances = 0, since = "v1", rationale = "twenty character rationale here""#
.parse()
.unwrap();
let args = syn::parse2::<RecurrenceAnchorArgs>(tokens).unwrap();
assert!(args.validate().is_err());
}
#[test]
fn recurrence_anchor_validate_rejects_missing_antigen() {
let tokens: TokenStream =
r#"instances = 2, since = "v1", rationale = "twenty character rationale here""#
.parse()
.unwrap();
let args = syn::parse2::<RecurrenceAnchorArgs>(tokens).unwrap();
assert!(args.validate().is_err());
}
#[test]
fn recurrence_anchor_validate_rejects_short_rationale() {
let tokens: TokenStream = r#"X, instances = 2, since = "v1", rationale = "too short""#
.parse()
.unwrap();
let args = syn::parse2::<RecurrenceAnchorArgs>(tokens).unwrap();
assert!(args.validate().is_err());
}
#[test]
fn crystallize_parser_accepts_with_from_itches() {
let tokens: TokenStream = r#"name = "drop-panic", from_itches = [DropPanicItch, CleanupUnwrapItch], summary = "crystallized from two itches""#
.parse()
.unwrap();
let args = syn::parse2::<CrystallizeArgs>(tokens).unwrap();
assert_eq!(args.from_itches.len(), 2);
args.validate().unwrap();
}
#[test]
fn crystallize_validate_rejects_missing_summary() {
let tokens: TokenStream = r#"name = "x""#.parse().unwrap();
let args = syn::parse2::<CrystallizeArgs>(tokens).unwrap();
assert!(args.validate().is_err());
}
#[test]
fn chronic_parser_accepts_canonical() {
let tokens: TokenStream = r#"FlakeyCiStep, since = "v0.2.0", managed_by = "ci-team""#
.parse()
.unwrap();
let args = syn::parse2::<ChronicArgs>(tokens).unwrap();
assert!(args.antigen.is_some());
args.validate().unwrap();
}
#[test]
fn chronic_validate_rejects_missing_since() {
let tokens: TokenStream = "X".parse().unwrap();
let args = syn::parse2::<ChronicArgs>(tokens).unwrap();
assert!(args.validate().is_err());
}
#[test]
fn chronic_validate_rejects_missing_antigen() {
let tokens: TokenStream = r#"since = "v1""#.parse().unwrap();
let args = syn::parse2::<ChronicArgs>(tokens).unwrap();
assert!(args.validate().is_err());
}
#[test]
fn saturate_parser_accepts_minimal() {
let tokens: TokenStream =
r#"description = "evidence accumulating toward MSRV-creep anchor""#
.parse()
.unwrap();
let args = syn::parse2::<SaturateArgs>(tokens).unwrap();
args.validate().unwrap();
}
#[test]
fn saturate_validate_rejects_short_description() {
let tokens: TokenStream = r#"description = "short""#.parse().unwrap();
let args = syn::parse2::<SaturateArgs>(tokens).unwrap();
assert!(args.validate().is_err());
}
#[test]
fn strand_parser_accepts_with_anchored_by() {
let tokens: TokenStream = r#"name = "vcs-loss-thread", anchored_by = [ForcePushItch, SquashMergeItch], description = "thread of history-loss noticings""#
.parse()
.unwrap();
let args = syn::parse2::<StrandArgs>(tokens).unwrap();
assert_eq!(args.anchored_by.len(), 2);
args.validate().unwrap();
}
#[test]
fn strand_validate_rejects_missing_name() {
let tokens: TokenStream = r#"description = "thread of related noticings here""#
.parse()
.unwrap();
let args = syn::parse2::<StrandArgs>(tokens).unwrap();
assert!(args.validate().is_err());
}
#[test]
fn mucosal_parser_accepts_canonical() {
let tokens: TokenStream =
r#"kind = MucosalKind::UserInput, rationale = "public comment form; XSS sanitized at render""#
.parse()
.unwrap();
let args = syn::parse2::<MucosalArgs>(tokens).unwrap();
assert_eq!(args.kind, Some(MacroMucosalKind::UserInput));
args.validate().unwrap();
}
#[test]
fn mucosal_parser_accepts_bare_variant() {
let tokens: TokenStream =
r#"kind = DatabaseQuery, rationale = "parameterized queries only at this layer""#
.parse()
.unwrap();
let args = syn::parse2::<MucosalArgs>(tokens).unwrap();
assert_eq!(args.kind, Some(MacroMucosalKind::DatabaseQuery));
args.validate().unwrap();
}
#[test]
fn mucosal_validate_rejects_missing_kind() {
let tokens: TokenStream = r#"rationale = "twenty character rationale here padding""#
.parse()
.unwrap();
let args = syn::parse2::<MucosalArgs>(tokens).unwrap();
assert!(args.validate().is_err());
}
#[test]
fn mucosal_validate_rejects_short_rationale() {
let tokens: TokenStream = r#"kind = MucosalKind::UserInput, rationale = "short""#
.parse()
.unwrap();
let args = syn::parse2::<MucosalArgs>(tokens).unwrap();
assert!(args.validate().is_err());
}
#[test]
fn mucosal_parser_rejects_removed_pr_boundary_variant() {
let tokens: TokenStream =
r#"kind = MucosalKind::PrBoundary, rationale = "twenty character rationale text""#
.parse()
.unwrap();
assert!(syn::parse2::<MucosalArgs>(tokens).is_err());
}
#[test]
fn mucosal_parser_rejects_string_literal_kind() {
let tokens: TokenStream =
r#"kind = "user-input", rationale = "twenty character rationale text""#
.parse()
.unwrap();
assert!(syn::parse2::<MucosalArgs>(tokens).is_err());
}
#[test]
fn mucosal_delegate_parser_accepts_canonical() {
let tokens: TokenStream = r#"boundary = MucosalKind::UserInput, handled_by = crate::sanitize::user_input, rationale = "delegated to central sanitizer module""#
.parse()
.unwrap();
let args = syn::parse2::<MucosalDelegateArgs>(tokens).unwrap();
assert_eq!(args.boundary, Some(MacroMucosalKind::UserInput));
assert!(args.handled_by.is_some());
args.validate().unwrap();
}
#[test]
fn mucosal_delegate_validate_rejects_missing_handled_by() {
let tokens: TokenStream =
r#"boundary = MucosalKind::UserInput, rationale = "twenty character rationale text""#
.parse()
.unwrap();
let args = syn::parse2::<MucosalDelegateArgs>(tokens).unwrap();
assert!(args.validate().is_err());
}
#[test]
fn mucosal_delegate_rejects_string_handled_by() {
let tokens: TokenStream = r#"boundary = MucosalKind::UserInput, handled_by = "sanitize_fn", rationale = "twenty character rationale text""#
.parse()
.unwrap();
assert!(
syn::parse2::<MucosalDelegateArgs>(tokens).is_err(),
"string-literal handled_by must be rejected (path expression required)"
);
}
#[test]
fn mucosal_tolerant_parser_accepts_canonical() {
let tokens: TokenStream = r#"kind = MucosalKind::UserInput, rationale = "public firehose intake endpoint accepts anonymous submissions by design", accepts = "anonymous JSON payloads up to 64KB""#
.parse()
.unwrap();
let args = syn::parse2::<MucosalTolerantArgs>(tokens).unwrap();
assert_eq!(args.kind, Some(MacroMucosalKind::UserInput));
args.validate().unwrap();
}
#[test]
fn mucosal_tolerant_validate_rejects_rationale_under_40() {
let tokens: TokenStream = r#"kind = MucosalKind::UserInput, rationale = "twenty-five char rationale", accepts = "anything""#
.parse()
.unwrap();
let args = syn::parse2::<MucosalTolerantArgs>(tokens).unwrap();
assert!(
args.validate().is_err(),
"25-char rationale must fail the ≥40 tolerance floor"
);
}
#[test]
fn mucosal_tolerant_validate_rejects_empty_accepts() {
let tokens: TokenStream = r#"kind = MucosalKind::UserInput, rationale = "this rationale is definitely longer than forty characters", accepts = " ""#
.parse()
.unwrap();
let args = syn::parse2::<MucosalTolerantArgs>(tokens).unwrap();
assert!(args.validate().is_err());
}
#[test]
fn mucosal_tolerant_validate_rejects_missing_accepts() {
let tokens: TokenStream = r#"kind = MucosalKind::UserInput, rationale = "this rationale is definitely longer than forty characters""#
.parse()
.unwrap();
let args = syn::parse2::<MucosalTolerantArgs>(tokens).unwrap();
assert!(args.validate().is_err());
}
#[test]
fn generates_parser_accepts_canonical() {
let tokens: TokenStream =
r#"PanickingInDrop, rationale = "the emitted Drop impl may panic; verify inner types""#
.parse()
.unwrap();
let args = syn::parse2::<GeneratesArgs>(tokens).unwrap();
assert!(args.validate().is_ok());
assert_eq!(args.antigen_name(), "PanickingInDrop");
}
#[test]
fn generates_parser_uses_last_path_segment_for_name() {
let tokens: TokenStream =
r#"antigen::stdlib::PanickingInDrop, rationale = "qualified path resolves to last seg""#
.parse()
.unwrap();
let args = syn::parse2::<GeneratesArgs>(tokens).unwrap();
assert_eq!(args.antigen_name(), "PanickingInDrop");
}
#[test]
fn generates_validate_rejects_missing_rationale() {
let tokens: TokenStream = "PanickingInDrop".parse().unwrap();
let args = syn::parse2::<GeneratesArgs>(tokens).unwrap();
assert!(
args.validate().is_err(),
"a generation claim without rationale is not a claim"
);
}
#[test]
fn generates_validate_rejects_empty_rationale() {
let tokens: TokenStream = r#"PanickingInDrop, rationale = """#.parse().unwrap();
let args = syn::parse2::<GeneratesArgs>(tokens).unwrap();
assert!(args.validate().is_err(), "empty rationale must be rejected");
}
#[test]
fn generates_validate_rejects_empty_optional_fields() {
let tokens: TokenStream = r#"PanickingInDrop, rationale = "valid", witness_template = """#
.parse()
.unwrap();
let args = syn::parse2::<GeneratesArgs>(tokens).unwrap();
assert!(
args.validate().is_err(),
"an empty witness_template indicates user error (mirror until=\"\" rejection)"
);
}
#[test]
fn generates_parser_rejects_unknown_field() {
let tokens: TokenStream = r#"PanickingInDrop, rationale = "valid", bogus_field = "x""#
.parse()
.unwrap();
assert!(
syn::parse2::<GeneratesArgs>(tokens).is_err(),
"unknown field must be a parse error (strict macro-side surface)"
);
}
#[test]
fn generates_parser_accepts_v2_optional_fields() {
let tokens: TokenStream = r#"PanickingInDrop, rationale = "valid", witness_template = "tests/drop_panic.rs", if_attr_present = "skip_drop""#
.parse()
.unwrap();
let args = syn::parse2::<GeneratesArgs>(tokens).unwrap();
assert!(args.validate().is_ok());
assert_eq!(
args.witness_template.as_deref(),
Some("tests/drop_panic.rs")
);
assert_eq!(args.if_attr_present.as_deref(), Some("skip_drop"));
}
#[test]
fn mucosal_tolerant_accepts_optional_reviewed_by_and_until() {
let tokens: TokenStream = r#"kind = MucosalKind::ApiRequest, rationale = "internal admin endpoint behind VPN; trusted-network assumption documented", accepts = "admin-panel form posts", reviewed_by = "security-team", until = "2026-12-31""#
.parse()
.unwrap();
let args = syn::parse2::<MucosalTolerantArgs>(tokens).unwrap();
assert_eq!(args.reviewed_by.as_deref(), Some("security-team"));
assert_eq!(args.until.as_deref(), Some("2026-12-31"));
args.validate().unwrap();
}
#[test]
fn immunosuppress_validate_rejects_until_in_the_past() {
let yesterday = (today_utc() - chrono::Duration::days(1))
.format("%Y-%m-%d")
.to_string();
let tokens: TokenStream = format!(
r#"PanickingInDrop,
rationale = "Suppressing while we wait for proptest infrastructure build",
until = "{yesterday}""#
)
.parse()
.unwrap();
let args = syn::parse2::<ImmunosuppressArgs>(tokens).unwrap();
assert!(
args.validate().is_err(),
"ATK-IMMUNOSUPPRESS-PAST-DATE: #[immunosuppress] with until in the past must fail \
validation — expired suppression is silent check-disabling with no accountability. \
Got Ok(()) for until={yesterday}. \
Fix: add duration_days < 0 check in ImmunosuppressArgs::validate() at ~line 856."
);
}
#[test]
fn poxparty_validate_rejects_until_in_the_past() {
let yesterday = (today_utc() - chrono::Duration::days(1))
.format("%Y-%m-%d")
.to_string();
let tokens: TokenStream = format!(
r#"PanickingInDrop,
exercise_type = "Trigger deliberate Drop panic and measure detection lag",
until = "{yesterday}""#
)
.parse()
.unwrap();
let args = syn::parse2::<PoxpartyArgs>(tokens).unwrap();
assert!(
args.validate().is_err(),
"ATK-POXPARTY-PAST-DATE: #[poxparty] with until in the past must fail validation — \
an expired pox-party exercise represents stale controlled-exposure with no accountability. \
Got Ok(()) for until={yesterday}. \
Fix: add parse_iso_date() + horizon_days < 0 check to PoxpartyArgs::validate()."
);
}
#[test]
fn anergy_validate_rejects_until_in_the_past() {
let yesterday = (today_utc() - chrono::Duration::days(1))
.format("%Y-%m-%d")
.to_string();
let tokens: TokenStream = format!(
r#"PanickingInDrop,
reason = "Suppressing until we audit all Drop impls in the codebase",
until = "{yesterday}""#
)
.parse()
.unwrap();
let args = syn::parse2::<AnergyArgs>(tokens).unwrap();
assert!(
args.validate().is_err(),
"ATK-ANERGY-PAST-DATE: #[anergy] with until in the past must fail validation — \
an expired anergy window represents silent check-suppression with no accountability. \
Got Ok(()) for until={yesterday}. \
Fix: add parse_iso_date() + horizon_days < 0 check to AnergyArgs::validate() \
parallel to OrientArgs commit 53d2bab."
);
}
#[test]
fn anergy_validate_rejects_invalid_date_format() {
let tokens: TokenStream =
r#"PanickingInDrop, reason = "Suppressing until we audit all Drop impls in the codebase", until = "not-a-date""#
.parse()
.unwrap();
let args = syn::parse2::<AnergyArgs>(tokens).unwrap();
assert!(
args.validate().is_err(),
"ATK-ANERGY-INVALID-DATE: #[anergy] with until = 'not-a-date' must fail validation. \
parse_iso_date returns Err(()), so the if-let-Ok arm is skipped — no error returned. \
An unparseable until date is an unbounded anergy window (no expiry ever fires). \
Fix: after the parse_iso_date call, also return Err if parsing FAILED — \
invalid dates should be rejected, not silently accepted."
);
}
#[test]
fn immunosuppress_validate_rejects_invalid_date_format() {
let tokens: TokenStream =
r#"PanickingInDrop, rationale = "Suppressing this check until we audit all Drop impls", until = "2999-13-01""#
.parse()
.unwrap();
let args = syn::parse2::<ImmunosuppressArgs>(tokens).unwrap();
assert!(
args.validate().is_err(),
"ATK-IMMUNOSUPPRESS-INVALID-DATE: #[immunosuppress] with until = '2999-13-01' \
(invalid month 13) must fail validation. parse_iso_date returns Err(()), \
so the if-let-Ok arm is skipped — no error returned. \
An unparseable until date is an unbounded suppression window. \
Fix: treat parse_iso_date failure as a validation error."
);
}
#[test]
fn poxparty_validate_rejects_invalid_date_format() {
let tokens: TokenStream =
r#"PanickingInDrop, exercise_type = "Trigger deliberate Drop panic and measure detection lag", until = "v2.0-release""#
.parse()
.unwrap();
let args = syn::parse2::<PoxpartyArgs>(tokens).unwrap();
assert!(
args.validate().is_err(),
"ATK-POXPARTY-INVALID-DATE: #[poxparty] with until = 'v2.0-release' \
must fail validation. parse_iso_date returns Err(()), so the if-let-Ok \
arm is skipped — no error returned. A version tag as until date creates \
an unbounded poxparty window that never expires. \
Fix: treat parse_iso_date failure as a validation error."
);
}
#[test]
fn anergy_validate_rejects_whitespace_only_reason_of_20_chars() {
let tokens: TokenStream =
r#"PanickingInDrop, reason = " ", until = "2099-12-31""#
.parse()
.unwrap();
let args = syn::parse2::<AnergyArgs>(tokens).unwrap();
assert_eq!(args.reason.as_deref().map(str::len), Some(20));
assert!(
args.validate().is_err(),
"ATK-WHITESPACE-RATIONALE: #[anergy] reason of 20 spaces must fail validation. \
AnergyArgs::validate() line 643 uses r.len() < 20 but not trim().is_empty(). \
Fix: add 'Some(r) if r.trim().is_empty() => Err(...)' arm to the reason match, \
parallel to triage_commit's fix at line 1640."
);
}
#[test]
fn immunosuppress_validate_rejects_whitespace_only_rationale_of_20_chars() {
let tokens: TokenStream =
r#"PanickingInDrop, rationale = " ", until = "2099-12-31""#
.parse()
.unwrap();
let args = syn::parse2::<ImmunosuppressArgs>(tokens).unwrap();
assert_eq!(args.rationale.as_deref().map(str::len), Some(20));
assert!(
args.validate().is_err(),
"ATK-WHITESPACE-RATIONALE: #[immunosuppress] rationale of 20 spaces must fail. \
ImmunosuppressArgs::validate() line 843 uses r.len() < 20 without trim(). \
Fix: add trim().is_empty() arm parallel to triage_commit fix at line 1640."
);
}
#[test]
fn poxparty_validate_rejects_whitespace_only_exercise_type_of_20_chars() {
let tokens: TokenStream =
r#"PanickingInDrop, exercise_type = " ", until = "2099-12-31""#
.parse()
.unwrap();
let args = syn::parse2::<PoxpartyArgs>(tokens).unwrap();
assert_eq!(args.exercise_type.as_deref().map(str::len), Some(20));
assert!(
args.validate().is_err(),
"ATK-WHITESPACE-RATIONALE: #[poxparty] exercise_type of 20 spaces must fail. \
PoxpartyArgs::validate() line 1062 uses et.len() < 20 without trim(). \
Fix: add trim().is_empty() arm parallel to triage_commit fix at line 1640."
);
}
#[test]
fn orient_validate_rejects_whitespace_only_learning_path_of_20_chars() {
let until = (today_utc() + chrono::Duration::days(90))
.format("%Y-%m-%d")
.to_string();
let src = format!(
r#"PanickingInDrop, learning_path = " ", until = "{until}""#
);
let tokens: TokenStream = src.parse().unwrap();
let args = syn::parse2::<OrientArgs>(tokens).unwrap();
assert_eq!(args.learning_path.as_deref().map(str::len), Some(20));
assert!(
args.validate().is_err(),
"ATK-WHITESPACE-RATIONALE: #[orient] learning_path of 20 spaces must fail. \
OrientArgs::validate() line 1269 uses p.len() < 20 without trim(). \
Fix: add trim().is_empty() arm parallel to triage_commit fix at line 1640."
);
}
}
#[cfg(test)]
mod parser_props {
use super::*;
use proc_macro2::TokenStream;
use proptest::prelude::*;
const RUST_KEYWORDS: &[&str] = &[
"as", "async", "await", "box", "break", "const", "continue", "crate", "do", "dyn", "else",
"enum", "extern", "false", "fn", "for", "if", "impl", "in", "let", "loop", "macro",
"match", "mod", "move", "mut", "pub", "ref", "return", "self", "static", "struct", "super",
"trait", "true", "type", "union", "unsafe", "use", "where", "while", "yield", "abstract",
"become", "final", "override", "priv", "try",
];
fn valid_kebab() -> impl Strategy<Value = String> {
proptest::collection::vec(
(
proptest::char::range('a', 'z'),
proptest::collection::vec(
prop_oneof![
proptest::char::range('a', 'z'),
proptest::char::range('0', '9')
],
0..8usize,
),
)
.prop_map(|(first, rest)| {
let mut s = String::with_capacity(rest.len() + 1);
s.push(first);
for c in rest {
s.push(c);
}
s
}),
1..5usize,
)
.prop_map(|segments| segments.join("-"))
}
fn valid_text(max_len: usize) -> impl Strategy<Value = String> {
proptest::collection::vec(
prop_oneof![
proptest::char::range(' ', '~').prop_filter("excluded chars", |c| {
*c != '\\' && *c != '"' && *c != '\0'
}),
],
1..=max_len,
)
.prop_map(|chars| chars.into_iter().collect())
}
fn lit(s: &str) -> String {
format!("{s:?}")
}
fn render_antigen_body(
name: &str,
fingerprint: &str,
family: Option<&str>,
summary: Option<&str>,
) -> String {
let mut parts = vec![
format!("name = {}", lit(name)),
format!("fingerprint = {}", lit(fingerprint)),
];
if let Some(f) = family {
parts.push(format!("family = {}", lit(f)));
}
if let Some(s) = summary {
parts.push(format!("summary = {}", lit(s)));
}
parts.join(", ")
}
proptest! {
#[test]
fn antigen_parser_round_trip(
name in valid_kebab(),
fingerprint in valid_text(64),
family in proptest::option::of(valid_text(32)),
summary in proptest::option::of(valid_text(64)),
) {
let body = render_antigen_body(&name, &fingerprint, family.as_deref(), summary.as_deref());
let tokens: TokenStream = body.parse().expect("body must tokenize");
let args = syn::parse2::<AntigenArgs>(tokens).expect("body must parse");
prop_assert_eq!(&args.name, &name);
prop_assert_eq!(args.fingerprint.as_deref(), Some(fingerprint.as_str()));
prop_assert_eq!(args.family.as_deref(), family.as_deref());
prop_assert_eq!(args.summary.as_deref(), summary.as_deref());
let re_rendered = render_antigen_body(&args.name, args.fingerprint.as_deref().unwrap_or(""), args.family.as_deref(), args.summary.as_deref());
let re_tokens: TokenStream = re_rendered.parse().expect("re-rendered body must tokenize");
let args2 = syn::parse2::<AntigenArgs>(re_tokens).expect("re-rendered body must parse");
prop_assert_eq!(&args.name, &args2.name);
prop_assert_eq!(&args.fingerprint, &args2.fingerprint);
prop_assert_eq!(args.family, args2.family);
prop_assert_eq!(args.summary, args2.summary);
}
#[test]
fn antigen_parser_order_invariant(
name in valid_kebab(),
fingerprint in valid_text(48),
family in valid_text(24),
summary in valid_text(48),
) {
let canonical = format!(
"name = {}, fingerprint = {}, family = {}, summary = {}",
lit(&name), lit(&fingerprint), lit(&family), lit(&summary),
);
let reversed = format!(
"summary = {}, family = {}, fingerprint = {}, name = {}",
lit(&summary), lit(&family), lit(&fingerprint), lit(&name),
);
let a: AntigenArgs = syn::parse2(canonical.parse::<TokenStream>().unwrap()).unwrap();
let b: AntigenArgs = syn::parse2(reversed.parse::<TokenStream>().unwrap()).unwrap();
prop_assert_eq!(&a.name, &b.name);
prop_assert_eq!(&a.fingerprint, &b.fingerprint);
prop_assert_eq!(&a.family, &b.family);
prop_assert_eq!(&a.summary, &b.summary);
}
#[test]
fn is_kebab_case_accepts_generator(name in valid_kebab()) {
prop_assert!(is_kebab_case(&name), "is_kebab_case rejected generator output: {name:?}");
}
#[test]
fn antigen_parser_requires_name(
fingerprint in valid_text(32),
family in proptest::option::of(valid_text(16)),
) {
let mut parts = vec![format!("fingerprint = {}", lit(&fingerprint))];
if let Some(f) = &family {
parts.push(format!("family = {}", lit(f)));
}
let body = parts.join(", ");
let tokens: TokenStream = body.parse().expect("body tokenizes");
let result = syn::parse2::<AntigenArgs>(tokens);
prop_assert!(result.is_err(), "expected parser to reject input missing `name`: {body:?}");
let err = result.unwrap_err().to_string();
prop_assert!(err.contains("name"), "error must mention `name`, got: {err:?}");
}
#[test]
fn antigen_parser_accepts_missing_fingerprint_verify_only(
name in valid_kebab(),
family in proptest::option::of(valid_text(16)),
) {
let mut parts = vec![format!("name = {}", lit(&name))];
if let Some(f) = &family {
parts.push(format!("family = {}", lit(f)));
}
let body = parts.join(", ");
let tokens: TokenStream = body.parse().expect("body tokenizes");
let args = syn::parse2::<AntigenArgs>(tokens)
.expect("a fingerprint-less #[antigen] is a valid verify-only declaration");
prop_assert!(args.fingerprint.is_none(), "absent fingerprint must be None");
prop_assert!(args.validate().is_ok(), "verify-only antigen must validate");
}
#[test]
fn antigen_parser_rejects_unknown_field(
name in valid_kebab(),
fingerprint in valid_text(32),
unknown in "[a-z][a-z_]{2,12}".prop_filter(
"must not collide with known fields or Rust keywords",
|s| {
!matches!(s.as_str(), "name" | "fingerprint" | "family" | "summary" | "references")
&& !RUST_KEYWORDS.contains(&s.as_str())
},
),
) {
let body = format!(
"name = {}, fingerprint = {}, {} = \"x\"",
lit(&name), lit(&fingerprint), unknown,
);
let tokens: TokenStream = body.parse().expect("body tokenizes");
let result = syn::parse2::<AntigenArgs>(tokens);
prop_assert!(result.is_err(), "expected unknown field rejection for: {body:?}");
let err = result.unwrap_err().to_string();
prop_assert!(
err.contains("unknown") && err.contains(&unknown),
"error must name the unknown field. got: {err:?}",
);
}
#[test]
fn antigen_parser_accepts_references_array(
name in valid_kebab(),
fingerprint in valid_text(32),
refs in proptest::collection::vec(valid_text(24), 0..6usize),
) {
let refs_lit: Vec<String> = refs.iter().map(|s| lit(s)).collect();
let body = format!(
"name = {}, fingerprint = {}, references = [{}]",
lit(&name), lit(&fingerprint), refs_lit.join(", "),
);
let tokens: TokenStream = body.parse().expect("body tokenizes");
let args = syn::parse2::<AntigenArgs>(tokens).expect("body parses");
prop_assert_eq!(&args.references, &refs);
}
#[test]
fn immune_parser_accepts_witness(
antigen in "[A-Z][A-Za-z0-9]{0,16}",
witness_segments in proptest::collection::vec(
"[a-z][a-z_0-9]{0,8}".prop_filter("must not be a Rust keyword", |s| {
!RUST_KEYWORDS.contains(&s.as_str())
}),
1..4usize,
),
) {
let witness = witness_segments.join("::");
let body = format!("{antigen}, witness = {witness}");
let tokens: TokenStream = body.parse().expect("body tokenizes");
let args = syn::parse2::<ImmuneArgs>(tokens).expect("body parses");
prop_assert!(args.witness.is_some());
prop_assert!(args.validate().is_ok());
}
#[test]
fn immune_parser_validate_rejects_missing_witness(
antigen in "[A-Z][A-Za-z0-9]{0,16}",
) {
let tokens: TokenStream = antigen.parse().expect("antigen tokenizes");
let args = syn::parse2::<ImmuneArgs>(tokens).expect("bare path parses");
prop_assert!(args.witness.is_none());
let err = args.validate().unwrap_err().to_string();
prop_assert!(err.contains("witness"), "validate must mention `witness`, got: {err:?}");
}
}
#[test]
fn defended_by_parses_bare_antigen_path() {
let tokens: proc_macro2::TokenStream = "ParallelStateTrackersDiverge".parse().unwrap();
let args = syn::parse2::<DefendedByArgs>(tokens).expect("bare path parses");
assert_eq!(
args.antigen.segments.last().unwrap().ident.to_string(),
"ParallelStateTrackersDiverge"
);
}
#[test]
fn defended_by_parses_qualified_path() {
let tokens: proc_macro2::TokenStream = "crate::antigens::DropPanicClass".parse().unwrap();
let args = syn::parse2::<DefendedByArgs>(tokens).expect("qualified path parses");
assert_eq!(
args.antigen.segments.last().unwrap().ident.to_string(),
"DropPanicClass"
);
}
#[test]
fn defended_by_rejects_trailing_witness_args() {
let tokens: proc_macro2::TokenStream =
"DropPanicClass, witness = some_test".parse().unwrap();
let err = syn::parse2::<DefendedByArgs>(tokens)
.expect_err("trailing args must be rejected")
.to_string();
assert!(
err.contains("exactly one positional argument"),
"error must explain the single-arg shape; got: {err:?}"
);
}
#[test]
fn defended_by_rejects_empty() {
let tokens = proc_macro2::TokenStream::new();
assert!(
syn::parse2::<DefendedByArgs>(tokens).is_err(),
"empty #[defended_by] body must be rejected"
);
}
#[test]
fn presents_parses_bare_antigen_still() {
let tokens: proc_macro2::TokenStream = "PanickingInDrop".parse().unwrap();
let args = syn::parse2::<PresentsArgs>(tokens).expect("bare presents parses");
assert!(args.requires.is_none());
assert!(args.proof.is_none());
assert!(args.validate().is_ok());
}
#[test]
fn presents_parses_requires_predicate() {
let tokens: proc_macro2::TokenStream =
r#"UnpinnedDependency, requires = ratified_doc(path = "docs/x.md")"#
.parse()
.unwrap();
let args = syn::parse2::<PresentsArgs>(tokens).expect("requires folds in");
assert!(args.requires.is_some());
assert!(args.validate().is_ok());
assert!(args.requires_json().is_some());
}
#[test]
fn presents_parses_proof_expression() {
let tokens: proc_macro2::TokenStream =
"DropPanicClass, proof = NonPanickingProof::<T>::verified"
.parse()
.unwrap();
let args = syn::parse2::<PresentsArgs>(tokens).expect("proof folds in");
assert!(args.proof.is_some());
assert!(args.requires_json().is_none());
}
#[test]
fn presents_rejects_unknown_field() {
let body = "DropPanicClass, witness = some_test";
let tokens: proc_macro2::TokenStream = body.parse().unwrap();
let err = syn::parse2::<PresentsArgs>(tokens)
.expect_err("witness= on presents is rejected")
.to_string();
assert!(
err.contains("defended_by"),
"error should point at #[defended_by] for code-tier evidence; got: {err:?}"
);
}
#[test]
fn panel_accepts_full_shape() {
let tokens: TokenStream = r#"needs = ["review error path", "check edge case"], filled_by = ["alice"], reviewed_by = ["bob"], ordered_by = "carol", due = "2027-01-01""#
.parse()
.unwrap();
let args = syn::parse2::<PanelArgs>(tokens).unwrap();
assert_eq!(args.needs.len(), 2);
assert_eq!(args.filled_by, vec!["alice"]);
assert_eq!(args.reviewed_by, vec!["bob"]);
assert_eq!(args.ordered_by.as_deref(), Some("carol"));
args.validate().unwrap();
}
#[test]
fn panel_rejects_empty_needs() {
let tokens: TokenStream = r"needs = []".parse().unwrap();
let args = syn::parse2::<PanelArgs>(tokens).unwrap();
let err = args
.validate()
.expect_err("empty needs must reject")
.to_string();
assert!(
err.contains("non-empty"),
"error must explain vacuity: {err:?}"
);
}
#[test]
fn panel_rejects_missing_needs() {
let tokens: TokenStream = r#"filled_by = ["alice"]"#.parse().unwrap();
let args = syn::parse2::<PanelArgs>(tokens).unwrap();
assert!(args.validate().is_err(), "missing needs must reject");
}
#[test]
fn panel_rejects_unknown_field() {
let tokens: TokenStream = r#"needs = ["x"], bogus = "y""#.parse().unwrap();
assert!(syn::parse2::<PanelArgs>(tokens).is_err());
}
#[test]
fn ddx_accepts_full_shape() {
let tokens: TokenStream = r#"symptom = "slow query", rule_out = ["missing index", "n+1"], investigator = "alice""#
.parse()
.unwrap();
let args = syn::parse2::<DdxArgs>(tokens).unwrap();
assert_eq!(args.symptom.as_deref(), Some("slow query"));
assert_eq!(args.rule_out.len(), 2);
args.validate().unwrap();
}
#[test]
fn ddx_rejects_empty_rule_out() {
let tokens: TokenStream = r#"symptom = "x", rule_out = []"#.parse().unwrap();
let args = syn::parse2::<DdxArgs>(tokens).unwrap();
assert!(args.validate().is_err(), "empty rule_out must reject");
}
#[test]
fn ddx_rejects_missing_symptom() {
let tokens: TokenStream = r#"rule_out = ["a"]"#.parse().unwrap();
let args = syn::parse2::<DdxArgs>(tokens).unwrap();
assert!(args.validate().is_err());
}
#[test]
fn quarantine_accepts_full_shape() {
let tokens: TokenStream =
r#"scope = "legacy::module", until = "2027-06-01", reason = "pending upstream fix""#
.parse()
.unwrap();
let args = syn::parse2::<QuarantineArgs>(tokens).unwrap();
assert_eq!(args.scope.as_deref(), Some("legacy::module"));
args.validate().unwrap();
}
#[test]
fn quarantine_rejects_empty_reason() {
let tokens: TokenStream = r#"scope = "x", reason = """#.parse().unwrap();
let args = syn::parse2::<QuarantineArgs>(tokens).unwrap();
let err = args
.validate()
.expect_err("empty reason must reject")
.to_string();
assert!(
err.contains("Amendment 2"),
"error must cite ADR-005 Amd2: {err:?}"
);
}
#[test]
fn quarantine_rejects_missing_reason() {
let tokens: TokenStream = r#"scope = "x""#.parse().unwrap();
let args = syn::parse2::<QuarantineArgs>(tokens).unwrap();
assert!(
args.validate().is_err(),
"missing reason must reject (ADR-005 Amd2)"
);
}
#[test]
fn rx_accepts_minimal_and_rejects_empty_treatment() {
let ok: TokenStream = r#"treatment = "add retry with backoff""#.parse().unwrap();
syn::parse2::<RxArgs>(ok).unwrap().validate().unwrap();
let empty: TokenStream = r#"treatment = """#.parse().unwrap();
assert!(syn::parse2::<RxArgs>(empty).unwrap().validate().is_err());
}
#[test]
fn refer_requires_to() {
let ok: TokenStream = r#"to = "platform-team", response_due = "2027-02-01""#
.parse()
.unwrap();
syn::parse2::<ReferArgs>(ok).unwrap().validate().unwrap();
let missing: TokenStream = r#"response_due = "2027-02-01""#.parse().unwrap();
assert!(syn::parse2::<ReferArgs>(missing)
.unwrap()
.validate()
.is_err());
}
#[test]
fn biopsy_requires_location_and_request_text() {
let ok: TokenStream =
r#"location = "parser::fast_path", request_text = "why does it allocate twice""#
.parse()
.unwrap();
syn::parse2::<BiopsyArgs>(ok).unwrap().validate().unwrap();
let missing_text: TokenStream = r#"location = "x""#.parse().unwrap();
assert!(syn::parse2::<BiopsyArgs>(missing_text)
.unwrap()
.validate()
.is_err());
}
#[test]
fn culture_requires_test_kind() {
let ok: TokenStream = r#"test_kind = "24h soak", runs_until = "2027-01-02""#
.parse()
.unwrap();
syn::parse2::<CultureArgs>(ok).unwrap().validate().unwrap();
let missing: TokenStream = r#"duration = "24h""#.parse().unwrap();
assert!(syn::parse2::<CultureArgs>(missing)
.unwrap()
.validate()
.is_err());
}
#[test]
fn triage_accepts_priority_order_of_code_sites() {
let tokens: TokenStream =
r#"priority_order = ["src/a.rs::foo", "src/b.rs::bar"], triaged_by = "navigator", re_triage_due = "2027-03-01""#
.parse()
.unwrap();
let args = syn::parse2::<TriageArgs>(tokens).unwrap();
assert_eq!(args.priority_order.len(), 2);
assert_eq!(args.triaged_by.as_deref(), Some("navigator"));
args.validate().unwrap();
}
#[test]
fn triage_rejects_empty_priority_order() {
let tokens: TokenStream = r"priority_order = []".parse().unwrap();
let args = syn::parse2::<TriageArgs>(tokens).unwrap();
assert!(args.validate().is_err(), "empty priority_order is vacuous");
}
#[test]
fn triage_rejects_dropped_campsites_field() {
let tokens: TokenStream = r#"campsites = ["x"]"#.parse().unwrap();
let err = syn::parse2::<TriageArgs>(tokens)
.expect_err("campsites was dropped — must reject")
.to_string();
assert!(
err.contains("priority_order"),
"the unknown-field error must redirect to priority_order: {err:?}"
);
}
}