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