#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum AdobeCompatRuleId {
SuppressEmptyPagesOnlyWhenRealDataBound,
IgnoreInvisibleServerMetadataBindingsForDataBoundSignal,
ExcludeNonDataWidgetsFromPageSuppression,
CapSuppressionByFormDomPageAreaCount,
TrimStaticXfafExcessPagesWhenLayoutIsSinglePageAndFormDomAllows,
ExcludeBindNoneFieldsFromPageDataSuppression,
RepeatingSubformInstanceCountClampedToOccurRange,
BindNoneSubformDoesNotAutoExpand,
FormDomDrivenRepeatInstanceReplication,
}
impl AdobeCompatRuleId {
pub const fn as_str(self) -> &'static str {
match self {
Self::SuppressEmptyPagesOnlyWhenRealDataBound => {
"suppress_empty_pages_only_when_real_data_bound"
}
Self::IgnoreInvisibleServerMetadataBindingsForDataBoundSignal => {
"ignore_invisible_server_metadata_bindings_for_data_bound_signal"
}
Self::ExcludeNonDataWidgetsFromPageSuppression => {
"exclude_non_data_widgets_from_page_suppression"
}
Self::CapSuppressionByFormDomPageAreaCount => {
"cap_suppression_by_form_dom_page_area_count"
}
Self::TrimStaticXfafExcessPagesWhenLayoutIsSinglePageAndFormDomAllows => {
"trim_static_xfaf_excess_pages_when_layout_is_single_page_and_form_dom_allows"
}
Self::ExcludeBindNoneFieldsFromPageDataSuppression => {
"exclude_bind_none_fields_from_page_data_suppression"
}
Self::RepeatingSubformInstanceCountClampedToOccurRange => {
"repeating_subform_instance_count_clamped_to_occur_range"
}
Self::BindNoneSubformDoesNotAutoExpand => "bind_none_subform_does_not_auto_expand",
Self::FormDomDrivenRepeatInstanceReplication => {
"form_dom_driven_repeat_instance_replication"
}
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AdobeCompatCategory {
Binding,
Pagination,
StaticPreservation,
}
impl AdobeCompatCategory {
pub const fn as_str(self) -> &'static str {
match self {
Self::Binding => "binding",
Self::Pagination => "pagination",
Self::StaticPreservation => "static_preservation",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AdobeCompatPhase {
Bind,
Paginate,
Suppress,
Write,
}
impl AdobeCompatPhase {
pub const fn as_str(self) -> &'static str {
match self {
Self::Bind => "bind",
Self::Paginate => "paginate",
Self::Suppress => "suppress",
Self::Write => "write",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AdobeCompatStatus {
Active,
Experimental,
Deprecated,
}
impl AdobeCompatStatus {
pub const fn as_str(self) -> &'static str {
match self {
Self::Active => "active",
Self::Experimental => "experimental",
Self::Deprecated => "deprecated",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct DocumentRef {
pub doc_id: &'static str,
pub note: &'static str,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct AdobeCompatRule {
pub id: AdobeCompatRuleId,
pub category: AdobeCompatCategory,
pub phase: AdobeCompatPhase,
pub status: AdobeCompatStatus,
pub statement: &'static str,
pub positive_examples: &'static [DocumentRef],
pub counterexamples: &'static [DocumentRef],
pub source_docs: &'static [&'static str],
pub related_tests: &'static [&'static str],
pub trace_reason: Option<&'static str>,
}
impl AdobeCompatRule {
pub fn stable_summary(&self) -> String {
format!(
"{}|{}|{}|{}|{}|examples:{}|counterexamples:{}|sources:{}|trace:{}",
self.id.as_str(),
self.category.as_str(),
self.phase.as_str(),
self.status.as_str(),
self.statement,
join_doc_ids(self.positive_examples),
join_doc_ids(self.counterexamples),
self.source_docs.join(","),
self.trace_reason.unwrap_or("none"),
)
}
}
const RULE_SUPPRESS_EMPTY_PAGES_EXAMPLES: &[DocumentRef] = &[
DocumentRef {
doc_id: "13275420",
note: "data-bound suppression must be bounded by real visible data",
},
DocumentRef {
doc_id: "d9ec06f8",
note: "rich datasets with empty terminal fields exercise suppression",
},
];
const RULE_INVISIBLE_METADATA_EXAMPLES: &[DocumentRef] = &[DocumentRef {
doc_id: "M3B_PAGECOUNT_FIDELITY_WAVE_RESULT",
note: "AEM server metadata fields are invisible and should not set data-bound signal",
}];
const RULE_INVISIBLE_METADATA_COUNTEREXAMPLES: &[DocumentRef] = &[DocumentRef {
doc_id: "XDP_VISIBLE_BOUND_DROPS_EMPTY_PAGE",
note: "visible CustomerName field with a dataset binding must set any_data_bound=true",
}];
const RULE_WIDGET_COUNTEREXAMPLES: &[DocumentRef] = &[
DocumentRef {
doc_id: "778a1138",
note: "signature-only page is structurally required",
},
DocumentRef {
doc_id: "e0909cf9",
note: "barcode continuation is an Adobe-specific pagination counterexample",
},
];
const RULE_WIDGET_RULE_COUNTEREXAMPLES: &[DocumentRef] = &[DocumentRef {
doc_id: "XDP_VISIBLE_BOUND_DROPS_EMPTY_PAGE",
note:
"regular Text field on the page must count as a data field; widget rule must not exclude it",
}];
const RULE_FORM_DOM_EXAMPLES: &[DocumentRef] = &[DocumentRef {
doc_id: "322faac4",
note: "form DOM declares 17 pages and prevents over-trimming",
}];
const RULE_FORM_DOM_COUNTEREXAMPLES: &[DocumentRef] = &[DocumentRef {
doc_id: "7dbbe9d9",
note: "static XFAF form without form DOM; cap must be uncapped here",
}];
const RULE_STATIC_TRIM_EXAMPLES: &[DocumentRef] = &[DocumentRef {
doc_id: "fe5de953",
note: "form DOM present and single-page collapse allows trim of stale placeholders",
}];
const RULE_STATIC_TRIM_COUNTEREXAMPLES: &[DocumentRef] = &[
DocumentRef {
doc_id: "322faac4",
note: "form DOM declares 17 pages > layout 7; trim blocked by form DOM",
},
DocumentRef {
doc_id: "b5bd3a97",
note: "static multi-page preservation guards against over-trimming",
},
DocumentRef {
doc_id: "7dbbe9d9",
note: "no form DOM, n_layout=1, host pages preserved (2/2) per Wave 9 None-arm hardening",
},
DocumentRef {
doc_id: "0b86389a",
note: "no form DOM, n_layout=1, host pages preserved (3/3) per Wave 9 None-arm hardening",
},
];
const RULE_BIND_NONE_EXAMPLES: &[DocumentRef] = &[DocumentRef {
doc_id: "XDP_BIND_NONE_PAGE_IS_NOT_DROPPED",
note: "page-1 static <bind match=\"none\"> field must not count as data; page is retained",
}];
const RULE_BIND_NONE_COUNTEREXAMPLES: &[DocumentRef] = &[DocumentRef {
doc_id: "XDP_VISIBLE_BOUND_DROPS_EMPTY_PAGE",
note:
"regular data field on page-1 is included in the data-field check; empty page-2 is dropped",
}];
const RULE_OCCUR_CLAMP_EXAMPLES: &[DocumentRef] = &[
DocumentRef {
doc_id: "merger::tests::repeating_subform_clamps_to_occur_max",
note: "5 data records, occur.max=2 → instance count clamped down to 2",
},
DocumentRef {
doc_id: "merger::tests::repeating_subform_respects_occur_min",
note: "1 data record, occur.min=3 → instance count lifted up to 3",
},
];
const RULE_FORM_DOM_REPEAT_EXAMPLES: &[DocumentRef] = &[DocumentRef {
doc_id: "XDP_FORM_DOM_THREE_INSTANCES",
note: "form DOM declares 3 instances; template default is 1 → rule clones 2 more",
}];
const RULE_FORM_DOM_REPEAT_COUNTEREXAMPLES: &[DocumentRef] = &[
DocumentRef {
doc_id: "merger::tests::repeating_subform_expands_from_data",
note: "no form DOM present → rule never consulted; expansion is data-driven only",
},
DocumentRef {
doc_id: "XDP_FORM_DOM_SAME_AS_TEMPLATE",
note: "form DOM declares same instance count as template → rule fires but clones 0",
},
];
const RULE_BIND_NONE_OCCUR_EXAMPLES: &[DocumentRef] = &[DocumentRef {
doc_id: "XDP_BIND_NONE_OCCUR_REPEATING",
note: "subform with <bind match=\"none\"/> and <occur max=\"5\"/> stays single-instance",
}];
const RULE_BIND_NONE_OCCUR_COUNTEREXAMPLES: &[DocumentRef] = &[
DocumentRef {
doc_id: "merger::tests::repeating_subform_expands_from_data",
note: "no bind=none → rule allows expansion; 3 data records produce 3 instances",
},
DocumentRef {
doc_id: "XDP_BIND_NONE_NON_REPEATING_OCCUR",
note:
"bind=none on non-repeating subform → rule fires but had no observable effect (silent)",
},
];
const RULE_OCCUR_CLAMP_COUNTEREXAMPLES: &[DocumentRef] = &[
DocumentRef {
doc_id: "merger::tests::repeating_subform_unbounded_uses_data_count",
note: "occur.max=-1 (unbounded) → rule returns data_count unchanged",
},
DocumentRef {
doc_id: "XDP_BIND_NONE_PAGE_IS_NOT_DROPPED",
note: "<bind match=\"none\"> short-circuits expansion → clamp rule never consulted",
},
];
static REGISTRY: &[AdobeCompatRule] = &[
AdobeCompatRule {
id: AdobeCompatRuleId::SuppressEmptyPagesOnlyWhenRealDataBound,
category: AdobeCompatCategory::Pagination,
phase: AdobeCompatPhase::Suppress,
status: AdobeCompatStatus::Active,
statement: "Suppress data-empty pages only after visible DataDom binding proves real submitted data is present.",
positive_examples: RULE_SUPPRESS_EMPTY_PAGES_EXAMPLES,
counterexamples: RULE_WIDGET_COUNTEREXAMPLES,
source_docs: &[
"benchmarks/runs/M3B_PAGECOUNT_FIDELITY_WAVE_RESULT.md",
"benchmarks/runs/M3B_PAGECOUNT_MISMATCH_BUCKETS.md",
"benchmarks/runs/M5_PAGE_SUPPRESSION_RULES_DOSSIER.md",
],
related_tests: &[
"crates/pdf-xfa/tests/m3b_phaseD_kappa_page_drop.rs",
"crates/pdf-xfa/src/adobe_compat/rules.rs::tests",
"crates/pdf-xfa/tests/m5_page_suppression_rules.rs",
],
trace_reason: Some("suppress_gated_by_data_bound_signal"),
},
AdobeCompatRule {
id: AdobeCompatRuleId::IgnoreInvisibleServerMetadataBindingsForDataBoundSignal,
category: AdobeCompatCategory::Binding,
phase: AdobeCompatPhase::Bind,
status: AdobeCompatStatus::Active,
statement: "Bindings on invisible, hidden, or inactive fields do not set the global data-bound signal.",
positive_examples: RULE_INVISIBLE_METADATA_EXAMPLES,
counterexamples: RULE_INVISIBLE_METADATA_COUNTEREXAMPLES,
source_docs: &[
"benchmarks/runs/M3B_PAGECOUNT_FIDELITY_WAVE_RESULT.md",
"benchmarks/runs/M5_SUPPRESSION_METADATA_RULES_DOSSIER.md",
],
related_tests: &[
"crates/pdf-xfa/src/merger.rs::parse_field",
"crates/pdf-xfa/src/adobe_compat/rules.rs::tests",
"crates/pdf-xfa/tests/m5_suppression_metadata_rules.rs",
],
trace_reason: Some("invisible_field_binding_ignored"),
},
AdobeCompatRule {
id: AdobeCompatRuleId::ExcludeNonDataWidgetsFromPageSuppression,
category: AdobeCompatCategory::Pagination,
phase: AdobeCompatPhase::Suppress,
status: AdobeCompatStatus::Active,
statement: "Signature, button, and barcode widgets are structural unless another rule proves they are data-empty repeaters.",
positive_examples: RULE_WIDGET_COUNTEREXAMPLES,
counterexamples: RULE_WIDGET_RULE_COUNTEREXAMPLES,
source_docs: &[
"benchmarks/runs/M3_XFA_NEXT_FIDELITY_TARGETS.md",
"benchmarks/runs/M3B_PAGECOUNT_WAVE6_RESULT.md",
"benchmarks/runs/M5_SUPPRESSION_METADATA_RULES_DOSSIER.md",
],
related_tests: &[
"crates/pdf-xfa/src/merger.rs::detect_field_kind",
"crates/pdf-xfa/src/adobe_compat/rules.rs::tests",
"crates/pdf-xfa/tests/m5_suppression_metadata_rules.rs",
],
trace_reason: Some("non_data_widget_excluded_from_data_check"),
},
AdobeCompatRule {
id: AdobeCompatRuleId::CapSuppressionByFormDomPageAreaCount,
category: AdobeCompatCategory::Pagination,
phase: AdobeCompatPhase::Suppress,
status: AdobeCompatStatus::Active,
statement: "When a saved form DOM declares a page-area count, page suppression must not reduce output below that count.",
positive_examples: RULE_FORM_DOM_EXAMPLES,
counterexamples: RULE_FORM_DOM_COUNTEREXAMPLES,
source_docs: &[
"benchmarks/runs/M3B_PAGECOUNT_WAVE5_RESULT.md",
"benchmarks/runs/M3B_PAGECOUNT_CURRENT_STATE_AFTER_WAVE5B.md",
"benchmarks/runs/UX1_TRACE_COMPAT_DOSSIER.md",
],
related_tests: &[
"corpus_322faac4_seventeen_pages",
"crates/pdf-xfa/src/adobe_compat/rules.rs::tests",
"crates/pdf-xfa/tests/ux1_trace_compat.rs",
],
trace_reason: Some("suppress_capped_by_form_dom"),
},
AdobeCompatRule {
id: AdobeCompatRuleId::TrimStaticXfafExcessPagesWhenLayoutIsSinglePageAndFormDomAllows,
category: AdobeCompatCategory::StaticPreservation,
phase: AdobeCompatPhase::Write,
status: AdobeCompatStatus::Active,
statement: "Static XFAF surplus host pages may be trimmed only when a saved form DOM corroborates trim intent (form-DOM page count <= layout, or single-page collapse with form DOM present); without a form DOM the host PDF page count is authoritative and no trim is performed.",
positive_examples: RULE_STATIC_TRIM_EXAMPLES,
counterexamples: RULE_STATIC_TRIM_COUNTEREXAMPLES,
source_docs: &[
"benchmarks/runs/M3B_PAGECOUNT_WAVE5_RESULT.md",
"benchmarks/runs/M3B_FIDELITY_WAVE7_RESULT.md",
"benchmarks/runs/M5_PAGE_SUPPRESSION_RULES_DOSSIER.md",
"benchmarks/runs/PHASE2_WAVE9_RESULT.md",
"benchmarks/runs/PHASE2_WAVE10_RESULT.md",
],
related_tests: &[
"corpus_7dbbe9d9_two_pages",
"corpus_322faac4_seventeen_pages",
"corpus_fe5de953_one_page",
"static_xfaf_host_pages_preserved_without_form_dom",
"static_trim_blocked_when_no_form_dom_present",
"crates/pdf-xfa/src/adobe_compat/rules.rs::tests",
"crates/pdf-xfa/tests/m5_page_suppression_rules.rs",
],
trace_reason: Some("static_xfaf_trim_allowed"),
},
AdobeCompatRule {
id: AdobeCompatRuleId::ExcludeBindNoneFieldsFromPageDataSuppression,
category: AdobeCompatCategory::Pagination,
phase: AdobeCompatPhase::Suppress,
status: AdobeCompatStatus::Active,
statement: "Fields with <bind match=\"none\"> are template-only and never carry dataset values; they are excluded from the per-page \"has data field\" signal used by page suppression.",
positive_examples: RULE_BIND_NONE_EXAMPLES,
counterexamples: RULE_BIND_NONE_COUNTEREXAMPLES,
source_docs: &[
"benchmarks/runs/M3B_PAGECOUNT_WAVE6_RESULT.md",
"benchmarks/runs/M5_PAGE_SUPPRESSION_RULES_DOSSIER.md",
],
related_tests: &[
"bind_none_field_with_data_is_not_dropped",
"crates/pdf-xfa/src/adobe_compat/rules.rs::tests",
"crates/pdf-xfa/tests/m5_page_suppression_rules.rs",
],
trace_reason: Some("bind_none_field_excluded_from_data_check"),
},
AdobeCompatRule {
id: AdobeCompatRuleId::RepeatingSubformInstanceCountClampedToOccurRange,
category: AdobeCompatCategory::Binding,
phase: AdobeCompatPhase::Bind,
status: AdobeCompatStatus::Active,
statement: "Repeating subform instance count is the matched dataset record count clamped to [occur.min, occur.max] (XFA \u{00A7}4.4.3, p186-192). When data exceeds occur.max the count is capped; when data is below occur.min the count is lifted.",
positive_examples: RULE_OCCUR_CLAMP_EXAMPLES,
counterexamples: RULE_OCCUR_CLAMP_COUNTEREXAMPLES,
source_docs: &[
"benchmarks/runs/M5_OCCUR_RULE_DOSSIER.md",
],
related_tests: &[
"merger::tests::repeating_subform_clamps_to_occur_max",
"merger::tests::repeating_subform_unbounded_uses_data_count",
"merger::tests::repeating_subform_respects_occur_min",
"crates/pdf-xfa/src/adobe_compat/rules.rs::tests",
"crates/pdf-xfa/tests/m5_occur_rule.rs",
],
trace_reason: Some("data_count_clamped_by_occur_max"),
},
AdobeCompatRule {
id: AdobeCompatRuleId::BindNoneSubformDoesNotAutoExpand,
category: AdobeCompatCategory::Binding,
phase: AdobeCompatPhase::Bind,
status: AdobeCompatStatus::Active,
statement: "Subforms marked <bind match=\"none\"> are not auto-expanded from datasets even when their <occur> would otherwise allow repetition. They remain as a single template instance; only the scriptable InstanceManager may add more (XFA \u{00A7}4.4.3).",
positive_examples: RULE_BIND_NONE_OCCUR_EXAMPLES,
counterexamples: RULE_BIND_NONE_OCCUR_COUNTEREXAMPLES,
source_docs: &["benchmarks/runs/M5_OCCUR_BIND_NONE_DOSSIER.md"],
related_tests: &[
"crates/pdf-xfa/src/adobe_compat/rules.rs::tests",
"crates/pdf-xfa/tests/m5_occur_bind_none.rs",
],
trace_reason: Some("bind_none_subform_expansion_skipped"),
},
AdobeCompatRule {
id: AdobeCompatRuleId::FormDomDrivenRepeatInstanceReplication,
category: AdobeCompatCategory::Binding,
phase: AdobeCompatPhase::Bind,
status: AdobeCompatStatus::Active,
statement: "When the saved form DOM records more repeating-subform instances than the template's initial expansion produced, the engine clones the template instance to match the form DOM's count. The form DOM is authoritative because it captures the runtime's InstanceManager decisions after scripts executed (XFA \u{00A7}4.4.3 + \u{00A7}6.4.3).",
positive_examples: RULE_FORM_DOM_REPEAT_EXAMPLES,
counterexamples: RULE_FORM_DOM_REPEAT_COUNTEREXAMPLES,
source_docs: &["benchmarks/runs/M5_OCCUR_FORMDOM_REPEAT_DOSSIER.md"],
related_tests: &[
"crates/pdf-xfa/src/adobe_compat/rules.rs::tests",
"crates/pdf-xfa/tests/m5_occur_formdom_repeat.rs",
],
trace_reason: Some("subform_materialised_from_data"),
},
];
pub fn registry() -> &'static [AdobeCompatRule] {
REGISTRY
}
fn join_doc_ids(refs: &[DocumentRef]) -> String {
refs.iter()
.map(|doc| doc.doc_id)
.collect::<Vec<_>>()
.join(",")
}