Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
b28d9cb
fix(f1040): Form 8958 staged projection runs under the return's year
claude Oct 19, 2026
4813af8
fix(f1040): Schedule J source replay takes the return's context
claude Oct 19, 2026
028ba3c
fix(f1040): Form 8990 two-pass and its MeF/PDF reconcile take the ret…
claude Oct 19, 2026
b05ba91
fix(f1040): Form 7203 prior reduced note execution takes the return's…
claude Oct 19, 2026
3490b1e
Merge remote-tracking branch 'origin/main' into ag39-ty26-staged
claude Oct 19, 2026
1dba827
chore: shrink year-pinned baseline
claude Oct 19, 2026
320a293
fix(cli): Form 8990 workpaper reconciles under the return's year
claude Oct 19, 2026
da0d36f
fix(f1040): Form 4972 and Schedule A line 16 reconcile under the retu…
claude Oct 19, 2026
d88ed0b
fix(f1040): Form 982 QPRI threads the return's year and refuses TY2026
claude Oct 19, 2026
c0f4117
fix(f1040): Form 172 workpapers thread the return's year and refuse T…
claude Oct 19, 2026
6160567
fix(f1040): Form 8621 replays take the return's context, no TY2025 de…
claude Oct 19, 2026
75bfb2f
fix(f1040): passive S-corp copies replay Form 8582 and AGI under the …
claude Oct 19, 2026
91073f8
chore: shrink year-pinned baseline
claude Oct 19, 2026
95d8e42
Merge origin/main into ag40-ty26-reconcilers
claude Oct 19, 2026
154d9b1
test(f1040): type the TY2026 participant collection check
claude Oct 19, 2026
82fc12c
Merge origin/main into ag41-ty26-pdf-scope
claude Oct 19, 2026
0de56ae
refactor(pdf): PDF builder and Form 1040/Schedule 3 projections take …
claude Oct 19, 2026
22ccb52
feat(ty2026): PDF export of Form 1040 and Schedules 1-3 on IRS 2026 d…
claude Oct 19, 2026
f301410
fix(ty2026): load the 2025 PDF modules on use so the compiled binary …
claude Oct 19, 2026
dc0f1ec
test(release): smoke a TY2026 draft PDF export from the binary
claude Oct 19, 2026
503b7f7
Merge remote-tracking branch 'origin/main' into ag42-ty26-pdf-schedules
claude Oct 19, 2026
990845a
feat(ty2026): PDF export of Schedules B, C, D, SE and Forms 8863, 888…
claude Oct 19, 2026
6d046b5
feat(ty2026): PDF export of Form 2441 on the IRS 2026 draft
claude Oct 19, 2026
bfa7d7b
docs(ty2026): list the PDF forms printed on 2026 drafts
claude Oct 19, 2026
59af497
chore(structure): shrink the year-pinned baseline
claude Oct 19, 2026
6b0571e
feat(ty2026): PDF export of Schedule E on the IRS 2026 draft
claude Oct 19, 2026
57c6270
feat(ty2026): PDF export of Schedule A on the IRS 2026 draft
claude Oct 19, 2026
74beffb
Merge origin/main into ag43-ty26-pdf-e-a
claude Oct 19, 2026
169eec2
feat(ty2026): PDF export of Schedule 1-A car loan and senior parts on…
claude Oct 19, 2026
01d5731
test(ty2026): render Form 8863 on the IRS 2026 draft
claude Oct 19, 2026
2092acc
chore(structure): shrink the year-pinned baseline
claude Oct 19, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -236,14 +236,15 @@ const fields: ReadonlyArray<PdfFieldEntry> = [
},
];

export const schedule1aPdf: PdfFormDescriptor = {
export function schedule1aPdfFor(taxYear: number): PdfFormDescriptor {
return {
pendingKey: "schedule1a",
pdfUrl: "https://www.irs.gov/pub/irs-prior/f1040s1a--2025.pdf",
projectFields(raw, allPending) {
if (Object.keys(raw).length === 0) return raw;
const input = inputSchema.parse(raw);
// Keep the PDF authorization identical to native XML authorization.
if (!schedule1a.build(input, { pending: allPending })) return {};
if (!schedule1a.build(input, { pending: allPending, taxYear })) return {};
const projected: Record<string, unknown> = {
line1_agi: input.magi,
line2b_form2555_exclusion: input.form2555_line45_exclusion,
Expand All @@ -260,7 +261,7 @@ export const schedule1aPdf: PdfFormDescriptor = {
(input.qualified_trade_business_tips?.length ?? 0) > 0
) {
const lines = calculateQualifiedTipsSchedule1A(
{ taxYear: 2025, formType: "f1040" },
{ taxYear, formType: "f1040" },
input,
);
Object.assign(projected, {
Expand All @@ -281,7 +282,7 @@ export const schedule1aPdf: PdfFormDescriptor = {
}
if ((input.qualified_w2_overtime?.length ?? 0) > 0) {
const lines = calculateW2OvertimeSchedule1A(
{ taxYear: 2025, formType: "f1040" },
{ taxYear, formType: "f1040" },
input,
);
Object.assign(projected, {
Expand All @@ -300,7 +301,7 @@ export const schedule1aPdf: PdfFormDescriptor = {
}
if ((input.vehicle_loans?.length ?? 0) > 0) {
const lines = calculateVehicleInterestSchedule1A(
{ taxYear: 2025, formType: "f1040" },
{ taxYear, formType: "f1040" },
input,
);
const [first, second] = lines.line22_vehicles;
Expand Down Expand Up @@ -331,9 +332,9 @@ export const schedule1aPdf: PdfFormDescriptor = {
});
total += lines.line30_vehicle_interest;
}
if (seniorDeduction({ taxYear: 2025, formType: "f1040" }, input) > 0) {
if (seniorDeduction({ taxYear, formType: "f1040" }, input) > 0) {
const lines = calculateSeniorOnlySchedule1A(
{ taxYear: 2025, formType: "f1040" },
{ taxYear, formType: "f1040" },
input,
);
Object.assign(projected, {
Expand Down Expand Up @@ -362,4 +363,7 @@ export const schedule1aPdf: PdfFormDescriptor = {
pdfField: `${page1}.f1_02[0]`,
},
],
};
};
}

export const schedule1aPdf: PdfFormDescriptor = schedule1aPdfFor(2025);
21 changes: 14 additions & 7 deletions forms/f1040/2025/pdf/forms/deductions/itemized/schedule_a.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { mefTaxYear } from "../../../../mef/support/year-config.ts";
import { assertRetirementStateLocalTaxSource } from "../../../../domains/income/retirement/retirement_state_local_tax_source.ts";
import type {
PdfFieldEntry,
Expand Down Expand Up @@ -222,7 +221,8 @@ function line16Description(
: undefined;
}

export const scheduleAPdf: PdfFormDescriptor = {
export function scheduleAPdfFor(taxYear: number): PdfFormDescriptor {
return {
pendingKey: "schedule_a",
pdfUrl: "https://www.irs.gov/pub/irs-prior/f1040sa--2025.pdf",
filerFields: [
Expand Down Expand Up @@ -322,7 +322,7 @@ export const scheduleAPdf: PdfFormDescriptor = {
all.f1098,
recipients,
Number(input.line_8c_points_no_1098 ?? 0),
mefTaxYear(),
taxYear,
);
}
const hasPriorCarryover = [
Expand All @@ -348,14 +348,18 @@ export const scheduleAPdf: PdfFormDescriptor = {
assertRetirementStateLocalTaxSource(all, input);
const parsed = scheduleAInputSchema.parse(source);
const recomputed = scheduleA.compute(
{ taxYear: 2025, formType: "f1040" },
{ taxYear, formType: "f1040" },
parsed,
);
const standard = recomputed.outputs.find((output) =>
output.nodeType === "standard_deduction"
)?.fields;
// TY2026 Form 1040 line 12e is after the IRC §68 reduction.
const overallLimit = Number(
all?.standard_deduction?.overall_itemized_limit_reduction ?? 0,
);
if (
!standard || standard.itemized_deductions !==
!standard || Number(standard.itemized_deductions) - overallLimit !==
all?.f1040?.line12e_itemized_deductions
) {
throw new Error(
Expand Down Expand Up @@ -405,7 +409,7 @@ export const scheduleAPdf: PdfFormDescriptor = {
const line16Rows = scheduleALine16AllRows(
all,
input.line_16_other_deductions,
{ taxYear: mefTaxYear(), formType: "f1040" },
{ taxYear, formType: "f1040" },
);
const line16 = input.line_16_other_deductions === undefined &&
input.net_qualified_disaster_loss === undefined
Expand Down Expand Up @@ -509,4 +513,7 @@ export const scheduleAPdf: PdfFormDescriptor = {
}
return true;
},
};
};
}

export const scheduleAPdf: PdfFormDescriptor = scheduleAPdfFor(2025);
22 changes: 13 additions & 9 deletions forms/f1040/2025/pdf/forms/income/rental-passthrough/schedule_e.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { mefTaxYear } from "../../../../mef/support/year-config.ts";
import { StandardFonts } from "pdf-lib";
import { currentPropertyPassiveAmounts } from "../../../../../nodes/inputs/income/rental-passthrough/schedule_e/current-property-source.ts";
import type {
Expand Down Expand Up @@ -236,7 +235,8 @@ function retainedScheduleEPages(projected: Record<string, unknown>): number[] {
return partI && partII ? [0, 1] : partII ? [1] : [0];
}

export const scheduleEPdf: PdfFormDescriptor = {
export function scheduleEPdfFor(taxYear: number): PdfFormDescriptor {
return {
pendingKey: "schedule_e",
pdfUrl: "https://www.irs.gov/pub/irs-prior/f1040se--2025.pdf",
pageIndices: retainedScheduleEPages,
Expand All @@ -261,7 +261,7 @@ export const scheduleEPdf: PdfFormDescriptor = {
],
projectFields(raw, allPending) {
const k1Rows = scheduleEK1Part2Rows(allPending, {
taxYear: mefTaxYear(),
taxYear,
formType: "f1040",
});
if (Object.keys(raw).length === 0 && k1Rows.length === 0) return {};
Expand Down Expand Up @@ -297,7 +297,7 @@ export const scheduleEPdf: PdfFormDescriptor = {
input.rental_income !== undefined ||
input.royalty_income !== undefined ||
trustRows.length > 2 ||
!scheduleE.build(input, { pending: allPending })
!scheduleE.build(input, { pending: allPending, taxYear })
) {
throw new Error(
"Schedule E PDF Part II needs up to four sourced K-1 rows on a supported page 2",
Expand Down Expand Up @@ -363,7 +363,7 @@ export const scheduleEPdf: PdfFormDescriptor = {
input.rental_income !== undefined ||
input.royalty_income !== undefined ||
trustRows.length > 2 ||
!scheduleE.build(input, { pending: allPending })
!scheduleE.build(input, { pending: allPending, taxYear })
) {
throw new Error(
"Schedule E PDF trust rows need a sourced Part III return",
Expand Down Expand Up @@ -395,7 +395,7 @@ export const scheduleEPdf: PdfFormDescriptor = {
if (input.schedule_es.length === 0) return partIIFields!;
}
if (input.schedule_es.length === 0 && input.farm_rental_net !== undefined) {
if (!scheduleE.build(input, { pending: allPending })) {
if (!scheduleE.build(input, { pending: allPending, taxYear })) {
throw new Error(
"Schedule E PDF farm rental needs a native Form 4835 join",
);
Expand Down Expand Up @@ -449,6 +449,7 @@ export const scheduleEPdf: PdfFormDescriptor = {
).some(Boolean);
const allowedByActivity = validatePassiveActivityLink(items, {
pending: allPending,
taxYear,
});
// Native Schedule E checks every source row, limitation, and finalized total.
if (items[0].f1099m_royalty_source) {
Expand All @@ -458,7 +459,7 @@ export const scheduleEPdf: PdfFormDescriptor = {
undefined,
allPending,
);
} else if (!scheduleE.build(input, { pending: allPending })) {
} else if (!scheduleE.build(input, { pending: allPending, taxYear })) {
throw new Error("Schedule E PDF needs native Part I properties");
}
const rows = items.map((item, index) => {
Expand Down Expand Up @@ -666,7 +667,7 @@ export const scheduleEPdf: PdfFormDescriptor = {
if (input?.schedule_es[0]?.f1099m_royalty_source) {
if (
!filer || !allPending ||
!scheduleE.build(input, { pending: allPending, filer })
!scheduleE.build(input, { pending: allPending, filer, taxYear })
) {
throw new Error(
"Schedule E PDF linked 1099-MISC royalty needs its primary filer and native property",
Expand All @@ -692,4 +693,7 @@ export const scheduleEPdf: PdfFormDescriptor = {
filer,
);
},
};
};
}

export const scheduleEPdf: PdfFormDescriptor = scheduleEPdfFor(2025);
7 changes: 4 additions & 3 deletions forms/f1040/2026/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,14 @@
normalization with the 2026v1.0 schema. It accepts every input TY2025 accepts;
there is no allowlist.

PDF output (`pdf/builder.ts`) prints Form 1040, Schedules 1, 2, 3, B, C, D and
SE and Forms 2441, 8863, 8889 and 8995 on the IRS 2026 early-release drafts, fetched from `irs.gov/pub/irs-dft/` and pinned by
PDF output (`pdf/builder.ts`) prints Form 1040, Schedules 1, 1-A, 2, 3, A, B, C, D,
E and SE and Forms 2441, 8863, 8889 and 8995 on the IRS 2026 early-release drafts, fetched from `irs.gov/pub/irs-dft/` and pinned by
SHA-256 like the 2025 templates; the IRS cover sheet is dropped and the packet's
PDF Subject and the CLI say it uses draft forms. Each overlay in `pdf/forms/`
reuses the 2025 projection, built for the 2026 tax year, and renames only the
fields the 2026 form moved; new 2026 lines the engine does not model (Form 8995
line 16 minimum deduction, 2025-titled continuation statements) refuse by name.
line 16 minimum deduction, Schedule A lines 17a-17k, Schedule 1-A tip and
overtime rows, 2025-titled continuation statements) refuse by name.
Every other form refuses by name when the return needs it, as do refundable
credits that need Schedule 3-A (Form 1040 lines 27a-30). A redrafted IRS PDF
fails its digest until the mapping is reviewed.
Expand Down
107 changes: 107 additions & 0 deletions forms/f1040/2026/pdf/builder.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { Box12Code } from "../../nodes/inputs/income/wages/w2/index.ts";
import { f1040_2026 } from "../index.ts";
import { TY2026_DRAFT_NOTICE } from "./draft-template.ts";
import { TY2026_PDF_MAPPED_FORMS, ty2026PdfForms } from "./builder.ts";
import { scheduleEPdf2026 } from "./forms/income/rental-passthrough/schedule_e.ts";
import { loadPdfTemplate } from "../../2025/pdf/support/template-cache.ts";

const fixture = (id: string) => {
Expand Down Expand Up @@ -221,6 +222,112 @@ test("TY2026 PDF prints Form 2441 Part I providers as columns", async () => {
assertMatch(printed, /on Schedule 3 \(Form 1040\), line 2[^\n]* 11 500/);
});

test("TY2026 PDF prints Schedule E on the 2026 draft pages", async () => {
const rental = fixture("single-nonparticipating-rental-loss");
const [property] = rental.schedule_e as Array<Record<string, unknown>>;
const trust = fixture("single-trust-k1-box5-schedule-e");
const doc = await render({
...rental,
k1_trust: trust.k1_trust,
schedule_e: [{
...property,
activity_type: "A",
rent_income: 12000,
expense_repairs: 1000,
expense_mortgage_interest: 2000,
expense_other_interest: 300,
}],
});
const printed = await text(doc);
assertMatch(printed, /SCHEDULE E[\s\S]*2026/);
assertMatch(printed, /A 12 Example Street Austin TX 78701/);
assertMatch(printed, /3 Rents received[^\n]* 3 12000/);
assertMatch(printed, /12 Mortgage interest[^\n]* 12 2000/);
assertMatch(printed, /13 Interest \(see instructions\)[^\n]* 13\n/);
assertMatch(printed, /13b Other[^\n]* 13b 300/);
assertMatch(printed, /14 Repairs[^\n]* 14 1000/);
assertMatch(printed, /20 Total expenses[^\n]* 20 3300/);
assertMatch(printed, /23a Total of all amounts reported on line 3[^\n]* 23a 12000/);
assertMatch(printed, /23e Total of all amounts reported on line 20[^\n]* 23e 3300/);
assertMatch(printed, /24 Income\. Add positive amounts[^\n]* 24 8700/);
assertMatch(printed, /26 Total rental real estate[\s\S]*? 26 8700/);
assertMatch(printed, /A Family Trust[^\n]* 123456789/);
assertMatch(printed, /37 Total estate and trust income[^\n]* 37 750/);
assertMatch(printed, /41 Total income or \(loss\)\. Combine[\s\S]*? 41 9450/);
const k1Columns = scheduleEPdf2026.fields
.filter((entry) => entry.domainKey.startsWith("k1_0_"))
.map((entry) => [entry.domainKey, entry.pdfField.split(".").pop()]);
assertEquals(Object.fromEntries(k1Columns), {
k1_0_name: "f2_6[0]",
k1_0_code: "f2_7[0]",
k1_0_ein: "f2_8[0]",
k1_0_passive_loss: "f2_18[0]",
k1_0_passive_income: "f2_19[0]",
k1_0_basis_required: "c2_3[0]",
k1_0_nonpassive_income: "f2_22[0]",
});
});

test("TY2026 PDF prints Schedule A with the 0.5% gift floor and line 18", async () => {
const itemizer = fixture("single-1098-purchase-points");
const schedule_a = {
line_5b_real_estate_tax: 4000,
cash_contributions_to_50_percent_organizations: 10000,
};
const printed = await text(await render({ ...itemizer, schedule_a }));
assertMatch(printed, /SCHEDULE A[\s\S]*2026/);
assertMatch(printed, /line 11b[^\n]* 2 80000/);
assertMatch(printed, /7 Add lines 5e and 6[^\n]* 7 4000/);
assertMatch(printed, /8e Add lines 8a through 8d[^\n]* 8e 20400/);
assertMatch(printed, /10 Add lines 8e and 9[^\n]* 10 20400/);
assertMatch(printed, /see instructions[^\n]* 11 10000/);
assertMatch(printed, /Limitation Worksheet[^\n]* 13 9600/);
assertMatch(printed, /15 Add lines 13 and 14[^\n]* 15 9600/);
assertMatch(printed, /✔ No\. Your deductions are not limited/);
assertMatch(printed, /the amount to enter[^\n]* 18 34000/);
assertMatch(printed, /12e Standard deduction or itemized[^\n]* 12e 34000/);
});

test("TY2026 PDF prints Schedule 1-A car loan and senior parts", async () => {
const car = await text(
await render(fixture("single-reviewed-car-loan-schedule1a")),
);
assertMatch(car, /SCHEDULE 1-A[\s\S]*2026/);
assertMatch(car, /line 11b[^\n]* 1 80000/);
assertMatch(car, /28a 1 H G C M 8 2 6 3 3 A 0 0 4 3 5 2 0 4000/);
assertMatch(car, /does not qualify\)\?[^\n]* ✔ Yes No/);
assertMatch(car, /final assembly\)\?[^\n]* ✔ Yes No/);
assertMatch(car, /29 Add lines 28a and 28b[^\n]* 29 4000/);
assertMatch(car, /32 Enter \$100,000[^\n]* 32 100000/);
assertMatch(car, /enter -0- [. ]* 36 4000/);
assertMatch(car, /44 Add lines 15, 27, 36, and 43[^\n]* 44 4000/);
const senior = await text(await render(fixture("joint-senior-schedule1a")));
assertMatch(senior, /37 Enter the amount from line 3[^\n]* 37 160000/);
assertMatch(senior, /40 Multiply line 39 by 6%[^\n]* 40 600/);
assertMatch(senior, /41 Subtract line 40 from \$6,000[^\n]* 41 5400/);
assertMatch(senior, /43 Enhanced deduction for seniors[^\n]* 43 10800/);
assertMatch(senior, /44 Add lines 15, 27, 36, and 43[^\n]* 44 10800/);
await assertRejects(
() => render(fixture("single-w2-qualified-tips-schedule1a")),
Error,
"Schedule 1-A tips, overtime",
);
});

test("TY2026 PDF prints Form 8863 on the 2026 draft", async () => {
// The fixture's worksheet holds 2025 tax; the 2026 return supplies its own.
const { f8863_credit_limit_worksheet: _, ...inputs } = fixture(
"single-form8863-lifetime-learning-scholarship",
);
const printed = await text(await render(inputs));
assertMatch(printed, /Education Credits[\s\S]*2026/);
assertMatch(printed, /11 Enter the smaller of line 10 or \$10,000[^\n]* 11 7500/);
assertMatch(printed, /12 Multiply line 11 by 20%[^\n]* 12 1500/);
assertMatch(printed, /Schedule 3 \(Form 1040\), line 3[^\n]* 19 1500/);
assertMatch(printed, /\n Test University\n/);
assertMatch(printed, /Part II, line 10[^\n]* 31 7500/);
});

test("TY2026 PDF refuses, by name, a form with no TY2026 mapping", async () => {
await assertRejects(
() => render(fixture("single-child-unearned-income")),
Expand Down
6 changes: 6 additions & 0 deletions forms/f1040/2026/pdf/builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,23 @@ import { form2441Pdf2026 } from "./forms/credits/individual/f2441.ts";
import { scheduleBPdf2026 } from "./forms/income/investments/schedule_b.ts";
import { scheduleDPdf2026 } from "./forms/income/investments/schedule_d.ts";
import { form8889Pdf2026 } from "./forms/adjustments/health/f8889.ts";
import { scheduleEPdf2026 } from "./forms/income/rental-passthrough/schedule_e.ts";
import { scheduleAPdf2026 } from "./forms/deductions/itemized/schedule_a.ts";
import { schedule1aPdf2026 } from "./forms/deductions/additional/schedule1a.ts";
import { form8863Pdf2026 } from "./forms/credits/individual/f8863.ts";

const MAPPED: ReadonlyMap<string, PdfFormDescriptor> = new Map(
[
irs1040Pdf2026,
schedule1Pdf2026,
schedule1aPdf2026,
schedule2Pdf2026,
schedule3Pdf2026,
scheduleAPdf2026,
scheduleBPdf2026,
scheduleCPdf2026,
scheduleDPdf2026,
scheduleEPdf2026,
scheduleSePdf2026,
form2441Pdf2026,
form8863Pdf2026,
Expand Down
Loading
Loading