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