1#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
5pub enum AdobeCompatRuleId {
6 SuppressEmptyPagesOnlyWhenRealDataBound,
8 IgnoreInvisibleServerMetadataBindingsForDataBoundSignal,
10 ExcludeNonDataWidgetsFromPageSuppression,
12 CapSuppressionByFormDomPageAreaCount,
14 TrimStaticXfafExcessPagesWhenLayoutIsSinglePageAndFormDomAllows,
16 ExcludeBindNoneFieldsFromPageDataSuppression,
20 RepeatingSubformInstanceCountClampedToOccurRange,
26 BindNoneSubformDoesNotAutoExpand,
31 FormDomDrivenRepeatInstanceReplication,
38}
39
40impl AdobeCompatRuleId {
41 pub const fn as_str(self) -> &'static str {
43 match self {
44 Self::SuppressEmptyPagesOnlyWhenRealDataBound => {
45 "suppress_empty_pages_only_when_real_data_bound"
46 }
47 Self::IgnoreInvisibleServerMetadataBindingsForDataBoundSignal => {
48 "ignore_invisible_server_metadata_bindings_for_data_bound_signal"
49 }
50 Self::ExcludeNonDataWidgetsFromPageSuppression => {
51 "exclude_non_data_widgets_from_page_suppression"
52 }
53 Self::CapSuppressionByFormDomPageAreaCount => {
54 "cap_suppression_by_form_dom_page_area_count"
55 }
56 Self::TrimStaticXfafExcessPagesWhenLayoutIsSinglePageAndFormDomAllows => {
57 "trim_static_xfaf_excess_pages_when_layout_is_single_page_and_form_dom_allows"
58 }
59 Self::ExcludeBindNoneFieldsFromPageDataSuppression => {
60 "exclude_bind_none_fields_from_page_data_suppression"
61 }
62 Self::RepeatingSubformInstanceCountClampedToOccurRange => {
63 "repeating_subform_instance_count_clamped_to_occur_range"
64 }
65 Self::BindNoneSubformDoesNotAutoExpand => "bind_none_subform_does_not_auto_expand",
66 Self::FormDomDrivenRepeatInstanceReplication => {
67 "form_dom_driven_repeat_instance_replication"
68 }
69 }
70 }
71}
72
73#[derive(Debug, Clone, Copy, PartialEq, Eq)]
75pub enum AdobeCompatCategory {
76 Binding,
78 Pagination,
80 StaticPreservation,
82}
83
84impl AdobeCompatCategory {
85 pub const fn as_str(self) -> &'static str {
87 match self {
88 Self::Binding => "binding",
89 Self::Pagination => "pagination",
90 Self::StaticPreservation => "static_preservation",
91 }
92 }
93}
94
95#[derive(Debug, Clone, Copy, PartialEq, Eq)]
97pub enum AdobeCompatPhase {
98 Bind,
100 Paginate,
102 Suppress,
104 Write,
106}
107
108impl AdobeCompatPhase {
109 pub const fn as_str(self) -> &'static str {
111 match self {
112 Self::Bind => "bind",
113 Self::Paginate => "paginate",
114 Self::Suppress => "suppress",
115 Self::Write => "write",
116 }
117 }
118}
119
120#[derive(Debug, Clone, Copy, PartialEq, Eq)]
122pub enum AdobeCompatStatus {
123 Active,
125 Experimental,
127 Deprecated,
129}
130
131impl AdobeCompatStatus {
132 pub const fn as_str(self) -> &'static str {
134 match self {
135 Self::Active => "active",
136 Self::Experimental => "experimental",
137 Self::Deprecated => "deprecated",
138 }
139 }
140}
141
142#[derive(Debug, Clone, Copy, PartialEq, Eq)]
144pub struct DocumentRef {
145 pub doc_id: &'static str,
147 pub note: &'static str,
149}
150
151#[derive(Debug, Clone, Copy, PartialEq, Eq)]
153pub struct AdobeCompatRule {
154 pub id: AdobeCompatRuleId,
156 pub category: AdobeCompatCategory,
158 pub phase: AdobeCompatPhase,
160 pub status: AdobeCompatStatus,
162 pub statement: &'static str,
164 pub positive_examples: &'static [DocumentRef],
166 pub counterexamples: &'static [DocumentRef],
168 pub source_docs: &'static [&'static str],
170 pub related_tests: &'static [&'static str],
172 pub trace_reason: Option<&'static str>,
174}
175
176impl AdobeCompatRule {
177 pub fn stable_summary(&self) -> String {
179 format!(
180 "{}|{}|{}|{}|{}|examples:{}|counterexamples:{}|sources:{}|trace:{}",
181 self.id.as_str(),
182 self.category.as_str(),
183 self.phase.as_str(),
184 self.status.as_str(),
185 self.statement,
186 join_doc_ids(self.positive_examples),
187 join_doc_ids(self.counterexamples),
188 self.source_docs.join(","),
189 self.trace_reason.unwrap_or("none"),
190 )
191 }
192}
193
194const RULE_SUPPRESS_EMPTY_PAGES_EXAMPLES: &[DocumentRef] = &[
195 DocumentRef {
196 doc_id: "13275420",
197 note: "data-bound suppression must be bounded by real visible data",
198 },
199 DocumentRef {
200 doc_id: "d9ec06f8",
201 note: "rich datasets with empty terminal fields exercise suppression",
202 },
203];
204
205const RULE_INVISIBLE_METADATA_EXAMPLES: &[DocumentRef] = &[DocumentRef {
206 doc_id: "M3B_PAGECOUNT_FIDELITY_WAVE_RESULT",
207 note: "AEM server metadata fields are invisible and should not set data-bound signal",
208}];
209
210const RULE_INVISIBLE_METADATA_COUNTEREXAMPLES: &[DocumentRef] = &[DocumentRef {
214 doc_id: "XDP_VISIBLE_BOUND_DROPS_EMPTY_PAGE",
215 note: "visible CustomerName field with a dataset binding must set any_data_bound=true",
216}];
217
218const RULE_WIDGET_COUNTEREXAMPLES: &[DocumentRef] = &[
219 DocumentRef {
220 doc_id: "778a1138",
221 note: "signature-only page is structurally required",
222 },
223 DocumentRef {
224 doc_id: "e0909cf9",
225 note: "barcode continuation is an Adobe-specific pagination counterexample",
226 },
227];
228
229const RULE_WIDGET_RULE_COUNTEREXAMPLES: &[DocumentRef] = &[DocumentRef {
234 doc_id: "XDP_VISIBLE_BOUND_DROPS_EMPTY_PAGE",
235 note:
236 "regular Text field on the page must count as a data field; widget rule must not exclude it",
237}];
238
239const RULE_FORM_DOM_EXAMPLES: &[DocumentRef] = &[DocumentRef {
240 doc_id: "322faac4",
241 note: "form DOM declares 17 pages and prevents over-trimming",
242}];
243
244const RULE_FORM_DOM_COUNTEREXAMPLES: &[DocumentRef] = &[DocumentRef {
249 doc_id: "7dbbe9d9",
250 note: "static XFAF form without form DOM; cap must be uncapped here",
251}];
252
253const RULE_STATIC_TRIM_EXAMPLES: &[DocumentRef] = &[DocumentRef {
254 doc_id: "fe5de953",
255 note: "form DOM present and single-page collapse allows trim of stale placeholders",
256}];
257
258const RULE_STATIC_TRIM_COUNTEREXAMPLES: &[DocumentRef] = &[
259 DocumentRef {
260 doc_id: "322faac4",
261 note: "form DOM declares 17 pages > layout 7; trim blocked by form DOM",
262 },
263 DocumentRef {
264 doc_id: "b5bd3a97",
265 note: "static multi-page preservation guards against over-trimming",
266 },
267 DocumentRef {
268 doc_id: "7dbbe9d9",
269 note: "no form DOM, n_layout=1, host pages preserved (2/2) per Wave 9 None-arm hardening",
270 },
271 DocumentRef {
272 doc_id: "0b86389a",
273 note: "no form DOM, n_layout=1, host pages preserved (3/3) per Wave 9 None-arm hardening",
274 },
275];
276
277const RULE_BIND_NONE_EXAMPLES: &[DocumentRef] = &[DocumentRef {
279 doc_id: "XDP_BIND_NONE_PAGE_IS_NOT_DROPPED",
280 note: "page-1 static <bind match=\"none\"> field must not count as data; page is retained",
281}];
282
283const RULE_BIND_NONE_COUNTEREXAMPLES: &[DocumentRef] = &[DocumentRef {
287 doc_id: "XDP_VISIBLE_BOUND_DROPS_EMPTY_PAGE",
288 note:
289 "regular data field on page-1 is included in the data-field check; empty page-2 is dropped",
290}];
291
292const RULE_OCCUR_CLAMP_EXAMPLES: &[DocumentRef] = &[
296 DocumentRef {
297 doc_id: "merger::tests::repeating_subform_clamps_to_occur_max",
298 note: "5 data records, occur.max=2 → instance count clamped down to 2",
299 },
300 DocumentRef {
301 doc_id: "merger::tests::repeating_subform_respects_occur_min",
302 note: "1 data record, occur.min=3 → instance count lifted up to 3",
303 },
304];
305
306const RULE_FORM_DOM_REPEAT_EXAMPLES: &[DocumentRef] = &[DocumentRef {
312 doc_id: "XDP_FORM_DOM_THREE_INSTANCES",
313 note: "form DOM declares 3 instances; template default is 1 → rule clones 2 more",
314}];
315
316const RULE_FORM_DOM_REPEAT_COUNTEREXAMPLES: &[DocumentRef] = &[
319 DocumentRef {
320 doc_id: "merger::tests::repeating_subform_expands_from_data",
321 note: "no form DOM present → rule never consulted; expansion is data-driven only",
322 },
323 DocumentRef {
324 doc_id: "XDP_FORM_DOM_SAME_AS_TEMPLATE",
325 note: "form DOM declares same instance count as template → rule fires but clones 0",
326 },
327];
328
329const RULE_BIND_NONE_OCCUR_EXAMPLES: &[DocumentRef] = &[DocumentRef {
331 doc_id: "XDP_BIND_NONE_OCCUR_REPEATING",
332 note: "subform with <bind match=\"none\"/> and <occur max=\"5\"/> stays single-instance",
333}];
334
335const RULE_BIND_NONE_OCCUR_COUNTEREXAMPLES: &[DocumentRef] = &[
338 DocumentRef {
339 doc_id: "merger::tests::repeating_subform_expands_from_data",
340 note: "no bind=none → rule allows expansion; 3 data records produce 3 instances",
341 },
342 DocumentRef {
343 doc_id: "XDP_BIND_NONE_NON_REPEATING_OCCUR",
344 note:
345 "bind=none on non-repeating subform → rule fires but had no observable effect (silent)",
346 },
347];
348
349const RULE_OCCUR_CLAMP_COUNTEREXAMPLES: &[DocumentRef] = &[
350 DocumentRef {
351 doc_id: "merger::tests::repeating_subform_unbounded_uses_data_count",
352 note: "occur.max=-1 (unbounded) → rule returns data_count unchanged",
353 },
354 DocumentRef {
355 doc_id: "XDP_BIND_NONE_PAGE_IS_NOT_DROPPED",
356 note: "<bind match=\"none\"> short-circuits expansion → clamp rule never consulted",
357 },
358];
359
360static REGISTRY: &[AdobeCompatRule] = &[
361 AdobeCompatRule {
362 id: AdobeCompatRuleId::SuppressEmptyPagesOnlyWhenRealDataBound,
363 category: AdobeCompatCategory::Pagination,
364 phase: AdobeCompatPhase::Suppress,
365 status: AdobeCompatStatus::Active,
366 statement: "Suppress data-empty pages only after visible DataDom binding proves real submitted data is present.",
367 positive_examples: RULE_SUPPRESS_EMPTY_PAGES_EXAMPLES,
368 counterexamples: RULE_WIDGET_COUNTEREXAMPLES,
369 source_docs: &[
370 "benchmarks/runs/M3B_PAGECOUNT_FIDELITY_WAVE_RESULT.md",
371 "benchmarks/runs/M3B_PAGECOUNT_MISMATCH_BUCKETS.md",
372 "benchmarks/runs/M5_PAGE_SUPPRESSION_RULES_DOSSIER.md",
373 ],
374 related_tests: &[
375 "crates/pdf-xfa/tests/m3b_phaseD_kappa_page_drop.rs",
376 "crates/pdf-xfa/src/adobe_compat/rules.rs::tests",
377 "crates/pdf-xfa/tests/m5_page_suppression_rules.rs",
378 ],
379 trace_reason: Some("suppress_gated_by_data_bound_signal"),
380 },
381 AdobeCompatRule {
382 id: AdobeCompatRuleId::IgnoreInvisibleServerMetadataBindingsForDataBoundSignal,
383 category: AdobeCompatCategory::Binding,
384 phase: AdobeCompatPhase::Bind,
385 status: AdobeCompatStatus::Active,
386 statement: "Bindings on invisible, hidden, or inactive fields do not set the global data-bound signal.",
387 positive_examples: RULE_INVISIBLE_METADATA_EXAMPLES,
388 counterexamples: RULE_INVISIBLE_METADATA_COUNTEREXAMPLES,
389 source_docs: &[
390 "benchmarks/runs/M3B_PAGECOUNT_FIDELITY_WAVE_RESULT.md",
391 "benchmarks/runs/M5_SUPPRESSION_METADATA_RULES_DOSSIER.md",
392 ],
393 related_tests: &[
394 "crates/pdf-xfa/src/merger.rs::parse_field",
395 "crates/pdf-xfa/src/adobe_compat/rules.rs::tests",
396 "crates/pdf-xfa/tests/m5_suppression_metadata_rules.rs",
397 ],
398 trace_reason: Some("invisible_field_binding_ignored"),
399 },
400 AdobeCompatRule {
401 id: AdobeCompatRuleId::ExcludeNonDataWidgetsFromPageSuppression,
402 category: AdobeCompatCategory::Pagination,
403 phase: AdobeCompatPhase::Suppress,
404 status: AdobeCompatStatus::Active,
405 statement: "Signature, button, and barcode widgets are structural unless another rule proves they are data-empty repeaters.",
406 positive_examples: RULE_WIDGET_COUNTEREXAMPLES,
407 counterexamples: RULE_WIDGET_RULE_COUNTEREXAMPLES,
408 source_docs: &[
409 "benchmarks/runs/M3_XFA_NEXT_FIDELITY_TARGETS.md",
410 "benchmarks/runs/M3B_PAGECOUNT_WAVE6_RESULT.md",
411 "benchmarks/runs/M5_SUPPRESSION_METADATA_RULES_DOSSIER.md",
412 ],
413 related_tests: &[
414 "crates/pdf-xfa/src/merger.rs::detect_field_kind",
415 "crates/pdf-xfa/src/adobe_compat/rules.rs::tests",
416 "crates/pdf-xfa/tests/m5_suppression_metadata_rules.rs",
417 ],
418 trace_reason: Some("non_data_widget_excluded_from_data_check"),
419 },
420 AdobeCompatRule {
421 id: AdobeCompatRuleId::CapSuppressionByFormDomPageAreaCount,
422 category: AdobeCompatCategory::Pagination,
423 phase: AdobeCompatPhase::Suppress,
424 status: AdobeCompatStatus::Active,
425 statement: "When a saved form DOM declares a page-area count, page suppression must not reduce output below that count.",
426 positive_examples: RULE_FORM_DOM_EXAMPLES,
427 counterexamples: RULE_FORM_DOM_COUNTEREXAMPLES,
428 source_docs: &[
429 "benchmarks/runs/M3B_PAGECOUNT_WAVE5_RESULT.md",
430 "benchmarks/runs/M3B_PAGECOUNT_CURRENT_STATE_AFTER_WAVE5B.md",
431 "benchmarks/runs/UX1_TRACE_COMPAT_DOSSIER.md",
432 ],
433 related_tests: &[
434 "corpus_322faac4_seventeen_pages",
435 "crates/pdf-xfa/src/adobe_compat/rules.rs::tests",
436 "crates/pdf-xfa/tests/ux1_trace_compat.rs",
437 ],
438 trace_reason: Some("suppress_capped_by_form_dom"),
439 },
440 AdobeCompatRule {
441 id: AdobeCompatRuleId::TrimStaticXfafExcessPagesWhenLayoutIsSinglePageAndFormDomAllows,
442 category: AdobeCompatCategory::StaticPreservation,
443 phase: AdobeCompatPhase::Write,
444 status: AdobeCompatStatus::Active,
445 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.",
446 positive_examples: RULE_STATIC_TRIM_EXAMPLES,
447 counterexamples: RULE_STATIC_TRIM_COUNTEREXAMPLES,
448 source_docs: &[
449 "benchmarks/runs/M3B_PAGECOUNT_WAVE5_RESULT.md",
450 "benchmarks/runs/M3B_FIDELITY_WAVE7_RESULT.md",
451 "benchmarks/runs/M5_PAGE_SUPPRESSION_RULES_DOSSIER.md",
452 "benchmarks/runs/PHASE2_WAVE9_RESULT.md",
453 "benchmarks/runs/PHASE2_WAVE10_RESULT.md",
454 ],
455 related_tests: &[
456 "corpus_7dbbe9d9_two_pages",
457 "corpus_322faac4_seventeen_pages",
458 "corpus_fe5de953_one_page",
459 "static_xfaf_host_pages_preserved_without_form_dom",
460 "static_trim_blocked_when_no_form_dom_present",
461 "crates/pdf-xfa/src/adobe_compat/rules.rs::tests",
462 "crates/pdf-xfa/tests/m5_page_suppression_rules.rs",
463 ],
464 trace_reason: Some("static_xfaf_trim_allowed"),
465 },
466 AdobeCompatRule {
467 id: AdobeCompatRuleId::ExcludeBindNoneFieldsFromPageDataSuppression,
468 category: AdobeCompatCategory::Pagination,
469 phase: AdobeCompatPhase::Suppress,
470 status: AdobeCompatStatus::Active,
471 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.",
472 positive_examples: RULE_BIND_NONE_EXAMPLES,
473 counterexamples: RULE_BIND_NONE_COUNTEREXAMPLES,
474 source_docs: &[
475 "benchmarks/runs/M3B_PAGECOUNT_WAVE6_RESULT.md",
476 "benchmarks/runs/M5_PAGE_SUPPRESSION_RULES_DOSSIER.md",
477 ],
478 related_tests: &[
479 "bind_none_field_with_data_is_not_dropped",
480 "crates/pdf-xfa/src/adobe_compat/rules.rs::tests",
481 "crates/pdf-xfa/tests/m5_page_suppression_rules.rs",
482 ],
483 trace_reason: Some("bind_none_field_excluded_from_data_check"),
484 },
485 AdobeCompatRule {
486 id: AdobeCompatRuleId::RepeatingSubformInstanceCountClampedToOccurRange,
487 category: AdobeCompatCategory::Binding,
488 phase: AdobeCompatPhase::Bind,
489 status: AdobeCompatStatus::Active,
490 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.",
491 positive_examples: RULE_OCCUR_CLAMP_EXAMPLES,
492 counterexamples: RULE_OCCUR_CLAMP_COUNTEREXAMPLES,
493 source_docs: &[
494 "benchmarks/runs/M5_OCCUR_RULE_DOSSIER.md",
495 ],
496 related_tests: &[
497 "merger::tests::repeating_subform_clamps_to_occur_max",
498 "merger::tests::repeating_subform_unbounded_uses_data_count",
499 "merger::tests::repeating_subform_respects_occur_min",
500 "crates/pdf-xfa/src/adobe_compat/rules.rs::tests",
501 "crates/pdf-xfa/tests/m5_occur_rule.rs",
502 ],
503 trace_reason: Some("data_count_clamped_by_occur_max"),
504 },
505 AdobeCompatRule {
506 id: AdobeCompatRuleId::BindNoneSubformDoesNotAutoExpand,
507 category: AdobeCompatCategory::Binding,
508 phase: AdobeCompatPhase::Bind,
509 status: AdobeCompatStatus::Active,
510 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).",
511 positive_examples: RULE_BIND_NONE_OCCUR_EXAMPLES,
512 counterexamples: RULE_BIND_NONE_OCCUR_COUNTEREXAMPLES,
513 source_docs: &["benchmarks/runs/M5_OCCUR_BIND_NONE_DOSSIER.md"],
514 related_tests: &[
515 "crates/pdf-xfa/src/adobe_compat/rules.rs::tests",
516 "crates/pdf-xfa/tests/m5_occur_bind_none.rs",
517 ],
518 trace_reason: Some("bind_none_subform_expansion_skipped"),
519 },
520 AdobeCompatRule {
521 id: AdobeCompatRuleId::FormDomDrivenRepeatInstanceReplication,
522 category: AdobeCompatCategory::Binding,
523 phase: AdobeCompatPhase::Bind,
524 status: AdobeCompatStatus::Active,
525 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).",
526 positive_examples: RULE_FORM_DOM_REPEAT_EXAMPLES,
527 counterexamples: RULE_FORM_DOM_REPEAT_COUNTEREXAMPLES,
528 source_docs: &["benchmarks/runs/M5_OCCUR_FORMDOM_REPEAT_DOSSIER.md"],
529 related_tests: &[
530 "crates/pdf-xfa/src/adobe_compat/rules.rs::tests",
531 "crates/pdf-xfa/tests/m5_occur_formdom_repeat.rs",
532 ],
533 trace_reason: Some("subform_materialised_from_data"),
534 },
535];
536
537pub fn registry() -> &'static [AdobeCompatRule] {
539 REGISTRY
540}
541
542fn join_doc_ids(refs: &[DocumentRef]) -> String {
543 refs.iter()
544 .map(|doc| doc.doc_id)
545 .collect::<Vec<_>>()
546 .join(",")
547}