Skip to main content

pdf_xfa/adobe_compat/
registry.rs

1//! Static Adobe compatibility rule registry.
2
3/// Stable identifier for a documented compatibility rule.
4#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
5pub enum AdobeCompatRuleId {
6    /// Suppress data-empty pages only after real visible data binding occurred.
7    SuppressEmptyPagesOnlyWhenRealDataBound,
8    /// Hidden metadata bindings do not prove user data is present.
9    IgnoreInvisibleServerMetadataBindingsForDataBoundSignal,
10    /// Non-data widgets are structural and should not drive empty-page drops.
11    ExcludeNonDataWidgetsFromPageSuppression,
12    /// Saved form DOM page counts cap page suppression.
13    CapSuppressionByFormDomPageAreaCount,
14    /// Static XFAF surplus host pages can be trimmed under conservative guards.
15    TrimStaticXfafExcessPagesWhenLayoutIsSinglePageAndFormDomAllows,
16    /// Fields with `<bind match="none">` are template-only and never carry
17    /// dataset values; they are excluded from the per-page "has data field"
18    /// signal used by page suppression.
19    ExcludeBindNoneFieldsFromPageDataSuppression,
20    /// Repeating subform instance count is the matched dataset record
21    /// count clamped to `[occur.min, occur.max]` (XFA §4.4.3, p186-192).
22    /// When data exceeds `occur.max` the count is capped; when data is
23    /// below `occur.min` the count is lifted. Acrobat enforces this
24    /// clamp on every repeating-subform expansion.
25    RepeatingSubformInstanceCountClampedToOccurRange,
26    /// Subforms marked `<bind match="none">` are not auto-expanded
27    /// from datasets even when their `<occur>` would otherwise allow
28    /// repetition. They remain as a single template instance; only
29    /// the scriptable `InstanceManager` may add more (XFA §4.4.3).
30    BindNoneSubformDoesNotAutoExpand,
31    /// When the saved form DOM records more repeating-subform
32    /// instances than the template's initial expansion produced,
33    /// the engine clones the template instance to match the form
34    /// DOM's count. The form DOM is authoritative because it
35    /// captures the runtime's `InstanceManager` decisions after
36    /// scripts executed (XFA §4.4.3 + §6.4.3).
37    FormDomDrivenRepeatInstanceReplication,
38}
39
40impl AdobeCompatRuleId {
41    /// Stable snake-case identifier.
42    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/// Broad compatibility behavior category.
74#[derive(Debug, Clone, Copy, PartialEq, Eq)]
75pub enum AdobeCompatCategory {
76    /// Data binding and data-presence interpretation.
77    Binding,
78    /// Pagination and page suppression.
79    Pagination,
80    /// Static XFAF and hybrid PDF preservation/trimming.
81    StaticPreservation,
82}
83
84impl AdobeCompatCategory {
85    /// Stable lower-case tag.
86    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/// Engine phase where a rule is observed or expected to be traced.
96#[derive(Debug, Clone, Copy, PartialEq, Eq)]
97pub enum AdobeCompatPhase {
98    /// Bind/merge phase.
99    Bind,
100    /// Pagination/layout phase.
101    Paginate,
102    /// Page suppression phase.
103    Suppress,
104    /// Final PDF write/trim phase.
105    Write,
106}
107
108impl AdobeCompatPhase {
109    /// Stable lower-case tag.
110    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/// Rule lifecycle.
121#[derive(Debug, Clone, Copy, PartialEq, Eq)]
122pub enum AdobeCompatStatus {
123    /// Existing behavior documented and expected to remain active.
124    Active,
125    /// Proposed or evidence-only behavior that is not active yet.
126    Experimental,
127    /// Retained for compatibility history but no longer active.
128    Deprecated,
129}
130
131impl AdobeCompatStatus {
132    /// Stable lower-case tag.
133    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/// Corpus or report reference used as rule evidence.
143#[derive(Debug, Clone, Copy, PartialEq, Eq)]
144pub struct DocumentRef {
145    /// Stable corpus document ID or short report ID.
146    pub doc_id: &'static str,
147    /// Short reason this document is evidence for the rule.
148    pub note: &'static str,
149}
150
151/// Metadata for a single Adobe compatibility rule.
152#[derive(Debug, Clone, Copy, PartialEq, Eq)]
153pub struct AdobeCompatRule {
154    /// Stable typed rule identifier.
155    pub id: AdobeCompatRuleId,
156    /// Rule category.
157    pub category: AdobeCompatCategory,
158    /// Phase where the behavior is observed.
159    pub phase: AdobeCompatPhase,
160    /// Lifecycle status.
161    pub status: AdobeCompatStatus,
162    /// One-line behavior statement.
163    pub statement: &'static str,
164    /// Documents where this rule is positive evidence.
165    pub positive_examples: &'static [DocumentRef],
166    /// Documents where naive application of this rule would be wrong.
167    pub counterexamples: &'static [DocumentRef],
168    /// Report/source documents supporting the rule.
169    pub source_docs: &'static [&'static str],
170    /// Tests or intended tests related to the rule.
171    pub related_tests: &'static [&'static str],
172    /// Optional M1 trace reason tag for future runtime linkage.
173    pub trace_reason: Option<&'static str>,
174}
175
176impl AdobeCompatRule {
177    /// Stable single-line metadata rendering for tests and reports.
178    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
210/// M5.2b — counterexamples for `IgnoreInvisibleServerMetadataBindingsForDataBoundSignal`:
211/// documents whose visible field bindings MUST set the global
212/// data-bound signal so suppression downstream can run.
213const 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
229/// M5.2b — counterexamples for `ExcludeNonDataWidgetsFromPageSuppression`:
230/// documents whose pages contain regular data fields (text, checkbox,
231/// dropdown, etc.) that MUST count as data fields for the suppression
232/// heuristic.
233const 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
244/// UX1 counterexamples for the form-DOM cap rule: documents where the cap
245/// rule must remain inactive (no form DOM is present, so suppression must
246/// stay uncapped — naïvely caching `max_suppress = 0` here would over-preserve
247/// pages).
248const 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
277/// M5.2 — `ExcludeBindNoneFieldsFromPageDataSuppression` positive evidence.
278const 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
283/// M5.2 — bind=none counterexamples: pages where a regular (not bind=none)
284/// data field IS present and the suppression heuristic must still consider
285/// the page as data-bearing.
286const 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
292/// M5.3 — positive evidence for the occur-clamp rule: synthetic test
293/// templates that already exist in `merger.rs::tests` and exercise the
294/// three clamp branches (max-bound, min-bound, unbounded-passthrough).
295const 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
306/// M5.3 — counterexamples for the occur-clamp rule: cases where the
307/// rule must NOT trigger a clamp (rule returns the data-driven count
308/// unchanged), and cases where the rule never runs because the caller
309/// short-circuits.
310/// M5.3d — positive evidence for `FormDomDrivenRepeatInstanceReplication`.
311const 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
316/// M5.3d — counterexamples: form DOM count equal to or below the
317/// template default (rule decides to add zero clones; trace silent).
318const 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
329/// M5.3b — positive evidence for `BindNoneSubformDoesNotAutoExpand`.
330const 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
335/// M5.3b — counterexamples: subforms that DO expand because the rule
336/// does not apply (no bind=none, or non-repeating occur).
337const 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
537/// Return the deterministic v1 registry.
538pub 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}