Retire Planner v1: engine (live 2026 CPP/OAS/GIS/RRIF/tax), 17-step UI, 4 SVG charts, vision gallery, tests
This commit is contained in:
commit
e25dd4e0a1
29 files changed
+3143
No files matched your search
@@ -0,0 +1,122 @@
|
||||
// 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(/<table[\s\S]*?<\/table>/i); // province table is first on page
|
||||
if (!first) return [];
|
||||
const rows = [...first[0].matchAll(/<tr[^>]*>([\s\S]*?)<\/tr>/gi)];
|
||||
const out = [];
|
||||
for (const tr of rows) {
|
||||
const cells = [...tr[1].matchAll(/<td[^>]*>([\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]));
|
||||
@@ -0,0 +1,523 @@
|
||||
{
|
||||
"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": "https://www.canada.ca/en/employment-social-development/programs/pensions/pension/statistics/2026-quarterly-april-june.html Table 1, Jan 2026 (incl. CPP enhancement)",
|
||||
"asOf": "2026-04-01"
|
||||
},
|
||||
"ympe": {
|
||||
"value": 74600,
|
||||
"source": "https://www.canada.ca/en/employment-social-development/programs/pensions/pension/statistics/2026-quarterly-april-june.html Table 2 YMPE 2026 (YBE $3,500)",
|
||||
"asOf": "2026-04-01"
|
||||
},
|
||||
"oasMax6574Monthly": {
|
||||
"value": 762.5,
|
||||
"source": "https://www.canada.ca/en/services/benefits/publicpensions/old-age-security/payments.html Oct-Dec 2026",
|
||||
"asOf": "2026-09-29"
|
||||
},
|
||||
"oas75Boost": {
|
||||
"value": 1.1,
|
||||
"source": "https://www.canada.ca/en/services/benefits/publicpensions/old-age-security/payments.html (+10% at 75, permanent since Jul 2022)",
|
||||
"asOf": "2026-09-29"
|
||||
},
|
||||
"oasClawbackThreshold": {
|
||||
"value": 95323,
|
||||
"source": "https://www.canada.ca/en/services/benefits/publicpensions/old-age-security/recovery-tax.html (2026 income year; est. published 2026-09-29)",
|
||||
"asOf": "2026-09-29"
|
||||
},
|
||||
"oasFullRecovery6574": {
|
||||
"value": 155320,
|
||||
"source": "https://www.canada.ca/en/services/benefits/publicpensions/old-age-security/recovery-tax.html",
|
||||
"asOf": "2026-09-29"
|
||||
},
|
||||
"gisMaxSingleMonthly": {
|
||||
"value": 1138.9,
|
||||
"source": "https://www.canada.ca/en/services/benefits/publicpensions/old-age-security/payments.html Oct-Dec 2026",
|
||||
"asOf": "2026-09-29"
|
||||
},
|
||||
"gisSingleThr": {
|
||||
"value": 23112,
|
||||
"source": "https://www.canada.ca/en/services/benefits/publicpensions/old-age-security/payments.html (income ex-OAS threshold, single)",
|
||||
"asOf": "2026-09-29"
|
||||
},
|
||||
"gisMaxCoupleMonthly": {
|
||||
"value": 1371.12,
|
||||
"source": "https://www.canada.ca/en/services/benefits/publicpensions/old-age-security/payments.html (685.56 x2, spouse receives full OAS)",
|
||||
"asOf": "2026-09-29"
|
||||
},
|
||||
"gisCoupleThr": {
|
||||
"value": 30528,
|
||||
"source": "https://www.canada.ca/en/services/benefits/publicpensions/old-age-security/payments.html (combined income ex-OAS, both on OAS)",
|
||||
"asOf": "2026-09-29"
|
||||
},
|
||||
"fedBrackets": {
|
||||
"value": [
|
||||
{
|
||||
"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
|
||||
}
|
||||
],
|
||||
"source": "https://www.canada.ca/en/revenue-agency/services/tax/individuals/tax-rates-brackets/current-year.html",
|
||||
"asOf": "2026-06-25"
|
||||
},
|
||||
"fedBPA": {
|
||||
"value": 16452,
|
||||
"source": "taxtips FAQ: federal BPA 2026 max $16,452 (min $14,829)",
|
||||
"asOf": "2026-02-22"
|
||||
},
|
||||
"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": "https://www.canada.ca/en/employment-social-development/programs/pensions/pension/statistics/2026-quarterly-april-june.html Table 5 footnote 8 (+0.6%/mo, max 36%)",
|
||||
"asOf": "2026-04-01"
|
||||
},
|
||||
"rrifMinPct": {
|
||||
"value": {
|
||||
"71": 5.28,
|
||||
"72": 5.4,
|
||||
"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,
|
||||
"91": 10.48,
|
||||
"92": 11,
|
||||
"93": 11.56,
|
||||
"94": 12.18,
|
||||
"95": 20
|
||||
},
|
||||
"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": {
|
||||
"ON": {
|
||||
"name": "Ontario",
|
||||
"brackets": [
|
||||
{
|
||||
"up": 53891,
|
||||
"rate": 0.050499999999999996
|
||||
},
|
||||
{
|
||||
"up": 107785,
|
||||
"rate": 0.0915
|
||||
},
|
||||
{
|
||||
"up": 150000,
|
||||
"rate": 0.1116
|
||||
},
|
||||
{
|
||||
"up": 220000,
|
||||
"rate": 0.1216
|
||||
},
|
||||
{
|
||||
"up": null,
|
||||
"rate": 0.1316
|
||||
}
|
||||
],
|
||||
"bpa": 12989,
|
||||
"source": "taxtips.ca/taxrates/on.htm (2026 table)",
|
||||
"asOf": "2026-02-22",
|
||||
"surtax": [
|
||||
{
|
||||
"from": 6474,
|
||||
"rate": 0.2
|
||||
},
|
||||
{
|
||||
"from": 8102,
|
||||
"rate": 0.36
|
||||
}
|
||||
]
|
||||
},
|
||||
"BC": {
|
||||
"name": "British Columbia",
|
||||
"brackets": [
|
||||
{
|
||||
"up": 50363,
|
||||
"rate": 0.055999999999999994
|
||||
},
|
||||
{
|
||||
"up": 100728,
|
||||
"rate": 0.077
|
||||
},
|
||||
{
|
||||
"up": 115648,
|
||||
"rate": 0.105
|
||||
},
|
||||
{
|
||||
"up": 140430,
|
||||
"rate": 0.1229
|
||||
},
|
||||
{
|
||||
"up": 190405,
|
||||
"rate": 0.147
|
||||
},
|
||||
{
|
||||
"up": 265545,
|
||||
"rate": 0.168
|
||||
},
|
||||
{
|
||||
"up": null,
|
||||
"rate": 0.205
|
||||
}
|
||||
],
|
||||
"bpa": 13216,
|
||||
"source": "taxtips.ca/taxrates/bc.htm (2026 table)",
|
||||
"asOf": "2026-02-22"
|
||||
},
|
||||
"AB": {
|
||||
"name": "Alberta",
|
||||
"brackets": [
|
||||
{
|
||||
"up": 61200,
|
||||
"rate": 0.08
|
||||
},
|
||||
{
|
||||
"up": 154259,
|
||||
"rate": 0.1
|
||||
},
|
||||
{
|
||||
"up": 185111,
|
||||
"rate": 0.12
|
||||
},
|
||||
{
|
||||
"up": 246813,
|
||||
"rate": 0.13
|
||||
},
|
||||
{
|
||||
"up": 370220,
|
||||
"rate": 0.14
|
||||
},
|
||||
{
|
||||
"up": null,
|
||||
"rate": 0.15
|
||||
}
|
||||
],
|
||||
"bpa": 22769,
|
||||
"source": "taxtips.ca/taxrates/ab.htm (2026 table)",
|
||||
"asOf": "2026-02-22"
|
||||
},
|
||||
"MB": {
|
||||
"name": "Manitoba",
|
||||
"brackets": [
|
||||
{
|
||||
"up": 47000,
|
||||
"rate": 0.10800000000000001
|
||||
},
|
||||
{
|
||||
"up": 100000,
|
||||
"rate": 0.1275
|
||||
},
|
||||
{
|
||||
"up": null,
|
||||
"rate": 0.174
|
||||
}
|
||||
],
|
||||
"bpa": 15780,
|
||||
"source": "taxtips.ca/taxrates/mb.htm (2026 table)",
|
||||
"asOf": "2026-02-22"
|
||||
},
|
||||
"SK": {
|
||||
"name": "Saskatchewan",
|
||||
"brackets": [
|
||||
{
|
||||
"up": 54532,
|
||||
"rate": 0.105
|
||||
},
|
||||
{
|
||||
"up": 155805,
|
||||
"rate": 0.125
|
||||
},
|
||||
{
|
||||
"up": null,
|
||||
"rate": 0.145
|
||||
}
|
||||
],
|
||||
"bpa": 20381,
|
||||
"source": "taxtips.ca/taxrates/sk.htm (2026 table)",
|
||||
"asOf": "2026-02-22"
|
||||
},
|
||||
"NS": {
|
||||
"name": "Nova Scotia",
|
||||
"brackets": [
|
||||
{
|
||||
"up": 30995,
|
||||
"rate": 0.08789999999999999
|
||||
},
|
||||
{
|
||||
"up": 61991,
|
||||
"rate": 0.1495
|
||||
},
|
||||
{
|
||||
"up": 97417,
|
||||
"rate": 0.16670000000000001
|
||||
},
|
||||
{
|
||||
"up": 157124,
|
||||
"rate": 0.175
|
||||
},
|
||||
{
|
||||
"up": null,
|
||||
"rate": 0.21
|
||||
}
|
||||
],
|
||||
"bpa": 11932,
|
||||
"source": "taxtips.ca/taxrates/ns.htm (2026 table)",
|
||||
"asOf": "2026-02-22"
|
||||
},
|
||||
"NB": {
|
||||
"name": "New Brunswick",
|
||||
"brackets": [
|
||||
{
|
||||
"up": 52333,
|
||||
"rate": 0.094
|
||||
},
|
||||
{
|
||||
"up": 104666,
|
||||
"rate": 0.14
|
||||
},
|
||||
{
|
||||
"up": 193861,
|
||||
"rate": 0.16
|
||||
},
|
||||
{
|
||||
"up": null,
|
||||
"rate": 0.195
|
||||
}
|
||||
],
|
||||
"bpa": 13664,
|
||||
"source": "taxtips.ca/taxrates/nb.htm (2026 table)",
|
||||
"asOf": "2026-02-22"
|
||||
},
|
||||
"NL": {
|
||||
"name": "Newfoundland & Labrador",
|
||||
"brackets": [
|
||||
{
|
||||
"up": 44678,
|
||||
"rate": 0.087
|
||||
},
|
||||
{
|
||||
"up": 89354,
|
||||
"rate": 0.145
|
||||
},
|
||||
{
|
||||
"up": 159528,
|
||||
"rate": 0.158
|
||||
},
|
||||
{
|
||||
"up": 223340,
|
||||
"rate": 0.17800000000000002
|
||||
},
|
||||
{
|
||||
"up": 285319,
|
||||
"rate": 0.198
|
||||
},
|
||||
{
|
||||
"up": 570638,
|
||||
"rate": 0.20800000000000002
|
||||
},
|
||||
{
|
||||
"up": 1141275,
|
||||
"rate": 0.213
|
||||
},
|
||||
{
|
||||
"up": null,
|
||||
"rate": 0.218
|
||||
}
|
||||
],
|
||||
"bpa": 13094,
|
||||
"source": "taxtips.ca/taxrates/nl.htm (2026 table)",
|
||||
"asOf": "2026-02-22"
|
||||
},
|
||||
"PE": {
|
||||
"name": "Prince Edward Island",
|
||||
"brackets": [
|
||||
{
|
||||
"up": 33928,
|
||||
"rate": 0.095
|
||||
},
|
||||
{
|
||||
"up": 65820,
|
||||
"rate": 0.13470000000000001
|
||||
},
|
||||
{
|
||||
"up": 106890,
|
||||
"rate": 0.166
|
||||
},
|
||||
{
|
||||
"up": 142250,
|
||||
"rate": 0.17620000000000002
|
||||
},
|
||||
{
|
||||
"up": 200000,
|
||||
"rate": 0.19
|
||||
},
|
||||
{
|
||||
"up": null,
|
||||
"rate": 0.2
|
||||
}
|
||||
],
|
||||
"bpa": 15000,
|
||||
"source": "taxtips.ca/taxrates/pe.htm (2026 table)",
|
||||
"asOf": "2026-02-22"
|
||||
},
|
||||
"YT": {
|
||||
"name": "Yukon",
|
||||
"brackets": [
|
||||
{
|
||||
"up": 58523,
|
||||
"rate": 0.064
|
||||
},
|
||||
{
|
||||
"up": 117045,
|
||||
"rate": 0.09
|
||||
},
|
||||
{
|
||||
"up": 181440,
|
||||
"rate": 0.109
|
||||
},
|
||||
{
|
||||
"up": 258482,
|
||||
"rate": 0.1293
|
||||
},
|
||||
{
|
||||
"up": 500000,
|
||||
"rate": 0.128
|
||||
},
|
||||
{
|
||||
"up": null,
|
||||
"rate": 0.15
|
||||
}
|
||||
],
|
||||
"bpa": 16452,
|
||||
"source": "taxtips.ca/taxrates/yt.htm (2026 table)",
|
||||
"asOf": "2026-02-22"
|
||||
},
|
||||
"NT": {
|
||||
"name": "Northwest Territories",
|
||||
"brackets": [
|
||||
{
|
||||
"up": 53003,
|
||||
"rate": 0.059000000000000004
|
||||
},
|
||||
{
|
||||
"up": 106009,
|
||||
"rate": 0.086
|
||||
},
|
||||
{
|
||||
"up": 172346,
|
||||
"rate": 0.122
|
||||
},
|
||||
{
|
||||
"up": null,
|
||||
"rate": 0.1405
|
||||
}
|
||||
],
|
||||
"bpa": 18198,
|
||||
"source": "taxtips.ca/taxrates/nt.htm (2026 table)",
|
||||
"asOf": "2026-02-22"
|
||||
},
|
||||
"NU": {
|
||||
"name": "Nunavut",
|
||||
"brackets": [
|
||||
{
|
||||
"up": 55801,
|
||||
"rate": 0.04
|
||||
},
|
||||
{
|
||||
"up": 111602,
|
||||
"rate": 0.07
|
||||
},
|
||||
{
|
||||
"up": 181439,
|
||||
"rate": 0.09
|
||||
},
|
||||
{
|
||||
"up": null,
|
||||
"rate": 0.115
|
||||
}
|
||||
],
|
||||
"bpa": 19659,
|
||||
"source": "taxtips.ca/taxrates/nu.htm (2026 table)",
|
||||
"asOf": "2026-02-22"
|
||||
},
|
||||
"QC": {
|
||||
"name": "Quebec",
|
||||
"brackets": [
|
||||
{
|
||||
"up": 54345,
|
||||
"rate": 0.14
|
||||
},
|
||||
{
|
||||
"up": 108680,
|
||||
"rate": 0.19
|
||||
},
|
||||
{
|
||||
"up": 132245,
|
||||
"rate": 0.24
|
||||
},
|
||||
{
|
||||
"up": null,
|
||||
"rate": 0.2575
|
||||
}
|
||||
],
|
||||
"bpa": 18952,
|
||||
"source": "taxtips.ca/taxrates/qc.htm (2026 table)",
|
||||
"asOf": "2026-02-22"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
// Generate one vision image via nano-gpt Image API. Usage: KEY env var set, node gen1.js <slug> "<prompt>"
|
||||
// Prints one JSON line: {slug, ok, cost, bal, bytes, type, err}
|
||||
const [slug, prompt] = process.argv.slice(2);
|
||||
const KEY = process.env.RETIRE_NANO_KEY;
|
||||
async function main() {
|
||||
const out = { slug, ok: false };
|
||||
try {
|
||||
const res = await fetch("https://nano-gpt.com/api/v1/images/generations", {
|
||||
method: "POST",
|
||||
headers: { "Authorization": "Bearer " + KEY, "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ model: "z-image-turbo", prompt, resolution: "1024*1024", n: 1 }),
|
||||
signal: AbortSignal.timeout(180000),
|
||||
});
|
||||
const j = await res.json().catch(() => ({}));
|
||||
out.cost = j.cost ?? (j.data && j.data[0] && j.data[0].cost) ?? null;
|
||||
out.bal = j.remainingBalance ?? null;
|
||||
if (!res.ok) { out.err = "http " + res.status + " " + JSON.stringify(j).slice(0, 300); return out; }
|
||||
const d = j.data && j.data[0];
|
||||
if (!d) { out.err = "no data: " + JSON.stringify(j).slice(0, 300); return out; }
|
||||
// dump raw response shape (strip image payloads) for evidence
|
||||
try {
|
||||
const fs0 = require("fs");
|
||||
const copy = JSON.parse(JSON.stringify(j));
|
||||
if (copy.data) copy.data = copy.data.map(x => ({ ...x, b64_json: x.b64_json ? "<" + x.b64_json.length + "b>" : undefined, url: x.url ? x.url.slice(0, 80) : undefined }));
|
||||
fs0.writeFileSync(".fetch/resp_" + slug + ".json", JSON.stringify(copy).slice(0, 4000));
|
||||
} catch (e) {}
|
||||
let buf;
|
||||
if (d.b64_json) {
|
||||
buf = Buffer.from(d.b64_json, "base64");
|
||||
} else if (d.url) {
|
||||
const img = await fetch(d.url, { signal: AbortSignal.timeout(120000) });
|
||||
if (!img.ok) { out.err = "img fetch http " + img.status; return out; }
|
||||
buf = Buffer.from(await img.arrayBuffer());
|
||||
} else { out.err = "no image in response"; return out; }
|
||||
// magic bytes
|
||||
let type = "bin";
|
||||
if (buf.length > 12 && buf[0] === 0x89 && buf[1] === 0x50) type = "png";
|
||||
else if (buf.slice(0, 4).toString("latin1") === "RIFF" && buf.slice(8, 12).toString("latin1") === "WEBP") type = "webp";
|
||||
else if (buf[0] === 0xff && buf[1] === 0xd8) type = "jpeg";
|
||||
out.type = type;
|
||||
out.bytes = buf.length;
|
||||
const fs = require("fs");
|
||||
fs.mkdirSync("site/vision", { recursive: true });
|
||||
// browsers sniff magic bytes; keep .png extension for the UI contract, save true extension alongside
|
||||
fs.writeFileSync("site/vision/" + slug + "." + (type === "png" ? "png" : type), buf);
|
||||
if (type !== "png") fs.copyFileSync("site/vision/" + slug + "." + type, "site/vision/" + slug + ".png");
|
||||
out.ok = true;
|
||||
} catch (e) {
|
||||
out.err = String(e).slice(0, 300);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
main().then(o => console.log(JSON.stringify(o)));
|
||||
@@ -0,0 +1,43 @@
|
||||
#!/usr/bin/env bash
|
||||
# Stage 4 driver: generate 13 vision images with z-image-turbo ($0.0119/img).
|
||||
# Aborts if remainingBalance < $1.00 or cumulative cost > $2.00.
|
||||
set -u
|
||||
cd "$(dirname "$0")/.."
|
||||
mkdir -p site/vision .fetch
|
||||
export RETIRE_NANO_KEY="$(grep -m1 '^HERMES_CUSTOM_NANO_GPT_API_KEY=' ~/.hermes/.env | cut -d= -f2-)"
|
||||
[ -n "$RETIRE_NANO_KEY" ] || { echo "NO KEY"; exit 3; }
|
||||
TOTAL=0
|
||||
LOG=.fetch/vision_log.jsonl
|
||||
: > "$LOG"
|
||||
while IFS=$'\t' read -r slug prompt; do
|
||||
[ -n "$slug" ] || continue
|
||||
if [ -s "site/vision/$slug.png" ] && [ "${FORCE:-0}" != 1 ]; then
|
||||
echo "SKIP $slug (exists)"; continue
|
||||
fi
|
||||
line=$(node scripts/gen1.js "$slug" "$prompt" 2>&1 | tail -1)
|
||||
echo "RES $line"
|
||||
echo "$line" >> "$LOG"
|
||||
read -r ok cost bal <<<"$(node -e "const o=JSON.parse(process.argv[1]);console.log(o.ok?1:0,(o.cost??0),(o.bal??-1))" "$line")"
|
||||
if [ "$ok" != "1" ]; then
|
||||
echo "GEN FAIL $slug — continuing"
|
||||
sleep 3
|
||||
continue
|
||||
fi
|
||||
TOTAL=$(node -e "console.log((parseFloat(process.argv[1])+parseFloat(process.argv[2])).toFixed(4))" "$TOTAL" "$cost")
|
||||
echo "OK $slug cost=$cost cumulative=$TOTAL bal=$bal"
|
||||
awk -v a="$TOTAL" -v b="2.00" 'BEGIN{exit (a+0 > b+0)?0:1}' && { echo "ABORT: cumulative cost $TOTAL > \$2"; break; }
|
||||
awk -v b="$bal" 'BEGIN{exit (b+0 >= 0 && b+0 < 1.0)?0:1}' && { echo "ABORT: remainingBalance $bal < \$1"; break; }
|
||||
sleep 1
|
||||
done < scripts/prompts.tsv
|
||||
echo "DONE cumulative=$TOTAL"
|
||||
ls -la site/vision/
|
||||
node -e '
|
||||
const fs=require("fs");
|
||||
const m={};
|
||||
for (const f of fs.readdirSync("site/vision")) {
|
||||
const mt = f.match(/^(.+)\.(png|webp|jpeg)$/); if (!mt) continue;
|
||||
m[mt[1]] = f;
|
||||
}
|
||||
fs.writeFileSync("site/vision/manifest.json", JSON.stringify(m, null, 2));
|
||||
console.log("manifest:", JSON.stringify(m));
|
||||
'
|
||||
@@ -0,0 +1,13 @@
|
||||
hero A calm Canadian lakeside at dawn, mist over still water, pine forest, soft pastel sky, wide serene landscape, photorealistic
|
||||
muskoka A cozy wooden cottage on a pine-framed lake at golden hour, Muskoka Ontario, loon, warm window light, photorealistic
|
||||
rockies Turquoise glacial lake with snow-capped Rocky Mountains at sunrise, Banff, photorealistic
|
||||
maritimes White lighthouse on rugged red sandstone cliffs, Atlantic waves, Peggy's Cove style, soft morning light, photorealistic
|
||||
prairies Endless golden wheat field under a huge prairie sky with a dramatic summer sunset, Saskatchewan, photorealistic
|
||||
quebec Colourful historic houses and cobblestone streets of Quebec City old town in autumn, photorealistic
|
||||
vancouver Vancouver skyline with North Shore mountains and Stanley Park seawall at dusk, photorealistic
|
||||
toronto Toronto skyline and Lake Ontario waterfront at blue hour, photorealistic
|
||||
garden A sunny backyard vegetable and flower garden with a small garden shed, Canada, photorealistic
|
||||
travel Aerial view of a seaplane over a northern Canadian lake surrounded by forest, adventure travel, photorealistic
|
||||
workshop A bright woodworking workshop with hand tools on a pegboard wall, warm light, hobby, photorealistic
|
||||
winter A snow-covered log cabin with warm glowing windows in a quiet northern forest, winter evening, photorealistic
|
||||
dock A wooden dock at sunset with two empty chairs overlooking a quiet lake, peaceful, photorealistic
|
||||
|
Reference in new issue
Block a user