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:
krisf committed 2026-10-08 23:00:18 -04:00
commit e25dd4e0a1
29 files changed
+3143

No files matched your search

+122
View File
@@ -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(/&nbsp;/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(/&nbsp;/g, " ").replace(/&#\d+;/g, " ")
.replace(/&amp;/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]));
+523
View File
@@ -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"
}
}
}
+53
View File
@@ -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)));
+43
View File
@@ -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));
'
+13
View File
@@ -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
1 hero A calm Canadian lakeside at dawn, mist over still water, pine forest, soft pastel sky, wide serene landscape, photorealistic
2 muskoka A cozy wooden cottage on a pine-framed lake at golden hour, Muskoka Ontario, loon, warm window light, photorealistic
3 rockies Turquoise glacial lake with snow-capped Rocky Mountains at sunrise, Banff, photorealistic
4 maritimes White lighthouse on rugged red sandstone cliffs, Atlantic waves, Peggy's Cove style, soft morning light, photorealistic
5 prairies Endless golden wheat field under a huge prairie sky with a dramatic summer sunset, Saskatchewan, photorealistic
6 quebec Colourful historic houses and cobblestone streets of Quebec City old town in autumn, photorealistic
7 vancouver Vancouver skyline with North Shore mountains and Stanley Park seawall at dusk, photorealistic
8 toronto Toronto skyline and Lake Ontario waterfront at blue hour, photorealistic
9 garden A sunny backyard vegetable and flower garden with a small garden shed, Canada, photorealistic
10 travel Aerial view of a seaplane over a northern Canadian lake surrounded by forest, adventure travel, photorealistic
11 workshop A bright woodworking workshop with hand tools on a pegboard wall, warm light, hobby, photorealistic
12 winter A snow-covered log cabin with warm glowing windows in a quiet northern forest, winter evening, photorealistic
13 dock A wooden dock at sunset with two empty chairs overlooking a quiet lake, peaceful, photorealistic