Summary
Two stacked bugs make /finances unusable out of the box:
- The backend throws a 500 on the shipped sample data (and on any real data filled in per the documented schema).
- The frontend can't tell a failed fetch from "still loading," so instead of showing an error it spins on "Loading finances…" forever.
Bug 1 — vendors.yaml/obligations.yaml schema doesn't match what the code parses
Files: LIFEOS/USER/TELOS/FINANCES/{vendors,obligations,schema}.yaml, PULSE/Observability/observability.ts (handleLifeFinances, VendorYaml, ObligationYaml)
Symptom: GET /api/life/finances → 500 {"error":"undefined is not an object (evaluating 'v.id.toLowerCase')"}
Root cause: schema.yaml documents (and the shipped sample templates use) this shape:
vendor: { required: [name, category, direction], properties: [name, purpose, category, direction, match, notes] }
obligation: { required: [vendor, category, amount, frequency], properties: [vendor, category, amount, frequency, due_day, account, autopay, notes] }
But observability.ts parses them against a disjoint shape:
interface VendorYaml {
id: string; name?: string
scope: "business" | "personal" | "mixed"
cadence: "monthly" | "annual" | "quarterly" | "one_time" | "variable"
source: "collector" | "manual" | "stripe" | "webhook"
manual_monthly_usd?: number; manual_annual_usd?: number
...
}
interface ObligationYaml {
id: string; name?: string; scope: "personal"
cadence: ...; amount_usd: number; category: string
...
}
None of id, scope, cadence, source, amount_usd/manual_monthly_usd exist in the documented schema or the shipped template — vendor/amount/frequency do instead. So v.id is undefined for every vendor, and:
const knownLabels = new Set<string>([
...resolvedVendors.map(v => v.name.toLowerCase()),
...resolvedVendors.map(v => v.id.toLowerCase()), // <-- throws
])
This means Finances cannot work for any user who fills in these files per their own documented schema — it's a permanent break between the shipped docs/template and the shipped code, not a missing-data edge case.
Bug 2 — frontend can't distinguish "failed" from "loading"
File: PULSE/Observability/src/app/finances/page.tsx
fetch("/api/life/finances")
.then((r) => (r.ok ? r.json() : null)) // non-ok -> null, same as "not fetched yet"
.then(setData)
.catch((e) => setError(String(e))); // never fires on HTTP errors
data === null is indistinguishable from the pre-fetch state, so the page spins on "Loading finances…" forever instead of showing its own already-built "Failed to load finances" card. growth/page.tsx gets this right via Promise.reject(new Error(...)) — finances/page.tsx doesn't.
Reproduction
- Fresh install, don't run the Finances interview (files still the shipped samples).
- Open Pulse dashboard → Finances tab → stuck on "Loading finances…".
curl localhost:31337/api/life/finances shows the actual 500.
Proposed fix
- Bug 2 (mechanical):
Promise.reject(new Error(\HTTP ${r.status}`))on non-ok, matchinggrowth/page.tsx`.
- Bug 1 (needs a decision): either update
schema.yaml + templates to match the code's id/scope/cadence/amount_usd shape, or update the parser to read vendor/amount/frequency per the documented schema. They currently don't share a single field name. Stopgap patch (derives a slugified id from name/vendor when absent, stops the crash) verified locally, but real dollar amounts still won't populate until the schema is reconciled.
Summary
Two stacked bugs make
/financesunusable out of the box:Bug 1 — vendors.yaml/obligations.yaml schema doesn't match what the code parses
Files:
LIFEOS/USER/TELOS/FINANCES/{vendors,obligations,schema}.yaml,PULSE/Observability/observability.ts(handleLifeFinances,VendorYaml,ObligationYaml)Symptom:
GET /api/life/finances→500 {"error":"undefined is not an object (evaluating 'v.id.toLowerCase')"}Root cause:
schema.yamldocuments (and the shipped sample templates use) this shape:But
observability.tsparses them against a disjoint shape:None of
id,scope,cadence,source,amount_usd/manual_monthly_usdexist in the documented schema or the shipped template —vendor/amount/frequencydo instead. Sov.idisundefinedfor every vendor, and:This means Finances cannot work for any user who fills in these files per their own documented schema — it's a permanent break between the shipped docs/template and the shipped code, not a missing-data edge case.
Bug 2 — frontend can't distinguish "failed" from "loading"
File:
PULSE/Observability/src/app/finances/page.tsxdata === nullis indistinguishable from the pre-fetch state, so the page spins on "Loading finances…" forever instead of showing its own already-built "Failed to load finances" card.growth/page.tsxgets this right viaPromise.reject(new Error(...))—finances/page.tsxdoesn't.Reproduction
curl localhost:31337/api/life/financesshows the actual 500.Proposed fix
Promise.reject(new Error(\HTTP ${r.status}`))on non-ok, matchinggrowth/page.tsx`.schema.yaml+ templates to match the code'sid/scope/cadence/amount_usdshape, or update the parser to readvendor/amount/frequencyper the documented schema. They currently don't share a single field name. Stopgap patch (derives a slugifiedidfromname/vendorwhen absent, stops the crash) verified locally, but real dollar amounts still won't populate until the schema is reconciled.