One hierarchical theme registry as the single source of truth — the CIO's four major macro themes as a fixed spine, specific sub-themes underneath, full book coverage, and a stable back-end↔front-end contract that can't silently drift.
The fund_themes slug world (broker heatmap, positions, theme_world) and the hard-coded 9/10 ALLOC_THEME_META (allocation page) disagree — two catch-alls, one English one Icelandic, mapped in two places. Any rename drifts one side from the other.
One thesis emitted under two slugs → the same broker showed two different numbers for “the same” theme:
| Broker | EUROPE RESURGENCE | ENDURREISN EVRÓPU |
|---|---|---|
| JPM | +0.30 | +0.35 |
| Rabobank | +0.45 | +0.30 |
| UBS | +0.62 | +0.60 |
Fixed in PR #114 (resolve → collapse-per-broker). This spec removes the root: one vocabulary.
Every traded asset is assigned to exactly one major and one sub-theme. Because the four are fixed and exhaustive, coverage is guaranteed by construction — an asset always has a major even before its sub-theme is picked. Sub-themes are the dynamic, client-authorable layer.
Majors display in Icelandic, sub-themes in English (CIO ruling). NEW = sub-themes that close the current holes. Minerals IS term “Málmar” vs “Steinefni” + Layer-5 sign-off pending (rule #12).
Two vocabularies; ~$3M in catch-alls; XOM/CVX/RIG with no real theme; “Oil Majors” doesn’t exist.
unmapped = $0. One registry; every asset = major + sub-theme; catch-all is a monitored zero-target, not a bucket.
Drill into any major → its sub-themes (the cards above). Four rows, always complete — never an empty or “Annað” slice.
Every theme dashboard is served from the perspective of coverage of the four majors. Broker calls roll up (raw → sub-theme → major); the heatmap becomes Brokers × the four majors. Real rollup, latest report date:
| Broker | Gervigreind AI |
Málmar Minerals |
Orkuframleiðsla Power Gen |
Orkuinnviðir Energy Infra |
|---|---|---|---|---|
| ANZ | 0.00 | −0.60 | ||
| Deutsche Bank | +0.63 | +0.65 | ||
| HSBC | +0.13 | 0.00 | ||
| JPM | +0.45 | +0.75 | ||
| Rabobank | −0.63 | −0.63 | ||
| RBC | 0.00 | +0.18 | +0.38 | |
| ST | −0.48 | |||
| UBS | +0.70 | |||
| ZeroHedge | −0.20 | 0.00 |
Four columns always present — a major with no call reads “no grounded call”, never absent. The granular sub-theme heatmap (PR #114) becomes the drilldown. Out-of-thesis broker themes (de-dollarization, Europe, defense) sit in a separate “broader macro signal” strip — never forced into a major (§11 confirm).
slug · name_en · isl_heiti · major · status. Plus resolve_theme_slug — the single mapping point.
v_allocation_by_major · v_allocation_by_theme · per-position theme.
No hard-coded 9/10, no positionTheme() derivation. Language toggle reads name_en / isl_heiti. Add a theme = one row + reload; FE re-renders, zero code change.
The four majors are fixed. Sub-themes are live-authorable: the client converses with Mímir → a proposal row → a human confirm promotes it under its major. No deploy.
One law: agent output is a proposal; a human confirm gates anything that moves NAV%. The coverage alert (INV‑1) is what triggers a proposal.
On approval → writing-plans → Class-C migration (theme_categories + major column + oil themes + rollup views + propose_theme extension), shadow-gated → then the sequenced ViskaFront cutover. Nothing prod-bound until approved and viska-pm pushes.