Skip to main content

btctax_forms/
map.rs

1//! Committed, per-(form, year) field maps: **logical cell → fully-qualified PDF field name**.
2//!
3//! The maps are DATA (TOML committed next to the bundled PDFs), not code — "adding a year" is a
4//! `forms/<year>/` directory (PDF + maps), never a code change. Keys are the fully-qualified,
5//! bracketed AcroForm names (`topmostSubform[0].Page1[0].Table_Line1_Part1[0].Row1[0].f1_03[0]`).
6//!
7//! Nothing here is trusted blindly: the geometric read-back ([`crate::verify`]) re-derives the
8//! column/row bands from the bundled PDF's own widget `/Rect`s and would flag any mis-labeled cell,
9//! and `map_2025_matches_bundled_pdf_fieldset` asserts every name here exists in the PDF.
10
11use crate::error::FormsError;
12use serde::Deserialize;
13
14/// The two identity cells every IRS form carries at its top: the name line and the SSN.
15///
16/// **Required** on the nine full-return schedule maps (a map without it fails at DESERIALIZATION —
17/// fail-closed at load, and every map is loaded by a test), and `Option` on the two maps shared with
18/// the crypto slice (`ScheduleDMap`, `Form1040Map`), whose 2017/2025 editions have no verified identity
19/// FQNs and no `ReturnInputs` to source an identity from. The full-return fillers refuse on `None`.
20///
21/// The SSN's RENDERING is not fixed here: it is chosen per-cell from the PDF's own `/MaxLen` (11 ⇒
22/// hyphenated, 9 ⇒ bare digits — the schedules and the 1040 genuinely differ). See
23/// [`crate::cells::push_identity`].
24#[derive(Debug, Clone, Deserialize)]
25pub struct IdentityCells {
26    /// "Name(s) shown on return" — or, on Schedule C, "Name of proprietor".
27    pub name: String,
28    /// The SSN cell.
29    pub ssn: String,
30}
31
32/// The TY2025 Form 8949 map (embedded at compile time).
33pub const F8949_MAP_2025: &str = include_str!("../forms/2025/f8949.map.toml");
34/// The TY2025 Schedule D map (embedded at compile time).
35pub const SCHEDULE_D_MAP_2025: &str = include_str!("../forms/2025/schedule_d.map.toml");
36/// The TY2025 Schedule SE map (embedded at compile time).
37pub const SCHEDULE_SE_MAP_2025: &str = include_str!("../forms/2025/schedule_se.map.toml");
38/// The TY2025 Form 8283 map (embedded at compile time).
39pub const F8283_MAP_2025: &str = include_str!("../forms/2025/f8283.map.toml");
40/// The TY2025 Form 1040 map (embedded at compile time).
41pub const F1040_MAP_2025: &str = include_str!("../forms/2025/f1040.map.toml");
42
43/// The TY2024 Form 8949 map (embedded at compile time).
44pub const F8949_MAP_2024: &str = include_str!("../forms/2024/f8949.map.toml");
45/// The TY2024 Schedule D map (embedded at compile time).
46pub const SCHEDULE_D_MAP_2024: &str = include_str!("../forms/2024/schedule_d.map.toml");
47/// The TY2024 Schedule SE map (embedded at compile time).
48pub const SCHEDULE_SE_MAP_2024: &str = include_str!("../forms/2024/schedule_se.map.toml");
49/// The TY2024 Form 8283 map (Rev. 12-2023, embedded at compile time).
50pub const F8283_MAP_2024: &str = include_str!("../forms/2024/f8283.map.toml");
51/// The TY2024 Form 1040 map (embedded at compile time).
52pub const F1040_MAP_2024: &str = include_str!("../forms/2024/f1040.map.toml");
53/// The TY2024 Form 8959 (Additional Medicare Tax) map (embedded at compile time).
54pub const F8959_MAP_2024: &str = include_str!("../forms/2024/f8959.map.toml");
55/// The TY2024 Form 8960 (Net Investment Income Tax) map (embedded at compile time).
56pub const F8960_MAP_2024: &str = include_str!("../forms/2024/f8960.map.toml");
57/// The TY2024 Form 8995 (QBI deduction, simplified) map (embedded at compile time).
58pub const F8995_MAP_2024: &str = include_str!("../forms/2024/f8995.map.toml");
59/// The TY2024 Schedule 2 (Additional Taxes) map (embedded at compile time).
60pub const SCHEDULE_2_MAP_2024: &str = include_str!("../forms/2024/f1040s2.map.toml");
61/// The TY2024 Schedule 3 (Additional Credits and Payments) map (embedded at compile time).
62pub const SCHEDULE_3_MAP_2024: &str = include_str!("../forms/2024/f1040s3.map.toml");
63/// The TY2024 Schedule A (Itemized Deductions) map (embedded at compile time).
64pub const SCHEDULE_A_MAP_2024: &str = include_str!("../forms/2024/f1040sa.map.toml");
65/// The TY2024 Schedule 1 (Additional Income and Adjustments) map (embedded at compile time).
66pub const SCHEDULE_1_MAP_2024: &str = include_str!("../forms/2024/f1040s1.map.toml");
67/// The TY2024 Schedule C (Profit or Loss From Business) map (embedded at compile time).
68pub const SCHEDULE_C_MAP_2024: &str = include_str!("../forms/2024/f1040sc.map.toml");
69/// The TY2024 Schedule B (Interest and Ordinary Dividends) map (embedded at compile time).
70pub const SCHEDULE_B_MAP_2024: &str = include_str!("../forms/2024/f1040sb.map.toml");
71
72/// The TY2017 Form 8949 map (embedded at compile time).
73pub const F8949_MAP_2017: &str = include_str!("../forms/2017/f8949.map.toml");
74/// The TY2017 Schedule D map (embedded at compile time).
75pub const SCHEDULE_D_MAP_2017: &str = include_str!("../forms/2017/schedule_d.map.toml");
76/// The TY2017 Schedule SE map (OLD short+long form; btctax fills §B long — embedded at compile time).
77pub const SCHEDULE_SE_MAP_2017: &str = include_str!("../forms/2017/schedule_se.map.toml");
78/// The TY2017 Form 8283 map (Rev. 12-2014, "j Other" — embedded at compile time).
79pub const F8283_MAP_2017: &str = include_str!("../forms/2017/f8283.map.toml");
80/// The TY2017 Form 1040 map (line 13, no DA question — embedded at compile time).
81pub const F1040_MAP_2017: &str = include_str!("../forms/2017/f1040.map.toml");
82
83/// The 4 monetary "amount" columns of a Form 8949 / Schedule D totals row: (d) proceeds, (e) cost,
84/// (g) adjustment, (h) gain. Column (f) — the code column — has no total (a spacer), so it is absent.
85#[derive(Debug, Clone, Deserialize)]
86pub struct AmountCols {
87    /// Column (d) — proceeds.
88    pub proceeds_d: String,
89    /// Column (e) — cost basis.
90    pub cost_e: String,
91    /// Column (g) — adjustment amount.
92    pub adj_g: String,
93    /// Column (h) — gain/loss.
94    pub gain_h: String,
95}
96
97/// One Form 8949 part (Part I short-term on page 0, Part II long-term on page 1).
98#[derive(Debug, Clone, Deserialize)]
99pub struct PartMap {
100    /// `"short"` (Part I) or `"long"` (Part II).
101    pub term: String,
102    /// 0-based page index of this part within the bundled 2-page PDF.
103    pub page: usize,
104    /// The digital-asset box checkbox field — **Box I** (ST) / **Box L** (LT), NOT C/F.
105    pub box_field: String,
106    /// The checkbox on-state (a PDF name without the leading `/`), e.g. `"6"`.
107    pub box_on: String,
108    /// The line-2 per-part totals row (d,e,g,h).
109    pub totals: AmountCols,
110    /// The 11 data rows; each row is the 8 column field names in order a,b,c,d,e,f,g,h.
111    pub rows: Vec<Vec<String>>,
112}
113
114/// The full Form 8949 field map for one tax year.
115#[derive(Debug, Clone, Deserialize)]
116pub struct Form8949Map {
117    /// `"f8949"`.
118    pub form: String,
119    /// Tax year (e.g. 2025).
120    pub year: i32,
121    /// "Name(s) shown on return" + SSN — on **both pages** (the 8949 is a two-page detail attachment, and
122    /// each page carries the header). `Option`: the crypto slice never writes it, and the 2017/2025 maps
123    /// have no verified FQNs. The FULL-return filler refuses on `None` — an unnamed 8949 is not filable
124    /// (Fable P6 r1 I3).
125    #[serde(default)]
126    pub identity_page1: Option<IdentityCells>,
127    #[serde(default)]
128    pub identity_page2: Option<IdentityCells>,
129    /// Rows per part per page — **map data**, not a hard-coded constant (a new form revision that
130    /// changes the grid is a data-only edit).
131    pub rows_per_page: usize,
132    /// The data-grid subform token used to re-derive the geometry bands — **per-year map config**,
133    /// not a const (2024 = `Table_Line1`, 2025 = `Table_Line1_Part`; the row fqns differ by year).
134    pub table_token: String,
135    /// Part I then Part II.
136    pub parts: Vec<PartMap>,
137}
138
139impl Form8949Map {
140    /// Parse the committed TOML.
141    pub fn parse(toml_src: &str) -> Result<Self, toml::de::Error> {
142        toml::from_str(toml_src)
143    }
144
145    /// The TY2025 map.
146    pub fn ty2025() -> Self {
147        Self::parse(F8949_MAP_2025).expect("bundled f8949 2025 map parses")
148    }
149
150    /// The TY2024 map.
151    pub fn ty2024() -> Self {
152        Self::parse(F8949_MAP_2024).expect("bundled f8949 2024 map parses")
153    }
154
155    /// The TY2017 map (pre-1099-DA: Box C/F, `/3`; field-identical grid to 2024).
156    pub fn ty2017() -> Self {
157        Self::parse(F8949_MAP_2017).expect("bundled f8949 2017 map parses")
158    }
159
160    /// The map for a supported tax year.
161    pub fn for_year(year: i32) -> Result<Self, FormsError> {
162        match year {
163            2017 => Ok(Self::ty2017()),
164            2024 => Ok(Self::ty2024()),
165            2025 => Ok(Self::ty2025()),
166            _ => Err(FormsError::UnsupportedYear(year)),
167        }
168    }
169
170    /// The part with the given term, if present.
171    pub fn part(&self, term: &str) -> Option<&PartMap> {
172        self.parts.iter().find(|p| p.term == term)
173    }
174}
175
176/// A checkbox choice (field + on-state) — used for the Schedule D QOF Yes/No answer and the Form 1040
177/// Digital-Asset Yes/No question.
178#[derive(Debug, Clone, Deserialize)]
179pub struct CheckChoice {
180    /// The checkbox field name.
181    pub field: String,
182    /// On-state PDF name (without leading `/`).
183    pub on: String,
184}
185
186/// A dollars-field + cents-field PAIR (the 2017 Schedule SE / Form 1040 / Form 8283 split every money
187/// amount into a whole-dollars field and a 2-digit cents field). The geometric oracle treats the pair
188/// as ONE logical cell **at the dollars-field geometry** (the cents field rides along as an authorized
189/// but geometry-exempt write). Because both fields descend from the same AcroForm root, `merge_copies`
190/// (which renames only the root `/T`) rewrites BOTH names as a unit — so overflow is safe.
191#[derive(Debug, Clone, Deserialize)]
192pub struct MoneyPair {
193    /// The whole-dollars field (the one the geometry oracle checks — column-x + row/descent).
194    pub dollars_field: String,
195    /// The 2-digit cents field (an authorized write; NOT independently geometry-checked).
196    pub cents_field: String,
197}
198
199/// A monetary cell: a single field carrying the whole formatted amount (2024/2025), or a
200/// dollars+cents [`MoneyPair`] (the 2017 forms). Deserializes untagged: a TOML **string** →
201/// [`MoneyCell::Single`]; a TOML **inline table** `{ dollars_field, cents_field }` →
202/// [`MoneyCell::Pair`].
203#[derive(Debug, Clone, Deserialize)]
204#[serde(untagged)]
205pub enum MoneyCell {
206    /// A single field holding the whole formatted amount.
207    Single(String),
208    /// A dollars-field + cents-field pair.
209    Pair(MoneyPair),
210}
211
212impl MoneyCell {
213    /// Every PDF field this cell targets (1 for a single, 2 for a pair) — for coverage guards.
214    pub fn fields(&self) -> Vec<&str> {
215        match self {
216            MoneyCell::Single(f) => vec![f.as_str()],
217            MoneyCell::Pair(p) => vec![p.dollars_field.as_str(), p.cents_field.as_str()],
218        }
219    }
220}
221
222/// A per-year default: the Digital-Asset question is present unless a year's map says otherwise.
223fn default_da_present() -> bool {
224    true
225}
226
227/// The Form 1040 capital-gains field map for one tax year: the capital-gain amount cell (line 7a in
228/// 2025 / line 7 in 2024 / **line 13** in 2017) + the Digital-Asset question (absent in 2017).
229/// The Form 1040's identity block (P6.2) — dumped and correlated against the printed form, never
230/// extrapolated. The SSN cells here declare `/MaxLen 9` (comb), so they take the nine BARE digits,
231/// while every schedule's SSN cell is `/MaxLen 11` and takes the hyphenated form. `push_identity`
232/// reads each cell's capacity rather than assuming either.
233#[derive(Debug, Clone, Deserialize)]
234pub struct Form1040HeaderCells {
235    pub taxpayer_first: String,
236    pub taxpayer_last: String,
237    pub taxpayer_ssn: String,
238    pub spouse_first: String,
239    pub spouse_last: String,
240    pub spouse_ssn: String,
241    pub address_street: String,
242    pub address_apt: String,
243    pub address_city: String,
244    pub address_state: String,
245    pub address_zip: String,
246    /// "If you checked the MFS box, enter the name of your spouse" — written on MFS only.
247    pub mfs_spouse_name: String,
248    /// The signature block's occupation cells (page 2).
249    pub occupation_taxpayer: String,
250    pub occupation_spouse: String,
251    /// The taxpayer's Identity Protection PIN cell (page 2, a 6-character comb). A paper return that
252    /// omits an ISSUED IP PIN is rejected or delayed (ARCH-P6.3a Q7 item 5).
253    pub ip_pin: String,
254    /// The §6096 Presidential Election Campaign boxes.
255    pub presidential_taxpayer: CheckChoice,
256    pub presidential_spouse: CheckChoice,
257    /// "Someone can claim: You / Your spouse as a dependent" — the §63(c)(5) floor's own checkbox.
258    pub claimed_dependent_taxpayer: CheckChoice,
259    pub claimed_dependent_spouse: CheckChoice,
260    /// "Spouse itemizes on a separate return or you were a dual-status alien" — §63(c)(6).
261    pub mfs_spouse_itemizes: CheckChoice,
262    /// ★ The four §63(f) aged/blind boxes. The IRS validates a nonstandard standard deduction by
263    /// COUNTING these, so L12 and this checkbox count must agree or the return fails its own
264    /// arithmetic cross-check (`p6-aged-blind-checkboxes-missing`).
265    pub taxpayer_aged: CheckChoice,
266    pub taxpayer_blind: CheckChoice,
267    pub spouse_aged: CheckChoice,
268    pub spouse_blind: CheckChoice,
269    /// "If more than four dependents, see instructions and check here" — v1 REFUSES instead (the
270    /// continuation statement is a synthetic page generator we do not have; same posture as Schedule
271    /// B's >14-payer refusal, SPEC §7.4 as amended). Mapped so the refusal can name the cell it will
272    /// not fill.
273    pub more_than_four_dependents: CheckChoice,
274    /// The four dependents rows the form physically has.
275    pub dependent_rows: Vec<DependentRowCells>,
276}
277
278/// One row of the 1040's dependents table. The name is a SINGLE cell spanning the printed
279/// "(1) First name / Last name" columns — the form has one widget there, not two.
280#[derive(Debug, Clone, Deserialize)]
281pub struct DependentRowCells {
282    pub name: String,
283    pub ssn: String,
284    pub relationship: String,
285    /// The Child-Tax-Credit box. NEVER checked: v1 omits CTC/ODC entirely (1040 L19 = 0, with the
286    /// `CtcOdcOmitted` advisory), and a checked credit box beside a zero credit is a form
287    /// contradicting itself. Mapped so the no-unmapped oracle knows the cell exists and is DELIBERATELY
288    /// left blank.
289    pub ctc: CheckChoice,
290    /// The Credit-for-Other-Dependents box. Never checked, same reason.
291    pub odc: CheckChoice,
292}
293
294#[derive(Debug, Clone, Deserialize)]
295pub struct Form1040Map {
296    /// `"f1040"`.
297    pub form: String,
298    /// Tax year.
299    pub year: i32,
300    /// The full-return identity BLOCK (P6.2). The 1040's header is not two cells like a schedule's: it
301    /// is names + SSNs + address + the §63(f) aged/blind checkboxes + the dependents table. `Option`
302    /// because this map is SHARED with the crypto slice, whose 2017/2025 editions have no verified
303    /// header FQNs; the FULL-return filler refuses on `None` rather than emit an unnamed 1040.
304    #[serde(default)]
305    pub header: Option<Form1040HeaderCells>,
306    /// The capital-gain amount cell (line 7a for 2025, line 7 for 2024, **line 13 for 2017**). A
307    /// single field on 2024/2025; a dollars+cents [`MoneyPair`] on the 2017 form.
308    pub line7a: MoneyCell,
309    /// Whether this year's 1040 carries the Digital-Asset question — **per-year scaffolding**. When
310    /// `true` (2024/2025) the fill answers it "Yes" and runs the map-independent adjacency guard;
311    /// **2017 sets it `false`** (no DA question — the map omits `da_yes`/`da_no` and the fill produces
312    /// the 1040 iff there is reportable capital activity).
313    #[serde(default = "default_da_present")]
314    pub da_present: bool,
315    /// Digital-Asset question "Yes" (LEFT member of the adjacent pair, on-state `/1`). `None` when the
316    /// year's 1040 has no DA question (2017).
317    #[serde(default)]
318    pub da_yes: Option<CheckChoice>,
319    /// Digital-Asset question "No" (right member, on-state `/2`) — never checked by btctax. `None`
320    /// when the year's 1040 has no DA question (2017).
321    #[serde(default)]
322    pub da_no: Option<CheckChoice>,
323
324    // ── Full-return extension (P6). Absent from the 2017/2025 maps, hence optional. ───────────
325    /// L1a — Σ W-2 box 1. AMOUNT column. Full-return only.
326    #[serde(default)]
327    pub line1a: Option<MoneyCell>,
328    /// L2a — tax-exempt interest. SUBLINE column. Full-return only (absent from the 2017/2025 maps).
329    #[serde(default)]
330    pub line2a: Option<MoneyCell>,
331    /// L1z — wages. AMOUNT column.
332    #[serde(default)]
333    pub line1z: Option<MoneyCell>,
334    /// L2b — taxable interest. AMOUNT column.
335    #[serde(default)]
336    pub line2b: Option<MoneyCell>,
337    /// L3a — qualified dividends. **SUBLINE column** (x ≈ [252,324]), not MID or AMOUNT.
338    #[serde(default)]
339    pub line3a: Option<MoneyCell>,
340    /// L3b — ordinary dividends. AMOUNT column.
341    #[serde(default)]
342    pub line3b: Option<MoneyCell>,
343    /// L8 — Schedule 1's printed L10.
344    #[serde(default)]
345    pub line8: Option<MoneyCell>,
346    /// L9 — total income.
347    #[serde(default)]
348    pub line9: Option<MoneyCell>,
349    /// L10 — Schedule 1's printed L26.
350    #[serde(default)]
351    pub line10: Option<MoneyCell>,
352    /// L11 — AGI.
353    #[serde(default)]
354    pub line11: Option<MoneyCell>,
355    /// L12 — the deduction claimed. **★ `f1_57` on the 2024 form is L12; on the 2025 form the same
356    /// field name is L1z** (SPEC §7.4). Per-(form, year) maps exist for exactly this.
357    #[serde(default)]
358    pub line12: Option<MoneyCell>,
359    /// L13 — Form 8995's printed L15 (QBI).
360    #[serde(default)]
361    pub line13: Option<MoneyCell>,
362    /// L14 — 12 + 13.
363    #[serde(default)]
364    pub line14: Option<MoneyCell>,
365    /// L15 — taxable income.
366    #[serde(default)]
367    pub line15: Option<MoneyCell>,
368    /// L16 — tax.
369    #[serde(default)]
370    pub line16: Option<MoneyCell>,
371    /// L17 — Schedule 2's printed L3 (always 0 in v1).
372    #[serde(default)]
373    pub line17: Option<MoneyCell>,
374    /// L18 — 16 + 17.
375    #[serde(default)]
376    pub line18: Option<MoneyCell>,
377    /// L19 — CTC/ODC (always 0 — a §3.4 conservative omission).
378    #[serde(default)]
379    pub line19: Option<MoneyCell>,
380    /// L20 — Schedule 3's printed L8.
381    #[serde(default)]
382    pub line20: Option<MoneyCell>,
383    /// L21 — 19 + 20.
384    #[serde(default)]
385    pub line21: Option<MoneyCell>,
386    /// L22 — 18 − 21.
387    #[serde(default)]
388    pub line22: Option<MoneyCell>,
389    /// L23 — Schedule 2's printed L21.
390    #[serde(default)]
391    pub line23: Option<MoneyCell>,
392    /// L24 — TOTAL TAX.
393    #[serde(default)]
394    pub line24: Option<MoneyCell>,
395    /// L25a — W-2 withholding. MID column.
396    #[serde(default)]
397    pub line25a: Option<MoneyCell>,
398    /// L25b — 1099 withholding. MID column.
399    #[serde(default)]
400    pub line25b: Option<MoneyCell>,
401    /// L25c — other withholding (Form 8959's printed L24). MID column.
402    #[serde(default)]
403    pub line25c: Option<MoneyCell>,
404    /// L25d — 25a + 25b + 25c.
405    #[serde(default)]
406    pub line25d: Option<MoneyCell>,
407    /// L26 — estimated tax payments.
408    #[serde(default)]
409    pub line26: Option<MoneyCell>,
410    /// L31 — Schedule 3's printed L15. MID column.
411    #[serde(default)]
412    pub line31: Option<MoneyCell>,
413    /// L32 — total other payments.
414    #[serde(default)]
415    pub line32: Option<MoneyCell>,
416    /// L33 — TOTAL PAYMENTS.
417    #[serde(default)]
418    pub line33: Option<MoneyCell>,
419    /// L34 — overpayment.
420    #[serde(default)]
421    pub line34: Option<MoneyCell>,
422    /// L35a — refunded to you.
423    #[serde(default)]
424    pub line35a: Option<MoneyCell>,
425    /// L37 — amount you owe.
426    #[serde(default)]
427    pub line37: Option<MoneyCell>,
428    /// The 5-way filing-status checkbox group.
429    #[serde(default)]
430    pub filing_status: Option<FilingStatusBoxes>,
431}
432
433/// The 1040's **5-way filing-status checkbox group**.
434///
435/// **★ The leaf field names COLLIDE.** Two distinct fields are both called `c1_3[0]` and two are both
436/// called `c1_3[1]`, distinguished only by their parent subform:
437///
438/// | status | fully-qualified name | on-state |
439/// |---|---|---|
440/// | Single | `…FilingStatus_ReadOrder[0].c1_3[0]` | `1` |
441/// | HoH | `…Page1[0].c1_3[0]` (no wrapper!) | `2` |
442/// | MFJ | `…FilingStatus_ReadOrder[0].c1_3[1]` | `3` |
443/// | MFS | `…FilingStatus_ReadOrder[0].c1_3[2]` | `4` |
444/// | QSS | `…Page1[0].c1_3[1]` (no wrapper!) | `5` |
445///
446/// A map keyed on the leaf name would silently check the WRONG FILING STATUS — which changes the
447/// standard deduction, every bracket, and every threshold on the return. The on-states are distinct
448/// and independently corroborate the mapping, so the filler asserts both.
449#[derive(Debug, Clone, Deserialize)]
450pub struct FilingStatusBoxes {
451    /// Single — on-state `1`.
452    pub single: CheckChoice,
453    /// Head of household — on-state `2`.
454    pub hoh: CheckChoice,
455    /// Married filing jointly — on-state `3`.
456    pub mfj: CheckChoice,
457    /// Married filing separately — on-state `4`.
458    pub mfs: CheckChoice,
459    /// Qualifying surviving spouse — on-state `5`.
460    pub qss: CheckChoice,
461}
462
463impl Form1040Map {
464    /// Parse the committed TOML.
465    pub fn parse(toml_src: &str) -> Result<Self, toml::de::Error> {
466        toml::from_str(toml_src)
467    }
468
469    /// The TY2025 map.
470    pub fn ty2025() -> Self {
471        Self::parse(F1040_MAP_2025).expect("bundled f1040 2025 map parses")
472    }
473
474    /// The TY2024 map.
475    pub fn ty2024() -> Self {
476        Self::parse(F1040_MAP_2024).expect("bundled f1040 2024 map parses")
477    }
478
479    /// The TY2017 map (capital gain on line 13; NO Digital-Asset question).
480    pub fn ty2017() -> Self {
481        Self::parse(F1040_MAP_2017).expect("bundled f1040 2017 map parses")
482    }
483
484    /// The map for a supported tax year.
485    pub fn for_year(year: i32) -> Result<Self, FormsError> {
486        match year {
487            2017 => Ok(Self::ty2017()),
488            2024 => Ok(Self::ty2024()),
489            2025 => Ok(Self::ty2025()),
490            _ => Err(FormsError::UnsupportedYear(year)),
491        }
492    }
493}
494
495/// One Form 8283 **Section A** row (Donated Property of $5,000 or Less): the 8 filled columns.
496#[derive(Debug, Clone, Deserialize)]
497pub struct Section8283ARow {
498    /// (a) Name and address of the donee organization.
499    pub donee: String,
500    /// (c) Description and condition of donated property.
501    pub desc: String,
502    /// (d) Date of the contribution (full date).
503    pub date_contrib: String,
504    /// (e) Date acquired by donor (mo., yr.).
505    pub date_acq: String,
506    /// (f) How acquired by donor.
507    pub how: String,
508    /// (g) Donor's cost or adjusted basis (money — a [`MoneyPair`] on the 2017 Rev. 12-2014 form).
509    pub cost: MoneyCell,
510    /// (h) Fair market value (money — a [`MoneyPair`] on the 2017 form).
511    pub fmv: MoneyCell,
512    /// (i) Method used to determine the FMV.
513    pub method: String,
514}
515
516/// Form 8283 Section A (page 1, Line 1) — up to 4 rows A–D.
517#[derive(Debug, Clone, Deserialize)]
518pub struct Section8283A {
519    /// The 4 rows A–D.
520    pub rows: Vec<Section8283ARow>,
521}
522
523/// One Form 8283 **Section B Part I** row (Over $5,000): the filled columns.
524#[derive(Debug, Clone, Deserialize)]
525pub struct Section8283BRow {
526    /// (a) Description of donated property.
527    pub desc: String,
528    /// (c) Appraised fair market value (money — a [`MoneyPair`] on the 2017 Rev. 12-2014 form).
529    pub fmv: MoneyCell,
530    /// (d) Date acquired by donor (mo., yr.).
531    pub date_acq: String,
532    /// (e) How acquired by donor.
533    pub how: String,
534    /// (f) Donor's cost or adjusted basis (money — a [`MoneyPair`] on the 2017 form).
535    pub cost: MoneyCell,
536    /// (i)/(h) Amount claimed as a deduction (carrier row only; money — a [`MoneyPair`] on 2017).
537    pub deduction: MoneyCell,
538}
539
540/// Form 8283 Section B (page 1/2, over-$5,000 property + page 2 identity) — up to 3 rows (2024/2025)
541/// or 4 rows (2017 Rev. 12-2014, `Line5A`–`Line5D`).
542#[derive(Debug, Clone, Deserialize)]
543pub struct Section8283B {
544    /// The property-type checkbox MUST be checked for BTC: **"k Digital assets"** (on-state `/11`) on
545    /// the Rev. 12-2023/2025 forms; the Rev. 12-2014 form has no digital-asset box, so 2017 uses
546    /// **"j Other"** (on-state `/9`) plus [`Self::btc_property_note`].
547    pub k_digital_assets: CheckChoice,
548    /// 2017 only: since "j Other" gives no category, the digital-asset nature is identified by a
549    /// printed note **prepended to the first row's (a) description** (e.g. "Other property: digital
550    /// asset (virtual currency)"). `None` on 2024/2025 ("k Digital assets" is self-describing).
551    #[serde(default)]
552    pub btc_property_note: Option<String>,
553    /// Part IV/III appraiser name (page 2). `None` when the revision has no printed-name field (the
554    /// Rev. 12-2014 form: the appraiser identity is the handwritten signature, left blank).
555    #[serde(default)]
556    pub appraiser_name: Option<String>,
557    /// Appraiser business address (page 2).
558    pub appraiser_address: String,
559    /// Appraiser identifying number (TIN/PTIN, page 2).
560    pub appraiser_tin: String,
561    /// Donee organization name (page 2).
562    pub donee_name: String,
563    /// Donee EIN (page 2).
564    pub donee_ein: String,
565    /// Donee address (page 2).
566    pub donee_address: String,
567    /// The rows (3 on 2024/2025, 4 on 2017) — the row count also sets the per-copy overflow cap.
568    pub rows: Vec<Section8283BRow>,
569}
570
571/// The Form 8283 (Rev. 12-2025) field map for one tax year.
572#[derive(Debug, Clone, Deserialize)]
573pub struct Form8283Map {
574    /// `"f8283"`.
575    pub form: String,
576    /// Tax year.
577    pub year: i32,
578    /// The FILER's identity — "Name(s) shown on your income tax return" + identifying number. `Option`
579    /// because the crypto slice never writes it (its 8283 rides beside a return btctax did not produce)
580    /// and the 2017/2025 maps have no verified FQNs; the FULL-return filler refuses on `None`.
581    #[serde(default)]
582    pub identity: Option<IdentityCells>,
583    /// Section A (≤ $5,000).
584    pub section_a: Section8283A,
585    /// Section B (> $5,000).
586    pub section_b: Section8283B,
587}
588
589impl Form8283Map {
590    /// Parse the committed TOML.
591    pub fn parse(toml_src: &str) -> Result<Self, toml::de::Error> {
592        toml::from_str(toml_src)
593    }
594
595    /// The TY2025 map.
596    pub fn ty2025() -> Self {
597        Self::parse(F8283_MAP_2025).expect("bundled f8283 2025 map parses")
598    }
599
600    /// The TY2024 map (Form 8283 Rev. 12-2023).
601    pub fn ty2024() -> Self {
602        Self::parse(F8283_MAP_2024).expect("bundled f8283 2024 map parses")
603    }
604
605    /// The TY2017 map (Form 8283 Rev. 12-2014 — "j Other", no DA box, 5/4 rows, ¢-pairs).
606    pub fn ty2017() -> Self {
607        Self::parse(F8283_MAP_2017).expect("bundled f8283 2017 map parses")
608    }
609
610    /// The map for a supported tax year.
611    pub fn for_year(year: i32) -> Result<Self, FormsError> {
612        match year {
613            2017 => Ok(Self::ty2017()),
614            2024 => Ok(Self::ty2024()),
615            2025 => Ok(Self::ty2025()),
616            _ => Err(FormsError::UnsupportedYear(year)),
617        }
618    }
619
620    /// Every field name the map targets (for the `map_YYYY_matches_bundled_pdf_fieldset` guard).
621    pub fn field_names(&self) -> Vec<&str> {
622        let mut v = Vec::new();
623        for r in &self.section_a.rows {
624            v.extend([
625                r.donee.as_str(),
626                r.desc.as_str(),
627                r.date_contrib.as_str(),
628                r.date_acq.as_str(),
629                r.how.as_str(),
630            ]);
631            v.extend(r.cost.fields());
632            v.extend(r.fmv.fields());
633            v.push(r.method.as_str());
634        }
635        let b = &self.section_b;
636        v.push(b.k_digital_assets.field.as_str());
637        if let Some(n) = &b.appraiser_name {
638            v.push(n.as_str());
639        }
640        v.extend([
641            b.appraiser_address.as_str(),
642            b.appraiser_tin.as_str(),
643            b.donee_name.as_str(),
644            b.donee_ein.as_str(),
645            b.donee_address.as_str(),
646        ]);
647        for r in &b.rows {
648            v.extend([r.desc.as_str(), r.date_acq.as_str(), r.how.as_str()]);
649            v.extend(r.fmv.fields());
650            v.extend(r.cost.fields());
651            v.extend(r.deduction.fields());
652        }
653        v
654    }
655}
656
657/// The Schedule D field map for one tax year.
658#[derive(Debug, Clone, Deserialize)]
659pub struct ScheduleDMap {
660    /// `"schedule_d"`.
661    pub form: String,
662    /// Tax year.
663    pub year: i32,
664    /// The name + SSN header cells (P6.2). `Option` because this map is SHARED with the crypto-slice
665    /// path, whose 2017/2025 editions have no verified identity FQNs and no `ReturnInputs` to source an
666    /// identity from. The FULL-return filler refuses on `None` — it may not emit an unnamed form.
667    #[serde(default)]
668    pub identity: Option<IdentityCells>,
669    /// Line 3 — Part I total from Form 8949 (Box C **or Box I**): columns d,e,g,h.
670    pub line3: AmountCols,
671    /// Line 7 — net short-term gain/loss (column h).
672    pub line7_h: String,
673    /// Line 10 — Part II total from Form 8949 (Box F **or Box L**): columns d,e,g,h.
674    pub line10: AmountCols,
675    /// Line 15 — net long-term gain/loss (column h).
676    pub line15_h: String,
677    /// Line 16 — total (line 7 + line 15), column h, page 2.
678    pub line16_h: String,
679    /// L6 — short-term capital loss carryover. **PAREN box ⇒ positive magnitude.** Full-return only
680    /// (`None` on the 2017/2025 maps, which serve the crypto-slice fill).
681    #[serde(default)]
682    pub line6: Option<MoneyCell>,
683    /// L13 — capital gain distributions (Σ 1099-DIV box 2a). Full-return only.
684    #[serde(default)]
685    pub line13: Option<MoneyCell>,
686    /// L14 — long-term capital loss carryover. **PAREN box ⇒ positive magnitude.** Full-return only.
687    #[serde(default)]
688    pub line14: Option<MoneyCell>,
689    /// L18 — 28%-Rate Gain Worksheet (always 0; a nonzero amount is refused upstream). Full-return only.
690    #[serde(default)]
691    pub line18: Option<MoneyCell>,
692    /// L19 — Unrecaptured §1250 Gain Worksheet (always 0; refused upstream). Full-return only.
693    #[serde(default)]
694    pub line19: Option<MoneyCell>,
695    /// L21 — the §1211(b) allowed loss offset. **PAREN box ⇒ positive magnitude.** Full-return only.
696    #[serde(default)]
697    pub line21: Option<MoneyCell>,
698    /// L17 — "Are lines 15 and 16 both gains?" Full-return only.
699    #[serde(default)]
700    pub line17: Option<YesNoPair>,
701    /// L20 — "Are lines 18 and 19 both zero or blank…?" Full-return only.
702    #[serde(default)]
703    pub line20: Option<YesNoPair>,
704    /// L22 — "Do you have qualified dividends on Form 1040, line 3a?" Full-return only.
705    #[serde(default)]
706    pub line22: Option<YesNoPair>,
707    /// The Part I amount-column subform token used to re-derive the geometry bands — **per-year map
708    /// config** (`Table_PartI` for 2024/2025, **`TablePartI`** (no underscore) for the 2017 form).
709    #[serde(default = "default_sched_d_token")]
710    pub table_token: String,
711    /// QOF question "Yes" choice. `None` on years whose Schedule D has no QOF question (2017 —
712    /// Qualified Opportunity Funds began in 2019).
713    #[serde(default)]
714    pub qof_yes: Option<CheckChoice>,
715    /// QOF question "No" choice (answered No when present). `None` on 2017 (no QOF question).
716    #[serde(default)]
717    pub qof_no: Option<CheckChoice>,
718}
719
720/// The default Schedule D Part I grid token (2024/2025); the 2017 map overrides it to `TablePartI`.
721fn default_sched_d_token() -> String {
722    "Table_PartI".to_string()
723}
724
725impl ScheduleDMap {
726    /// Parse the committed TOML.
727    pub fn parse(toml_src: &str) -> Result<Self, toml::de::Error> {
728        toml::from_str(toml_src)
729    }
730
731    /// The TY2025 map.
732    pub fn ty2025() -> Self {
733        Self::parse(SCHEDULE_D_MAP_2025).expect("bundled schedule_d 2025 map parses")
734    }
735
736    /// The TY2024 map.
737    pub fn ty2024() -> Self {
738        Self::parse(SCHEDULE_D_MAP_2024).expect("bundled schedule_d 2024 map parses")
739    }
740
741    /// The TY2017 map (grid token `TablePartI`; NO QOF question).
742    pub fn ty2017() -> Self {
743        Self::parse(SCHEDULE_D_MAP_2017).expect("bundled schedule_d 2017 map parses")
744    }
745
746    /// The map for a supported tax year.
747    pub fn for_year(year: i32) -> Result<Self, FormsError> {
748        match year {
749            2017 => Ok(Self::ty2017()),
750            2024 => Ok(Self::ty2024()),
751            2025 => Ok(Self::ty2025()),
752            _ => Err(FormsError::UnsupportedYear(year)),
753        }
754    }
755}
756
757/// The Form 8959 (Additional Medicare Tax) field map for one tax year.
758///
759/// Only the lines we FILL are mapped. Lines 2/3 (Form 4137 / Form 8919) and all of Part III plus
760/// line 23 (RRTA) are unmodeled and are deliberately absent — they stay blank on the filed form,
761/// which is why line 4 = line 1, line 18 = 7 + 13, and line 24 = line 22.
762#[derive(Debug, Clone, Deserialize)]
763pub struct Form8959Map {
764    /// `"f8959"`.
765    pub form: String,
766    /// Tax year.
767    pub year: i32,
768    /// The name + SSN header cells (P6.2). REQUIRED: a full-return schedule that does not name its
769    /// taxpayer is not a filable form, so a map lacking `[identity]` fails at deserialization.
770    pub identity: IdentityCells,
771    /// L1 — Σ W-2 box 5 Medicare wages, MID column.
772    pub line1: MoneyCell,
773    /// L4 — add lines 1–3 (2/3 blank ⇒ = line 1), MID column.
774    pub line4: MoneyCell,
775    /// L5 — filing-status threshold, MID column.
776    pub line5: MoneyCell,
777    /// L6 — line 4 − line 5, floored at 0, AMOUNT column.
778    pub line6: MoneyCell,
779    /// L7 — 0.9% × line 6, AMOUNT column.
780    pub line7: MoneyCell,
781    /// L8 — Schedule SE Part I line 6 (net SE earnings), MID column.
782    pub line8: MoneyCell,
783    /// L9 — filing-status threshold (again), MID column.
784    pub line9: MoneyCell,
785    /// L10 — the amount from line 4, MID column.
786    pub line10: MoneyCell,
787    /// L11 — line 9 − line 10, floored at 0, MID column.
788    pub line11: MoneyCell,
789    /// L12 — line 8 − line 11, floored at 0, AMOUNT column.
790    pub line12: MoneyCell,
791    /// L13 — 0.9% × line 12, AMOUNT column.
792    pub line13: MoneyCell,
793    /// L18 — add 7, 13, 17 → Schedule 2 line 11, AMOUNT column.
794    pub line18: MoneyCell,
795    /// L19 — Σ W-2 box 6 Medicare tax withheld, MID column.
796    pub line19: MoneyCell,
797    /// L20 — the amount from line 1, MID column.
798    pub line20: MoneyCell,
799    /// L21 — 1.45% × line 20, MID column.
800    pub line21: MoneyCell,
801    /// L22 — line 19 − line 21, floored at 0, AMOUNT column.
802    pub line22: MoneyCell,
803    /// L24 — add 22 and 23 → 1040 line 25c, AMOUNT column.
804    pub line24: MoneyCell,
805}
806
807impl Form8959Map {
808    /// Parse the committed TOML.
809    pub fn parse(toml_src: &str) -> Result<Self, toml::de::Error> {
810        toml::from_str(toml_src)
811    }
812
813    /// The TY2024 map.
814    pub fn ty2024() -> Self {
815        Self::parse(F8959_MAP_2024).expect("bundled f8959 2024 map parses")
816    }
817
818    /// The map for a supported tax year. Full-return v1 is **TY2024-only**: Form 8959 is reachable
819    /// only from the absolute return, which itself has tables for 2024 alone.
820    pub fn for_year(year: i32) -> Result<Self, FormsError> {
821        match year {
822            2024 => Ok(Self::ty2024()),
823            _ => Err(FormsError::UnsupportedYear(year)),
824        }
825    }
826
827    /// The 17 filled cells, in **printed reading order** (strictly descending y on page 1) — the
828    /// order `fill_form_8959` walks and the ordinal the geometric verifier checks the descent of.
829    pub fn lines(&self) -> [&MoneyCell; 17] {
830        [
831            &self.line1,
832            &self.line4,
833            &self.line5,
834            &self.line6,
835            &self.line7,
836            &self.line8,
837            &self.line9,
838            &self.line10,
839            &self.line11,
840            &self.line12,
841            &self.line13,
842            &self.line18,
843            &self.line19,
844            &self.line20,
845            &self.line21,
846            &self.line22,
847            &self.line24,
848        ]
849    }
850}
851
852/// The Form 8960 (Net Investment Income Tax) field map for one tax year.
853///
854/// Only the lines v1 FILLS are mapped. Annuities (3), Schedule E (4a–4c), CFC/PFIC (6), investment
855/// expenses (9a–9c, 10) and the whole estates-and-trusts branch (18a–21) are unmodeled and stay
856/// BLANK. The derived totals 9d and 11 ARE filled at zero — the form's arithmetic adds them.
857#[derive(Debug, Clone, Deserialize)]
858pub struct Form8960Map {
859    /// `"f8960"`.
860    pub form: String,
861    /// Tax year.
862    pub year: i32,
863    /// The name + SSN header cells (P6.2). REQUIRED: a full-return schedule that does not name its
864    /// taxpayer is not a filable form, so a map lacking `[identity]` fails at deserialization.
865    pub identity: IdentityCells,
866    /// L1 — taxable interest, AMOUNT column.
867    pub line1: MoneyCell,
868    /// L2 — ordinary dividends, AMOUNT column.
869    pub line2: MoneyCell,
870    /// L5a — net gain/loss from disposition of property, MID column.
871    pub line5a: MoneyCell,
872    /// L5d — combine 5a–5c, AMOUNT column.
873    pub line5d: MoneyCell,
874    /// L7 — other modifications, AMOUNT column.
875    pub line7: MoneyCell,
876    /// L8 — total investment income, AMOUNT column.
877    pub line8: MoneyCell,
878    /// L9d — add 9a/9b/9c (zero in v1), AMOUNT column.
879    pub line9d: MoneyCell,
880    /// L11 — total deductions and modifications (zero in v1), AMOUNT column.
881    pub line11: MoneyCell,
882    /// L12 — net investment income, AMOUNT column.
883    pub line12: MoneyCell,
884    /// L13 — modified AGI, MID column.
885    pub line13: MoneyCell,
886    /// L14 — the §1411(b) threshold (fillable, NOT pre-printed), MID column.
887    pub line14: MoneyCell,
888    /// L15 — 13 − 14, floored, MID column.
889    pub line15: MoneyCell,
890    /// L16 — smaller of 12 or 15, AMOUNT column.
891    pub line16: MoneyCell,
892    /// L17 — 3.8% × 16 → Schedule 2 line 12, AMOUNT column.
893    pub line17: MoneyCell,
894}
895
896impl Form8960Map {
897    /// Parse the committed TOML.
898    pub fn parse(toml_src: &str) -> Result<Self, toml::de::Error> {
899        toml::from_str(toml_src)
900    }
901    /// The TY2024 map.
902    pub fn ty2024() -> Self {
903        Self::parse(F8960_MAP_2024).expect("bundled f8960 2024 map parses")
904    }
905    /// The map for a supported tax year. Full-return v1 is TY2024-only.
906    pub fn for_year(year: i32) -> Result<Self, FormsError> {
907        match year {
908            2024 => Ok(Self::ty2024()),
909            _ => Err(FormsError::UnsupportedYear(year)),
910        }
911    }
912    /// The 14 filled cells in printed reading order (strictly descending y on page 1).
913    pub fn lines(&self) -> [&MoneyCell; 14] {
914        [
915            &self.line1,
916            &self.line2,
917            &self.line5a,
918            &self.line5d,
919            &self.line7,
920            &self.line8,
921            &self.line9d,
922            &self.line11,
923            &self.line12,
924            &self.line13,
925            &self.line14,
926            &self.line15,
927            &self.line16,
928            &self.line17,
929        ]
930    }
931}
932
933/// The Form 8995 (QBI deduction, simplified) field map for one tax year.
934///
935/// The Part I trade/business table (rows 1i–1v) and line 3 are deliberately unmapped: v1's only QBI
936/// is §199A REIT dividends, so there is no business to list. Lines 2/4/5 ARE filled, at zero.
937///
938/// **Lines 7, 16 and 17 are PARENTHESIZED boxes — the form prints the minus sign, so the value must
939/// be a POSITIVE MAGNITUDE.** `qbi::Form8995Lines` guarantees that.
940#[derive(Debug, Clone, Deserialize)]
941pub struct Form8995Map {
942    /// `"f8995"`.
943    pub form: String,
944    /// Tax year.
945    pub year: i32,
946    /// The name + SSN header cells (P6.2). REQUIRED: a full-return schedule that does not name its
947    /// taxpayer is not a filable form, so a map lacking `[identity]` fails at deserialization.
948    pub identity: IdentityCells,
949    /// Part I row 1i(a) — the trade or business's description.
950    pub row1_business: MoneyCell,
951    /// Part I row 1i(b) — its TIN (the filer's SSN; `/MaxLen` 11 ⇒ hyphenated).
952    pub row1_tin: MoneyCell,
953    /// Part I row 1i(c) — its QBI. With one business this IS line 2, which the form totals from it.
954    pub row1_qbi: MoneyCell,
955    /// L2 — total QBI: "Combine lines 1i through 1v, column (c)". MID column.
956    pub line2: MoneyCell,
957    /// L4 — combine 2 and 3, MID column.
958    pub line4: MoneyCell,
959    /// L5 — QBI component (20% × 4), AMOUNT column.
960    pub line5: MoneyCell,
961    /// L6 — qualified REIT dividends + PTP income, MID column.
962    pub line6: MoneyCell,
963    /// L7 — prior-year REIT/PTP loss carryforward, MID column. ★ positive magnitude (paren box).
964    pub line7: MoneyCell,
965    /// L8 — combine 6 and 7, MID column.
966    pub line8: MoneyCell,
967    /// L9 — REIT/PTP component (20% × 8), AMOUNT column.
968    pub line9: MoneyCell,
969    /// L10 — add 5 and 9, AMOUNT column.
970    pub line10: MoneyCell,
971    /// L11 — taxable income before the QBI deduction, MID column.
972    pub line11: MoneyCell,
973    /// L12 — net capital gain + qualified dividends, MID column.
974    pub line12: MoneyCell,
975    /// L13 — 11 − 12, floored, MID column.
976    pub line13: MoneyCell,
977    /// L14 — income limitation (20% × 13), AMOUNT column.
978    pub line14: MoneyCell,
979    /// L15 — the deduction: smaller of 10 or 14 → 1040 L13, AMOUNT column.
980    pub line15: MoneyCell,
981    /// L16 — total QB (loss) carryforward, AMOUNT column. ★ positive magnitude (paren box).
982    pub line16: MoneyCell,
983    /// L17 — total REIT/PTP (loss) carryforward, AMOUNT column. ★ positive magnitude (paren box).
984    pub line17: MoneyCell,
985}
986
987impl Form8995Map {
988    /// Parse the committed TOML.
989    pub fn parse(toml_src: &str) -> Result<Self, toml::de::Error> {
990        toml::from_str(toml_src)
991    }
992    /// The TY2024 map.
993    pub fn ty2024() -> Self {
994        Self::parse(F8995_MAP_2024).expect("bundled f8995 2024 map parses")
995    }
996    /// The map for a supported tax year. Full-return v1 is TY2024-only.
997    pub fn for_year(year: i32) -> Result<Self, FormsError> {
998        match year {
999            2024 => Ok(Self::ty2024()),
1000            _ => Err(FormsError::UnsupportedYear(year)),
1001        }
1002    }
1003    /// The 15 filled cells in printed reading order (strictly descending y on page 1).
1004    pub fn lines(&self) -> [&MoneyCell; 15] {
1005        [
1006            &self.line2,
1007            &self.line4,
1008            &self.line5,
1009            &self.line6,
1010            &self.line7,
1011            &self.line8,
1012            &self.line9,
1013            &self.line10,
1014            &self.line11,
1015            &self.line12,
1016            &self.line13,
1017            &self.line14,
1018            &self.line15,
1019            &self.line16,
1020            &self.line17,
1021        ]
1022    }
1023}
1024
1025/// The Schedule 2 (Additional Taxes) field map for one tax year.
1026///
1027/// Part I is entirely absent: line 1a (excess APTC) has no input and would refuse if it did, and
1028/// line 2 (AMT) is $0 by construction (the return is refused if the Form 6251 screen trips). Only
1029/// the three Part II taxes v1 computes are mapped. **Line 21 is on PAGE 2.**
1030#[derive(Debug, Clone, Deserialize)]
1031pub struct Schedule2Map {
1032    /// `"f1040s2"`.
1033    pub form: String,
1034    /// Tax year.
1035    pub year: i32,
1036    /// The name + SSN header cells (P6.2). REQUIRED: a full-return schedule that does not name its
1037    /// taxpayer is not a filable form, so a map lacking `[identity]` fails at deserialization.
1038    pub identity: IdentityCells,
1039    /// L4 — self-employment tax (SS + regular Medicare only), AMOUNT column, page 1.
1040    pub line4: MoneyCell,
1041    /// L11 — Additional Medicare Tax (Form 8959's printed L18), AMOUNT column, page 1.
1042    pub line11: MoneyCell,
1043    /// L12 — net investment income tax (Form 8960's printed L17), AMOUNT column, page 1.
1044    pub line12: MoneyCell,
1045    /// L21 — total other taxes → 1040 L23, AMOUNT column, **page 2**.
1046    pub line21: MoneyCell,
1047}
1048
1049impl Schedule2Map {
1050    /// Parse the committed TOML.
1051    pub fn parse(toml_src: &str) -> Result<Self, toml::de::Error> {
1052        toml::from_str(toml_src)
1053    }
1054    /// The TY2024 map.
1055    pub fn ty2024() -> Self {
1056        Self::parse(SCHEDULE_2_MAP_2024).expect("bundled schedule 2 2024 map parses")
1057    }
1058    /// The map for a supported tax year. Full-return v1 is TY2024-only.
1059    pub fn for_year(year: i32) -> Result<Self, FormsError> {
1060        match year {
1061            2024 => Ok(Self::ty2024()),
1062            _ => Err(FormsError::UnsupportedYear(year)),
1063        }
1064    }
1065    /// The 4 filled cells in printed reading order. **Descent is grouped by PAGE** — line 21 sits on
1066    /// page 2, whose y-coordinates are not comparable with page 1's.
1067    pub fn lines(&self) -> [&MoneyCell; 4] {
1068        [&self.line4, &self.line11, &self.line12, &self.line21]
1069    }
1070}
1071
1072/// The Schedule 3 (Additional Credits and Payments) field map for one tax year.
1073///
1074/// Only the foreign tax credit (L1) and the §6413(c) excess-Social-Security credit (L11) are mapped.
1075/// Every other Part I credit is a §3.4 conservative omission and stays BLANK.
1076#[derive(Debug, Clone, Deserialize)]
1077pub struct Schedule3Map {
1078    /// `"f1040s3"`.
1079    pub form: String,
1080    /// Tax year.
1081    pub year: i32,
1082    /// The name + SSN header cells (P6.2). REQUIRED: a full-return schedule that does not name its
1083    /// taxpayer is not a filable form, so a map lacking `[identity]` fails at deserialization.
1084    pub identity: IdentityCells,
1085    /// L1 — foreign tax credit, AMOUNT column.
1086    pub line1: MoneyCell,
1087    /// L8 — total nonrefundable credits → 1040 L20, AMOUNT column.
1088    pub line8: MoneyCell,
1089    /// L10 — "Amount paid with request for extension to file", AMOUNT column. ★ Its absence made the
1090    /// filed return demand a payment the filer had ALREADY made (Fable ARCH-P6.3a D1).
1091    pub line10: MoneyCell,
1092    /// L11 — excess Social Security / tier-1 RRTA withheld, AMOUNT column.
1093    pub line11: MoneyCell,
1094    /// L15 — total other payments → 1040 L31, AMOUNT column.
1095    pub line15: MoneyCell,
1096}
1097
1098impl Schedule3Map {
1099    /// Parse the committed TOML.
1100    pub fn parse(toml_src: &str) -> Result<Self, toml::de::Error> {
1101        toml::from_str(toml_src)
1102    }
1103    /// The TY2024 map.
1104    pub fn ty2024() -> Self {
1105        Self::parse(SCHEDULE_3_MAP_2024).expect("bundled schedule 3 2024 map parses")
1106    }
1107    /// The map for a supported tax year. Full-return v1 is TY2024-only.
1108    pub fn for_year(year: i32) -> Result<Self, FormsError> {
1109        match year {
1110            2024 => Ok(Self::ty2024()),
1111            _ => Err(FormsError::UnsupportedYear(year)),
1112        }
1113    }
1114    /// The 4 filled cells in printed reading order (strictly descending y on page 1).
1115    pub fn lines(&self) -> [&MoneyCell; 5] {
1116        [
1117            &self.line1,
1118            &self.line8,
1119            &self.line10,
1120            &self.line11,
1121            &self.line15,
1122        ]
1123    }
1124}
1125
1126/// The Schedule A (Itemized Deductions) field map for one tax year.
1127///
1128/// **Three x-clusters** — Schedule A is the only form here that needs a third. Line 2 (the AGI the
1129/// 7.5% medical floor is taken on) sits INLINE with the printed sentence at x ≈ [331,403], not in the
1130/// MID column, and it is the same WIDTH as MID, so nothing but its x-position distinguishes it.
1131///
1132/// Unmapped on purpose: line 6 (other taxes), 8b/8c (mortgage not on a 1098; points), 9 (investment
1133/// interest), 15 (casualty), 16 (other). **Line 8d is a ReadOnly "Reserved for future use" widget** —
1134/// live, and it consumes a suffix number. Never write it.
1135#[derive(Debug, Clone, Deserialize)]
1136pub struct ScheduleAMap {
1137    /// `"f1040sa"`.
1138    pub form: String,
1139    /// Tax year.
1140    pub year: i32,
1141    /// L5a's §164(b)(5) sales-tax election checkbox — the election core already honours in the
1142    /// arithmetic, which the filed form never showed (ARCH-P6.3a Q7 item 3).
1143    pub check_5a_sales_tax: CheckChoice,
1144    /// ★ §2.7 — L8's §163(h)(3)(F) mixed-use-mortgage checkbox: "If you didn't use all of your home
1145    /// mortgage loan(s) to buy, build, or improve your home, check this box." Nested under
1146    /// `Line8_ReadOrder[0]`, like line 18's own read-order box.
1147    pub check_8_mixed_use: CheckChoice,
1148    /// L18's §63(e) "itemize even though less than the standard deduction" checkbox (Q7 item 4).
1149    pub check_18_elects_smaller: CheckChoice,
1150    /// The name + SSN header cells (P6.2). REQUIRED: a full-return schedule that does not name its
1151    /// taxpayer is not a filable form, so a map lacking `[identity]` fails at deserialization.
1152    pub identity: IdentityCells,
1153    /// L1 — medical and dental expenses, MID column.
1154    pub line1: MoneyCell,
1155    /// L2 — AGI. ★ **AGI-INLINE column**, not MID.
1156    pub line2: MoneyCell,
1157    /// L3 — the §213(a) 7.5% floor, MID column.
1158    pub line3: MoneyCell,
1159    /// L4 — medical allowed, AMOUNT column.
1160    pub line4: MoneyCell,
1161    /// L5a — state/local income or sales taxes, MID column.
1162    pub line5a: MoneyCell,
1163    /// L5b — real-estate taxes, MID column.
1164    pub line5b: MoneyCell,
1165    /// L5c — personal-property taxes, MID column.
1166    pub line5c: MoneyCell,
1167    /// L5d — add 5a-5c, MID column.
1168    pub line5d: MoneyCell,
1169    /// L5e — the §164(b) SALT cap, MID column.
1170    pub line5e: MoneyCell,
1171    /// L7 — add 5e and 6, AMOUNT column.
1172    pub line7: MoneyCell,
1173    /// L8a — mortgage interest on Form 1098, MID column.
1174    pub line8a: MoneyCell,
1175    /// L8e — add 8a-8c, MID column.
1176    pub line8e: MoneyCell,
1177    /// L10 — add 8e and 9, AMOUNT column.
1178    pub line10: MoneyCell,
1179    /// L11 — gifts by cash or check, MID column.
1180    pub line11: MoneyCell,
1181    /// L12 — gifts other than cash (incl. crypto), MID column.
1182    pub line12: MoneyCell,
1183    /// L13 — prior-year carryover, MID column.
1184    pub line13: MoneyCell,
1185    /// L14 — add 11-13, AMOUNT column.
1186    pub line14: MoneyCell,
1187    /// L17 — total itemized deductions → 1040 L12, AMOUNT column.
1188    pub line17: MoneyCell,
1189}
1190
1191impl ScheduleAMap {
1192    /// Parse the committed TOML.
1193    pub fn parse(toml_src: &str) -> Result<Self, toml::de::Error> {
1194        toml::from_str(toml_src)
1195    }
1196    /// The TY2024 map.
1197    pub fn ty2024() -> Self {
1198        Self::parse(SCHEDULE_A_MAP_2024).expect("bundled schedule A 2024 map parses")
1199    }
1200    /// The map for a supported tax year. Full-return v1 is TY2024-only.
1201    pub fn for_year(year: i32) -> Result<Self, FormsError> {
1202        match year {
1203            2024 => Ok(Self::ty2024()),
1204            _ => Err(FormsError::UnsupportedYear(year)),
1205        }
1206    }
1207    /// The 18 filled cells in printed reading order (strictly descending y on page 1).
1208    pub fn lines(&self) -> [&MoneyCell; 18] {
1209        [
1210            &self.line1,
1211            &self.line2,
1212            &self.line3,
1213            &self.line4,
1214            &self.line5a,
1215            &self.line5b,
1216            &self.line5c,
1217            &self.line5d,
1218            &self.line5e,
1219            &self.line7,
1220            &self.line8a,
1221            &self.line8e,
1222            &self.line10,
1223            &self.line11,
1224            &self.line12,
1225            &self.line13,
1226            &self.line14,
1227            &self.line17,
1228        ]
1229    }
1230}
1231
1232/// The Schedule 1 (Additional Income and Adjustments to Income) field map for one tax year.
1233///
1234/// Root subform is `form1[0]` (as on Schedule 2), NOT `topmostSubform[0]`. **Two pages** — Part II is
1235/// entirely on page 2, so descent is grouped by page.
1236///
1237/// **Line 22 is a ReadOnly "Reserved for future use" widget** that consumes a suffix number; never
1238/// written. Non-money fields (a date on 2b, an SSN comb on 19b, a date on 19c) sit inside the money
1239/// x-band — writing a dollar amount into one prints garbage.
1240#[derive(Debug, Clone, Deserialize)]
1241pub struct Schedule1Map {
1242    /// `"f1040s1"`.
1243    pub form: String,
1244    /// Tax year.
1245    pub year: i32,
1246    /// The name + SSN header cells (P6.2). REQUIRED: a full-return schedule that does not name its
1247    /// taxpayer is not a filable form, so a map lacking `[identity]` fails at deserialization.
1248    pub identity: IdentityCells,
1249    /// L1 — taxable state/local refund, AMOUNT column, page 1.
1250    pub line1: MoneyCell,
1251    /// L3 — business income (crypto Schedule C net), AMOUNT column, page 1.
1252    pub line3: MoneyCell,
1253    /// L7 — unemployment compensation, AMOUNT column, page 1.
1254    pub line7: MoneyCell,
1255    /// L8v — digital assets received as ordinary income, **MID column**, page 1.
1256    pub line8v: MoneyCell,
1257    /// L9 — total other income, AMOUNT column, page 1.
1258    pub line9: MoneyCell,
1259    /// L10 — combine 1–7 and 9 → 1040 L8, AMOUNT column, page 1.
1260    pub line10: MoneyCell,
1261    /// L15 — deductible part of SE tax, AMOUNT column, **page 2**.
1262    pub line15: MoneyCell,
1263    /// L18 — early-withdrawal penalty, AMOUNT column, page 2.
1264    pub line18: MoneyCell,
1265    /// L21 — student-loan interest deduction, AMOUNT column, page 2.
1266    pub line21: MoneyCell,
1267    /// L26 — total adjustments → 1040 L10, AMOUNT column, page 2.
1268    pub line26: MoneyCell,
1269}
1270
1271impl Schedule1Map {
1272    /// Parse the committed TOML.
1273    pub fn parse(toml_src: &str) -> Result<Self, toml::de::Error> {
1274        toml::from_str(toml_src)
1275    }
1276    /// The TY2024 map.
1277    pub fn ty2024() -> Self {
1278        Self::parse(SCHEDULE_1_MAP_2024).expect("bundled schedule 1 2024 map parses")
1279    }
1280    /// The map for a supported tax year. Full-return v1 is TY2024-only.
1281    pub fn for_year(year: i32) -> Result<Self, FormsError> {
1282        match year {
1283            2024 => Ok(Self::ty2024()),
1284            _ => Err(FormsError::UnsupportedYear(year)),
1285        }
1286    }
1287    /// The 10 filled cells in printed reading order. **Descent is grouped by PAGE.**
1288    pub fn lines(&self) -> [&MoneyCell; 10] {
1289        [
1290            &self.line1,
1291            &self.line3,
1292            &self.line7,
1293            &self.line8v,
1294            &self.line9,
1295            &self.line10,
1296            &self.line15,
1297            &self.line18,
1298            &self.line21,
1299            &self.line26,
1300        ]
1301    }
1302}
1303
1304/// The Schedule C (Profit or Loss From Business) field map — the crypto trade or business.
1305///
1306/// **Its money column is x ≈ [475, 576]** — not the [504, 576] of Schedules 1/2/3/A and Forms
1307/// 8959/8960/8995, and not Schedule B's [489.6, 576]. No amount-column constant is shared between
1308/// forms in this crate, and none may be.
1309///
1310/// Part II's individual expense lines (8–27b) are unmapped: v1 takes a FLAT expense total, so only
1311/// line 28 is printed. Line 30 (home office) and the line-32 at-risk checkboxes are unmapped too — a
1312/// Schedule C loss refuses upstream, so line 31 is always ≥ 0.
1313#[derive(Debug, Clone, Deserialize)]
1314pub struct ScheduleCMap {
1315    /// `"f1040sc"`.
1316    pub form: String,
1317    /// Tax year.
1318    pub year: i32,
1319    /// Line A — "Principal business or profession".
1320    pub line_a_business: String,
1321    /// Line B — the NAICS code (a 6-character comb).
1322    pub line_b_naics: String,
1323    /// Line F — the accounting-method checkboxes. `(1) Cash` and `(2) Accrual`; `(3) Other` is never
1324    /// checked (v1 captures only the two).
1325    pub method_cash: CheckChoice,
1326    pub method_accrual: CheckChoice,
1327    /// The name + SSN header cells (P6.2). REQUIRED: a full-return schedule that does not name its
1328    /// taxpayer is not a filable form, so a map lacking `[identity]` fails at deserialization.
1329    pub identity: IdentityCells,
1330    /// L1 — gross receipts or sales.
1331    pub line1: MoneyCell,
1332    /// L3 — line 1 − line 2 (returns, blank).
1333    pub line3: MoneyCell,
1334    /// L5 — gross profit (line 3 − line 4, COGS blank).
1335    pub line5: MoneyCell,
1336    /// L7 — gross income (line 5 + line 6, other income blank).
1337    pub line7: MoneyCell,
1338    /// L28 — total expenses.
1339    pub line28: MoneyCell,
1340    /// L29 — tentative profit (line 7 − line 28).
1341    pub line29: MoneyCell,
1342    /// L31 — net profit → Schedule 1 L3 **and** Schedule SE L2.
1343    pub line31: MoneyCell,
1344}
1345
1346impl ScheduleCMap {
1347    /// Parse the committed TOML.
1348    pub fn parse(toml_src: &str) -> Result<Self, toml::de::Error> {
1349        toml::from_str(toml_src)
1350    }
1351    /// The TY2024 map.
1352    pub fn ty2024() -> Self {
1353        Self::parse(SCHEDULE_C_MAP_2024).expect("bundled schedule C 2024 map parses")
1354    }
1355    /// The map for a supported tax year. Full-return v1 is TY2024-only.
1356    pub fn for_year(year: i32) -> Result<Self, FormsError> {
1357        match year {
1358            2024 => Ok(Self::ty2024()),
1359            _ => Err(FormsError::UnsupportedYear(year)),
1360        }
1361    }
1362    /// The 7 filled cells in printed reading order (strictly descending y on page 1).
1363    pub fn lines(&self) -> [&MoneyCell; 7] {
1364        [
1365            &self.line1,
1366            &self.line3,
1367            &self.line5,
1368            &self.line7,
1369            &self.line28,
1370            &self.line29,
1371            &self.line31,
1372        ]
1373    }
1374}
1375
1376/// One listed-payer row on Schedule B: the payer-name text cell + the amount cell.
1377#[derive(Debug, Clone, Deserialize)]
1378pub struct ScheduleBRowMap {
1379    /// The payer-name field (a wide text cell in the PAYER column).
1380    pub payer: String,
1381    /// The amount field.
1382    pub amount: MoneyCell,
1383}
1384
1385/// A Yes/No checkbox pair (Schedule B Part III). Both boxes share the same on-states (`"1"`/`"2"`) and
1386/// the same x geometry across every pair on the form, so only the field NAME distinguishes them.
1387#[derive(Debug, Clone, Deserialize)]
1388pub struct YesNoPair {
1389    /// The "Yes" box.
1390    pub yes: CheckChoice,
1391    /// The "No" box.
1392    pub no: CheckChoice,
1393}
1394
1395/// The Schedule B (Interest and Ordinary Dividends) field map for one tax year.
1396///
1397/// **Its amount column is x ≈ [489.6, 576]** — not the [504, 576] of Schedules 1/2/3/A and Forms
1398/// 8959/8960/8995, nor Schedule C's [475, 576]. A shared constant would reject every cell.
1399///
1400/// **Row 1 of BOTH repeating tables has a different parent subform** (`Line1_ReadOrder` in Part I,
1401/// `ReadOrderControl` in Part II) while its amount sibling does not — so the rows are written out in
1402/// full in the TOML rather than interpolated. **Part I has 14 rows, Part II has 15**; the asymmetry
1403/// is real.
1404#[derive(Debug, Clone, Deserialize)]
1405pub struct ScheduleBMap {
1406    /// `"f1040sb"`.
1407    pub form: String,
1408    /// Tax year.
1409    pub year: i32,
1410    /// L7b — the foreign-country list. It IS a captured input; the claim that v1 had none was false
1411    /// (ARCH-P6.3a Q7 item 7).
1412    pub line7b_countries: String,
1413    /// The name + SSN header cells (P6.2). REQUIRED: a full-return schedule that does not name its
1414    /// taxpayer is not a filable form, so a map lacking `[identity]` fails at deserialization.
1415    pub identity: IdentityCells,
1416    /// Part I line 1 — the 14 interest-payer rows.
1417    pub part1_rows: Vec<ScheduleBRowMap>,
1418    /// L2 — add the amounts on line 1.
1419    pub line2: MoneyCell,
1420    /// L4 — line 2 − line 3 → 1040 L2b.
1421    pub line4: MoneyCell,
1422    /// Part II line 5 — the 15 dividend-payer rows.
1423    pub part2_rows: Vec<ScheduleBRowMap>,
1424    /// L6 — add the amounts on line 5 → 1040 L3b.
1425    pub line6: MoneyCell,
1426    /// L7a — the foreign-account Yes/No pair.
1427    pub line7a: YesNoPair,
1428    /// L8 — the foreign-trust Yes/No pair.
1429    pub line8: YesNoPair,
1430}
1431
1432impl ScheduleBMap {
1433    /// Parse the committed TOML.
1434    pub fn parse(toml_src: &str) -> Result<Self, toml::de::Error> {
1435        toml::from_str(toml_src)
1436    }
1437    /// The TY2024 map.
1438    pub fn ty2024() -> Self {
1439        Self::parse(SCHEDULE_B_MAP_2024).expect("bundled schedule B 2024 map parses")
1440    }
1441    /// The map for a supported tax year. Full-return v1 is TY2024-only.
1442    pub fn for_year(year: i32) -> Result<Self, FormsError> {
1443        match year {
1444            2024 => Ok(Self::ty2024()),
1445            _ => Err(FormsError::UnsupportedYear(year)),
1446        }
1447    }
1448}
1449
1450/// The Schedule SE (Form 1040) field map for one tax year — the filled §1401 line chain.
1451#[derive(Debug, Clone, Deserialize)]
1452pub struct ScheduleSeMap {
1453    /// `"schedule_se"`.
1454    pub form: String,
1455    /// Tax year.
1456    pub year: i32,
1457    /// The identity header — "Name of person **with self-employment income**" + THAT person's SSN, i.e.
1458    /// the PROPRIETOR, not the return's joint name line. `Option` because this map is shared with the
1459    /// crypto slice (whose 2017/2025 editions have no verified identity FQNs and write no identity at
1460    /// all); the FULL-return filler refuses on `None`.
1461    #[serde(default)]
1462    pub identity: Option<IdentityCells>,
1463    /// Line 2 — net profit (net_se), amount column.
1464    pub line2: MoneyCell,
1465    /// Line 3 — combine 1a/1b/2 (= line 2), amount column.
1466    pub line3: MoneyCell,
1467    /// Line 4a — net SE earnings (base = net_se × 92.35%), amount column.
1468    pub line4a: MoneyCell,
1469    /// Line 4c — combine 4a/4b (= line 4a), amount column. The $400 STOP threshold.
1470    pub line4c: MoneyCell,
1471    /// Line 6 — add 4c/5b (= line 4c), amount column.
1472    pub line6: MoneyCell,
1473    /// Line 8a — Form W-2 Social Security wages, **MID column**.
1474    pub line8a: MoneyCell,
1475    /// Line 8d — add 8a/8b/8c (= line 8a), amount column.
1476    pub line8d: MoneyCell,
1477    /// Line 9 — line 7 (`ss_wage_base` constant) − line 8d, amount column.
1478    pub line9: MoneyCell,
1479    /// Line 10 — Social Security portion (`ss`), amount column.
1480    pub line10: MoneyCell,
1481    /// Line 11 — regular Medicare portion (`medicare`), amount column.
1482    pub line11: MoneyCell,
1483    /// Line 12 — SE tax = line 10 + line 11 (**SS + regular Medicare ONLY**), amount column.
1484    pub line12: MoneyCell,
1485    /// Line 13 — one-half SE-tax deduction (= line 12 × 50% = `deductible_half`), **MID column**.
1486    pub line13: MoneyCell,
1487    /// Fields the BLANK form already carries a factory `/V` for (the 2017 §B long form pre-prints
1488    /// line 7 = `127,200`/`00` and line 14 = `5,200`/`00`) — excluded from the `no_unmapped_filled`
1489    /// guard so those constants don't read as stray writes. Empty on 2024/2025.
1490    #[serde(default)]
1491    pub prefilled_exempt: Vec<String>,
1492}
1493
1494impl ScheduleSeMap {
1495    /// Parse the committed TOML.
1496    pub fn parse(toml_src: &str) -> Result<Self, toml::de::Error> {
1497        toml::from_str(toml_src)
1498    }
1499
1500    /// The TY2025 map.
1501    pub fn ty2025() -> Self {
1502        Self::parse(SCHEDULE_SE_MAP_2025).expect("bundled schedule_se 2025 map parses")
1503    }
1504
1505    /// The TY2024 map (field-name-identical to 2025; only the wage base differs).
1506    pub fn ty2024() -> Self {
1507        Self::parse(SCHEDULE_SE_MAP_2024).expect("bundled schedule_se 2024 map parses")
1508    }
1509
1510    /// The TY2017 map (OLD §B long form: dollars+cents pairs; pre-filled line 7/14 exempt).
1511    pub fn ty2017() -> Self {
1512        Self::parse(SCHEDULE_SE_MAP_2017).expect("bundled schedule_se 2017 map parses")
1513    }
1514
1515    /// The map for a supported tax year.
1516    pub fn for_year(year: i32) -> Result<Self, FormsError> {
1517        match year {
1518            2017 => Ok(Self::ty2017()),
1519            2024 => Ok(Self::ty2024()),
1520            2025 => Ok(Self::ty2025()),
1521            _ => Err(FormsError::UnsupportedYear(year)),
1522        }
1523    }
1524
1525    /// The 12 filled line cells, in chain order.
1526    pub fn lines(&self) -> [&MoneyCell; 12] {
1527        [
1528            &self.line2,
1529            &self.line3,
1530            &self.line4a,
1531            &self.line4c,
1532            &self.line6,
1533            &self.line8a,
1534            &self.line8d,
1535            &self.line9,
1536            &self.line10,
1537            &self.line11,
1538            &self.line12,
1539            &self.line13,
1540        ]
1541    }
1542
1543    /// Every field name the map targets (for the `map_YYYY_matches_bundled_pdf_fieldset` guard) —
1544    /// both members of each dollars+cents pair on the 2017 form.
1545    pub fn field_names(&self) -> Vec<&str> {
1546        self.lines().iter().flat_map(|c| c.fields()).collect()
1547    }
1548}