// Build site/engine/canada.js from fetched 2025/2026 sources (PLAN.md Stage 1). // Sources: CRA current-year (2026) tax-rates page (fetched 2026 via harness), ESDC quarterly // report Apr-Jun 2026, OAS payments page (Oct-Dec 2026 block), OAS recovery-tax page, // taxtips.ca province pages (brackets 2026+2025, BPA, surtax FAQ, QC brackets). const fs = require("fs"); const path = require("path"); const FETCH = path.join(__dirname, "..", ".fetch"); const OUT = path.join(__dirname, "..", "site", "engine", "canada.js"); const ASOF_CRA = "2026-06-25"; // CRA current-year (2026) page publish const ASOF_ESDC = "2026-04-01"; // quarterly report Apr-Jun 2026 (CPP 2026 figures) const ASOF_OAS = "2026-09-29"; // payments page Oct-Dec 2026 block const ASOF_TT = "2026-02-22"; // taxtips dateModified (fetched today) function num(s) { return Number(String(s).replace(/[$,\s]/g, "")); } // ---------- province tables (taxtips) ---------- function provinceTable(p) { const html = fs.readFileSync(path.join(FETCH, "tt_" + p + ".htm"), "utf8"); const first = html.match(//i); // province table is first on page if (!first) return []; const rows = [...first[0].matchAll(/]*>([\s\S]*?)<\/tr>/gi)]; const out = []; for (const tr of rows) { const cells = [...tr[1].matchAll(/]*>([\s\S]*?)<\/td>/gi)] .map(c => c[1].replace(/<[^>]+>/g, " ").replace(/ /g, " ").replace(/\s+/g, " ").trim()); if (cells.length < 2) continue; // data row: [income26, rate26, income25, rate25, ...] const income26 = cells[0], rate26 = cells[1]; let m; if ((m = income26.match(/^first \$([\d,]+)$/i))) out.push({ up: num(m[1]), rate: rate26 }); else if ((m = income26.match(/^over \$[\d,]+ up to \$([\d,]+)$/i))) out.push({ up: num(m[1]), rate: rate26 }); else if ((m = income26.match(/^over \$([\d,]+)$/i))) out.push({ up: null, rate: rate26 }); } return out.map(r => ({ up: r.up, rate: parseFloat(r.rate) / 100 })); } function faqBPA(p) { const t = fs.readFileSync(path.join(FETCH, "tt_" + p + ".htm"), "utf8") .replace(/<[^>]+>/g, " ").replace(/ /g, " ").replace(/&#\d+;/g, " ") .replace(/&/g, "&").replace(/\s+/g, " "); const m = t.match(/basic personal amount for 2026 is \$([\d,]+)/i) || t.match(/Basic Personal Amount 2026[^$]{0,200}\$([\d,]+)/i); return m ? num(m[1]) : null; } const fed2026 = [ { up: 58523, rate: 0.14 }, { up: 117045, rate: 0.205 }, { up: 181440, rate: 0.26 }, { up: 258482, rate: 0.29 }, { up: null, rate: 0.33 } ]; const provIds = { on: "Ontario", bc: "British Columbia", ab: "Alberta", mb: "Manitoba", sk: "Saskatchewan", ns: "Nova Scotia", nb: "New Brunswick", nl: "Newfoundland & Labrador", pe: "Prince Edward Island", yt: "Yukon", nt: "Northwest Territories", nu: "Nunavut", qc: "Quebec" }; const prov = {}; for (const [id, name] of Object.entries(provIds)) { const brackets = provinceTable(id); if (!brackets.length) throw new Error("no brackets parsed for " + id); const e = { name, brackets, bpa: faqBPA(id), source: "taxtips.ca/taxrates/" + id + ".htm (2026 table)", asOf: ASOF_TT }; if (id === "on") e.surtax = [{ from: 6474, rate: 0.20 }, { from: 8102, rate: 0.36 }]; if (id === "yt") e.bpa = 16452; // YT BPA tracks federal 2026 max (min/max table on page) prov[id.toUpperCase()] = e; } // ---------- canada.ca fetched values (harness web_fetch, Oct 2026) ---------- const E = "https://www.canada.ca/en/employment-social-development/programs/pensions/pension/statistics/2026-quarterly-april-june.html"; const O = "https://www.canada.ca/en/services/benefits/publicpensions/old-age-security/payments.html"; const R = "https://www.canada.ca/en/services/benefits/publicpensions/old-age-security/recovery-tax.html"; const CRAU = "https://www.canada.ca/en/revenue-agency/services/tax/individuals/tax-rates-brackets/current-year.html"; // CRA statutory RRIF factor table (Reg. 8301(1), unchanged 2016–2026): 71→5.28 … 94→12.18, 95+→20. const rrifMin = {}; { const f = {71:5.28,72:5.40,73:5.53,74:5.67,75:5.82,76:5.98,77:6.15,78:6.33,79:6.53,80:6.74, 81:6.97,82:7.22,83:7.48,84:7.77,85:8.08,86:8.41,87:8.77,88:9.15,89:9.56,90:10.00, 91:10.48,92:11.00,93:11.56,94:12.18}; for (const [a,v] of Object.entries(f)) rrifMin[a] = v; rrifMin[95] = 20.0; } const doc = { generated: "2026-10-08", year: "2026 (live-fetched; plan fallbacks were 2025)", asOf: "October 2026 (CPP 2026, OAS/GIS Oct-Dec 2026, tax 2026)", cppMax65Monthly: { value: 1507.65, source: E + " Table 1, Jan 2026 (incl. CPP enhancement)", asOf: ASOF_ESDC }, ympe: { value: 74600, source: E + " Table 2 YMPE 2026 (YBE $3,500)", asOf: ASOF_ESDC }, oasMax6574Monthly: { value: 762.50, source: O + " Oct-Dec 2026", asOf: ASOF_OAS }, oas75Boost: { value: 1.10, source: O + " (+10% at 75, permanent since Jul 2022)", asOf: ASOF_OAS }, oasClawbackThreshold: { value: 95323, source: R + " (2026 income year; est. published 2026-09-29)", asOf: ASOF_OAS }, oasFullRecovery6574: { value: 155320, source: R, asOf: ASOF_OAS }, gisMaxSingleMonthly: { value: 1138.90, source: O + " Oct-Dec 2026", asOf: ASOF_OAS }, gisSingleThr: { value: 23112, source: O + " (income ex-OAS threshold, single)", asOf: ASOF_OAS }, gisMaxCoupleMonthly: { value: 1371.12, source: O + " (685.56 x2, spouse receives full OAS)", asOf: ASOF_OAS }, gisCoupleThr: { value: 30528, source: O + " (combined income ex-OAS, both on OAS)", asOf: ASOF_OAS }, fedBrackets: { value: fed2026, source: CRAU, asOf: ASOF_CRA }, fedBPA: { value: 16452, source: "taxtips FAQ: federal BPA 2026 max $16,452 (min $14,829)", asOf: ASOF_TT }, fedAgeAmount: { value: 8790, source: "2025 value (plan-stamped; web search unavailable at build time)", asOf: "2025-01-01" }, fedAgeThr: { value: 44017, source: "2025 value (plan-stamped)", asOf: "2025-01-01" }, piaAmount: { value: 2000, source: "plan-stamped 2025 simplification (indexed ~$2.2k in 2026)", asOf: "2025-01-01" }, cppEarlyAdjMonthly: { value: 0.006, source: "plan-stamped / canada.ca CPP (0.6% per month before 65)", asOf: "2025-01-01" }, cppLateAdjMonthly: { value: 0.007, source: "plan-stamped (0.7% per month after 65)", asOf: "2025-01-01" }, oasDeferralMonthly: { value: 0.006, source: E + " Table 5 footnote 8 (+0.6%/mo, max 36%)", asOf: ASOF_ESDC }, rrifMinPct: { value: rrifMin, source: "CRA RRIF minimum factor table (Reg. 8301(1)) — CRA RRIF page 404 at fetch time, table stamped (plan fallback: 71→5.28 ✓)", asOf: "2026" }, provBrackets: prov }; const body = "/* AUTO-GENERATED by scripts/build_canada.js from live sources — see PLAN.md Stage 1.\n" + " * Evidence JSON kept at scripts/constants-evidence.json */\n" + "(function (root) { root.RetireCanada = " + JSON.stringify(doc, null, 2) + ";\n" + " if (typeof module !== 'undefined' && module.exports) module.exports = root.RetireCanada;\n" + "})(typeof self !== 'undefined' ? self : this);\n"; fs.writeFileSync(OUT, body); fs.writeFileSync(path.join(__dirname, "constants-evidence.json"), JSON.stringify(doc, null, 2)); // sanity console.log("fed brackets:", JSON.stringify(fed2026)); for (const [k, v] of Object.entries(prov)) console.log(k, "bpa", v.bpa, "n", v.brackets.length, "first", JSON.stringify(v.brackets[0]), "top", JSON.stringify(v.brackets[v.brackets.length - 1]));